@shapio/local 0.5.0 → 0.5.2
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 +9 -2
- package/dist/index.js +115 -37
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["SHAPIO_VERSION","packageJson.version","ShapioVersionSkewError","DeliveryRuntimeError","EMPTY_MASK","DATA","fieldJson","fieldText","fieldValue","castParameter","DATA","ARRAY_TYPES","fieldText","fieldValue","castParameter","castParameter","isRecord","NUMERIC_STRING_TYPES","sourceOf","mediaVariantsRepository.listForAssets","isRecord","contentQueriesRepository.runHeadQuery","mediaAssetsRepository.findLiveManyOnSite","isTree","siteNotFound","sitesRepository.findSeoDefaults","mediaAssetsRepository.findLiveManyOnSite","contentQueriesRepository.runHeadPage","contentQueriesRepository.withConsistentRead","contentQueriesRepository.runHeadPageMeta","contentRevisionsRepository.findDataByIds","contentQueriesRepository.withConsistentRead","publicationsRepository.currentSeq","snapshotDiffRepository.listChanges","snapshotDiffRepository.schemaVersionsAt","snapshotDiffRepository.snapshotCreatedAt","schemaVersionsRepository.getSchemaVersion","PERMISSION_COLUMNS","listAllPermissions","apiTokensRepository.findLiveByHash","apiTokensRepository.touchLastUsed","sitesRepository.findById","sitesRepository.findByKey","sitesRepository.findPrimary","ShapioVersionSkewError","SHAPIO_VERSION","requestStateRepository.readForDelivery","ShapioVersionSkewError","adminRolesRepository.listAllPermissions","appRolesRepository.listAllPermissions","siteAppRolesRepository.listAll","permissionsVersionRepository.getPermissionsVersion","schemaVersionsRepository.getSchemaVersion","schemaModelsRepository.findActiveDefinitions","localesRepository.list","schemaVersionsRepository.getSchemaVersion","DeliveryRuntimeError","systemSettingsRepository.findValue","delivery.SHAPIO_VERSION","delivery.ShapioVersionSkewError","delivery.DeliveryRuntimeError","delivery.createDeliveryRuntime","delivery.createInProcessRequest"],"sources":["../../../apps/api/package.json","../../../apps/api/src/constants/version.ts","../../../apps/api/src/helpers/errorResponse.ts","../../../apps/api/src/delivery/errors.ts","../../../apps/api/src/constants/sites.ts","../../../apps/api/src/content/model.ts","../../../apps/api/src/helpers/siteQuery.ts","../../../apps/api/src/constants/auth.ts","../../../apps/api/src/permissions/types.ts","../../../apps/api/src/routes/schemas/adminIdentity.ts","../../../apps/api/src/routes/schemas/error.ts","../../../apps/api/src/routes/snapshots/schemas.ts","../../../apps/api/src/content/compiler/types.ts","../../../apps/api/src/content/compiler/paginate.ts","../../../apps/api/src/content/errors.ts","../../../apps/api/src/usage/keys.ts","../../../apps/api/src/content/validator/scalars.ts","../../../apps/api/src/content/validator/index.ts","../../../apps/api/src/db/dialect.ts","../../../apps/api/src/db/mysql/fieldIndexColumns.ts","../../../apps/api/src/content/compiler/heads.ts","../../../apps/api/src/content/compiler/dialect/mysql.ts","../../../apps/api/src/content/compiler/dialect/postgres.ts","../../../apps/api/src/content/compiler/dialect/sqlite.ts","../../../apps/api/src/content/compiler/dialect/index.ts","../../../apps/api/src/content/compiler/currentDialect.ts","../../../apps/api/src/content/compiler/policy.ts","../../../apps/api/src/content/compiler/select.ts","../../../apps/api/src/usage/restFieldReads.ts","../../../apps/api/src/content/compiler/expressions.ts","../../../apps/api/src/content/compiler/operators.ts","../../../apps/api/src/content/compiler/compile.ts","../../../apps/api/src/content/compiler/conditions.ts","../../../apps/api/src/content/compiler/sort.ts","../../../apps/api/src/content/locales.ts","../../../apps/api/src/db/index.ts","../../../apps/api/src/repositories/contentQueries.ts","../../../apps/api/src/repositories/mediaAssets.ts","../../../apps/api/src/constants/media.ts","../../../apps/api/src/media/urls.ts","../../../apps/api/src/repositories/mediaVariants.ts","../../../apps/api/src/services/mediaViews.ts","../../../apps/api/src/content/compiler/populate.ts","../../../apps/api/src/content/media.ts","../../../apps/api/src/content/relations.ts","../../../apps/api/src/content/seo.ts","../../../apps/api/src/content/read.ts","../../../apps/api/src/content/compiler/querystring.ts","../../../apps/api/src/content/compiler/parse.ts","../../../apps/api/src/repositories/contentRevisions.ts","../../../apps/api/src/db/sql/values.ts","../../../apps/api/src/db/sql/json.ts","../../../apps/api/src/services/contentReads.ts","../../../apps/api/src/repositories/sites.ts","../../../apps/api/src/services/siteSeo.ts","../../../apps/api/src/services/contentDeliveryReads.ts","../../../apps/api/src/services/contentDelivery.ts","../../../apps/api/src/repositories/publications.ts","../../../apps/api/src/repositories/schemaVersions.ts","../../../apps/api/src/db/snapshotDiffQuery.ts","../../../apps/api/src/repositories/snapshotDiff.ts","../../../apps/api/src/services/snapshotChanges.ts","../../../apps/api/src/helpers/tokens.ts","../../../apps/api/src/permissions/memo.ts","../../../apps/api/src/permissions/scoped.ts","../../../apps/api/src/permissions/sites.ts","../../../apps/api/src/constants/delivery.ts","../../../apps/api/src/repositories/requestState.ts","../../../apps/api/src/repositories/adminRoles.ts","../../../apps/api/src/repositories/permissionsVersion.ts","../../../apps/api/src/permissions/seedRoles.ts","../../../apps/api/src/repositories/apiTokens.ts","../../../apps/api/src/services/apiTokens.ts","../../../apps/api/src/content/hooks.ts","../../../apps/api/src/services/contentContext.ts","../../../apps/api/src/repositories/schemaModels.ts","../../../apps/api/src/schema/planner/locks.ts","../../../apps/api/src/services/sites.ts","../../../apps/api/src/delivery/scope.ts","../../../apps/api/src/delivery/request.ts","../../../apps/api/src/helpers/publicUrl.ts","../../../apps/api/src/media/signing.ts","../../../apps/api/src/media/localAdapter.ts","../../../apps/api/src/media/storage.ts","../../../apps/api/src/repositories/appRoles.ts","../../../apps/api/src/repositories/siteAppRoles.ts","../../../apps/api/src/permissions/cache.ts","../../../apps/api/src/permissions/appRoles.ts","../../../apps/api/src/permissions/policy.ts","../../../apps/api/src/permissions/evaluator.ts","../../../apps/api/src/repositories/systemSettings.ts","../../../apps/api/src/schema/fieldVisibility.ts","../../../apps/api/src/repositories/locales.ts","../../../apps/api/src/schema/snapshot.ts","../../../apps/api/src/schema/storedDefinition.ts","../../../apps/api/src/schema/loadSnapshot.ts","../../../apps/api/src/schema/registry.ts","../../../apps/api/src/services/deliveryDescriptor.ts","../../../apps/api/src/services/signingSecret.ts","../../../apps/api/src/delivery/runtime.ts","../src/index.ts"],"sourcesContent":["","import packageJson from '../../package.json' with { type: 'json' };\n\nexport const SHAPIO_VERSION: string = packageJson.version;\n","import type { ErrorResponse } from '../routes/schemas/error.js';\nimport { AppError } from './appError.js';\n\n/**\n * What the error response reads from an error that is not an `AppError`: the parts Fastify's errors carry\n * (status, code, request-validation issues). Structural, so callers outside Fastify (the in-process delivery\n * runtime) share this mapping without importing it.\n */\nexport type ErrorLike = Error & {\n code?: string;\n statusCode?: number;\n validation?: ReadonlyArray<{ instancePath: string; message?: string; params: unknown }>;\n};\n\nconst STATUS_CODES: Readonly<Record<number, string>> = {\n 400: 'BAD_REQUEST',\n 401: 'UNAUTHORIZED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_ALLOWED',\n 406: 'NOT_ACCEPTABLE',\n 409: 'CONFLICT',\n 413: 'PAYLOAD_TOO_LARGE',\n 415: 'UNSUPPORTED_MEDIA_TYPE',\n 422: 'UNPROCESSABLE_ENTITY',\n 429: 'RATE_LIMITED',\n 503: 'SERVICE_UNAVAILABLE',\n};\n\n/** Errors raised by @fastify/csrf-protection (plugins/csrf.ts). */\nconst CSRF_ERROR_CODES: ReadonlySet<string> = new Set(['FST_CSRF_INVALID_TOKEN', 'FST_CSRF_MISSING_SECRET']);\n\nexport type ErrorResult = { statusCode: number; body: ErrorResponse };\n\n/**\n * The one error shape every endpoint returns, `{ error: { code, message, details? } }`, and its status.\n * Server errors (5xx) never carry their internal text; the caller logs the original error.\n */\nexport const toErrorResponse = (error: ErrorLike | AppError): ErrorResult => {\n if (error instanceof AppError) {\n const body: ErrorResponse = { error: { code: error.code, message: error.message } };\n if (error.details !== undefined) {\n body.error.details = error.details;\n }\n return { statusCode: error.statusCode, body };\n }\n if (error.code !== undefined && CSRF_ERROR_CODES.has(error.code)) {\n // Its own code, so clients know to fetch a fresh token (GET /api/admin/auth/csrf) and retry.\n return {\n statusCode: 403,\n body: { error: { code: 'CSRF_INVALID', message: 'Missing or invalid CSRF token' } },\n };\n }\n if (error.validation) {\n return {\n statusCode: 400,\n body: {\n error: {\n code: 'VALIDATION_ERROR',\n message: error.message,\n details: error.validation.map(({ instancePath, message, params }) => ({\n instancePath,\n message,\n params,\n })),\n },\n },\n };\n }\n const statusCode = error.statusCode !== undefined && error.statusCode >= 400 ? error.statusCode : 500;\n if (statusCode >= 500) {\n // Never echo internal error text to clients.\n return { statusCode, body: { error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } } };\n }\n return {\n statusCode,\n body: { error: { code: STATUS_CODES[statusCode] ?? 'ERROR', message: error.message } },\n };\n};\n","import { ShapioApiError } from '@shapio/client';\nimport type { Logger } from 'pino';\nimport { toErrorResponse, type ErrorLike } from '../helpers/errorResponse.js';\n\n/**\n * The server's release and this library's differ, or the database predates in-process delivery: reading\n * across versions could run SQL against a schema it was not written for (plan next-in-process §2). Callers\n * that catch `ShapioApiError` see a 503 `VERSION_SKEW`.\n */\nexport class ShapioVersionSkewError extends ShapioApiError {\n readonly serverRelease: string | null;\n readonly libraryRelease: string;\n\n constructor(serverRelease: string | null, libraryRelease: string, options?: ErrorOptions) {\n super(503, {\n error: {\n code: 'VERSION_SKEW',\n message:\n serverRelease === null\n ? `The Shapio database has not been served by Shapio ${libraryRelease} yet; start the Shapio server ` +\n `of this release (or install the release the server runs) before reading in process`\n : `The Shapio server runs ${serverRelease} and this in-process reader is ${libraryRelease}; ` +\n 'install the same release on both',\n },\n });\n this.name = 'ShapioVersionSkewError';\n if (options?.cause !== undefined) {\n this.cause = options.cause;\n }\n this.serverRelease = serverRelease;\n this.libraryRelease = libraryRelease;\n }\n}\n\n/** Thrown when the runtime cannot be created for this database (SQLite, an unknown URL, a second database). */\nexport class DeliveryRuntimeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'DeliveryRuntimeError';\n }\n}\n\n/** The body and status the HTTP API would have answered, thrown as the client throws it. */\nexport const toShapioApiError = (error: unknown, log: Pick<Logger, 'error'>): ShapioApiError => {\n if (error instanceof ShapioApiError) {\n return error;\n }\n const failure = error instanceof Error ? (error as ErrorLike) : new Error(String(error));\n const { statusCode, body } = toErrorResponse(failure);\n if (statusCode >= 500) {\n log.error({ err: error }, 'in-process delivery read failed');\n }\n return new ShapioApiError(statusCode, body);\n};\n","/** Sites (plan agentic-ecosystem §H, ADR 0011). */\n\n/** The primary site the sites migration created; every row that existed before sites belongs to it. */\nexport const PRIMARY_SITE_ID = '00000000-0000-4000-b000-000000000001';\n\n/** A site key: lower case, starts with a letter (the same rule as role keys). Used in URLs and `?site=`. */\nexport const SITE_KEY_PATTERN = '^[a-z][a-z0-9-]{0,62}$';\n\n/** Admin API requests name their site with this header (the admin sends it on every request). */\nexport const SITE_HEADER = 'shapio-site';\n\n/** Delivery requests may name their site with this query parameter (it must match a site token's site). */\nexport const SITE_QUERY_PARAMETER = 'site';\n\n/**\n * `Vary` of cacheable delivery responses (REST and GraphQL GETs): the credentials that select the principal,\n * and the header that selects the site. CDNs that key on the URL only need `?site=` instead of the header.\n */\nexport const DELIVERY_VARY = 'Authorization, Cookie, Shapio-Site';\n","import {\n embeddedComponentIds,\n isComponentDefinition,\n type ComponentDefinition,\n type FieldDefinition,\n type ModelDefinition,\n} from '@shapio/schema';\nimport { AppError } from '../helpers/appError.js';\nimport type { ActiveDefinition, SchemaById, SchemaSnapshot } from '../schema/snapshot.js';\n\n/** Head states (ADR 0001): every entry has a draft per locale and, once published, a published head. */\nexport const HEAD_STATES = ['draft', 'published'] as const;\nexport type HeadState = (typeof HEAD_STATES)[number];\n\n/** Unique-registry locale for shared (non-localized) fields: one value across every locale. */\nexport const SHARED_LOCALE = '*';\n\n/** A collection or singleton resolved from a pinned snapshot, with what writes need to know about it. */\nexport type ContentModel = {\n definition: ModelDefinition;\n /** Per-model version at the pinned snapshot (re-checked at commit). */\n version: number;\n revisionId: string;\n /** Components reachable from the model (directly or through other components), with their versions. */\n components: ReadonlyMap<string, { definition: ComponentDefinition; version: number }>;\n};\n\nexport const modelNotFound = (key: string) =>\n new AppError(404, 'MODEL_NOT_FOUND', `No content model \"${key}\"`, { modelKey: key });\n\nconst collectComponents = (snapshot: SchemaById, model: ModelDefinition): ContentModel['components'] => {\n const found = new Map<string, { definition: ComponentDefinition; version: number }>();\n const queue = embeddedComponentIds(model);\n while (queue.length > 0) {\n const id = queue.shift() as string;\n const active = snapshot.byId.get(id);\n if (!active || found.has(id) || !isComponentDefinition(active.definition)) {\n continue;\n }\n found.set(id, { definition: active.definition, version: active.version });\n queue.push(...embeddedComponentIds(active.definition));\n }\n return found;\n};\n\nconst toContentModel = (\n snapshot: SchemaById,\n active: ActiveDefinition & { definition: ModelDefinition },\n): ContentModel => ({\n definition: active.definition,\n version: active.version,\n revisionId: active.revisionId,\n components: collectComponents(snapshot, active.definition),\n});\n\nconst isModel = (\n active: ActiveDefinition | undefined,\n): active is ActiveDefinition & { definition: ModelDefinition } =>\n active !== undefined && !isComponentDefinition(active.definition);\n\n/**\n * Resolves `/api/…/content/:modelKey` through the registry at request time (CONTRIBUTING.md rule 2). Only a\n * site's view resolves API IDs: another site's model is not in it, so it reads as not found.\n */\nexport const resolveModel = (snapshot: SchemaSnapshot, modelKey: string): ContentModel => {\n const active = snapshot.modelsByApiKey.get(modelKey);\n if (!isModel(active)) {\n throw modelNotFound(modelKey);\n }\n return toContentModel(snapshot, active);\n};\n\n/**\n * Resolves the delivery and preview routes' `:modelKey`, the route key (`routeKeyOf`): the plural API ID\n * of a collection, the API ID of a singleton. A collection's singular API ID is not a route key (404).\n */\nexport const resolveRouteModel = (snapshot: SchemaSnapshot, routeKey: string): ContentModel => {\n const active = snapshot.modelsByRouteKey.get(routeKey);\n if (!active || isComponentDefinition(active.definition)) {\n throw modelNotFound(routeKey);\n }\n return resolveModel(snapshot, active.definition.apiKey);\n};\n\n/** The API ID behind a route key: REST entry points translate with it, then call the services. */\nexport const apiKeyOfRoute = (snapshot: SchemaSnapshot, routeKey: string): string =>\n resolveRouteModel(snapshot, routeKey).definition.apiKey;\n\n/** By stable ID: works on a view and on the full set (jobs and conversions that already know the site). */\nexport const resolveModelById = (snapshot: SchemaById, modelId: string): ContentModel | undefined => {\n const active = snapshot.byId.get(modelId);\n return isModel(active) ? toContentModel(snapshot, active) : undefined;\n};\n\n/** Whether a field's value differs per locale. Fields of a non-localized model are always shared. */\nexport const isLocalizedField = (model: ModelDefinition, field: FieldDefinition): boolean =>\n model.localized && field.localized;\n\n/** Fields the API exposes (deprecated fields keep their stored values but are hidden). */\nexport const liveFields = (fields: readonly FieldDefinition[]): FieldDefinition[] =>\n fields.filter((field) => !field.deprecated);\n\nexport const findFieldByApiKey = (\n fields: readonly FieldDefinition[],\n apiKey: string,\n): FieldDefinition | undefined => fields.find((field) => field.apiKey === apiKey && !field.deprecated);\n","import { SITE_QUERY_PARAMETER } from '../constants/sites.js';\n\nconst isSiteSegment = (segment: string): boolean => {\n const key = segment.split('=', 1)[0] ?? '';\n try {\n return decodeURIComponent(key.replace(/\\+/g, ' ')) === SITE_QUERY_PARAMETER;\n } catch {\n return false;\n }\n};\n\n/**\n * A raw querystring (without `?`) without its `site` parameter: site resolution consumes it, and the content\n * compiler refuses unknown keys. The rest is left exactly as sent (bracket syntax is the compiler's to parse).\n */\nexport const withoutSiteParameter = (rawQuery: string): string =>\n rawQuery\n .split('&')\n .filter((segment) => !isSiteSegment(segment))\n .join('&');\n","/** Admin authentication settings (ADR 0005). Not configurable by env on purpose: safe defaults for everyone. */\n\nexport const SESSION_COOKIE_NAME = 'shapio_session';\n\n/** A session dies after this long without a request. */\nexport const SESSION_IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1000;\n/** A session dies this long after login, however active it is. */\nexport const SESSION_ABSOLUTE_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000;\n/** `last_seen_at` / `last_used_at` are written at most this often, so reads stay reads. */\nexport const LAST_SEEN_WRITE_INTERVAL_MS = 60 * 1000;\n\nexport const CSRF_HEADER = 'x-csrf-token';\n\nexport const INVITATION_TTL_MS = 7 * 24 * 60 * 60 * 1000;\nexport const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000;\n\nexport const PASSWORD_MIN_LENGTH = 12;\n/** argon2 accepts more, but unbounded input is a denial-of-service vector. */\nexport const PASSWORD_MAX_LENGTH = 256;\n\n/** API tokens carry a recognisable prefix (secret scanners, and telling them apart from app-user JWTs). */\nexport const API_TOKEN_PREFIX = 'shp_';\n/** Characters of the token kept in plain text for display (`shp_` + 6). */\nexport const API_TOKEN_DISPLAY_LENGTH = 10;\n\n/** Rate limits for unauthenticated credential endpoints. Per IP via @fastify/rate-limit route config. */\nexport const CREDENTIAL_RATE_LIMIT_PER_IP = { max: 20, timeWindow: 60 * 1000 } as const;\n/** Per email address and client IP (logins), or per email address (reset, registration, confirmation mail). */\nexport const CREDENTIAL_RATE_LIMIT_PER_EMAIL = { max: 5, timeWindow: 15 * 60 * 1000 } as const;\n","import type { Database } from '../db/index.js';\n\n/**\n * Principals and policies (ADR 0005). One evaluator serves admin users, app users, tokens and anonymous\n * callers, for REST and GraphQL alike. Package A ships the types and a deny-all evaluator; package B\n * implements roles and the real evaluator against this interface.\n */\n\n/** One role held by an admin user: on one site, or on every site (`siteId` null). */\nexport type RoleAssignment = { roleId: string; siteId: string | null };\n\n/**\n * An admin user as the session knows them: every role assignment, on any site. Never evaluated directly;\n * `narrowToSite` (permissions/sites.ts) turns it into the `AdminPrincipal` of one request's site.\n */\nexport type AdminIdentity = {\n adminUserId: string;\n sessionId: string;\n assignments: readonly RoleAssignment[];\n};\n\n/**\n * An admin user narrowed to one site (sites plan §H). `roleIds`: roles assigned on this site or on every\n * site; site actions and content actions are evaluated against them. `networkRoleIds`: roles assigned on\n * every site only; network actions (users, roles, schema, audit, sites) are evaluated against them, so a\n * site-only role can never reach the network. `siteId` null: a network route, where `roleIds` equals\n * `networkRoleIds`.\n */\nexport type AdminPrincipal = {\n kind: 'admin';\n adminUserId: string;\n sessionId: string;\n /** Every assignment, on any site (which sites the admin works on). */\n assignments: readonly RoleAssignment[];\n siteId: string | null;\n roleIds: readonly string[];\n networkRoleIds: readonly string[];\n};\n\n/**\n * An app user: an end user of one site (sites plan §H). `roleIds`: the custom app roles assigned to the\n * account; the roles bound to the site's `authenticated` audience (`site_app_roles`) apply on top.\n * `siteId`: the account's site, from the access token's `site` claim; the account acts on no other site.\n */\nexport type AppUserPrincipal = {\n kind: 'appUser';\n appUserId: string;\n siteId: string;\n roleIds: readonly string[];\n};\n\n/** API tokens bind to exactly one role: an admin role or a delivery role. */\nexport type TokenPrincipal = {\n kind: 'token';\n tokenId: string;\n scope: 'admin' | 'delivery';\n roleId: string;\n /**\n * The token's site; null for a network admin token (its role applies on every site and to network\n * actions). Delivery tokens always have a site.\n */\n siteId: string | null;\n};\n\n/**\n * A caller without credentials. On a site route `siteId` is the request's site, and the roles bound to the\n * site's `public` audience (`site_app_roles`) apply. Null before site resolution and on network routes,\n * where an anonymous caller holds no role at all.\n */\nexport type AnonymousPrincipal = { kind: 'anonymous'; siteId: string | null };\n\n/** Work Shapio does on its own behalf: scheduled jobs, migrations, the worker. Never derived from a request. */\nexport type SystemPrincipal = { kind: 'system'; component: string };\n\nexport type Principal =\n AdminPrincipal | AppUserPrincipal | TokenPrincipal | AnonymousPrincipal | SystemPrincipal;\n\nexport const CONTENT_ACTIONS = ['read', 'create', 'update', 'delete', 'publish', 'schemaManage'] as const;\nexport type ContentAction = (typeof CONTENT_ACTIONS)[number];\n\n/**\n * Content actions about the schema rather than one site's entries: network roles only for a shared\n * definition; for a site's own definition, the roles that apply on that site (when the principal acts there).\n */\nexport const NETWORK_CONTENT_ACTIONS: ReadonlySet<ContentAction> = new Set(['schemaManage']);\n\n/**\n * Actions that are not scoped to one model, in two kinds (sites plan §H):\n * - Network actions are about the whole instance (creating models, managing admin users, roles and sites,\n * reading the audit log). Only roles assigned on every site grant them: a role held on one site never does,\n * so a site admin cannot grant themselves other sites.\n * - Site actions are about one site's library, tokens and publishing; any role held on the site grants them.\n */\nexport const NETWORK_ACTIONS = [\n 'schema.create',\n 'users.manage',\n 'roles.manage',\n 'audit.read',\n 'sites.manage',\n] as const;\nexport type NetworkAction = (typeof NETWORK_ACTIONS)[number];\n\n/**\n * Network actions a role held on one site also grants for that site alone (plan site-schema, rule 4):\n * `schema.create` creates that site's own definitions. They are checked with `canPerformOnSite` and the\n * request's site; `canPerform` keeps the every-site meaning (shared definitions, locales, the schema lock).\n */\nexport const SITE_GRANTABLE_ACTION_LIST = ['schema.create'] as const satisfies readonly NetworkAction[];\nexport type SiteGrantableAction = (typeof SITE_GRANTABLE_ACTION_LIST)[number];\nexport const SITE_GRANTABLE_ACTIONS: ReadonlySet<GlobalAction> = new Set<GlobalAction>(\n SITE_GRANTABLE_ACTION_LIST,\n);\n\nexport const SITE_ACTIONS = [\n 'tokens.manage',\n // Media library (package G): browse; upload, edit and organise; delete and change visibility.\n 'media.read',\n 'media.write',\n 'media.manage',\n // Publishing (package H): the jobs view and scheduling overview; webhooks; deployment connections;\n // triggering and retrying deployment runs; change sets (schema + content shipped as one snapshot) and\n // shipping them (`changes.ship`, split so an agent's role can prepare change sets but not make them live).\n 'publishing.manage',\n 'webhooks.manage',\n 'deployments.manage',\n 'deployments.trigger',\n 'changes.manage',\n 'changes.ship',\n // The site's own settings (plan seo-fields): its SEO defaults.\n 'site.settings',\n] as const;\nexport type SiteAction = (typeof SITE_ACTIONS)[number];\n\nexport const GLOBAL_ACTIONS = [...NETWORK_ACTIONS, ...SITE_ACTIONS] as const;\nexport type GlobalAction = (typeof GLOBAL_ACTIONS)[number];\n\n/** Field visibility by stable field ID. `all` means every field the schema exposes to this principal kind. */\nexport type FieldMask = { mode: 'all' } | { mode: 'only'; fieldIds: readonly string[] };\n\n/** Enumerated row conditions; compiled to SQL only by content/compiler. Never free-form expressions. */\nexport type RowCondition = { kind: 'ownedByPrincipal' };\n\nexport type RowFilter =\n | RowCondition\n | { kind: 'and'; filters: readonly RowFilter[] }\n | { kind: 'or'; filters: readonly RowFilter[] };\n\nexport type Policy = {\n allowed: boolean;\n /** Extra restriction on which rows the principal may act on. `null` means no restriction. */\n rowFilter: RowFilter | null;\n readMask: FieldMask;\n writeMask: FieldMask;\n};\n\nexport type PolicyRequest = {\n action: ContentAction;\n modelId: string;\n};\n\n/**\n * Where a permission lookup reads its durable version checks (and any reload): the open transaction when\n * the caller is inside one, so the lookup never waits for a second pooled connection while it holds one\n * (an exhausted pool would then wait forever). Omitted: the pool.\n */\nexport type PermissionExecutor = Database;\n\n/**\n * Durable versions a request already read (`repositories/requestState.ts`, once per request, before any\n * transaction). A lookup given one compares its cache against it instead of reading it again, and still\n * reloads when its cache is older; a change made after the read applies from the next request. Omitted: the\n * lookup reads the version itself (jobs, the worker, the CLI).\n */\nexport type KnownVersions = { schemaVersion?: number; permissionsVersion?: number };\n\nexport type PermissionEvaluator = {\n evaluate: (principal: Principal, request: PolicyRequest, executor?: PermissionExecutor) => Promise<Policy>;\n canPerform: (principal: Principal, action: GlobalAction, executor?: PermissionExecutor) => Promise<boolean>;\n /**\n * `canPerform` for the request's site: an action in `SITE_GRANTABLE_ACTIONS` also counts the roles that\n * apply on `siteId` when the principal acts on that site (an admin narrowed to it, a site token of it, a\n * network token). Any other action is `canPerform`.\n */\n canPerformOnSite: (\n principal: Principal,\n action: GlobalAction,\n siteId: string,\n executor?: PermissionExecutor,\n ) => Promise<boolean>;\n /**\n * The same evaluator, checking its caches against versions the request already read instead of reading\n * them on every call. Absent on evaluators without durable caches (fixed test evaluators, wrappers).\n */\n atVersions?: (versions: KnownVersions) => PermissionEvaluator;\n};\n\nconst EMPTY_MASK: FieldMask = { mode: 'only', fieldIds: [] };\n\nexport const DENIED_POLICY: Policy = Object.freeze({\n allowed: false,\n rowFilter: null,\n readMask: EMPTY_MASK,\n writeMask: EMPTY_MASK,\n});\n","import { Type, type Static } from 'typebox';\nimport { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from '../../constants/auth.js';\nimport {\n CONTENT_ACTIONS,\n GLOBAL_ACTIONS,\n NETWORK_ACTIONS,\n SITE_ACTIONS,\n SITE_GRANTABLE_ACTION_LIST,\n} from '../../permissions/types.js';\n\n/** Schemas shared by the admin identity routes (setup, auth, users, roles, tokens). */\n\n/** A timestamp in a response: a Date in handlers, ISO-8601 on the wire (fast-json-stringify formats it). */\nexport const DateTimeSchema = Type.Unsafe<Date>({ type: 'string', format: 'date-time' });\nexport const NullableDateTimeSchema = Type.Union([DateTimeSchema, Type.Null()]);\n/** A timestamp in a request: an ISO-8601 string. */\nexport const DateTimeInputSchema = Type.String({ format: 'date-time' });\nexport const UuidSchema = Type.String({ format: 'uuid' });\n\nexport const IdParamsSchema = Type.Object({ id: UuidSchema });\nexport type IdParams = Static<typeof IdParamsSchema>;\n\nexport const EmailSchema = Type.String({ format: 'email', minLength: 3, maxLength: 254 });\nexport const PasswordSchema = Type.String({ minLength: PASSWORD_MIN_LENGTH, maxLength: PASSWORD_MAX_LENGTH });\n/** A password being checked (login, current password): any length policy applies only to new ones. */\nexport const PasswordAttemptSchema = Type.String({ minLength: 1, maxLength: PASSWORD_MAX_LENGTH });\nexport const PersonNameSchema = Type.String({ minLength: 1, maxLength: 200 });\n/** Single-use tokens from links and logs: 43 base64url characters. */\nexport const OneTimeTokenSchema = Type.String({ minLength: 16, maxLength: 128, pattern: '^[A-Za-z0-9_-]+$' });\n\n/** A role held on one site, or on every site (`siteId` null). */\nexport const RoleAssignmentSchema = Type.Object(\n { roleId: UuidSchema, siteId: Type.Union([UuidSchema, Type.Null()]) },\n { additionalProperties: false },\n);\n\nexport const AdminUserSchema = Type.Object({\n id: UuidSchema,\n email: Type.String(),\n name: Type.String(),\n status: Type.Union([Type.Literal('active'), Type.Literal('disabled')]),\n /** Where each role applies. */\n assignments: Type.Array(RoleAssignmentSchema),\n /** Deprecated: the distinct roles of `assignments`, on any site. */\n roleIds: Type.Array(UuidSchema),\n lastLoginAt: NullableDateTimeSchema,\n createdAt: DateTimeSchema,\n updatedAt: DateTimeSchema,\n});\n\n/** Returned whenever a session starts: the user, and the CSRF token for its mutations. */\nexport const SessionStartedSchema = Type.Object({ user: AdminUserSchema, csrfToken: Type.String() });\n\nexport const ContentActionSchema = Type.Enum(CONTENT_ACTIONS);\nexport const GlobalActionSchema = Type.Enum(GLOBAL_ACTIONS);\nexport const NetworkActionSchema = Type.Enum(NETWORK_ACTIONS);\nexport const SiteActionSchema = Type.Enum(SITE_ACTIONS);\n/** What `me.sitePermissions` lists: site actions, and network actions a role on the site grants there. */\nexport const SitePermissionSchema = Type.Enum([...SITE_ACTIONS, ...SITE_GRANTABLE_ACTION_LIST]);\n\nexport const PermissionSchema = Type.Object(\n {\n action: Type.Enum([...CONTENT_ACTIONS, ...GLOBAL_ACTIONS]),\n modelId: Type.Union([Type.String({ minLength: 1, maxLength: 100 }), Type.Null()]),\n condition: Type.Union([Type.Literal('ownedByPrincipal'), Type.Null()]),\n fieldIds: Type.Union([\n Type.Array(Type.String({ minLength: 1, maxLength: 100 }), { maxItems: 500 }),\n Type.Null(),\n ]),\n },\n { additionalProperties: false },\n);\n","import { Type, type Static } from 'typebox';\n\n/** The one error shape every endpoint returns (central error handler). */\nexport const ErrorResponseSchema = Type.Object({\n error: Type.Object({\n code: Type.String(),\n message: Type.String(),\n details: Type.Optional(Type.Unknown()),\n }),\n});\n\nexport type ErrorResponse = Static<typeof ErrorResponseSchema>;\n","import { Type, type Static } from 'typebox';\nimport { NullableDateTimeSchema, UuidSchema } from '../schemas/adminIdentity.js';\nimport { ErrorResponseSchema } from '../schemas/error.js';\n\nconst SeqSchema = Type.Integer({ minimum: 0 });\nconst NullableInteger = Type.Union([Type.Integer(), Type.Null()]);\nconst ERRORS = { 400: ErrorResponseSchema, 401: ErrorResponseSchema };\n\nexport const SnapshotChangesQuerySchema = Type.Object(\n {\n from: SeqSchema,\n /** Defaults to the current snapshot. */\n to: Type.Optional(SeqSchema),\n /** `nextCursor` of the previous page. */\n after: Type.Optional(UuidSchema),\n limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 500 })),\n },\n { additionalProperties: false },\n);\nexport type SnapshotChangesQuery = Static<typeof SnapshotChangesQuerySchema>;\n\nexport const SnapshotChangeSchema = Type.Object({\n id: UuidSchema,\n modelId: UuidSchema,\n modelKey: Type.String(),\n routeKey: Type.String(),\n /** From the revision live at `to` (at `from` when unpublished); null without a readable title. */\n title: Type.Union([Type.String(), Type.Null()]),\n /** The cover image's media asset ID, when the model has a readable cover. */\n coverMediaId: Type.Union([UuidSchema, Type.Null()]),\n locales: Type.Array(\n Type.Object({\n locale: Type.String(),\n change: Type.Enum(['published', 'updated', 'unpublished']),\n /** The revision live at `to`, or at `from` for `unpublished`. */\n revisionId: Type.Union([UuidSchema, Type.Null()]),\n }),\n ),\n});\n\nexport const snapshotChangesSchema = {\n querystring: SnapshotChangesQuerySchema,\n response: {\n 200: Type.Object({\n from: Type.Integer(),\n to: Type.Integer(),\n schemaVersions: Type.Object({ from: NullableInteger, to: NullableInteger }),\n items: Type.Array(SnapshotChangeSchema),\n nextCursor: Type.Union([UuidSchema, Type.Null()]),\n }),\n ...ERRORS,\n },\n};\n\nexport const currentSnapshotSchema = {\n response: {\n 200: Type.Object({\n snapshot: Type.Integer(),\n schemaVersion: Type.Integer(),\n publishedAt: NullableDateTimeSchema,\n }),\n ...ERRORS,\n },\n};\n","import type { FieldDefinition } from '@shapio/schema';\nimport { AppError } from '../../helpers/appError.js';\n\n/** The parsed, allowlisted form of a content query. Nothing in it is raw request text used as SQL. */\n\nexport const FILTER_OPERATORS = [\n '$eq',\n '$ne',\n '$in',\n '$nin',\n '$lt',\n '$lte',\n '$gt',\n '$gte',\n '$contains',\n '$notContains',\n '$containsi',\n '$startsWith',\n '$endsWith',\n '$null',\n '$notNull',\n] as const;\nexport type FilterOperator = (typeof FILTER_OPERATORS)[number];\n\n/** System attributes every entry has. They are not stored in `data`. */\nexport const SYSTEM_ATTRIBUTES = ['id', 'createdAt', 'updatedAt'] as const;\nexport type SystemAttribute = (typeof SYSTEM_ATTRIBUTES)[number];\n\nexport type FilterTarget =\n { kind: 'field'; field: FieldDefinition } | { kind: 'system'; name: SystemAttribute };\n\nexport type FilterNode =\n | { kind: 'and'; nodes: FilterNode[] }\n | { kind: 'or'; nodes: FilterNode[] }\n | { kind: 'not'; node: FilterNode }\n | { kind: 'condition'; target: FilterTarget; operator: FilterOperator; value: unknown };\n\nexport type SortTerm = { target: FilterTarget; direction: 'asc' | 'desc' };\n\n/** Relation fields to expand, with what to expand inside each target. */\nexport type PopulateTree = ReadonlyMap<string, PopulateTree>;\n\nexport type ContentQuery = {\n filter: FilterNode | null;\n /** Free-text search over the model's title field. */\n search: { field: FieldDefinition; text: string } | null;\n sort: SortTerm[];\n page: number;\n pageSize: number;\n /** Top-level fields to return; null = every readable field. */\n fields: FieldDefinition[] | null;\n /** Keyed by relation field ID. */\n populate: PopulateTree;\n locale: string | undefined;\n snapshot: number | undefined;\n /** Delivery and preview only: how rich-text values are returned (`?richText=`); undefined on admin reads. */\n richText?: RichTextMode;\n /** Delivery and preview only: SEO fields as stored (`raw`, the default) or with the site's defaults (`?seo=`). */\n seo?: SeoMode;\n /** Admin list only: entries whose draft in the served locale has this status. */\n status?: EntryListStatus;\n /** Admin list only: entries created by this admin user. */\n author?: string;\n};\n\n/**\n * Delivery shapes of a rich-text value (ADR 0003): `json` the stored document `{ format, version, doc }`,\n * `html` `{ format, version, html }` (sanitized HTML rendered from the document), `both` the document plus\n * `html`.\n */\nexport const RICH_TEXT_MODES = ['json', 'html', 'both'] as const;\nexport type RichTextMode = (typeof RICH_TEXT_MODES)[number];\n\n/** What a delivery read returns for rich text when the request does not say (`?richText=`). */\nexport const DEFAULT_RICH_TEXT_MODE: RichTextMode = 'json';\n\n/** `?seo=`: SEO fields as stored, or merged with the site's SEO defaults (plan seo-fields). */\nexport const SEO_MODES = ['raw', 'resolved'] as const;\nexport type SeoMode = (typeof SEO_MODES)[number];\n\nexport const ENTRY_LIST_STATUSES = ['draft', 'published', 'modified'] as const;\nexport type EntryListStatus = (typeof ENTRY_LIST_STATUSES)[number];\n\nexport const QUERY_LIMITS = {\n maxPageSize: 100,\n defaultPageSize: 25,\n maxConditions: 50,\n maxFilterDepth: 6,\n maxInValues: 100,\n maxValueLength: 1000,\n maxParameters: 200,\n maxPopulateDepth: 3,\n maxSortTerms: 5,\n} as const;\n\n/** A malformed or disallowed query: 400 for unknown/invalid, 403 for fields the caller may not use. */\nexport const queryInvalid = (message: string, details?: Record<string, unknown>) =>\n new AppError(400, 'INVALID_QUERY', message, details);\n\nexport const queryForbidden = (message: string, details?: Record<string, unknown>) =>\n new AppError(403, 'FORBIDDEN_FIELD', message, details);\n","import { QUERY_LIMITS } from './types.js';\n\n/** Offset pagination with a hard page-size cap (brief §4: \"enforce pagination\"). */\nexport type Pagination = { page: number; pageSize: number; total: number; pageCount: number };\n\nexport const toLimitOffset = (page: number, pageSize: number) => {\n const size = Math.min(Math.max(1, pageSize), QUERY_LIMITS.maxPageSize);\n return { limit: size, offset: (Math.max(1, page) - 1) * size };\n};\n\nexport const paginationMeta = (page: number, pageSize: number, total: number): Pagination => ({\n page,\n pageSize,\n total,\n pageCount: Math.ceil(total / pageSize),\n});\n","import { AppError } from '../helpers/appError.js';\n\n/** Errors the content services throw; the central error handler renders them. */\n\nexport const entryNotFound = (id: string) => new AppError(404, 'ENTRY_NOT_FOUND', `No entry ${id}`, { id });\n\nexport const entryLocaleNotFound = (id: string, locale: string, locales: readonly string[]) =>\n new AppError(404, 'ENTRY_LOCALE_NOT_FOUND', `Entry ${id} has no \"${locale}\" version yet`, {\n id,\n locale,\n locales,\n });\n\nexport const contentVersionConflict = (expectedVersion: number | null, currentVersion: number | null) =>\n new AppError(\n 409,\n 'CONTENT_VERSION_CONFLICT',\n 'The entry changed since you loaded it. Reload it, reapply your edit and save again.',\n { expectedVersion, currentVersion },\n );\n\n/** The model (or a component it embeds) was activated in a new version after this request pinned its schema. */\nexport const schemaChanged = () =>\n new AppError(\n 409,\n 'SCHEMA_CHANGED',\n 'The content model changed while saving. Reload the form and save again.',\n );\n\nexport const unknownLocale = (locale: string) =>\n new AppError(422, 'LOCALE_NOT_FOUND', `Locale \"${locale}\" does not exist`, { locale });\n\nexport const singletonExists = (modelKey: string) =>\n new AppError(409, 'SINGLETON_EXISTS', `\"${modelKey}\" is a single type and already has its entry`, {\n modelKey,\n });\n\nexport const entryReferenced = (referrers: readonly unknown[]) =>\n new AppError(\n 409,\n 'ENTRY_REFERENCED',\n 'Other entries still point at this entry. Remove those references first.',\n {\n referrers,\n },\n );\n\nexport const publishingDisabled = (modelKey: string) =>\n new AppError(422, 'PUBLISHING_DISABLED', `\"${modelKey}\" has no draft state: every save is published`, {\n modelKey,\n });\n\nexport const revisionNotFound = (id: string) =>\n new AppError(404, 'REVISION_NOT_FOUND', `No revision ${id}`, { id });\n\nexport const snapshotInvalid = (requested: number, current: number) =>\n new AppError(400, 'SNAPSHOT_INVALID', `Snapshot ${requested} does not exist yet (latest is ${current})`, {\n requested,\n current,\n });\n","import type { Principal } from '../permissions/types.js';\n\n/**\n * How delivery reads are attributed (plan developer-face §5). Delivery tokens are consumers, one key per\n * token; app users are counted together, and so are anonymous callers. Admin reads (admin users and\n * admin-scope API tokens), system work and previews are not consumers and are never counted.\n */\nexport const APP_USERS_PRINCIPAL_KEY = 'app_users';\nexport const ANONYMOUS_PRINCIPAL_KEY = 'anonymous';\n\n/** `token:<id>`, `app_users`, `anonymous`, or null for principals whose reads are not counted. */\nexport const usagePrincipalKey = (principal: Principal): string | null => {\n switch (principal.kind) {\n case 'token':\n return principal.scope === 'delivery' ? `token:${principal.tokenId}` : null;\n case 'appUser':\n return APP_USERS_PRINCIPAL_KEY;\n case 'anonymous':\n return ANONYMOUS_PRINCIPAL_KEY;\n default:\n return null;\n }\n};\n\n/** The UTC day a read is bucketed in, `YYYY-MM-DD`. */\nexport const usageDayOf = (at: Date): string => at.toISOString().slice(0, 10);\n\n/** A populated relation's target field: `<relation field ID>.<target field ID>`. */\nexport const relationFieldPath = (relationFieldId: string, targetFieldId: string): string =>\n `${relationFieldId}.${targetFieldId}`;\n","import {\n BIGINTEGER_PATTERN,\n DATE_PATTERN,\n DECIMAL_PATTERN,\n isStableId,\n SLUG_PATTERN,\n TIME_PATTERN,\n UID_PATTERN,\n type FieldDefinition,\n} from '@shapio/schema';\nimport type { IssueCode } from './issues.js';\n\n/**\n * Value rules for scalar data types (ADR 0001 storage formats). Each returns the canonical stored value or\n * a problem. Canonical forms are what make text sorting and containment equality correct:\n * - datetime: UTC with milliseconds, `YYYY-MM-DDTHH:mm:ss.sssZ` (offsets are converted to UTC);\n * - time: `HH:mm:ss.sss`;\n * - decimal and biginteger: strings (numbers are accepted when they convert exactly).\n */\nexport type ScalarOutcome = { ok: true; value: unknown } | { ok: false; code: IssueCode; message: string };\n\nconst ok = (value: unknown): ScalarOutcome => ({ ok: true, value });\nconst problem = (code: IssueCode, message: string): ScalarOutcome => ({ ok: false, code, message });\n\nconst INPUT_DATETIME =\n /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(\\.\\d{1,9})?)?(Z|[+-]\\d{2}:\\d{2})$/;\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst MAX_EMAIL_LENGTH = 254;\nconst DEFAULT_URL_PROTOCOLS = ['http', 'https', 'mailto', 'tel'];\n\nconst isRealDate = (text: string) => {\n if (!DATE_PATTERN.test(text)) {\n return false;\n }\n const parsed = new Date(`${text}T00:00:00.000Z`);\n return !Number.isNaN(parsed.getTime()) && parsed.toISOString().startsWith(text);\n};\n\nexport const canonicalDate = (value: unknown): string | undefined =>\n typeof value === 'string' && isRealDate(value) ? value : undefined;\n\nexport const canonicalDatetime = (value: unknown): string | undefined => {\n if (typeof value !== 'string') {\n return undefined;\n }\n const match = INPUT_DATETIME.exec(value);\n if (!match || !isRealDate(`${match[1]}-${match[2]}-${match[3]}`)) {\n return undefined;\n }\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();\n};\n\nexport const canonicalTime = (value: unknown): string | undefined => {\n if (typeof value !== 'string' || !TIME_PATTERN.test(value)) {\n return undefined;\n }\n const [hours, minutes, rest = '00'] = value.split(':') as [string, string, string?];\n const [seconds = '00', millis = '000'] = rest.split('.');\n return `${hours}:${minutes}:${seconds}.${millis.padEnd(3, '0')}`;\n};\n\n/** Decimal strings compared exactly (BigInt on a common scale). */\nexport const compareDecimal = (a: string, b: string): number => {\n const scale = (text: string) => (text.split('.')[1] ?? '').length;\n const digits = Math.max(scale(a), scale(b));\n const toBig = (text: string) => {\n const negative = text.startsWith('-');\n const [whole = '0', fraction = ''] = text.replace('-', '').split('.');\n const big = BigInt(`${whole}${fraction.padEnd(digits, '0')}`);\n return negative ? -big : big;\n };\n const difference = toBig(a) - toBig(b);\n return difference === 0n ? 0 : difference > 0n ? 1 : -1;\n};\n\nexport const canonicalDecimal = (value: unknown): string | undefined => {\n if (typeof value === 'number' && Number.isFinite(value)) {\n const text = String(value);\n return DECIMAL_PATTERN.test(text) ? text : undefined;\n }\n return typeof value === 'string' && DECIMAL_PATTERN.test(value) ? value : undefined;\n};\n\nexport const canonicalBigInteger = (value: unknown): string | undefined => {\n if (typeof value === 'number' && Number.isSafeInteger(value)) {\n return String(value);\n }\n return typeof value === 'string' && BIGINTEGER_PATTERN.test(value) ? value : undefined;\n};\n\nconst textLength = (text: string) => Array.from(text).length;\n\nconst checkLength = (text: string, settings: { minLength?: number; maxLength?: number }): ScalarOutcome => {\n const length = textLength(text);\n if (settings.minLength !== undefined && length < settings.minLength) {\n return problem('TOO_SHORT', `must be at least ${settings.minLength} characters`);\n }\n if (settings.maxLength !== undefined && length > settings.maxLength) {\n return problem('TOO_LONG', `must be at most ${settings.maxLength} characters`);\n }\n return ok(text);\n};\n\nconst checkRange = <T>(\n value: T,\n bounds: { min?: T | undefined; max?: T | undefined },\n compare: (a: T, b: T) => number,\n): ScalarOutcome => {\n if (bounds.min !== undefined && compare(value, bounds.min) < 0) {\n return problem('TOO_SMALL', `must be at least ${String(bounds.min)}`);\n }\n if (bounds.max !== undefined && compare(value, bounds.max) > 0) {\n return problem('TOO_LARGE', `must be at most ${String(bounds.max)}`);\n }\n return ok(value);\n};\n\nconst compareText = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);\nconst compareNumber = (a: number, b: number) => a - b;\nconst compareBig = (a: string, b: string) => {\n const difference = BigInt(a) - BigInt(b);\n return difference === 0n ? 0 : difference > 0n ? 1 : -1;\n};\n\nconst patternCache = new Map<string, RegExp | null>();\nconst compilePattern = (source: string): RegExp | null => {\n if (!patternCache.has(source)) {\n try {\n patternCache.set(source, new RegExp(source, 'u'));\n } catch {\n patternCache.set(source, null);\n }\n }\n return patternCache.get(source) ?? null;\n};\n\nconst decimalDigits = (text: string) => {\n const [whole = '', fraction = ''] = text.replace('-', '').split('.');\n return { precision: whole.replace(/^0+(?=\\d)/, '').length + fraction.length, scale: fraction.length };\n};\n\nconst checkUrl = (value: string, protocols: readonly string[] | undefined): ScalarOutcome => {\n if (!URL.canParse(value)) {\n return problem('INVALID_FORMAT', 'must be an absolute URL');\n }\n const protocol = new URL(value).protocol.replace(/:$/, '');\n return (protocols ?? DEFAULT_URL_PROTOCOLS).includes(protocol)\n ? ok(value)\n : problem('INVALID_FORMAT', `must use one of: ${(protocols ?? DEFAULT_URL_PROTOCOLS).join(', ')}`);\n};\n\nconst invalidType = (expected: string) => problem('INVALID_TYPE', `must be ${expected}`);\n\nconst checkString = (field: FieldDefinition, value: unknown): ScalarOutcome => {\n if (typeof value !== 'string') {\n return invalidType('a string');\n }\n switch (field.type) {\n case 'string': {\n const length = checkLength(value, field.settings);\n if (!length.ok || field.settings.pattern === undefined) {\n return length;\n }\n const pattern = compilePattern(field.settings.pattern);\n return pattern && !pattern.test(value)\n ? problem('PATTERN_MISMATCH', 'does not match the required pattern')\n : ok(value);\n }\n case 'text':\n return checkLength(value, field.settings);\n case 'slug':\n return SLUG_PATTERN.test(value)\n ? checkLength(value, field.settings)\n : problem('INVALID_FORMAT', 'must be lower-case words joined by hyphens');\n case 'uid':\n return UID_PATTERN.test(value)\n ? checkLength(value, field.settings)\n : problem('INVALID_FORMAT', 'may only contain letters, digits and _ . ~ -');\n case 'email':\n return EMAIL.test(value) && value.length <= MAX_EMAIL_LENGTH\n ? ok(value)\n : problem('INVALID_FORMAT', 'must be an email address');\n case 'url':\n return checkUrl(value, field.settings.protocols);\n default:\n return invalidType('a string');\n }\n};\n\n/** Checks and canonicalizes one present (non-missing) scalar value. */\nexport const checkScalar = (field: FieldDefinition, value: unknown): ScalarOutcome => {\n switch (field.type) {\n case 'string':\n case 'text':\n case 'slug':\n case 'uid':\n case 'email':\n case 'url':\n return checkString(field, value);\n case 'number':\n return typeof value === 'number' && Number.isFinite(value)\n ? checkRange(value, field.settings, compareNumber)\n : invalidType('a number');\n case 'integer':\n return Number.isSafeInteger(value)\n ? checkRange(value as number, field.settings, compareNumber)\n : invalidType('an integer');\n case 'decimal': {\n const decimal = canonicalDecimal(value);\n if (decimal === undefined) {\n return invalidType('a decimal string, e.g. \"12.50\"');\n }\n const { precision, scale } = decimalDigits(decimal);\n if (field.settings.scale !== undefined && scale > field.settings.scale) {\n return problem('INVALID_FORMAT', `must have at most ${field.settings.scale} decimal places`);\n }\n if (field.settings.precision !== undefined && precision > field.settings.precision) {\n return problem('INVALID_FORMAT', `must have at most ${field.settings.precision} digits`);\n }\n return checkRange(decimal, field.settings, compareDecimal);\n }\n case 'biginteger': {\n const big = canonicalBigInteger(value);\n return big === undefined\n ? invalidType('an integer string, e.g. \"9007199254740993\"')\n : checkRange(big, field.settings, compareBig);\n }\n case 'boolean':\n return typeof value === 'boolean' ? ok(value) : invalidType('true or false');\n case 'date': {\n const date = canonicalDate(value);\n return date === undefined\n ? invalidType('a date, YYYY-MM-DD')\n : checkRange(date, field.settings, compareText);\n }\n case 'datetime': {\n const datetime = canonicalDatetime(value);\n return datetime === undefined\n ? invalidType('an ISO-8601 date-time with a time zone, e.g. 2026-10-01T09:30:00Z')\n : checkRange(\n datetime,\n {\n min: canonicalDatetime(field.settings.min),\n max: canonicalDatetime(field.settings.max),\n },\n compareText,\n );\n }\n case 'time': {\n const time = canonicalTime(value);\n return time === undefined\n ? invalidType('a time, HH:mm[:ss[.sss]]')\n : checkRange(\n time,\n { min: canonicalTime(field.settings.min), max: canonicalTime(field.settings.max) },\n compareText,\n );\n }\n case 'enum': {\n const allowed = new Set(field.settings.values.map((entry) => entry.value));\n if (!field.settings.multiple) {\n return typeof value === 'string' && allowed.has(value)\n ? ok(value)\n : problem('NOT_ALLOWED', 'must be one of the allowed values');\n }\n if (!Array.isArray(value) || !value.every((item) => typeof item === 'string' && allowed.has(item))) {\n return problem('NOT_ALLOWED', 'must be a list of the allowed values');\n }\n return new Set(value).size === value.length ? ok(value) : problem('DUPLICATE', 'lists a value twice');\n }\n default:\n return invalidType(field.type);\n }\n};\n\n/** A single reference (relation target entry or media asset): a stable ID. */\nexport const isReferenceId = (value: unknown): value is string => isStableId(value);\n","import {\n isComponentDefinition,\n isEmptyRichText,\n richTextMediaIds,\n SCALAR_DATA_TYPES,\n type ComponentDefinition,\n type FieldDefinition,\n type SchemaDefinition,\n validateRichText,\n} from '@shapio/schema';\nimport type { ContentData } from '../../db/contentData.js';\nimport type { SchemaById } from '../../schema/snapshot.js';\nimport { findFieldByApiKey, type ContentModel } from '../model.js';\nimport { pointer, type ContentIssue } from './issues.js';\nimport { checkScalar, isReferenceId } from './scalars.js';\n\n/**\n * The content validator (build plan §4.E2). Built from the active model and the components it embeds,\n * compiled once per combination of schema revisions and cached. Values arrive keyed by API keys at the\n * boundary (`fromInput`) and are validated in storage form, keyed by stable field IDs (`validate`).\n *\n * Storage format of structured values:\n * - component: an object keyed by the component's field IDs (an array of them when repeatable);\n * - dynamic zone: an array of such objects, each with `__component` holding the component's stable ID\n * (its API key at the boundary);\n * - relation: the target entry ID (`one`) or an ordered array of IDs (`many`);\n * - media: an asset ID, or an array of them when `multiple`.\n * Missing values (absent, null, empty string, empty list, empty rich text) are not stored.\n * Keys that no field defines (values of removed fields) are preserved untouched (brief §5: field removal\n * keeps values until an explicit cleanup); deprecated fields' values are kept but not validated.\n */\nexport const COMPONENT_KEY = '__component';\n\nexport type RelationReference = { path: string; fieldId: string; targetModelId: string; entryIds: string[] };\n\n/** Media a document points at (media fields and rich-text images), checked against the library on write. */\nexport type MediaReferenceCheck = {\n path: string;\n assetIds: string[];\n allowedKinds: readonly string[] | null;\n};\n\nexport type ValidateOptions = {\n /** Autosave: everything but `required` is enforced, so half-finished drafts can be kept. */\n skipRequired?: boolean;\n};\n\nexport type ValidationOutcome = {\n data: ContentData;\n issues: ContentIssue[];\n relations: RelationReference[];\n media: MediaReferenceCheck[];\n};\n\nexport type ModelValidator = {\n model: ContentModel;\n /** Maps an API-keyed patch to storage form. Unknown or deprecated keys are reported, not dropped silently. */\n fromInput: (input: Readonly<Record<string, unknown>>) => { patch: ContentData; issues: ContentIssue[] };\n /** Validates and canonicalizes a complete document in storage form. */\n validate: (data: Readonly<ContentData>, options?: ValidateOptions) => ValidationOutcome;\n};\n\ntype Walk = ValidationOutcome & { options: ValidateOptions };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nexport const isMissingValue = (value: unknown): boolean =>\n value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);\n\nconst issue = (\n walk: { issues: ContentIssue[] },\n path: string,\n code: ContentIssue['code'],\n message: string,\n) => {\n walk.issues.push({ path, code, message });\n return undefined;\n};\n\ntype Components = ReadonlyMap<string, ComponentDefinition>;\n\nconst checkCount = (walk: Walk, path: string, count: number, settings: { min?: number; max?: number }) => {\n if (settings.min !== undefined && count < settings.min) {\n issue(walk, path, 'TOO_FEW', `needs at least ${settings.min} item(s)`);\n }\n if (settings.max !== undefined && count > settings.max) {\n issue(walk, path, 'TOO_MANY', `allows at most ${settings.max} item(s)`);\n }\n};\n\nconst checkReferences = (walk: Walk, path: string, value: unknown, multiple: boolean) => {\n const ids = multiple ? value : [value];\n if (!Array.isArray(ids) || !ids.every(isReferenceId)) {\n return issue(walk, path, 'INVALID_TYPE', multiple ? 'must be a list of IDs' : 'must be an ID');\n }\n if (new Set(ids).size !== ids.length) {\n return issue(walk, path, 'DUPLICATE', 'lists an ID twice');\n }\n return ids;\n};\n\nconst validateFields = (\n definition: SchemaDefinition,\n data: Readonly<Record<string, unknown>>,\n path: string,\n walk: Walk,\n components: Components,\n): Record<string, unknown> => {\n const known = new Set(definition.fields.map((field) => field.id));\n const output: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (!known.has(key) && key !== COMPONENT_KEY) {\n output[key] = value;\n }\n }\n for (const field of definition.fields) {\n const value = data[field.id];\n if (field.deprecated) {\n if (value !== undefined) {\n output[field.id] = value;\n }\n continue;\n }\n const checked = checkField(field, value, pointer(path, field.apiKey), walk, components);\n if (checked !== undefined) {\n output[field.id] = checked;\n }\n }\n return output;\n};\n\nconst checkComponentObject = (\n component: ComponentDefinition,\n value: unknown,\n path: string,\n walk: Walk,\n components: Components,\n) =>\n isRecord(value)\n ? validateFields(component, value, path, walk, components)\n : issue(walk, path, 'INVALID_TYPE', 'must be an object');\n\nconst checkStructured = (\n field: FieldDefinition,\n value: unknown,\n path: string,\n walk: Walk,\n components: Components,\n): unknown => {\n switch (field.type) {\n case 'component': {\n const component = components.get(field.settings.component);\n if (!component) {\n return issue(walk, path, 'UNKNOWN_COMPONENT', 'refers to a component that does not exist');\n }\n if (!field.settings.repeatable) {\n return checkComponentObject(component, value, path, walk, components);\n }\n if (!Array.isArray(value)) {\n return issue(walk, path, 'INVALID_TYPE', 'must be a list');\n }\n checkCount(walk, path, value.length, field.settings);\n return value.map((item, index) =>\n checkComponentObject(component, item, pointer(path, index), walk, components),\n );\n }\n case 'dynamiczone': {\n if (!Array.isArray(value)) {\n return issue(walk, path, 'INVALID_TYPE', 'must be a list');\n }\n checkCount(walk, path, value.length, field.settings);\n return value.map((item, index) => {\n const at = pointer(path, index);\n const componentId = isRecord(item) ? item[COMPONENT_KEY] : undefined;\n const component =\n typeof componentId === 'string' && field.settings.components.includes(componentId)\n ? components.get(componentId)\n : undefined;\n if (!component) {\n return issue(walk, pointer(at, COMPONENT_KEY), 'UNKNOWN_COMPONENT', 'is not allowed in this zone');\n }\n const fields = checkComponentObject(component, item, at, walk, components);\n return fields ? { [COMPONENT_KEY]: component.id, ...fields } : undefined;\n });\n }\n case 'relation': {\n const ids = checkReferences(walk, path, value, field.settings.cardinality === 'many');\n if (!ids) {\n return undefined;\n }\n if (field.settings.cardinality === 'many') {\n checkCount(walk, path, ids.length, field.settings);\n }\n walk.relations.push({ path, fieldId: field.id, targetModelId: field.settings.target, entryIds: ids });\n return value;\n }\n case 'media': {\n const ids = checkReferences(walk, path, value, field.settings.multiple);\n if (!ids) {\n return undefined;\n }\n if (field.settings.multiple) {\n checkCount(walk, path, ids.length, field.settings);\n }\n walk.media.push({ path, assetIds: ids, allowedKinds: field.settings.allowedKinds ?? null });\n return value;\n }\n case 'richtext': {\n const result = validateRichText(value);\n if (!result.ok) {\n walk.issues.push(\n ...result.problems.map((problem) => ({\n path: `${path}${problem.path}`,\n code: 'INVALID_RICHTEXT' as const,\n message: problem.message,\n })),\n );\n return undefined;\n }\n if (isEmptyRichText(result.document)) {\n // An empty editor is a missing value.\n return field.required && !walk.options.skipRequired\n ? issue(walk, path, 'REQUIRED', 'is required')\n : undefined;\n }\n const images = richTextMediaIds(result.document);\n if (images.length > 0) {\n walk.media.push({ path, assetIds: images, allowedKinds: ['image'] });\n }\n if (field.settings.maxLength !== undefined && result.textLength > field.settings.maxLength) {\n return issue(\n walk,\n path,\n 'TOO_LONG',\n `must be at most ${field.settings.maxLength} characters of text`,\n );\n }\n return result.document;\n }\n case 'json':\n return value;\n default:\n return issue(walk, path, 'INVALID_TYPE', `unsupported type ${field.type}`);\n }\n};\n\nfunction checkField(\n field: FieldDefinition,\n value: unknown,\n path: string,\n walk: Walk,\n components: Components,\n): unknown {\n if (isMissingValue(value)) {\n if (field.required && !walk.options.skipRequired) {\n issue(walk, path, 'REQUIRED', 'is required');\n }\n return undefined;\n }\n if (SCALAR_DATA_TYPES.has(field.type) || (field.type === 'enum' && field.settings.multiple)) {\n const outcome = checkScalar(field, value);\n return outcome.ok ? outcome.value : issue(walk, path, outcome.code, outcome.message);\n }\n return checkStructured(field, value, path, walk, components);\n}\n\n/** API keys → field IDs, recursively through components and dynamic zones. */\nconst mapInput = (\n definition: SchemaDefinition,\n input: Readonly<Record<string, unknown>>,\n path: string,\n issues: ContentIssue[],\n components: Components,\n): Record<string, unknown> => {\n const output: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(input)) {\n if (key === COMPONENT_KEY && isComponentDefinition(definition)) {\n continue;\n }\n const field = findFieldByApiKey(definition.fields, key);\n if (!field) {\n issues.push({\n path: pointer(path, key),\n code: 'UNKNOWN_FIELD',\n message: 'is not a field of this model',\n });\n continue;\n }\n output[field.id] = mapFieldInput(field, value, pointer(path, key), issues, components);\n }\n return output;\n};\n\nconst mapComponentInput = (\n component: ComponentDefinition | undefined,\n value: unknown,\n path: string,\n issues: ContentIssue[],\n components: Components,\n) => (component && isRecord(value) ? mapInput(component, value, path, issues, components) : value);\n\nfunction mapFieldInput(\n field: FieldDefinition,\n value: unknown,\n path: string,\n issues: ContentIssue[],\n components: Components,\n): unknown {\n if (field.type === 'component') {\n const component = components.get(field.settings.component);\n return field.settings.repeatable && Array.isArray(value)\n ? value.map((item, index) =>\n mapComponentInput(component, item, pointer(path, index), issues, components),\n )\n : mapComponentInput(component, value, path, issues, components);\n }\n if (field.type === 'dynamiczone' && Array.isArray(value)) {\n return value.map((item, index) => {\n const at = pointer(path, index);\n const apiKey = isRecord(item) ? item[COMPONENT_KEY] : undefined;\n const component = field.settings.components\n .map((id) => components.get(id))\n .find((candidate) => candidate?.apiKey === apiKey);\n if (!component) {\n issues.push({\n path: pointer(at, COMPONENT_KEY),\n code: 'UNKNOWN_COMPONENT',\n message: 'is not allowed in this zone',\n });\n return item as unknown;\n }\n return {\n [COMPONENT_KEY]: component.id,\n ...mapInput(component, item as Record<string, unknown>, at, issues, components),\n };\n });\n }\n return value;\n}\n\nconst compile = (model: ContentModel): ModelValidator => {\n const components: Components = new Map(\n [...model.components].map(([id, entry]) => [id, entry.definition] as const),\n );\n return {\n model,\n fromInput: (input) => {\n const issues: ContentIssue[] = [];\n return { patch: mapInput(model.definition, input, '', issues, components), issues };\n },\n validate: (data, options = {}) => {\n const walk: Walk = { data: {}, issues: [], relations: [], media: [], options };\n walk.data = validateFields(model.definition, data, '', walk, components);\n return { data: walk.data, issues: walk.issues, relations: walk.relations, media: walk.media };\n },\n };\n};\n\nconst MAX_CACHED_VALIDATORS = 500;\nconst cache = new Map<string, ModelValidator>();\n\n/** The cache key: the model's revision plus every embedded component's revision. */\nconst cacheKey = (snapshot: SchemaById, model: ContentModel) =>\n [\n model.revisionId,\n ...[...model.components.keys()].sort().map((id) => snapshot.byId.get(id)?.revisionId ?? id),\n ].join(':');\n\n/** The compiled validator for a model at the pinned snapshot, cached by schema revision IDs. */\nexport const buildValidator = (snapshot: SchemaById, model: ContentModel): ModelValidator => {\n const key = cacheKey(snapshot, model);\n let validator = cache.get(key);\n if (!validator) {\n if (cache.size >= MAX_CACHED_VALIDATORS) {\n cache.clear();\n }\n validator = compile(model);\n cache.set(key, validator);\n }\n return validator;\n};\n","/**\n * The database dialect this process talks to (ADR 0001, \"Dialect boundary\"). Chosen from DATABASE_URL:\n * `postgres://…`/`postgresql://…`, `mysql://…` or `sqlite:<path>` (`sqlite::memory:` for a throwaway\n * in-memory database).\n * The `db/sql` primitives and the content compiler read `currentDialect()` while building SQL, so it is set\n * once, when the process creates its database handle (`createDb`), before any query is built.\n */\nexport type DialectName = 'postgres' | 'sqlite' | 'mysql';\n\nconst SQLITE_PREFIX = 'sqlite:';\nconst MEMORY = ':memory:';\n\n/** The dialect a DATABASE_URL names, or undefined when it names none of them. */\nexport const dialectOfUrl = (url: string): DialectName | undefined => {\n if (url.startsWith(SQLITE_PREFIX)) {\n return 'sqlite';\n }\n if (url.startsWith('mysql://')) {\n return 'mysql';\n }\n return /^postgres(ql)?:\\/\\//.test(url) ? 'postgres' : undefined;\n};\n\nexport type SqliteLocation = { kind: 'memory' } | { kind: 'file'; path: string };\n\n/**\n * Where a `sqlite:` URL points: `sqlite::memory:` or `sqlite:<path>` (relative to the working directory or\n * absolute; `sqlite:///abs/path` is accepted too). Throws when the path is empty.\n */\nexport const sqliteLocationOfUrl = (url: string): SqliteLocation => {\n const rest = url.slice(SQLITE_PREFIX.length);\n if (rest === MEMORY) {\n return { kind: 'memory' };\n }\n const path = rest.startsWith('//') ? rest.slice(2) : rest;\n if (path === '') {\n throw new Error('A sqlite: DATABASE_URL needs a file path, e.g. sqlite:./shapio.db');\n }\n return { kind: 'file', path };\n};\n\n/** `host:port/database` (PostgreSQL, MySQL) or the file path (SQLite) from DATABASE_URL, never the credentials. */\nexport const describeDatabaseTarget = (connectionString: string): string => {\n if (dialectOfUrl(connectionString) === 'sqlite') {\n const location = sqliteLocationOfUrl(connectionString);\n return location.kind === 'memory' ? 'an in-memory database' : location.path;\n }\n try {\n const url = new URL(connectionString);\n const defaultPort = dialectOfUrl(connectionString) === 'mysql' ? '3306' : '5432';\n return `${url.hostname}:${url.port || defaultPort}${url.pathname}`;\n } catch {\n return 'the configured database';\n }\n};\n\nlet current: DialectName | undefined;\n\n/** The dialect queries are built for. PostgreSQL until a database handle says otherwise. */\nexport const currentDialect = (): DialectName => current ?? 'postgres';\n\n/**\n * Sets the process dialect. `createDb` calls it; one process talks to one dialect, so switching after a\n * handle exists is a bug (`force` is for unit tests that compile SQL for both dialects without a database).\n */\nexport const setCurrentDialect = (\n dialect: DialectName,\n { force = false }: { force?: boolean } = {},\n): void => {\n if (!force && current !== undefined && current !== dialect) {\n throw new Error(`This process already uses ${current}; it cannot also open a ${dialect} database`);\n }\n current = dialect;\n};\n\n/** Runs `fn` with the dialect switched (unit tests of SQL builders only), then restores the previous one. */\nexport const withDialect = <T>(dialect: DialectName, fn: () => T): T => {\n const previous = current;\n current = dialect;\n try {\n return fn();\n } finally {\n current = previous;\n }\n};\n\nexport const isSqlite = (): boolean => currentDialect() === 'sqlite';\n\nexport const isMysql = (): boolean => currentDialect() === 'mysql';\n","/**\n * On MySQL a field index is built on an invisible virtual column holding the field's value expression\n * (`content/compiler/dialect/mysql.ts`); the column is named after the index: `eh_<hash>` → `ev_<hash>`.\n */\nexport const fieldIndexColumnName = (indexName: string): string => indexName.replace(/^eh_/, 'ev_');\n","/** Content heads (package E). Expression indexes live on this table only. */\nexport const CONTENT_HEADS_TABLE = 'entry_heads';\n/** Columns of `entry_heads` the index layout depends on. Package E must keep these names. */\nexport const CONTENT_HEADS_COLUMNS = {\n data: 'data',\n siteId: 'site_id',\n modelId: 'model_id',\n locale: 'locale',\n state: 'state',\n} as const;\n","import { sql, type RawBuilder } from 'kysely';\nimport { toDatetimeText } from '../../../db/mysql/codec.js';\nimport { fieldIndexColumnName } from '../../../db/mysql/fieldIndexColumns.js';\nimport { CONTENT_HEADS_COLUMNS, CONTENT_HEADS_TABLE } from '../heads.js';\nimport type { ContentSqlDialect, FieldIndexDefinition, ListElement, ValueCast } from './types.js';\n\n/**\n * MySQL content SQL (ADR 0001, \"MySQL\"). `data` is a JSON column; there is no containment index and no\n * partial index, so:\n * - each filterable or sortable field gets an invisible virtual column holding its value expression and one\n * index `(site_id, model_id[, locale], state, column)`, built online (`ALGORITHM=INPLACE, LOCK=NONE`; a\n * functional index cannot be). Queries use the expression; MySQL matches it to the column for `=`, ranges\n * and ORDER BY (it does not for `<=>`, so equality is `expr = ? and expr is not null`: false, never null,\n * for a missing value, as containment is);\n * - values are read with `json_value`, which gives SQL NULL for JSON null (`->>` gives the text 'null');\n * text indexes and compares the first 255 characters (an index key is bounded), and equality with a longer\n * value also compares the whole text; numbers compare as DOUBLE, decimal and biginteger strings as\n * DECIMAL(65,30) (parameters are cast the same way, or MySQL does not use the column);\n * - text compares in the session's binary `utf8mb4_0900_bin` collation (case-sensitive, code point order),\n * matched with `locate`, so no LIKE escaping; `$containsi` and search fold both sides with `lower()`;\n * - list membership is `member of` (unindexed); bound lists are `in (?, …)`;\n * - JSON functions return `utf8mb4_bin` text, which MySQL refuses to compare with a literal in the session's\n * collation, so JSON is compared as JSON (`= cast('null' as json)`) or the literal's collation is named;\n * - NULL placement in sorts is written out: `(expr is null), expr`.\n */\nconst DATA = sql.ref(CONTENT_HEADS_COLUMNS.data);\n\n/** Longest text `json_value` returns; longer values read as NULL. */\nconst TEXT_LIMIT = 1_000_000;\n/** Characters of a text value that are indexed, compared in ranges and sorted. */\nexport const INDEXED_TEXT_LENGTH = 255;\n\n/**\n * The JSON path of a field. The `_utf8mb4` introducer gives the literal the character set's own collation\n * whatever the session's: MySQL stores a generated column's expression that way, and only matches a query's\n * expression to the column (to use its index) when every literal in it has the same collation.\n */\nconst path = (fieldId: string) => sql`_utf8mb4${sql.lit(`$.\"${fieldId}\"`)}`;\n\nconst fieldJson = (fieldId: string): RawBuilder<unknown> => sql`json_extract(${DATA}, ${path(fieldId)})`;\nconst fieldText = (fieldId: string): RawBuilder<unknown> =>\n sql`json_value(${DATA}, ${path(fieldId)} returning char(${sql.lit(TEXT_LIMIT)}))`;\n\n/** decimal and biginteger: exact to 35 integer and 30 fractional digits; other numbers are doubles. */\nconst DECIMAL = sql`decimal(65,30)`;\n\nconst fieldValue = (fieldId: string, cast: ValueCast, numericString = false): RawBuilder<unknown> => {\n switch (cast) {\n case 'numeric':\n return numericString\n ? sql`json_value(${DATA}, ${path(fieldId)} returning ${DECIMAL})`\n : sql`json_value(${DATA}, ${path(fieldId)} returning double)`;\n case 'boolean':\n return sql`json_value(${DATA}, ${path(fieldId)} returning char(5))`;\n default:\n return sql`left(${fieldText(fieldId)}, ${sql.lit(INDEXED_TEXT_LENGTH)})`;\n }\n};\n\nconst booleanText = (value: unknown) => (value === true || value === 'true' ? 'true' : 'false');\n\nconst castParameter = (value: unknown, cast: ValueCast, numericString = false): RawBuilder<unknown> => {\n switch (cast) {\n case 'numeric':\n return numericString ? sql`cast(${value} as ${DECIMAL})` : sql`cast(${value} as double)`;\n case 'boolean':\n return sql`${booleanText(value)}`;\n default:\n return sql`${value}`;\n }\n};\n\nconst listValue = (value: string, element: ListElement): string =>\n element === 'timestamp' ? toDatetimeText(value) : value;\n\n/** `expr = ? and expr is not null`: false (not null) for a missing value, and still index-served. */\nconst presentAndEqual = (expression: RawBuilder<unknown>, value: RawBuilder<unknown>) =>\n sql`(${expression} = ${value} and ${expression} is not null)`;\n\nconst textEquals = (fieldId: string, value: unknown): RawBuilder<unknown> => {\n const prefix = fieldValue(fieldId, 'text');\n if (typeof value === 'string' && [...value].length < INDEXED_TEXT_LENGTH) {\n return presentAndEqual(prefix, sql`${value}`);\n }\n return sql`(${prefix} = left(${value}, ${sql.lit(INDEXED_TEXT_LENGTH)}) and ${fieldText(fieldId)} = ${value})`;\n};\n\nconst COLUMN_TYPES: Readonly<Record<ValueCast, RawBuilder<unknown>>> = {\n text: sql`varchar(${sql.lit(INDEXED_TEXT_LENGTH)}) collate utf8mb4_0900_bin`,\n numeric: sql`double`,\n boolean: sql`varchar(5) collate utf8mb4_0900_bin`,\n};\n\nconst addIndexColumn = ({\n name,\n table,\n expression,\n cast,\n numericString,\n}: FieldIndexDefinition): RawBuilder<unknown> =>\n sql`alter table ${sql.table(table)} add column ${sql.id(fieldIndexColumnName(name))} ${numericString ? DECIMAL : COLUMN_TYPES[cast]}\n generated always as (${expression}) virtual invisible, algorithm=inplace, lock=none`;\n\nexport const mysqlContentDialect: ContentSqlDialect = {\n name: 'mysql',\n\n uuid: (value) => sql`${value}`,\n timestamp: (value) => sql`${toDatetimeText(value)}`,\n bool: (value) => (value ? sql`true` : sql`false`),\n oneOf: (column, values, element) =>\n values.length === 0\n ? sql<boolean>`false`\n : sql<boolean>`${column} in (${sql.join(values.map((value) => listValue(value, element)))})`,\n\n fieldValue,\n fieldText,\n fieldJson,\n castParameter,\n\n fieldEquals: (target, value) => {\n if (target.list) {\n // `is not null` first: false, not null, for a missing list (so `$nin` includes it, as on PostgreSQL).\n const json = fieldJson(target.fieldId);\n return sql`(${json} is not null and json_type(${json}) = 'ARRAY' collate utf8mb4_bin and ${value} member of (${json}))`;\n }\n if (target.numericString && target.cast) {\n // Numeric like PostgreSQL (\"12.5\" equals \"12.50\"), as DECIMAL(65,30).\n return sql`(${fieldValue(target.fieldId, target.cast, true)} = ${castParameter(value, target.cast, true)})`;\n }\n if (target.cast === null || target.cast === 'text') {\n return target.cast === null\n ? presentAndEqual(fieldText(target.fieldId), sql`${value}`)\n : textEquals(target.fieldId, value);\n }\n return presentAndEqual(fieldValue(target.fieldId, target.cast), castParameter(value, target.cast));\n },\n fieldMissing: (fieldId) => {\n const json = fieldJson(fieldId);\n return sql`(${json} is null or ${json} = cast('null' as json) or ${json} = cast('\"\"' as json) or ${json} = json_array())`;\n },\n textMatch: (fieldId, operator, text) => {\n const value = fieldText(fieldId);\n switch (operator) {\n case '$startsWith':\n return sql`(locate(${text}, ${value}) = 1)`;\n case '$endsWith':\n return sql`(right(${value}, ${sql.lit([...text].length)}) = ${text})`;\n case '$notContains':\n return sql`(locate(${text}, coalesce(${value}, '')) = 0)`;\n default:\n return sql`(locate(${text}, ${value}) > 0)`;\n }\n },\n containsInsensitive: (fieldId, text) => sql`(locate(lower(${text}), lower(${fieldText(fieldId)})) > 0)`,\n\n localeRank: (chain, column) =>\n sql`(case ${column} ${sql.join(\n chain.map((locale, index) => sql`when ${locale} then ${sql.lit(index)}`),\n sql` `,\n )} end)`,\n sortDirection: (direction) => (direction === 'desc' ? sql`desc` : sql`asc`),\n sortTerm: (expression, direction, nullable) => {\n const order = direction === 'desc' ? sql`desc` : sql`asc`;\n // PostgreSQL's default: nulls sort as the largest value. MySQL sorts them as the smallest.\n return nullable\n ? sql`(${expression} is null) ${order}, ${expression} ${order}`\n : sql`${expression} ${order}`;\n },\n snapshotCte: ({ name, siteId, modelId, seq }) => sql`with ${sql.id(name)} as (\n select pl.entry_id, pl.site_id, pl.model_id, pl.locale, 'published' as state, r.data, 0 as version,\n r.id as revision_id, pl.published_at as updated_at, null as autosaved_at,\n en.created_at as entry_created_at\n from publication_log pl\n join content_revisions r on r.id = pl.revision_id\n join entries en on en.id = pl.entry_id\n where pl.site_id = ${siteId} and pl.model_id = ${modelId}\n and pl.from_seq <= ${seq}\n and (pl.to_seq is null or pl.to_seq > ${seq})\n ) `,\n\n fieldIndexColumn: {\n add: addIndexColumn,\n drop: (name) =>\n sql`alter table ${sql.table(CONTENT_HEADS_TABLE)} drop column ${sql.id(fieldIndexColumnName(name))}, algorithm=inplace, lock=none`,\n },\n createFieldIndex: ({ name, table, localized, modelIdColumn }) => {\n const { siteId, locale, state } = CONTENT_HEADS_COLUMNS;\n const leading = localized\n ? sql`${sql.ref(siteId)}, ${sql.ref(modelIdColumn)}, ${sql.ref(locale)}, ${sql.ref(state)}`\n : sql`${sql.ref(siteId)}, ${sql.ref(modelIdColumn)}, ${sql.ref(state)}`;\n return sql`create index ${sql.id(name)} on ${sql.table(table)} (${leading}, ${sql.id(fieldIndexColumnName(name))}) algorithm=inplace lock=none`;\n },\n createFieldStatistics: () => null,\n dropFieldIndex: (name) =>\n sql`drop index ${sql.id(name)} on ${sql.table(CONTENT_HEADS_TABLE)} algorithm=inplace lock=none`,\n dropFieldStatistics: () => null,\n analyze: (table) => sql`analyze table ${sql.table(table)}`,\n};\n","import { sql, type RawBuilder } from 'kysely';\nimport { CONTENT_HEADS_COLUMNS } from '../heads.js';\nimport type { ContentSqlDialect, ListElement, TextMatchOperator, ValueCast } from './types.js';\n\n/**\n * PostgreSQL content SQL (ADR 0001): JSONB with a GIN `jsonb_path_ops` index serving containment, so\n * equality on a field without an index is emitted as `data @> {...}`; ranges, sorts and equality on indexed\n * fields (filterable or sortable) use per-field B-tree expression indexes built `CONCURRENTLY` with a\n * matching `CREATE STATISTICS` object.\n */\nconst DATA = sql.ref(CONTENT_HEADS_COLUMNS.data);\n\nconst ARRAY_TYPES: Readonly<Record<ListElement, RawBuilder<unknown>>> = {\n uuid: sql`uuid[]`,\n text: sql`text[]`,\n timestamp: sql`timestamptz[]`,\n};\n\nconst MISSING_JSON = sql`('null'::jsonb, '\"\"'::jsonb, '[]'::jsonb)`;\n\n/** LIKE escaping (the default `\\` escape character). */\nexport const escapeLike = (text: string) => text.replace(/[\\\\%_]/g, (char) => `\\\\${char}`);\n\nconst likePattern = (operator: TextMatchOperator | '$containsi', text: string) => {\n const escaped = escapeLike(text);\n switch (operator) {\n case '$startsWith':\n return `${escaped}%`;\n case '$endsWith':\n return `%${escaped}`;\n default:\n return `%${escaped}%`;\n }\n};\n\nconst fieldText = (fieldId: string): RawBuilder<unknown> => sql`(${DATA} ->> ${sql.lit(fieldId)})`;\n\nconst fieldValue = (fieldId: string, cast: ValueCast): RawBuilder<unknown> => {\n const text = fieldText(fieldId);\n switch (cast) {\n case 'numeric':\n return sql`(${text}::numeric)`;\n case 'boolean':\n return sql`(${text}::boolean)`;\n default:\n return text;\n }\n};\n\nconst castParameter = (value: unknown, cast: ValueCast): RawBuilder<unknown> => {\n switch (cast) {\n case 'numeric':\n return sql`${value}::numeric`;\n case 'boolean':\n return sql`${value}::boolean`;\n default:\n return sql`${value}::text`;\n }\n};\n\n/**\n * `data @> $1::jsonb`: the only form the GIN index serves. The document is a parameter, field IDs included,\n * so nothing from the request is ever spliced into the SQL text.\n */\nexport const containment = (document: Readonly<Record<string, unknown>>): RawBuilder<unknown> =>\n sql`(${DATA} @> ${JSON.stringify(document)}::jsonb)`;\n\nexport const postgresContentDialect: ContentSqlDialect = {\n name: 'postgres',\n\n uuid: (value) => sql`${value}::uuid`,\n timestamp: (value) => sql`${value}::timestamptz`,\n bool: (value) => sql`${value}::boolean`,\n oneOf: (column, values, element) => sql<boolean>`${column} = any(${[...values]}::${ARRAY_TYPES[element]})`,\n\n fieldValue,\n fieldText,\n fieldJson: (fieldId) => sql`(${DATA} -> ${sql.lit(fieldId)})`,\n castParameter,\n\n fieldEquals: (target, value) => {\n if (target.numericString && target.cast) {\n // \"12.5\" and \"12.50\" are equal numbers but different JSON strings: compare numerically.\n return sql`(${fieldValue(target.fieldId, target.cast)} = ${castParameter(value, target.cast)})`;\n }\n if (target.indexed && target.cast) {\n // The index expression (and its statistics) serve the filter; a GIN containment match must recheck,\n // and decompress, every candidate row. `is not null` keeps it false, never null, for a missing value,\n // like containment, so `$ne`/`$nin`/`$not` still include missing values (ADR 0001 amendment).\n const expression = fieldValue(target.fieldId, target.cast);\n return sql`(${expression} = ${castParameter(value, target.cast)} and ${expression} is not null)`;\n }\n return containment({ [target.fieldId]: target.list ? [value] : value });\n },\n fieldMissing: (fieldId) => {\n const json = postgresContentDialect.fieldJson(fieldId);\n return sql`(${json} is null or ${json} in ${MISSING_JSON})`;\n },\n textMatch: (fieldId, operator, text) =>\n operator === '$notContains'\n ? sql`(coalesce(${fieldText(fieldId)}, '') not like ${likePattern(operator, text)})`\n : sql`(${fieldText(fieldId)} like ${likePattern(operator, text)})`,\n containsInsensitive: (fieldId, text) =>\n sql`(${fieldText(fieldId)} ilike ${likePattern('$containsi', text)})`,\n\n localeRank: (chain, column) => sql`array_position(${[...chain]}::text[], ${column})`,\n sortDirection: (direction) => (direction === 'desc' ? sql`desc` : sql`asc`),\n snapshotCte: ({ name, siteId, modelId, seq }) => sql`with ${sql.id(name)} as (\n select pl.entry_id, pl.site_id, pl.model_id, pl.locale, 'published'::text as state, r.data, 0 as version,\n r.id as revision_id, pl.published_at as updated_at, null::timestamptz as autosaved_at,\n en.created_at as entry_created_at\n from publication_log pl\n join content_revisions r on r.id = pl.revision_id\n join entries en on en.id = pl.entry_id\n where pl.site_id = ${siteId}::uuid and pl.model_id = ${modelId}\n and pl.from_seq <= ${seq}::bigint\n and (pl.to_seq is null or pl.to_seq > ${seq}::bigint)\n ) `,\n\n createFieldIndex: ({ name, table, leading, expression, modelIdColumn, modelId }) =>\n sql`create index concurrently if not exists ${sql.id(name)} on ${sql.table(table)} (${leading}, ${expression}) where ${sql.ref(modelIdColumn)} = ${modelId}`,\n createFieldStatistics: (name, expression, table) =>\n sql`create statistics if not exists ${sql.id(name)} on ${expression} from ${sql.table(table)}`,\n dropFieldIndex: (name) => sql`drop index concurrently if exists ${sql.id(name)}`,\n dropFieldStatistics: (name) => sql`drop statistics if exists ${sql.id(name)}`,\n analyze: (table) => sql`analyze ${sql.table(table)}`,\n};\n","import { sql, type RawBuilder } from 'kysely';\nimport { toStoredTimestamp } from '../../../db/sql/time.js';\nimport { CONTENT_HEADS_COLUMNS } from '../heads.js';\nimport type { ContentSqlDialect, ListElement, ValueCast } from './types.js';\n\n/**\n * SQLite content SQL (ADR 0001, \"D2: as built\"). `data` is JSON text read with JSON1's `->`/`->>`; there is\n * no GIN index and no containment, so:\n * - one partial expression index per filterable or sortable field serves equality, ranges and sorts, and\n * scalar equality is emitted on that same expression (`expr is ?`, which the planner treats like `=` and\n * which is false, never null, for a missing value, as containment is);\n * - list membership is `json_each` (unindexed; the model's heads are scanned);\n * - LIKE folds ASCII case, so case-sensitive matches use `instr`/`substr`, and case-insensitive ones use\n * `shapio_fold` (JavaScript `toLowerCase`, a deterministic function registered on every connection by\n * the database layer) on the column and the same folding of the parameter in JavaScript;\n * - there are no `::` casts and no array parameters: lists are bound as one JSON text parameter, and\n * timestamps as the stored text (`toStoredTimestamp`), which compares as time;\n * - indexes are built with a plain `CREATE INDEX` (no `CONCURRENTLY`: the build holds the single write lock\n * until it finishes) and there is no `CREATE STATISTICS`.\n */\nconst DATA = sql.ref(CONTENT_HEADS_COLUMNS.data);\n\n/** The name of the case-folding SQL function the database layer registers on each SQLite connection. */\nexport const FOLD_FUNCTION = 'shapio_fold';\n/** What `shapio_fold` computes, for the parameter side (and the function's registration). */\nexport const foldText = (text: string): string => text.toLowerCase();\n\nconst listParameter = (values: readonly string[], element: ListElement): string =>\n JSON.stringify(element === 'timestamp' ? values.map((value) => toStoredTimestamp(value)) : [...values]);\n\nconst fieldText = (fieldId: string): RawBuilder<unknown> => sql`(${DATA} ->> ${sql.lit(fieldId)})`;\nconst fieldJson = (fieldId: string): RawBuilder<unknown> => sql`(${DATA} -> ${sql.lit(fieldId)})`;\n\n/** Booleans extract as 1/0 and numbers as numbers; decimal and biginteger strings need the cast. */\nconst fieldValue = (fieldId: string, cast: ValueCast): RawBuilder<unknown> =>\n cast === 'numeric' ? sql`cast(${fieldText(fieldId)} as numeric)` : fieldText(fieldId);\n\nconst castParameter = (value: unknown, cast: ValueCast): RawBuilder<unknown> => {\n switch (cast) {\n case 'numeric':\n return sql`cast(${value} as numeric)`;\n case 'boolean':\n return sql`${value === true || value === 'true' ? 1 : 0}`;\n default:\n return sql`${value}`;\n }\n};\n\nexport const sqliteContentDialect: ContentSqlDialect = {\n name: 'sqlite',\n\n uuid: (value) => sql`${value}`,\n timestamp: (value) => sql`${toStoredTimestamp(value)}`,\n bool: (value) => (value ? sql`true` : sql`false`),\n oneOf: (column, values, element) =>\n sql<boolean>`${column} in (select value from json_each(${listParameter(values, element)}))`,\n\n fieldValue,\n fieldText,\n fieldJson,\n castParameter,\n\n fieldEquals: (target, value) => {\n if (target.list) {\n const json = fieldJson(target.fieldId);\n return sql`(json_type(${json}) = 'array' and exists (select 1 from json_each(${json}) where value = ${value}))`;\n }\n if (target.numericString && target.cast) {\n // Numeric like PostgreSQL (\"12.5\" equals \"12.50\"); exact to about 15 significant digits.\n return sql`(${fieldValue(target.fieldId, target.cast)} = ${castParameter(value, target.cast)})`;\n }\n const expression = target.cast ? fieldValue(target.fieldId, target.cast) : fieldText(target.fieldId);\n return sql`(${expression} is ${castParameter(value, target.cast ?? 'text')})`;\n },\n fieldMissing: (fieldId) => {\n const json = fieldJson(fieldId);\n return sql`(${json} is null or ${json} in ('null', '\"\"', '[]'))`;\n },\n textMatch: (fieldId, operator, text) => {\n const value = fieldText(fieldId);\n switch (operator) {\n case '$startsWith':\n return sql`(instr(${value}, ${text}) = 1)`;\n case '$endsWith':\n return sql`(substr(${value}, -length(${text})) = ${text})`;\n case '$notContains':\n return sql`(instr(coalesce(${value}, ''), ${text}) = 0)`;\n default:\n return sql`(instr(${value}, ${text}) > 0)`;\n }\n },\n containsInsensitive: (fieldId, text) =>\n sql`(instr(${sql.raw(FOLD_FUNCTION)}(${fieldText(fieldId)}), ${foldText(text)}) > 0)`,\n\n localeRank: (chain, column) =>\n sql`(case ${column} ${sql.join(\n chain.map((locale, index) => sql`when ${locale} then ${sql.lit(index)}`),\n sql` `,\n )} end)`,\n sortDirection: (direction, nullable) => {\n if (!nullable) {\n return direction === 'desc' ? sql`desc` : sql`asc`;\n }\n // PostgreSQL's default: nulls sort as the largest value.\n return direction === 'desc' ? sql`desc nulls first` : sql`asc nulls last`;\n },\n snapshotCte: ({ name, siteId, modelId, seq }) => sql`with ${sql.id(name)} as (\n select pl.entry_id, pl.site_id, pl.model_id, pl.locale, 'published' as state, r.data, 0 as version,\n r.id as revision_id, pl.published_at as updated_at, null as autosaved_at,\n en.created_at as entry_created_at\n from publication_log pl\n join content_revisions r on r.id = pl.revision_id\n join entries en on en.id = pl.entry_id\n where pl.site_id = ${siteId} and pl.model_id = ${modelId}\n and pl.from_seq <= ${seq}\n and (pl.to_seq is null or pl.to_seq > ${seq})\n ) `,\n\n createFieldIndex: ({ name, table, leading, expression, modelIdColumn, modelId }) =>\n sql`create index if not exists ${sql.id(name)} on ${sql.table(table)} (${leading}, ${expression}) where ${sql.ref(modelIdColumn)} = ${modelId}`,\n createFieldStatistics: () => null,\n dropFieldIndex: (name) => sql`drop index if exists ${sql.id(name)}`,\n dropFieldStatistics: () => null,\n analyze: (table) => sql`analyze ${sql.table(table)}`,\n};\n","import { mysqlContentDialect } from './mysql.js';\nimport { postgresContentDialect } from './postgres.js';\nimport { sqliteContentDialect } from './sqlite.js';\nimport type { ContentDialectName, ContentSqlDialect } from './types.js';\n\nexport type { ContentDialectName, ContentSqlDialect } from './types.js';\n\nconst DIALECTS: Readonly<Record<ContentDialectName, ContentSqlDialect>> = {\n postgres: postgresContentDialect,\n sqlite: sqliteContentDialect,\n mysql: mysqlContentDialect,\n};\n\nexport const contentDialectFor = (name: ContentDialectName): ContentSqlDialect => DIALECTS[name];\n","import { currentDialect } from '../../db/dialect.js';\nimport { contentDialectFor } from './dialect/index.js';\nimport type { ContentSqlDialect } from './dialect/types.js';\n\n/**\n * The content dialect of this process's database (`db/dialect.ts`, chosen from `DATABASE_URL`). Every\n * compiler function takes a dialect as an optional last argument that defaults to this one.\n */\nexport const contentDialect = (): ContentSqlDialect => contentDialectFor(currentDialect());\n","import type { FieldDefinition } from '@shapio/schema';\nimport { sql, type RawBuilder } from 'kysely';\nimport { AppError } from '../../helpers/appError.js';\nimport type { FieldMask, Policy, Principal, RowFilter } from '../../permissions/types.js';\nimport { contentDialect } from './currentDialect.js';\nimport type { ContentSqlDialect } from './dialect/types.js';\n\n/**\n * Policies in content queries (ADR 0005): row filters compile to SQL here (and only here); masks decide\n * which fields a caller sees, may filter on and may write. Row-filter conditions are an enumerated set,\n * never free-form expressions.\n */\n\n/** Entry columns row conditions read. `e` is the entries alias of every head query. */\nconst OWNER_COLUMNS = { admin: 'e.created_by_admin_id', appUser: 'e.owner_app_user_id' } as const;\n\nexport const compileRowFilter = (\n filter: RowFilter | null,\n principal: Principal,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null => {\n if (!filter) {\n return null;\n }\n switch (filter.kind) {\n case 'ownedByPrincipal':\n if (principal.kind === 'admin') {\n return sql`${sql.ref(OWNER_COLUMNS.admin)} = ${dialect.uuid(principal.adminUserId)}`;\n }\n if (principal.kind === 'appUser') {\n return sql`${sql.ref(OWNER_COLUMNS.appUser)} = ${dialect.uuid(principal.appUserId)}`;\n }\n // Tokens and anonymous callers own nothing.\n return sql`false`;\n case 'and':\n case 'or': {\n const parts = filter.filters.map((child) => compileRowFilter(child, principal, dialect) ?? sql`true`);\n if (parts.length === 0) {\n return dialect.bool(filter.kind === 'and');\n }\n return sql`(${sql.join(parts, filter.kind === 'and' ? sql` and ` : sql` or `)})`;\n }\n }\n};\n\nexport type EntryOwnership = { created_by_admin_id: string | null; owner_app_user_id: string | null };\n\n/** The same row filter evaluated on one loaded entry (writes check it before touching the entry). */\nexport const matchesRowFilter = (\n filter: RowFilter | null,\n principal: Principal,\n entry: EntryOwnership,\n): boolean => {\n if (!filter) {\n return true;\n }\n switch (filter.kind) {\n case 'ownedByPrincipal':\n return (\n (principal.kind === 'admin' && entry.created_by_admin_id === principal.adminUserId) ||\n (principal.kind === 'appUser' && entry.owner_app_user_id === principal.appUserId)\n );\n case 'and':\n return filter.filters.every((child) => matchesRowFilter(child, principal, entry));\n case 'or':\n return filter.filters.some((child) => matchesRowFilter(child, principal, entry));\n }\n};\n\n/**\n * Whether a mask lets the caller see a field. A wildcard mask covers every live field; deprecated fields\n * stay hidden unless a mask names them explicitly.\n */\nexport const maskAllows = (mask: FieldMask, field: FieldDefinition): boolean =>\n mask.mode === 'all' ? !field.deprecated : mask.fieldIds.includes(field.id);\n\nexport const forbidden = (message: string, details?: Record<string, unknown>) =>\n new AppError(403, 'FORBIDDEN', message, details);\n\n/** Throws 403 unless the policy allows the action. */\nexport const assertAllowed = (policy: Policy, what: string): void => {\n if (!policy.allowed) {\n throw forbidden(`Your role does not allow ${what}`);\n }\n};\n\n/** Writes may only touch fields in the write mask (ADR 0005: 403, never a silent drop). */\nexport const assertWritable = (\n mask: FieldMask,\n fields: readonly FieldDefinition[],\n fieldIds: Iterable<string>,\n) => {\n const byId = new Map(fields.map((field) => [field.id, field]));\n const denied = [...fieldIds].filter((id) => {\n const field = byId.get(id);\n return field !== undefined && !maskAllows(mask, field);\n });\n if (denied.length > 0) {\n throw new AppError(403, 'FORBIDDEN_FIELD', 'Your role may not change some of these fields', {\n fields: denied.map((id) => byId.get(id)?.apiKey ?? id),\n });\n }\n};\n\n/**\n * One condition over the entries row `e` for reads spanning several models (the snapshot diff): an entry\n * passes when its model is readable without a row filter, or its model's row filter holds for it.\n */\nexport const compileModelRowFilters = (\n models: ReadonlyArray<{ modelId: string; rowFilter: RowFilter | null }>,\n principal: Principal,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null => {\n if (models.every((model) => model.rowFilter === null)) {\n return null;\n }\n const open = models.filter((model) => model.rowFilter === null).map((model) => model.modelId);\n const parts = [\n ...(open.length > 0 ? [dialect.oneOf(sql`e.model_id`, open, 'uuid')] : []),\n ...models.flatMap((model) =>\n model.rowFilter === null\n ? []\n : [\n sql`(e.model_id = ${dialect.uuid(model.modelId)} and ${compileRowFilter(model.rowFilter, principal, dialect) ?? sql`true`})`,\n ],\n ),\n ];\n return sql`(${sql.join(parts, sql` or `)})`;\n};\n","import {\n renderRichTextHtml,\n type ComponentDefinition,\n type FieldDefinition,\n type RichTextDocument,\n} from '@shapio/schema';\nimport type { FieldMask } from '../../permissions/types.js';\nimport type { ContentModel } from '../model.js';\nimport { COMPONENT_KEY } from '../validator/index.js';\nimport { maskAllows } from './policy.js';\nimport type { RichTextMode } from './types.js';\n\n/**\n * Storage → API projection with explicit masks (build plan §3.10: generic content routes cannot carry\n * fixed response schemas, so this is where leaks are prevented). Only fields the mask allows, and the\n * caller selected, are emitted; field IDs become API keys; relation IDs are limited to targets the caller\n * may see (published, readable) unless they were populated. Absent values are emitted as null (or [] for\n * lists) so every entry of a model has the same shape.\n */\n\n/** The top-level fields a response carries: readable, live (unless named by the mask) and selected. */\nexport const selectFields = (\n model: ContentModel,\n mask: FieldMask,\n selected: readonly FieldDefinition[] | null,\n): FieldDefinition[] => {\n const allowed = model.definition.fields.filter((field) => maskAllows(mask, field));\n return selected ? allowed.filter((field) => selected.some((choice) => choice.id === field.id)) : allowed;\n};\n\nexport type ProjectionOptions = {\n model: ContentModel;\n fields: readonly FieldDefinition[];\n /** Relation targets the caller may see; null shows every ID (admin reads). */\n visibleTargets: ReadonlySet<string> | null;\n /** Expanded targets: relation field ID → target entry ID → projected entry. */\n populated: ReadonlyMap<string, ReadonlyMap<string, unknown>>;\n /**\n * Delivery reads: the shape of rich-text values (`?richText=`), each also carrying its HTML rendering behind\n * `RICH_TEXT_HTML` for GraphQL's `html` field. Absent: the stored document as is (admin reads, hooks).\n */\n richText?: RichTextMode;\n /** Asset views by ID (media fields become views, missing assets are dropped); null leaves IDs as stored. */\n mediaAssets: ReadonlyMap<string, { url: string }> | null;\n};\n\n/**\n * Renders a delivered rich-text value's HTML on demand (GraphQL's `RichText.html` resolver). A symbol key, so\n * `JSON.stringify` never writes it: REST responses carry `html` only when the request asked for it.\n */\nexport const RICH_TEXT_HTML: unique symbol = Symbol('shapio.richTextHtml');\n\nexport type DeliveredRichText = Record<string, unknown> & { [RICH_TEXT_HTML]?: () => string };\n\nconst projectRichText = (\n document: RichTextDocument,\n mode: RichTextMode,\n options: ProjectionOptions,\n): DeliveredRichText => {\n const render = () => renderRichTextHtml(document, (id) => options.mediaAssets?.get(id)?.url);\n const { doc, ...envelope } = document;\n const shaped: DeliveredRichText =\n mode === 'json'\n ? { ...document }\n : mode === 'html'\n ? { ...envelope, html: render() }\n : { ...envelope, doc, html: render() };\n shaped[RICH_TEXT_HTML] = render;\n return shaped;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isListField = (field: FieldDefinition) =>\n (field.type === 'relation' && field.settings.cardinality === 'many') ||\n (field.type === 'media' && field.settings.multiple) ||\n (field.type === 'component' && field.settings.repeatable) ||\n field.type === 'dynamiczone';\n\nconst projectRelation = (\n field: FieldDefinition & { type: 'relation' },\n value: unknown,\n options: ProjectionOptions,\n topLevel: boolean,\n) => {\n const ids = (Array.isArray(value) ? value : [value]).filter((id): id is string => typeof id === 'string');\n const populated = topLevel ? options.populated.get(field.id) : undefined;\n const shown = ids.flatMap((id) => {\n if (populated) {\n return populated.has(id) ? [populated.get(id)] : [];\n }\n return options.visibleTargets === null || options.visibleTargets.has(id) ? [id] : [];\n });\n return field.settings.cardinality === 'many' ? shown : (shown[0] ?? null);\n};\n\nconst projectComponent = (\n component: ComponentDefinition | undefined,\n value: unknown,\n options: ProjectionOptions,\n): Record<string, unknown> | null =>\n component && isRecord(value)\n ? projectFields(\n component.fields.filter((field) => !field.deprecated),\n value,\n options,\n false,\n )\n : null;\n\nconst isStringOrStrings = (value: unknown) =>\n typeof value === 'string' || (Array.isArray(value) && value.every((item) => typeof item === 'string'));\n\n/**\n * Whether a stored value has the shape the field's current type produces. Old snapshots are served through\n * the current schema (plan developer-face §5): a value written before a type change (text → rich text) is\n * not converted at read time, it is emitted as empty instead of being passed off as the new type.\n */\nconst matchesFieldType = (field: FieldDefinition, value: unknown): boolean => {\n switch (field.type) {\n case 'number':\n case 'integer':\n return typeof value === 'number';\n case 'boolean':\n return typeof value === 'boolean';\n case 'json':\n return true;\n case 'richtext':\n return isRecord(value) && isRecord(value.doc);\n case 'relation':\n case 'media':\n return isStringOrStrings(value);\n case 'enum':\n return field.settings.multiple\n ? Array.isArray(value) && value.every((item) => typeof item === 'string')\n : typeof value === 'string';\n case 'component':\n return field.settings.repeatable ? Array.isArray(value) : isRecord(value);\n case 'dynamiczone':\n return Array.isArray(value);\n default:\n // string, text, slug, email, url, uid, date, datetime, time, decimal, biginteger\n return typeof value === 'string';\n }\n};\n\nconst projectValue = (\n field: FieldDefinition,\n value: unknown,\n options: ProjectionOptions,\n topLevel: boolean,\n): unknown => {\n if (value === undefined || value === null || !matchesFieldType(field, value)) {\n return isListField(field) ? [] : null;\n }\n switch (field.type) {\n case 'relation':\n return projectRelation(field, value, options, topLevel);\n case 'component': {\n const component = options.model.components.get(field.settings.component)?.definition;\n return field.settings.repeatable\n ? (Array.isArray(value) ? value : []).map((item) => projectComponent(component, item, options))\n : projectComponent(component, value, options);\n }\n case 'dynamiczone':\n return (Array.isArray(value) ? value : []).flatMap((item) => {\n const component = isRecord(item)\n ? options.model.components.get(String(item[COMPONENT_KEY]))?.definition\n : undefined;\n const projected = projectComponent(component, item, options);\n return component && projected ? [{ [COMPONENT_KEY]: component.apiKey, ...projected }] : [];\n });\n case 'richtext':\n return options.richText ? projectRichText(value as RichTextDocument, options.richText, options) : value;\n case 'media': {\n if (!options.mediaAssets) {\n return value;\n }\n const assets = (Array.isArray(value) ? value : [value]).flatMap((id) => {\n const asset = typeof id === 'string' ? options.mediaAssets?.get(id) : undefined;\n return asset ? [asset] : [];\n });\n return field.settings.multiple ? assets : (assets[0] ?? null);\n }\n default:\n return value;\n }\n};\n\nfunction projectFields(\n fields: readonly FieldDefinition[],\n data: Readonly<Record<string, unknown>>,\n options: ProjectionOptions,\n topLevel: boolean,\n): Record<string, unknown> {\n const output: Record<string, unknown> = {};\n for (const field of fields) {\n output[field.apiKey] = projectValue(field, data[field.id], options, topLevel);\n }\n return output;\n}\n\n/** A head's data as the API returns it. */\nexport const projectData = (data: Readonly<Record<string, unknown>>, options: ProjectionOptions) =>\n projectFields(options.fields, data, options, true);\n","import type { FieldDefinition } from '@shapio/schema';\nimport { selectFields } from '../content/compiler/select.js';\nimport type { PopulateTree } from '../content/compiler/types.js';\nimport { resolveModelById, type ContentModel } from '../content/model.js';\nimport type { Policy } from '../permissions/types.js';\nimport type { SchemaSnapshot } from '../schema/snapshot.js';\nimport type { FieldRead } from './aggregator.js';\nimport { relationFieldPath } from './keys.js';\n\n/**\n * The fields a REST delivery response carried, as usage reads: the projected top-level fields (`explicit`\n * when the request named them with `fields`, `implicit` when it took the whole model) and, one level deep,\n * the fields of populated relation targets (always `implicit`: populate expands whole targets).\n */\nexport type RestReadShape = {\n snapshot: SchemaSnapshot;\n model: ContentModel;\n policy: Policy;\n query: { fields: readonly FieldDefinition[] | null; populate: PopulateTree };\n /** The caller's read policy for a relation's target model. */\n policyFor: (modelId: string) => Promise<Policy>;\n};\n\nexport const restFieldReads = async ({\n snapshot,\n model,\n policy,\n query,\n policyFor,\n}: RestReadShape): Promise<FieldRead[]> => {\n const fields = selectFields(model, policy.readMask, query.fields);\n const selection = query.fields === null ? 'implicit' : 'explicit';\n const reads: FieldRead[] = fields.map((field) => ({ path: field.id, selection }));\n for (const field of fields) {\n if (field.type !== 'relation' || !query.populate.has(field.id)) {\n continue;\n }\n const target = resolveModelById(snapshot, field.settings.target);\n if (!target) {\n continue;\n }\n const targetPolicy = await policyFor(target.definition.id);\n if (!targetPolicy.allowed) {\n continue;\n }\n for (const targetField of selectFields(target, targetPolicy.readMask, null)) {\n reads.push({ path: relationFieldPath(field.id, targetField.id), selection: 'implicit' });\n }\n }\n return reads;\n};\n","import { createHash } from 'node:crypto';\nimport { isStableId, SCALAR_DATA_TYPES, type DataType, type FieldDefinition } from '@shapio/schema';\nimport { sql, type RawBuilder } from 'kysely';\nimport { contentDialect } from './currentDialect.js';\nimport type {\n ContentSqlDialect,\n EqualityTarget,\n FieldIndexDefinition,\n TextMatchOperator,\n ValueCast,\n} from './dialect/types.js';\nimport { CONTENT_HEADS_COLUMNS, CONTENT_HEADS_TABLE } from './heads.js';\n\n/**\n * The single source of SQL expressions over content values (ADR 0001). The query compiler (package E)\n * and the index builder (schema planner) both call these functions, so an expression index matches the\n * filter or sort that should use it byte for byte. Package E extends this module; never duplicate it.\n *\n * Rules (build plan §3.1):\n * - date, datetime and time are canonical UTC ISO-8601 text: compared and sorted as text, because\n * timestamp casts are not IMMUTABLE and cannot be indexed;\n * - decimal and biginteger are JSON strings cast to numeric; number and integer are JSON numbers cast\n * to numeric (text→numeric is IMMUTABLE, so it can be indexed);\n * - booleans cast to boolean; every other scalar is the raw text value.\n *\n * The spelling of each expression comes from the content dialect (`dialect/`): the same functions build\n * the query and the index on PostgreSQL, and the same functions build them on SQLite.\n *\n * Field IDs are embedded as SQL literals, never parameters, because a parameterised expression cannot\n * match an index expression. That is safe only because field IDs come from the registry and are checked\n * to be UUIDs here; nothing from a request ever reaches these functions unvalidated.\n */\n\nexport { CONTENT_HEADS_COLUMNS, CONTENT_HEADS_TABLE } from './heads.js';\nexport type { ValueCast } from './dialect/types.js';\n\n/** Bump when any expression below changes: index names change with it, so old indexes are rebuilt. */\nexport const EXPRESSION_VERSION = 1;\n\n/**\n * Column layout of field indexes. 1: (locale, state, value). 2 (sites plan §H): `site_id` leads, because\n * every content query names its site. The layout is part of the index name, so a new layout means new\n * indexes; the `fieldIndexLayout` job builds the current layout and drops the old one.\n */\nexport type FieldIndexLayout = 1 | 2;\nexport const FIELD_INDEX_LAYOUT: FieldIndexLayout = 2;\n\nconst NUMERIC_TYPES: ReadonlySet<DataType> = new Set(['number', 'integer', 'decimal', 'biginteger']);\n/** Numbers stored as JSON strings (exact decimals, big integers). */\nconst NUMERIC_STRING_TYPES: ReadonlySet<DataType> = new Set(['decimal', 'biginteger']);\n\n/**\n * Whether a field has a B-tree expression index: filterable or sortable, and not deprecated. The schema\n * planner builds an index for exactly these fields (`indexStepsOf`), and PostgreSQL equality compares on\n * the index expression for them (`EqualityTarget.indexed`).\n */\nexport const hasFieldIndex = (\n field: Pick<FieldDefinition, 'filterable' | 'sortable' | 'deprecated'>,\n): boolean => (field.filterable || field.sortable) && !field.deprecated;\n\nexport const valueCastFor = (type: DataType): ValueCast => {\n if (NUMERIC_TYPES.has(type)) {\n return 'numeric';\n }\n return type === 'boolean' ? 'boolean' : 'text';\n};\n\nexport class UnsafeIdentifierError extends Error {\n constructor(what: string) {\n super(`Refusing to build SQL with ${what}: not a stable ID`);\n this.name = 'UnsafeIdentifierError';\n }\n}\n\nconst assertStableId = (value: string, what: string) => {\n if (!isStableId(value)) {\n throw new UnsafeIdentifierError(what);\n }\n};\n\n/**\n * The expression for one top-level scalar field of a head's `data`, e.g. `((data ->> '0b1c…')::numeric)`\n * on PostgreSQL. Throws for non-scalar types or a malformed field ID.\n */\nexport const fieldValueExpression = (\n fieldId: string,\n type: DataType,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n if (!SCALAR_DATA_TYPES.has(type)) {\n throw new Error(`${type} fields have no scalar expression`);\n }\n return dialect.fieldValue(fieldId, valueCastFor(type), NUMERIC_STRING_TYPES.has(type));\n};\n\n/** `data -> 'fieldId'`: the raw JSON value of a top-level field (any type). */\nexport const fieldJsonExpression = (\n fieldId: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.fieldJson(fieldId);\n};\n\n/** `data ->> 'fieldId'`: a top-level field as text, uncast. */\nexport const fieldTextExpression = (\n fieldId: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.fieldText(fieldId);\n};\n\n/**\n * Equality on a top-level field (lists: \"contains\"). PostgreSQL compares the field's index expression when\n * the field has one (`EqualityTarget.indexed`) and emits containment, which its GIN index serves, otherwise;\n * SQLite and MySQL compare the field's index expression.\n */\nexport const fieldEqualsExpression = (\n target: EqualityTarget,\n value: unknown,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(target.fieldId, 'a field ID');\n return dialect.fieldEquals(target, value);\n};\n\n/** The field's value is missing: absent, null, \"\" or []. */\nexport const fieldMissingExpression = (\n fieldId: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.fieldMissing(fieldId);\n};\n\n/** Case-sensitive text match on a top-level field; `text` is a parameter, never SQL. */\nexport const textMatchExpression = (\n fieldId: string,\n operator: TextMatchOperator,\n text: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.textMatch(fieldId, operator, text);\n};\n\n/** Case-insensitive contains on a top-level field (`$containsi`, search). */\nexport const containsInsensitiveExpression = (\n fieldId: string,\n text: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.containsInsensitive(fieldId, text);\n};\n\n/**\n * The model ID as a SQL literal for `model_id = '…'`. Expression indexes are partial on the model, and\n * the planner can only prove a partial index's predicate against a literal, never a parameter.\n */\nexport const modelIdLiteral = (modelId: string): RawBuilder<unknown> => {\n assertStableId(modelId, 'a model ID');\n return sql.lit(modelId);\n};\n\n/** A comparison value cast the way `fieldValueExpression` casts the stored value, so indexes match. */\nexport const castParameter = (\n value: unknown,\n type: DataType,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => dialect.castParameter(value, valueCastFor(type), NUMERIC_STRING_TYPES.has(type));\n\nexport type FieldIndexSpec = {\n modelId: string;\n fieldId: string;\n type: DataType;\n /**\n * Whether the model is localized. Localized models' queries always name a locale, so their indexes lead\n * with it; non-localized models' heads are read without a locale condition, so theirs omit it. Absent\n * means localized (the layout of indexes planned before this flag existed).\n */\n localized?: boolean;\n};\n\n/**\n * Deterministic index name: a hash of the model ID, field ID, value cast, column layout and expression\n * version, so the same field with the same expression and layout always maps to the same index, and a\n * changed one to a new one. 63 bytes is PostgreSQL's identifier limit; this is 3 + 32. `layoutVersion` 1\n * gives the names indexes had before sites (to drop them). A database has one dialect, so the name does\n * not depend on it.\n */\nexport const fieldIndexName = (\n { modelId, fieldId, type, localized = true }: FieldIndexSpec,\n layoutVersion: FieldIndexLayout = FIELD_INDEX_LAYOUT,\n): string => {\n const layout = `${localized ? '' : ':shared'}${layoutVersion === 1 ? '' : `:l${layoutVersion}`}`;\n const digest = createHash('sha256')\n .update(`${modelId}:${fieldId}:${valueCastFor(type)}:v${EXPRESSION_VERSION}${layout}`)\n .digest('hex');\n return `eh_${digest.slice(0, 32)}`;\n};\n\nconst INDEX_NAME_PATTERN = /^eh_([0-9a-f]{32})$/;\n\nconst assertIndexName = (indexName: string): string => {\n const match = INDEX_NAME_PATTERN.exec(indexName);\n if (!match) {\n throw new UnsafeIdentifierError('an index name');\n }\n return match[1] as string;\n};\n\n/**\n * The extended-statistics object that goes with a field index (same hash, `es_` prefix). PostgreSQL keeps\n * no expression statistics for *partial* indexes, so without it every range filter is estimated at a third\n * of the table and the planner ignores the field index.\n */\nexport const fieldStatisticsName = (indexName: string): string => `es_${assertIndexName(indexName)}`;\n\n/**\n * The field index over the model's heads only: (site, locale, state, value) for localized models, (site,\n * state, value) otherwise; the site leads because every query names one (the compiler's base condition).\n * PostgreSQL builds it `CONCURRENTLY` (outside a transaction); SQLite with a plain `CREATE INDEX`, which\n * holds the write lock while it builds. One index serves equality, ranges and sorts on SQLite.\n */\n/** The parts of a field index the dialect spells (`createFieldIndexStatement`). */\nconst fieldIndexDefinition = (spec: FieldIndexSpec, dialect: ContentSqlDialect): FieldIndexDefinition => {\n assertStableId(spec.modelId, 'a model ID');\n const { siteId, modelId, locale, state } = CONTENT_HEADS_COLUMNS;\n const leading =\n spec.localized === false\n ? sql`${sql.ref(siteId)}, ${sql.ref(state)}`\n : sql`${sql.ref(siteId)}, ${sql.ref(locale)}, ${sql.ref(state)}`;\n return {\n name: fieldIndexName(spec),\n table: CONTENT_HEADS_TABLE,\n leading,\n expression: fieldValueExpression(spec.fieldId, spec.type, dialect),\n modelIdColumn: modelId,\n modelId: sql.lit(spec.modelId),\n localized: spec.localized !== false,\n cast: valueCastFor(spec.type),\n numericString: NUMERIC_STRING_TYPES.has(spec.type),\n };\n};\n\nexport const createFieldIndexStatement = (\n spec: FieldIndexSpec,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => dialect.createFieldIndex(fieldIndexDefinition(spec, dialect));\n\n/**\n * Adds the generated column a field index is built on, where the dialect indexes one (MySQL); null\n * elsewhere. Run before `createFieldIndexStatement`.\n */\nexport const createFieldIndexColumnStatement = (\n spec: FieldIndexSpec,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null =>\n dialect.fieldIndexColumn ? dialect.fieldIndexColumn.add(fieldIndexDefinition(spec, dialect)) : null;\n\n/** Drops a field index's generated column (MySQL); null elsewhere. Run after `dropIndexStatement`. */\nexport const dropFieldIndexColumnStatement = (\n indexName: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null => {\n assertIndexName(indexName);\n return dialect.fieldIndexColumn ? dialect.fieldIndexColumn.drop(indexName) : null;\n};\n\n/**\n * `CREATE STATISTICS` on the field's value expression (the same expression the index and queries use);\n * null where the database has no extended statistics (SQLite).\n */\nexport const createFieldStatisticsStatement = (\n spec: FieldIndexSpec,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null =>\n dialect.createFieldStatistics(\n fieldStatisticsName(fieldIndexName(spec)),\n fieldValueExpression(spec.fieldId, spec.type, dialect),\n CONTENT_HEADS_TABLE,\n );\n\n/** Drops a field index (`CONCURRENTLY` on PostgreSQL); names only ever come from `fieldIndexName`. */\nexport const dropIndexStatement = (\n indexName: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertIndexName(indexName);\n return dialect.dropFieldIndex(indexName);\n};\n\n/** Drops the statistics object of a field index (`fieldStatisticsName`); null where there is none. */\nexport const dropStatisticsStatement = (\n indexName: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null => dialect.dropFieldStatistics(fieldStatisticsName(indexName));\n\n/** Refreshes planner statistics of the heads table (after an index and its statistics object are built). */\nexport const analyzeHeadsStatement = (dialect: ContentSqlDialect = contentDialect()): RawBuilder<unknown> =>\n dialect.analyze(CONTENT_HEADS_TABLE);\n","import { isStableId, type DataType, type FieldDefinition } from '@shapio/schema';\nimport {\n canonicalBigInteger,\n canonicalDate,\n canonicalDatetime,\n canonicalDecimal,\n canonicalTime,\n} from '../validator/scalars.js';\nimport { queryInvalid, type FilterOperator, type FilterTarget } from './types.js';\n\n/**\n * Which operators each data type accepts, and how query text becomes a typed value (ADR 0001):\n * - equality (`$eq $ne $in $nin`) works on every filterable type. On PostgreSQL it compiles to `data @> …`,\n * which the GIN index serves, so no per-field index is needed; a field that has its own index (filterable or\n * sortable) compares on that index's expression instead, which is far cheaper on unselective values. On\n * list values (multiple enums, `many` relations, multiple media) `$eq x` means \"contains x\".\n * - `$null`/`$notNull` test for a missing value (absent, null, \"\" or []).\n * - ranges (`$lt $lte $gt $gte`) need an expression index, so only fields marked filterable or sortable\n * accept them; text matches (`$contains`…) are unindexed scans and need `filterable`.\n * Values are canonicalized exactly like stored values, so `2026-10-01T10:00:00+02:00` finds\n * `2026-10-01T08:00:00.000Z`.\n */\n\ntype TypeFamily =\n 'text' | 'temporal' | 'number' | 'numericString' | 'boolean' | 'list' | 'reference' | 'none';\n\nconst familyOf = (field: FieldDefinition): TypeFamily => {\n switch (field.type) {\n case 'string':\n case 'text':\n case 'slug':\n case 'email':\n case 'url':\n case 'uid':\n return 'text';\n case 'enum':\n return field.settings.multiple ? 'list' : 'text';\n case 'date':\n case 'datetime':\n case 'time':\n return 'temporal';\n case 'number':\n case 'integer':\n return 'number';\n case 'decimal':\n case 'biginteger':\n return 'numericString';\n case 'boolean':\n return 'boolean';\n case 'relation':\n case 'media':\n return 'reference';\n default:\n return 'none';\n }\n};\n\nconst EQUALITY: readonly FilterOperator[] = ['$eq', '$ne', '$in', '$nin', '$null', '$notNull'];\nconst RANGE: readonly FilterOperator[] = ['$lt', '$lte', '$gt', '$gte'];\nconst TEXT_MATCH: readonly FilterOperator[] = [\n '$contains',\n '$notContains',\n '$containsi',\n '$startsWith',\n '$endsWith',\n];\n\nexport const OPERATORS_BY_FAMILY: Readonly<Record<TypeFamily, readonly FilterOperator[]>> = {\n text: [...EQUALITY, ...RANGE, ...TEXT_MATCH],\n temporal: [...EQUALITY, ...RANGE],\n number: [...EQUALITY, ...RANGE],\n numericString: [...EQUALITY, ...RANGE],\n boolean: ['$eq', '$ne', '$null', '$notNull'],\n list: EQUALITY,\n reference: EQUALITY,\n none: [],\n};\n\nexport const isRangeOperator = (operator: FilterOperator) => RANGE.includes(operator);\nexport const isTextMatchOperator = (operator: FilterOperator) => TEXT_MATCH.includes(operator);\nexport const isListOperator = (operator: FilterOperator) => operator === '$in' || operator === '$nin';\nexport const isNullOperator = (operator: FilterOperator) => operator === '$null' || operator === '$notNull';\n\n/** Whether the field holds a list (equality means \"contains\"). */\nexport const isListValued = (field: FieldDefinition): boolean =>\n (field.type === 'enum' && field.settings.multiple) ||\n (field.type === 'relation' && field.settings.cardinality === 'many') ||\n (field.type === 'media' && field.settings.multiple);\n\n/** Types whose stored JSON string is not unique per value, so equality compares numerically. */\nexport const NUMERIC_STRING_TYPES: ReadonlySet<DataType> = new Set(['decimal', 'biginteger']);\n\nconst SYSTEM_OPERATORS: Readonly<Record<string, readonly FilterOperator[]>> = {\n id: ['$eq', '$ne', '$in', '$nin'],\n createdAt: ['$eq', '$ne', '$in', '$nin', ...RANGE],\n updatedAt: ['$eq', '$ne', '$in', '$nin', ...RANGE],\n};\n\n/** Throws 400 unless the operator is allowed for the target. */\nexport const assertOperatorAllowed = (target: FilterTarget, operator: FilterOperator, label: string) => {\n if (target.kind === 'system') {\n if (!(SYSTEM_OPERATORS[target.name] ?? []).includes(operator)) {\n throw queryInvalid(`Operator ${operator} is not supported on \"${label}\"`);\n }\n return;\n }\n const { field } = target;\n if (!OPERATORS_BY_FAMILY[familyOf(field)].includes(operator)) {\n throw queryInvalid(`Operator ${operator} is not supported on \"${label}\" (${field.type})`);\n }\n if (isRangeOperator(operator) && !field.filterable && !field.sortable) {\n throw queryInvalid(`\"${label}\" is not filterable: mark it filterable to use ${operator}`);\n }\n if (isTextMatchOperator(operator) && !field.filterable) {\n throw queryInvalid(`\"${label}\" is not filterable: mark it filterable to use ${operator}`);\n }\n};\n\n/** Whether a field can be sorted on: an expression index exists only for sortable/filterable scalars. */\nexport const isSortable = (field: FieldDefinition) =>\n field.sortable &&\n familyOf(field) !== 'none' &&\n familyOf(field) !== 'list' &&\n familyOf(field) !== 'reference';\n\nconst coerceBoolean = (raw: unknown, label: string): boolean => {\n if (raw === true || raw === 'true') {\n return true;\n }\n if (raw === false || raw === 'false') {\n return false;\n }\n throw queryInvalid(`\"${label}\" needs true or false`);\n};\n\nconst coerceText = (raw: unknown, label: string): string => {\n if (typeof raw !== 'string') {\n throw queryInvalid(`\"${label}\" needs a single text value`);\n }\n return raw;\n};\n\nconst coerceOrFail = <T>(value: T | undefined, label: string, expected: string): T => {\n if (value === undefined) {\n throw queryInvalid(`\"${label}\" needs ${expected}`);\n }\n return value;\n};\n\n/** One comparison value for a field, canonicalized like stored values. */\nconst coerceFieldValue = (field: FieldDefinition, raw: unknown, label: string): unknown => {\n switch (familyOf(field)) {\n case 'number': {\n const number = Number(coerceText(raw, label));\n if (!Number.isFinite(number) || (raw as string).trim() === '') {\n throw queryInvalid(`\"${label}\" needs a number`);\n }\n return number;\n }\n case 'numericString':\n return coerceOrFail(\n field.type === 'decimal'\n ? canonicalDecimal(coerceText(raw, label))\n : canonicalBigInteger(coerceText(raw, label)),\n label,\n 'a number',\n );\n case 'temporal': {\n const text = coerceText(raw, label);\n const canonical =\n field.type === 'date'\n ? canonicalDate(text)\n : field.type === 'datetime'\n ? canonicalDatetime(text)\n : canonicalTime(text);\n return coerceOrFail(canonical, label, `a valid ${field.type}`);\n }\n case 'boolean':\n return coerceBoolean(raw, label);\n case 'reference': {\n const id = coerceText(raw, label);\n if (!isStableId(id)) {\n throw queryInvalid(`\"${label}\" needs an ID`);\n }\n return id;\n }\n default:\n return coerceText(raw, label);\n }\n};\n\nconst coerceSystemValue = (name: string, raw: unknown, label: string): unknown => {\n const text = coerceText(raw, label);\n if (name === 'id') {\n if (!isStableId(text)) {\n throw queryInvalid(`\"${label}\" needs an entry ID`);\n }\n return text;\n }\n return coerceOrFail(canonicalDatetime(text), label, 'an ISO-8601 date-time with a time zone');\n};\n\n/** Turns the raw operand of `operator` into typed value(s); lists for `$in`/`$nin`, a boolean for `$null`. */\nexport const coerceOperand = (\n target: FilterTarget,\n operator: FilterOperator,\n raw: unknown,\n label: string,\n maxListLength: number,\n): unknown => {\n if (isNullOperator(operator)) {\n return coerceBoolean(raw, label);\n }\n const coerceOne = (value: unknown) =>\n target.kind === 'system'\n ? coerceSystemValue(target.name, value, label)\n : coerceFieldValue(target.field, value, label);\n if (isListOperator(operator)) {\n if (!Array.isArray(raw)) {\n throw queryInvalid(`\"${label}\" ${operator} needs a list`);\n }\n if (raw.length > maxListLength) {\n throw queryInvalid(`\"${label}\" ${operator} accepts at most ${maxListLength} values`);\n }\n return raw.map(coerceOne);\n }\n if (isTextMatchOperator(operator)) {\n const text = coerceText(raw, label);\n if (text.length === 0) {\n throw queryInvalid(`\"${label}\" ${operator} needs a non-empty value`);\n }\n return text;\n }\n return coerceOne(raw);\n};\n\n/** Every operator a field accepts right now (documentation and generated filter types). */\nexport const operatorsFor = (field: FieldDefinition): FilterOperator[] =>\n OPERATORS_BY_FAMILY[familyOf(field)].filter(\n (operator) =>\n !(isRangeOperator(operator) && !field.filterable && !field.sortable) &&\n !(isTextMatchOperator(operator) && !field.filterable),\n );\n","import { SCALAR_DATA_TYPES } from '@shapio/schema';\nimport { sql, type RawBuilder } from 'kysely';\nimport type { HeadState } from '../model.js';\nimport { contentDialect } from './currentDialect.js';\nimport type { ContentSqlDialect, EqualityTarget } from './dialect/types.js';\nimport {\n castParameter,\n containsInsensitiveExpression,\n fieldEqualsExpression,\n fieldMissingExpression,\n fieldValueExpression,\n hasFieldIndex,\n modelIdLiteral,\n textMatchExpression,\n valueCastFor,\n} from './expressions.js';\nimport { isListValued, NUMERIC_STRING_TYPES } from './operators.js';\nimport type { EntryListStatus, FilterNode, FilterOperator, FilterTarget } from './types.js';\n\n/**\n * AST → parameterised SQL (Kysely `sql` fragments only). Every value is a bound parameter; the only\n * identifiers are fixed column names and stable field/model IDs that come from the registry and are checked\n * by `expressions.ts`. Head queries alias `entry_heads` (or a snapshot of it) as `h` and `entries` as `e`.\n * Database-specific spelling comes from the content dialect (`dialect/`), passed as the optional last\n * argument of every exported function (the process's dialect by default).\n */\n\n/** Where heads come from: live heads in one state, or published revisions as of a publication sequence. */\nexport type HeadSource = { kind: 'heads'; state: HeadState } | { kind: 'snapshot'; seq: number };\n\n/** Which locale's head serves each entry. */\nexport type LocaleScope =\n /** Non-localized models: each entry has heads in exactly one locale. */\n | { kind: 'any' }\n /** The first locale of the chain that has a head serves the entry (fallback, ADR 0004). */\n | { kind: 'chain'; chain: readonly string[] };\n\nconst RANGE_SQL: Readonly<Partial<Record<FilterOperator, RawBuilder<unknown>>>> = {\n $lt: sql`<`,\n $lte: sql`<=`,\n $gt: sql`>`,\n $gte: sql`>=`,\n $eq: sql`=`,\n $ne: sql`<>`,\n};\n\nconst anyOf = (parts: RawBuilder<unknown>[]): RawBuilder<unknown> =>\n parts.length === 0 ? sql`false` : sql`(${sql.join(parts, sql` or `)})`;\n\ntype FieldTarget = Extract<FilterTarget, { kind: 'field' }>;\n\nconst equalityTarget = ({ field }: FieldTarget): EqualityTarget => {\n const cast = SCALAR_DATA_TYPES.has(field.type) ? valueCastFor(field.type) : null;\n const list = isListValued(field);\n return {\n fieldId: field.id,\n cast,\n list,\n numericString: NUMERIC_STRING_TYPES.has(field.type),\n indexed: cast !== null && !list && hasFieldIndex(field),\n };\n};\n\n/** System filter columns. `createdAt` is the entry's, copied on every head (`h.entry_created_at`). */\nconst SYSTEM_COLUMNS = {\n id: 'h.entry_id',\n createdAt: 'h.entry_created_at',\n updatedAt: 'h.updated_at',\n} as const;\n\n/** Whether a filter names a column of `entries` (the head query's `e`), so its COUNT must join entries. */\nexport const filterReadsEntries = (node: FilterNode): boolean => {\n switch (node.kind) {\n case 'and':\n case 'or':\n return node.nodes.some(filterReadsEntries);\n case 'not':\n return filterReadsEntries(node.node);\n case 'condition':\n return node.target.kind === 'system' && SYSTEM_COLUMNS[node.target.name].startsWith('e.');\n }\n};\n\nconst systemCondition = (\n name: keyof typeof SYSTEM_COLUMNS,\n operator: FilterOperator,\n value: unknown,\n dialect: ContentSqlDialect,\n): RawBuilder<unknown> => {\n const column = sql.ref(SYSTEM_COLUMNS[name]);\n const element = name === 'id' ? 'uuid' : 'timestamp';\n switch (operator) {\n case '$in':\n return sql`(${dialect.oneOf(column, value as string[], element)})`;\n case '$nin':\n return sql`(not (${dialect.oneOf(column, value as string[], element)}))`;\n default: {\n const parameter = name === 'id' ? dialect.uuid(value as string) : dialect.timestamp(value as string);\n return sql`(${column} ${RANGE_SQL[operator] ?? sql`=`} ${parameter})`;\n }\n }\n};\n\nconst fieldCondition = (\n target: FieldTarget,\n operator: FilterOperator,\n value: unknown,\n dialect: ContentSqlDialect,\n): RawBuilder<unknown> => {\n const { field } = target;\n const equals = (item: unknown) => fieldEqualsExpression(equalityTarget(target), item, dialect);\n switch (operator) {\n case '$eq':\n return equals(value);\n case '$ne':\n return sql`(not ${equals(value)})`;\n case '$in':\n return anyOf((value as unknown[]).map(equals));\n case '$nin':\n return sql`(not ${anyOf((value as unknown[]).map(equals))})`;\n case '$null':\n case '$notNull': {\n const missing = fieldMissingExpression(field.id, dialect);\n return (operator === '$null') === value ? missing : sql`(not ${missing})`;\n }\n case '$lt':\n case '$lte':\n case '$gt':\n case '$gte':\n return sql`(${fieldValueExpression(field.id, field.type, dialect)} ${RANGE_SQL[operator]} ${castParameter(value, field.type, dialect)})`;\n case '$contains':\n case '$startsWith':\n case '$endsWith':\n case '$notContains':\n return textMatchExpression(field.id, operator, value as string, dialect);\n case '$containsi':\n return containsInsensitiveExpression(field.id, value as string, dialect);\n }\n};\n\n/** A filter AST as one boolean SQL expression. */\nexport const compileFilter = (\n node: FilterNode,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n const compileChild = (child: FilterNode) => compileFilter(child, dialect);\n switch (node.kind) {\n case 'and':\n return sql`(${sql.join(node.nodes.map(compileChild), sql` and `)})`;\n case 'or':\n return sql`(${sql.join(node.nodes.map(compileChild), sql` or `)})`;\n case 'not':\n return sql`(not ${compileChild(node.node)})`;\n case 'condition':\n return node.target.kind === 'system'\n ? systemCondition(node.target.name, node.operator, node.value, dialect)\n : fieldCondition(node.target, node.operator, node.value, dialect);\n }\n};\n\n/**\n * Admin list status of the served draft head `h`, against the published head of its locale (the same rule as\n * `statusOf`): no published head = draft; the same revision and no autosaved work = published; else modified.\n * Served by the entry_heads primary key.\n */\nexport const compileStatusCondition = (status: EntryListStatus): RawBuilder<unknown> => {\n const published = sql`select 1 from entry_heads p\n where p.entry_id = h.entry_id and p.locale = h.locale and p.state = 'published'`;\n switch (status) {\n case 'draft':\n return sql`not exists (${published})`;\n case 'published':\n return sql`(h.autosaved_at is null and exists (${published} and p.revision_id = h.revision_id))`;\n case 'modified':\n return sql`exists (${published} and (p.revision_id <> h.revision_id or h.autosaved_at is not null))`;\n }\n};\n\n/** Entries an admin user created (admin list `?author=`). */\nexport const compileAuthorCondition = (\n adminUserId: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => sql`e.created_by_admin_id = ${dialect.uuid(adminUserId)}`;\n\n/** Case-insensitive search of one text field. */\nexport const compileSearch = (\n fieldId: string,\n text: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => containsInsensitiveExpression(fieldId, text, dialect);\n\nconst SNAPSHOT_CTE = 'content_snapshot';\n\n/** The FROM item for a source, aliased `alias`, and the CTE it needs (snapshots only). */\nconst sourceOf = (source: HeadSource, modelId: string, siteId: string, dialect: ContentSqlDialect) => {\n if (source.kind === 'heads') {\n return { cte: null, table: sql.table('entry_heads') };\n }\n const cte = dialect.snapshotCte({\n name: SNAPSHOT_CTE,\n siteId,\n modelId: modelIdLiteral(modelId),\n seq: source.seq,\n });\n return { cte, table: sql.id(SNAPSHOT_CTE) };\n};\n\nexport type HeadQueryPlan = {\n /**\n * The site whose content is read (sites plan §H). A tenancy boundary, always applied, separate from the\n * ownership row filter in `conditions`. It comes from the service context, never from the request.\n */\n siteId: string;\n modelId: string;\n source: HeadSource;\n locales: LocaleScope;\n /** Extra conditions (filters, search, row filter, ID restriction), all ANDed. */\n conditions: readonly RawBuilder<unknown>[];\n /**\n * Whether any of `conditions` names an `entries` column (`e`: ownership row filters, the admin author filter,\n * `createdAt` filters). Unknown when absent: assumed whenever there are conditions.\n */\n conditionsReadEntries?: boolean;\n orderBy: readonly RawBuilder<unknown>[];\n limit?: number;\n offset?: number;\n};\n\nconst localeConditions = (\n scope: LocaleScope,\n table: RawBuilder<unknown>,\n source: HeadSource,\n dialect: ContentSqlDialect,\n) => {\n if (scope.kind === 'any') {\n return [];\n }\n if (scope.chain.length === 1) {\n return [sql`h.locale = ${scope.chain[0]}`];\n }\n const chain = [...scope.chain];\n const stateMatch = source.kind === 'heads' ? sql` and h2.state = h.state` : sql``;\n return [\n dialect.oneOf(sql`h.locale`, chain, 'text'),\n // The first locale in the chain that has a head serves the entry; later ones are fallbacks.\n sql`not exists (select 1 from ${table} h2 where h2.entry_id = h.entry_id${stateMatch}\n and ${dialect.oneOf(sql`h2.locale`, chain, 'text')}\n and ${dialect.localeRank(chain, sql`h2.locale`)} < ${dialect.localeRank(chain, sql`h.locale`)})`,\n ];\n};\n\nconst whereOf = (plan: HeadQueryPlan, table: RawBuilder<unknown>, dialect: ContentSqlDialect) => {\n const conditions = [\n sql`h.site_id = ${dialect.uuid(plan.siteId)}`,\n sql`h.model_id = ${modelIdLiteral(plan.modelId)}`,\n ...(plan.source.kind === 'heads' ? [sql`h.state = ${plan.source.state}`] : []),\n ...localeConditions(plan.locales, table, plan.source, dialect),\n ...plan.conditions,\n ];\n return sql.join(conditions, sql` and `);\n};\n\n/** Columns every head query returns. */\nexport type HeadRow = {\n entry_id: string;\n locale: string;\n data: Record<string, unknown>;\n version: number;\n revision_id: string;\n updated_at: Date;\n autosaved_at: Date | null;\n created_at: Date;\n entry_updated_at: Date;\n created_by_admin_id: string | null;\n owner_app_user_id: string | null;\n};\n\n/**\n * The COUNT's FROM. Live heads exist only for live entries (deleting an entry removes its heads in the same\n * transaction; entries soft-deleted for having no head have none; `headLiveEntry.int.test.ts` pins it), so a\n * count of heads whose conditions name no `e` column needs no join: it is read from the heads' indexes alone\n * (the model index, or a field index for an indexed filter). A snapshot keeps the join: the publication log\n * still lists entries deleted since.\n */\nconst countFrom = (\n plan: HeadQueryPlan,\n table: RawBuilder<unknown>,\n where: RawBuilder<unknown>,\n joined: RawBuilder<unknown>,\n): RawBuilder<unknown> =>\n plan.source.kind === 'heads' && !(plan.conditionsReadEntries ?? plan.conditions.length > 0)\n ? sql`from ${table} h where ${where}`\n : joined;\n\nconst HEAD_COLUMNS = sql`h.entry_id, h.locale, h.data, h.version, h.revision_id, h.updated_at,\n h.autosaved_at, e.created_at, e.updated_at as entry_updated_at, e.created_by_admin_id, e.owner_app_user_id`;\n\n/** The parts every statement over a plan's heads shares. */\nconst headQueryParts = (plan: HeadQueryPlan, dialect: ContentSqlDialect) => {\n const { cte, table } = sourceOf(plan.source, plan.modelId, plan.siteId, dialect);\n const where = whereOf(plan, table, dialect);\n const from = sql`from ${table} h join entries e on e.id = h.entry_id and e.deleted_at is null where ${where}`;\n const orderBy = plan.orderBy.length > 0 ? sql` order by ${sql.join([...plan.orderBy])}` : sql``;\n const limit = plan.limit !== undefined ? sql` limit ${plan.limit}` : sql``;\n const offset = plan.offset ? sql` offset ${plan.offset}` : sql``;\n return {\n prefix: cte ?? sql``,\n rows: sql`${from}${orderBy}${limit}${offset}`,\n countFrom: countFrom(plan, table, where, from),\n };\n};\n\n/** The SELECT for a plan: rows, plus the matching COUNT for pagination. */\nexport const compileHeadQuery = (plan: HeadQueryPlan, dialect: ContentSqlDialect = contentDialect()) => {\n const { prefix, rows, countFrom: counted } = headQueryParts(plan, dialect);\n return {\n rows: sql<HeadRow>`${prefix}select ${HEAD_COLUMNS}\n ${rows}`,\n count: sql<{\n total: string | number;\n }>`${prefix}select count(*) as total ${counted}`,\n };\n};\n\n/** The page's total (when asked for) and the site's publication sequence, read in the same statement. */\nexport type HeadPageMeta = { page_total: string | number | null; page_seq: string | number | null };\n\n/**\n * One delivery read as a single statement (plan delivery-perf): the page's heads with, on every row, the\n * matching total and the site's current publication sequence (`publication_state.last_seq`), so all three\n * come from one snapshot of the database on every dialect without a transaction. The uncorrelated\n * subqueries run once per statement. An empty page carries no row, so `meta` reads the total and the\n * sequence alone, for callers that read it in the same transaction as `rows`.\n */\nexport const compileHeadPage = (\n plan: HeadQueryPlan,\n options: { total: boolean },\n dialect: ContentSqlDialect = contentDialect(),\n) => {\n const { prefix, rows, countFrom: counted } = headQueryParts(plan, dialect);\n const total = options.total ? sql`(select count(*) ${counted})` : sql`null`;\n const seq = sql`(select ps.last_seq from publication_state ps where ps.site_id = ${dialect.uuid(plan.siteId)})`;\n return {\n rows: sql<\n HeadRow & HeadPageMeta\n >`${prefix}select ${HEAD_COLUMNS}, ${total} as page_total, ${seq} as page_seq\n ${rows}`,\n meta: sql<HeadPageMeta>`${prefix}select ${total} as page_total, ${seq} as page_seq`,\n };\n};\n","import { sql, type RawBuilder } from 'kysely';\nimport { contentDialect } from './currentDialect.js';\nimport type { ContentSqlDialect } from './dialect/types.js';\n\n/**\n * Conditions on the head query's entry (alias `h`, compile.ts) for `HeadQueryPlan.conditions`, so callers\n * outside the compiler never write SQL against its aliases.\n */\n\n/** The head belongs to entry `id`. */\nexport const entryIdIs = (id: string, dialect: ContentSqlDialect = contentDialect()): RawBuilder<boolean> =>\n sql<boolean>`h.entry_id = ${dialect.uuid(id)}`;\n\n/** The head belongs to one of `ids` (one bound parameter, whatever its length). */\nexport const entryIdIn = (\n ids: readonly string[],\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<boolean> => dialect.oneOf(sql`h.entry_id`, ids, 'uuid');\n","import type { FieldDefinition, ModelDefinition } from '@shapio/schema';\nimport { sql, type RawBuilder } from 'kysely';\nimport { contentDialect } from './currentDialect.js';\nimport type { ContentSqlDialect } from './dialect/types.js';\nimport { fieldValueExpression } from './expressions.js';\nimport { isSortable } from './operators.js';\nimport type { SortTerm } from './types.js';\n\n/**\n * ORDER BY for a content query. Field sorts use the same expression as the field's index (so the index can\n * serve them); the entry ID is always the final tie-breaker so pages are stable. Without an explicit sort\n * the model's default sort applies when the caller may read that field, else newest first. `createdAt` is the\n * entry's creation time copied on the head (`h.entry_created_at`), so the default order and its tie-breaker\n * are both head columns, which `entry_heads_entry_order_idx` (site, model, state, `entry_created_at desc`,\n * `entry_id`) serves: a page is read in index order and the scan stops at the limit (ADR 0001, \"Delivery perf\n * 2\").\n */\nconst SYSTEM_SORT_COLUMNS = {\n id: 'h.entry_id',\n createdAt: 'h.entry_created_at',\n updatedAt: 'h.updated_at',\n} as const;\n\nconst termSql = (term: SortTerm, dialect: ContentSqlDialect): RawBuilder<unknown> => {\n // System columns are never null; field values may be missing.\n const nullable = term.target.kind === 'field';\n const expression =\n term.target.kind === 'system'\n ? sql.ref(SYSTEM_SORT_COLUMNS[term.target.name])\n : fieldValueExpression(term.target.field.id, term.target.field.type, dialect);\n if (dialect.sortTerm) {\n return dialect.sortTerm(expression, term.direction, nullable);\n }\n return sql`${expression} ${dialect.sortDirection(term.direction, nullable)}`;\n};\n\nexport const defaultSortTerms = (\n model: ModelDefinition,\n isReadable: (field: FieldDefinition) => boolean,\n): SortTerm[] => {\n const preferred = model.display.defaultSort;\n const field = preferred ? model.fields.find((candidate) => candidate.id === preferred.fieldId) : undefined;\n if (preferred && field && !field.deprecated && isSortable(field) && isReadable(field)) {\n return [{ target: { kind: 'field', field }, direction: preferred.direction }];\n }\n return [{ target: { kind: 'system', name: 'createdAt' }, direction: 'desc' }];\n};\n\nexport const compileOrderBy = (\n terms: readonly SortTerm[],\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown>[] => [...terms.map((term) => termSql(term, dialect)), sql`h.entry_id asc`];\n","import { isDeepStrictEqual } from 'node:util';\nimport type { FieldDefinition, ModelDefinition } from '@shapio/schema';\nimport type { ContentData } from '../db/contentData.js';\nimport type { SchemaById } from '../schema/snapshot.js';\nimport type { LocaleScope } from './compiler/compile.js';\nimport { queryInvalid } from './compiler/types.js';\nimport { isLocalizedField } from './model.js';\n\n/**\n * Localization rules (ADR 0004). Every locale's head holds the complete document; shared (non-localized)\n * fields are fanned out to every existing draft of the entry when saved; publishing is strictly per locale.\n */\n\n/** The locale itself, then its configured fallbacks, then the default locale (always last). */\nexport const fallbackChain = (snapshot: SchemaById, locale: string): string[] => {\n const configured = snapshot.locales.find((candidate) => candidate.code === locale);\n const chain = [locale, ...(configured?.fallbacks ?? []), snapshot.defaultLocale];\n return chain.filter(\n (code, index) => chain.indexOf(code) === index && snapshot.locales.some((l) => l.code === code),\n );\n};\n\n/** The locale a write targets: the requested one for localized models, the default locale otherwise. */\nexport const writeLocaleFor = (\n snapshot: SchemaById,\n model: ModelDefinition,\n requested: string | undefined,\n) => {\n if (!model.localized) {\n return snapshot.defaultLocale;\n }\n const locale = requested ?? snapshot.defaultLocale;\n if (!snapshot.locales.some((candidate) => candidate.code === locale)) {\n throw queryInvalid(`Unknown locale \"${locale}\"`);\n }\n return locale;\n};\n\n/**\n * Which heads serve a read. Non-localized models keep their heads in whichever locale they were created in,\n * so they are read regardless of locale; localized models serve the requested locale or fall back.\n */\nexport const readScopeFor = (\n snapshot: SchemaById,\n model: ModelDefinition,\n requested: string | undefined,\n { fallback }: { fallback: boolean },\n): LocaleScope => {\n if (!model.localized) {\n return { kind: 'any' };\n }\n const locale = requested ?? snapshot.defaultLocale;\n return { kind: 'chain', chain: fallback ? fallbackChain(snapshot, locale) : [locale] };\n};\n\nexport const sharedFields = (model: ModelDefinition): FieldDefinition[] =>\n model.fields.filter((field) => !isLocalizedField(model, field));\n\n/** Copies the shared fields' values (present or absent) from `source` into `target`. */\nexport const withSharedValuesOf = (\n model: ModelDefinition,\n target: Readonly<ContentData>,\n source: Readonly<ContentData>,\n fieldIds?: ReadonlySet<string>,\n): ContentData => {\n const output: ContentData = { ...target };\n for (const field of sharedFields(model)) {\n if (fieldIds && !fieldIds.has(field.id)) {\n continue;\n }\n if (source[field.id] === undefined) {\n delete output[field.id];\n } else {\n output[field.id] = source[field.id];\n }\n }\n return output;\n};\n\n/** Shared fields whose values differ between two documents. */\nexport const changedSharedFieldIds = (\n model: ModelDefinition,\n before: Readonly<ContentData>,\n after: Readonly<ContentData>,\n): Set<string> =>\n new Set(\n sharedFields(model)\n .filter((field) => !isDeepStrictEqual(before[field.id], after[field.id]))\n .map((field) => field.id),\n );\n\nexport type LocaleHeads = {\n locale: string;\n draft?: Readonly<ContentData>;\n published?: Readonly<ContentData>;\n};\n\n/**\n * Published locales whose live shared values are older than their draft's (which always holds the latest\n * shared values). The admin offers to publish these too after a shared field changed (ADR 0004).\n */\nexport const outdatedSharedLocales = (model: ModelDefinition, heads: readonly LocaleHeads[]): string[] =>\n model.localized\n ? heads\n .filter(\n ({ draft, published }) =>\n draft && published && changedSharedFieldIds(model, published, draft).size > 0,\n )\n .map(({ locale }) => locale)\n : [];\n","import { Kysely, PostgresDialect, type PostgresPool, type TransactionBuilder } from 'kysely';\nimport pg from 'pg';\nimport { assertNotNested, withConnectionScope } from './connectionScope.js';\nimport { dialectOfUrl, setCurrentDialect, sqliteLocationOfUrl } from './dialect.js';\nimport { createMysqlDb } from './mysql/index.js';\nimport { DEFAULT_POOL_ACQUIRE_TIMEOUT_MS, pgAcquireError } from './poolAcquire.js';\nimport { createSqliteDb } from './sqlite/index.js';\nimport type { DB } from './types.js';\n\nexport type Database = Kysely<DB>;\n\ntype CreateDbOptions = {\n connectionString: string;\n poolMax: number;\n /**\n * PostgreSQL and MySQL: how long a query waits for a free pooled connection before it fails with a 503\n * (`PoolAcquireTimeoutError`). On PostgreSQL it also bounds opening a new connection (503\n * `DatabaseUnavailableError`). SQLite never waits for a pooled connection: autocommit reads share one\n * reader, and a write that would wait for its own transaction fails at once (`db/sqlite/writeLock.ts`).\n */\n acquireTimeoutMs?: number;\n applicationName?: string;\n /**\n * Called when an idle pooled connection fails (PostgreSQL restarted, a network blip). The pool already\n * discards that connection and opens a new one on demand; this only reports it. Defaults to a process\n * warning, so the error is never silent.\n */\n onIdleConnectionError?: (error: Error) => void;\n /**\n * Tests turn it on. SQLite: fail on computed result columns that look like undecoded timestamps or JSON,\n * so a missing `db/sql/typed` marker is caught. PostgreSQL and MySQL: fail a query that asks the pool for\n * a connection inside an open transaction (`db/connectionScope.ts`).\n */\n strict?: boolean;\n};\n\nconst warnIdleConnectionError = (error: Error) =>\n process.emitWarning(`PostgreSQL connection lost while idle: ${error.message}`);\n\n/**\n * The pg pool as Kysely sees it: an exhausted pool's queue timeout becomes `PoolAcquireTimeoutError`, a\n * connection that cannot be opened in time `DatabaseUnavailableError`, and in strict mode a nested\n * acquisition fails at once.\n */\nconst acquiringPool = (\n pool: pg.Pool,\n poolMax: number,\n acquireTimeoutMs: number,\n strict: boolean,\n): PostgresPool => ({\n options: pool.options,\n connect: async () => {\n if (strict) {\n assertNotNested();\n }\n try {\n return await pool.connect();\n } catch (error) {\n throw pgAcquireError(error, poolMax, acquireTimeoutMs);\n }\n },\n end: () => pool.end(),\n});\n\n/** Kysely on PostgreSQL in strict mode: transactions run in a connection scope (`db/connectionScope.ts`). */\nclass StrictPostgresKysely<DB> extends Kysely<DB> {\n override transaction(): TransactionBuilder<DB> {\n return withConnectionScope(super.transaction());\n }\n}\n\nconst warnIdleMysqlConnectionError = (error: Error) =>\n process.emitWarning(`MySQL connection lost while idle: ${error.message}`);\n\n/**\n * The database handle for DATABASE_URL: PostgreSQL through a `pg` pool, MySQL (`mysql://…`) through a\n * `mysql2` pool, or SQLite (`sqlite:<path>`) through Shapio's `node:sqlite` driver, where `poolMax` is the\n * number of read connections (one connection writes).\n * Sets the process dialect (`db/dialect.ts`) that SQL builders read.\n */\nexport const createDb = ({\n connectionString,\n poolMax,\n acquireTimeoutMs = DEFAULT_POOL_ACQUIRE_TIMEOUT_MS,\n applicationName = 'shapio',\n onIdleConnectionError,\n strict = false,\n}: CreateDbOptions): Database => {\n if (dialectOfUrl(connectionString) === 'sqlite') {\n setCurrentDialect('sqlite');\n return createSqliteDb<DB>({ location: sqliteLocationOfUrl(connectionString), readers: poolMax, strict });\n }\n if (dialectOfUrl(connectionString) === 'mysql') {\n setCurrentDialect('mysql');\n return createMysqlDb<DB>({\n url: connectionString,\n poolMax,\n acquireTimeoutMs,\n applicationName,\n onIdleConnectionError: onIdleConnectionError ?? warnIdleMysqlConnectionError,\n strict,\n });\n }\n setCurrentDialect('postgres');\n const pool = new pg.Pool({\n connectionString,\n max: poolMax,\n // Bounds both the wait for a free connection and the opening of a new one.\n connectionTimeoutMillis: acquireTimeoutMs,\n application_name: applicationName,\n });\n // Without a listener, pg's 'error' event on an idle client would crash the process on a database restart.\n pool.on('error', onIdleConnectionError ?? warnIdleConnectionError);\n // pg-pool drops its own listener while a client is checked out, yet a client whose socket dies still emits\n // 'error' (after rejecting its running query, or before the next query of a transaction). Unheard, that is\n // an uncaught exception that takes the process down. The error is not lost: the query or transaction using\n // the client fails with it, and an idle client reports it through onIdleConnectionError above.\n pool.on('connect', (client) => {\n client.on('error', () => undefined);\n });\n const dialect = new PostgresDialect({\n pool: acquiringPool(pool, poolMax, acquireTimeoutMs, strict),\n // What the pool would use itself; Kysely opens it to cancel a query.\n controlClient: pg.Client,\n });\n return strict ? new StrictPostgresKysely<DB>({ dialect }) : new Kysely<DB>({ dialect });\n};\n\n/**\n * The process-wide database handle used as the default executor by repositories.\n * `buildApp` sets it; tests and child processes each run in their own process, so one handle per process holds.\n */\nexport let db: Database = undefined as unknown as Database;\n\nexport const setDb = (instance: Database): void => {\n db = instance;\n};\n\n/** Whether this process has its database handle yet (a server in the same process set it first). */\nexport const hasDb = (): boolean => (db as Database | undefined) !== undefined;\n\n/** Unsets the process handle if it is `instance` (its owner is closing it); leaves any other handle alone. */\nexport const clearDb = (instance: Database): void => {\n if (db === instance) {\n db = undefined as unknown as Database;\n }\n};\n","import type { Kysely, RawBuilder, Transaction } from 'kysely';\nimport type { HeadPageMeta, HeadRow } from '../content/compiler/compile.js';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/**\n * Executes head queries compiled by `content/compiler` (the only place content SQL is written). Kept as a\n * repository so services never run SQL themselves.\n */\nexport const runHeadQuery = async (query: RawBuilder<HeadRow>, executor: Executor = db): Promise<HeadRow[]> =>\n (await query.execute(executor)).rows;\n\nexport const runCountQuery = async (\n query: RawBuilder<{ total: string | number }>,\n executor: Executor = db,\n): Promise<number> => Number((await query.execute(executor)).rows[0]?.total ?? 0);\n\n/** A page's total (null when not asked for) and the publication sequence its statement saw (null: no site row). */\nexport type PageMeta = { total: number | null; seq: number | null };\n\nconst toPageMeta = (row: HeadPageMeta): PageMeta => ({\n total: row.page_total === null ? null : Number(row.page_total),\n seq: row.page_seq === null ? null : Number(row.page_seq),\n});\n\n/**\n * Runs a head page compiled by `compileHeadPage`: the rows, and the total and sequence carried on them (null\n * when the page is empty; then `runHeadPageMeta` in the same transaction reads them).\n */\nexport const runHeadPage = async (\n query: RawBuilder<HeadRow & HeadPageMeta>,\n executor: Executor = db,\n): Promise<{ rows: HeadRow[]; meta: PageMeta | null }> => {\n const result = (await query.execute(executor)).rows;\n const [first] = result;\n return {\n rows: result.map(({ page_total: _total, page_seq: _seq, ...row }) => row),\n meta: first ? toPageMeta(first) : null,\n };\n};\n\nexport const runHeadPageMeta = async (\n query: RawBuilder<HeadPageMeta>,\n executor: Executor = db,\n): Promise<PageMeta> => {\n const [row] = (await query.execute(executor)).rows;\n return row ? toPageMeta(row) : { total: null, seq: null };\n};\n\n/** Runs `fn` in one REPEATABLE READ, read-only transaction: a response reads one consistent moment. */\nexport const withConsistentRead = <T>(fn: (trx: Transaction<DB>) => Promise<T>, database: Kysely<DB> = db) =>\n database.transaction().setIsolationLevel('repeatable read').setAccessMode('read only').execute(fn);\n","import type { Insertable, Kysely, Selectable, Transaction, Updateable } from 'kysely';\nimport { db } from '../db/index.js';\nimport { containsInsensitive, startsWith } from '../db/sql/text.js';\nimport { timestampCursorText, timestampParam } from '../db/sql/time.js';\nimport type { DB, MediaAssets } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type MediaAssetRow = Selectable<MediaAssets>;\nexport type NewMediaAsset = Insertable<MediaAssets>;\nexport type MediaAssetPatch = Updateable<MediaAssets>;\n\nexport type MediaAssetFilter = {\n /** A folder ID, `null` for the root (no folder), or undefined for every folder. */\n folderId?: string | null;\n /** `image/png`, or a top-level type with a wildcard: `image/*`. */\n mimeType?: string;\n /** Matches the file name, alt text and caption, case-insensitively. */\n search?: string;\n};\n\n/** Keyset cursor: (created_at, id) of the last row, created_at as ISO-8601 with microseconds. */\nexport type MediaAssetCursor = { createdAt: string; id: string };\n\nconst live = (trx: Executor) => trx.selectFrom('media_assets').where('deleted_at', 'is', null);\n\nexport const insert = (asset: NewMediaAsset, trx: Executor = db) =>\n trx.insertInto('media_assets').values(asset).returningAll().executeTakeFirstOrThrow();\n\n/** Any site's asset, deleted or not. Unscoped: jobs and exists-anywhere checks only; requests use `findLiveOnSite`. */\nexport const findById = (id: string, trx: Executor = db) =>\n trx.selectFrom('media_assets').selectAll().where('id', '=', id).executeTakeFirst();\n\n/** A live asset of one site; another site's asset reads as missing (sites plan §H). */\nexport const findLiveOnSite = (siteId: string, id: string, trx: Executor = db) =>\n live(trx).selectAll().where('site_id', '=', siteId).where('id', '=', id).executeTakeFirst();\n\n/** The live assets of one site among `ids`. */\nexport const findLiveManyOnSite = (siteId: string, ids: readonly string[], trx: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : live(trx).selectAll().where('site_id', '=', siteId).where('id', 'in', ids).execute();\n\n/** Locks the row for the rest of the transaction (deleted rows too, so callers can report them). Jobs only. */\nexport const lockById = (id: string, trx: Transaction<DB>) =>\n trx.selectFrom('media_assets').selectAll().where('id', '=', id).forUpdate().executeTakeFirst();\n\n/** Locks one site's asset row (deleted rows too); another site's asset reads as missing. */\nexport const lockOnSite = (siteId: string, id: string, trx: Transaction<DB>) =>\n trx\n .selectFrom('media_assets')\n .selectAll()\n .where('site_id', '=', siteId)\n .where('id', '=', id)\n .forUpdate()\n .executeTakeFirst();\n\nexport const findLiveByStorageKey = (key: string, trx: Executor = db) =>\n live(trx).selectAll().where('storage_key', '=', key).executeTakeFirst();\n\nexport const update = (id: string, patch: MediaAssetPatch, trx: Executor = db) =>\n trx\n .updateTable('media_assets')\n .set({ ...patch, updated_at: new Date() })\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n\n/** Changes made by jobs: only while the asset still has `storageKey` (not replaced, moved or deleted). */\nexport const updateIfStorageKey = (\n id: string,\n storageKey: string,\n patch: MediaAssetPatch,\n trx: Executor = db,\n) =>\n trx\n .updateTable('media_assets')\n .set({ ...patch, updated_at: new Date() })\n .where('id', '=', id)\n .where('storage_key', '=', storageKey)\n .where('deleted_at', 'is', null)\n .returningAll()\n .executeTakeFirst();\n\n/** Metadata edits by people: only when the caller saw the current version (optimistic concurrency). */\nexport const updateIfVersion = (\n siteId: string,\n id: string,\n expectedVersion: number,\n patch: MediaAssetPatch,\n trx: Executor = db,\n) =>\n trx\n .updateTable('media_assets')\n .set((eb) => ({ ...patch, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('site_id', '=', siteId)\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .where('deleted_at', 'is', null)\n .returningAll()\n .executeTakeFirst();\n\nexport const list = (\n siteId: string,\n filter: MediaAssetFilter,\n cursor: MediaAssetCursor | undefined,\n limit: number,\n trx: Executor = db,\n) =>\n live(trx)\n .selectAll()\n .select(timestampCursorText('created_at').as('cursor_at'))\n .where('site_id', '=', siteId)\n .$if(filter.folderId !== undefined, (qb) =>\n filter.folderId === null\n ? qb.where('folder_id', 'is', null)\n : qb.where('folder_id', '=', filter.folderId ?? ''),\n )\n .$if(filter.mimeType !== undefined, (qb) => {\n const mimeType = filter.mimeType ?? '';\n return mimeType.endsWith('/*')\n ? qb.where((eb) => startsWith(eb.ref('mime_type'), mimeType.slice(0, -1)))\n : qb.where('mime_type', '=', mimeType);\n })\n .$if(filter.search !== undefined, (qb) => {\n const search = filter.search ?? '';\n return qb.where((eb) =>\n eb.or([\n containsInsensitive(eb.ref('original_filename'), search),\n containsInsensitive(eb.ref('alt'), search),\n containsInsensitive(eb.ref('caption'), search),\n ]),\n );\n })\n .$if(cursor !== undefined, (qb) =>\n qb.where((eb) => {\n const at = timestampParam(cursor?.createdAt ?? null);\n return eb.or([\n eb('created_at', '<', at),\n eb.and([eb('created_at', '=', at), eb('id', '<', cursor?.id ?? '')]),\n ]);\n }),\n )\n .orderBy('created_at', 'desc')\n .orderBy('id', 'desc')\n .limit(limit)\n .execute();\n\n/** Moves one site's live assets into a folder of that site (or the root); returns the IDs that moved. */\nexport const moveToFolder = async (\n siteId: string,\n ids: readonly string[],\n folderId: string | null,\n trx: Executor = db,\n) =>\n (\n await trx\n .updateTable('media_assets')\n .set((eb) => ({ folder_id: folderId, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('site_id', '=', siteId)\n .where('id', 'in', ids)\n .where('deleted_at', 'is', null)\n .returning('id')\n .execute()\n ).map((row) => row.id);\n\nexport const countLiveInFolder = async (folderId: string, trx: Executor = db) =>\n Number(\n (\n await live(trx)\n .select((eb) => eb.fn.countAll<string>().as('count'))\n .where('folder_id', '=', folderId)\n .executeTakeFirstOrThrow()\n ).count,\n );\n\n/** Live assets on a driver, in ID order after `afterId` (for `shapio media migrate`). */\nexport const listLiveByDriver = (\n driver: string,\n afterId: string | undefined,\n limit: number,\n trx: Executor = db,\n) =>\n live(trx)\n .selectAll()\n .where('storage_driver', '=', driver)\n .$if(afterId !== undefined, (qb) => qb.where('id', '>', afterId ?? ''))\n .orderBy('id')\n .limit(limit)\n .execute();\n\n/** Library alt text and type of one site's live assets among `ids` (content health's alt-text rule). */\nexport const findAltOnSite = (siteId: string, ids: readonly string[], executor: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : executor\n .selectFrom('media_assets')\n .select(['id', 'alt', 'mime_type'])\n .where('site_id', '=', siteId)\n .where('id', 'in', ids)\n .where('deleted_at', 'is', null)\n .execute();\n\n/**\n * Hard-deletes a site's soft-deleted assets (site deletion). Their objects were queued for purge when they\n * were deleted; their variants and references cascade.\n */\nexport const deleteSoftDeletedOfSite = (siteId: string, trx: Executor = db) =>\n trx.deleteFrom('media_assets').where('site_id', '=', siteId).where('deleted_at', 'is not', null).execute();\n","/** Media library limits and timings (package G). Upload size and allowed types are configuration. */\n\n/** Shapio's route for stored objects: public ones at a stable URL, private ones with a signature. */\nexport const MEDIA_FILE_ROUTE_PREFIX = '/api/media/f/';\n/** Where the local driver receives upload bytes (`/api/media/uploads/:grantId`). */\nexport const MEDIA_UPLOAD_ROUTE_PREFIX = '/api/media/uploads/';\n\n/** How long a signed URL for a private asset works. */\nexport const MEDIA_SIGNED_URL_TTL_SECONDS = 60 * 60;\n/** How long an upload grant accepts bytes. */\nexport const MEDIA_UPLOAD_GRANT_TTL_SECONDS = 15 * 60;\n/** How long after a grant expires an uploaded-but-unconfirmed object is kept before it is deleted. */\nexport const MEDIA_UPLOAD_CONFIRM_GRACE_SECONDS = 60 * 60;\n/** Bytes read from the start of an upload to detect its type. */\nexport const MEDIA_SNIFF_SAMPLE_BYTES = 16 * 1024;\n\nexport const MEDIA_PROCESS_JOB = 'media.process';\nexport const MEDIA_PURGE_JOB = 'media.purge';\nexport const MEDIA_RELOCATE_JOB = 'media.relocate';\nexport const MEDIA_GRANT_EXPIRE_JOB = 'media.grant.expire';\nexport const MEDIA_JOB_MAX_ATTEMPTS = 5;\n\n/** Outbox event types (webhook catalogue `media.*`, package H). */\nexport const MEDIA_EVENTS = {\n created: 'media.created',\n updated: 'media.updated',\n deleted: 'media.deleted',\n} as const;\n","import { MEDIA_FILE_ROUTE_PREFIX, MEDIA_SIGNED_URL_TTL_SECONDS } from '../constants/media.js';\nimport type { UrlBuilder } from '../helpers/publicUrl.js';\nimport { encodeKeyPath, type MediaVisibility } from './keys.js';\nimport type { MediaStorage, StorageDriver } from './types.js';\n\nexport type StoredObjectRef = {\n driver: StorageDriver;\n key: string;\n visibility: MediaVisibility;\n filename: string;\n mimeType: string;\n};\n\nexport type ResolvedUrl = { url: string; expiresAt: Date | null };\n\n/**\n * The URL a client should use for a stored object. Public: the CDN/bucket base when configured, otherwise\n * Shapio's stable route. Private: always an expiring signed URL; nothing else reaches private bytes.\n */\nexport const resolveObjectUrl = async (\n storage: MediaStorage,\n urls: UrlBuilder,\n object: StoredObjectRef,\n ttlSeconds = MEDIA_SIGNED_URL_TTL_SECONDS,\n): Promise<ResolvedUrl> => {\n const adapter = storage.get(object.driver);\n if (object.visibility === 'public') {\n return {\n url:\n adapter.publicUrl(object.key) ??\n urls.absoluteUrl(`${MEDIA_FILE_ROUTE_PREFIX}${encodeKeyPath(object.key)}`),\n expiresAt: null,\n };\n }\n const url = await adapter.signedGetUrl(object.key, {\n expiresInSeconds: ttlSeconds,\n filename: object.filename,\n contentType: object.mimeType,\n });\n return { url, expiresAt: new Date(Date.now() + ttlSeconds * 1000) };\n};\n","import type { Insertable, Kysely, Selectable, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB, MediaVariants } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type MediaVariantRow = Selectable<MediaVariants>;\n\nexport const listForAssets = (assetIds: readonly string[], trx: Executor = db) =>\n assetIds.length === 0\n ? Promise.resolve([])\n : trx\n .selectFrom('media_variants')\n .selectAll()\n .where('asset_id', 'in', assetIds)\n .orderBy('asset_id')\n .orderBy('width')\n .execute();\n\n/** Creates (or resets to pending) the variants an asset will get. */\nexport const upsertPending = async (rows: readonly Insertable<MediaVariants>[], trx: Executor = db) => {\n if (rows.length === 0) {\n return;\n }\n await trx\n .insertInto('media_variants')\n .values(rows.map((row) => ({ ...row, status: 'pending' })))\n .onConflict((oc) =>\n oc.columns(['asset_id', 'name']).doUpdateSet((eb) => ({\n width: eb.ref('excluded.width'),\n height: eb.ref('excluded.height'),\n format: eb.ref('excluded.format'),\n mime_type: eb.ref('excluded.mime_type'),\n storage_key: null,\n size_bytes: null,\n checksum_sha256: null,\n status: 'pending',\n error: null,\n updated_at: new Date(),\n })),\n )\n .execute();\n};\n\ntype VariantResult =\n | {\n status: 'ready';\n storageKey: string;\n width: number;\n height: number;\n sizeBytes: number;\n checksumSha256: string;\n }\n | { status: 'failed'; error: string };\n\n/**\n * Records a variant's outcome, but only while the asset still has the original it was made from\n * (`assetStorageKey`); returns false when the asset was replaced, moved or deleted meanwhile.\n */\nexport const recordResult = async (\n assetId: string,\n name: string,\n assetStorageKey: string,\n result: VariantResult,\n trx: Executor = db,\n) => {\n const updated = await trx\n .updateTable('media_variants')\n .set(\n result.status === 'ready'\n ? {\n status: 'ready',\n storage_key: result.storageKey,\n width: result.width,\n height: result.height,\n size_bytes: result.sizeBytes,\n checksum_sha256: result.checksumSha256,\n error: null,\n updated_at: new Date(),\n }\n : { status: 'failed', error: result.error, updated_at: new Date() },\n )\n .where('asset_id', '=', assetId)\n .where('name', '=', name)\n .where((eb) =>\n eb.exists(\n eb\n .selectFrom('media_assets')\n .select('id')\n .where('id', '=', assetId)\n .where('storage_key', '=', assetStorageKey)\n .where('deleted_at', 'is', null),\n ),\n )\n .executeTakeFirst();\n return updated.numUpdatedRows > 0n;\n};\n\n/** Removes an asset's variants; returns the stored keys so the objects can be purged. */\nexport const deleteForAsset = async (assetId: string, trx: Executor = db) =>\n (\n await trx.deleteFrom('media_variants').where('asset_id', '=', assetId).returning('storage_key').execute()\n ).flatMap((row) => (row.storage_key ? [row.storage_key] : []));\n\nexport const updateStorageKey = (id: string, storageKey: string, trx: Executor = db) =>\n trx\n .updateTable('media_variants')\n .set({ storage_key: storageKey, updated_at: new Date() })\n .where('id', '=', id)\n .execute();\n\n/** A ready variant by its key, with what serving it needs from its (live) asset. */\nexport const findServableByStorageKey = (key: string, trx: Executor = db) =>\n trx\n .selectFrom('media_variants')\n .innerJoin('media_assets', 'media_assets.id', 'media_variants.asset_id')\n .select([\n 'media_variants.storage_key',\n 'media_variants.mime_type',\n 'media_variants.size_bytes',\n 'media_variants.name',\n 'media_assets.storage_driver',\n 'media_assets.visibility',\n 'media_assets.original_filename',\n ])\n .where('media_variants.storage_key', '=', key)\n .where('media_variants.status', '=', 'ready')\n .where('media_assets.deleted_at', 'is', null)\n .executeTakeFirst();\n","import type { Database } from '../db/index.js';\nimport type { UrlBuilder } from '../helpers/publicUrl.js';\nimport type { MediaVisibility } from '../media/keys.js';\nimport type { MediaStorage, StorageDriver } from '../media/types.js';\nimport { resolveObjectUrl } from '../media/urls.js';\nimport type { MediaAssetRow } from '../repositories/mediaAssets.js';\nimport * as mediaVariantsRepository from '../repositories/mediaVariants.js';\nimport type { MediaVariantRow } from '../repositories/mediaVariants.js';\n\nexport type MediaVariantView = {\n name: string;\n width: number | null;\n height: number | null;\n format: string;\n mimeType: string;\n sizeBytes: number | null;\n status: 'pending' | 'ready' | 'failed';\n /** Present once the variant is ready. */\n url: string | null;\n};\n\nexport type MediaAssetView = {\n id: string;\n folderId: string | null;\n filename: string;\n mimeType: string;\n sizeBytes: number;\n width: number | null;\n height: number | null;\n checksumSha256: string | null;\n alt: string;\n caption: string;\n focalPoint: { x: number; y: number } | null;\n visibility: MediaVisibility;\n /** `processing` until the checksum, dimensions and variants are done. */\n status: 'processing' | 'ready' | 'failed';\n processingError: string | null;\n storageDriver: StorageDriver;\n url: string;\n /** When `url` stops working (private assets); null for stable public URLs. */\n urlExpiresAt: Date | null;\n variants: MediaVariantView[];\n createdBy: string | null;\n createdAt: Date;\n updatedAt: Date;\n version: number;\n};\n\ntype ViewDependencies = { storage: MediaStorage; urls: UrlBuilder };\n\nconst toVariantView = async (\n deps: ViewDependencies,\n asset: MediaAssetRow,\n variant: MediaVariantRow,\n): Promise<MediaVariantView> => ({\n name: variant.name,\n width: variant.width,\n height: variant.height,\n format: variant.format,\n mimeType: variant.mime_type,\n sizeBytes: variant.size_bytes === null ? null : Number(variant.size_bytes),\n status: variant.status as MediaVariantView['status'],\n url:\n variant.status === 'ready' && variant.storage_key\n ? (\n await resolveObjectUrl(deps.storage, deps.urls, {\n driver: asset.storage_driver as StorageDriver,\n key: variant.storage_key,\n visibility: asset.visibility as MediaVisibility,\n filename: asset.original_filename,\n mimeType: variant.mime_type,\n })\n ).url\n : null,\n});\n\nconst toAssetView = async (\n deps: ViewDependencies,\n asset: MediaAssetRow,\n variants: readonly MediaVariantRow[],\n): Promise<MediaAssetView> => {\n const { url, expiresAt } = await resolveObjectUrl(deps.storage, deps.urls, {\n driver: asset.storage_driver as StorageDriver,\n key: asset.storage_key,\n visibility: asset.visibility as MediaVisibility,\n filename: asset.original_filename,\n mimeType: asset.mime_type,\n });\n return {\n id: asset.id,\n folderId: asset.folder_id,\n filename: asset.original_filename,\n mimeType: asset.mime_type,\n sizeBytes: Number(asset.size_bytes),\n width: asset.width,\n height: asset.height,\n checksumSha256: asset.checksum_sha256,\n alt: asset.alt,\n caption: asset.caption,\n focalPoint:\n asset.focal_x !== null && asset.focal_y !== null ? { x: asset.focal_x, y: asset.focal_y } : null,\n visibility: asset.visibility as MediaVisibility,\n status: asset.status as MediaAssetView['status'],\n processingError: asset.processing_error,\n storageDriver: asset.storage_driver as StorageDriver,\n url,\n urlExpiresAt: expiresAt,\n variants: await Promise.all(variants.map((variant) => toVariantView(deps, asset, variant))),\n createdBy: asset.created_by,\n createdAt: asset.created_at,\n updatedAt: asset.updated_at,\n version: asset.version,\n };\n};\n\n/**\n * API views of assets with their variants (one query for all variants) and URLs: stable for public\n * assets, signed and expiring for private ones. Package E can use this to resolve media fields.\n * `executor`: the caller's open transaction, when there is one.\n */\nexport const toAssetViews = async (\n deps: ViewDependencies,\n assets: readonly MediaAssetRow[],\n executor?: Database,\n): Promise<MediaAssetView[]> => {\n const variants = await mediaVariantsRepository.listForAssets(\n assets.map((asset) => asset.id),\n executor,\n );\n const byAsset = Map.groupBy(variants, (variant) => variant.asset_id);\n return Promise.all(assets.map((asset) => toAssetView(deps, asset, byAsset.get(asset.id) ?? [])));\n};\n\nexport const toAssetViewOne = async (\n deps: ViewDependencies,\n asset: MediaAssetRow,\n): Promise<MediaAssetView> => {\n const [view] = await toAssetViews(deps, [asset]);\n if (!view) {\n throw new Error('Asset view missing');\n }\n return view;\n};\n","import type { FieldDefinition } from '@shapio/schema';\nimport type { Policy } from '../../permissions/types.js';\nimport { resolveModelById, type ContentModel } from '../model.js';\nimport type { ReadEnvironment } from '../read.js';\nimport type { HeadRow } from './compile.js';\nimport type { PopulateTree } from './types.js';\n\n/**\n * Relation expansion (`populate`). Depth is bounded by the parser (QUERY_LIMITS.maxPopulateDepth); each\n * level applies the *target* model's read policy (row filter and read mask) and, for delivery, reads only\n * published targets. A target the caller may not read is left out, never shown partially.\n */\nexport type PopulateEnvironment = ReadEnvironment & {\n fetch: (\n env: ReadEnvironment,\n model: ContentModel,\n policy: Policy,\n ids: readonly string[],\n ) => Promise<HeadRow[]>;\n project: (\n env: ReadEnvironment,\n model: ContentModel,\n policy: Policy,\n rows: readonly HeadRow[],\n query: { fields: null; populate: PopulateTree },\n ) => Promise<Array<{ row: HeadRow; data: Record<string, unknown> }>>;\n policyFor: (env: ReadEnvironment, modelId: string) => Promise<Policy>;\n system: (env: ReadEnvironment, row: HeadRow) => Record<string, unknown>;\n};\n\nconst idsOf = (rows: readonly HeadRow[], fieldId: string): string[] => {\n const ids = new Set<string>();\n for (const row of rows) {\n const value = row.data[fieldId];\n for (const id of Array.isArray(value) ? value : [value]) {\n if (typeof id === 'string') {\n ids.add(id);\n }\n }\n }\n return [...ids];\n};\n\n/** relation field ID → target entry ID → projected target entry. */\nexport const populateRelations = async (\n env: PopulateEnvironment,\n rows: readonly HeadRow[],\n fields: readonly FieldDefinition[],\n tree: PopulateTree,\n): Promise<Map<string, Map<string, unknown>>> => {\n const populated = new Map<string, Map<string, unknown>>();\n for (const [fieldId, subtree] of tree) {\n const field = fields.find((candidate) => candidate.id === fieldId);\n if (!field || field.type !== 'relation') {\n continue;\n }\n const byId = new Map<string, unknown>();\n populated.set(fieldId, byId);\n const target = resolveModelById(env.snapshot, field.settings.target);\n if (!target) {\n continue;\n }\n const policy = await env.policyFor(env, target.definition.id);\n const targetRows = await env.fetch(env, target, policy, idsOf(rows, fieldId));\n for (const { row, data } of await env.project(env, target, policy, targetRows, {\n fields: null,\n populate: subtree,\n })) {\n byId.set(row.entry_id, { ...env.system(env, row), ...data });\n }\n }\n return populated;\n};\n","import { richTextMediaIds, type FieldDefinition, type RichTextDocument } from '@shapio/schema';\nimport type { ContentData } from '../db/contentData.js';\nimport type { ContentModel } from './model.js';\nimport { COMPONENT_KEY } from './validator/index.js';\n\n/**\n * Media a document uses: media field values and rich-text images, also inside components and dynamic zones.\n * Each use is attributed to the top-level field that holds it (package G's `media_references` rows).\n */\nexport type MediaUse = { assetId: string; fieldId: string };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst collect = (\n model: ContentModel,\n fields: readonly FieldDefinition[],\n data: Readonly<Record<string, unknown>>,\n add: (assetId: string) => void,\n) => {\n for (const field of fields) {\n const value = data[field.id];\n if (value === undefined || value === null) {\n continue;\n }\n switch (field.type) {\n case 'media':\n (Array.isArray(value) ? value : [value]).forEach((id) => typeof id === 'string' && add(id));\n break;\n case 'richtext':\n if (isRecord(value) && isRecord(value.doc)) {\n richTextMediaIds(value as RichTextDocument).forEach(add);\n }\n break;\n case 'component': {\n const component = model.components.get(field.settings.component)?.definition;\n for (const item of Array.isArray(value) ? value : [value]) {\n if (component && isRecord(item)) {\n collect(model, component.fields, item, add);\n }\n }\n break;\n }\n case 'dynamiczone':\n for (const item of Array.isArray(value) ? value : []) {\n const component = isRecord(item)\n ? model.components.get(String(item[COMPONENT_KEY]))?.definition\n : undefined;\n if (component && isRecord(item)) {\n collect(model, component.fields, item, add);\n }\n }\n break;\n default:\n break;\n }\n }\n};\n\n/** Every (asset, top-level field) pair a document uses, without repeats. */\nexport const mediaUsesOf = (model: ContentModel, data: Readonly<ContentData>): MediaUse[] => {\n const uses = new Map<string, MediaUse>();\n for (const field of model.definition.fields) {\n collect(model, [field], data, (assetId) =>\n uses.set(`${assetId}|${field.id}`, { assetId, fieldId: field.id }),\n );\n }\n return [...uses.values()];\n};\n\n/** Asset IDs used by the given top-level fields of the documents. */\nexport const mediaIdsOf = (\n model: ContentModel,\n documents: readonly Readonly<ContentData>[],\n fields: readonly FieldDefinition[],\n): Set<string> => {\n const ids = new Set<string>();\n for (const data of documents) {\n collect(model, fields, data, (id) => ids.add(id));\n }\n return ids;\n};\n\n/** The `allowedKinds` family of a MIME type. */\nexport const mediaKindOf = (mimeType: string): string => {\n const [type = '', subtype = ''] = mimeType.toLowerCase().split('/');\n if (type === 'image' || type === 'video' || type === 'audio') {\n return type;\n }\n if (type === 'text' || subtype === 'pdf' || /(msword|officedocument|opendocument|rtf|epub)/.test(subtype)) {\n return 'document';\n }\n return 'other';\n};\n","import type { ComponentDefinition, FieldDefinition } from '@shapio/schema';\nimport type { ContentData } from '../db/contentData.js';\nimport type { ContentModel } from './model.js';\nimport { COMPONENT_KEY } from './validator/index.js';\n\n/**\n * Relations live in the head JSON (field → ordered target entry IDs, ADR 0001). These helpers walk a\n * document, including components and dynamic zones, to derive `relation_edges` rows and to find the\n * targets a response mentions.\n */\nexport type RelationVisit = { field: FieldDefinition & { type: 'relation' }; entryIds: string[] };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst referenceIds = (value: unknown): string[] =>\n (Array.isArray(value) ? value : [value]).filter((item): item is string => typeof item === 'string');\n\ntype Components = ContentModel['components'];\n\nconst walkFields = (\n fields: readonly FieldDefinition[],\n data: Readonly<Record<string, unknown>>,\n components: Components,\n visit: (relation: RelationVisit) => void,\n) => {\n for (const field of fields) {\n const value = data[field.id];\n if (value === undefined || value === null) {\n continue;\n }\n if (field.type === 'relation') {\n visit({ field, entryIds: referenceIds(value) });\n } else if (field.type === 'component') {\n const component = components.get(field.settings.component)?.definition;\n for (const item of Array.isArray(value) ? value : [value]) {\n if (component && isRecord(item)) {\n walkFields(component.fields, item, components, visit);\n }\n }\n } else if (field.type === 'dynamiczone' && Array.isArray(value)) {\n for (const item of value) {\n const component: ComponentDefinition | undefined = isRecord(item)\n ? components.get(String(item[COMPONENT_KEY]))?.definition\n : undefined;\n if (component && isRecord(item)) {\n walkFields(component.fields, item, components, visit);\n }\n }\n }\n }\n};\n\n/** Calls `visit` for every relation value in a document, in document order. */\nexport const walkRelations = (\n model: ContentModel,\n data: Readonly<ContentData>,\n visit: (relation: RelationVisit) => void,\n topLevelFields: readonly FieldDefinition[] = model.definition.fields,\n) => walkFields(topLevelFields, data, model.components, visit);\n\nexport type RelationEdge = { fieldId: string; position: number; targetEntryId: string };\n\n/** The `relation_edges` rows of one head: per field, targets numbered in document order. */\nexport const edgesOf = (model: ContentModel, data: Readonly<ContentData>): RelationEdge[] => {\n const positions = new Map<string, number>();\n const edges: RelationEdge[] = [];\n walkRelations(model, data, ({ field, entryIds }) => {\n for (const targetEntryId of entryIds) {\n const position = positions.get(field.id) ?? 0;\n positions.set(field.id, position + 1);\n edges.push({ fieldId: field.id, position, targetEntryId });\n }\n });\n return edges;\n};\n\n/** Every target a set of documents mentions, grouped by target model. */\nexport const targetsByModel = (\n model: ContentModel,\n documents: readonly Readonly<ContentData>[],\n topLevelFields?: readonly FieldDefinition[],\n): Map<string, Set<string>> => {\n const targets = new Map<string, Set<string>>();\n for (const data of documents) {\n walkRelations(\n model,\n data,\n ({ field, entryIds }) => {\n const set = targets.get(field.settings.target) ?? new Set<string>();\n entryIds.forEach((id) => set.add(id));\n targets.set(field.settings.target, set);\n },\n topLevelFields,\n );\n }\n return targets;\n};\n","import {\n effectiveTitleField,\n isSeoField,\n resolveSeo,\n SEO_COMPONENT_ID,\n SEO_FIELD_IDS,\n seoDefaultsForLocale,\n type DataType,\n type FieldDefinition,\n type SeoDefaults,\n type SeoFieldKey,\n type SeoKeys,\n} from '@shapio/schema';\nimport type { Policy } from '../permissions/types.js';\nimport type { HeadRow } from './compiler/compile.js';\nimport { maskAllows } from './compiler/policy.js';\nimport { fallbackChain } from './locales.js';\nimport type { ContentModel } from './model.js';\nimport type { ReadEnvironment } from './read.js';\n\n/**\n * `?seo=resolved` (plan seo-fields): the SEO fields a delivered entry carries, with the site's defaults filled\n * in. The built-in component is found by stable ID; its fields by stable ID and the type they were created\n * with, so a renamed field still resolves and a removed or retyped one is passed through untouched.\n */\nexport type SeoReadOptions = {\n defaults: SeoDefaults;\n /** The default social image in the delivered shape, when it is a live public image of the site. */\n image: unknown;\n};\n\nconst EXPECTED_TYPES: Readonly<Record<SeoFieldKey, ReadonlySet<DataType>>> = {\n title: new Set(['string', 'text']),\n description: new Set(['text', 'string']),\n image: new Set(['media']),\n canonical: new Set(['url', 'string']),\n noindex: new Set(['boolean']),\n};\n\nconst keysOf = (model: ContentModel): SeoKeys => {\n const component = model.components.get(SEO_COMPONENT_ID)?.definition;\n const keys: SeoKeys = {};\n for (const [key, id] of Object.entries(SEO_FIELD_IDS) as Array<[SeoFieldKey, string]>) {\n const field = component?.fields.find((candidate) => candidate.id === id && !candidate.deprecated);\n const single = field?.type !== 'media' || !field.settings.multiple;\n if (field && single && EXPECTED_TYPES[key].has(field.type)) {\n keys[key] = field.apiKey;\n }\n }\n return keys;\n};\n\n/** The entry's own title, only when the caller may read the title field (never a hidden value). */\nconst fallbackTitleOf = (model: ContentModel, policy: Policy, row: HeadRow): string | null => {\n const field = effectiveTitleField(model.definition);\n if (!field || !maskAllows(policy.readMask, field)) {\n return null;\n }\n const value = row.data[field.id];\n return typeof value === 'string' ? value : null;\n};\n\n/** The locale whose defaults apply: the head's for localized models, the requested one otherwise. */\nconst defaultsLocaleOf = (env: ReadEnvironment, model: ContentModel, row: HeadRow): string =>\n model.definition.localized ? row.locale : (env.locale ?? env.snapshot.defaultLocale);\n\n/** Whether the selected fields include an SEO field (only those are resolved). */\nexport const seoFieldsOf = (fields: readonly FieldDefinition[]): FieldDefinition[] =>\n fields.filter(isSeoField);\n\n/** Replaces each SEO field's projected value with its resolved value. */\nexport const resolveSeoFields = (\n env: ReadEnvironment,\n options: SeoReadOptions,\n model: ContentModel,\n policy: Policy,\n fields: readonly FieldDefinition[],\n row: HeadRow,\n data: Record<string, unknown>,\n): Record<string, unknown> => {\n const seoFields = seoFieldsOf(fields);\n if (seoFields.length === 0) {\n return data;\n }\n const keys = keysOf(model);\n const context = {\n defaults: seoDefaultsForLocale(\n options.defaults,\n fallbackChain(env.snapshot, defaultsLocaleOf(env, model, row)),\n ),\n image: options.image,\n fallbackTitle: fallbackTitleOf(model, policy, row),\n keys,\n };\n const output = { ...data };\n for (const field of seoFields) {\n output[field.apiKey] = resolveSeo(data[field.apiKey], context);\n }\n return output;\n};\n","import type { FieldDefinition, SeoDefaults } from '@shapio/schema';\nimport type { Kysely, Transaction } from 'kysely';\nimport type { DB } from '../db/types.js';\nimport type { PermissionEvaluator, Policy, Principal } from '../permissions/types.js';\nimport * as contentQueriesRepository from '../repositories/contentQueries.js';\nimport * as mediaAssetsRepository from '../repositories/mediaAssets.js';\nimport type { SchemaSnapshot } from '../schema/snapshot.js';\nimport { toAssetViews, type MediaAssetView } from '../services/mediaViews.js';\nimport { compileHeadQuery, type HeadRow, type HeadSource, type LocaleScope } from './compiler/compile.js';\nimport { entryIdIn } from './compiler/conditions.js';\nimport { compileRowFilter } from './compiler/policy.js';\nimport { populateRelations, type PopulateEnvironment } from './compiler/populate.js';\nimport { projectData, selectFields } from './compiler/select.js';\nimport { DEFAULT_RICH_TEXT_MODE, type PopulateTree, type RichTextMode } from './compiler/types.js';\nimport { readScopeFor } from './locales.js';\nimport { mediaIdsOf } from './media.js';\nimport { resolveModelById, type ContentModel } from './model.js';\nimport { targetsByModel } from './relations.js';\nimport { resolveSeoFields, seoFieldsOf } from './seo.js';\n\n/**\n * The read side shared by the admin and delivery APIs: fetching heads under a policy, deciding which\n * relation targets a caller may see, populating, and projecting through the read mask.\n */\ntype MediaViewDependencies = Parameters<typeof toAssetViews>[0];\n\nexport type ReadEnvironment = {\n executor: Kysely<DB> | Transaction<DB>;\n snapshot: SchemaSnapshot;\n permissions: PermissionEvaluator;\n actor: Principal;\n /** The site read from (sites plan §H): heads, relation targets and media never cross it. */\n siteId: string;\n /** Admin reads see draft heads and every relation ID; delivery reads see published content only. */\n audience: 'admin' | 'delivery';\n source: HeadSource;\n /** Requested locale (the default when absent). */\n locale: string | undefined;\n /** Media storage for asset URLs; without it media fields stay asset IDs. */\n media?: MediaViewDependencies;\n /** Delivery reads: the shape of rich-text values, populated targets included (`?richText=`; the default when absent). */\n richText?: RichTextMode;\n /** Delivery reads with `?seo=resolved`: the site's SEO defaults, merged into SEO fields. */\n seo?: SeoDefaults;\n};\n\n/** Evaluated through the read's executor: a read inside a transaction never asks the pool for a second connection. */\nexport const readPolicy = (env: ReadEnvironment, modelId: string): Promise<Policy> =>\n env.permissions.evaluate(env.actor, { action: 'read', modelId }, env.executor);\n\n/**\n * Heads of the given entries that the policy lets the caller read, served per the locale chain (or per\n * `locales` when given: GraphQL's `fallback: false` and `localizations`).\n */\nexport const fetchHeadsByIds = async (\n env: ReadEnvironment,\n model: ContentModel,\n policy: Policy,\n ids: readonly string[],\n locales?: LocaleScope,\n): Promise<HeadRow[]> => {\n if (ids.length === 0 || !policy.allowed) {\n return [];\n }\n const rowFilter = compileRowFilter(policy.rowFilter, env.actor);\n const { rows } = compileHeadQuery({\n siteId: env.siteId,\n modelId: model.definition.id,\n source: env.source,\n locales: locales ?? readScopeFor(env.snapshot, model.definition, env.locale, { fallback: true }),\n conditions: [entryIdIn(ids), ...(rowFilter ? [rowFilter] : [])],\n orderBy: [],\n });\n return contentQueriesRepository.runHeadQuery(rows, env.executor);\n};\n\n/** Target heads read for relation visibility, by target model, for populate to reuse (same policy and locales). */\ntype FetchedTargets = Map<string, { ids: ReadonlySet<string>; rows: readonly HeadRow[] }>;\n\n/**\n * Relation targets the caller may see in these rows: for delivery, only targets that are published (at the\n * same snapshot) and readable under their model's policy, so a published entry never leaks the ID of a\n * draft (brief §10). Admin reads see every ID (null). The heads read are returned for populate.\n */\nconst relationTargets = async (\n env: ReadEnvironment,\n model: ContentModel,\n rows: readonly HeadRow[],\n fields: readonly FieldDefinition[],\n): Promise<{ visible: Set<string> | null; fetched: FetchedTargets }> => {\n const fetched: FetchedTargets = new Map();\n if (env.audience === 'admin') {\n return { visible: null, fetched };\n }\n const visible = new Set<string>();\n const targets = targetsByModel(\n model,\n rows.map((row) => row.data),\n fields,\n );\n for (const [targetModelId, ids] of targets) {\n const target = resolveModelById(env.snapshot, targetModelId);\n if (!target) {\n continue;\n }\n const policy = await readPolicy(env, targetModelId);\n const found = await fetchHeadsByIds(env, target, policy, [...ids]);\n found.forEach((row) => visible.add(row.entry_id));\n fetched.set(targetModelId, { ids, rows: found });\n }\n return { visible, fetched };\n};\n\n/**\n * `fetchHeadsByIds` for populate, answered from the heads relation visibility already read when it asked for\n * every one of these IDs under the same policy and locale scope (populate's default), else from the database.\n */\nconst fetchReusing =\n (fetched: FetchedTargets): PopulateEnvironment['fetch'] =>\n async (env, model, policy, ids) => {\n const known = fetched.get(model.definition.id);\n if (known && ids.every((id) => known.ids.has(id))) {\n const wanted = new Set(ids);\n return known.rows.filter((row) => wanted.has(row.entry_id));\n }\n return fetchHeadsByIds(env, model, policy, ids);\n };\n\n/**\n * Whether projecting these rows reads anything more (relation targets, populated entries, asset views). When\n * not, a delivery read is complete in the one statement that fetched the rows (plan delivery-perf).\n */\nexport const needsFollowUpReads = (\n env: ReadEnvironment,\n model: ContentModel,\n rows: readonly HeadRow[],\n query: { fields: readonly FieldDefinition[] | null; populate: PopulateTree },\n policy: Policy,\n): boolean => {\n if (query.populate.size > 0) {\n return true;\n }\n const fields = selectFields(model, policy.readMask, query.fields);\n const documents = rows.map((row) => row.data);\n return (\n (env.audience === 'delivery' && targetsByModel(model, documents, fields).size > 0) ||\n needsMediaReads(env, model, rows, fields)\n );\n};\n\n/** Whether projecting these rows reads asset views (media fields, rich-text images, a default SEO image). */\nexport const needsMediaReads = (\n env: ReadEnvironment,\n model: ContentModel,\n rows: readonly HeadRow[],\n fields: readonly FieldDefinition[],\n): boolean =>\n env.media !== undefined &&\n (mediaIdsOf(\n model,\n rows.map((row) => row.data),\n fields,\n ).size > 0 ||\n seoImageIdOf(env, fields) !== undefined);\n\n/** The site's default social image a `?seo=resolved` read needs (when the response carries an SEO field). */\nconst seoImageIdOf = (env: ReadEnvironment, fields: readonly FieldDefinition[]): string | undefined =>\n env.seo?.imageId && seoFieldsOf(fields).length > 0 ? env.seo.imageId : undefined;\n\n/** A default SEO image is shown only while it is a public image (never through a signed URL). */\nexport const isPublicImage = (view: Pick<MediaAssetView, 'visibility' | 'mimeType'>): boolean =>\n view.visibility === 'public' && view.mimeType.startsWith('image/');\n\ntype MediaViews = {\n /** Views by asset ID, in the audience's shape; null leaves IDs as stored. */\n byId: Map<string, { url: string }> | null;\n /** The default social image in the delivered shape, only when it is a live public image. */\n seoImage: unknown;\n};\n\n/**\n * Asset views for the media a response shows (media fields and rich-text images). Private assets get signed,\n * expiring URLs (package G); the read mask already decided whether the caller may see the field at all.\n * Delivery views carry only what a site needs, never storage keys or internal attribution.\n */\nconst loadMediaViews = async (\n env: ReadEnvironment,\n model: ContentModel,\n rows: readonly HeadRow[],\n fields: readonly FieldDefinition[],\n): Promise<MediaViews> => {\n if (!env.media) {\n return { byId: null, seoImage: null };\n }\n const ids = mediaIdsOf(\n model,\n rows.map((row) => row.data),\n fields,\n );\n const seoImageId = seoImageIdOf(env, fields);\n if (seoImageId !== undefined) {\n ids.add(seoImageId);\n }\n if (ids.size === 0) {\n return { byId: new Map(), seoImage: null };\n }\n const assets = await mediaAssetsRepository.findLiveManyOnSite(env.siteId, [...ids], env.executor);\n const views = await toAssetViews(env.media, assets, env.executor);\n const shaped = (view: MediaAssetView) => (env.audience === 'delivery' ? toDeliveryAsset(view) : view);\n // Re-checked on every read: an image made private (or replaced by a non-image) after it was chosen never\n // appears, signed or not (rule 7).\n const seoImage = views.find((view) => view.id === seoImageId && isPublicImage(view));\n return {\n byId: new Map(views.map((view) => [view.id, shaped(view)])),\n seoImage: seoImage ? shaped(seoImage) : null,\n };\n};\n\n/** An asset as delivery shows it: what a site needs, never storage keys or internal attribution. */\nexport const toDeliveryAsset = (view: MediaAssetView) => ({\n id: view.id,\n filename: view.filename,\n mimeType: view.mimeType,\n sizeBytes: view.sizeBytes,\n width: view.width,\n height: view.height,\n alt: view.alt,\n caption: view.caption,\n focalPoint: view.focalPoint,\n url: view.url,\n urlExpiresAt: view.urlExpiresAt?.toISOString() ?? null,\n variants: view.variants\n .filter((variant) => variant.url !== null)\n .map(({ name, width, height, format, mimeType, url }) => ({\n name,\n width,\n height,\n format,\n mimeType,\n url,\n })),\n});\n\nexport type ProjectedEntry = Record<string, unknown>;\n\n/** System attributes first, then the readable, selected fields by API key. */\nexport const systemAttributes = (env: ReadEnvironment, row: HeadRow): Record<string, unknown> => ({\n id: row.entry_id,\n locale: row.locale,\n createdAt: row.created_at.toISOString(),\n updatedAt: row.updated_at.toISOString(),\n ...(env.audience === 'delivery' ? { publishedAt: row.updated_at.toISOString() } : {}),\n});\n\nexport const projectRows = async (\n env: ReadEnvironment,\n model: ContentModel,\n policy: Policy,\n rows: readonly HeadRow[],\n query: { fields: readonly FieldDefinition[] | null; populate: PopulateTree },\n): Promise<Array<{ row: HeadRow; data: ProjectedEntry }>> => {\n const fields = selectFields(model, policy.readMask, query.fields);\n const { visible: visibleTargets, fetched } = await relationTargets(env, model, rows, fields);\n const populateEnv: PopulateEnvironment = {\n ...env,\n project: projectRows,\n fetch: fetchReusing(fetched),\n policyFor: readPolicy,\n system: systemAttributes,\n };\n const populated = await populateRelations(populateEnv, rows, fields, query.populate);\n const media = await loadMediaViews(env, model, rows, fields);\n return rows.map((row) => {\n const data = projectData(row.data, {\n model,\n fields,\n visibleTargets,\n populated,\n ...(env.audience === 'delivery' ? { richText: env.richText ?? DEFAULT_RICH_TEXT_MODE } : {}),\n mediaAssets: media.byId,\n });\n return {\n row,\n data: env.seo\n ? resolveSeoFields(\n env,\n { defaults: env.seo, image: media.seoImage },\n model,\n policy,\n fields,\n row,\n data,\n )\n : data,\n };\n });\n};\n","import { QUERY_LIMITS, queryInvalid } from './types.js';\n\n/**\n * Bracket querystrings (`filters[title][$eq]=x`, `filters[$or][0][slug][$eq]=y`, `fields[0]=title`) to a\n * nested object of strings. Lists stay objects with numeric keys here; `parse.ts` decides where a list is\n * expected. Bounded in parameter count, depth and length so a hostile query cannot cost much.\n */\nexport type QueryTree = { [key: string]: QueryValue };\nexport type QueryValue = string | string[] | QueryTree;\n\nconst SEGMENT = /^\\[([^[\\]]*)\\]/;\nconst MAX_DEPTH = 10;\nconst MAX_KEY_LENGTH = 300;\n\nconst splitKey = (key: string): string[] => {\n const open = key.indexOf('[');\n if (open === -1) {\n return [key];\n }\n const segments = [key.slice(0, open)];\n let rest = key.slice(open);\n while (rest.length > 0) {\n const match = SEGMENT.exec(rest);\n if (!match) {\n throw queryInvalid(`Malformed query parameter \"${key}\"`);\n }\n segments.push(match[1] as string);\n rest = rest.slice(match[0].length);\n }\n return segments;\n};\n\nconst isTree = (value: QueryValue | undefined): value is QueryTree =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst assign = (tree: QueryTree, segments: readonly string[], value: string, key: string) => {\n let node = tree;\n segments.forEach((segment, index) => {\n if (segment === '' || segment === '__proto__' || segment === 'constructor' || segment === 'prototype') {\n throw queryInvalid(`Malformed query parameter \"${key}\"`);\n }\n const last = index === segments.length - 1;\n const existing = Object.hasOwn(node, segment) ? node[segment] : undefined;\n if (last) {\n if (existing === undefined) {\n node[segment] = value;\n } else if (typeof existing === 'string') {\n node[segment] = [existing, value];\n } else if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n throw queryInvalid(`Query parameter \"${key}\" conflicts with another parameter`);\n }\n return;\n }\n if (existing === undefined) {\n const child: QueryTree = Object.create(null) as QueryTree;\n node[segment] = child;\n node = child;\n } else if (isTree(existing)) {\n node = existing;\n } else {\n throw queryInvalid(`Query parameter \"${key}\" conflicts with another parameter`);\n }\n });\n};\n\nexport const parseQueryTree = (search: string): QueryTree => {\n const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);\n const tree: QueryTree = Object.create(null) as QueryTree;\n let count = 0;\n for (const [key, value] of params) {\n count += 1;\n if (count > QUERY_LIMITS.maxParameters) {\n throw queryInvalid(`At most ${QUERY_LIMITS.maxParameters} query parameters are allowed`);\n }\n if (key.length > MAX_KEY_LENGTH || value.length > QUERY_LIMITS.maxValueLength) {\n throw queryInvalid('A query parameter is too long');\n }\n const segments = splitKey(key);\n if (segments.length > MAX_DEPTH) {\n throw queryInvalid(`Query parameter \"${key}\" is nested too deeply`);\n }\n assign(tree, segments, value, key);\n }\n return tree;\n};\n\n/** A list value: repeated parameters, `a,b` comma lists, or `[0]`, `[1]`… keys. */\nexport const toList = (value: QueryValue | undefined, { commas = true } = {}): QueryValue[] => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value;\n }\n if (typeof value === 'string') {\n return commas ? value.split(',').filter((item) => item.length > 0) : [value];\n }\n const keys = Object.keys(value);\n if (!keys.every((key) => /^\\d+$/.test(key))) {\n throw queryInvalid('Expected a list');\n }\n return keys.sort((a, b) => Number(a) - Number(b)).map((key) => value[key] as QueryValue);\n};\n","import {\n effectiveTitleField,\n isSeoField,\n isStableId,\n TEXT_TITLE_FIELD_TYPES,\n type FieldDefinition,\n type ModelDefinition,\n} from '@shapio/schema';\nimport { findFieldByApiKey } from '../model.js';\nimport { assertOperatorAllowed, coerceOperand, isListOperator, isSortable } from './operators.js';\nimport { toList, type QueryTree, type QueryValue } from './querystring.js';\nimport {\n DEFAULT_RICH_TEXT_MODE,\n ENTRY_LIST_STATUSES,\n FILTER_OPERATORS,\n QUERY_LIMITS,\n queryForbidden,\n queryInvalid,\n RICH_TEXT_MODES,\n SEO_MODES,\n SYSTEM_ATTRIBUTES,\n type ContentQuery,\n type EntryListStatus,\n type FilterNode,\n type FilterOperator,\n type FilterTarget,\n type PopulateTree,\n type RichTextMode,\n type SeoMode,\n type SortTerm,\n type SystemAttribute,\n} from './types.js';\n\n/**\n * Querystring → content query AST, allowlisted against the pinned schema (brief §4 \"runtime APIs\"). Every\n * field name must be a live field of the model, every operator must be allowed for its type, and fields\n * outside the caller's read mask are rejected with 403 so filters, sorts and searches can never be used as\n * an oracle for hidden values (ADR 0005). Unknown parameters, fields and operators are 400s.\n */\nexport type ParseContext = {\n model: ModelDefinition;\n /** Locale codes the instance has. */\n locales: readonly string[];\n /** The read mask, per field. */\n isReadable: (field: FieldDefinition) => boolean;\n /** `?snapshot=N` is a delivery feature. */\n allowSnapshot: boolean;\n /** `?status=` and `?author=` are admin list features. */\n allowAdminFilters?: boolean;\n /** `?richText=` is a delivery and preview feature (admin reads return the stored document). */\n allowRichText?: boolean;\n /** `?seo=` is a delivery and preview feature. */\n allowSeo?: boolean;\n /** Target models of relation fields, for nested populate paths. */\n resolveModel: (modelId: string) => ModelDefinition | undefined;\n};\n\nconst TOP_LEVEL_KEYS = new Set([\n 'filters',\n 'sort',\n 'page',\n 'pageSize',\n 'fields',\n 'populate',\n 'locale',\n 'snapshot',\n 'richText',\n 'seo',\n 'q',\n 'status',\n 'author',\n]);\nconst OPERATOR_SET: ReadonlySet<string> = new Set(FILTER_OPERATORS);\n\nconst isTree = (value: QueryValue | undefined): value is QueryTree =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst single = (value: QueryValue | undefined, name: string): string | undefined => {\n if (value === undefined) {\n return undefined;\n }\n if (typeof value !== 'string') {\n throw queryInvalid(`\"${name}\" must be given once`);\n }\n return value;\n};\n\nconst positiveInteger = (value: QueryValue | undefined, name: string, max: number): number | undefined => {\n const text = single(value, name);\n if (text === undefined) {\n return undefined;\n }\n if (!/^\\d{1,15}$/.test(text) || Number(text) < 1 || Number(text) > max) {\n throw queryInvalid(`\"${name}\" must be an integer between 1 and ${max}`);\n }\n return Number(text);\n};\n\ntype FilterState = { context: ParseContext; conditions: number };\n\nconst resolveTarget = (context: ParseContext, key: string): FilterTarget => {\n if ((SYSTEM_ATTRIBUTES as readonly string[]).includes(key)) {\n return { kind: 'system', name: key as SystemAttribute };\n }\n const field = findFieldByApiKey(context.model.fields, key);\n if (!field) {\n throw queryInvalid(`Unknown field \"${key}\"`, { field: key });\n }\n if (!context.isReadable(field)) {\n throw queryForbidden(`You may not filter, sort or search on \"${key}\"`, { field: key });\n }\n return { kind: 'field', field };\n};\n\nconst conditionFor = (\n state: FilterState,\n target: FilterTarget,\n operator: string,\n raw: QueryValue,\n label: string,\n): FilterNode => {\n if (!OPERATOR_SET.has(operator)) {\n throw queryInvalid(`Unknown operator \"${operator}\" on \"${label}\"`);\n }\n const op = operator as FilterOperator;\n state.conditions += 1;\n if (state.conditions > QUERY_LIMITS.maxConditions) {\n throw queryInvalid(`At most ${QUERY_LIMITS.maxConditions} filter conditions are allowed`);\n }\n assertOperatorAllowed(target, op, label);\n if (isTree(raw) && !isListOperator(op)) {\n throw queryInvalid(`\"${label}\" ${op} needs a value`);\n }\n const operand = isListOperator(op) ? toList(raw) : raw;\n if (!isListOperator(op) && Array.isArray(operand)) {\n throw queryInvalid(`\"${label}\" ${op} needs a single value`);\n }\n return {\n kind: 'condition',\n target,\n operator: op,\n value: coerceOperand(target, op, operand, label, QUERY_LIMITS.maxInValues),\n };\n};\n\nconst combine = (kind: 'and' | 'or', nodes: FilterNode[]): FilterNode =>\n nodes.length === 1 ? (nodes[0] as FilterNode) : { kind, nodes };\n\nconst parseFilterTree = (state: FilterState, tree: QueryTree, depth: number): FilterNode => {\n if (depth > QUERY_LIMITS.maxFilterDepth) {\n throw queryInvalid('Filters are nested too deeply');\n }\n const nodes: FilterNode[] = [];\n for (const [key, value] of Object.entries(tree)) {\n if (key === '$and' || key === '$or') {\n const children = toList(value, { commas: false }).map((child) => {\n if (!isTree(child)) {\n throw queryInvalid(`${key} needs a list of filter objects`);\n }\n return parseFilterTree(state, child, depth + 1);\n });\n if (children.length === 0) {\n throw queryInvalid(`${key} needs at least one filter`);\n }\n nodes.push(combine(key === '$and' ? 'and' : 'or', children));\n } else if (key === '$not') {\n if (!isTree(value)) {\n throw queryInvalid('$not needs a filter object');\n }\n nodes.push({ kind: 'not', node: parseFilterTree(state, value, depth + 1) });\n } else if (key.startsWith('$')) {\n throw queryInvalid(`Unknown filter keyword \"${key}\"`);\n } else {\n const target = resolveTarget(state.context, key);\n if (!isTree(value)) {\n nodes.push(conditionFor(state, target, '$eq', value, key));\n continue;\n }\n const operators = Object.entries(value);\n if (operators.length === 0) {\n throw queryInvalid(`\"${key}\" needs an operator`);\n }\n nodes.push(\n combine(\n 'and',\n operators.map(([operator, operand]) => conditionFor(state, target, operator, operand, key)),\n ),\n );\n }\n }\n if (nodes.length === 0) {\n throw queryInvalid('Empty filter');\n }\n return combine('and', nodes);\n};\n\nconst parseSort = (context: ParseContext, value: QueryValue | undefined): SortTerm[] => {\n const items = toList(value);\n if (items.length > QUERY_LIMITS.maxSortTerms) {\n throw queryInvalid(`At most ${QUERY_LIMITS.maxSortTerms} sort terms are allowed`);\n }\n return items.map((item) => {\n if (typeof item !== 'string') {\n throw queryInvalid('sort needs \"field\" or \"field:asc|desc\" values');\n }\n const [key = '', direction = 'asc', extra] = item.split(':');\n if (extra !== undefined || (direction !== 'asc' && direction !== 'desc')) {\n throw queryInvalid(`Invalid sort \"${item}\"`);\n }\n const target = resolveTarget(context, key);\n if (target.kind === 'field' && !isSortable(target.field)) {\n throw queryInvalid(`\"${key}\" is not sortable: mark it sortable first`);\n }\n return { target, direction };\n });\n};\n\nconst parseFields = (context: ParseContext, value: QueryValue | undefined): FieldDefinition[] | null => {\n if (value === undefined) {\n return null;\n }\n return toList(value).map((item) => {\n if (typeof item !== 'string') {\n throw queryInvalid('fields needs a list of field names');\n }\n const target = resolveTarget(context, item);\n if (target.kind === 'system') {\n throw queryInvalid(`\"${item}\" is always returned; list only model fields`);\n }\n return target.field;\n });\n};\n\nconst relationField = (context: ParseContext, model: ModelDefinition, key: string): FieldDefinition => {\n const field = findFieldByApiKey(model.fields, key);\n if (!field || field.type !== 'relation') {\n throw queryInvalid(`\"${key}\" is not a relation of \"${model.apiKey}\" and cannot be populated`);\n }\n if (model.id === context.model.id && !context.isReadable(field)) {\n throw queryForbidden(`You may not populate \"${key}\"`, { field: key });\n }\n return field;\n};\n\ntype MutablePopulate = Map<string, MutablePopulate>;\n\nconst parsePopulate = (context: ParseContext, value: QueryValue | undefined): PopulateTree => {\n const root: MutablePopulate = new Map();\n for (const item of toList(value)) {\n if (typeof item !== 'string') {\n throw queryInvalid('populate needs a list of relation paths');\n }\n if (item === '*') {\n for (const field of context.model.fields) {\n if (field.type === 'relation' && !field.deprecated && context.isReadable(field)) {\n root.set(field.id, root.get(field.id) ?? new Map<string, MutablePopulate>());\n }\n }\n continue;\n }\n const segments = item.split('.');\n if (segments.length > QUERY_LIMITS.maxPopulateDepth) {\n throw queryInvalid(`populate is limited to ${QUERY_LIMITS.maxPopulateDepth} levels`);\n }\n let model: ModelDefinition | undefined = context.model;\n let node = root;\n for (const segment of segments) {\n if (!model) {\n throw queryInvalid(`Cannot populate \"${item}\"`);\n }\n const field = relationField(context, model, segment);\n const next: MutablePopulate = node.get(field.id) ?? new Map<string, MutablePopulate>();\n node.set(field.id, next);\n node = next;\n model = field.type === 'relation' ? context.resolveModel(field.settings.target) : undefined;\n }\n }\n return root;\n};\n\nconst parseSearch = (context: ParseContext, value: QueryValue | undefined): ContentQuery['search'] => {\n const text = single(value, 'q')?.trim();\n if (!text) {\n return null;\n }\n const field = effectiveTitleField(context.model);\n if (!field || !TEXT_TITLE_FIELD_TYPES.has(field.type)) {\n throw queryInvalid(`\"${context.model.apiKey}\" has no text title field to search`);\n }\n if (!context.isReadable(field)) {\n throw queryForbidden('You may not search this model', { field: field.apiKey });\n }\n return { field, text };\n};\n\n/** Parses a bracket querystring tree into a content query for one model. */\nconst STATUS_SET: ReadonlySet<string> = new Set(ENTRY_LIST_STATUSES);\n\n/** `status` and `author` (admin lists only). */\nconst parseAdminFilters = (tree: QueryTree, context: ParseContext) => {\n const status = single(tree.status, 'status');\n const author = single(tree.author, 'author');\n if ((status !== undefined || author !== undefined) && !context.allowAdminFilters) {\n throw queryInvalid('\"status\" and \"author\" are only available on the admin API');\n }\n if (status !== undefined && !STATUS_SET.has(status)) {\n throw queryInvalid(`\"status\" must be one of ${ENTRY_LIST_STATUSES.join(', ')}`);\n }\n if (author !== undefined && !isStableId(author)) {\n throw queryInvalid('\"author\" must be an admin user ID');\n }\n return {\n ...(status !== undefined ? { status: status as EntryListStatus } : {}),\n ...(author !== undefined ? { author } : {}),\n };\n};\n\nconst RICH_TEXT_MODE_SET: ReadonlySet<string> = new Set(RICH_TEXT_MODES);\n\n/** `?richText=json|html|both` on delivery and preview reads; the default there, nothing on admin reads. */\nconst parseRichText = (tree: QueryTree, context: ParseContext): { richText?: RichTextMode } => {\n const value = single(tree.richText, 'richText');\n if (!context.allowRichText) {\n if (value !== undefined) {\n throw queryInvalid('richText is only available on the delivery and preview APIs');\n }\n return {};\n }\n if (value !== undefined && !RICH_TEXT_MODE_SET.has(value)) {\n throw queryInvalid(`\"richText\" must be one of ${RICH_TEXT_MODES.join(', ')}`);\n }\n return { richText: (value as RichTextMode | undefined) ?? DEFAULT_RICH_TEXT_MODE };\n};\n\nconst SEO_MODE_SET: ReadonlySet<string> = new Set(SEO_MODES);\n\n/** `?seo=raw|resolved` on delivery and preview reads; `resolved` needs a readable SEO field on the model. */\nconst parseSeo = (tree: QueryTree, context: ParseContext): { seo?: SeoMode } => {\n const value = single(tree.seo, 'seo');\n if (value === undefined) {\n return {};\n }\n if (!context.allowSeo) {\n throw queryInvalid('seo is only available on the delivery and preview APIs');\n }\n if (!SEO_MODE_SET.has(value)) {\n throw queryInvalid(`\"seo\" must be one of ${SEO_MODES.join(', ')}`);\n }\n if (\n value === 'resolved' &&\n !context.model.fields.some((field) => !field.deprecated && isSeoField(field) && context.isReadable(field))\n ) {\n throw queryInvalid(`\"${context.model.apiKey}\" has no SEO field to resolve`);\n }\n return { seo: value as SeoMode };\n};\n\nexport const parseContentQuery = (tree: QueryTree, context: ParseContext): ContentQuery => {\n for (const key of Object.keys(tree)) {\n if (!TOP_LEVEL_KEYS.has(key)) {\n throw queryInvalid(`Unknown query parameter \"${key}\"`);\n }\n }\n const filters = tree.filters;\n if (filters !== undefined && !isTree(filters)) {\n throw queryInvalid('filters needs field conditions, e.g. filters[title][$eq]=Hello');\n }\n const locale = single(tree.locale, 'locale');\n if (locale !== undefined && !context.locales.includes(locale)) {\n throw queryInvalid(`Unknown locale \"${locale}\"`);\n }\n const snapshot = positiveInteger(tree.snapshot, 'snapshot', Number.MAX_SAFE_INTEGER);\n if (snapshot !== undefined && !context.allowSnapshot) {\n throw queryInvalid('snapshot is only available on the delivery API');\n }\n return {\n filter: filters ? parseFilterTree({ context, conditions: 0 }, filters, 1) : null,\n search: parseSearch(context, tree.q),\n sort: parseSort(context, tree.sort),\n page: positiveInteger(tree.page, 'page', 1_000_000) ?? 1,\n pageSize:\n positiveInteger(tree.pageSize, 'pageSize', QUERY_LIMITS.maxPageSize) ?? QUERY_LIMITS.defaultPageSize,\n fields: parseFields(context, tree.fields),\n populate: parsePopulate(context, tree.populate),\n locale,\n snapshot,\n ...parseRichText(tree, context),\n ...parseSeo(tree, context),\n ...parseAdminFilters(tree, context),\n };\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport type { ContentData } from '../db/contentData.js';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type RevisionReason =\n 'create' | 'save' | 'publish' | 'restore' | 'duplicate' | 'localize' | 'conversion';\n\nexport type NewRevision = {\n entryId: string;\n locale: string;\n schemaRevisionId: string;\n data: ContentData;\n reason: RevisionReason;\n authorType: string;\n authorId: string | null;\n parentRevisionId: string | null;\n};\n\nconst SUMMARY_COLUMNS = [\n 'id',\n 'entry_id',\n 'locale',\n 'schema_revision_id',\n 'reason',\n 'author_type',\n 'author_id',\n 'parent_revision_id',\n 'created_at',\n] as const;\n\nexport const insert = (revision: NewRevision, trx: Executor = db) =>\n trx\n .insertInto('content_revisions')\n .values({\n entry_id: revision.entryId,\n locale: revision.locale,\n schema_revision_id: revision.schemaRevisionId,\n // pg serializes objects as JSON (content data is always an object, never an array).\n data: revision.data,\n reason: revision.reason,\n author_type: revision.authorType,\n author_id: revision.authorId,\n parent_revision_id: revision.parentRevisionId,\n })\n .returning(SUMMARY_COLUMNS)\n .executeTakeFirstOrThrow();\n\nexport const findById = (id: string, executor: Executor = db) =>\n executor\n .selectFrom('content_revisions')\n .select([...SUMMARY_COLUMNS, 'data'])\n .where('id', '=', id)\n .executeTakeFirst();\n\n/** Stored data of several revisions at once (titles and covers for the snapshot diff). */\nexport const findDataByIds = (ids: readonly string[], executor: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : executor\n .selectFrom('content_revisions')\n .select(['id', 'data'])\n .where('id', 'in', [...ids])\n .execute();\n\nexport const listForEntry = (\n entryId: string,\n locale: string | null,\n limit: number,\n executor: Executor = db,\n) => {\n let query = executor\n .selectFrom('content_revisions')\n .select(SUMMARY_COLUMNS)\n .where('entry_id', '=', entryId)\n .orderBy('created_at', 'desc')\n .orderBy('id', 'desc')\n .limit(limit);\n if (locale) {\n query = query.where('locale', '=', locale);\n }\n return query.execute();\n};\n\nexport type RevisionSummaryRow = Awaited<ReturnType<typeof listForEntry>>[number];\n\n/** Removes a deleted locale's history (purge follow-up). Revisions' parents never cross locales. */\nexport const deleteForLocale = (locale: string, trx: Executor = db) =>\n trx.deleteFrom('content_revisions').where('locale', '=', locale).executeTakeFirst();\n","import { sql, type Expression, type RawBuilder } from 'kysely';\nimport { isMysql, isSqlite } from '../dialect.js';\nimport { SequenceValue } from '../mysql/parameters.js';\nimport { asBigint, asJson } from './typed.js';\n\n/**\n * Typed values, arrays and sequences (dialect boundary, ADR 0001). Array columns (`text[]`) are read and\n * written as JavaScript arrays at the driver boundary; SQLite stores them as JSON text through its value\n * codec, so plain Kysely reads and writes of those columns stay unchanged.\n */\n\n/** Element types an array parameter may carry. */\nexport type ArrayElementType = 'uuid' | 'text';\n\nconst ARRAY_TYPES = {\n uuid: sql`uuid[]`,\n text: sql`text[]`,\n} as const satisfies Record<ArrayElementType, RawBuilder<unknown>>;\n\n/**\n * A bound value read as a UUID.\n * PostgreSQL: `cast($1 as uuid)`. SQLite: the lowercased text (UUIDs are stored as lowercase text, which\n * compares and sorts like PostgreSQL's uuid; other values compare case-sensitively there). MySQL: the\n * lowercased text too (UUID columns are `CHAR(36)` with a case-insensitive collation).\n */\nexport const uuidParam = (value: string | null): RawBuilder<string> =>\n isSqlite() || isMysql()\n ? sql<string>`${value === null ? null : value.toLowerCase()}`\n : sql<string>`cast(${value} as uuid)`;\n\n/**\n * `value` equals one of `values`, bound as a single parameter (so the statement text and plan do not\n * depend on the list length).\n * PostgreSQL: `value = any($1::uuid[])`. SQLite: `value in (select value from json_each($1))` with the list\n * bound as JSON. MySQL: `value in (?, …)` (its client binds parameters into the text, so there is no plan to\n * keep stable). Contract: false for an empty list; null never matches.\n */\nexport const anyOf = (\n value: Expression<unknown>,\n values: readonly string[],\n type: ArrayElementType,\n): RawBuilder<boolean> => {\n if (isMysql()) {\n return values.length === 0 ? sql<boolean>`false` : sql<boolean>`${value} in (${sql.join([...values])})`;\n }\n return isSqlite()\n ? sql<boolean>`${value} in (select value from json_each(${JSON.stringify(values)}))`\n : sql<boolean>`${value} = any(${[...values]}::${ARRAY_TYPES[type]})`;\n};\n\n/**\n * An array column contains `value`.\n * PostgreSQL: `$1 = any(column)`. SQLite: `exists (select 1 from json_each(column) where value = $1)`.\n * MySQL: `$1 member of (column)` (array columns are JSON).\n * Contract: false for a null or empty array.\n */\nexport const arrayContains = (\n column: Expression<readonly string[] | null>,\n value: string,\n): RawBuilder<boolean> => {\n if (isMysql()) {\n return sql<boolean>`${value} member of (${column})`;\n }\n return isSqlite()\n ? sql<boolean>`exists (select 1 from json_each(${column}) where value = ${value})`\n : sql<boolean>`${value} = any(${column})`;\n};\n\n/**\n * The values of `column` across the group, sorted by the value itself (optionally distinct); null when the\n * group is empty (pair with `emptyArray` in a `coalesce`, and mark the coalesce with `asJson`).\n * PostgreSQL: `array_agg([distinct] col order by col)`. SQLite: `json_group_array([distinct] col order by\n * col)`, which gives `[]` instead of null for an empty group. MySQL: `group_concat` of the JSON-quoted text\n * values, cast to JSON (`JSON_ARRAYAGG` has no ORDER BY); null for an empty group. Contract: a JavaScript\n * array once read.\n */\nexport const sortedArrayAgg = <T>(column: string, options: { distinct?: boolean } = {}): RawBuilder<T[]> => {\n const distinct = options.distinct ? sql`distinct ` : sql``;\n if (isMysql()) {\n return mysqlJsonArrayOf<T[]>(sql`${distinct}json_quote(${sql.ref(column)}) order by ${sql.ref(column)}`);\n }\n return isSqlite()\n ? asJson<T[]>(sql`json_group_array(${distinct}${sql.ref(column)} order by ${sql.ref(column)})`)\n : sql<T[]>`array_agg(${distinct}${sql.ref(column)} order by ${sql.ref(column)})`;\n};\n\n/**\n * An empty array of the given element type.\n * PostgreSQL: `'{}'::uuid[]`. SQLite: `'[]'`. MySQL: `json_array()`. Contract: an empty JavaScript array\n * once read.\n */\nexport const emptyArray = <T>(type: ArrayElementType): RawBuilder<T[]> => {\n if (isMysql()) {\n return sql<T[]>`json_array()`;\n }\n return isSqlite() ? asJson<T[]>(sql`'[]'`) : sql<T[]>`'{}'::${ARRAY_TYPES[type]}`;\n};\n\n/**\n * The value of the first row (by `orderColumn` descending) whose `column` is not null, per group.\n * PostgreSQL: `(array_agg(col order by o desc) filter (where col is not null))[1]`. SQLite: the first\n * element of the matching `json_group_array`. MySQL: the first element of an ordered `group_concat` (which\n * skips nulls), as text. Contract: null when no row of the group has a value. Mark the selected result with\n * its type (e.g. `asBigint`) for SQLite.\n */\nexport const latestNonNull = <T>(column: string, orderColumn: string): RawBuilder<T | null> => {\n if (isMysql()) {\n // group_concat skips nulls; the first element of the JSON array is the latest non-null value.\n return sql<T | null>`json_unquote(json_extract(${mysqlJsonArrayOf(\n sql`json_quote(cast(${sql.ref(column)} as char)) order by ${sql.ref(orderColumn)} desc`,\n )}, '$[0]'))`;\n }\n return isSqlite()\n ? sql<T | null>`(json_group_array(${sql.ref(column)} order by ${sql.ref(orderColumn)} desc)\n filter (where ${sql.ref(column)} is not null)) ->> 0`\n : sql<T | null>`(array_agg(${sql.ref(column)} order by ${sql.ref(orderColumn)} desc)\n filter (where ${sql.ref(column)} is not null))[1]`;\n};\n\n/** Database sequences Shapio uses. */\nexport type SequenceName = 'entry_heads_change_seq';\n\n/**\n * The next value of a database sequence.\n * PostgreSQL: `nextval('name')`. SQLite: `shapio_nextval('name')`, a counter row in `sequences` that rolls\n * back with its transaction (so SQLite numbers have no gaps). MySQL: a parameter the driver fills just before\n * the statement runs, from an AUTO_INCREMENT table (no lock held to commit). Contract: unique and increasing across every\n * transaction (gaps allowed; a value may commit after a higher one). Returned as a string (bigint).\n */\nexport const nextSequenceValue = (name: SequenceName): RawBuilder<string> => {\n if (isMysql()) {\n return sql<string>`${new SequenceValue(name)}`;\n }\n return isSqlite()\n ? asBigint(sql`shapio_nextval(${sql.lit(name)})`)\n : sql<string>`nextval(${sql.lit(name)})`;\n};\n\n/**\n * MySQL: `cast(concat('[', group_concat(<json texts> separator ','), ']') as json)`, an ordered JSON array\n * aggregate (`JSON_ARRAYAGG` takes no ORDER BY); null for an empty group, like PostgreSQL's aggregates.\n * `items` is the inside of `group_concat`: JSON text per row and its ORDER BY.\n */\nexport const mysqlJsonArrayOf = <T>(items: RawBuilder<unknown>): RawBuilder<T> =>\n sql<T>`cast(concat('[', group_concat(${items} separator ','), ']') as json)`;\n","import { sql, type Expression, type RawBuilder } from 'kysely';\nimport { isMysql, isSqlite } from '../dialect.js';\nimport type { JsonArray } from '../types.js';\nimport { asJson } from './typed.js';\nimport { mysqlJsonArrayOf } from './values.js';\n\n/**\n * JSON primitives for Shapio's own JSON columns and aggregates (dialect boundary, ADR 0001). Content\n * values are compiled by `content/compiler`, not here.\n *\n * `flavor` picks PostgreSQL's `jsonb` or `json` functions. Both read back as the same JavaScript value\n * (only key order differs); it exists so existing statements keep their exact SQL. SQLite has one JSON\n * type (JSON text) and ignores it, as does MySQL (`JSON`).\n *\n * On SQLite the values inside a JSON object or array are SQL scalars: text, numbers and null keep their\n * JSON type, a boolean column becomes 0/1 and a JSON column is embedded as a string. Shapio only builds JSON\n * from text, UUID and number columns here.\n */\nexport type JsonFlavor = 'jsonb' | 'json';\n\n/** One ordering term of an aggregate. */\nexport type AggregateOrder = { column: string; direction?: 'asc' | 'desc'; nulls?: 'first' | 'last' };\n\nconst FUNCTIONS = {\n jsonb: { object: sql`jsonb_build_object`, agg: sql`jsonb_agg`, emptyArray: sql`'[]'::jsonb` },\n json: { object: sql`json_build_object`, agg: sql`json_agg`, emptyArray: sql`'[]'::json` },\n} as const;\n\nconst DIRECTIONS = { asc: sql``, desc: sql` desc` } as const;\nconst NULLS = { first: sql` nulls first`, last: sql` nulls last` } as const;\n/** PostgreSQL's default null placement, which SQLite reverses; written out on SQLite. */\nconst DEFAULT_NULLS = { asc: 'last', desc: 'first' } as const;\n\n/** MySQL has no NULLS FIRST/LAST and sorts NULLs as the smallest value: sort on `col is null` first. */\nconst mysqlOrderTerm = ({ column, direction = 'asc', nulls }: AggregateOrder) => {\n const placement = nulls ?? DEFAULT_NULLS[direction];\n const nullsLast = placement === 'last';\n const mysqlNullsLast = direction === 'desc';\n const term = sql`${sql.ref(column)}${DIRECTIONS[direction]}`;\n return nullsLast === mysqlNullsLast\n ? term\n : sql`(${sql.ref(column)} is null)${nullsLast ? sql`` : sql` desc`}, ${term}`;\n};\n\nconst orderTerm = (order: AggregateOrder) => {\n if (isMysql()) {\n return mysqlOrderTerm(order);\n }\n const { column, direction = 'asc', nulls } = order;\n const placement = nulls ?? (isSqlite() ? DEFAULT_NULLS[direction] : undefined);\n return sql`${sql.ref(column)}${DIRECTIONS[direction]}${placement ? NULLS[placement] : sql``}`;\n};\n\nconst pairs = (fields: Readonly<Record<string, Expression<unknown>>>) =>\n sql.join(Object.entries(fields).map(([key, value]) => sql`${sql.lit(key)}, ${value}`));\n\n/**\n * A JSON object from named expressions. Keys are fixed by the caller's code (emitted as literals).\n * PostgreSQL: `jsonb_build_object('key', value, …)`. SQLite and MySQL: `json_object('key', value, …)`.\n * Contract: a plain object once read.\n */\nexport const jsonObject = <T>(\n fields: Readonly<Record<string, Expression<unknown>>>,\n flavor: JsonFlavor = 'jsonb',\n): RawBuilder<T> =>\n isSqlite() || isMysql()\n ? asJson<T>(sql`json_object(${pairs(fields)})`)\n : sql<T>`${FUNCTIONS[flavor].object}(${pairs(fields)})`;\n\n/**\n * The group's values as a JSON array, in `orderBy` order; null when the group is empty (pair with\n * `emptyJsonArray` in a `coalesce`, and mark the coalesce with `asJson`).\n * PostgreSQL: `jsonb_agg(value order by …)`. SQLite: `json_group_array(value order by …)` (`[]` for an empty\n * group). MySQL: an ordered `group_concat` of each value as JSON, cast to JSON (null for an empty group).\n * Contract: an array once read.\n */\nexport const jsonAgg = <T>(\n value: Expression<unknown>,\n orderBy: readonly AggregateOrder[],\n flavor: JsonFlavor = 'jsonb',\n): RawBuilder<T[]> => {\n const order = sql.join(orderBy.map(orderTerm));\n if (isMysql()) {\n // Each value as JSON (`json_extract(json_array(v), '$[0]')` quotes text and keeps JSON as JSON).\n return mysqlJsonArrayOf<T[]>(sql`json_extract(json_array(${value}), '$[0]') order by ${order}`);\n }\n return isSqlite()\n ? asJson<T[]>(sql`json_group_array(${value} order by ${order})`)\n : sql<T[]>`${FUNCTIONS[flavor].agg}(${value} order by ${order})`;\n};\n\n/**\n * An empty JSON array.\n * PostgreSQL: `'[]'::jsonb`. SQLite: `json('[]')`. MySQL: `json_array()`. Contract: an empty JavaScript\n * array once read.\n */\nexport const emptyJsonArray = <T>(flavor: JsonFlavor = 'jsonb'): RawBuilder<T[]> => {\n if (isMysql()) {\n return sql<T[]>`json_array()`;\n }\n return isSqlite() ? asJson<T[]>(sql`json('[]')`) : sql<T[]>`${FUNCTIONS[flavor].emptyArray}`;\n};\n\n/**\n * A JSON array column with `value` appended as one more element (a log that grows by one entry per write).\n * PostgreSQL: `column || $1::jsonb` with `[value]` bound. SQLite: `json_insert(column, '$[#]', json($1))`\n * with `value` bound as JSON. MySQL: `json_array_append(column, '$', cast($1 as json))`. Contract: the\n * stored array gains exactly one element at the end; the column must not be null.\n */\nexport const jsonArrayAppend = (column: string, value: unknown): RawBuilder<JsonArray> =>\n isMysql()\n ? sql<JsonArray>`json_array_append(${sql.ref(column)}, '$', cast(${JSON.stringify(value)} as json))`\n : isSqlite()\n ? sql<JsonArray>`json_insert(${sql.ref(column)}, '$[#]', json(${JSON.stringify(value)}))`\n : sql<JsonArray>`${sql.ref(column)} || ${JSON.stringify([value])}::jsonb`;\n","import {\n compileAuthorCondition,\n compileFilter,\n filterReadsEntries,\n compileHeadQuery,\n compileSearch,\n compileStatusCondition,\n type HeadRow,\n} from '../content/compiler/compile.js';\nimport { paginationMeta, toLimitOffset, type Pagination } from '../content/compiler/paginate.js';\nimport { parseContentQuery } from '../content/compiler/parse.js';\nimport { compileRowFilter, maskAllows } from '../content/compiler/policy.js';\nimport { parseQueryTree } from '../content/compiler/querystring.js';\nimport { compileOrderBy, defaultSortTerms } from '../content/compiler/sort.js';\nimport type { ContentQuery } from '../content/compiler/types.js';\nimport { entryLocaleNotFound, revisionNotFound } from '../content/errors.js';\nimport { outdatedSharedLocales, readScopeFor } from '../content/locales.js';\nimport { resolveModelById, type ContentModel } from '../content/model.js';\nimport { projectRows, type ReadEnvironment } from '../content/read.js';\nimport type { Policy } from '../permissions/types.js';\nimport * as contentQueriesRepository from '../repositories/contentQueries.js';\nimport * as contentRevisionsRepository from '../repositories/contentRevisions.js';\nimport * as entriesRepository from '../repositories/entries.js';\nimport type { EntryRow } from '../repositories/entries.js';\nimport * as entryHeadsRepository from '../repositories/entryHeads.js';\nimport type { HeadRecord } from '../repositories/entryHeads.js';\nimport { assertEntryVisible, modelWithPolicy, type ContentServiceContext } from './contentAccess.js';\nimport { listDetailsFor, type ListAuthor, type ListLocaleStatus } from './contentListDetails.js';\n\n/** Admin reads of content: drafts, per-locale status, revisions. */\n\nexport type EntryStatus = 'draft' | 'published' | 'modified';\n\nexport const statusOf = (\n draft: Pick<HeadRecord, 'revision_id' | 'autosaved_at'>,\n published?: Pick<HeadRecord, 'revision_id'>,\n): EntryStatus => {\n if (!published) {\n return 'draft';\n }\n return published.revision_id === draft.revision_id && !draft.autosaved_at ? 'published' : 'modified';\n};\n\nconst adminEnvironment = (context: ContentServiceContext, locale: string | undefined): ReadEnvironment => ({\n executor: context.db,\n snapshot: context.snapshot,\n permissions: context.permissions,\n actor: context.actor,\n siteId: context.site.id,\n audience: 'admin',\n source: { kind: 'heads', state: 'draft' },\n locale,\n ...(context.media ? { media: context.media } : {}),\n});\n\nconst toHeadRow = (\n entry: EntryRow,\n head: Pick<HeadRecord, 'locale' | 'data' | 'version' | 'revision_id' | 'updated_at' | 'autosaved_at'>,\n): HeadRow => ({\n entry_id: entry.id,\n locale: head.locale,\n data: head.data,\n version: head.version,\n revision_id: head.revision_id,\n updated_at: head.updated_at,\n autosaved_at: head.autosaved_at,\n created_at: entry.created_at,\n entry_updated_at: entry.updated_at,\n created_by_admin_id: entry.created_by_admin_id,\n owner_app_user_id: entry.owner_app_user_id,\n});\n\nexport type LocaleState = {\n locale: string;\n version: number;\n status: EntryStatus;\n publishedAt: string | null;\n /** The published version's shared fields are older than the draft's (ADR 0004 prompt). */\n sharedOutdated: boolean;\n};\n\nconst localeStates = (model: ContentModel, heads: readonly HeadRecord[]): LocaleState[] => {\n const drafts = heads.filter((head) => head.state === 'draft');\n const publishedOf = (locale: string) =>\n heads.find((head) => head.state === 'published' && head.locale === locale);\n const outdated = new Set(\n outdatedSharedLocales(\n model.definition,\n drafts.map((draft) => {\n const published = publishedOf(draft.locale);\n return {\n locale: draft.locale,\n draft: draft.data,\n ...(published ? { published: published.data } : {}),\n };\n }),\n ),\n );\n return drafts.map((draft) => {\n const published = publishedOf(draft.locale);\n return {\n locale: draft.locale,\n version: draft.version,\n status: statusOf(draft, published),\n publishedAt: published?.updated_at.toISOString() ?? null,\n sharedOutdated: outdated.has(draft.locale),\n };\n });\n};\n\nexport type AdminEntryView = {\n id: string;\n model: string;\n locale: string;\n version: number;\n revisionId: string;\n status: EntryStatus;\n /** Saved without full validation (autosave or duplicate): Save or Publish validates it. */\n autosaved: boolean;\n createdAt: string;\n updatedAt: string;\n publishedAt: string | null;\n data: Record<string, unknown>;\n locales: LocaleState[];\n /** Other published locales whose live shared values are older than their drafts. */\n sharedOutdatedLocales: string[];\n};\n\n/** One entry's draft in one locale, with every locale's status. */\nexport const buildAdminEntryView = async (\n context: ContentServiceContext,\n model: ContentModel,\n policy: Policy,\n entry: EntryRow,\n requestedLocale: string | undefined,\n): Promise<AdminEntryView> => {\n const heads = await entryHeadsRepository.findForEntry(entry.id, context.db);\n const drafts = heads.filter((head) => head.state === 'draft');\n const locale = model.definition.localized\n ? (requestedLocale ?? context.snapshot.defaultLocale)\n : drafts[0]?.locale;\n const draft = drafts.find((head) => head.locale === locale);\n if (!draft || locale === undefined) {\n throw entryLocaleNotFound(\n entry.id,\n locale ?? context.snapshot.defaultLocale,\n drafts.map((head) => head.locale),\n );\n }\n const published = heads.find((head) => head.state === 'published' && head.locale === locale);\n const [projected] = await projectRows(\n adminEnvironment(context, locale),\n model,\n policy,\n [toHeadRow(entry, draft)],\n {\n fields: null,\n populate: new Map(),\n },\n );\n const states = localeStates(model, heads);\n return {\n id: entry.id,\n model: model.definition.apiKey,\n locale,\n version: draft.version,\n revisionId: draft.revision_id,\n status: statusOf(draft, published),\n autosaved: draft.autosaved_at !== null,\n createdAt: entry.created_at.toISOString(),\n updatedAt: draft.updated_at.toISOString(),\n publishedAt: published?.updated_at.toISOString() ?? null,\n data: projected?.data ?? {},\n locales: states,\n sharedOutdatedLocales: states\n .filter((state) => state.sharedOutdated && state.locale !== locale)\n .map((state) => state.locale),\n };\n};\n\n/**\n * The entry view returned after a write. The caller may lack `read` (a write-only role): it then sees the\n * fields its read grants allow (possibly none), never more.\n */\nexport const viewAfterWrite = async (\n context: ContentServiceContext,\n model: ContentModel,\n writePolicy: Policy,\n entryId: string,\n locale: string | undefined,\n): Promise<AdminEntryView> => {\n const readPolicy = await context.permissions.evaluate(context.actor, {\n action: 'read',\n modelId: model.definition.id,\n });\n const entry = assertEntryVisible(\n writePolicy,\n context.actor,\n await entriesRepository.findLive(entryId, model.definition.id, context.site.id, context.db),\n entryId,\n );\n return buildAdminEntryView(context, model, readPolicy.allowed ? readPolicy : writePolicy, entry, locale);\n};\n\nexport const getAdminEntry = async (\n context: ContentServiceContext,\n modelKey: string,\n id: string,\n locale: string | undefined,\n): Promise<AdminEntryView> => {\n const { model, policy } = await modelWithPolicy(context, modelKey, 'read');\n const entry = assertEntryVisible(\n policy,\n context.actor,\n await entriesRepository.findLive(id, model.definition.id, context.site.id, context.db),\n id,\n );\n return buildAdminEntryView(context, model, policy, entry, locale);\n};\n\n/** Filters, search and row filter of a parsed query as SQL conditions. */\n/** Whether `queryConditions` names an `entries` column (`HeadQueryPlan.conditionsReadEntries`). */\nexport const queryReadsEntries = (query: ContentQuery, policy: Policy): boolean =>\n (query.filter !== null && filterReadsEntries(query.filter)) ||\n query.author !== undefined ||\n policy.rowFilter !== null;\n\nexport const queryConditions = (context: ContentServiceContext, query: ContentQuery, policy: Policy) => {\n const rowFilter = compileRowFilter(policy.rowFilter, context.actor);\n return [\n ...(query.filter ? [compileFilter(query.filter)] : []),\n ...(query.search ? [compileSearch(query.search.field.id, query.search.text)] : []),\n ...(query.status ? [compileStatusCondition(query.status)] : []),\n ...(query.author ? [compileAuthorCondition(query.author)] : []),\n ...(rowFilter ? [rowFilter] : []),\n ];\n};\n\nexport const parseQueryFor = (\n context: ContentServiceContext,\n model: ContentModel,\n policy: Policy,\n rawQuery: string,\n options: {\n allowSnapshot: boolean;\n allowAdminFilters?: boolean;\n allowRichText?: boolean;\n allowSeo?: boolean;\n },\n): ContentQuery =>\n parseContentQuery(parseQueryTree(rawQuery), {\n model: model.definition,\n locales: context.snapshot.locales.map((locale) => locale.code),\n isReadable: (field) => maskAllows(policy.readMask, field),\n allowSnapshot: options.allowSnapshot,\n allowAdminFilters: options.allowAdminFilters ?? false,\n allowRichText: options.allowRichText ?? false,\n allowSeo: options.allowSeo ?? false,\n resolveModel: (modelId) => resolveModelById(context.snapshot, modelId)?.definition,\n });\n\nexport type AdminListItem = {\n id: string;\n locale: string;\n version: number;\n status: EntryStatus;\n autosaved: boolean;\n createdAt: string;\n updatedAt: string;\n data: Record<string, unknown>;\n author: ListAuthor | null;\n locales?: ListLocaleStatus[];\n};\n\n/** The admin content list: drafts, filtered/sorted/paginated by the same compiler as delivery. */\nexport const listAdminEntries = async (\n context: ContentServiceContext,\n modelKey: string,\n rawQuery: string,\n): Promise<{ items: AdminListItem[]; pagination: Pagination; locale: string }> => {\n const { model, policy } = await modelWithPolicy(context, modelKey, 'read');\n const query = parseQueryFor(context, model, policy, rawQuery, {\n allowSnapshot: false,\n allowAdminFilters: true,\n });\n const { limit, offset } = toLimitOffset(query.page, query.pageSize);\n const sort =\n query.sort.length > 0\n ? query.sort\n : defaultSortTerms(model.definition, (field) => maskAllows(policy.readMask, field));\n const compiled = compileHeadQuery({\n siteId: context.site.id,\n modelId: model.definition.id,\n source: { kind: 'heads', state: 'draft' },\n locales: readScopeFor(context.snapshot, model.definition, query.locale, { fallback: true }),\n conditions: queryConditions(context, query, policy),\n conditionsReadEntries: queryReadsEntries(query, policy),\n orderBy: compileOrderBy(sort),\n limit,\n offset,\n });\n return contentQueriesRepository.withConsistentRead(async (trx) => {\n const rows = await contentQueriesRepository.runHeadQuery(compiled.rows, trx);\n const total = await contentQueriesRepository.runCountQuery(compiled.count, trx);\n const published = await entryHeadsRepository.findPublishedRevisions(\n rows.map((row) => row.entry_id),\n trx,\n );\n const publishedBy = new Map(published.map((head) => [`${head.entry_id}|${head.locale}`, head]));\n const env = { ...adminEnvironment(context, query.locale), executor: trx };\n const projected = await projectRows(env, model, policy, rows, query);\n const details = await listDetailsFor(trx, model, rows, statusOf);\n return {\n items: projected.map(({ row, data }) => ({\n id: row.entry_id,\n locale: row.locale,\n version: row.version,\n status: statusOf(row, publishedBy.get(`${row.entry_id}|${row.locale}`)),\n autosaved: row.autosaved_at !== null,\n createdAt: row.created_at.toISOString(),\n updatedAt: row.updated_at.toISOString(),\n data,\n author: details.authorOf(row),\n ...(model.definition.localized ? { locales: details.localesOf(row.entry_id) ?? [] } : {}),\n })),\n pagination: paginationMeta(query.page, limit, total),\n locale: query.locale ?? context.snapshot.defaultLocale,\n };\n }, context.db);\n};\n\nconst visibleEntry = async (context: ContentServiceContext, modelKey: string, id: string) => {\n const { model, policy } = await modelWithPolicy(context, modelKey, 'read');\n const entry = assertEntryVisible(\n policy,\n context.actor,\n await entriesRepository.findLive(id, model.definition.id, context.site.id, context.db),\n id,\n );\n return { model, policy, entry };\n};\n\nexport const listRevisions = async (\n context: ContentServiceContext,\n modelKey: string,\n id: string,\n locale: string | undefined,\n) => {\n const { entry } = await visibleEntry(context, modelKey, id);\n return contentRevisionsRepository.listForEntry(entry.id, locale ?? null, 200, context.db);\n};\n\n/** One revision with its data projected through the current schema and the caller's read mask. */\nexport const getRevision = async (\n context: ContentServiceContext,\n modelKey: string,\n id: string,\n revisionId: string,\n) => {\n const { model, policy, entry } = await visibleEntry(context, modelKey, id);\n const revision = await contentRevisionsRepository.findById(revisionId, context.db);\n if (!revision || revision.entry_id !== entry.id) {\n throw revisionNotFound(revisionId);\n }\n const row = toHeadRow(entry, {\n locale: revision.locale,\n data: revision.data,\n version: 0,\n revision_id: revision.id,\n updated_at: revision.created_at,\n autosaved_at: null,\n });\n const [projected] = await projectRows(adminEnvironment(context, revision.locale), model, policy, [row], {\n fields: null,\n populate: new Map(),\n });\n return { revision, data: projected?.data ?? {} };\n};\n","import type { Insertable, Kysely, Selectable, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB, Sites } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type SiteRow = Selectable<Sites>;\nexport type NewSite = Pick<Insertable<Sites>, 'key' | 'name'>;\n\nexport const list = (trx: Executor = db) =>\n trx.selectFrom('sites').selectAll().orderBy('is_primary', 'desc').orderBy('name').orderBy('key').execute();\n\n/** How many sites the instance has. */\nexport const count = async (trx: Executor = db): Promise<number> => {\n const row = await trx\n .selectFrom('sites')\n .select((eb) => eb.fn.countAll<number | string | bigint>().as('count'))\n .executeTakeFirst();\n return Number(row?.count ?? 0);\n};\n\nexport const findById = (id: string, trx: Executor = db) =>\n trx.selectFrom('sites').selectAll().where('id', '=', id).executeTakeFirst();\n\nexport const findByKey = (key: string, trx: Executor = db) =>\n trx.selectFrom('sites').selectAll().where('key', '=', key).executeTakeFirst();\n\nexport const findPrimary = (trx: Executor = db) =>\n trx.selectFrom('sites').selectAll().where('is_primary', '=', true).executeTakeFirstOrThrow();\n\nexport const findByIds = (ids: readonly string[], trx: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : trx\n .selectFrom('sites')\n .selectAll()\n .where('id', 'in', ids)\n .orderBy('is_primary', 'desc')\n .orderBy('name')\n .execute();\n\nexport const insert = (site: NewSite, trx: Executor = db) =>\n trx.insertInto('sites').values(site).returningAll().executeTakeFirstOrThrow();\n\n/** Every site starts its own publication sequence at 0 (snapshot numbers are per site). */\nexport const insertPublicationState = (siteId: string, trx: Executor = db) =>\n trx.insertInto('publication_state').values({ site_id: siteId }).execute();\n\n/** Optimistic update: applies only if the stored version is `expectedVersion`; bumps the version. */\nexport const renameIfVersion = (id: string, expectedVersion: number, name: string, trx: Executor = db) =>\n trx\n .updateTable('sites')\n .set((eb) => ({ name, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .returningAll()\n .executeTakeFirst();\n\nexport const lockById = (id: string, trx: Transaction<DB>) =>\n trx.selectFrom('sites').selectAll().where('id', '=', id).forUpdate().executeTakeFirst();\n\n/**\n * What keeps a site from being deleted: live content, media, change sets and app users (counted per kind).\n * Soft-deleted entries, media assets and app users do not count; `deleteSite` purges them with the site.\n */\nexport const countContents = async (siteId: string, trx: Executor = db) => {\n const count = (\n table: 'entries' | 'media_assets' | 'media_folders' | 'change_sets' | 'app_users',\n liveOnly = false,\n ) => {\n let query = trx\n .selectFrom(table)\n .select((eb) => eb.fn.countAll<string>().as('n'))\n .where('site_id', '=', siteId);\n if (liveOnly) {\n query = query.where('deleted_at', 'is', null);\n }\n return query.executeTakeFirstOrThrow().then((row) => Number(row.n));\n };\n const [entries, mediaAssets, mediaFolders, changeSets, appUsers] = await Promise.all([\n count('entries', true),\n count('media_assets', true),\n count('media_folders'),\n count('change_sets'),\n count('app_users', true),\n ]);\n return { entries, mediaAssets, mediaFolders, changeSets, appUsers };\n};\n\nexport const deleteById = (id: string, trx: Executor = db) =>\n trx.deleteFrom('sites').where('id', '=', id).executeTakeFirst();\n\n/** A site's SEO defaults (null until first saved) and the version an update must name. */\nexport const findSeoDefaults = (id: string, trx: Executor = db) =>\n trx\n .selectFrom('sites')\n .select(['id', 'key', 'name', 'version', 'seo_defaults'])\n .where('id', '=', id)\n .executeTakeFirst();\n\n/** Optimistic update of the SEO defaults: applies only at `expectedVersion`; bumps the version. */\nexport const updateSeoDefaultsIfVersion = (\n id: string,\n expectedVersion: number,\n seoDefaults: unknown,\n trx: Executor = db,\n) =>\n trx\n .updateTable('sites')\n .set((eb) => ({\n seo_defaults: JSON.stringify(seoDefaults),\n version: eb('version', '+', 1),\n updated_at: new Date(),\n }))\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .returning(['id', 'key', 'name', 'version', 'seo_defaults'])\n .executeTakeFirst();\n","import { EMPTY_SEO_DEFAULTS, isModelDefinition, SeoDefaultsSchema, type SeoDefaults } from '@shapio/schema';\nimport { Value } from 'typebox/value';\nimport { isPublicImage, toDeliveryAsset, type ReadEnvironment } from '../content/read.js';\nimport type { Database } from '../db/index.js';\nimport { AppError } from '../helpers/appError.js';\nimport { writeOutboxEvent } from '../jobs/outbox.js';\nimport type { PermissionEvaluator, Principal } from '../permissions/types.js';\nimport * as mediaAssetsRepository from '../repositories/mediaAssets.js';\nimport * as sitesRepository from '../repositories/sites.js';\nimport type { SchemaSnapshot } from '../schema/snapshot.js';\nimport type { SiteActorContext, SiteRef } from './actorContext.js';\nimport { recordAudit } from './audit.js';\nimport { toAssetViews } from './mediaViews.js';\n\n/**\n * A site's SEO defaults (plan seo-fields): default texts per locale, a default social image and the Twitter\n * handle, stored on `sites.seo_defaults`. Edited by `site.settings`; delivered on `GET /api/site` and merged\n * into SEO fields by `?seo=resolved` reads.\n */\nexport const SITE_UPDATED_EVENT = 'site.updated';\n\nexport type SiteSeoContext = SiteActorContext & {\n db: Database;\n snapshot: SchemaSnapshot;\n permissions: PermissionEvaluator;\n};\n\nexport type SiteSeoView = { version: number; seo: SeoDefaults };\n\nconst siteNotFound = () => new AppError(404, 'SITE_NOT_FOUND', 'Site not found');\n\n/** Stored defaults, or the empty ones when none were saved (or the stored value no longer fits the schema). */\nconst readDefaults = (stored: unknown): SeoDefaults =>\n stored !== null && Value.Check(SeoDefaultsSchema, stored) ? stored : EMPTY_SEO_DEFAULTS;\n\n/** The defaults a resolved delivery read merges in. */\nexport const loadSeoDefaults = async (siteId: string, executor: Database): Promise<SeoDefaults> =>\n readDefaults((await sitesRepository.findSeoDefaults(siteId, executor))?.seo_defaults ?? null);\n\n/** The caller reads something on this site (the same rule as its content: deny by default). */\nconst assertReadsSite = async (context: Pick<DeliverySiteContext, 'snapshot' | 'permissions' | 'actor'>) => {\n for (const { definition } of context.snapshot.definitions) {\n if (!isModelDefinition(definition)) {\n continue;\n }\n const policy = await context.permissions.evaluate(context.actor, {\n action: 'read',\n modelId: definition.id,\n });\n if (policy.allowed) {\n return;\n }\n }\n throw context.actor.kind === 'anonymous'\n ? new AppError(401, 'UNAUTHENTICATED', 'This site needs a delivery token')\n : new AppError(403, 'FORBIDDEN', 'You may not read content on this site');\n};\n\n/**\n * Reading the defaults needs read access to some content of the site (editors see the template in the entry's\n * search-result preview); changing them needs `site.settings` (the route checks it).\n */\nexport const getSiteSeo = async (context: SiteSeoContext): Promise<SiteSeoView> => {\n await assertReadsSite(context);\n const row = await sitesRepository.findSeoDefaults(context.site.id, context.db);\n if (!row) {\n throw siteNotFound();\n }\n return { version: row.version, seo: readDefaults(row.seo_defaults) };\n};\n\nconst invalid = (code: string, message: string, details?: Record<string, unknown>) =>\n new AppError(400, code, message, details);\n\nconst assertKnownLocales = (snapshot: SchemaSnapshot, seo: SeoDefaults) => {\n const known = new Set(snapshot.locales.map((locale) => locale.code));\n const unknown = Object.keys(seo.locales).filter((code) => !known.has(code));\n if (unknown.length > 0) {\n throw invalid('UNKNOWN_LOCALE', `Unknown locale \"${unknown[0]}\"`, { locales: unknown });\n }\n};\n\n/** The default image must be a live, public image of this site: delivery shows it to anyone. */\nconst assertUsableImage = async (context: SiteSeoContext, imageId: string | null) => {\n if (imageId === null) {\n return;\n }\n const [asset] = await mediaAssetsRepository.findLiveManyOnSite(context.site.id, [imageId], context.db);\n if (!asset) {\n throw invalid('SEO_IMAGE_INVALID', 'The default image is not an asset of this site', { imageId });\n }\n if (!isPublicImage({ visibility: asset.visibility as 'public' | 'private', mimeType: asset.mime_type })) {\n throw invalid('SEO_IMAGE_INVALID', 'The default image must be a public image', { imageId });\n }\n};\n\n/** Without empty locale entries, so a cleared locale leaves nothing behind. */\nconst compact = (seo: SeoDefaults): SeoDefaults => ({\n ...seo,\n locales: Object.fromEntries(\n Object.entries(seo.locales).filter(([, texts]) => Object.keys(texts).length > 0),\n ),\n});\n\n/** Optimistic: fails with VERSION_CONFLICT when the site changed since `expectedVersion` was read. */\nexport const updateSiteSeo = async (\n context: SiteSeoContext,\n input: { expectedVersion: number; seo: SeoDefaults },\n): Promise<SiteSeoView> => {\n const seo = compact(input.seo);\n assertKnownLocales(context.snapshot, seo);\n await assertUsableImage(context, seo.imageId);\n return context.db.transaction().execute(async (trx) => {\n const current = await sitesRepository.findSeoDefaults(context.site.id, trx);\n if (!current) {\n throw siteNotFound();\n }\n const updated = await sitesRepository.updateSeoDefaultsIfVersion(\n context.site.id,\n input.expectedVersion,\n seo,\n trx,\n );\n if (!updated) {\n throw new AppError(409, 'VERSION_CONFLICT', 'The site changed since you loaded it', {\n expectedVersion: input.expectedVersion,\n actualVersion: current.version,\n });\n }\n const metadata = {\n locales: Object.keys(seo.locales),\n imageId: seo.imageId,\n twitterHandle: seo.twitterHandle,\n };\n await recordAudit(trx, {\n ...context,\n action: 'site.seo_update',\n target: { type: 'site', id: context.site.id },\n metadata,\n });\n await writeOutboxEvent(trx, {\n type: SITE_UPDATED_EVENT,\n aggregateType: 'site',\n aggregateId: context.site.id,\n payload: { siteKey: context.site.key, changed: ['seo'] },\n siteId: context.site.id,\n });\n return { version: updated.version, seo: readDefaults(updated.seo_defaults) };\n });\n};\n\nexport type DeliverySiteContext = {\n db: Database;\n snapshot: SchemaSnapshot;\n permissions: PermissionEvaluator;\n actor: Principal;\n site: SiteRef;\n media?: ReadEnvironment['media'];\n};\n\nexport type DeliverySiteView = {\n key: string;\n name: string;\n seo: Omit<SeoDefaults, 'imageId'> & { image: ReturnType<typeof toDeliveryAsset> | null };\n};\n\nconst deliveryImage = async (context: DeliverySiteContext, imageId: string | null) => {\n if (imageId === null || !context.media) {\n return null;\n }\n const assets = await mediaAssetsRepository.findLiveManyOnSite(context.site.id, [imageId], context.db);\n const [view] = await toAssetViews(context.media, assets, context.db);\n return view && isPublicImage(view) ? toDeliveryAsset(view) : null;\n};\n\n/** `GET /api/site`: the site's key, name and SEO defaults, its default image as a delivered asset. */\nexport const getDeliverySite = async (context: DeliverySiteContext): Promise<DeliverySiteView> => {\n await assertReadsSite(context);\n const row = await sitesRepository.findSeoDefaults(context.site.id, context.db);\n if (!row) {\n throw siteNotFound();\n }\n const { imageId, ...seo } = readDefaults(row.seo_defaults);\n return { key: row.key, name: row.name, seo: { ...seo, image: await deliveryImage(context, imageId) } };\n};\n","import { isSeoField, type SeoDefaults } from '@shapio/schema';\nimport type { RawBuilder } from 'kysely';\nimport {\n compileHeadPage,\n type HeadQueryPlan,\n type HeadRow,\n type HeadSource,\n} from '../content/compiler/compile.js';\nimport { entryIdIs } from '../content/compiler/conditions.js';\nimport { toLimitOffset } from '../content/compiler/paginate.js';\nimport { maskAllows } from '../content/compiler/policy.js';\nimport { selectFields } from '../content/compiler/select.js';\nimport { compileOrderBy, defaultSortTerms } from '../content/compiler/sort.js';\nimport type { ContentQuery, RichTextMode } from '../content/compiler/types.js';\nimport { readScopeFor } from '../content/locales.js';\nimport type { ContentModel } from '../content/model.js';\nimport {\n needsFollowUpReads,\n needsMediaReads,\n projectRows,\n systemAttributes,\n type ReadEnvironment,\n} from '../content/read.js';\nimport type { Policy } from '../permissions/types.js';\nimport * as contentQueriesRepository from '../repositories/contentQueries.js';\nimport type { ContentServiceContext } from './contentAccess.js';\nimport { queryConditions, queryReadsEntries } from './contentReads.js';\nimport { loadSeoDefaults } from './siteSeo.js';\n\n/**\n * Delivery-shaped reads (delivery, preview, GraphQL root fields) at one consistent moment (plan\n * delivery-perf). The heads, the page total and the site's publication sequence come from one statement, so\n * a read that needs nothing else (no relation, populate or asset follow-up) runs without a transaction.\n * Anything that needs a follow-up statement runs in one REPEATABLE READ, read-only transaction, so every\n * statement of the response sees the moment `meta.snapshot` names, except a live published read whose only\n * follow-up is relation visibility: that runs on the pool (`readConsistently` explains why it is safe).\n */\n\nexport const deliveryEnvironment = (\n context: ContentServiceContext,\n executor: ReadEnvironment['executor'],\n source: HeadSource,\n locale: string | undefined,\n richText?: RichTextMode,\n seo?: SeoDefaults,\n): ReadEnvironment => ({\n executor,\n snapshot: context.snapshot,\n permissions: context.permissions,\n actor: context.actor,\n siteId: context.site.id,\n audience: 'delivery',\n source,\n locale,\n ...(context.media ? { media: context.media } : {}),\n ...(richText ? { richText } : {}),\n ...(seo ? { seo } : {}),\n});\n\n/**\n * A delivery-shaped read of one model, once the caller is authorised and the query parsed. `source` is\n * published heads, a snapshot, or drafts (previews); `conditions` adds row restrictions on top of the query's\n * own (a preview token's entry scope).\n */\nexport type DeliveryRead = {\n context: ContentServiceContext;\n source: HeadSource;\n model: ContentModel;\n policy: Policy;\n query: ContentQuery;\n fallback: boolean;\n conditions?: readonly RawBuilder<unknown>[];\n /** `?seo=resolved`: the site's SEO defaults (loaded by `withSeoDefaults`). */\n seo?: SeoDefaults;\n};\n\n/** What a read saw: its environment (for projection), the rows, the page total and the publication sequence. */\ntype ConsistentHeads = {\n env: ReadEnvironment;\n rows: HeadRow[];\n /** Null unless the read asked for it. */\n total: number | null;\n seq: number;\n};\n\nconst environmentOf = (read: DeliveryRead, executor: ReadEnvironment['executor']) =>\n deliveryEnvironment(read.context, executor, read.source, read.query.locale, read.query.richText, read.seo);\n\n/**\n * Selected top-level relation or media fields, populate, or a default SEO image to show: the read will need a\n * follow-up statement.\n */\nconst followsUp = (read: DeliveryRead): boolean =>\n read.query.populate.size > 0 ||\n selectFields(read.model, read.policy.readMask, read.query.fields).some(\n (field) =>\n field.type === 'relation' ||\n field.type === 'media' ||\n (Boolean(read.seo?.imageId) && isSeoField(field)),\n );\n\n/**\n * A live read of published heads (no `?snapshot`, not a preview or draft read, no extra conditions) without\n * populate: its only possible follow-up is relation visibility (asset views are checked on the rows).\n */\nconst isLiveVisibilityRead = (read: DeliveryRead): boolean =>\n read.source.kind === 'heads' &&\n read.source.state === 'published' &&\n read.query.populate.size === 0 &&\n (read.conditions?.length ?? 0) === 0;\n\n/**\n * Loads the site's SEO defaults when the query asks for resolved SEO. Read before the heads, outside their\n * consistent moment: defaults are not part of a snapshot (`?snapshot=N&seo=resolved` uses today's).\n */\nconst withSeoDefaults = async (read: DeliveryRead): Promise<DeliveryRead> =>\n read.query.seo === 'resolved'\n ? { ...read, seo: await loadSeoDefaults(read.context.site.id, read.context.db) }\n : read;\n\nconst seqOf = (seq: number | null): number => {\n if (seq === null) {\n throw new Error('The site has no publication state');\n }\n return seq;\n};\n\n/**\n * Runs the plan's statement on the pool when the read cannot need a follow-up, and keeps that result when\n * the rows indeed need none; otherwise reads again inside one REPEATABLE READ transaction. `finish` projects\n * the rows (through the same executor), so its own statements see the same moment. A live published read\n * whose only follow-up is relation visibility also stays on the pool (see below).\n */\nconst readConsistently = async <T>(\n read: DeliveryRead,\n plan: HeadQueryPlan,\n options: { total: boolean },\n finish: (heads: ConsistentHeads) => Promise<T>,\n): Promise<T> => {\n const compiled = compileHeadPage(plan, options);\n const { db } = read.context;\n const live = isLiveVisibilityRead(read);\n if (!followsUp(read) || live) {\n const env = environmentOf(read, db);\n const { rows, meta } = await contentQueriesRepository.runHeadPage(compiled.rows, db);\n const fields = selectFields(read.model, read.policy.readMask, read.query.fields);\n // An empty page carries no meta row; the transaction below reads it consistently.\n if (meta && !needsFollowUpReads(env, read.model, rows, read.query, read.policy)) {\n return finish({ env, rows, total: meta.total, seq: seqOf(meta.seq) });\n }\n // Relation visibility on the pool, without BEGIN … REPEATABLE READ … COMMIT (plan delivery-perf-2, step\n // 4). The heads and `meta.snapshot` come from the one statement above; the visibility fetch is a second\n // statement that may see a later moment. That can only move a target to its newer live state: the fetch\n // itself reads published heads only (and the target's read policy), so a target unpublished or deleted in\n // between is hidden, and one published in between is shown although the snapshot named predates it. It\n // never shows an unpublished target (rule 7). Pinned (`?snapshot=N`), preview and draft reads, populate and\n // asset views keep the transaction, so their responses are exactly the moment they name.\n if (meta && live && !needsMediaReads(env, read.model, rows, fields)) {\n return finish({ env, rows, total: meta.total, seq: seqOf(meta.seq) });\n }\n }\n return contentQueriesRepository.withConsistentRead(async (trx) => {\n const env = environmentOf(read, trx);\n const page = await contentQueriesRepository.runHeadPage(compiled.rows, trx);\n const meta = page.meta ?? (await contentQueriesRepository.runHeadPageMeta(compiled.meta, trx));\n return finish({ env, rows: page.rows, total: meta.total, seq: seqOf(meta.seq) });\n }, db);\n};\n\nconst localesOf = (read: DeliveryRead) =>\n readScopeFor(read.context.snapshot, read.model.definition, read.query.locale, { fallback: read.fallback });\n\nconst toEntries = async (read: DeliveryRead, env: ReadEnvironment, rows: readonly HeadRow[]) =>\n (await projectRows(env, read.model, read.policy, rows, read.query)).map(({ row, data }) => ({\n ...systemAttributes(env, row),\n ...data,\n }));\n\nexport type EntryPage = {\n entries: Array<Record<string, unknown>>;\n limit: number;\n /** The matching total (0 when not asked for). */\n total: number;\n /** The site's publication sequence when the page was read. */\n seq: number;\n};\n\n/**\n * One page of entries in the delivery shape, with the total (`total: false` skips counting: a singleton) and\n * the publication sequence it was read at. `check` runs on the sequence before anything is projected.\n */\nexport const readEntryPage = async (\n unloaded: DeliveryRead,\n options: { total: boolean; check?: (seq: number) => void } = { total: true },\n): Promise<EntryPage> => {\n const read = await withSeoDefaults(unloaded);\n const { context, model, policy, query } = read;\n const { limit, offset } = toLimitOffset(query.page, query.pageSize);\n const sort =\n query.sort.length > 0\n ? query.sort\n : defaultSortTerms(model.definition, (field) => maskAllows(policy.readMask, field));\n const plan: HeadQueryPlan = {\n siteId: context.site.id,\n modelId: model.definition.id,\n source: read.source,\n locales: localesOf(read),\n conditions: [...queryConditions(context, query, policy), ...(read.conditions ?? [])],\n // A caller's extra conditions (a preview token's entry scope) are not inspected: they keep the join.\n conditionsReadEntries: queryReadsEntries(query, policy) || (read.conditions?.length ?? 0) > 0,\n orderBy: compileOrderBy(sort),\n limit,\n offset,\n };\n return readConsistently(read, plan, { total: options.total }, async ({ env, rows, total, seq }) => {\n options.check?.(seq);\n return { entries: await toEntries(read, env, rows), limit, total: total ?? 0, seq };\n });\n};\n\n/** One entry by ID in the delivery shape (undefined when it is not there for this read), and the sequence. */\nexport const readOneEntry = async (\n unloaded: DeliveryRead,\n id: string,\n options: { check?: (seq: number) => void } = {},\n): Promise<{ entry: Record<string, unknown> | undefined; seq: number }> => {\n const read = await withSeoDefaults(unloaded);\n const { context, model, policy, query } = read;\n const plan: HeadQueryPlan = {\n siteId: context.site.id,\n modelId: model.definition.id,\n source: read.source,\n locales: localesOf(read),\n conditions: [\n ...queryConditions(context, { ...query, filter: null, search: null }, policy),\n ...(read.conditions ?? []),\n entryIdIs(id),\n ],\n orderBy: [],\n limit: 1,\n };\n return readConsistently(read, plan, { total: false }, async ({ env, rows, seq }) => {\n options.check?.(seq);\n const [entry] = await toEntries(read, env, rows);\n return { entry, seq };\n });\n};\n","import type { HeadSource } from '../content/compiler/compile.js';\nimport { paginationMeta, type Pagination } from '../content/compiler/paginate.js';\nimport { queryInvalid, type ContentQuery } from '../content/compiler/types.js';\nimport { entryNotFound, snapshotInvalid } from '../content/errors.js';\nimport { resolveModel, type ContentModel } from '../content/model.js';\nimport { AppError } from '../helpers/appError.js';\nimport type { Policy } from '../permissions/types.js';\nimport type { UsageRecorder } from '../usage/aggregator.js';\nimport { usagePrincipalKey } from '../usage/keys.js';\nimport { restFieldReads } from '../usage/restFieldReads.js';\nimport type { ContentServiceContext } from './contentAccess.js';\nimport { readEntryPage, readOneEntry } from './contentDeliveryReads.js';\nimport { parseQueryFor } from './contentReads.js';\n\n/**\n * The delivery API (build plan §4.E6): published heads only, through the permission evaluator (delivery\n * tokens, app users, anonymous callers). Each response reads one consistent moment (one statement, or a\n * REPEATABLE READ transaction when it needs more: `contentDeliveryReads.ts`; a live read's relation\n * visibility is the exception, checked against the live state on the pool) and reports the publication\n * sequence it saw, which a site build can pin with `?snapshot=N`.\n */\nexport type DeliveryMeta = {\n /** The requested locale (or the default); each entry's own `locale` says which one served it. */\n locale: string;\n /** Publication sequence number this response reflects. */\n snapshot: number;\n};\n\nexport type DeliveryList = {\n data: Array<Record<string, unknown>>;\n meta: DeliveryMeta & { pagination: Pagination };\n};\nexport type DeliveryItem = { data: Record<string, unknown>; meta: DeliveryMeta };\n\n/**\n * Read variants GraphQL exposes as arguments (REST always uses the defaults):\n * - `fallback: false` serves the requested locale only, never a fallback locale (ADR 0004).\n * - `drafts: true` reads draft heads in the delivery shape (GraphQL `publicationState: DRAFT`). Only admin\n * principals may ask for it; the caller (plugins/graphql) checks that before passing it.\n */\nexport type DeliveryReadOptions = {\n fallback?: boolean;\n drafts?: boolean;\n /**\n * Field usage counters (REST only). GraphQL counts its own selections once per operation\n * (plugins/graphql.ts), so its root reads through this service pass none.\n */\n usage?: UsageRecorder;\n /**\n * Runs usage recording after the response is sent (REST passes the request's after-response queue); without\n * it the read records before returning.\n */\n defer?: (task: () => Promise<void>) => void;\n};\n\nconst readableModel = async (context: ContentServiceContext, modelKey: string) => {\n const model = resolveModel(context.snapshot, modelKey);\n const policy = await context.permissions.evaluate(context.actor, {\n action: 'read',\n modelId: model.definition.id,\n });\n if (!policy.allowed) {\n throw context.actor.kind === 'anonymous'\n ? new AppError(401, 'UNAUTHENTICATED', 'This content needs a delivery token')\n : new AppError(403, 'FORBIDDEN', `Your token may not read \"${modelKey}\"`);\n }\n return { model, policy };\n};\n\n/** The heads a read serves: drafts (GraphQL previews), a pinned snapshot, or the published heads. */\nconst sourceOf = (query: ContentQuery, options: DeliveryReadOptions): HeadSource => {\n if (query.snapshot !== undefined && options.drafts) {\n throw queryInvalid('snapshot reads published content; it cannot be combined with drafts');\n }\n return options.drafts\n ? { kind: 'heads', state: 'draft' }\n : query.snapshot !== undefined\n ? { kind: 'snapshot', seq: query.snapshot }\n : { kind: 'heads', state: 'published' };\n};\n\n/** A pinned snapshot must already exist: checked against the sequence the read itself saw. */\nconst snapshotCheck =\n (query: ContentQuery) =>\n (current: number): void => {\n if (query.snapshot !== undefined && query.snapshot > current) {\n throw snapshotInvalid(query.snapshot, current);\n }\n };\n\n/**\n * Counts a successful REST read for field usage (plan developer-face §5): once per request, after the\n * projection, by principal. Admin users and draft reads are never counted.\n */\nconst recordUsage = async (\n context: ContentServiceContext,\n options: DeliveryReadOptions,\n model: ContentModel,\n policy: Policy,\n query: ContentQuery,\n) => {\n const principalKey = usagePrincipalKey(context.actor);\n if (!options.usage || options.drafts || principalKey === null) {\n return;\n }\n const reads = await restFieldReads({\n snapshot: context.snapshot,\n model,\n policy,\n query,\n policyFor: (modelId) => context.permissions.evaluate(context.actor, { action: 'read', modelId }),\n });\n options.usage.recordRequest(context.site.id, principalKey, query.snapshot ?? null);\n options.usage.recordFieldReads(context.site.id, principalKey, model.definition.id, reads);\n};\n\n/** Records usage after the response when the caller can defer it (REST), else before returning. */\nconst afterRead = async (options: DeliveryReadOptions, task: () => Promise<void>) => {\n if (options.defer) {\n options.defer(task);\n return;\n }\n await task();\n};\n\nconst ITEM_PARAMETERS = new Set(['fields', 'populate', 'locale', 'snapshot', 'richText', 'seo']);\n\n/** Reading one entry takes only these query parameters; anything else is refused, not ignored. */\nexport const assertItemParameters = (rawQuery: string, allowed: ReadonlySet<string>): void => {\n const unsupported = [...new URLSearchParams(rawQuery).keys()].find(\n (key) => !allowed.has(key.split('[')[0] ?? key),\n );\n if (unsupported !== undefined) {\n throw queryInvalid(`\"${unsupported}\" is not supported when reading one entry`);\n }\n};\n\n/** `GET /api/content/:modelKey`: a page of a collection, or a singleton's one entry. */\nexport const listDelivery = async (\n context: ContentServiceContext,\n modelKey: string,\n rawQuery: string,\n options: DeliveryReadOptions = {},\n): Promise<DeliveryList | DeliveryItem> => {\n const { model, policy } = await readableModel(context, modelKey);\n const query = parseQueryFor(context, model, policy, rawQuery, {\n allowSnapshot: true,\n allowRichText: true,\n allowSeo: true,\n });\n const locale = query.locale ?? context.snapshot.defaultLocale;\n const singleton = model.definition.kind === 'singleton';\n const { entries, limit, total, seq } = await readEntryPage(\n { context, source: sourceOf(query, options), model, policy, query, fallback: options.fallback ?? true },\n { total: !singleton, check: snapshotCheck(query) },\n );\n const snapshot = query.snapshot ?? seq;\n let result: DeliveryList | DeliveryItem;\n if (singleton) {\n const [entry] = entries;\n if (!entry) {\n throw entryNotFound(modelKey);\n }\n result = { data: entry, meta: { locale, snapshot } };\n } else {\n result = {\n data: entries,\n meta: { locale, snapshot, pagination: paginationMeta(query.page, limit, total) },\n };\n }\n await afterRead(options, () => recordUsage(context, options, model, policy, query));\n return result;\n};\n\n/** `GET /api/content/:modelKey/:id`: one published entry (404 for drafts, unknown or unreadable entries). */\nexport const getDelivery = async (\n context: ContentServiceContext,\n modelKey: string,\n id: string,\n rawQuery: string,\n options: DeliveryReadOptions = {},\n): Promise<DeliveryItem> => {\n assertItemParameters(rawQuery, ITEM_PARAMETERS);\n const { model, policy } = await readableModel(context, modelKey);\n const query = parseQueryFor(context, model, policy, rawQuery, {\n allowSnapshot: true,\n allowRichText: true,\n allowSeo: true,\n });\n const { entry, seq } = await readOneEntry(\n { context, source: sourceOf(query, options), model, policy, query, fallback: options.fallback ?? true },\n id,\n { check: snapshotCheck(query) },\n );\n if (!entry) {\n throw entryNotFound(id);\n }\n await afterRead(options, () => recordUsage(context, options, model, policy, query));\n return {\n data: entry,\n meta: { locale: query.locale ?? context.snapshot.defaultLocale, snapshot: query.snapshot ?? seq },\n };\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\nimport { entrySiteOf } from './entries.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** Why a publication sequence number was taken (the `publication_snapshots` ledger). */\nexport type SnapshotSource =\n 'publish' | 'unpublish' | 'delete' | 'schedule' | 'change_set' | 'schema' | 'conversion' | 'import';\n\nexport type SnapshotMeta = {\n source: SnapshotSource;\n changeSetId?: string | null;\n /** Who caused it, in audit vocabulary (`schema/planner/actor.ts actorColumns`). */\n actor?: { type: string; id: string | null } | null;\n};\n\n/**\n * The publication sequence and log behind `?snapshot=N` (ADR 0001). The sequence is one row incremented\n * inside each publishing transaction: its row lock orders publications by commit, so a reader that sees N\n * also sees every publication before N. Take it as late as possible in the transaction (it serialises every\n * publication instance-wide until commit), and never ahead of the transaction that uses it.\n *\n * Every number also gets a `publication_snapshots` ledger row (why, who, the schema version live with it),\n * written in the same transaction.\n */\nexport const nextSeq = async (trx: Transaction<DB>, siteId: string, meta: SnapshotMeta): Promise<number> => {\n // One sequence per site (sites plan §H): publishing on one site never waits for another's.\n const row = await trx\n .updateTable('publication_state')\n .set((eb) => ({ last_seq: eb('last_seq', '+', '1') }))\n .where('site_id', '=', siteId)\n .returning('last_seq')\n .executeTakeFirstOrThrow();\n const seq = Number(row.last_seq);\n await trx\n .insertInto('publication_snapshots')\n .values((eb) => ({\n site_id: siteId,\n seq: String(seq),\n schema_version: eb.selectFrom('system_versions').select('schema_version'),\n source: meta.source,\n change_set_id: meta.changeSetId ?? null,\n actor_type: meta.actor?.type ?? null,\n actor_id: meta.actor?.id ?? null,\n }))\n .execute();\n return seq;\n};\n\n/**\n * One sequence number per transaction, taken on first use: everything a batch publishes, unpublishes or\n * converts shares it, so the batch is exactly one snapshot.\n */\nexport type SeqAllocator = {\n next: () => Promise<number>;\n /** The number if one was taken already. */\n taken: () => number | undefined;\n};\n\nexport const createSeqAllocator = (\n trx: Transaction<DB>,\n siteId: string,\n meta: SnapshotMeta,\n): SeqAllocator => {\n let seq: Promise<number> | undefined;\n let value: number | undefined;\n return {\n next: () => {\n seq ??= nextSeq(trx, siteId, meta).then((taken) => {\n value = taken;\n return taken;\n });\n return seq;\n },\n taken: () => value,\n };\n};\n\nexport const currentSeq = async (siteId: string, executor: Executor = db): Promise<number> => {\n const row = await executor\n .selectFrom('publication_state')\n .select('last_seq')\n .where('site_id', '=', siteId)\n .executeTakeFirstOrThrow();\n return Number(row.last_seq);\n};\n\n/** Ends the live period of (entry, locale) at `seq`, if one is open. */\nexport const close = (entryId: string, locale: string | null, seq: number, trx: Executor = db) => {\n let query = trx\n .updateTable('publication_log')\n .set({ to_seq: String(seq) })\n .where('entry_id', '=', entryId)\n .where('to_seq', 'is', null);\n if (locale !== null) {\n query = query.where('locale', '=', locale);\n }\n return query.executeTakeFirst();\n};\n\nexport const open = (\n row: { entryId: string; modelId: string; locale: string; revisionId: string; seq: number; now: Date },\n trx: Executor = db,\n) =>\n trx\n .insertInto('publication_log')\n .values({\n entry_id: row.entryId,\n // The log is per site (snapshot numbers are): a copy of the entry's site.\n site_id: entrySiteOf(trx, row.entryId),\n model_id: row.modelId,\n locale: row.locale,\n revision_id: row.revisionId,\n from_seq: String(row.seq),\n published_at: row.now,\n })\n .execute();\n\nexport const hasOpen = async (entryId: string, executor: Executor = db): Promise<boolean> => {\n const row = await executor\n .selectFrom('publication_log')\n .select('id')\n .where('entry_id', '=', entryId)\n .where('to_seq', 'is', null)\n .executeTakeFirst();\n return row !== undefined;\n};\n\nexport const removeForLocale = (locale: string, trx: Executor = db) =>\n trx.deleteFrom('publication_log').where('locale', '=', locale).executeTakeFirst();\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** The global schema version: the durable value every instance compares its cached snapshot against. */\nexport const getSchemaVersion = async (executor: Executor = db): Promise<number> => {\n const row = await executor.selectFrom('system_versions').select('schema_version').executeTakeFirstOrThrow();\n return row.schema_version;\n};\n\n/** Increments the global schema version in the caller's transaction and returns the new value. */\nexport const bumpSchemaVersion = async (trx: Executor, now: Date): Promise<number> => {\n const row = await trx\n .updateTable('system_versions')\n .set((eb) => ({ schema_version: eb('schema_version', '+', eb.lit(1)), updated_at: now }))\n .returning('schema_version')\n .executeTakeFirstOrThrow();\n return row.schema_version;\n};\n\n/** Records the Shapio release this database is served by (in-process readers compare their own with it). */\nexport const setRelease = async (release: string, executor: Executor = db): Promise<void> => {\n await executor.updateTable('system_versions').set({ release }).execute();\n};\n","import { sql, type Kysely, type RawBuilder, type Transaction } from 'kysely';\nimport { isMysql, isSqlite } from './dialect.js';\nimport { mysqlJsonArrayOf } from './sql/values.js';\nimport type { DB } from './types.js';\n\n/**\n * The snapshot diff query over `publication_log` (PostgreSQL-specific SQL lives in `db/`, the content\n * compiler and migrations; CONTRIBUTING.md). repositories/snapshotDiff.ts is its typed wrapper and documents the\n * classification. Rows come back in entry-ID order, at most `maxRows` of them, with each entry's changed\n * locales aggregated as JSON.\n */\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type SnapshotDiffLocaleRow = {\n locale: string;\n change: 'published' | 'updated' | 'unpublished';\n fromRevisionId: string | null;\n toRevisionId: string | null;\n};\n\nexport type SnapshotDiffRow = { entry_id: string; model_id: string; locales: SnapshotDiffLocaleRow[] };\n\nexport type SnapshotDiffParams = {\n /** The site whose publication log is diffed (snapshot numbers are per site). */\n siteId: string;\n from: number;\n to: number;\n /** Keyset cursor: only entries with a greater ID. */\n after: string | undefined;\n /** Only these models; null = every model. */\n modelIds: readonly string[] | null;\n /** A compiled condition over the `entries` row aliased `e`. */\n rowFilter: RawBuilder<unknown> | null;\n maxRows: number;\n};\n\n/**\n * The dialect's spelling of the few typed pieces (PostgreSQL casts, arrays and `jsonb_agg`; SQLite binds\n * plain values, expands the model list with `json_each` and aggregates with `json_group_array`, whose text\n * result is parsed here).\n */\ntype DiffDialect = {\n uuid: (value: string) => RawBuilder<unknown>;\n seq: (value: number) => RawBuilder<unknown>;\n inModels: (modelIds: readonly string[]) => RawBuilder<unknown>;\n /** `a <op> b`: the two revision IDs differ (nulls compare as values). */\n distinct: (a: RawBuilder<unknown>, b: RawBuilder<unknown>) => RawBuilder<unknown>;\n /** The JSON objects of a group as an array ordered by locale. */\n aggregate: (object: RawBuilder<unknown>) => RawBuilder<unknown>;\n object: RawBuilder<unknown>;\n /** The classification column's name (`change` is reserved on MySQL). */\n change: RawBuilder<unknown>;\n};\n\nconst POSTGRES: DiffDialect = {\n uuid: (value: string) => sql`${value}::uuid`,\n seq: (value: number) => sql`${value}::bigint`,\n inModels: (modelIds: readonly string[]) => sql`pl.model_id = any(${[...modelIds]}::uuid[])`,\n distinct: (a, b) => sql`${a} is distinct from ${b}`,\n aggregate: (object) => sql`jsonb_agg(\n ${object}\n order by ch.locale\n )`,\n object: sql`jsonb_build_object`,\n change: sql`change`,\n};\n\nconst SQLITE: DiffDialect = {\n uuid: (value) => sql`${value.toLowerCase()}`,\n seq: (value) => sql`${value}`,\n inModels: (modelIds) => sql`pl.model_id in (select value from json_each(${JSON.stringify(modelIds)}))`,\n distinct: (a, b) => sql`${a} is not ${b}`,\n aggregate: (object) => sql`json_group_array(\n ${object}\n order by ch.locale\n )`,\n object: sql`json_object`,\n change: sql`change`,\n};\n\n/** MySQL: `<=>` is null-safe equality; JSON_ARRAYAGG takes no ORDER BY (`mysqlJsonArrayOf`). */\nconst MYSQL: DiffDialect = {\n uuid: (value) => sql`${value.toLowerCase()}`,\n seq: (value) => sql`${value}`,\n inModels: (modelIds) =>\n modelIds.length === 0 ? sql`false` : sql`pl.model_id in (${sql.join([...modelIds])})`,\n distinct: (a, b) => sql`not (${a} <=> ${b})`,\n aggregate: (object) => mysqlJsonArrayOf(sql`${object} order by ch.locale`),\n object: sql`json_object`,\n change: sql`\\`change\\``,\n};\n\nconst modelCondition = (dialect: DiffDialect, modelIds: readonly string[] | null) =>\n modelIds === null ? sql`true` : dialect.inModels(modelIds);\n\nconst afterCondition = (dialect: DiffDialect, after: string | undefined) =>\n after === undefined ? sql`true` : sql`pl.entry_id > ${dialect.uuid(after)}`;\n\n/** The revision of (entry, locale) live at `seq`, as a lateral subquery over `c`. */\nconst liveAt = (dialect: DiffDialect, siteId: string, seq: number) => sql`(\n select p.revision_id from publication_log p\n where p.site_id = ${dialect.uuid(siteId)} and p.entry_id = c.entry_id and p.locale = c.locale\n and p.from_seq <= ${dialect.seq(seq)} and (p.to_seq is null or p.to_seq > ${dialect.seq(seq)})\n order by p.from_seq desc\n limit 1\n)`;\n\nexport const selectSnapshotDiffRows = async (\n params: SnapshotDiffParams,\n executor: Executor,\n): Promise<SnapshotDiffRow[]> => {\n const dialect = isMysql() ? MYSQL : isSqlite() ? SQLITE : POSTGRES;\n const { from, to } = params;\n const scope = sql`pl.site_id = ${dialect.uuid(params.siteId)} and ${modelCondition(dialect, params.modelIds)}\n and ${afterCondition(dialect, params.after)}`;\n const result = await sql<SnapshotDiffRow>`\n with candidates as (\n select pl.entry_id, pl.locale from publication_log pl\n where pl.from_seq > ${dialect.seq(from)} and pl.from_seq <= ${dialect.seq(to)} and ${scope}\n union\n select pl.entry_id, pl.locale from publication_log pl\n where pl.to_seq > ${dialect.seq(from)} and pl.to_seq <= ${dialect.seq(to)} and ${scope}\n ),\n states as (\n select c.entry_id, c.locale, ${liveAt(dialect, params.siteId, from)} as from_revision_id,\n ${liveAt(dialect, params.siteId, to)} as to_revision_id\n from candidates c\n ),\n changes as (\n select s.*,\n case\n when s.from_revision_id is null then 'published'\n when s.to_revision_id is null then 'unpublished'\n else 'updated'\n end as ${dialect.change}\n from states s\n where ${dialect.distinct(sql`s.from_revision_id`, sql`s.to_revision_id`)}\n )\n select ch.entry_id, e.model_id,\n ${dialect.aggregate(sql`${dialect.object}(\n 'locale', ch.locale,\n 'change', ch.${dialect.change},\n 'fromRevisionId', ch.from_revision_id,\n 'toRevisionId', ch.to_revision_id\n )`)} as locales\n from changes ch\n join entries e on e.id = ch.entry_id\n where ${params.rowFilter ?? sql`true`}\n group by ch.entry_id, e.model_id\n order by ch.entry_id\n limit ${params.maxRows}\n `.execute(executor);\n return result.rows.map((row) =>\n typeof row.locales === 'string'\n ? { ...row, locales: JSON.parse(row.locales) as SnapshotDiffLocaleRow[] }\n : row,\n );\n};\n","import type { Kysely, RawBuilder, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport { selectSnapshotDiffRows } from '../db/snapshotDiffQuery.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/**\n * The snapshot diff (plan developer-face §5) of one site's ledger: which of its (entry, locale) pairs serve different live content at\n * publication sequence `to` than at `from`, in one query over `publication_log` (db/snapshotDiffQuery.ts).\n *\n * Candidates are the pairs whose live period opened (`from_seq`) or closed (`to_seq`) in `(from, to]`; only\n * those can differ. For each, the revision live at `from` and at `to` is \"the row whose period contains\n * the sequence number\" (`from_seq <= N and (to_seq is null or to_seq > N)`), the same rule `?snapshot=N`\n * reads with (content/compiler/compile.ts). Classification:\n *\n * | live at `from` | live at `to` | change |\n * |----------------|--------------------|---------------|\n * | no | yes | `published` |\n * | yes | no | `unpublished` |\n * | yes (R1) | yes (R2, R2 ≠ R1) | `updated` |\n * | yes (R) | yes (same R) | dropped |\n * | no | no | dropped |\n *\n * Entries are returned in entry-ID order with their changed locales grouped, paginated by keyset on the\n * entry ID (`after`). A page holds at most `limit` entries; `nextAfter` is set when more follow.\n *\n * Entry deletion closes the entry's live periods (it reads as `unpublished`); locale deletion removes that\n * locale's rows (history is rewritten, so a later diff no longer mentions the locale).\n */\nexport type SnapshotChangeKind = 'published' | 'updated' | 'unpublished';\n\nexport type SnapshotLocaleChange = {\n locale: string;\n change: SnapshotChangeKind;\n /** The revision live at `from` (null when the locale was not live then). */\n fromRevisionId: string | null;\n /** The revision live at `to` (null when the locale is not live then). */\n toRevisionId: string | null;\n};\n\nexport type SnapshotEntryChange = {\n entryId: string;\n modelId: string;\n locales: SnapshotLocaleChange[];\n};\n\nexport type SnapshotDiffQuery = {\n /** The site whose ledger is diffed (snapshot numbers are per site). */\n siteId: string;\n from: number;\n to: number;\n /** Keyset cursor: only entries with an ID greater than this. */\n after?: string | undefined;\n /** Entries per page (the caller bounds it). */\n limit: number;\n /** Only these models (the ones the caller may read); null = every model. */\n modelIds: readonly string[] | null;\n /**\n * A compiled row filter over the `entries` row aliased `e` (content/compiler/policy.ts\n * `compileRowFilter`). It reads the entry as it is now, which for a diff ending at the current snapshot\n * is the state at `to`.\n */\n rowFilter?: RawBuilder<unknown> | null;\n};\n\nexport type SnapshotDiffPage = { items: SnapshotEntryChange[]; nextAfter: string | null };\n\nexport const listChanges = async (\n query: SnapshotDiffQuery,\n executor: Executor = db,\n): Promise<SnapshotDiffPage> => {\n if (query.to <= query.from || (query.modelIds !== null && query.modelIds.length === 0)) {\n return { items: [], nextAfter: null };\n }\n const rows = await selectSnapshotDiffRows(\n {\n siteId: query.siteId,\n from: query.from,\n to: query.to,\n after: query.after,\n modelIds: query.modelIds,\n rowFilter: query.rowFilter ?? null,\n maxRows: query.limit + 1,\n },\n executor,\n );\n const page = rows.slice(0, query.limit);\n return {\n items: page.map((row) => ({ entryId: row.entry_id, modelId: row.model_id, locales: row.locales })),\n nextAfter: rows.length > query.limit ? (page.at(-1)?.entry_id ?? null) : null,\n };\n};\n\n/**\n * The schema version recorded for each publication sequence number in the `publication_snapshots`\n * ledger; numbers without a ledger row (snapshot 0, rows from before the ledger) are absent.\n */\nexport const schemaVersionsAt = async (\n siteId: string,\n seqs: readonly number[],\n executor: Executor = db,\n): Promise<Map<number, number | null>> => {\n if (seqs.length === 0) {\n return new Map();\n }\n const rows = await executor\n .selectFrom('publication_snapshots')\n .select(['seq', 'schema_version'])\n .where('site_id', '=', siteId)\n .where(\n 'seq',\n 'in',\n seqs.map((seq) => String(seq)),\n )\n .execute();\n return new Map(rows.map((row) => [Number(row.seq), row.schema_version]));\n};\n\n/** When the site's snapshot `seq` was created, from the ledger (undefined without a ledger row). */\nexport const snapshotCreatedAt = async (\n siteId: string,\n seq: number,\n executor: Executor = db,\n): Promise<Date | undefined> =>\n (\n await executor\n .selectFrom('publication_snapshots')\n .select('created_at')\n .where('site_id', '=', siteId)\n .where('seq', '=', String(seq))\n .executeTakeFirst()\n )?.created_at;\n","import {\n effectiveLayout,\n effectiveTitleField,\n isModelDefinition,\n routeKeyOf,\n type ModelDefinition,\n} from '@shapio/schema';\nimport type { Transaction } from 'kysely';\nimport { compileModelRowFilters, maskAllows } from '../content/compiler/policy.js';\nimport { queryInvalid } from '../content/compiler/types.js';\nimport { snapshotInvalid } from '../content/errors.js';\nimport type { DB } from '../db/types.js';\nimport { AppError } from '../helpers/appError.js';\nimport type { FieldMask, RowFilter } from '../permissions/types.js';\nimport * as contentQueriesRepository from '../repositories/contentQueries.js';\nimport * as contentRevisionsRepository from '../repositories/contentRevisions.js';\nimport * as publicationsRepository from '../repositories/publications.js';\nimport * as schemaVersionsRepository from '../repositories/schemaVersions.js';\nimport * as snapshotDiffRepository from '../repositories/snapshotDiff.js';\nimport type { SnapshotChangeKind, SnapshotEntryChange } from '../repositories/snapshotDiff.js';\nimport type { ContentServiceContext } from './contentAccess.js';\n\n/**\n * The snapshot diff API (plan developer-face §5): which entries' live content changed between two\n * publication sequence numbers, for incremental site builds. Delivery principals see only models they may\n * read. Row filters (e.g. `ownedByPrincipal`) are evaluated on the entry as it is now, i.e. at `to` when\n * `to` is the current snapshot; an entry the caller may read now but not at `from` (or the reverse) is\n * judged by its state now. The response names entries and locales, never values, so read masks do not\n * apply: an entry whose only change is in a field the caller cannot read is still listed as `updated`.\n *\n * Old snapshots are diffed as recorded. When the schema version differs between the two ends, clients\n * should rebuild the affected models rather than trust per-entry changes (fields may have been added,\n * converted or removed without new publications); locale deletion rewrites history.\n */\nexport const SNAPSHOT_CHANGES_DEFAULT_LIMIT = 100;\nexport const SNAPSHOT_CHANGES_MAX_LIMIT = 500;\n\nexport type SnapshotChangeView = {\n id: string;\n modelId: string;\n /** The model's API ID (GraphQL, admin API). */\n modelKey: string;\n /** The model's delivery route key (`/api/content/<routeKey>`). */\n routeKey: string;\n /**\n * The entry's title from its revision live at `to` (at `from` when it was unpublished), in the default\n * locale when that changed, else the first changed locale; null without a title field, a readable one\n * or a value.\n */\n title: string | null;\n /** The cover image's media asset ID from the same revision, when the model has a readable cover. */\n coverMediaId: string | null;\n locales: Array<{\n locale: string;\n change: SnapshotChangeKind;\n /** The revision live at `to`, or the one live at `from` for `unpublished` (open it in the admin). */\n revisionId: string | null;\n }>;\n};\n\nexport type SnapshotChangesPage = {\n from: number;\n to: number;\n /** The schema version recorded with each end's snapshot; null when none was recorded. */\n schemaVersions: { from: number | null; to: number | null };\n items: SnapshotChangeView[];\n /** Pass as `after` for the next page; null on the last page. */\n nextCursor: string | null;\n};\n\nexport type SnapshotChangesQuery = { from: number; to?: number; after?: string; limit?: number };\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ntype ReadableModel = { definition: ModelDefinition; rowFilter: RowFilter | null; readMask: FieldMask };\n\n/**\n * Models the caller may read. An authenticated caller (session, token, app user) with none (or an instance\n * with no models yet) gets empty results, never an error. Anonymous callers with nothing readable get 401,\n * as the delivery routes answer them when the `public` role grants nothing.\n */\nconst readableModels = async (context: ContentServiceContext): Promise<ReadableModel[]> => {\n const models = context.snapshot.definitions\n .map((active) => active.definition)\n .filter((definition): definition is ModelDefinition => isModelDefinition(definition));\n const readable: ReadableModel[] = [];\n for (const definition of models) {\n const policy = await context.permissions.evaluate(context.actor, {\n action: 'read',\n modelId: definition.id,\n });\n if (policy.allowed) {\n readable.push({ definition, rowFilter: policy.rowFilter, readMask: policy.readMask });\n }\n }\n if (readable.length === 0 && context.actor.kind === 'anonymous') {\n throw new AppError(401, 'UNAUTHENTICATED', 'Snapshots need a delivery token');\n }\n return readable;\n};\n\ntype Describe = (item: SnapshotEntryChange) => { title: string | null; coverMediaId: string | null };\n\n/** The revision an entry is described from: the default locale's when it changed, else the first. */\nconst revisionOf = (item: SnapshotEntryChange, defaultLocale: string): string | null => {\n const locale = item.locales.find((candidate) => candidate.locale === defaultLocale) ?? item.locales[0];\n return locale ? (locale.toRevisionId ?? locale.fromRevisionId) : null;\n};\n\n/**\n * Title and cover per changed entry, read from one revision each (one query for the page). Fields the\n * caller's read mask hides are left out.\n */\nconst describeEntries = async (\n items: readonly SnapshotEntryChange[],\n models: ReadonlyMap<string, ReadableModel>,\n defaultLocale: string,\n executor: Transaction<DB>,\n): Promise<Describe> => {\n const revisionIds = [...new Set(items.flatMap((item) => revisionOf(item, defaultLocale) ?? []))];\n const rows = await contentRevisionsRepository.findDataByIds(revisionIds, executor);\n const dataById = new Map(rows.map((row) => [row.id, row.data as Record<string, unknown>]));\n return (item) => {\n const model = models.get(item.modelId);\n const revisionId = revisionOf(item, defaultLocale);\n const data = revisionId ? dataById.get(revisionId) : undefined;\n if (!model || !data) {\n return { title: null, coverMediaId: null };\n }\n const titleField = effectiveTitleField(model.definition);\n const cover = effectiveLayout(model.definition).cover;\n const title = titleField && maskAllows(model.readMask, titleField) ? data[titleField.id] : undefined;\n const coverId = cover && maskAllows(model.readMask, cover) ? data[cover.id] : undefined;\n return {\n title:\n typeof title === 'string' && title.trim() !== ''\n ? title\n : typeof title === 'number'\n ? String(title)\n : null,\n coverMediaId: typeof coverId === 'string' ? coverId : null,\n };\n };\n};\n\nconst boundedLimit = (limit: number | undefined) =>\n Math.min(Math.max(limit ?? SNAPSHOT_CHANGES_DEFAULT_LIMIT, 1), SNAPSHOT_CHANGES_MAX_LIMIT);\n\n/** `GET /api/snapshots/changes`, GraphQL `_changes`: one page of entries changed between `from` and `to`. */\nexport const listSnapshotChanges = async (\n context: ContentServiceContext,\n query: SnapshotChangesQuery,\n): Promise<SnapshotChangesPage> => {\n if (query.after !== undefined && !UUID.test(query.after)) {\n throw queryInvalid('\"after\" must be a cursor returned by a previous page');\n }\n const readable = await readableModels(context);\n const byId = new Map(readable.map((model) => [model.definition.id, model]));\n return contentQueriesRepository.withConsistentRead(async (trx) => {\n const current = await publicationsRepository.currentSeq(context.site.id, trx);\n const to = query.to ?? current;\n if (to > current) {\n throw snapshotInvalid(to, current);\n }\n if (query.from < 0 || query.from > to) {\n throw queryInvalid('\"from\" must be between 0 and \"to\"');\n }\n const page = await snapshotDiffRepository.listChanges(\n {\n siteId: context.site.id,\n from: query.from,\n to,\n after: query.after,\n limit: boundedLimit(query.limit),\n modelIds: [...byId.keys()],\n rowFilter: compileModelRowFilters(\n readable.map((model) => ({ modelId: model.definition.id, rowFilter: model.rowFilter })),\n context.actor,\n ),\n },\n trx,\n );\n const versions = await snapshotDiffRepository.schemaVersionsAt(context.site.id, [query.from, to], trx);\n const describe = await describeEntries(page.items, byId, context.snapshot.defaultLocale, trx);\n return {\n from: query.from,\n to,\n schemaVersions: { from: versions.get(query.from) ?? null, to: versions.get(to) ?? null },\n items: page.items.flatMap((item) => {\n const model = byId.get(item.modelId)?.definition;\n return model\n ? [\n {\n id: item.entryId,\n modelId: item.modelId,\n modelKey: model.apiKey,\n routeKey: routeKeyOf(model),\n ...describe(item),\n locales: item.locales.map(({ locale, change, fromRevisionId, toRevisionId }) => ({\n locale,\n change,\n revisionId: toRevisionId ?? fromRevisionId,\n })),\n },\n ]\n : [];\n }),\n nextCursor: page.nextAfter,\n };\n }, context.db);\n};\n\nexport type CurrentSnapshot = {\n /** The latest publication sequence number (what an unpinned read serves). */\n snapshot: number;\n /** The active schema version (`system_versions`; what delivery serves now). */\n schemaVersion: number;\n /** When the latest snapshot was published; null when the ledger has no row for it (snapshot 0). */\n publishedAt: Date | null;\n};\n\n/** `GET /api/snapshots/current`, GraphQL `_snapshot`. */\nexport const currentSnapshot = async (context: ContentServiceContext): Promise<CurrentSnapshot> => {\n await readableModels(context);\n return contentQueriesRepository.withConsistentRead(async (trx) => {\n const snapshot = await publicationsRepository.currentSeq(context.site.id, trx);\n const publishedAt = await snapshotDiffRepository.snapshotCreatedAt(context.site.id, snapshot, trx);\n // The active version (system_versions), read in the same moment as the sequence. Not the ledger's:\n // metadata-only activations (a label rename) take no snapshot number, so the newest ledger row can\n // carry an older schema version than the live one.\n const schemaVersion = await schemaVersionsRepository.getSchemaVersion(trx);\n return { snapshot, schemaVersion, publishedAt: publishedAt ?? null };\n }, context.db);\n};\n","import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';\n\nconst TOKEN_BYTES = 32;\n\n/** A fresh secret: 32 random bytes, base64url (43 characters). */\nexport const generateToken = (): string => randomBytes(TOKEN_BYTES).toString('base64url');\n\n/**\n * What we store for a token: its SHA-256, hex. Tokens are high-entropy random values, so a fast hash is\n * enough (no salt or stretching needed); a leaked table does not reveal usable tokens.\n */\nexport const hashToken = (token: string): string => createHash('sha256').update(token, 'utf8').digest('hex');\n\n/** Constant-time comparison of two strings (unequal lengths return false without comparing). */\nexport const safeEqual = (a: string, b: string): boolean => {\n const left = Buffer.from(a, 'utf8');\n const right = Buffer.from(b, 'utf8');\n return left.length === right.length && timingSafeEqual(left, right);\n};\n\n/** Constant-time check that `token` hashes to `expectedHash`. */\nexport const verifyTokenHash = (token: string, expectedHash: string): boolean =>\n safeEqual(hashToken(token), expectedHash);\n","import type { PermissionEvaluator, PermissionExecutor, Policy, Principal } from './types.js';\n\n/**\n * One request's evaluator, memoized per (principal, action, model): a request touches the same models many\n * times (REST: the read, relation visibility, populate, usage; GraphQL: lists, relation batches,\n * localizations), and evaluates each policy once (ADR 0005). Calls for another principal bypass the memo.\n * A policy still being evaluated is shared only on the executor that evaluates it: one evaluated inside a\n * read transaction is never awaited from another transaction. Once settled, every executor reuses it.\n */\nexport const memoizePermissions = (base: PermissionEvaluator, principal: Principal): PermissionEvaluator => {\n const settled = new Map<string, Policy>();\n const pooled = new Map<string, Promise<Policy>>();\n const byTransaction = new WeakMap<PermissionExecutor, Map<string, Promise<Policy>>>();\n const pendingFor = (executor: PermissionExecutor | undefined) => {\n if (!executor) {\n return pooled;\n }\n let pending = byTransaction.get(executor);\n if (!pending) {\n pending = new Map();\n byTransaction.set(executor, pending);\n }\n return pending;\n };\n return {\n evaluate: (caller, request, executor) => {\n if (caller !== principal) {\n return base.evaluate(caller, request, executor);\n }\n const key = `${request.action}:${request.modelId}`;\n const done = settled.get(key);\n if (done) {\n return Promise.resolve(done);\n }\n const pending = pendingFor(executor);\n let policy = pending.get(key);\n if (!policy) {\n policy = base.evaluate(caller, request, executor);\n pending.set(key, policy);\n // A failure reaches the caller through `policy`; here it only keeps the policy out of `settled`.\n policy.then(\n (value) => settled.set(key, value),\n () => undefined,\n );\n }\n return policy;\n },\n canPerform: (caller, action, executor) => base.canPerform(caller, action, executor),\n canPerformOnSite: (caller, action, siteId, executor) =>\n base.canPerformOnSite(caller, action, siteId, executor),\n };\n};\n","import { memoizePermissions } from './memo.js';\nimport type { KnownVersions, PermissionEvaluator, PermissionExecutor, Principal } from './types.js';\n\n/**\n * Where a scope's durable versions come from. HTTP reads them lazily, once per request (`plugins/requestState.ts`):\n * `read` reads them (or returns the read already made) and `isRead` says whether a read was started. A caller\n * that already holds them passes the versions themselves.\n */\nexport type ScopeVersions = KnownVersions | { read: () => Promise<KnownVersions>; isRead: () => boolean };\n\nconst isLazy = (\n versions: ScopeVersions,\n): versions is { read: () => Promise<KnownVersions>; isRead: () => boolean } => 'read' in versions;\n\n/**\n * The evaluator at the scope's versions. Inside a transaction before a lazy scope read its versions, the\n * unbound evaluator checks the versions itself through the transaction, so no pooled connection is asked\n * for while the transaction holds one.\n */\nconst boundEvaluator = async (\n base: PermissionEvaluator,\n versions: ScopeVersions,\n executor: PermissionExecutor | undefined,\n): Promise<PermissionEvaluator> => {\n if (!isLazy(versions)) {\n return base.atVersions?.(versions) ?? base;\n }\n if (executor && !versions.isRead()) {\n return base;\n }\n return base.atVersions?.(await versions.read()) ?? base;\n};\n\n/**\n * One scope's evaluator (an HTTP request, an in-process delivery call): checked against the scope's durable\n * versions (ADR 0005: a revoke applies from the next scope, not mid-scope) and memoized per (principal, action,\n * model) for the scope (`memo.ts`).\n */\nexport const scopePermissions = (\n base: PermissionEvaluator,\n principal: Principal,\n versions: ScopeVersions,\n): PermissionEvaluator =>\n memoizePermissions(\n {\n evaluate: async (caller, policyRequest, executor) =>\n (await boundEvaluator(base, versions, executor)).evaluate(caller, policyRequest, executor),\n canPerform: async (caller, action, executor) =>\n (await boundEvaluator(base, versions, executor)).canPerform(caller, action, executor),\n canPerformOnSite: async (caller, action, siteId, executor) =>\n (await boundEvaluator(base, versions, executor)).canPerformOnSite(caller, action, siteId, executor),\n },\n principal,\n );\n","import { AppError } from '../helpers/appError.js';\nimport type { AdminIdentity, AdminPrincipal, Principal, RoleAssignment, TokenPrincipal } from './types.js';\n\n/**\n * Narrowing principals to one site (sites plan §H). Roles are instance-wide; assignments say where a role\n * applies. On a site, an admin holds the roles assigned on that site plus the roles assigned on every site.\n * Network actions only ever see the roles assigned on every site.\n */\n\nconst unique = (ids: readonly string[]) => [...new Set(ids)].sort();\n\n/** Roles assigned on every site. */\nexport const networkRolesOf = (assignments: readonly RoleAssignment[]): string[] =>\n unique(\n assignments.filter((assignment) => assignment.siteId === null).map((assignment) => assignment.roleId),\n );\n\n/** Roles that apply on `siteId`: those assigned there and those assigned on every site. */\nexport const siteRolesOf = (assignments: readonly RoleAssignment[], siteId: string | null): string[] =>\n unique(\n assignments\n .filter((assignment) => assignment.siteId === null || assignment.siteId === siteId)\n .map((assignment) => assignment.roleId),\n );\n\n/** Sites the admin holds a role on specifically (not counting roles assigned on every site). */\nexport const assignedSiteIdsOf = (assignments: readonly RoleAssignment[]): string[] =>\n unique(assignments.flatMap((assignment) => (assignment.siteId === null ? [] : [assignment.siteId])));\n\n/** The admin principal for one site, or for a network route (`siteId` null: network roles only). */\nexport const narrowToSite = (identity: AdminIdentity, siteId: string | null): AdminPrincipal => ({\n kind: 'admin',\n adminUserId: identity.adminUserId,\n sessionId: identity.sessionId,\n assignments: identity.assignments,\n siteId,\n roleIds: siteId === null ? networkRolesOf(identity.assignments) : siteRolesOf(identity.assignments, siteId),\n networkRoleIds: networkRolesOf(identity.assignments),\n});\n\n/** The identity behind an admin principal (to narrow it again to another site). */\nexport const identityOf = (principal: AdminPrincipal): AdminIdentity => ({\n adminUserId: principal.adminUserId,\n sessionId: principal.sessionId,\n assignments: principal.assignments,\n});\n\n/** A credential (token, app-user session, link or code) used on another site than its own. */\nexport const siteMismatch = () =>\n new AppError(403, 'SITE_MISMATCH', 'This credential belongs to another site than the one requested');\n\n/**\n * Re-narrows a principal to the request's site. Admins get that site's roles; anonymous callers get the site\n * (its `public` bindings apply). Site credentials (a site token, an app user) act on their own site only:\n * another site is refused (403 `SITE_MISMATCH`), whatever resolved it. A network token's role applies on\n * every site.\n */\nexport const principalForSite = (principal: Principal, siteId: string | null): Principal => {\n switch (principal.kind) {\n case 'admin':\n return narrowToSite(identityOf(principal), siteId);\n case 'anonymous':\n return { kind: 'anonymous', siteId };\n case 'appUser':\n if (principal.siteId !== siteId) {\n throw siteMismatch();\n }\n return principal;\n case 'token':\n if (principal.siteId !== null && principal.siteId !== siteId) {\n throw siteMismatch();\n }\n return principal;\n case 'system':\n return principal;\n }\n};\n\n/** The roles a token holds for network actions: only a network token's (site tokens never reach the network). */\nexport const tokenNetworkRoleIds = (token: TokenPrincipal): readonly string[] =>\n token.siteId === null ? [token.roleId] : [];\n","/** `system_settings` key of the delivery descriptor the server writes at start (services/deliveryDescriptor.ts). */\nexport const DELIVERY_DESCRIPTOR_SETTING = 'delivery_descriptor';\n","import type { JoinBuilder, Kysely, Transaction } from 'kysely';\nimport { DELIVERY_DESCRIPTOR_SETTING } from '../constants/delivery.js';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** Which site a request is about: by ID (the credential's site), by key (the site it names) or the primary. */\nexport type SiteLookup = { by: 'id'; id: string } | { by: 'key'; key: string } | { by: 'primary' };\n\nexport type RequestStateRow = {\n schemaVersion: number;\n permissionsVersion: number;\n /** The looked-up site; undefined when none matches or no lookup was asked for. */\n site: { id: string; key: string } | undefined;\n};\n\n/** The site a lookup names, joined to the one `system_versions` row (at most one row matches). */\nconst siteJoin = (join: JoinBuilder<DB, 'system_versions' | 'sites'>, lookup: SiteLookup) => {\n switch (lookup.by) {\n case 'id':\n return join.on('sites.id', '=', lookup.id);\n case 'key':\n return join.on('sites.key', '=', lookup.key);\n case 'primary':\n return join.on('sites.is_primary', '=', true);\n }\n};\n\n/**\n * Everything a request checks before its work, in one statement: the durable schema and permissions versions\n * (`system_versions`, one row) and, when asked, the request's site. Read once per request on the pool, before\n * any transaction; caches compare against these versions for the rest of the request.\n */\nexport const read = async (lookup: SiteLookup | undefined, trx: Executor = db): Promise<RequestStateRow> => {\n if (!lookup) {\n const row = await trx\n .selectFrom('system_versions')\n .select(['schema_version', 'permissions_version'])\n .executeTakeFirstOrThrow();\n return {\n schemaVersion: row.schema_version,\n permissionsVersion: row.permissions_version,\n site: undefined,\n };\n }\n const row = await trx\n .selectFrom('system_versions')\n .leftJoin('sites', (join) => siteJoin(join, lookup))\n .select([\n 'system_versions.schema_version',\n 'system_versions.permissions_version',\n 'sites.id as site_id',\n 'sites.key as site_key',\n ])\n .executeTakeFirstOrThrow();\n return {\n schemaVersion: row.schema_version,\n permissionsVersion: row.permissions_version,\n site: row.site_id !== null && row.site_key !== null ? { id: row.site_id, key: row.site_key } : undefined,\n };\n};\n\n/** What an in-process delivery call reads besides the request state (plan next-in-process §2). */\nexport type DeliveryStateRow = RequestStateRow & {\n /** The release the server last started with; null before a server of this release started. */\n release: string | null;\n /** The stored delivery descriptor (JSON text); null before a server of this release started. */\n descriptor: string | null;\n /** The token's revocation and expiry; null when no token was asked about or the token no longer exists. */\n token: { revokedAt: Date | null; expiresAt: Date | null } | null;\n};\n\n/**\n * The request state for an in-process delivery call, in the same one statement: the versions and the site,\n * plus the server's release and delivery descriptor, and (for a token already resolved) whether it is still\n * live, so a call is this statement and its read.\n */\nexport const readForDelivery = async (\n input: { site: SiteLookup; tokenId?: string },\n trx: Executor = db,\n): Promise<DeliveryStateRow> => {\n const { tokenId } = input;\n const row = await trx\n .selectFrom('system_versions')\n .leftJoin('sites', (join) => siteJoin(join, input.site))\n .leftJoin('system_settings', (join) => join.on('system_settings.key', '=', DELIVERY_DESCRIPTOR_SETTING))\n .select([\n 'system_versions.schema_version',\n 'system_versions.permissions_version',\n 'system_versions.release',\n 'sites.id as site_id',\n 'sites.key as site_key',\n 'system_settings.value as descriptor',\n ])\n .$if(tokenId !== undefined, (query) =>\n query\n .leftJoin('api_tokens', (join) => join.on('api_tokens.id', '=', tokenId ?? ''))\n .select([\n 'api_tokens.id as token_id',\n 'api_tokens.revoked_at as token_revoked_at',\n 'api_tokens.expires_at as token_expires_at',\n ]),\n )\n .executeTakeFirstOrThrow();\n return {\n schemaVersion: row.schema_version,\n permissionsVersion: row.permissions_version,\n site: row.site_id !== null && row.site_key !== null ? { id: row.site_id, key: row.site_key } : undefined,\n release: row.release,\n descriptor: row.descriptor,\n token:\n row.token_id === undefined || row.token_id === null\n ? null\n : { revokedAt: row.token_revoked_at ?? null, expiresAt: row.token_expires_at ?? null },\n };\n};\n","import type { Insertable, Kysely, Selectable, Transaction, Updateable } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { AdminRolePermissions, AdminRoles, DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type AdminRoleRow = Selectable<AdminRoles>;\nexport type NewAdminRole = Insertable<AdminRoles>;\nexport type AdminRolePermissionRow = Selectable<AdminRolePermissions>;\nexport type NewAdminRolePermission = Insertable<AdminRolePermissions>;\n\nconst PERMISSION_COLUMNS = ['role_id', 'action', 'model_id', 'condition', 'field_ids'] as const;\n\n/** A grant as stored, without bookkeeping columns. */\nexport type PermissionGrantRow = Pick<AdminRolePermissionRow, (typeof PERMISSION_COLUMNS)[number]>;\n\nexport const listRoles = (trx: Executor = db) =>\n trx.selectFrom('admin_roles').selectAll().orderBy('is_system', 'desc').orderBy('name').execute();\n\nexport const findById = (id: string, trx: Executor = db) =>\n trx.selectFrom('admin_roles').selectAll().where('id', '=', id).executeTakeFirst();\n\nexport const findByIds = (ids: readonly string[], trx: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : trx.selectFrom('admin_roles').selectAll().where('id', 'in', ids).execute();\n\nexport const findByKeys = (keys: readonly string[], trx: Executor = db) =>\n keys.length === 0\n ? Promise.resolve([])\n : trx.selectFrom('admin_roles').selectAll().where('key', 'in', keys).execute();\n\n/** Inserts a role unless one with the same key exists; returns the inserted row or undefined. */\nexport const insertIfAbsent = (role: NewAdminRole, trx: Executor = db) =>\n trx\n .insertInto('admin_roles')\n .values(role)\n .onConflict((oc) => oc.column('key').doNothing())\n .returningAll()\n .executeTakeFirst();\n\nexport const insert = (role: NewAdminRole, trx: Executor = db) =>\n trx.insertInto('admin_roles').values(role).returningAll().executeTakeFirstOrThrow();\n\n/** Optimistic update: applies only if the stored version is `expectedVersion`; bumps the version. */\nexport const updateIfVersion = (\n id: string,\n expectedVersion: number,\n changes: Updateable<AdminRoles>,\n trx: Executor = db,\n) =>\n trx\n .updateTable('admin_roles')\n .set((eb) => ({ ...changes, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .returningAll()\n .executeTakeFirst();\n\nexport const deleteById = (id: string, trx: Executor = db) =>\n trx.deleteFrom('admin_roles').where('id', '=', id).executeTakeFirst();\n\n/** Every grant of every role; the permission cache loads them all at once (the table is small). */\nexport const listAllPermissions = (trx: Executor = db) =>\n trx.selectFrom('admin_role_permissions').select(PERMISSION_COLUMNS).execute();\n\nexport const listPermissionsForRoles = (roleIds: readonly string[], trx: Executor = db) =>\n roleIds.length === 0\n ? Promise.resolve([])\n : trx\n .selectFrom('admin_role_permissions')\n .select(PERMISSION_COLUMNS)\n .where('role_id', 'in', roleIds)\n .orderBy('action')\n .orderBy('model_id')\n .execute();\n\nexport const insertPermissionsIfAbsent = (rows: readonly NewAdminRolePermission[], trx: Executor = db) =>\n rows.length === 0\n ? Promise.resolve([])\n : trx\n .insertInto('admin_role_permissions')\n .values([...rows])\n .onConflict((oc) => oc.columns(['role_id', 'action', 'model_id']).doNothing())\n .returning('id')\n .execute();\n\nexport const deletePermissionsForRole = (roleId: string, trx: Executor = db) =>\n trx.deleteFrom('admin_role_permissions').where('role_id', '=', roleId).execute();\n\nexport const insertPermissions = (rows: readonly NewAdminRolePermission[], trx: Executor = db) =>\n rows.length === 0\n ? Promise.resolve([])\n : trx\n .insertInto('admin_role_permissions')\n .values([...rows])\n .returning('id')\n .execute();\n\n/** How many admin users and API tokens hold each role (for delete protection and list views). */\nexport const countHolders = async (roleId: string, trx: Executor = db) => {\n const row = await trx\n .selectNoFrom((eb) => [\n eb\n .selectFrom('admin_user_roles')\n .select((s) => s.fn.countAll<string>().as('n'))\n .where('role_id', '=', roleId)\n .as('users'),\n eb\n .selectFrom('api_tokens')\n .select((s) => s.fn.countAll<string>().as('n'))\n .where('role_id', '=', roleId)\n .where('revoked_at', 'is', null)\n .as('tokens'),\n ])\n .executeTakeFirstOrThrow();\n return { users: Number(row.users ?? 0), tokens: Number(row.tokens ?? 0) };\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport { currentTimestamp } from '../db/sql/time.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** The durable permissions version; caches compare against it (build plan §3.4). */\nexport const getPermissionsVersion = async (trx: Executor = db): Promise<number> => {\n const row = await trx.selectFrom('system_versions').select('permissions_version').executeTakeFirstOrThrow();\n return row.permissions_version;\n};\n\n/** Call in the same transaction as every permission change, so caches reload exactly when it commits. */\nexport const bumpPermissionsVersion = (trx: Executor = db) =>\n trx\n .updateTable('system_versions')\n .set((eb) => ({\n permissions_version: eb('permissions_version', '+', eb.lit(1)),\n updated_at: currentTimestamp(),\n }))\n .execute();\n","import type { Kysely } from 'kysely';\nimport type { DB } from '../db/types.js';\nimport * as adminRolesRepository from '../repositories/adminRoles.js';\nimport * as permissionsVersionRepository from '../repositories/permissionsVersion.js';\nimport { CONTENT_ACTIONS, GLOBAL_ACTIONS, type ContentAction, type GlobalAction } from './types.js';\n\nexport const SYSTEM_ROLE_KEYS = {\n owner: 'owner',\n admin: 'admin',\n editor: 'editor',\n readOnly: 'read-only',\n} as const;\n\ntype SystemRoleDefinition = {\n key: string;\n name: string;\n description: string;\n /** Granted on every model (model_id null), every field. */\n contentActions: readonly ContentAction[];\n globalActions: readonly GlobalAction[];\n};\n\n/**\n * Built-in roles. They cannot be edited or deleted; custom roles cover everything else. `admin` differs\n * from `owner` only in what the services allow around the owner role (only owners grant or remove it).\n */\nexport const SYSTEM_ROLES: readonly SystemRoleDefinition[] = [\n {\n key: SYSTEM_ROLE_KEYS.owner,\n name: 'Owner',\n description: 'Full access, including managing owners.',\n contentActions: CONTENT_ACTIONS,\n globalActions: GLOBAL_ACTIONS,\n },\n {\n key: SYSTEM_ROLE_KEYS.admin,\n name: 'Admin',\n description: 'Full access to content, models, users, roles and tokens. Cannot manage owners.',\n contentActions: CONTENT_ACTIONS,\n globalActions: GLOBAL_ACTIONS,\n },\n {\n key: SYSTEM_ROLE_KEYS.editor,\n name: 'Editor',\n description: 'Creates, edits, publishes and deletes content in every model.',\n contentActions: ['read', 'create', 'update', 'delete', 'publish'],\n // Editors group publications into change sets and (re)trigger site builds; webhooks, deployment\n // connections and the jobs view stay with admins.\n globalActions: ['media.read', 'media.write', 'changes.manage', 'changes.ship', 'deployments.trigger'],\n },\n {\n key: SYSTEM_ROLE_KEYS.readOnly,\n name: 'Read-only',\n description: 'Reads all content, changes nothing.',\n contentActions: ['read'],\n globalActions: ['media.read'],\n },\n];\n\n/**\n * Creates any missing built-in role and grant. Idempotent and safe to run from several instances at once\n * (conflicts are ignored); bumps the permissions version only when something was added. Runs at startup\n * and before `shapio admin create`.\n */\nexport const ensureSystemRoles = async (database: Kysely<DB>): Promise<void> => {\n await database.transaction().execute(async (trx) => {\n let changed = false;\n for (const definition of SYSTEM_ROLES) {\n const inserted = await adminRolesRepository.insertIfAbsent(\n {\n key: definition.key,\n name: definition.name,\n description: definition.description,\n kind: 'admin',\n is_system: true,\n },\n trx,\n );\n changed ||= inserted !== undefined;\n const [role] = await adminRolesRepository.findByKeys([definition.key], trx);\n if (!role) {\n throw new Error(`System role ${definition.key} is missing after insert`);\n }\n const actions = [...definition.contentActions, ...definition.globalActions];\n const added = await adminRolesRepository.insertPermissionsIfAbsent(\n actions.map((action) => ({\n role_id: role.id,\n action,\n model_id: null,\n condition: null,\n field_ids: null,\n })),\n trx,\n );\n changed ||= added.length > 0;\n }\n if (changed) {\n await permissionsVersionRepository.bumpPermissionsVersion(trx);\n }\n });\n};\n","import type { Insertable, Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { ApiTokens, DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type NewApiToken = Insertable<ApiTokens>;\n\nconst LIST_COLUMNS = [\n 'api_tokens.id',\n 'api_tokens.name',\n 'api_tokens.token_prefix',\n 'api_tokens.role_id',\n 'api_tokens.created_by',\n 'api_tokens.expires_at',\n 'api_tokens.last_used_at',\n 'api_tokens.revoked_at',\n 'api_tokens.created_at',\n 'api_tokens.site_id',\n 'admin_roles.kind as role_kind',\n] as const;\n\nconst withRole = (trx: Executor) =>\n trx\n .selectFrom('api_tokens')\n .innerJoin('admin_roles', 'admin_roles.id', 'api_tokens.role_id')\n .select(LIST_COLUMNS);\n\nexport const insert = (token: NewApiToken, trx: Executor = db) =>\n trx.insertInto('api_tokens').values(token).returning('id').executeTakeFirstOrThrow();\n\n/** One site's tokens, plus network tokens (no site) when `includeNetwork` is set. Newest first. */\nexport const listForSite = (siteId: string, includeNetwork: boolean, trx: Executor = db) =>\n withRole(trx)\n .where((eb) =>\n includeNetwork\n ? eb.or([eb('api_tokens.site_id', '=', siteId), eb('api_tokens.site_id', 'is', null)])\n : eb('api_tokens.site_id', '=', siteId),\n )\n .orderBy('api_tokens.created_at', 'desc')\n .orderBy('api_tokens.id', 'desc')\n .execute();\n\nexport const findById = (id: string, trx: Executor = db) =>\n withRole(trx).where('api_tokens.id', '=', id).executeTakeFirst();\n\n/** A usable token for this hash: not revoked. Expiry is checked by the caller. */\nexport const findLiveByHash = (tokenHash: string, trx: Executor = db) =>\n withRole(trx)\n .where('api_tokens.token_hash', '=', tokenHash)\n .where('api_tokens.revoked_at', 'is', null)\n .executeTakeFirst();\n\nexport const touchLastUsed = (id: string, now: Date, staleBefore: Date, trx: Executor = db) =>\n trx\n .updateTable('api_tokens')\n .set({ last_used_at: now })\n .where('id', '=', id)\n .where((eb) => eb.or([eb('last_used_at', 'is', null), eb('last_used_at', '<', staleBefore)]))\n .execute();\n\n/**\n * Revokes a live token of one site, or a network token (no site) when `includeNetwork` is set. Another\n * site's token is left alone (false, as for an unknown token).\n */\nexport const revoke = async (\n id: string,\n scope: { siteId: string; includeNetwork: boolean },\n now: Date,\n trx: Executor = db,\n): Promise<boolean> => {\n const result = await trx\n .updateTable('api_tokens')\n .set({ revoked_at: now, updated_at: now })\n .where('id', '=', id)\n .where('revoked_at', 'is', null)\n .where((eb) =>\n scope.includeNetwork\n ? eb.or([eb('site_id', '=', scope.siteId), eb('site_id', 'is', null)])\n : eb('site_id', '=', scope.siteId),\n )\n .executeTakeFirst();\n return result.numUpdatedRows > 0n;\n};\n\n/** Revoked tokens hold no permissions; they are removed with the role they referenced. */\nexport const deleteRevokedForRole = (roleId: string, trx: Executor = db) =>\n trx.deleteFrom('api_tokens').where('role_id', '=', roleId).where('revoked_at', 'is not', null).execute();\n","import type { Transaction } from 'kysely';\nimport {\n API_TOKEN_DISPLAY_LENGTH,\n API_TOKEN_PREFIX,\n LAST_SEEN_WRITE_INTERVAL_MS,\n} from '../constants/auth.js';\nimport { db, type Database } from '../db/index.js';\nimport type { DB } from '../db/types.js';\nimport { AppError } from '../helpers/appError.js';\nimport { generateToken, hashToken } from '../helpers/tokens.js';\nimport { SYSTEM_ROLE_KEYS } from '../permissions/seedRoles.js';\nimport type { PermissionEvaluator, PermissionExecutor, TokenPrincipal } from '../permissions/types.js';\nimport * as adminRolesRepository from '../repositories/adminRoles.js';\nimport * as apiTokensRepository from '../repositories/apiTokens.js';\nimport type { SiteActorContext } from './actorContext.js';\nimport { recordAudit } from './audit.js';\n\nexport type ApiTokenView = {\n id: string;\n name: string;\n /** The first characters of the token, for telling tokens apart. */\n tokenPrefix: string;\n roleId: string;\n scope: 'admin' | 'delivery';\n /** The token's site; null for a network admin token (its role applies on every site). */\n siteId: string | null;\n createdBy: string | null;\n expiresAt: Date | null;\n lastUsedAt: Date | null;\n revokedAt: Date | null;\n createdAt: Date;\n};\n\ntype ApiTokenListRow = Awaited<ReturnType<typeof apiTokensRepository.listForSite>>[number];\n\nconst scopeOf = (roleKind: string): 'admin' | 'delivery' => (roleKind === 'delivery' ? 'delivery' : 'admin');\n\nconst toApiTokenView = (row: ApiTokenListRow): ApiTokenView => ({\n id: row.id,\n name: row.name,\n tokenPrefix: row.token_prefix,\n roleId: row.role_id,\n scope: scopeOf(row.role_kind),\n siteId: row.site_id,\n createdBy: row.created_by,\n expiresAt: row.expires_at,\n lastUsedAt: row.last_used_at,\n revokedAt: row.revoked_at,\n createdAt: row.created_at,\n});\n\nexport const isApiTokenFormat = (value: string): boolean => value.startsWith(API_TOKEN_PREFIX);\n\n/**\n * Network tokens (no site) are visible and revocable only by admins who could mint them (`users.manage`, a\n * network action); everyone else sees their site's tokens only.\n */\nconst managesNetworkTokens = (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n executor?: PermissionExecutor,\n) => permissions.canPerform(context.actor, 'users.manage', executor);\n\n/** The request site's tokens, plus network tokens for those who manage them. */\nexport const listApiTokens = async (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n): Promise<ApiTokenView[]> =>\n (\n await apiTokensRepository.listForSite(context.site.id, await managesNetworkTokens(context, permissions))\n ).map(toApiTokenView);\n\ntype CreateApiTokenInput = {\n name: string;\n roleId: string;\n expiresAt: Date | null;\n /** A network admin token (every site and network actions); omitted or false: a token of the request's site. */\n network?: boolean | undefined;\n};\n\n/**\n * Whether the new token is a network token (sites plan §H). Tokens belong to the request's site unless the\n * creator asks for a network token explicitly. A network admin token carries its role on every site and\n * into network actions, so only someone who may assign roles on every site (`users.manage`, a network\n * action) may mint one. Delivery tokens always belong to a site.\n */\nconst isNetworkToken = async (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n roleKind: string,\n requested: boolean | undefined,\n trx: Transaction<DB>,\n): Promise<boolean> => {\n if (roleKind === 'delivery') {\n if (requested === true) {\n throw new AppError(400, 'INVALID_TOKEN_SITE', 'Delivery tokens always belong to one site');\n }\n return false;\n }\n if (requested !== true) {\n return false;\n }\n if (!(await managesNetworkTokens(context, permissions, trx))) {\n throw new AppError(\n 403,\n 'FORBIDDEN',\n 'Only admins who manage users on every site can create network tokens',\n );\n }\n return true;\n};\n\n/**\n * Creates a token bound to one role. The value is returned once and stored only as a hash. The owner\n * role cannot be bound to a token: owner powers stay with people.\n */\nexport const createApiToken = async (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n input: CreateApiTokenInput,\n): Promise<{ token: string; apiToken: ApiTokenView }> => {\n const now = new Date();\n if (input.expiresAt !== null && input.expiresAt.getTime() <= now.getTime()) {\n throw new AppError(400, 'INVALID_EXPIRY', 'The expiry must be in the future');\n }\n const token = `${API_TOKEN_PREFIX}${generateToken()}`;\n return db.transaction().execute(async (trx) => {\n const role = await adminRolesRepository.findById(input.roleId, trx);\n if (!role) {\n throw new AppError(400, 'INVALID_ROLES', 'The role does not exist');\n }\n if (role.key === SYSTEM_ROLE_KEYS.owner) {\n throw new AppError(400, 'INVALID_ROLES', 'API tokens cannot hold the owner role');\n }\n const network = await isNetworkToken(context, permissions, role.kind, input.network, trx);\n const { id } = await apiTokensRepository.insert(\n {\n name: input.name.trim(),\n token_hash: hashToken(token),\n token_prefix: token.slice(0, API_TOKEN_DISPLAY_LENGTH),\n role_id: role.id,\n site_id: network ? null : context.site.id,\n created_by: context.actor.kind === 'admin' ? context.actor.adminUserId : null,\n expires_at: input.expiresAt,\n },\n trx,\n );\n await recordAudit(trx, {\n ...context,\n action: 'api_token.create',\n target: { type: 'api_token', id },\n metadata: {\n name: input.name,\n roleId: role.id,\n siteId: network ? null : context.site.id,\n expiresAt: input.expiresAt?.toISOString() ?? null,\n },\n });\n const row = await apiTokensRepository.findById(id, trx);\n if (!row) {\n throw new Error('API token vanished after insert');\n }\n return { token, apiToken: toApiTokenView(row) };\n });\n};\n\n/** Revokes a token of the request's site (or a network token, for those who manage them). */\nexport const revokeApiToken = async (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n id: string,\n): Promise<void> => {\n const scope = { siteId: context.site.id, includeNetwork: await managesNetworkTokens(context, permissions) };\n await db.transaction().execute(async (trx) => {\n if (!(await apiTokensRepository.revoke(id, scope, new Date(), trx))) {\n throw new AppError(404, 'NOT_FOUND', 'Active API token not found');\n }\n await recordAudit(trx, { ...context, action: 'api_token.revoke', target: { type: 'api_token', id } });\n });\n};\n\ntype ResolveTokenOptions = {\n /** Records the token's last use (default). In-process delivery reads never write (plan next-in-process). */\n recordUse?: boolean;\n executor?: Database;\n};\n\n/** Whether a token with this expiry has expired at `now`. */\nexport const isTokenExpired = (expiresAt: Date | null, now: Date): boolean =>\n expiresAt !== null && expiresAt.getTime() <= now.getTime();\n\n/** The principal for a bearer API token, or undefined when unknown, revoked or expired. */\nexport const resolveApiToken = async (\n value: string,\n now = new Date(),\n { recordUse = true, executor = db }: ResolveTokenOptions = {},\n): Promise<TokenPrincipal | undefined> => {\n const row = await apiTokensRepository.findLiveByHash(hashToken(value), executor);\n if (!row || isTokenExpired(row.expires_at, now)) {\n return undefined;\n }\n const staleBefore = new Date(now.getTime() - LAST_SEEN_WRITE_INTERVAL_MS);\n // Skips the statement when the row just read is recent; the WHERE clause still guards concurrent writers.\n if (recordUse && (row.last_used_at === null || row.last_used_at.getTime() < staleBefore.getTime())) {\n await apiTokensRepository.touchLastUsed(row.id, now, staleBefore, executor);\n }\n return {\n kind: 'token',\n tokenId: row.id,\n scope: scopeOf(row.role_kind),\n roleId: row.role_id,\n siteId: row.site_id,\n };\n};\n","import type { Transaction } from 'kysely';\nimport type { ContentData } from '../db/contentData.js';\nimport type { DB } from '../db/types.js';\nimport type { Principal } from '../permissions/types.js';\nimport type { SiteRef } from '../services/actorContext.js';\nimport type { ContentModel } from './model.js';\n\n/**\n * Lifecycle hook points (ADR 0009). The content services call every hook point inside the owning\n * transaction; a hook that throws aborts the write. This registry is the engine only: the extension\n * runtime (extensions/hooks.ts) registers project hooks on it, running `before*` hooks right here in the\n * transaction and turning `after*` hook points into an outbox event, so project `after*` hooks run after\n * commit as jobs and can never roll the write back.\n */\nexport const BEFORE_EVENTS = ['beforeCreate', 'beforeUpdate', 'beforePublish', 'beforeDelete'] as const;\nexport const AFTER_EVENTS = ['afterCreate', 'afterUpdate', 'afterPublish', 'afterDelete'] as const;\nexport const LIFECYCLE_EVENTS = [...BEFORE_EVENTS, ...AFTER_EVENTS] as const;\n\nexport type BeforeEvent = (typeof BEFORE_EVENTS)[number];\nexport type AfterEvent = (typeof AFTER_EVENTS)[number];\nexport type LifecycleEvent = BeforeEvent | AfterEvent;\n\n/** The state of the entry the hook is about: its draft, its published head, or a deleted entry. */\nexport type LifecycleEntryState = 'draft' | 'published' | 'deleted';\n\nexport type LifecycleContext = {\n trx: Transaction<DB>;\n /** The site the entry belongs to (sites plan §H; ADR 0009 note). */\n site: SiteRef;\n model: ContentModel;\n entryId: string;\n locale: string | null;\n /** The document in storage form (keyed by field IDs); absent for deletes. */\n data?: Readonly<ContentData>;\n /** The document before the change (the previous draft, or the live version being replaced). */\n before?: Readonly<ContentData>;\n actor: Principal;\n};\n\nexport type LifecycleHook = (context: LifecycleContext) => Promise<void> | void;\n\nexport type ContentHooks = {\n on: (event: LifecycleEvent, hook: LifecycleHook) => () => void;\n run: (event: LifecycleEvent, context: LifecycleContext) => Promise<void>;\n};\n\nexport const ENTRY_STATE_BY_EVENT: Readonly<Record<LifecycleEvent, LifecycleEntryState>> = {\n beforeCreate: 'draft',\n afterCreate: 'draft',\n beforeUpdate: 'draft',\n afterUpdate: 'draft',\n beforePublish: 'published',\n afterPublish: 'published',\n beforeDelete: 'deleted',\n afterDelete: 'deleted',\n};\n\nexport const createContentHooks = (): ContentHooks => {\n const hooks = new Map<LifecycleEvent, LifecycleHook[]>();\n return {\n on: (event, hook) => {\n hooks.set(event, [...(hooks.get(event) ?? []), hook]);\n return () =>\n hooks.set(\n event,\n (hooks.get(event) ?? []).filter((candidate) => candidate !== hook),\n );\n },\n run: async (event, context) => {\n for (const hook of hooks.get(event) ?? []) {\n await hook(context);\n }\n },\n };\n};\n","import { createContentHooks, type ContentHooks } from '../content/hooks.js';\nimport type { Database } from '../db/index.js';\nimport type { PermissionEvaluator, Principal } from '../permissions/types.js';\nimport type { NetworkSchema } from '../schema/snapshot.js';\nimport type { SiteRef } from './actorContext.js';\nimport type { ContentServiceContext } from './contentAccess.js';\n\n/** What every caller of the content services has: an HTTP request, an extension service, an in-process read. */\nexport type ContentContextInput = {\n db: Database;\n /** The full schema the caller is pinned to; the context sees the site's view of it. */\n network: NetworkSchema;\n /** The caller's evaluator (memoized and version-bound for a request: `permissions/scoped.ts`). */\n permissions: PermissionEvaluator;\n actor: Principal;\n site: SiteRef;\n /** Lifecycle hooks for writes. Omitted: none (reads never reach a hook point). */\n hooks?: ContentHooks;\n media?: ContentServiceContext['media'];\n requestId?: string;\n ip?: string;\n};\n\n/**\n * The content service context for one call, the same however the call arrived: the site's view of the pinned\n * schema (plan site-schema: shared definitions and the site's own), scoped to the site.\n */\nexport const buildContentContext = ({\n network,\n hooks,\n media,\n requestId,\n ip,\n ...rest\n}: ContentContextInput): ContentServiceContext => ({\n ...rest,\n snapshot: network.forSite(rest.site.id),\n hooks: hooks ?? createContentHooks(),\n ...(media ? { media } : {}),\n ...(requestId !== undefined ? { requestId } : {}),\n ...(ip !== undefined ? { ip } : {}),\n});\n","import type { Kysely, Selectable, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB, SchemaRevisions } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type SchemaRevisionRow = Selectable<SchemaRevisions>;\n\n/** The active revision of every live (not deleted) model and component. */\nexport const findActiveDefinitions = (executor: Executor = db) =>\n executor\n .selectFrom('model_active_versions as active')\n .innerJoin('models', 'models.id', 'active.model_id')\n .innerJoin('schema_revisions as revision', 'revision.id', 'active.revision_id')\n .where('models.deleted_at', 'is', null)\n .select([\n 'models.id as modelId',\n 'models.kind as kind',\n 'models.site_id as siteId',\n 'active.version as version',\n 'active.revision_id as revisionId',\n 'active.activated_at as activatedAt',\n 'revision.hash as hash',\n 'revision.definition as definition',\n ])\n .orderBy('models.created_at')\n .orderBy('models.id')\n .execute();\n\nexport type ActiveDefinitionRow = Awaited<ReturnType<typeof findActiveDefinitions>>[number];\n\nexport const findModelById = (id: string, executor: Executor = db) =>\n executor.selectFrom('models').selectAll().where('id', '=', id).executeTakeFirst();\n\n/** `siteId` null: a shared definition (every site); otherwise the one site it belongs to. */\nexport const insertModel = (\n model: { id: string; kind: string; apiKey: string; siteId: string | null },\n trx: Executor = db,\n) =>\n trx\n .insertInto('models')\n .values({ id: model.id, kind: model.kind, api_key: model.apiKey, site_id: model.siteId })\n .execute();\n\n/** Definitions of one site that are not deleted (active or still pending their first activation). */\nexport const countSiteDefinitions = async (siteId: string, executor: Executor = db): Promise<number> => {\n const row = await executor\n .selectFrom('models')\n .select((eb) => eb.fn.countAll<number | string | bigint>().as('count'))\n .where('site_id', '=', siteId)\n .where('deleted_at', 'is', null)\n .executeTakeFirst();\n return Number(row?.count ?? 0);\n};\n\n/** A site's deleted definitions (what `deleteSite` purges with the site). */\nexport const findDeletedIdsOfSite = async (siteId: string, executor: Executor = db): Promise<string[]> =>\n (\n await executor\n .selectFrom('models')\n .select('id')\n .where('site_id', '=', siteId)\n .where('deleted_at', 'is not', null)\n .execute()\n ).map((row) => row.id);\n\n/**\n * Purges deleted definitions with their history (`deleteSite`, once the site has no live definition and the\n * definitions' deleted entries are purged): change rows, revisions, then the models rows.\n */\nexport const purgeDefinitions = async (ids: readonly string[], trx: Executor = db): Promise<void> => {\n if (ids.length === 0) {\n return;\n }\n await trx.deleteFrom('schema_change_jobs').where('model_id', 'in', ids).execute();\n await trx.deleteFrom('model_active_versions').where('model_id', 'in', ids).execute();\n // Newest first, one row at a time: a revision's parent always has a lower version, and MySQL checks the\n // self-reference row by row.\n const revisions = await trx\n .selectFrom('schema_revisions')\n .select('id')\n .where('model_id', 'in', ids)\n .orderBy('version', 'desc')\n .execute();\n for (const revision of revisions) {\n await trx.deleteFrom('schema_revisions').where('id', '=', revision.id).execute();\n }\n await trx.deleteFrom('models').where('id', 'in', ids).execute();\n};\n\n/**\n * Keeps the denormalised kind, API key and scope in step with the active revision; also undeletes. The scope\n * only differs from the stored one for a scope change (`ActivationItem.moveScope`).\n */\nexport const markModelActive = (\n model: { id: string; kind: string; apiKey: string; siteId: string | null; now: Date },\n trx: Executor = db,\n) =>\n trx\n .updateTable('models')\n .set({\n kind: model.kind,\n api_key: model.apiKey,\n site_id: model.siteId,\n updated_at: model.now,\n deleted_at: null,\n })\n .where('id', '=', model.id)\n .execute();\n\nexport const softDeleteModel = (id: string, now: Date, trx: Executor = db) =>\n trx.updateTable('models').set({ deleted_at: now, updated_at: now }).where('id', '=', id).execute();\n\nexport const findActivePointer = (modelId: string, executor: Executor = db) =>\n executor.selectFrom('model_active_versions').selectAll().where('model_id', '=', modelId).executeTakeFirst();\n\nexport const upsertActivePointer = (\n pointer: { modelId: string; revisionId: string; version: number; schemaVersion: number; now: Date },\n trx: Executor = db,\n) =>\n trx\n .insertInto('model_active_versions')\n .values({\n model_id: pointer.modelId,\n revision_id: pointer.revisionId,\n version: pointer.version,\n schema_version: pointer.schemaVersion,\n activated_at: pointer.now,\n })\n .onConflict((oc) =>\n oc.column('model_id').doUpdateSet({\n revision_id: pointer.revisionId,\n version: pointer.version,\n schema_version: pointer.schemaVersion,\n activated_at: pointer.now,\n }),\n )\n .execute();\n\nexport const deleteActivePointer = (modelId: string, trx: Executor = db) =>\n trx.deleteFrom('model_active_versions').where('model_id', '=', modelId).execute();\n\nexport const findLatestRevisionVersion = async (\n modelId: string,\n executor: Executor = db,\n): Promise<number> => {\n const row = await executor\n .selectFrom('schema_revisions')\n .select((eb) => eb.fn.max('version').as('version'))\n .where('model_id', '=', modelId)\n .executeTakeFirst();\n return row?.version ?? 0;\n};\n\nexport type NewRevision = {\n modelId: string;\n version: number;\n definition: unknown;\n hash: string;\n parentRevisionId: string | null;\n createdByType: string;\n createdById: string | null;\n};\n\nexport const insertRevision = (revision: NewRevision, trx: Executor = db) =>\n trx\n .insertInto('schema_revisions')\n .values({\n model_id: revision.modelId,\n version: revision.version,\n definition: JSON.stringify(revision.definition),\n hash: revision.hash,\n parent_revision_id: revision.parentRevisionId,\n created_by_type: revision.createdByType,\n created_by_id: revision.createdById,\n })\n .returning(['id', 'version', 'created_at'])\n .executeTakeFirstOrThrow();\n\nexport const findRevisionById = (id: string, executor: Executor = db) =>\n executor.selectFrom('schema_revisions').selectAll().where('id', '=', id).executeTakeFirst();\n\n/** History of one model, newest first, without the definitions (fetch one revision for its body). */\nexport const listRevisions = (modelId: string, limit: number, executor: Executor = db) =>\n executor\n .selectFrom('schema_revisions')\n .select(['id', 'version', 'hash', 'parent_revision_id', 'created_by_type', 'created_by_id', 'created_at'])\n .where('model_id', '=', modelId)\n .orderBy('version', 'desc')\n .limit(limit)\n .execute();\n\n/** The newest revision of a model, active or not (used to restore a deleted definition). */\nexport const findLatestRevision = (modelId: string, executor: Executor = db) =>\n executor\n .selectFrom('schema_revisions')\n .selectAll()\n .where('model_id', '=', modelId)\n .orderBy('version', 'desc')\n .limit(1)\n .executeTakeFirst();\n","import type { Transaction } from 'kysely';\nimport { LOCK_NAMESPACE } from '../../constants/lockKeys.js';\nimport { acquireXactLock, lockKeyFromId } from '../../db/advisoryLocks.js';\nimport type { DB } from '../../db/types.js';\n\n/**\n * Serialises every schema activation (and the start of every planned change) instance-wide. Activations are\n * rare, and holding this lock while re-validating the whole schema closes races between changes to\n * *different* definitions (e.g. two renames to colliding GraphQL names). It lives in the model namespace;\n * a collision with a model's hash only costs extra serialisation.\n */\nexport const SCHEMA_ACTIVATION_LOCK_KEY = lockKeyFromId('shapio:schema-activation');\n\n/**\n * Takes the global schema lock, then the exclusive per-model lock of each affected model in key order (the\n * same order entry writes use for their shared locks, so no deadlock). Entry writes to these models wait\n * until the activation commits (ADR 0002, transitional write policy).\n */\nexport const lockForActivation = async (trx: Transaction<DB>, modelIds: readonly string[]): Promise<void> => {\n await acquireXactLock(trx, LOCK_NAMESPACE.model, SCHEMA_ACTIVATION_LOCK_KEY);\n const keys = [...new Set(modelIds.map(lockKeyFromId))].sort((a, b) => a - b);\n for (const key of keys) {\n await acquireXactLock(trx, LOCK_NAMESPACE.model, key);\n }\n};\n\nexport const lockSchema = (trx: Transaction<DB>) =>\n acquireXactLock(trx, LOCK_NAMESPACE.model, SCHEMA_ACTIVATION_LOCK_KEY);\n","import { db } from '../db/index.js';\nimport type { Database } from '../db/index.js';\nimport { isUniqueViolation } from '../db/sql/errors.js';\nimport { AppError } from '../helpers/appError.js';\nimport { assignedSiteIdsOf } from '../permissions/sites.js';\nimport type { AdminPrincipal, TokenPrincipal } from '../permissions/types.js';\nimport * as appUsersRepository from '../repositories/appUsers.js';\nimport * as contentPurgeRepository from '../repositories/contentPurge.js';\nimport * as mediaAssetsRepository from '../repositories/mediaAssets.js';\nimport type { SiteLookup } from '../repositories/requestState.js';\nimport * as schemaModelsRepository from '../repositories/schemaModels.js';\nimport * as sitesRepository from '../repositories/sites.js';\nimport type { SiteRow } from '../repositories/sites.js';\nimport { lockSchema } from '../schema/planner/locks.js';\nimport type { ActorContext, SiteRef } from './actorContext.js';\nimport { recordAudit } from './audit.js';\n\n/**\n * Sites (plan agentic-ecosystem §H, ADR 0011): the tenants of content. The schema, locales, admin users and\n * roles are shared; everything a site publishes is its own. Exactly one site is primary: requests that name\n * no site go to it, and it can never be deleted.\n */\nexport type SiteView = {\n id: string;\n key: string;\n name: string;\n isPrimary: boolean;\n version: number;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type SiteSummaryView = Pick<SiteView, 'id' | 'key' | 'name' | 'isPrimary'>;\n\nexport const toSiteView = (row: SiteRow): SiteView => ({\n id: row.id,\n key: row.key,\n name: row.name,\n isPrimary: row.is_primary,\n version: row.version,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n});\n\nexport const toSiteSummary = (row: SiteRow): SiteSummaryView => ({\n id: row.id,\n key: row.key,\n name: row.name,\n isPrimary: row.is_primary,\n});\n\nexport const toSiteRef = (row: Pick<SiteRow, 'id' | 'key'>): SiteRef => ({ id: row.id, key: row.key });\n\nconst siteNotFound = () => new AppError(404, 'SITE_NOT_FOUND', 'Site not found');\n\n/** A site's summary by ID (`me` for a site the admin holds no role on: they see its name, nothing else). */\n/** How many sites the instance has, whatever roles the caller holds (one: per-site schema UI is moot). */\nexport const countSites = (): Promise<number> => sitesRepository.count();\n\nexport const getSiteSummary = async (siteId: string): Promise<SiteSummaryView> => {\n const site = await sitesRepository.findById(siteId);\n if (!site) {\n throw siteNotFound();\n }\n return toSiteSummary(site);\n};\n\n/** The reference of a site by ID (jobs and deferred work carry the ID of the site they belong to). */\nexport const getSiteRef = async (siteId: string, executor: Database = db): Promise<SiteRef> => {\n const site = await sitesRepository.findById(siteId, executor);\n if (!site) {\n throw siteNotFound();\n }\n return toSiteRef(site);\n};\n\n/**\n * The sites a principal works on: every site for a role assigned on every site (or a network token),\n * otherwise the sites it holds a role on. Used by `me` and the site switcher.\n */\nexport const listAccessibleSites = async (principal: AdminPrincipal | TokenPrincipal): Promise<SiteRow[]> => {\n if (principal.kind === 'token') {\n return principal.siteId === null ? sitesRepository.list() : sitesRepository.findByIds([principal.siteId]);\n }\n return principal.networkRoleIds.length > 0\n ? sitesRepository.list()\n : sitesRepository.findByIds(assignedSiteIdsOf(principal.assignments));\n};\n\nexport const listSites = async (principal: AdminPrincipal | TokenPrincipal): Promise<SiteView[]> =>\n (await listAccessibleSites(principal)).map(toSiteView);\n\nexport const getSite = async (principal: AdminPrincipal | TokenPrincipal, id: string): Promise<SiteView> => {\n const site = (await listAccessibleSites(principal)).find((row) => row.id === id);\n if (!site) {\n throw siteNotFound();\n }\n return toSiteView(site);\n};\n\n/** Every site, primary first (`shapio sites list`, which runs on the server host without a principal). */\nexport const listAllSites = async (database: Database = db): Promise<SiteView[]> =>\n (await sitesRepository.list(database)).map(toSiteView);\n\n/** A new site starts empty: its own publication sequence at 0, and no app roles bound (deny by default). */\nexport const createSite = async (\n context: ActorContext,\n input: { key: string; name: string },\n database: Database = db,\n): Promise<SiteView> => {\n try {\n return await database.transaction().execute(async (trx) => {\n const site = await sitesRepository.insert({ key: input.key, name: input.name.trim() }, trx);\n await sitesRepository.insertPublicationState(site.id, trx);\n await recordAudit(trx, {\n ...context,\n action: 'site.create',\n target: { type: 'site', id: site.id },\n metadata: { key: site.key, name: site.name },\n });\n return toSiteView(site);\n });\n } catch (error) {\n if (isUniqueViolation(error)) {\n throw new AppError(409, 'SITE_KEY_TAKEN', 'A site with this key already exists', undefined, {\n cause: error,\n });\n }\n throw error;\n }\n};\n\n/** Optimistic: fails with VERSION_CONFLICT when the site changed since `expectedVersion` was read. */\nexport const renameSite = async (\n context: ActorContext,\n id: string,\n input: { expectedVersion: number; name: string },\n): Promise<SiteView> =>\n db.transaction().execute(async (trx) => {\n const current = await sitesRepository.findById(id, trx);\n if (!current) {\n throw siteNotFound();\n }\n const updated = await sitesRepository.renameIfVersion(id, input.expectedVersion, input.name.trim(), trx);\n if (!updated) {\n throw new AppError(409, 'VERSION_CONFLICT', 'The site changed since you loaded it', {\n expectedVersion: input.expectedVersion,\n actualVersion: current.version,\n });\n }\n await recordAudit(trx, {\n ...context,\n action: 'site.update',\n target: { type: 'site', id },\n metadata: { name: { from: current.name, to: updated.name } },\n });\n return toSiteView(updated);\n });\n\n/**\n * Deletes an empty site: no live entries, media or app users, no folders, change sets or definitions of its\n * own. Its soft-deleted entries (with their history), definitions, media assets and app users are purged first; its tokens, webhooks, deployment\n * connections, role assignments, app role bindings, snapshot ledger and usage counters go with it.\n */\nexport const deleteSite = async (context: ActorContext, id: string): Promise<void> => {\n await db.transaction().execute(async (trx) => {\n const site = await sitesRepository.lockById(id, trx);\n if (!site) {\n throw siteNotFound();\n }\n if (site.is_primary) {\n throw new AppError(409, 'SITE_IS_PRIMARY', 'The primary site cannot be deleted');\n }\n // Serialised with schema changes, so no definition can be created on the site meanwhile.\n await lockSchema(trx);\n const contents = {\n ...(await sitesRepository.countContents(id, trx)),\n // The site's own content types and components (plan site-schema): delete or share them first.\n definitions: await schemaModelsRepository.countSiteDefinitions(id, trx),\n };\n if (Object.values(contents).some((count) => count > 0)) {\n throw new AppError(\n 409,\n 'SITE_NOT_EMPTY',\n 'Only an empty site can be deleted: remove its entries, media, change sets, app users and content types first',\n contents,\n );\n }\n await contentPurgeRepository.purgeDeletedEntriesOfSite(id, trx);\n const deletedDefinitions = await schemaModelsRepository.findDeletedIdsOfSite(id, trx);\n await contentPurgeRepository.purgeDeletedEntriesOfModels(deletedDefinitions, trx);\n await schemaModelsRepository.purgeDefinitions(deletedDefinitions, trx);\n await mediaAssetsRepository.deleteSoftDeletedOfSite(id, trx);\n await appUsersRepository.deleteSoftDeletedOfSite(id, trx);\n await sitesRepository.deleteById(id, trx);\n await recordAudit(trx, {\n ...context,\n action: 'site.delete',\n target: { type: 'site', id },\n metadata: { key: site.key, name: site.name },\n });\n });\n};\n\nexport type SiteResolutionInput = {\n /** The site the credential belongs to (a site token); undefined when the credential names none. */\n credentialSiteId: string | undefined;\n /** The site key the request names (`Shapio-Site` header or `?site=`). */\n requestedKey: string | undefined;\n};\n\n/** Reads the site a lookup names; undefined when none matches. */\nexport type SiteReader = (lookup: SiteLookup) => Promise<SiteRef | undefined>;\n\n/** The site a lookup names, read on its own (a request whose state was read without its site). */\nexport const findSite: SiteReader = async (lookup) => {\n switch (lookup.by) {\n case 'id':\n return sitesRepository.findById(lookup.id).then((row) => row && toSiteRef(row));\n case 'key':\n return sitesRepository.findByKey(lookup.key).then((row) => row && toSiteRef(row));\n case 'primary':\n return toSiteRef(await sitesRepository.findPrimary());\n }\n};\n\n/**\n * Which site a request is about (sites plan §H): the credential's site, else the site the request names,\n * else the primary site. A request that names a different site than its credential is refused\n * (403 `SITE_MISMATCH`), never redirected; an unknown key is 404 `SITE_NOT_FOUND`.\n * `read`: where the site is read; a request passes its once-per-request state read\n * (`repositories/requestState.ts`), which reads the site with the versions in one statement.\n */\nexport const resolveSite = async (\n { credentialSiteId, requestedKey }: SiteResolutionInput,\n read: SiteReader = findSite,\n): Promise<SiteRef> => {\n if (credentialSiteId !== undefined) {\n const site = await read({ by: 'id', id: credentialSiteId });\n if (!site) {\n throw siteNotFound();\n }\n if (requestedKey !== undefined && requestedKey !== site.key) {\n throw new AppError(\n 403,\n 'SITE_MISMATCH',\n `This credential belongs to site \"${site.key}\", not \"${requestedKey}\"`,\n );\n }\n return site;\n }\n if (requestedKey !== undefined) {\n const site = await read({ by: 'key', key: requestedKey });\n if (!site) {\n throw new AppError(404, 'SITE_NOT_FOUND', `No site has the key \"${requestedKey}\"`);\n }\n return site;\n }\n const primary = await read({ by: 'primary' });\n if (!primary) {\n // Every instance has exactly one primary site (migrations create it; it can never be deleted).\n throw new Error('No primary site');\n }\n return primary;\n};\n","import { SHAPIO_VERSION } from '../constants/version.js';\nimport { AppError } from '../helpers/appError.js';\nimport { hashToken } from '../helpers/tokens.js';\nimport { scopePermissions } from '../permissions/scoped.js';\nimport { principalForSite } from '../permissions/sites.js';\nimport type { Principal } from '../permissions/types.js';\nimport * as requestStateRepository from '../repositories/requestState.js';\nimport type { DeliveryStateRow, SiteLookup } from '../repositories/requestState.js';\nimport { isTokenExpired, resolveApiToken } from '../services/apiTokens.js';\nimport type { ContentServiceContext } from '../services/contentAccess.js';\nimport { buildContentContext } from '../services/contentContext.js';\nimport { resolveSite } from '../services/sites.js';\nimport { ShapioVersionSkewError } from './errors.js';\nimport type { DeliveryRuntime } from './runtime.js';\n\n/** Who an in-process call reads as, and which site it reads. */\nexport type CallCredentials = {\n /** A delivery API token; none reads as an anonymous caller (the site's `public` role). */\n token?: string;\n /** The site key; default the token's site, else the primary site (as over HTTP). */\n site?: string;\n};\n\nconst ANONYMOUS: Principal = Object.freeze({ kind: 'anonymous', siteId: null });\n\nconst invalidToken = () => new AppError(401, 'INVALID_TOKEN', 'The API token is invalid, expired or revoked');\n\n/**\n * Runs a statement of the call's preamble, mapping a failure on a database that predates this release (a\n * statement names a column or table it lacks) to a version skew rather than an SQL error.\n */\nconst guardingSkew = async <T>(runtime: DeliveryRuntime, statement: () => Promise<T>): Promise<T> => {\n try {\n return await statement();\n } catch (error) {\n if (error instanceof AppError) {\n throw error;\n }\n // Loaded only here: the migration list is not needed on the read path.\n const { getPendingMigrations } = await import('../db/migrator.js');\n if ((await getPendingMigrations(runtime.db)).length > 0) {\n throw new ShapioVersionSkewError(null, SHAPIO_VERSION, { cause: error });\n }\n throw error;\n }\n};\n\n/** The one state statement: versions, site, release, descriptor and the token's liveness. */\nconst readState = (\n runtime: DeliveryRuntime,\n site: SiteLookup,\n tokenId: string | undefined,\n): Promise<DeliveryStateRow> =>\n guardingSkew(runtime, () =>\n requestStateRepository.readForDelivery(\n { site, ...(tokenId !== undefined ? { tokenId } : {}) },\n runtime.db,\n ),\n );\n\n/**\n * The caller: anonymous, or a delivery token resolved once per runtime (read-only: in-process reads never\n * record a token's use) and checked for liveness on every call by the state read. Admin-scope tokens are\n * refused: in-process reads are delivery reads only (rule 7).\n */\nconst callerOf = async (\n runtime: DeliveryRuntime,\n token: string | undefined,\n): Promise<{ principal: Principal; tokenKey?: string; tokenId?: string }> => {\n if (token === undefined) {\n return { principal: ANONYMOUS };\n }\n const tokenKey = hashToken(token);\n let principal = runtime.tokens.get(tokenKey);\n if (!principal) {\n const resolved = await guardingSkew(runtime, () =>\n resolveApiToken(token, new Date(), { recordUse: false, executor: runtime.db }),\n );\n if (!resolved) {\n throw invalidToken();\n }\n if (resolved.scope !== 'delivery') {\n throw new AppError(\n 403,\n 'DELIVERY_TOKEN_REQUIRED',\n 'In-process reads take a delivery token (or none); this is an admin API token',\n );\n }\n runtime.tokens.set(tokenKey, resolved);\n principal = resolved;\n }\n return { principal, tokenKey, tokenId: principal.tokenId };\n};\n\n/**\n * Opens one in-process call the way the HTTP API opens a request: one statement reads the schema and\n * permissions versions, the site, the server's release and descriptor, and the token's liveness; the call\n * then sees the schema and the permissions at those versions (a change committed later applies from the next\n * call, so a model changed in the admin shows on the next call without a restart: rule 1).\n */\nexport const openCallScope = async (\n runtime: DeliveryRuntime,\n { token, site: requestedKey }: CallCredentials,\n): Promise<ContentServiceContext> => {\n const caller = await callerOf(runtime, token);\n let state: DeliveryStateRow | undefined;\n const site = await resolveSite(\n {\n credentialSiteId:\n caller.principal.kind === 'token' ? (caller.principal.siteId ?? undefined) : undefined,\n requestedKey,\n },\n async (lookup) => {\n state = await readState(runtime, lookup, caller.tokenId);\n // Before anything else of the row is trusted: another release may mean other semantics.\n if (state.release !== SHAPIO_VERSION) {\n throw new ShapioVersionSkewError(state.release, SHAPIO_VERSION);\n }\n return state.site;\n },\n );\n if (!state) {\n throw new Error('The site was resolved without reading the request state');\n }\n if (caller.tokenKey !== undefined) {\n const live = state.token;\n if (!live || live.revokedAt !== null || isTokenExpired(live.expiresAt, new Date())) {\n runtime.tokens.delete(caller.tokenKey);\n throw invalidToken();\n }\n }\n if (state.descriptor === null) {\n throw new AppError(503, 'SERVER_NOT_STARTED', 'Start the Shapio server of this release once');\n }\n const versions = {\n schemaVersion: state.schemaVersion,\n permissionsVersion: state.permissionsVersion,\n };\n const principal = principalForSite(caller.principal, site.id);\n const [network, media] = await Promise.all([\n runtime.registry.getSnapshot(undefined, versions),\n runtime.mediaFor(state.descriptor),\n ]);\n return buildContentContext({\n db: runtime.db,\n network,\n permissions: scopePermissions(runtime.permissions, principal, versions),\n actor: principal,\n site,\n media,\n });\n};\n","import { createClient, ShapioApiError, type RequestFn, type RequestOptions } from '@shapio/client';\nimport { Value } from 'typebox/value';\nimport { SITE_QUERY_PARAMETER } from '../constants/sites.js';\nimport { apiKeyOfRoute } from '../content/model.js';\nimport { AppError } from '../helpers/appError.js';\nimport type { ErrorLike } from '../helpers/errorResponse.js';\nimport { withoutSiteParameter } from '../helpers/siteQuery.js';\nimport { SnapshotChangesQuerySchema, type SnapshotChangesQuery } from '../routes/snapshots/schemas.js';\nimport type { ContentServiceContext } from '../services/contentAccess.js';\nimport { getDelivery, listDelivery } from '../services/contentDelivery.js';\nimport { getDeliverySite } from '../services/siteSeo.js';\nimport { currentSnapshot, listSnapshotChanges } from '../services/snapshotChanges.js';\nimport { ShapioVersionSkewError, toShapioApiError } from './errors.js';\nimport type { DeliveryRuntime } from './runtime.js';\nimport { openCallScope, type CallCredentials } from './scope.js';\n\n/**\n * The delivery API as a `RequestFn` (`@shapio/client`'s transport): the client's delivery, site and snapshot\n * groups call it with the same paths they send over HTTP, and it answers them in process, with the same\n * bodies, and errors as the same `ShapioApiError`. Paths:\n *\n * - `GET /api/content/:routeKey` (a collection's page, or a singleton's entry)\n * - `GET /api/content/:routeKey/:id`\n * - `GET /api/site`\n * - `GET /api/snapshots/current`\n * - `GET /api/snapshots/changes`\n *\n * Anything else (writes, previews, the admin API, GraphQL) is 404 `NOT_AVAILABLE_IN_PROCESS`.\n */\nexport type DeliveryRoute =\n | { kind: 'list'; routeKey: string }\n | { kind: 'item'; routeKey: string; id: string }\n | { kind: 'site' }\n | { kind: 'snapshotCurrent' }\n | { kind: 'snapshotChanges' };\n\n/** A matched path: the route, its querystring without `site`, and the site the path names, if any. */\nexport type MatchedRequest = { route: DeliveryRoute; query: string; site: string | undefined };\n\nconst decodeSegment = (segment: string): string | undefined => {\n try {\n return decodeURIComponent(segment);\n } catch {\n return undefined;\n }\n};\n\nconst routeOf = (segments: readonly string[]): DeliveryRoute | undefined => {\n const [api, group, first, second, ...rest] = segments;\n if (api !== 'api' || rest.length > 0) {\n return undefined;\n }\n if (group === 'content' && first) {\n const routeKey = decodeSegment(first);\n if (routeKey === undefined) {\n return undefined;\n }\n if (second === undefined) {\n return { kind: 'list', routeKey };\n }\n const id = decodeSegment(second);\n return id ? { kind: 'item', routeKey, id } : undefined;\n }\n if (group === 'site' && first === undefined) {\n return { kind: 'site' };\n }\n if (group === 'snapshots' && second === undefined) {\n if (first === 'current') {\n return { kind: 'snapshotCurrent' };\n }\n if (first === 'changes') {\n return { kind: 'snapshotChanges' };\n }\n }\n return undefined;\n};\n\n/** Matches a delivery read; undefined for any other method or path. */\nexport const matchDeliveryRequest = (method: string, path: string): MatchedRequest | undefined => {\n if (method.toUpperCase() !== 'GET') {\n return undefined;\n }\n const index = path.indexOf('?');\n const pathname = index === -1 ? path : path.slice(0, index);\n const rawQuery = index === -1 ? '' : path.slice(index + 1);\n if (!pathname.startsWith('/')) {\n return undefined;\n }\n const route = routeOf(pathname.slice(1).split('/'));\n if (!route) {\n return undefined;\n }\n // An empty `?site=` names no site, as over HTTP.\n const site = new URLSearchParams(rawQuery).get(SITE_QUERY_PARAMETER) || undefined;\n return { route, query: withoutSiteParameter(rawQuery), site };\n};\n\nconst notAvailable = (method: string, path: string) =>\n new ShapioApiError(404, {\n error: {\n code: 'NOT_AVAILABLE_IN_PROCESS',\n message: `${method.toUpperCase()} ${path.split('?')[0] ?? path} is not available in process; only delivery reads are`,\n },\n });\n\n/** The site a call reads: the path's `?site=` or the credentials' site; both must agree. */\nconst siteOf = (pathSite: string | undefined, credentialsSite: string | undefined): string | undefined => {\n if (pathSite !== undefined && credentialsSite !== undefined && pathSite !== credentialsSite) {\n throw new AppError(403, 'SITE_MISMATCH', 'The site option and ?site= name different sites');\n }\n return pathSite ?? credentialsSite;\n};\n\n/** `?from&to&after&limit`, coerced and checked against the HTTP route's own schema. */\nconst snapshotChangesQuery = (rawQuery: string): SnapshotChangesQuery => {\n const value = Value.Convert(SnapshotChangesQuerySchema, Object.fromEntries(new URLSearchParams(rawQuery)));\n if (Value.Check(SnapshotChangesQuerySchema, value)) {\n return value;\n }\n const validation = Value.Errors(SnapshotChangesQuerySchema, value).map(\n ({ instancePath, message, params }) => ({ instancePath, message, params }),\n );\n const [first] = validation;\n const failure: ErrorLike = Object.assign(\n new Error(`querystring${first?.instancePath ?? ''} ${first?.message ?? 'is invalid'}`),\n { statusCode: 400, validation },\n );\n throw failure;\n};\n\n/**\n * Values exactly as the HTTP API sends them: a JSON round trip, as the server serializes them. Dates become ISO\n * strings, `undefined` members disappear, and so do symbol-keyed members (a delivered rich-text value carries\n * its HTML renderer for GraphQL behind a symbol), so a caller gets plain data that holds nothing else.\n */\nconst asJson = (value: unknown): unknown => JSON.parse(JSON.stringify(value)) as unknown;\n\nconst dispatch = async (\n context: ContentServiceContext,\n { route, query }: MatchedRequest,\n): Promise<unknown> => {\n switch (route.kind) {\n case 'list':\n return listDelivery(context, apiKeyOfRoute(context.snapshot, route.routeKey), query);\n case 'item':\n return getDelivery(context, apiKeyOfRoute(context.snapshot, route.routeKey), route.id, query);\n case 'site':\n return getDeliverySite(context);\n case 'snapshotCurrent':\n return currentSnapshot(context);\n case 'snapshotChanges':\n return listSnapshotChanges(context, snapshotChangesQuery(query));\n }\n};\n\n/**\n * A `RequestFn` that answers delivery reads in this process, as `credentials` (a delivery token or none, and\n * a site). With `onVersionSkew: 'http'`, a call refused for a release mismatch is sent to the server instead.\n */\nexport const createInProcessRequest = (runtime: DeliveryRuntime, credentials: CallCredentials): RequestFn => {\n let fallback: RequestFn | undefined;\n let warnedSkew = false;\n const fallbackRequest = (): RequestFn | undefined => {\n if (runtime.onVersionSkew !== 'http' || runtime.fallbackUrl === undefined) {\n return undefined;\n }\n fallback ??= createClient({\n baseUrl: runtime.fallbackUrl,\n ...(credentials.token !== undefined ? { token: credentials.token } : {}),\n ...(credentials.site !== undefined ? { site: credentials.site } : {}),\n }).request;\n return fallback;\n };\n\n return async <T>(path: string, options: RequestOptions = {}): Promise<T> => {\n const method = options.method ?? 'GET';\n const matched = matchDeliveryRequest(method, path);\n if (!matched) {\n throw notAvailable(method, path);\n }\n try {\n const site = siteOf(matched.site, credentials.site);\n const context = await openCallScope(runtime, {\n ...(credentials.token !== undefined ? { token: credentials.token } : {}),\n ...(site !== undefined ? { site } : {}),\n });\n const result = asJson(await dispatch(context, matched)) as T;\n warnedSkew = false;\n return result;\n } catch (error) {\n const http = error instanceof ShapioVersionSkewError ? fallbackRequest() : undefined;\n if (http && error instanceof ShapioVersionSkewError) {\n if (!warnedSkew) {\n warnedSkew = true;\n runtime.log.warn(\n { serverRelease: error.serverRelease, libraryRelease: error.libraryRelease },\n 'release mismatch; reading over HTTP until the releases match',\n );\n }\n return http<T>(path, options);\n }\n throw toShapioApiError(error, runtime.log);\n }\n };\n};\n","/**\n * The ONLY place Shapio builds absolute URLs (media, preview, OAuth callbacks, emails, OpenAPI servers,\n * redirects). Everything derives from PUBLIC_URL (the origin) and BASE_PATH (the sub-path), so Shapio\n * works on any domain, port or sub-path without a reverse proxy rewriting links. ESLint forbids\n * building `http(s)://` strings anywhere else in apps/api/src.\n */\nexport type UrlBuilder = {\n /** Origin without a trailing slash, e.g. `https://cms.example.com`. */\n readonly publicUrl: string;\n /** `''` at the root, otherwise `/cms`. */\n readonly basePath: string;\n /** App-relative path → path on this server: `/api/ready` → `/cms/api/ready`. For routes and Location headers. */\n withBasePath: (path: string) => string;\n /** App-relative path → absolute URL: `/api/ready` → `https://cms.example.com/cms/api/ready`. */\n absoluteUrl: (path: string) => string;\n /** A request's raw URL (already including BASE_PATH) → absolute URL on the public origin. For redirects. */\n originUrl: (rawUrl: string) => string;\n};\n\nexport class InvalidUrlPathError extends Error {\n constructor(path: string) {\n super(`Expected an app-relative path starting with a single \"/\", got ${JSON.stringify(path)}`);\n this.name = 'InvalidUrlPathError';\n }\n}\n\nconst assertRelativePath = (path: string): void => {\n // `//host` would be protocol-relative: an open redirect waiting to happen.\n if (!path.startsWith('/') || path.startsWith('//')) {\n throw new InvalidUrlPathError(path);\n }\n};\n\nexport const createUrlBuilder = ({\n publicUrl,\n basePath,\n}: {\n publicUrl: string;\n basePath: string;\n}): UrlBuilder => {\n const origin = publicUrl.replace(/\\/+$/, '');\n const withBasePath = (path: string) => {\n assertRelativePath(path);\n return `${basePath}${path}`;\n };\n return {\n publicUrl: origin,\n basePath,\n withBasePath,\n absoluteUrl: (path) => `${origin}${withBasePath(path)}`,\n originUrl: (rawUrl) => {\n const safe = rawUrl.startsWith('/') ? rawUrl.replace(/^\\/+/, '/') : '/';\n return `${origin}${safe}`;\n },\n };\n};\n\ntype LoopbackTarget = { port: number; basePath: string; https: boolean };\n\n/**\n * URL of this server on the loopback interface (for health probes from inside the host or container),\n * which deliberately bypasses PUBLIC_URL.\n */\nexport const loopbackUrl = ({ port, basePath, https }: LoopbackTarget, path: string): string => {\n assertRelativePath(path);\n return `${https ? 'https' : 'http'}://127.0.0.1:${port}${basePath}${path}`;\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\n/**\n * HMAC signatures for media capabilities, keyed by the instance signing secret (`app.signingSecret`).\n * Each purpose has its own prefix so a signature for one can never be replayed as another.\n */\ntype SignedPurpose = 'media-get' | 'media-upload';\n\nconst sign = (secret: string, purpose: SignedPurpose, subject: string, expiresAt: number): string =>\n createHmac('sha256', secret).update(`${purpose}\\n${subject}\\n${expiresAt}`).digest('base64url');\n\nconst safeEqual = (a: string, b: string): boolean => {\n const left = Buffer.from(a);\n const right = Buffer.from(b);\n return left.length === right.length && timingSafeEqual(left, right);\n};\n\nexport type SignatureCheck = 'valid' | 'invalid' | 'expired';\n\nconst verify = (\n secret: string,\n purpose: SignedPurpose,\n subject: string,\n expiresAt: number,\n signature: string,\n now: number,\n): SignatureCheck => {\n if (!Number.isSafeInteger(expiresAt) || !safeEqual(sign(secret, purpose, subject, expiresAt), signature)) {\n return 'invalid';\n }\n return expiresAt * 1000 <= now ? 'expired' : 'valid';\n};\n\n/** `expiresAt` is in Unix seconds. */\nexport const signMediaGet = (secret: string, key: string, expiresAt: number): string =>\n sign(secret, 'media-get', key, expiresAt);\n\nexport const verifyMediaGet = (\n secret: string,\n key: string,\n expiresAt: number,\n signature: string,\n now = Date.now(),\n): SignatureCheck => verify(secret, 'media-get', key, expiresAt, signature, now);\n\n/** The capability to upload bytes for one grant (local driver), sent as a form field like an S3 policy. */\nexport const signUploadGrant = (secret: string, grantId: string, expiresAt: number): string =>\n sign(secret, 'media-upload', grantId, expiresAt);\n\nexport const verifyUploadGrant = (\n secret: string,\n grantId: string,\n expiresAt: number,\n signature: string,\n now = Date.now(),\n): SignatureCheck => verify(secret, 'media-upload', grantId, expiresAt, signature, now);\n","import { randomBytes } from 'node:crypto';\nimport { createReadStream, createWriteStream } from 'node:fs';\nimport { mkdir, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';\nimport { dirname, relative, resolve, sep } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { MEDIA_FILE_ROUTE_PREFIX } from '../constants/media.js';\nimport type { UrlBuilder } from '../helpers/publicUrl.js';\nimport { InvalidStorageKeyError, ObjectNotFoundError } from './errors.js';\nimport { encodeKeyPath, isValidStorageKey } from './keys.js';\nimport { signMediaGet } from './signing.js';\nimport type { StorageAdapter } from './types.js';\n\nexport type LocalAdapterOptions = {\n /** MEDIA_PATH, absolute. */\n root: string;\n urls: UrlBuilder;\n /** Needed to sign private URLs; jobs and the CLI never sign, so they may omit it. */\n signingSecret: string | undefined;\n publicBaseUrl: string | undefined;\n};\n\nconst isNotFound = (error: unknown) =>\n error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT';\n\n/**\n * Files under MEDIA_PATH, served by Shapio itself: public objects at a stable URL, private ones only with\n * an HMAC-signed, expiring URL. Writes go to a temporary file and are renamed into place, so a reader never\n * sees half a file.\n */\nexport const createLocalAdapter = ({\n root,\n urls,\n signingSecret,\n publicBaseUrl,\n}: LocalAdapterOptions): StorageAdapter => {\n const pathOf = (key: string): string => {\n const path = resolve(root, key);\n if (!isValidStorageKey(key) || relative(root, path).startsWith('..') || !path.startsWith(root + sep)) {\n throw new InvalidStorageKeyError(key);\n }\n return path;\n };\n\n const statOrThrow = async (key: string) => {\n try {\n return await stat(pathOf(key));\n } catch (error) {\n throw isNotFound(error) ? new ObjectNotFoundError(key, { cause: error }) : error;\n }\n };\n\n /** Removes now-empty directories up to the asset folder; a non-empty one stops the walk. */\n const pruneEmptyParents = async (path: string) => {\n let directory = dirname(path);\n while (directory.startsWith(root + sep) && relative(root, directory).split(sep).length > 1) {\n try {\n await rmdir(directory);\n } catch {\n return;\n }\n directory = dirname(directory);\n }\n };\n\n return {\n driver: 'local',\n put: async (key, body) => {\n const path = pathOf(key);\n await mkdir(dirname(path), { recursive: true });\n const temporary = `${path}.${randomBytes(6).toString('hex')}.tmp`;\n try {\n if (Buffer.isBuffer(body)) {\n await writeFile(temporary, body);\n } else {\n await pipeline(body, createWriteStream(temporary));\n }\n await rename(temporary, path);\n } catch (error) {\n await rm(temporary, { force: true });\n throw error;\n }\n },\n getStream: async (key, range) => {\n await statOrThrow(key);\n return createReadStream(pathOf(key), range ? { start: range.start, end: range.end } : {});\n },\n head: async (key) => {\n try {\n const info = await statOrThrow(key);\n return { size: info.size, contentType: undefined };\n } catch (error) {\n if (error instanceof ObjectNotFoundError) {\n return undefined;\n }\n throw error;\n }\n },\n exists: async (key) => {\n try {\n await statOrThrow(key);\n return true;\n } catch (error) {\n if (error instanceof ObjectNotFoundError) {\n return false;\n }\n throw error;\n }\n },\n delete: async (key) => {\n const path = pathOf(key);\n await rm(path, { force: true });\n await pruneEmptyParents(path);\n },\n signedGetUrl: async (key, { expiresInSeconds }) => {\n if (!signingSecret) {\n throw new Error('The local media adapter was created without a signing secret');\n }\n pathOf(key);\n const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;\n const signature = signMediaGet(signingSecret, key, expiresAt);\n return urls.absoluteUrl(\n `${MEDIA_FILE_ROUTE_PREFIX}${encodeKeyPath(key)}?expires=${expiresAt}&signature=${signature}`,\n );\n },\n publicUrl: (key) => (publicBaseUrl ? `${publicBaseUrl}/${encodeKeyPath(key)}` : undefined),\n };\n};\n","import type { StorageConfig } from '../config/index.js';\nimport type { UrlBuilder } from '../helpers/publicUrl.js';\nimport { createLocalAdapter } from './localAdapter.js';\nimport type { MediaStorage, StorageAdapter, StorageDriver } from './types.js';\n\nexport type MediaStorageDependencies = {\n urls: UrlBuilder;\n /** Required by the API (private URLs); jobs and the CLI never sign URLs. */\n signingSecret?: string;\n};\n\nexport class StorageNotConfiguredError extends Error {\n constructor(driver: StorageDriver) {\n super(\n driver === 's3'\n ? 'Media is stored on S3 but STORAGE_S3_BUCKET is not set'\n : `Storage driver ${driver} is not configured`,\n );\n this.name = 'StorageNotConfiguredError';\n }\n}\n\n/**\n * Every adapter this instance can reach. Local disk is always available; S3 whenever its bucket is set,\n * whatever STORAGE_DRIVER says, so assets keep being served while `shapio media migrate` moves them. The S3\n * adapter (and the AWS SDK) is loaded only then.\n */\nexport const createMediaStorage = async (\n config: StorageConfig,\n deps: MediaStorageDependencies,\n): Promise<MediaStorage> => {\n const adapters = new Map<StorageDriver, StorageAdapter>();\n adapters.set(\n 'local',\n createLocalAdapter({\n root: config.mediaPath,\n urls: deps.urls,\n signingSecret: deps.signingSecret,\n publicBaseUrl: config.publicBaseUrl,\n }),\n );\n if (config.s3) {\n const { createS3Adapter } = await import('./s3Adapter.js');\n adapters.set('s3', createS3Adapter({ config: config.s3, publicBaseUrl: config.publicBaseUrl }));\n }\n const get = (driver: StorageDriver): StorageAdapter => {\n const adapter = adapters.get(driver);\n if (!adapter) {\n throw new StorageNotConfiguredError(driver);\n }\n return adapter;\n };\n return { active: get(config.driver), get, has: (driver) => adapters.has(driver) };\n};\n","import type { Insertable, Kysely, Selectable, Transaction, Updateable } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { AppRolePermissions, AppRoles, DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type AppRoleRow = Selectable<AppRoles>;\nexport type NewAppRole = Insertable<AppRoles>;\nexport type NewAppRolePermission = Insertable<AppRolePermissions>;\n\nconst PERMISSION_COLUMNS = ['role_id', 'action', 'model_id', 'condition', 'field_ids'] as const;\n\n/** A grant as stored, without bookkeeping columns (the same shape as admin role grants). */\nexport type AppPermissionGrantRow = Pick<Selectable<AppRolePermissions>, (typeof PERMISSION_COLUMNS)[number]>;\n\nexport const listRoles = (trx: Executor = db) =>\n trx.selectFrom('app_roles').selectAll().orderBy('is_system', 'desc').orderBy('name').execute();\n\nexport const findById = (id: string, trx: Executor = db) =>\n trx.selectFrom('app_roles').selectAll().where('id', '=', id).executeTakeFirst();\n\nexport const findByIds = (ids: readonly string[], trx: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : trx.selectFrom('app_roles').selectAll().where('id', 'in', ids).execute();\n\nexport const insert = (role: NewAppRole, trx: Executor = db) =>\n trx.insertInto('app_roles').values(role).returningAll().executeTakeFirstOrThrow();\n\n/** Optimistic update: applies only if the stored version is `expectedVersion`; bumps the version. */\nexport const updateIfVersion = (\n id: string,\n expectedVersion: number,\n changes: Updateable<AppRoles>,\n trx: Executor = db,\n) =>\n trx\n .updateTable('app_roles')\n .set((eb) => ({ ...changes, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .returningAll()\n .executeTakeFirst();\n\nexport const deleteById = (id: string, trx: Executor = db) =>\n trx.deleteFrom('app_roles').where('id', '=', id).executeTakeFirst();\n\n/** Every grant of every app role; the permission cache loads them with the admin grants (small table). */\nexport const listAllPermissions = (trx: Executor = db) =>\n trx.selectFrom('app_role_permissions').select(PERMISSION_COLUMNS).execute();\n\nexport const listPermissionsForRoles = (roleIds: readonly string[], trx: Executor = db) =>\n roleIds.length === 0\n ? Promise.resolve([])\n : trx\n .selectFrom('app_role_permissions')\n .select(PERMISSION_COLUMNS)\n .where('role_id', 'in', roleIds)\n .orderBy('action')\n .orderBy('model_id')\n .execute();\n\nexport const deletePermissionsForRole = (roleId: string, trx: Executor = db) =>\n trx.deleteFrom('app_role_permissions').where('role_id', '=', roleId).execute();\n\nexport const insertPermissions = (rows: readonly NewAppRolePermission[], trx: Executor = db) =>\n rows.length === 0\n ? Promise.resolve([])\n : trx\n .insertInto('app_role_permissions')\n .values([...rows])\n .returning('id')\n .execute();\n\n/** How many live app users hold the role (delete protection, list views). */\nexport const countHolders = async (roleId: string, trx: Executor = db): Promise<number> => {\n const row = await trx\n .selectFrom('app_user_roles')\n .innerJoin('app_users', 'app_users.id', 'app_user_roles.app_user_id')\n .select((eb) => eb.fn.countAll<string>().as('n'))\n .where('app_user_roles.role_id', '=', roleId)\n .where('app_users.deleted_at', 'is', null)\n .executeTakeFirstOrThrow();\n return Number(row.n);\n};\n\n/** Live holders per role, for list views (roles nobody holds are absent). */\nexport const countHoldersByRole = async (trx: Executor = db): Promise<Map<string, number>> => {\n const rows = await trx\n .selectFrom('app_user_roles')\n .innerJoin('app_users', 'app_users.id', 'app_user_roles.app_user_id')\n .select((eb) => ['app_user_roles.role_id', eb.fn.countAll<string>().as('n')])\n .where('app_users.deleted_at', 'is', null)\n .groupBy('app_user_roles.role_id')\n .execute();\n return new Map(rows.map((row) => [row.role_id, Number(row.n)]));\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** Who a binding applies to: anonymous callers (`public`) or every signed-in app user (`authenticated`). */\nexport type AppRoleAudience = 'public' | 'authenticated';\n\nexport type SiteAppRoleBinding = { site_id: string; audience: string; role_id: string };\n\n/** Every binding on every site (the permission cache holds them all, like the grants). */\nexport const listAll = (trx: Executor = db): Promise<SiteAppRoleBinding[]> =>\n trx.selectFrom('site_app_roles').select(['site_id', 'audience', 'role_id']).execute();\n\nexport const listForSite = (siteId: string, trx: Executor = db): Promise<SiteAppRoleBinding[]> =>\n trx\n .selectFrom('site_app_roles')\n .select(['site_id', 'audience', 'role_id'])\n .where('site_id', '=', siteId)\n .orderBy('audience')\n .orderBy('role_id')\n .execute();\n\n/** Replaces the roles bound to one audience on one site. */\nexport const replaceForAudience = async (\n siteId: string,\n audience: AppRoleAudience,\n roleIds: readonly string[],\n trx: Executor = db,\n): Promise<void> => {\n await trx\n .deleteFrom('site_app_roles')\n .where('site_id', '=', siteId)\n .where('audience', '=', audience)\n .execute();\n if (roleIds.length > 0) {\n await trx\n .insertInto('site_app_roles')\n .values(roleIds.map((roleId) => ({ site_id: siteId, audience, role_id: roleId })))\n .execute();\n }\n};\n","import type { Kysely } from 'kysely';\nimport type { DB } from '../db/types.js';\nimport * as adminRolesRepository from '../repositories/adminRoles.js';\nimport * as appRolesRepository from '../repositories/appRoles.js';\nimport * as permissionsVersionRepository from '../repositories/permissionsVersion.js';\nimport * as siteAppRolesRepository from '../repositories/siteAppRoles.js';\nimport type { AppRoleAudience } from '../repositories/siteAppRoles.js';\nimport type { Grant } from './policy.js';\nimport {\n CONTENT_ACTIONS,\n GLOBAL_ACTIONS,\n type ContentAction,\n type GlobalAction,\n type KnownVersions,\n type PermissionExecutor,\n} from './types.js';\n\n/**\n * Where the evaluator gets grants from. The database-backed cache in production, a fixed list in unit tests.\n * `getSiteAppRoleIds`: the app roles a site binds to anonymous callers (`public`) or to every signed-in app\n * user (`authenticated`); a site that binds nothing grants them nothing (sites plan §H).\n */\nexport type GrantSource = {\n getGrants: (\n roleIds: readonly string[],\n executor?: PermissionExecutor,\n versions?: KnownVersions,\n ) => Promise<readonly Grant[]>;\n getSiteAppRoleIds: (\n siteId: string,\n audience: AppRoleAudience,\n executor?: PermissionExecutor,\n versions?: KnownVersions,\n ) => Promise<readonly string[]>;\n};\n\ntype Snapshot = {\n version: number;\n byRole: ReadonlyMap<string, readonly Grant[]>;\n /** `<siteId>:<audience>` → bound app role IDs. */\n bindings: ReadonlyMap<string, readonly string[]>;\n};\n\nconst bindingKey = (siteId: string, audience: string) => `${siteId}:${audience}`;\n\nconst groupBindings = (rows: readonly siteAppRolesRepository.SiteAppRoleBinding[]) => {\n const bindings = new Map<string, string[]>();\n for (const row of rows) {\n const key = bindingKey(row.site_id, row.audience);\n bindings.set(key, [...(bindings.get(key) ?? []), row.role_id]);\n }\n return bindings;\n};\n\nconst KNOWN_ACTIONS: ReadonlySet<string> = new Set([...CONTENT_ACTIONS, ...GLOBAL_ACTIONS]);\n\nconst KNOWN_CONDITIONS: ReadonlySet<string> = new Set<NonNullable<Grant['condition']>>(['ownedByPrincipal']);\n\n/**\n * A stored grant as this build understands it. Rows written by a newer Shapio may name an action or a row\n * condition this build does not know; they grant nothing here (fail closed). An unknown condition must never\n * read as \"no condition\", which would widen the grant to every row.\n */\nexport const toGrant = (row: adminRolesRepository.PermissionGrantRow): Grant | undefined => {\n if (!KNOWN_ACTIONS.has(row.action)) {\n return undefined;\n }\n if (row.condition !== null && !KNOWN_CONDITIONS.has(row.condition)) {\n return undefined;\n }\n return {\n roleId: row.role_id,\n action: row.action as ContentAction | GlobalAction,\n modelId: row.model_id,\n condition: row.condition as Grant['condition'],\n fieldIds: row.field_ids,\n };\n};\n\nconst groupByRole = (grants: readonly Grant[]): Map<string, Grant[]> => {\n const byRole = new Map<string, Grant[]>();\n for (const grant of grants) {\n const list = byRole.get(grant.roleId) ?? [];\n list.push(grant);\n byRole.set(grant.roleId, list);\n }\n return byRole;\n};\n\n/**\n * Every role's grants, held in memory and reloaded when `system_versions.permissions_version` moves.\n * The version is checked on every lookup: read here, or read once by the request (`versions`, see\n * `KnownVersions`). That durable check is what makes a permission change visible to every instance from the\n * next request (NOTIFY would only be an optimisation). Lookups on the pool share one reload; a\n * lookup inside a transaction reads (and, when stale, reloads) through that transaction alone, never\n * through a reload running on another request's transaction.\n */\nexport const createPermissionCache = (database: Kysely<DB>): GrantSource => {\n let snapshot: Snapshot | undefined;\n let reloading: Promise<Snapshot> | undefined;\n\n const reload = async (version: number, executor: PermissionExecutor): Promise<Snapshot> => {\n // Read after the version: grants are at least as new as the version they are filed under, and a\n // change that commits in between bumps the version again, so the next lookup reloads.\n // Admin and app role IDs are distinct UUIDs; principals only ever carry roles of their own kind.\n const rows = [\n ...(await adminRolesRepository.listAllPermissions(executor)),\n ...(await appRolesRepository.listAllPermissions(executor)),\n ];\n const grants = rows.map(toGrant).filter((grant): grant is Grant => grant !== undefined);\n const bindings = groupBindings(await siteAppRolesRepository.listAll(executor));\n return { version, byRole: groupByRole(grants), bindings };\n };\n\n const sharedReload = (version: number): Promise<Snapshot> =>\n (reloading ??= reload(version, database).finally(() => {\n reloading = undefined;\n }));\n\n const current = async (executor?: PermissionExecutor, versions?: KnownVersions): Promise<Snapshot> => {\n const version =\n versions?.permissionsVersion ??\n (await permissionsVersionRepository.getPermissionsVersion(executor ?? database));\n if (snapshot && snapshot.version === version) {\n return snapshot;\n }\n const loaded = await (executor ? reload(version, executor) : sharedReload(version));\n if (!snapshot || loaded.version >= snapshot.version) {\n snapshot = loaded;\n }\n return loaded;\n };\n\n return {\n getGrants: async (roleIds, executor, versions) => {\n if (roleIds.length === 0) {\n return [];\n }\n const { byRole } = await current(executor, versions);\n return roleIds.flatMap((roleId) => byRole.get(roleId) ?? []);\n },\n getSiteAppRoleIds: async (siteId, audience, executor, versions) => {\n const { bindings } = await current(executor, versions);\n return bindings.get(bindingKey(siteId, audience)) ?? [];\n },\n };\n};\n","import type { ContentAction } from './types.js';\n\n/**\n * Built-in app roles (package I). Their IDs are fixed (seeded by migration 20261001180000_create_app_users),\n * so the evaluator can apply them without a lookup: `public` to every anonymous caller, `authenticated` to\n * every signed-in app user on top of their custom roles.\n */\nexport const APP_ROLE_IDS = {\n public: '00000000-0000-4000-a000-000000000001',\n authenticated: '00000000-0000-4000-a000-000000000002',\n} as const;\n\nexport const APP_ROLE_KEYS = { public: 'public', authenticated: 'authenticated' } as const;\n\n/**\n * What app roles may grant: content actions only. Schema management and every instance-level action stay\n * with admin roles, whatever an app role row says.\n */\nexport const APP_CONTENT_ACTIONS = [\n 'read',\n 'create',\n 'update',\n 'delete',\n 'publish',\n] as const satisfies readonly ContentAction[];\nexport type AppContentAction = (typeof APP_CONTENT_ACTIONS)[number];\n","import {\n DENIED_POLICY,\n type ContentAction,\n type FieldMask,\n type GlobalAction,\n type KnownVersions,\n type PermissionExecutor,\n type Policy,\n type PolicyRequest,\n type RowCondition,\n type RowFilter,\n} from './types.js';\n\n/** One permission row of a role: an action on one model (or every model when `modelId` is null). */\nexport type Grant = {\n roleId: string;\n action: ContentAction | GlobalAction;\n /** null = every model, including ones created later. Always null for global actions. */\n modelId: string | null;\n condition: RowCondition['kind'] | null;\n /** null = every field the principal's audience may see; otherwise exactly these stable field IDs. */\n fieldIds: readonly string[] | null;\n};\n\n/**\n * Who the policy is for. Admin principals (admin users, admin-scope tokens) see every field their grants\n * allow. Delivery principals (delivery tokens, app users, anonymous callers) see only fields with\n * `public: true` unless a grant names a non-public field explicitly (build plan §3.4).\n */\nexport type Audience = 'admin' | 'delivery';\n\nexport type ModelField = { id: string; public: boolean };\n\n/**\n * The one port from permissions to the schema registry (package D implements it from the active schema).\n * `getModelFields`: field definitions for `public` semantics; `undefined` means the model does not exist,\n * which denies delivery access. `hasModel`: whether a model or component ID exists, so role grants can\n * only name real models.\n */\nexport type FieldVisibilityLookup = {\n getModelFields: (\n modelId: string,\n executor?: PermissionExecutor,\n versions?: KnownVersions,\n ) => Promise<readonly ModelField[] | undefined>;\n hasModel: (modelId: string, executor?: PermissionExecutor, versions?: KnownVersions) => Promise<boolean>;\n /**\n * The site a definition belongs to: null when it is shared (or unknown). Schema management of a site's\n * definition is granted by roles held on that site (plan site-schema, rule 4).\n */\n getModelSite: (\n modelId: string,\n executor?: PermissionExecutor,\n versions?: KnownVersions,\n ) => Promise<string | null>;\n};\n\n/** Used until the schema registry is wired in: no model is known, so delivery principals see nothing. */\nexport const NO_FIELD_VISIBILITY: FieldVisibilityLookup = {\n getModelFields: () => Promise.resolve(undefined),\n hasModel: () => Promise.resolve(false),\n getModelSite: () => Promise.resolve(null),\n};\n\nconst EMPTY_MASK: FieldMask = { mode: 'only', fieldIds: [] };\n\nexport const ALLOW_ALL_POLICY: Policy = Object.freeze<Policy>({\n allowed: true,\n rowFilter: null,\n readMask: { mode: 'all' },\n writeMask: { mode: 'all' },\n});\n\nconst WRITE_ACTIONS: ReadonlySet<ContentAction> = new Set(['create', 'update']);\n\nconst grantsFor = (grants: readonly Grant[], action: ContentAction, modelId: string) =>\n grants.filter((grant) => grant.action === action && (grant.modelId === null || grant.modelId === modelId));\n\n/** Rows: the union of what each grant allows. Any unconditional grant means no restriction. */\nexport const combineRowFilters = (grants: readonly Grant[]): RowFilter | null => {\n if (grants.some((grant) => grant.condition === null)) {\n return null;\n }\n const kinds = [...new Set(grants.map((grant) => grant.condition))].filter(\n (kind): kind is RowCondition['kind'] => kind !== null,\n );\n const filters: RowFilter[] = kinds.map((kind) => ({ kind }));\n if (filters.length === 1 && filters[0]) {\n return filters[0];\n }\n return { kind: 'or', filters };\n};\n\n/**\n * Fields: the union of what each grant allows. For delivery audiences a wildcard grant expands to the\n * model's public fields, so `public: false` fields stay hidden unless a grant names them. Delivery masks\n * are always explicit lists, so a field added after evaluation is never exposed by accident.\n */\nexport const combineFieldMasks = (\n grants: readonly Grant[],\n audience: Audience,\n fields: readonly ModelField[] | undefined,\n): FieldMask => {\n if (grants.length === 0) {\n return EMPTY_MASK;\n }\n const hasWildcard = grants.some((grant) => grant.fieldIds === null);\n if (audience === 'admin' && hasWildcard) {\n return { mode: 'all' };\n }\n const named = new Set(grants.flatMap((grant) => grant.fieldIds ?? []));\n if (audience === 'delivery') {\n const known = fields ?? [];\n const visible = known.filter((field) => named.has(field.id) || (hasWildcard && field.public));\n return { mode: 'only', fieldIds: visible.map((field) => field.id) };\n }\n return { mode: 'only', fieldIds: [...named].sort() };\n};\n\n/**\n * The policy for one (action, model) request from a principal's grants. Pure: no I/O, so it is unit- and\n * property-tested directly. Deny by default: no matching grant, no access.\n */\nexport const buildPolicy = (\n grants: readonly Grant[],\n request: PolicyRequest,\n audience: Audience,\n fields: readonly ModelField[] | undefined,\n): Policy => {\n if (audience === 'delivery' && fields === undefined) {\n // Unknown model: without its field definitions no delivery mask can be computed safely.\n return DENIED_POLICY;\n }\n const matching = grantsFor(grants, request.action, request.modelId);\n if (matching.length === 0) {\n return DENIED_POLICY;\n }\n const readGrants = request.action === 'read' ? matching : grantsFor(grants, 'read', request.modelId);\n return {\n allowed: true,\n rowFilter: combineRowFilters(matching),\n readMask: combineFieldMasks(readGrants, audience, fields),\n writeMask: WRITE_ACTIONS.has(request.action) ? combineFieldMasks(matching, audience, fields) : EMPTY_MASK,\n };\n};\n\n/** Global actions are granted only by a grant on that action with no model. */\nexport const allowsGlobalAction = (grants: readonly Grant[], action: GlobalAction): boolean =>\n grants.some((grant) => grant.action === action && grant.modelId === null);\n\n/**\n * A fixed lookup (model ID → fields), for tests and tools that have no schema registry. `hasModel` accepts\n * every ID unless `knownModelsOnly` is set, so tests can grant on models they never define.\n */\nexport const createStaticFieldVisibility = (\n models: Readonly<Record<string, readonly ModelField[]>>,\n {\n knownModelsOnly = false,\n modelSites = {},\n }: { knownModelsOnly?: boolean; modelSites?: Readonly<Record<string, string>> } = {},\n): FieldVisibilityLookup => ({\n getModelFields: (modelId) => Promise.resolve(Object.hasOwn(models, modelId) ? models[modelId] : undefined),\n hasModel: (modelId) => Promise.resolve(!knownModelsOnly || Object.hasOwn(models, modelId)),\n getModelSite: (modelId) =>\n Promise.resolve(Object.hasOwn(modelSites, modelId) ? (modelSites[modelId] ?? null) : null),\n});\n","import { APP_CONTENT_ACTIONS } from './appRoles.js';\nimport type { GrantSource } from './cache.js';\nimport {\n ALLOW_ALL_POLICY,\n allowsGlobalAction,\n buildPolicy,\n type Audience,\n type FieldVisibilityLookup,\n} from './policy.js';\nimport { tokenNetworkRoleIds } from './sites.js';\nimport {\n DENIED_POLICY,\n NETWORK_ACTIONS,\n NETWORK_CONTENT_ACTIONS,\n SITE_GRANTABLE_ACTIONS,\n type ContentAction,\n type GlobalAction,\n type KnownVersions,\n type PermissionEvaluator,\n type PermissionExecutor,\n type Principal,\n} from './types.js';\n\nconst NETWORK_ACTION_SET: ReadonlySet<GlobalAction> = new Set(NETWORK_ACTIONS);\n\ntype PrincipalRoles = {\n /** Roles that apply on the request's site (content and site actions). */\n roleIds: readonly string[];\n /** Roles that apply to the whole instance (network actions); a role held on one site is never here. */\n networkRoleIds: readonly string[];\n audience: Audience;\n /** Content actions the principal kind may ever perform; `all` = whatever its roles grant. */\n actions: ReadonlySet<ContentAction> | 'all';\n};\n\nconst READ_ONLY: ReadonlySet<ContentAction> = new Set(['read']);\nconst APP_ACTIONS: ReadonlySet<ContentAction> = new Set(APP_CONTENT_ACTIONS);\n\n/**\n * Which roles a principal holds and how its masks are computed. App users hold the custom app roles assigned\n * to them plus the roles their site binds to `authenticated`; anonymous callers hold the roles the request's\n * site binds to `public` (`site_app_roles`: a site that binds nothing grants nothing, and an anonymous caller\n * outside a site holds no role). Both are delivery audiences: they see `public: false` fields only where a\n * grant names them.\n */\nconst rolesOf = async (\n principal: Principal,\n grants: GrantSource,\n executor: PermissionExecutor | undefined,\n versions: KnownVersions | undefined,\n): Promise<PrincipalRoles | 'all'> => {\n switch (principal.kind) {\n case 'system':\n return 'all';\n case 'admin':\n return {\n roleIds: principal.roleIds,\n networkRoleIds: principal.networkRoleIds,\n audience: 'admin',\n actions: 'all',\n };\n case 'token':\n return principal.scope === 'admin'\n ? {\n roleIds: [principal.roleId],\n networkRoleIds: tokenNetworkRoleIds(principal),\n audience: 'admin',\n actions: 'all',\n }\n : // Delivery tokens only ever read, whatever their role says.\n { roleIds: [principal.roleId], networkRoleIds: [], audience: 'delivery', actions: READ_ONLY };\n case 'appUser':\n return {\n roleIds: [\n ...(await grants.getSiteAppRoleIds(principal.siteId, 'authenticated', executor, versions)),\n ...principal.roleIds,\n ],\n networkRoleIds: [],\n audience: 'delivery',\n actions: APP_ACTIONS,\n };\n case 'anonymous':\n return {\n roleIds:\n principal.siteId === null\n ? []\n : await grants.getSiteAppRoleIds(principal.siteId, 'public', executor, versions),\n networkRoleIds: [],\n audience: 'delivery',\n actions: APP_ACTIONS,\n };\n }\n};\n\ntype EvaluatorDependencies = { grants: GrantSource; fields: FieldVisibilityLookup };\n\n/**\n * Whether the principal acts on `siteId`: an admin narrowed to it, a site token of it, or a network token\n * (whose role applies on every site). Never an app user or anonymous caller (schema actions are admin-only).\n */\nconst actsOnSite = (principal: Principal, siteId: string): boolean => {\n switch (principal.kind) {\n case 'admin':\n return principal.siteId === siteId;\n case 'token':\n return principal.scope === 'admin' && (principal.siteId === null || principal.siteId === siteId);\n default:\n return false;\n }\n};\n\n/**\n * The one permission evaluator (ADR 0005): principal → roles → grants → Policy, for REST and GraphQL.\n * Deny by default at every step. `atVersions` gives the same evaluator for one request, whose caches are\n * checked against the versions the request read once instead of read again on every call.\n */\nexport const createPermissionEvaluator = ({ grants, fields }: EvaluatorDependencies): PermissionEvaluator => {\n const at = (versions: KnownVersions | undefined): PermissionEvaluator => ({\n evaluate: async (principal, request, executor) => {\n const roles = await rolesOf(principal, grants, executor, versions);\n if (roles === 'all') {\n return ALLOW_ALL_POLICY;\n }\n if (roles.actions !== 'all' && !roles.actions.has(request.action)) {\n return DENIED_POLICY;\n }\n // Schema management of a shared definition: network roles only (sites plan §H). Of a site's own\n // definition: the roles that apply on that site, when the principal acts there (plan site-schema).\n let roleIds = roles.roleIds;\n if (NETWORK_CONTENT_ACTIONS.has(request.action)) {\n const modelSite = await fields.getModelSite(request.modelId, executor, versions);\n roleIds =\n modelSite !== null && actsOnSite(principal, modelSite) ? roles.roleIds : roles.networkRoleIds;\n }\n const held = await grants.getGrants(roleIds, executor, versions);\n if (held.length === 0) {\n return DENIED_POLICY;\n }\n const modelFields =\n roles.audience === 'delivery'\n ? await fields.getModelFields(request.modelId, executor, versions)\n : undefined;\n return buildPolicy(held, request, roles.audience, modelFields);\n },\n canPerform: async (principal, action, executor) => {\n const roles = await rolesOf(principal, grants, executor, versions);\n if (roles === 'all') {\n return true;\n }\n if (roles.audience === 'delivery') {\n return false;\n }\n // Network actions only count roles assigned on every site, so a site role never reaches the network.\n const roleIds = NETWORK_ACTION_SET.has(action) ? roles.networkRoleIds : roles.roleIds;\n return allowsGlobalAction(await grants.getGrants(roleIds, executor, versions), action);\n },\n canPerformOnSite: async (principal, action, siteId, executor) => {\n const roles = await rolesOf(principal, grants, executor, versions);\n if (roles === 'all') {\n return true;\n }\n if (roles.audience === 'delivery') {\n return false;\n }\n const counted =\n !NETWORK_ACTION_SET.has(action) ||\n (SITE_GRANTABLE_ACTIONS.has(action) && actsOnSite(principal, siteId))\n ? roles.roleIds\n : roles.networkRoleIds;\n return allowsGlobalAction(await grants.getGrants(counted, executor, versions), action);\n },\n atVersions: at,\n });\n return at(undefined);\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport const findValue = async (key: string, trx: Executor = db): Promise<string | undefined> =>\n (await trx.selectFrom('system_settings').select('value').where('key', '=', key).executeTakeFirst())?.value;\n\n/** Inserts the setting unless it exists; returns true when this call created it. */\nexport const insertIfAbsent = async (key: string, value: string, trx: Executor = db): Promise<boolean> => {\n const inserted = await trx\n .insertInto('system_settings')\n .values({ key, value })\n .onConflict((oc) => oc.column('key').doNothing())\n .returning('key')\n .executeTakeFirst();\n return inserted !== undefined;\n};\n\n/** Creates or replaces a setting. */\nexport const upsertValue = async (key: string, value: string, trx: Executor = db): Promise<void> => {\n await trx\n .insertInto('system_settings')\n .values({ key, value })\n .onConflict((oc) => oc.column('key').doUpdateSet({ value, updated_at: new Date() }))\n .execute();\n};\n","import type { FieldVisibilityLookup, ModelField } from '../permissions/policy.js';\nimport type { KnownVersions, PermissionExecutor } from '../permissions/types.js';\nimport type { SchemaRegistry } from './registry.js';\n\n/**\n * The permission evaluator's view of field visibility (`public` flags), served from the active schema.\n * Uses the durable version check on every call (or the version the request read once, `versions`), so a\n * field made non-public is hidden on every instance from the next request, whether or not it got the\n * notification. Deprecated fields are reported as non-public: they are hidden from the API unless a role\n * names them explicitly. Components are not delivery models.\n */\nexport const createSchemaFieldVisibility = (registry: SchemaRegistry): FieldVisibilityLookup => {\n const activeModel = async (\n modelId: string,\n executor: PermissionExecutor | undefined,\n versions: KnownVersions | undefined,\n ) => {\n const active = (await registry.getSnapshot(executor, versions)).byId.get(modelId);\n return active && active.definition.kind !== 'component' ? active.definition : undefined;\n };\n return {\n getModelFields: async (modelId, executor, versions) =>\n (await activeModel(modelId, executor, versions))?.fields.map((field): ModelField => ({\n id: field.id,\n public: field.public && !field.deprecated,\n })),\n hasModel: async (modelId, executor, versions) =>\n (await activeModel(modelId, executor, versions)) !== undefined,\n // Components too: managing a site's component is a site's schema permission like its models'.\n getModelSite: async (modelId, executor, versions) =>\n (await registry.getSnapshot(executor, versions)).scopeOf(modelId) ?? null,\n };\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nconst COLUMNS = ['code', 'label', 'is_default', 'fallbacks', 'created_at', 'updated_at'] as const;\n\nexport const list = (executor: Executor = db) =>\n executor.selectFrom('locales').select(COLUMNS).orderBy('is_default', 'desc').orderBy('code').execute();\n\nexport type LocaleRow = Awaited<ReturnType<typeof list>>[number];\n\nexport const findByCode = (code: string, executor: Executor = db) =>\n executor.selectFrom('locales').select(COLUMNS).where('code', '=', code).executeTakeFirst();\n\nexport const insert = (locale: { code: string; label: string; fallbacks: string[] }, trx: Executor = db) =>\n trx\n .insertInto('locales')\n .values({ code: locale.code, label: locale.label, fallbacks: locale.fallbacks, is_default: false })\n .returning(COLUMNS)\n .executeTakeFirstOrThrow();\n\nexport const update = (\n code: string,\n changes: { label: string; fallbacks: string[]; now: Date },\n trx: Executor = db,\n) =>\n trx\n .updateTable('locales')\n .set({ label: changes.label, fallbacks: changes.fallbacks, updated_at: changes.now })\n .where('code', '=', code)\n .returning(COLUMNS)\n .executeTakeFirst();\n\n/** Makes `code` the only default locale. Clears the old default first (single-default unique index). */\nexport const setDefault = async (code: string, now: Date, trx: Executor = db) => {\n await trx\n .updateTable('locales')\n .set({ is_default: false, updated_at: now })\n .where('is_default', '=', true)\n .execute();\n await trx\n .updateTable('locales')\n .set({ is_default: true, updated_at: now })\n .where('code', '=', code)\n .execute();\n};\n\nexport const remove = (code: string, trx: Executor = db) =>\n trx.deleteFrom('locales').where('code', '=', code).executeTakeFirst();\n\n/** Removes `code` from every other locale's fallback chain. */\nexport const removeFromFallbacks = async (code: string, now: Date, trx: Executor = db) => {\n const rows = await trx.selectFrom('locales').select(['code', 'fallbacks']).execute();\n for (const row of rows.filter((candidate) => candidate.fallbacks.includes(code))) {\n await trx\n .updateTable('locales')\n .set({ fallbacks: row.fallbacks.filter((entry) => entry !== code), updated_at: now })\n .where('code', '=', row.code)\n .execute();\n }\n};\n","import {\n findDependentModels,\n isComponentDefinition,\n routeKeyOf,\n type LocaleDefinition,\n type SchemaDefinition,\n} from '@shapio/schema';\n\n/** One definition as it is active right now. */\nexport type ActiveDefinition = {\n definition: SchemaDefinition;\n /** The site the definition belongs to; null when it is shared by every site (plan site-schema). */\n siteId: string | null;\n /** Per-model version: the optimistic-concurrency token for edits. */\n version: number;\n revisionId: string;\n hash: string;\n activatedAt: Date;\n};\n\n/**\n * Two models whose route keys are equal. The validator refuses this on save, but a collection stored before\n * plural API IDs gets a derived plural on read, which can equal another model's API ID (`post` → `posts`).\n */\nexport type RouteKeyCollision = { routeKey: string; servedApiKey: string; hiddenApiKey: string };\n\n/**\n * What every schema exposes: lookups by stable ID, the locales, and scopes. Code that only resolves by ID\n * (content checks, validators, conversions) takes this, so it works on the full set and on a site's view.\n */\nexport type SchemaById = {\n version: number;\n definitions: readonly ActiveDefinition[];\n byId: ReadonlyMap<string, ActiveDefinition>;\n /** The site a definition belongs to: null when shared, undefined when no such definition is active. */\n scopeOf: (id: string) => string | null | undefined;\n locales: readonly LocaleDefinition[];\n defaultLocale: string;\n /** Models whose entries embed this component, directly or through other components. */\n dependentModelIds: (componentId: string) => string[];\n};\n\n/**\n * The whole active schema at one global version: every site's definitions and the shared ones. It has no\n * lookups by API ID on purpose: two sites may each have a `post`, so a key only means something in a view.\n * The registry loads and caches this; requests read a view of it (`forSite`).\n */\nexport type NetworkSchema = SchemaById & {\n /** A site's view: the shared definitions and that site's (memoised per site). */\n forSite: (siteId: string) => SchemaSnapshot;\n /** The shared definitions alone (network routes, the Network pages). */\n shared: () => SchemaSnapshot;\n};\n\n/**\n * One site's view of the schema at one global version (or the shared definitions alone, `siteId` null).\n * Requests pin one for their whole lifetime, so a model never changes shape halfway through a request.\n */\nexport type SchemaSnapshot = SchemaById & {\n /** The site this view is for; null for the shared definitions alone. */\n siteId: string | null;\n /** Collections and singletons by exact API key (the admin content API and GraphQL resolve here). */\n modelsByApiKey: ReadonlyMap<string, ActiveDefinition>;\n /**\n * Collections by plural API ID and singletons by API ID (`routeKeyOf`): the delivery and preview routes\n * resolve `/api/content/:modelKey` here.\n */\n modelsByRouteKey: ReadonlyMap<string, ActiveDefinition>;\n /** Route keys claimed twice; the registry logs them. Empty for any schema the validator accepted. */\n routeKeyCollisions: readonly RouteKeyCollision[];\n componentsByApiKey: ReadonlyMap<string, ActiveDefinition>;\n /** The full set at the same version (planning, validation across sites). */\n network: NetworkSchema;\n};\n\n/**\n * On a collision the model whose API ID is the route key keeps it (a singleton keeps the URL it always\n * had); otherwise the first definition does. Deterministic, so every instance serves the same model.\n */\nconst indexByRouteKey = (definitions: readonly ActiveDefinition[]) => {\n const modelsByRouteKey = new Map<string, ActiveDefinition>();\n const routeKeyCollisions: RouteKeyCollision[] = [];\n for (const active of definitions) {\n if (isComponentDefinition(active.definition)) {\n continue;\n }\n const routeKey = routeKeyOf(active.definition);\n const existing = modelsByRouteKey.get(routeKey);\n if (!existing) {\n modelsByRouteKey.set(routeKey, active);\n continue;\n }\n const takesOver = active.definition.apiKey === routeKey;\n const [served, hidden] = takesOver ? [active, existing] : [existing, active];\n modelsByRouteKey.set(routeKey, served);\n routeKeyCollisions.push({\n routeKey,\n servedApiKey: served.definition.apiKey,\n hiddenApiKey: hidden.definition.apiKey,\n });\n }\n return { modelsByRouteKey, routeKeyCollisions };\n};\n\nconst byIdPart = (\n version: number,\n definitions: readonly ActiveDefinition[],\n locales: readonly LocaleDefinition[],\n): SchemaById => {\n const byId = new Map(definitions.map((active) => [active.definition.id, active]));\n const all = definitions.map((active) => active.definition);\n const dependents = new Map<string, string[]>();\n return {\n version,\n definitions,\n byId,\n scopeOf: (id) => byId.get(id)?.siteId,\n locales,\n defaultLocale: locales.find((locale) => locale.isDefault)?.code ?? 'en',\n dependentModelIds: (componentId: string) => {\n let ids = dependents.get(componentId);\n if (!ids) {\n ids = findDependentModels(all, componentId).map((definition) => definition.id);\n dependents.set(componentId, ids);\n }\n return ids;\n },\n };\n};\n\nconst buildView = (\n network: NetworkSchema,\n siteId: string | null,\n definitions: readonly ActiveDefinition[],\n): SchemaSnapshot => {\n const modelsByApiKey = new Map<string, ActiveDefinition>();\n const componentsByApiKey = new Map<string, ActiveDefinition>();\n for (const active of definitions) {\n (isComponentDefinition(active.definition) ? componentsByApiKey : modelsByApiKey).set(\n active.definition.apiKey,\n active,\n );\n }\n const { modelsByRouteKey, routeKeyCollisions } = indexByRouteKey(definitions);\n return Object.freeze({\n ...byIdPart(network.version, definitions, network.locales),\n siteId,\n modelsByApiKey,\n modelsByRouteKey,\n routeKeyCollisions,\n componentsByApiKey,\n network,\n });\n};\n\n/** The full schema at one version; views are built on first use and kept for the snapshot's lifetime. */\nexport const buildNetworkSchema = (\n version: number,\n definitions: readonly ActiveDefinition[],\n locales: readonly LocaleDefinition[],\n): NetworkSchema => {\n const views = new Map<string | null, SchemaSnapshot>();\n const viewOf = (siteId: string | null): SchemaSnapshot => {\n let view = views.get(siteId);\n if (!view) {\n view = buildView(\n network,\n siteId,\n definitions.filter((active) => active.siteId === null || active.siteId === siteId),\n );\n views.set(siteId, view);\n }\n return view;\n };\n const network: NetworkSchema = Object.freeze({\n ...byIdPart(version, definitions, locales),\n forSite: (siteId: string) => viewOf(siteId),\n shared: () => viewOf(null),\n });\n return network;\n};\n\n/**\n * A view built straight from definitions (tests, generated references, proposed schemas): the view of\n * `siteId` (the shared definitions alone when null) over a full set made of `definitions`.\n */\nexport const buildSnapshot = (\n version: number,\n definitions: readonly ActiveDefinition[],\n locales: readonly LocaleDefinition[],\n siteId: string | null = null,\n): SchemaSnapshot => {\n const network = buildNetworkSchema(version, definitions, locales);\n return siteId === null ? network.shared() : network.forSite(siteId);\n};\n\n/** The sites whose views hold a definition of `definitions` (null: the shared definitions). */\nexport const siteIdsOf = (definitions: readonly ActiveDefinition[]): string[] => [\n ...new Set(definitions.flatMap((active) => (active.siteId === null ? [] : [active.siteId]))),\n];\n","import { hashDefinition, withDerivedPlural, type SchemaDefinition } from '@shapio/schema';\n\n/**\n * Reads a definition stored on a `schema_revisions` row. Revisions are immutable, so a collection saved\n * before plural API IDs existed has no `pluralApiKey`; it gets the value normalizing would fill\n * (`@shapio/schema` `withDerivedPlural`), so the snapshot, pull files and GraphQL agree before anyone saves.\n * Every read of a stored definition goes through here.\n */\nexport const readStoredDefinition = (stored: unknown): SchemaDefinition =>\n withDerivedPlural(stored as SchemaDefinition).definition;\n\nexport type StoredDefinition = { definition: SchemaDefinition; hash: string };\n\n/**\n * A stored definition with a hash that matches it: the stored hash, or, when a plural API ID was filled,\n * the hash of the filled definition (the hash a pull of it records, so pull → apply shows no change).\n */\nexport const readStoredRevision = async (stored: unknown, hash: string): Promise<StoredDefinition> => {\n const { definition, filled } = withDerivedPlural(stored as SchemaDefinition);\n return { definition, hash: filled ? await hashDefinition(definition) : hash };\n};\n","import type { LocaleDefinition } from '@shapio/schema';\nimport type { Kysely, Transaction } from 'kysely';\nimport type { Database } from '../db/index.js';\nimport type { DB } from '../db/types.js';\nimport * as localesRepository from '../repositories/locales.js';\nimport type { LocaleRow } from '../repositories/locales.js';\nimport * as schemaModelsRepository from '../repositories/schemaModels.js';\nimport * as schemaVersionsRepository from '../repositories/schemaVersions.js';\nimport { buildNetworkSchema, type ActiveDefinition, type NetworkSchema } from './snapshot.js';\nimport { readStoredRevision } from './storedDefinition.js';\n\nexport const toLocaleDefinition = (row: LocaleRow): LocaleDefinition => ({\n code: row.code,\n label: row.label,\n isDefault: row.is_default,\n fallbacks: row.fallbacks,\n});\n\nexport const toActiveDefinition = async (\n row: schemaModelsRepository.ActiveDefinitionRow,\n): Promise<ActiveDefinition> => ({\n ...(await readStoredRevision(row.definition, row.hash)),\n siteId: row.siteId,\n version: row.version,\n revisionId: row.revisionId,\n activatedAt: row.activatedAt,\n});\n\nexport const toActiveDefinitions = (\n rows: readonly schemaModelsRepository.ActiveDefinitionRow[],\n): Promise<ActiveDefinition[]> => Promise.all(rows.map(toActiveDefinition));\n\n/** The schema as the executor sees it (inside an activation transaction: the state right after the flip). */\nexport const readSnapshot = async (executor: Kysely<DB> | Transaction<DB>): Promise<NetworkSchema> => {\n const version = await schemaVersionsRepository.getSchemaVersion(executor);\n const rows = await schemaModelsRepository.findActiveDefinitions(executor);\n const locales = await localesRepository.list(executor);\n return buildNetworkSchema(version, await toActiveDefinitions(rows), locales.map(toLocaleDefinition));\n};\n\n/**\n * Reads the global version, every active definition and the locales in one REPEATABLE READ transaction,\n * so the snapshot is exactly the state at that version even if an activation commits meanwhile.\n */\nexport const loadSnapshot = (db: Database): Promise<NetworkSchema> =>\n db\n .transaction()\n .setIsolationLevel('repeatable read')\n .setAccessMode('read only')\n .execute((trx) => readSnapshot(trx));\n","import type { FastifyBaseLogger } from 'fastify';\nimport type { Database } from '../db/index.js';\nimport * as schemaVersionsRepository from '../repositories/schemaVersions.js';\nimport { loadSnapshot, readSnapshot } from './loadSnapshot.js';\nimport { siteIdsOf, type NetworkSchema } from './snapshot.js';\n\nexport type SchemaChangeListener = (snapshot: NetworkSchema) => void;\n\nexport type SchemaRegistry = {\n /**\n * The snapshot for the current global schema version. Costs one primary-key read of\n * `system_versions` when the cache is current; reloads (single-flight) when it is not. This durable\n * check is what keeps every instance correct; notifications only make reloads happen sooner.\n * `executor`: the caller's open transaction, which the check (and a reload, when the cache is stale) then\n * reads through, so it never waits for a second pooled connection while holding one.\n * `versions.schemaVersion`: the version the request already read (`repositories/requestState.ts`); the\n * check compares against it instead of reading it again, and still reloads when the cache is older.\n */\n getSnapshot: (executor?: Database, versions?: { schemaVersion?: number }) => Promise<NetworkSchema>;\n /** The cached snapshot without any check (may be stale or absent). */\n peek: () => NetworkSchema | undefined;\n /** A hint that the version moved (NOTIFY). Reloads in the background; never throws. */\n hint: (version?: number) => void;\n /** Called with each newer snapshot once loaded (GraphQL/OpenAPI regeneration, admin live refresh). */\n onChange: (listener: SchemaChangeListener) => () => void;\n /**\n * Ignores later hints and waits for a background reload still in flight. Call on shutdown before the\n * database is closed: a reload that is still acquiring a pooled connection when the pool ends never\n * returns it, and the pool's `end()` then waits forever.\n */\n close: () => Promise<void>;\n};\n\ntype RegistryOptions = { db: Database; log: FastifyBaseLogger };\n\nexport const createSchemaRegistry = ({ db, log }: RegistryOptions): SchemaRegistry => {\n let cached: NetworkSchema | undefined;\n let loading: Promise<NetworkSchema> | undefined;\n let closed = false;\n /** Reloads started by `hint`, which nobody else awaits. */\n const background = new Set<Promise<void>>();\n const listeners = new Set<SchemaChangeListener>();\n\n const publish = (snapshot: NetworkSchema) => {\n if (cached && cached.version >= snapshot.version) {\n return cached;\n }\n cached = snapshot;\n // Route keys only collide within a view (two sites may each serve `/posts`).\n const views = [snapshot.shared(), ...siteIdsOf(snapshot.definitions).map((id) => snapshot.forSite(id))];\n for (const view of views) {\n for (const collision of view.routeKeyCollisions) {\n log.error(\n { ...collision, siteId: view.siteId, schemaVersion: snapshot.version },\n 'two models share a delivery route key; set a plural API ID on the hidden one',\n );\n }\n }\n for (const listener of listeners) {\n try {\n listener(snapshot);\n } catch (error) {\n log.error({ err: error, schemaVersion: snapshot.version }, 'schema change listener failed');\n }\n }\n return snapshot;\n };\n\n /** Single-flight: concurrent callers share one load; a load that turns out too old is repeated. */\n const loadAtLeast = async (version: number): Promise<NetworkSchema> => {\n for (;;) {\n loading ??= loadSnapshot(db).finally(() => {\n loading = undefined;\n });\n const snapshot = publish(await loading);\n if (snapshot.version >= version) {\n return snapshot;\n }\n }\n };\n\n return {\n getSnapshot: async (executor, versions) => {\n const version =\n versions?.schemaVersion ?? (await schemaVersionsRepository.getSchemaVersion(executor ?? db));\n if (cached && cached.version >= version) {\n return cached;\n }\n // Through the caller's transaction: this caller's own read, not cached, since a transaction that is\n // not REPEATABLE READ can see an activation commit between its reads. The pool path refreshes the cache.\n return executor ? readSnapshot(executor) : loadAtLeast(version);\n },\n peek: () => cached,\n hint: (version) => {\n if (closed || (version !== undefined && cached && cached.version >= version)) {\n return;\n }\n const reload = loadAtLeast(version ?? 0).then(\n () => undefined,\n (error: unknown) => {\n log.warn({ err: error }, 'schema snapshot refresh failed; the next request will retry');\n },\n );\n background.add(reload);\n void reload.finally(() => background.delete(reload));\n },\n onChange: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n close: async () => {\n closed = true;\n // Each reload settles without throwing (failures are logged above).\n await Promise.all(background);\n },\n };\n};\n","import { Type, type Static } from 'typebox';\nimport { Value } from 'typebox/value';\nimport type { StorageConfig } from '../config/index.js';\nimport { DELIVERY_DESCRIPTOR_SETTING } from '../constants/delivery.js';\nimport type { Database } from '../db/index.js';\nimport * as schemaVersionsRepository from '../repositories/schemaVersions.js';\nimport * as systemSettingsRepository from '../repositories/systemSettings.js';\n\n/**\n * What an in-process delivery reader (plan next-in-process §2) needs from the server's configuration to answer\n * exactly as the server does, without its environment: where Shapio is served (absolute media URLs) and where\n * media lives. Never a secret: S3 credentials and the signing secret are the reader's own options.\n */\nconst DeliveryDescriptorSchema = Type.Object({\n format: Type.Literal(1),\n publicUrl: Type.String(),\n basePath: Type.String(),\n media: Type.Object({\n publicBaseUrl: Type.Union([Type.String(), Type.Null()]),\n s3: Type.Union([\n Type.Object({\n bucket: Type.String(),\n region: Type.Union([Type.String(), Type.Null()]),\n endpoint: Type.Union([Type.String(), Type.Null()]),\n forcePathStyle: Type.Boolean(),\n }),\n Type.Null(),\n ]),\n }),\n});\n\nexport type DeliveryDescriptor = Static<typeof DeliveryDescriptorSchema>;\n\ntype DescriptorSource = {\n server: { publicUrl: string; basePath: string };\n storage: Pick<StorageConfig, 'publicBaseUrl' | 's3'>;\n};\n\nexport const toDeliveryDescriptor = ({ server, storage }: DescriptorSource): DeliveryDescriptor => ({\n format: 1,\n publicUrl: server.publicUrl,\n basePath: server.basePath,\n media: {\n publicBaseUrl: storage.publicBaseUrl ?? null,\n s3: storage.s3\n ? {\n bucket: storage.s3.bucket,\n region: storage.s3.region ?? null,\n endpoint: storage.s3.endpoint ?? null,\n forcePathStyle: storage.s3.forcePathStyle,\n }\n : null,\n },\n});\n\nexport class InvalidDeliveryDescriptorError extends Error {\n constructor(options?: ErrorOptions) {\n super('The delivery descriptor stored by the Shapio server is not readable by this release', options);\n this.name = 'InvalidDeliveryDescriptorError';\n }\n}\n\n/** Parses the stored descriptor; throws when it is not in a format this release reads. */\nexport const parseDeliveryDescriptor = (stored: string): DeliveryDescriptor => {\n let value: unknown;\n try {\n value = JSON.parse(stored);\n } catch (error) {\n throw new InvalidDeliveryDescriptorError({ cause: error });\n }\n if (!Value.Check(DeliveryDescriptorSchema, value)) {\n throw new InvalidDeliveryDescriptorError();\n }\n return value;\n};\n\n/**\n * Called by the server at every start, after migrating: records its release and its delivery descriptor\n * (rewritten only when it changed), in one transaction.\n */\nexport const publishServerRelease = async (\n database: Database,\n release: string,\n source: DescriptorSource,\n): Promise<void> => {\n const descriptor = JSON.stringify(toDeliveryDescriptor(source));\n await database.transaction().execute(async (trx) => {\n await schemaVersionsRepository.setRelease(release, trx);\n if ((await systemSettingsRepository.findValue(DELIVERY_DESCRIPTOR_SETTING, trx)) !== descriptor) {\n await systemSettingsRepository.upsertValue(DELIVERY_DESCRIPTOR_SETTING, descriptor, trx);\n }\n });\n};\n","import type { FastifyBaseLogger } from 'fastify';\nimport type { Kysely } from 'kysely';\nimport type { DB } from '../db/types.js';\nimport { generateToken } from '../helpers/tokens.js';\nimport * as systemSettingsRepository from '../repositories/systemSettings.js';\n\nexport const SIGNING_SECRET_SETTING = 'signing_secret';\n\n/**\n * The instance's HMAC signing secret (preview tokens, webhook signatures). SESSION_SECRET always wins.\n * Without it, the secret is generated once and stored in `system_settings`, so it survives restarts and\n * every instance on the database agrees: concurrent first boots race on one insert and all re-read the\n * winner. The value is never logged.\n */\nexport const resolveSigningSecret = async (\n database: Kysely<DB>,\n configured: string | undefined,\n log: Pick<FastifyBaseLogger, 'info'>,\n): Promise<string> => {\n if (configured) {\n return configured;\n }\n const stored = await systemSettingsRepository.findValue(SIGNING_SECRET_SETTING, database);\n if (stored) {\n return stored;\n }\n const created = await systemSettingsRepository.insertIfAbsent(\n SIGNING_SECRET_SETTING,\n generateToken(),\n database,\n );\n const value = await systemSettingsRepository.findValue(SIGNING_SECRET_SETTING, database);\n if (!value) {\n throw new Error('The signing secret could not be stored');\n }\n if (created) {\n log.info('Generated signing secret; set SESSION_SECRET to pin it');\n }\n return value;\n};\n","import { resolve } from 'node:path';\nimport type { FastifyBaseLogger } from 'fastify';\nimport { pino, type Logger } from 'pino';\nimport type { StorageConfig } from '../config/index.js';\nimport { describeDatabaseTarget, dialectOfUrl } from '../db/dialect.js';\nimport { clearDb, createDb, hasDb, setDb, type Database } from '../db/index.js';\nimport { createUrlBuilder } from '../helpers/publicUrl.js';\nimport { createMediaStorage } from '../media/storage.js';\nimport { createPermissionCache } from '../permissions/cache.js';\nimport { createPermissionEvaluator } from '../permissions/evaluator.js';\nimport type { PermissionEvaluator, TokenPrincipal } from '../permissions/types.js';\nimport * as systemSettingsRepository from '../repositories/systemSettings.js';\nimport { createSchemaFieldVisibility } from '../schema/fieldVisibility.js';\nimport { createSchemaRegistry, type SchemaRegistry } from '../schema/registry.js';\nimport type { ContentServiceContext } from '../services/contentAccess.js';\nimport { parseDeliveryDescriptor, type DeliveryDescriptor } from '../services/deliveryDescriptor.js';\nimport { SIGNING_SECRET_SETTING } from '../services/signingSecret.js';\nimport { DeliveryRuntimeError } from './errors.js';\n\n/**\n * In-process delivery (plan next-in-process §2): the delivery API's reads as function calls in the reading\n * process (a Next.js server, an SSR site, a script), with the server's permissions, shapes and schema. Only\n * the database URL is required: the server records its release and a delivery descriptor (public URL, media\n * locations) at every start, and every call reads both with its request state.\n */\nexport type DeliveryRuntimeOptions = {\n /** The Shapio database: a `postgres://` or `mysql://` URL. SQLite is single-process and refused. */\n databaseUrl: string;\n /** Pooled connections (default 4). Serverless functions: 1 or 2. `next build` opens one pool per worker. */\n poolMax?: number;\n /** How long a read waits for a pooled connection before it fails with 503 (default 10 s, as the server). */\n acquireTimeoutMs?: number;\n /**\n * Signs URLs of private media stored on the server's disk. Default: the secret the server generated and\n * stored; pass the server's SESSION_SECRET when it sets one.\n */\n signingSecret?: string;\n /** Presigns URLs of private media on S3. Default: the AWS SDK's own credential chain (environment, profile). */\n s3Credentials?: { accessKeyId: string; secretAccessKey: string };\n /**\n * When the server's release differs from this one: `throw` (default) a `ShapioVersionSkewError`, or `http`:\n * read through the HTTP API at `fallbackUrl` until the releases match again.\n */\n onVersionSkew?: 'throw' | 'http';\n /** The Shapio server's URL (with BASE_PATH) for `onVersionSkew: 'http'`. */\n fallbackUrl?: string;\n /** Where failures are logged (pino). Default: warnings and errors to stdout. */\n logger?: Logger;\n};\n\n/** Media dependencies for one stored descriptor (rebuilt only when the server's descriptor changes). */\ntype MediaDependencies = NonNullable<ContentServiceContext['media']>;\n\nexport type DeliveryRuntime = {\n readonly databaseUrl: string;\n readonly db: Database;\n readonly registry: SchemaRegistry;\n /** The unscoped evaluator; each call scopes it to its principal and versions (`permissions/scoped.ts`). */\n readonly permissions: PermissionEvaluator;\n readonly log: Logger;\n readonly onVersionSkew: 'throw' | 'http';\n readonly fallbackUrl: string | undefined;\n /** Resolved delivery tokens by token hash; each call still checks the token is live. */\n readonly tokens: Map<string, TokenPrincipal>;\n /** Media storage and URLs for the stored descriptor text. */\n mediaFor: (descriptor: string) => Promise<MediaDependencies>;\n /**\n * Registers one more user of this shared runtime (a local client); the returned function releases it, and\n * the last release closes the runtime.\n */\n retain: () => () => Promise<void>;\n /** Closes the pool now, whoever else uses it. The runtime cannot be used afterwards; a new one may be created. */\n close: () => Promise<void>;\n};\n\n/** One runtime per database per process, kept on `globalThis` so a re-evaluated module (dev reloads, a\n * bundler's second copy) never opens a second pool. */\nconst RUNTIMES = Symbol.for('shapio.delivery');\n\nconst runtimes = (): Map<string, DeliveryRuntime> => {\n const holder = globalThis as typeof globalThis & { [RUNTIMES]?: Map<string, DeliveryRuntime> };\n holder[RUNTIMES] ??= new Map();\n return holder[RUNTIMES];\n};\n\nconst DEFAULT_POOL_MAX = 4;\n\n/** Local media is never read from disk here; the adapter only builds URLs. Any file access fails loudly. */\nconst NO_LOCAL_MEDIA_ROOT = resolve('/', '.shapio-in-process-has-no-media');\n\nconst storageConfigOf = (\n descriptor: DeliveryDescriptor,\n credentials: DeliveryRuntimeOptions['s3Credentials'],\n): StorageConfig => {\n const { s3 } = descriptor.media;\n return {\n driver: 'local',\n mediaPath: NO_LOCAL_MEDIA_ROOT,\n s3: s3\n ? {\n bucket: s3.bucket,\n region: s3.region ?? undefined,\n endpoint: s3.endpoint ?? undefined,\n forcePathStyle: s3.forcePathStyle,\n accessKeyId: credentials?.accessKeyId,\n secretAccessKey: credentials?.secretAccessKey,\n }\n : undefined,\n publicBaseUrl: descriptor.media.publicBaseUrl ?? undefined,\n maxUploadBytes: 0,\n allowedTypes: [],\n };\n};\n\nconst assertSupported = (options: DeliveryRuntimeOptions) => {\n const dialect = dialectOfUrl(options.databaseUrl);\n if (dialect === 'sqlite') {\n throw new DeliveryRuntimeError(\n 'In-process delivery needs PostgreSQL or MySQL: a SQLite database is served by one process only. ' +\n \"Read over HTTP with @shapio/client's createClient instead\",\n );\n }\n if (dialect === undefined) {\n throw new DeliveryRuntimeError('databaseUrl must be a postgres:// or mysql:// URL');\n }\n if (options.onVersionSkew === 'http' && !options.fallbackUrl) {\n throw new DeliveryRuntimeError(\"onVersionSkew: 'http' needs fallbackUrl, the Shapio server's URL\");\n }\n};\n\nconst openRuntime = (options: DeliveryRuntimeOptions): DeliveryRuntime => {\n const log = options.logger ?? pino({ level: 'warn' });\n const db = createDb({\n connectionString: options.databaseUrl,\n poolMax: options.poolMax ?? DEFAULT_POOL_MAX,\n ...(options.acquireTimeoutMs !== undefined ? { acquireTimeoutMs: options.acquireTimeoutMs } : {}),\n applicationName: 'shapio-in-process',\n onIdleConnectionError: (error) =>\n log.warn({ err: error }, 'database connection lost; it will be replaced on the next query'),\n });\n // Repositories default to the process handle. A Shapio server in this process keeps its own (same\n // database); otherwise this pool becomes it.\n const ownsProcessHandle = !hasDb();\n if (ownsProcessHandle) {\n setDb(db);\n }\n const registryLog = log.child({ component: 'schema-registry' }) as unknown as FastifyBaseLogger;\n const registry = createSchemaRegistry({ db, log: registryLog });\n const permissions = createPermissionEvaluator({\n grants: createPermissionCache(db),\n fields: createSchemaFieldVisibility(registry),\n });\n\n let media: { descriptor: string; dependencies: Promise<MediaDependencies> } | undefined;\n let users = 0;\n let closing: Promise<void> | undefined;\n const buildMedia = async (stored: string): Promise<MediaDependencies> => {\n const descriptor = parseDeliveryDescriptor(stored);\n const urls = createUrlBuilder(descriptor);\n const signingSecret =\n options.signingSecret ?? (await systemSettingsRepository.findValue(SIGNING_SECRET_SETTING, db));\n const storage = await createMediaStorage(storageConfigOf(descriptor, options.s3Credentials), {\n urls,\n ...(signingSecret !== undefined ? { signingSecret } : {}),\n });\n return { storage, urls };\n };\n\n const runtime: DeliveryRuntime = {\n databaseUrl: options.databaseUrl,\n db,\n registry,\n permissions,\n log,\n onVersionSkew: options.onVersionSkew ?? 'throw',\n fallbackUrl: options.fallbackUrl,\n tokens: new Map(),\n mediaFor: (descriptor) => {\n if (media?.descriptor !== descriptor) {\n const dependencies = buildMedia(descriptor);\n media = { descriptor, dependencies };\n // A failed build is retried by the next call instead of being cached.\n dependencies.catch(() => {\n if (media?.dependencies === dependencies) {\n media = undefined;\n }\n });\n }\n return media.dependencies;\n },\n retain: () => {\n users += 1;\n let released = false;\n return async () => {\n if (released) {\n return;\n }\n released = true;\n users -= 1;\n if (users === 0) {\n await runtime.close();\n }\n };\n },\n close: () => {\n closing ??= (async () => {\n if (runtimes().get(options.databaseUrl) === runtime) {\n runtimes().delete(options.databaseUrl);\n }\n await registry.close();\n await db.destroy();\n if (ownsProcessHandle) {\n clearDb(db);\n }\n })();\n return closing;\n },\n };\n return runtime;\n};\n\n/**\n * The in-process delivery runtime for a database: created once per process and shared (the same URL returns\n * the same runtime; the first caller's options apply). A process reads one Shapio database: another URL is\n * refused. Nothing is read until the first call, so creating it never fails on a database that is down.\n */\nexport const createDeliveryRuntime = (options: DeliveryRuntimeOptions): DeliveryRuntime => {\n assertSupported(options);\n const all = runtimes();\n const existing = all.get(options.databaseUrl);\n if (existing) {\n return existing;\n }\n const [other] = all.keys();\n if (other !== undefined) {\n throw new DeliveryRuntimeError(\n `This process already reads ${describeDatabaseTarget(other)} in process; one Shapio database per process`,\n );\n }\n const created = openRuntime(options);\n all.set(options.databaseUrl, created);\n return created;\n};\n","import { createDeliveryClient, type ShapioDeliveryClient } from '@shapio/client';\nimport * as delivery from '@shapio/cms/delivery';\nimport { assertNodeRuntime } from './nodeRuntime.js';\nimport type {\n DeliveryRuntimeError as DeliveryRuntimeErrorInstance,\n LocalClientOptions,\n ShapioVersionSkewError as ShapioVersionSkewErrorInstance,\n} from './types.js';\n\nexport type { LocalClientOptions } from './types.js';\n\n/** The Shapio release this package reads; the server must run the same one. */\nexport const SHAPIO_VERSION: string = delivery.SHAPIO_VERSION;\n\n/** Thrown (or 503 `VERSION_SKEW` caught as `ShapioApiError`) when the server runs another release. */\nexport const ShapioVersionSkewError: {\n new (serverRelease: string | null, libraryRelease: string): ShapioVersionSkewErrorInstance;\n readonly prototype: ShapioVersionSkewErrorInstance;\n} = delivery.ShapioVersionSkewError;\nexport type ShapioVersionSkewError = ShapioVersionSkewErrorInstance;\n\n/** Thrown by `createLocalClient` for options it cannot serve (SQLite, an unknown URL, a second database). */\nexport const DeliveryRuntimeError: {\n new (message: string): DeliveryRuntimeErrorInstance;\n readonly prototype: DeliveryRuntimeErrorInstance;\n} = delivery.DeliveryRuntimeError;\nexport type DeliveryRuntimeError = DeliveryRuntimeErrorInstance;\n\nexport type LocalClient = ShapioDeliveryClient & {\n /**\n * Stops this client. The database pool is shared by every local client of the process and closes when the\n * last one is closed. Long-running servers never need to call it; scripts and tests do.\n */\n close: () => Promise<void>;\n};\n\n/**\n * Shapio's delivery API as function calls in this process: the `delivery`, `site` and `snapshots` groups of\n * `@shapio/client`, with the same paths, permissions, response shapes and `ShapioApiError`s, answered from the\n * database without an HTTP hop. Reads as `token` (a delivery API token) or, without one, as an anonymous\n * caller (the site's `public` role). Model changes made in the admin show on the next call.\n *\n * Needs PostgreSQL or MySQL, the Node.js runtime, and the same Shapio release as the server (`SHAPIO_VERSION`);\n * see `onVersionSkew`. Nothing is read until the first call.\n */\nexport const createLocalClient = ({ token, site, ...runtimeOptions }: LocalClientOptions): LocalClient => {\n assertNodeRuntime();\n const runtime = delivery.createDeliveryRuntime(runtimeOptions);\n const release = runtime.retain();\n const request = delivery.createInProcessRequest(runtime, {\n ...(token !== undefined ? { token } : {}),\n ...(site !== undefined ? { site } : {}),\n });\n return { ...createDeliveryClient(request, site !== undefined ? { site } : {}), close: release };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["SHAPIO_VERSION","packageJson.version","ShapioVersionSkewError","DeliveryRuntimeError","EMPTY_MASK","DATA","fieldJson","fieldText","fieldValue","castParameter","DATA","ARRAY_TYPES","fieldText","fieldValue","castParameter","castParameter","isRecord","NUMERIC_STRING_TYPES","sourceOf","mediaVariantsRepository.listForAssets","isRecord","contentQueriesRepository.runHeadQuery","mediaAssetsRepository.findLiveManyOnSite","isTree","siteNotFound","sitesRepository.findSeoDefaults","mediaAssetsRepository.findLiveManyOnSite","contentQueriesRepository.runHeadPage","contentQueriesRepository.withConsistentRead","contentQueriesRepository.runHeadPageMeta","contentRevisionsRepository.findDataByIds","contentQueriesRepository.withConsistentRead","publicationsRepository.currentSeq","snapshotDiffRepository.listChanges","snapshotDiffRepository.schemaVersionsAt","snapshotDiffRepository.snapshotCreatedAt","schemaVersionsRepository.getSchemaVersion","PERMISSION_COLUMNS","listAllPermissions","apiTokensRepository.findLiveByHash","apiTokensRepository.touchLastUsed","sitesRepository.findById","sitesRepository.findByKey","sitesRepository.findPrimary","ShapioVersionSkewError","SHAPIO_VERSION","requestStateRepository.readForDelivery","ShapioVersionSkewError","adminRolesRepository.listAllPermissions","appRolesRepository.listAllPermissions","siteAppRolesRepository.listAll","permissionsVersionRepository.getPermissionsVersion","schemaVersionsRepository.getSchemaVersion","schemaModelsRepository.findActiveDefinitions","localesRepository.list","schemaVersionsRepository.getSchemaVersion","DeliveryRuntimeError","systemSettingsRepository.findValue","delivery.SHAPIO_VERSION","delivery.ShapioVersionSkewError","delivery.DeliveryRuntimeError","delivery.createDeliveryRuntime","delivery.createInProcessRequest"],"sources":["../../../apps/api/package.json","../../../apps/api/src/constants/version.ts","../../../apps/api/src/helpers/errorResponse.ts","../../../apps/api/src/delivery/errors.ts","../../../apps/api/src/constants/sites.ts","../../../apps/api/src/content/model.ts","../../../apps/api/src/helpers/siteQuery.ts","../../../apps/api/src/constants/auth.ts","../../../apps/api/src/permissions/types.ts","../../../apps/api/src/routes/schemas/adminIdentity.ts","../../../apps/api/src/routes/schemas/error.ts","../../../apps/api/src/routes/snapshots/schemas.ts","../../../apps/api/src/content/compiler/types.ts","../../../apps/api/src/content/compiler/paginate.ts","../../../apps/api/src/content/errors.ts","../../../apps/api/src/usage/keys.ts","../../../apps/api/src/content/validator/scalars.ts","../../../apps/api/src/content/validator/index.ts","../../../apps/api/src/db/dialect.ts","../../../apps/api/src/db/mysql/fieldIndexColumns.ts","../../../apps/api/src/content/compiler/heads.ts","../../../apps/api/src/content/compiler/dialect/mysql.ts","../../../apps/api/src/content/compiler/dialect/postgres.ts","../../../apps/api/src/content/compiler/dialect/sqlite.ts","../../../apps/api/src/content/compiler/dialect/index.ts","../../../apps/api/src/content/compiler/currentDialect.ts","../../../apps/api/src/content/compiler/policy.ts","../../../apps/api/src/content/compiler/select.ts","../../../apps/api/src/usage/restFieldReads.ts","../../../apps/api/src/content/compiler/expressions.ts","../../../apps/api/src/content/compiler/operators.ts","../../../apps/api/src/content/compiler/compile.ts","../../../apps/api/src/content/compiler/conditions.ts","../../../apps/api/src/content/compiler/sort.ts","../../../apps/api/src/content/locales.ts","../../../apps/api/src/db/index.ts","../../../apps/api/src/repositories/contentQueries.ts","../../../apps/api/src/repositories/mediaAssets.ts","../../../apps/api/src/constants/media.ts","../../../apps/api/src/media/urls.ts","../../../apps/api/src/repositories/mediaVariants.ts","../../../apps/api/src/services/mediaViews.ts","../../../apps/api/src/content/compiler/populate.ts","../../../apps/api/src/content/media.ts","../../../apps/api/src/content/relations.ts","../../../apps/api/src/content/seo.ts","../../../apps/api/src/content/read.ts","../../../apps/api/src/content/compiler/querystring.ts","../../../apps/api/src/content/compiler/parse.ts","../../../apps/api/src/repositories/contentRevisions.ts","../../../apps/api/src/db/sql/values.ts","../../../apps/api/src/db/sql/json.ts","../../../apps/api/src/services/contentReads.ts","../../../apps/api/src/repositories/sites.ts","../../../apps/api/src/services/siteSeo.ts","../../../apps/api/src/services/contentDeliveryReads.ts","../../../apps/api/src/services/draftAccess.ts","../../../apps/api/src/services/contentDelivery.ts","../../../apps/api/src/repositories/publications.ts","../../../apps/api/src/repositories/schemaVersions.ts","../../../apps/api/src/db/snapshotDiffQuery.ts","../../../apps/api/src/repositories/snapshotDiff.ts","../../../apps/api/src/services/snapshotChanges.ts","../../../apps/api/src/helpers/tokens.ts","../../../apps/api/src/permissions/memo.ts","../../../apps/api/src/permissions/scoped.ts","../../../apps/api/src/permissions/sites.ts","../../../apps/api/src/constants/delivery.ts","../../../apps/api/src/repositories/requestState.ts","../../../apps/api/src/repositories/adminRoles.ts","../../../apps/api/src/repositories/permissionsVersion.ts","../../../apps/api/src/permissions/seedRoles.ts","../../../apps/api/src/repositories/apiTokens.ts","../../../apps/api/src/services/apiTokens.ts","../../../apps/api/src/content/hooks.ts","../../../apps/api/src/services/contentContext.ts","../../../apps/api/src/repositories/schemaModels.ts","../../../apps/api/src/schema/planner/locks.ts","../../../apps/api/src/services/sites.ts","../../../apps/api/src/delivery/scope.ts","../../../apps/api/src/delivery/request.ts","../../../apps/api/src/helpers/publicUrl.ts","../../../apps/api/src/media/signing.ts","../../../apps/api/src/media/localAdapter.ts","../../../apps/api/src/media/storage.ts","../../../apps/api/src/repositories/appRoles.ts","../../../apps/api/src/repositories/siteAppRoles.ts","../../../apps/api/src/permissions/cache.ts","../../../apps/api/src/permissions/appRoles.ts","../../../apps/api/src/permissions/policy.ts","../../../apps/api/src/permissions/evaluator.ts","../../../apps/api/src/repositories/systemSettings.ts","../../../apps/api/src/schema/fieldVisibility.ts","../../../apps/api/src/repositories/locales.ts","../../../apps/api/src/schema/snapshot.ts","../../../apps/api/src/schema/storedDefinition.ts","../../../apps/api/src/schema/loadSnapshot.ts","../../../apps/api/src/schema/registry.ts","../../../apps/api/src/services/deliveryDescriptor.ts","../../../apps/api/src/services/signingSecret.ts","../../../apps/api/src/delivery/runtime.ts","../src/index.ts"],"sourcesContent":["","import packageJson from '../../package.json' with { type: 'json' };\n\nexport const SHAPIO_VERSION: string = packageJson.version;\n","import type { ErrorResponse } from '../routes/schemas/error.js';\nimport { AppError } from './appError.js';\n\n/**\n * What the error response reads from an error that is not an `AppError`: the parts Fastify's errors carry\n * (status, code, request-validation issues). Structural, so callers outside Fastify (the in-process delivery\n * runtime) share this mapping without importing it.\n */\nexport type ErrorLike = Error & {\n code?: string;\n statusCode?: number;\n validation?: ReadonlyArray<{ instancePath: string; message?: string; params: unknown }>;\n};\n\nconst STATUS_CODES: Readonly<Record<number, string>> = {\n 400: 'BAD_REQUEST',\n 401: 'UNAUTHORIZED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_ALLOWED',\n 406: 'NOT_ACCEPTABLE',\n 409: 'CONFLICT',\n 413: 'PAYLOAD_TOO_LARGE',\n 415: 'UNSUPPORTED_MEDIA_TYPE',\n 422: 'UNPROCESSABLE_ENTITY',\n 429: 'RATE_LIMITED',\n 503: 'SERVICE_UNAVAILABLE',\n};\n\n/** Errors raised by @fastify/csrf-protection (plugins/csrf.ts). */\nconst CSRF_ERROR_CODES: ReadonlySet<string> = new Set(['FST_CSRF_INVALID_TOKEN', 'FST_CSRF_MISSING_SECRET']);\n\nexport type ErrorResult = { statusCode: number; body: ErrorResponse };\n\n/**\n * The one error shape every endpoint returns, `{ error: { code, message, details? } }`, and its status.\n * Server errors (5xx) never carry their internal text; the caller logs the original error.\n */\nexport const toErrorResponse = (error: ErrorLike | AppError): ErrorResult => {\n if (error instanceof AppError) {\n const body: ErrorResponse = { error: { code: error.code, message: error.message } };\n if (error.details !== undefined) {\n body.error.details = error.details;\n }\n return { statusCode: error.statusCode, body };\n }\n if (error.code !== undefined && CSRF_ERROR_CODES.has(error.code)) {\n // Its own code, so clients know to fetch a fresh token (GET /api/admin/auth/csrf) and retry.\n return {\n statusCode: 403,\n body: { error: { code: 'CSRF_INVALID', message: 'Missing or invalid CSRF token' } },\n };\n }\n if (error.validation) {\n return {\n statusCode: 400,\n body: {\n error: {\n code: 'VALIDATION_ERROR',\n message: error.message,\n details: error.validation.map(({ instancePath, message, params }) => ({\n instancePath,\n message,\n params,\n })),\n },\n },\n };\n }\n const statusCode = error.statusCode !== undefined && error.statusCode >= 400 ? error.statusCode : 500;\n if (statusCode >= 500) {\n // Never echo internal error text to clients.\n return { statusCode, body: { error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } } };\n }\n return {\n statusCode,\n body: { error: { code: STATUS_CODES[statusCode] ?? 'ERROR', message: error.message } },\n };\n};\n","import { ShapioApiError } from '@shapio/client';\nimport type { Logger } from 'pino';\nimport { toErrorResponse, type ErrorLike } from '../helpers/errorResponse.js';\n\n/**\n * The server's release and this library's differ, or the database predates in-process delivery: reading\n * across versions could run SQL against a schema it was not written for (plan next-in-process §2). Callers\n * that catch `ShapioApiError` see a 503 `VERSION_SKEW`.\n */\nexport class ShapioVersionSkewError extends ShapioApiError {\n readonly serverRelease: string | null;\n readonly libraryRelease: string;\n\n constructor(serverRelease: string | null, libraryRelease: string, options?: ErrorOptions) {\n super(503, {\n error: {\n code: 'VERSION_SKEW',\n message:\n serverRelease === null\n ? `The Shapio database has not been served by Shapio ${libraryRelease} yet; start the Shapio server ` +\n `of this release (or install the release the server runs) before reading in process`\n : `The Shapio server runs ${serverRelease} and this in-process reader is ${libraryRelease}; ` +\n 'install the same release on both',\n },\n });\n this.name = 'ShapioVersionSkewError';\n if (options?.cause !== undefined) {\n this.cause = options.cause;\n }\n this.serverRelease = serverRelease;\n this.libraryRelease = libraryRelease;\n }\n}\n\n/** Thrown when the runtime cannot be created for this database (SQLite, an unknown URL, a second database). */\nexport class DeliveryRuntimeError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'DeliveryRuntimeError';\n }\n}\n\n/** The body and status the HTTP API would have answered, thrown as the client throws it. */\nexport const toShapioApiError = (error: unknown, log: Pick<Logger, 'error'>): ShapioApiError => {\n if (error instanceof ShapioApiError) {\n return error;\n }\n const failure = error instanceof Error ? (error as ErrorLike) : new Error(String(error));\n const { statusCode, body } = toErrorResponse(failure);\n if (statusCode >= 500) {\n log.error({ err: error }, 'in-process delivery read failed');\n }\n return new ShapioApiError(statusCode, body);\n};\n","/** Sites (plan agentic-ecosystem §H, ADR 0011). */\n\n/** The primary site the sites migration created; every row that existed before sites belongs to it. */\nexport const PRIMARY_SITE_ID = '00000000-0000-4000-b000-000000000001';\n\n/** A site key: lower case, starts with a letter (the same rule as role keys). Used in URLs and `?site=`. */\nexport const SITE_KEY_PATTERN = '^[a-z][a-z0-9-]{0,62}$';\n\n/** Admin API requests name their site with this header (the admin sends it on every request). */\nexport const SITE_HEADER = 'shapio-site';\n\n/** Delivery requests may name their site with this query parameter (it must match a site token's site). */\nexport const SITE_QUERY_PARAMETER = 'site';\n\n/**\n * `Vary` of cacheable delivery responses (REST and GraphQL GETs): the credentials that select the principal,\n * and the header that selects the site. CDNs that key on the URL only need `?site=` instead of the header.\n */\nexport const DELIVERY_VARY = 'Authorization, Cookie, Shapio-Site';\n\n/** `Cache-Control` of delivery responses carrying drafts (REST and GraphQL; plan drafts-mode): never stored. */\nexport const DRAFTS_CACHE_CONTROL = 'private, no-store';\n","import {\n embeddedComponentIds,\n isComponentDefinition,\n type ComponentDefinition,\n type FieldDefinition,\n type ModelDefinition,\n} from '@shapio/schema';\nimport { AppError } from '../helpers/appError.js';\nimport type { ActiveDefinition, SchemaById, SchemaSnapshot } from '../schema/snapshot.js';\n\n/** Head states (ADR 0001): every entry has a draft per locale and, once published, a published head. */\nexport const HEAD_STATES = ['draft', 'published'] as const;\nexport type HeadState = (typeof HEAD_STATES)[number];\n\n/** Unique-registry locale for shared (non-localized) fields: one value across every locale. */\nexport const SHARED_LOCALE = '*';\n\n/** A collection or singleton resolved from a pinned snapshot, with what writes need to know about it. */\nexport type ContentModel = {\n definition: ModelDefinition;\n /** Per-model version at the pinned snapshot (re-checked at commit). */\n version: number;\n revisionId: string;\n /** Components reachable from the model (directly or through other components), with their versions. */\n components: ReadonlyMap<string, { definition: ComponentDefinition; version: number }>;\n};\n\nexport const modelNotFound = (key: string) =>\n new AppError(404, 'MODEL_NOT_FOUND', `No content model \"${key}\"`, { modelKey: key });\n\nconst collectComponents = (snapshot: SchemaById, model: ModelDefinition): ContentModel['components'] => {\n const found = new Map<string, { definition: ComponentDefinition; version: number }>();\n const queue = embeddedComponentIds(model);\n while (queue.length > 0) {\n const id = queue.shift() as string;\n const active = snapshot.byId.get(id);\n if (!active || found.has(id) || !isComponentDefinition(active.definition)) {\n continue;\n }\n found.set(id, { definition: active.definition, version: active.version });\n queue.push(...embeddedComponentIds(active.definition));\n }\n return found;\n};\n\nconst toContentModel = (\n snapshot: SchemaById,\n active: ActiveDefinition & { definition: ModelDefinition },\n): ContentModel => ({\n definition: active.definition,\n version: active.version,\n revisionId: active.revisionId,\n components: collectComponents(snapshot, active.definition),\n});\n\nconst isModel = (\n active: ActiveDefinition | undefined,\n): active is ActiveDefinition & { definition: ModelDefinition } =>\n active !== undefined && !isComponentDefinition(active.definition);\n\n/**\n * Resolves `/api/…/content/:modelKey` through the registry at request time (CONTRIBUTING.md rule 2). Only a\n * site's view resolves API IDs: another site's model is not in it, so it reads as not found.\n */\nexport const resolveModel = (snapshot: SchemaSnapshot, modelKey: string): ContentModel => {\n const active = snapshot.modelsByApiKey.get(modelKey);\n if (!isModel(active)) {\n throw modelNotFound(modelKey);\n }\n return toContentModel(snapshot, active);\n};\n\n/**\n * Resolves the delivery and preview routes' `:modelKey`, the route key (`routeKeyOf`): the plural API ID\n * of a collection, the API ID of a singleton. A collection's singular API ID is not a route key (404).\n */\nexport const resolveRouteModel = (snapshot: SchemaSnapshot, routeKey: string): ContentModel => {\n const active = snapshot.modelsByRouteKey.get(routeKey);\n if (!active || isComponentDefinition(active.definition)) {\n throw modelNotFound(routeKey);\n }\n return resolveModel(snapshot, active.definition.apiKey);\n};\n\n/** The API ID behind a route key: REST entry points translate with it, then call the services. */\nexport const apiKeyOfRoute = (snapshot: SchemaSnapshot, routeKey: string): string =>\n resolveRouteModel(snapshot, routeKey).definition.apiKey;\n\n/** By stable ID: works on a view and on the full set (jobs and conversions that already know the site). */\nexport const resolveModelById = (snapshot: SchemaById, modelId: string): ContentModel | undefined => {\n const active = snapshot.byId.get(modelId);\n return isModel(active) ? toContentModel(snapshot, active) : undefined;\n};\n\n/** Whether a field's value differs per locale. Fields of a non-localized model are always shared. */\nexport const isLocalizedField = (model: ModelDefinition, field: FieldDefinition): boolean =>\n model.localized && field.localized;\n\n/** Fields the API exposes (deprecated fields keep their stored values but are hidden). */\nexport const liveFields = (fields: readonly FieldDefinition[]): FieldDefinition[] =>\n fields.filter((field) => !field.deprecated);\n\nexport const findFieldByApiKey = (\n fields: readonly FieldDefinition[],\n apiKey: string,\n): FieldDefinition | undefined => fields.find((field) => field.apiKey === apiKey && !field.deprecated);\n","import { SITE_QUERY_PARAMETER } from '../constants/sites.js';\n\nconst isSiteSegment = (segment: string): boolean => {\n const key = segment.split('=', 1)[0] ?? '';\n try {\n return decodeURIComponent(key.replace(/\\+/g, ' ')) === SITE_QUERY_PARAMETER;\n } catch {\n return false;\n }\n};\n\n/**\n * A raw querystring (without `?`) without its `site` parameter: site resolution consumes it, and the content\n * compiler refuses unknown keys. The rest is left exactly as sent (bracket syntax is the compiler's to parse).\n */\nexport const withoutSiteParameter = (rawQuery: string): string =>\n rawQuery\n .split('&')\n .filter((segment) => !isSiteSegment(segment))\n .join('&');\n","/** Admin authentication settings (ADR 0005). Not configurable by env on purpose: safe defaults for everyone. */\n\nexport const SESSION_COOKIE_NAME = 'shapio_session';\n\n/** A session dies after this long without a request. */\nexport const SESSION_IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1000;\n/** A session dies this long after login, however active it is. */\nexport const SESSION_ABSOLUTE_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000;\n/** `last_seen_at` / `last_used_at` are written at most this often, so reads stay reads. */\nexport const LAST_SEEN_WRITE_INTERVAL_MS = 60 * 1000;\n\nexport const CSRF_HEADER = 'x-csrf-token';\n\nexport const INVITATION_TTL_MS = 7 * 24 * 60 * 60 * 1000;\nexport const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000;\n\nexport const PASSWORD_MIN_LENGTH = 12;\n/** argon2 accepts more, but unbounded input is a denial-of-service vector. */\nexport const PASSWORD_MAX_LENGTH = 256;\n\n/** API tokens carry a recognisable prefix (secret scanners, and telling them apart from app-user JWTs). */\nexport const API_TOKEN_PREFIX = 'shp_';\n/** Characters of the token kept in plain text for display (`shp_` + 6). */\nexport const API_TOKEN_DISPLAY_LENGTH = 10;\n\n/** Rate limits for unauthenticated credential endpoints. Per IP via @fastify/rate-limit route config. */\nexport const CREDENTIAL_RATE_LIMIT_PER_IP = { max: 20, timeWindow: 60 * 1000 } as const;\n/** Per email address and client IP (logins), or per email address (reset, registration, confirmation mail). */\nexport const CREDENTIAL_RATE_LIMIT_PER_EMAIL = { max: 5, timeWindow: 15 * 60 * 1000 } as const;\n","import type { Database } from '../db/index.js';\n\n/**\n * Principals and policies (ADR 0005). One evaluator serves admin users, app users, tokens and anonymous\n * callers, for REST and GraphQL alike. Package A ships the types and a deny-all evaluator; package B\n * implements roles and the real evaluator against this interface.\n */\n\n/** One role held by an admin user: on one site, or on every site (`siteId` null). */\nexport type RoleAssignment = { roleId: string; siteId: string | null };\n\n/**\n * An admin user as the session knows them: every role assignment, on any site. Never evaluated directly;\n * `narrowToSite` (permissions/sites.ts) turns it into the `AdminPrincipal` of one request's site.\n */\nexport type AdminIdentity = {\n adminUserId: string;\n sessionId: string;\n assignments: readonly RoleAssignment[];\n};\n\n/**\n * An admin user narrowed to one site (sites plan §H). `roleIds`: roles assigned on this site or on every\n * site; site actions and content actions are evaluated against them. `networkRoleIds`: roles assigned on\n * every site only; network actions (users, roles, schema, audit, sites) are evaluated against them, so a\n * site-only role can never reach the network. `siteId` null: a network route, where `roleIds` equals\n * `networkRoleIds`.\n */\nexport type AdminPrincipal = {\n kind: 'admin';\n adminUserId: string;\n sessionId: string;\n /** Every assignment, on any site (which sites the admin works on). */\n assignments: readonly RoleAssignment[];\n siteId: string | null;\n roleIds: readonly string[];\n networkRoleIds: readonly string[];\n};\n\n/**\n * An app user: an end user of one site (sites plan §H). `roleIds`: the custom app roles assigned to the\n * account; the roles bound to the site's `authenticated` audience (`site_app_roles`) apply on top.\n * `siteId`: the account's site, from the access token's `site` claim; the account acts on no other site.\n */\nexport type AppUserPrincipal = {\n kind: 'appUser';\n appUserId: string;\n siteId: string;\n roleIds: readonly string[];\n};\n\n/** API tokens bind to exactly one role: an admin role or a delivery role. */\nexport type TokenPrincipal = {\n kind: 'token';\n tokenId: string;\n scope: 'admin' | 'delivery';\n roleId: string;\n /**\n * The token's site; null for a network admin token (its role applies on every site and to network\n * actions). Delivery tokens always have a site.\n */\n siteId: string | null;\n};\n\n/**\n * A caller without credentials. On a site route `siteId` is the request's site, and the roles bound to the\n * site's `public` audience (`site_app_roles`) apply. Null before site resolution and on network routes,\n * where an anonymous caller holds no role at all.\n */\nexport type AnonymousPrincipal = { kind: 'anonymous'; siteId: string | null };\n\n/** Work Shapio does on its own behalf: scheduled jobs, migrations, the worker. Never derived from a request. */\nexport type SystemPrincipal = { kind: 'system'; component: string };\n\nexport type Principal =\n AdminPrincipal | AppUserPrincipal | TokenPrincipal | AnonymousPrincipal | SystemPrincipal;\n\nexport const CONTENT_ACTIONS = ['read', 'create', 'update', 'delete', 'publish', 'schemaManage'] as const;\nexport type ContentAction = (typeof CONTENT_ACTIONS)[number];\n\n/**\n * What a delivery role may grant (plan drafts-mode §1): `read`, and `readDrafts`, which lets a delivery token\n * read draft heads (`?publicationState=draft`, GraphQL `publicationState: DRAFT`) on top of `read`. A\n * `readDrafts` grant always covers every model (no model, condition or field list), so one check per request\n * covers relation targets too. Admin and app roles never hold it; admin principals read drafts without it.\n */\nexport const DELIVERY_ACTIONS = ['read', 'readDrafts'] as const;\nexport type DeliveryAction = (typeof DELIVERY_ACTIONS)[number];\nexport const READ_DRAFTS_ACTION = 'readDrafts' satisfies DeliveryAction;\n\n/** Any action evaluated per model: content actions, and `readDrafts`. */\nexport type ModelAction = ContentAction | DeliveryAction;\n\n/**\n * Content actions about the schema rather than one site's entries: network roles only for a shared\n * definition; for a site's own definition, the roles that apply on that site (when the principal acts there).\n */\nexport const NETWORK_CONTENT_ACTIONS: ReadonlySet<ModelAction> = new Set<ModelAction>(['schemaManage']);\n\n/**\n * Actions that are not scoped to one model, in two kinds (sites plan §H):\n * - Network actions are about the whole instance (creating models, managing admin users, roles and sites,\n * reading the audit log). Only roles assigned on every site grant them: a role held on one site never does,\n * so a site admin cannot grant themselves other sites.\n * - Site actions are about one site's library, tokens and publishing; any role held on the site grants them.\n */\nexport const NETWORK_ACTIONS = [\n 'schema.create',\n 'users.manage',\n 'roles.manage',\n 'audit.read',\n 'sites.manage',\n] as const;\nexport type NetworkAction = (typeof NETWORK_ACTIONS)[number];\n\n/**\n * Network actions a role held on one site also grants for that site alone (plan site-schema, rule 4):\n * `schema.create` creates that site's own definitions. They are checked with `canPerformOnSite` and the\n * request's site; `canPerform` keeps the every-site meaning (shared definitions, locales, the schema lock).\n */\nexport const SITE_GRANTABLE_ACTION_LIST = ['schema.create'] as const satisfies readonly NetworkAction[];\nexport type SiteGrantableAction = (typeof SITE_GRANTABLE_ACTION_LIST)[number];\nexport const SITE_GRANTABLE_ACTIONS: ReadonlySet<GlobalAction> = new Set<GlobalAction>(\n SITE_GRANTABLE_ACTION_LIST,\n);\n\nexport const SITE_ACTIONS = [\n 'tokens.manage',\n // Media library (package G): browse; upload, edit and organise; delete and change visibility.\n 'media.read',\n 'media.write',\n 'media.manage',\n // Publishing (package H): the jobs view and scheduling overview; webhooks; deployment connections;\n // triggering and retrying deployment runs; change sets (schema + content shipped as one snapshot) and\n // shipping them (`changes.ship`, split so an agent's role can prepare change sets but not make them live).\n 'publishing.manage',\n 'webhooks.manage',\n 'deployments.manage',\n 'deployments.trigger',\n 'changes.manage',\n 'changes.ship',\n // The site's own settings (plan seo-fields): its SEO defaults.\n 'site.settings',\n] as const;\nexport type SiteAction = (typeof SITE_ACTIONS)[number];\n\nexport const GLOBAL_ACTIONS = [...NETWORK_ACTIONS, ...SITE_ACTIONS] as const;\nexport type GlobalAction = (typeof GLOBAL_ACTIONS)[number];\n\n/** Field visibility by stable field ID. `all` means every field the schema exposes to this principal kind. */\nexport type FieldMask = { mode: 'all' } | { mode: 'only'; fieldIds: readonly string[] };\n\n/** Enumerated row conditions; compiled to SQL only by content/compiler. Never free-form expressions. */\nexport type RowCondition = { kind: 'ownedByPrincipal' };\n\nexport type RowFilter =\n | RowCondition\n | { kind: 'and'; filters: readonly RowFilter[] }\n | { kind: 'or'; filters: readonly RowFilter[] };\n\nexport type Policy = {\n allowed: boolean;\n /** Extra restriction on which rows the principal may act on. `null` means no restriction. */\n rowFilter: RowFilter | null;\n readMask: FieldMask;\n writeMask: FieldMask;\n};\n\nexport type PolicyRequest = {\n action: ModelAction;\n modelId: string;\n};\n\n/**\n * Where a permission lookup reads its durable version checks (and any reload): the open transaction when\n * the caller is inside one, so the lookup never waits for a second pooled connection while it holds one\n * (an exhausted pool would then wait forever). Omitted: the pool.\n */\nexport type PermissionExecutor = Database;\n\n/**\n * Durable versions a request already read (`repositories/requestState.ts`, once per request, before any\n * transaction). A lookup given one compares its cache against it instead of reading it again, and still\n * reloads when its cache is older; a change made after the read applies from the next request. Omitted: the\n * lookup reads the version itself (jobs, the worker, the CLI).\n */\nexport type KnownVersions = { schemaVersion?: number; permissionsVersion?: number };\n\nexport type PermissionEvaluator = {\n evaluate: (principal: Principal, request: PolicyRequest, executor?: PermissionExecutor) => Promise<Policy>;\n canPerform: (principal: Principal, action: GlobalAction, executor?: PermissionExecutor) => Promise<boolean>;\n /**\n * `canPerform` for the request's site: an action in `SITE_GRANTABLE_ACTIONS` also counts the roles that\n * apply on `siteId` when the principal acts on that site (an admin narrowed to it, a site token of it, a\n * network token). Any other action is `canPerform`.\n */\n canPerformOnSite: (\n principal: Principal,\n action: GlobalAction,\n siteId: string,\n executor?: PermissionExecutor,\n ) => Promise<boolean>;\n /**\n * The same evaluator, checking its caches against versions the request already read instead of reading\n * them on every call. Absent on evaluators without durable caches (fixed test evaluators, wrappers).\n */\n atVersions?: (versions: KnownVersions) => PermissionEvaluator;\n};\n\nconst EMPTY_MASK: FieldMask = { mode: 'only', fieldIds: [] };\n\nexport const DENIED_POLICY: Policy = Object.freeze({\n allowed: false,\n rowFilter: null,\n readMask: EMPTY_MASK,\n writeMask: EMPTY_MASK,\n});\n","import { Type, type Static } from 'typebox';\nimport { PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from '../../constants/auth.js';\nimport {\n CONTENT_ACTIONS,\n GLOBAL_ACTIONS,\n NETWORK_ACTIONS,\n READ_DRAFTS_ACTION,\n SITE_ACTIONS,\n SITE_GRANTABLE_ACTION_LIST,\n} from '../../permissions/types.js';\n\n/** Schemas shared by the admin identity routes (setup, auth, users, roles, tokens). */\n\n/** A timestamp in a response: a Date in handlers, ISO-8601 on the wire (fast-json-stringify formats it). */\nexport const DateTimeSchema = Type.Unsafe<Date>({ type: 'string', format: 'date-time' });\nexport const NullableDateTimeSchema = Type.Union([DateTimeSchema, Type.Null()]);\n/** A timestamp in a request: an ISO-8601 string. */\nexport const DateTimeInputSchema = Type.String({ format: 'date-time' });\nexport const UuidSchema = Type.String({ format: 'uuid' });\n\nexport const IdParamsSchema = Type.Object({ id: UuidSchema });\nexport type IdParams = Static<typeof IdParamsSchema>;\n\nexport const EmailSchema = Type.String({ format: 'email', minLength: 3, maxLength: 254 });\nexport const PasswordSchema = Type.String({ minLength: PASSWORD_MIN_LENGTH, maxLength: PASSWORD_MAX_LENGTH });\n/** A password being checked (login, current password): any length policy applies only to new ones. */\nexport const PasswordAttemptSchema = Type.String({ minLength: 1, maxLength: PASSWORD_MAX_LENGTH });\nexport const PersonNameSchema = Type.String({ minLength: 1, maxLength: 200 });\n/** Single-use tokens from links and logs: 43 base64url characters. */\nexport const OneTimeTokenSchema = Type.String({ minLength: 16, maxLength: 128, pattern: '^[A-Za-z0-9_-]+$' });\n\n/** A role held on one site, or on every site (`siteId` null). */\nexport const RoleAssignmentSchema = Type.Object(\n { roleId: UuidSchema, siteId: Type.Union([UuidSchema, Type.Null()]) },\n { additionalProperties: false },\n);\n\nexport const AdminUserSchema = Type.Object({\n id: UuidSchema,\n email: Type.String(),\n name: Type.String(),\n status: Type.Union([Type.Literal('active'), Type.Literal('disabled')]),\n /** Where each role applies. */\n assignments: Type.Array(RoleAssignmentSchema),\n /** Deprecated: the distinct roles of `assignments`, on any site. */\n roleIds: Type.Array(UuidSchema),\n lastLoginAt: NullableDateTimeSchema,\n createdAt: DateTimeSchema,\n updatedAt: DateTimeSchema,\n});\n\n/** Returned whenever a session starts: the user, and the CSRF token for its mutations. */\nexport const SessionStartedSchema = Type.Object({ user: AdminUserSchema, csrfToken: Type.String() });\n\nexport const ContentActionSchema = Type.Enum(CONTENT_ACTIONS);\nexport const GlobalActionSchema = Type.Enum(GLOBAL_ACTIONS);\nexport const NetworkActionSchema = Type.Enum(NETWORK_ACTIONS);\nexport const SiteActionSchema = Type.Enum(SITE_ACTIONS);\n/** What `me.sitePermissions` lists: site actions, and network actions a role on the site grants there. */\nexport const SitePermissionSchema = Type.Enum([...SITE_ACTIONS, ...SITE_GRANTABLE_ACTION_LIST]);\n\nexport const PermissionSchema = Type.Object(\n {\n action: Type.Enum([...CONTENT_ACTIONS, READ_DRAFTS_ACTION, ...GLOBAL_ACTIONS]),\n modelId: Type.Union([Type.String({ minLength: 1, maxLength: 100 }), Type.Null()]),\n condition: Type.Union([Type.Literal('ownedByPrincipal'), Type.Null()]),\n fieldIds: Type.Union([\n Type.Array(Type.String({ minLength: 1, maxLength: 100 }), { maxItems: 500 }),\n Type.Null(),\n ]),\n },\n { additionalProperties: false },\n);\n","import { Type, type Static } from 'typebox';\n\n/** The one error shape every endpoint returns (central error handler). */\nexport const ErrorResponseSchema = Type.Object({\n error: Type.Object({\n code: Type.String(),\n message: Type.String(),\n details: Type.Optional(Type.Unknown()),\n }),\n});\n\nexport type ErrorResponse = Static<typeof ErrorResponseSchema>;\n","import { Type, type Static } from 'typebox';\nimport { NullableDateTimeSchema, UuidSchema } from '../schemas/adminIdentity.js';\nimport { ErrorResponseSchema } from '../schemas/error.js';\n\nconst SeqSchema = Type.Integer({ minimum: 0 });\nconst NullableInteger = Type.Union([Type.Integer(), Type.Null()]);\nconst ERRORS = { 400: ErrorResponseSchema, 401: ErrorResponseSchema };\n\nexport const SnapshotChangesQuerySchema = Type.Object(\n {\n from: SeqSchema,\n /** Defaults to the current snapshot. */\n to: Type.Optional(SeqSchema),\n /** `nextCursor` of the previous page. */\n after: Type.Optional(UuidSchema),\n limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 500 })),\n },\n { additionalProperties: false },\n);\nexport type SnapshotChangesQuery = Static<typeof SnapshotChangesQuerySchema>;\n\nexport const SnapshotChangeSchema = Type.Object({\n id: UuidSchema,\n modelId: UuidSchema,\n modelKey: Type.String(),\n routeKey: Type.String(),\n /** From the revision live at `to` (at `from` when unpublished); null without a readable title. */\n title: Type.Union([Type.String(), Type.Null()]),\n /** The cover image's media asset ID, when the model has a readable cover. */\n coverMediaId: Type.Union([UuidSchema, Type.Null()]),\n locales: Type.Array(\n Type.Object({\n locale: Type.String(),\n change: Type.Enum(['published', 'updated', 'unpublished']),\n /** The revision live at `to`, or at `from` for `unpublished`. */\n revisionId: Type.Union([UuidSchema, Type.Null()]),\n }),\n ),\n});\n\nexport const snapshotChangesSchema = {\n querystring: SnapshotChangesQuerySchema,\n response: {\n 200: Type.Object({\n from: Type.Integer(),\n to: Type.Integer(),\n schemaVersions: Type.Object({ from: NullableInteger, to: NullableInteger }),\n items: Type.Array(SnapshotChangeSchema),\n nextCursor: Type.Union([UuidSchema, Type.Null()]),\n }),\n ...ERRORS,\n },\n};\n\nexport const currentSnapshotSchema = {\n response: {\n 200: Type.Object({\n snapshot: Type.Integer(),\n schemaVersion: Type.Integer(),\n publishedAt: NullableDateTimeSchema,\n }),\n ...ERRORS,\n },\n};\n","import type { FieldDefinition } from '@shapio/schema';\nimport { AppError } from '../../helpers/appError.js';\n\n/** The parsed, allowlisted form of a content query. Nothing in it is raw request text used as SQL. */\n\nexport const FILTER_OPERATORS = [\n '$eq',\n '$ne',\n '$in',\n '$nin',\n '$lt',\n '$lte',\n '$gt',\n '$gte',\n '$contains',\n '$notContains',\n '$containsi',\n '$startsWith',\n '$endsWith',\n '$null',\n '$notNull',\n] as const;\nexport type FilterOperator = (typeof FILTER_OPERATORS)[number];\n\n/** System attributes every entry has. They are not stored in `data`. */\nexport const SYSTEM_ATTRIBUTES = ['id', 'createdAt', 'updatedAt'] as const;\nexport type SystemAttribute = (typeof SYSTEM_ATTRIBUTES)[number];\n\nexport type FilterTarget =\n { kind: 'field'; field: FieldDefinition } | { kind: 'system'; name: SystemAttribute };\n\nexport type FilterNode =\n | { kind: 'and'; nodes: FilterNode[] }\n | { kind: 'or'; nodes: FilterNode[] }\n | { kind: 'not'; node: FilterNode }\n | { kind: 'condition'; target: FilterTarget; operator: FilterOperator; value: unknown };\n\nexport type SortTerm = { target: FilterTarget; direction: 'asc' | 'desc' };\n\n/** Relation fields to expand, with what to expand inside each target. */\nexport type PopulateTree = ReadonlyMap<string, PopulateTree>;\n\nexport type ContentQuery = {\n filter: FilterNode | null;\n /** Free-text search over the model's title field. */\n search: { field: FieldDefinition; text: string } | null;\n sort: SortTerm[];\n page: number;\n pageSize: number;\n /** Top-level fields to return; null = every readable field. */\n fields: FieldDefinition[] | null;\n /** Keyed by relation field ID. */\n populate: PopulateTree;\n locale: string | undefined;\n snapshot: number | undefined;\n /** Delivery and preview only: how rich-text values are returned (`?richText=`); undefined on admin reads. */\n richText?: RichTextMode;\n /** Delivery and preview only: SEO fields as stored (`raw`, the default) or with the site's defaults (`?seo=`). */\n seo?: SeoMode;\n /** Delivery only: `?publicationState=draft` reads draft heads (if the caller may; plan drafts-mode). */\n publicationState?: PublicationState;\n /** Admin list only: entries whose draft in the served locale has this status. */\n status?: EntryListStatus;\n /** Admin list only: entries created by this admin user. */\n author?: string;\n};\n\n/**\n * Delivery shapes of a rich-text value (ADR 0003): `json` the stored document `{ format, version, doc }`,\n * `html` `{ format, version, html }` (sanitized HTML rendered from the document), `both` the document plus\n * `html`.\n */\nexport const RICH_TEXT_MODES = ['json', 'html', 'both'] as const;\nexport type RichTextMode = (typeof RICH_TEXT_MODES)[number];\n\n/** What a delivery read returns for rich text when the request does not say (`?richText=`). */\nexport const DEFAULT_RICH_TEXT_MODE: RichTextMode = 'json';\n\n/** `?seo=`: SEO fields as stored, or merged with the site's SEO defaults (plan seo-fields). */\n/** Which heads a delivery read serves (`?publicationState=`, GraphQL's `publicationState`). */\nexport const PUBLICATION_STATES = ['published', 'draft'] as const;\nexport type PublicationState = (typeof PUBLICATION_STATES)[number];\n\nexport const SEO_MODES = ['raw', 'resolved'] as const;\nexport type SeoMode = (typeof SEO_MODES)[number];\n\nexport const ENTRY_LIST_STATUSES = ['draft', 'published', 'modified'] as const;\nexport type EntryListStatus = (typeof ENTRY_LIST_STATUSES)[number];\n\nexport const QUERY_LIMITS = {\n maxPageSize: 100,\n defaultPageSize: 25,\n maxConditions: 50,\n maxFilterDepth: 6,\n maxInValues: 100,\n maxValueLength: 1000,\n maxParameters: 200,\n maxPopulateDepth: 3,\n maxSortTerms: 5,\n} as const;\n\n/** A malformed or disallowed query: 400 for unknown/invalid, 403 for fields the caller may not use. */\nexport const queryInvalid = (message: string, details?: Record<string, unknown>) =>\n new AppError(400, 'INVALID_QUERY', message, details);\n\nexport const queryForbidden = (message: string, details?: Record<string, unknown>) =>\n new AppError(403, 'FORBIDDEN_FIELD', message, details);\n","import { QUERY_LIMITS } from './types.js';\n\n/** Offset pagination with a hard page-size cap (brief §4: \"enforce pagination\"). */\nexport type Pagination = { page: number; pageSize: number; total: number; pageCount: number };\n\nexport const toLimitOffset = (page: number, pageSize: number) => {\n const size = Math.min(Math.max(1, pageSize), QUERY_LIMITS.maxPageSize);\n return { limit: size, offset: (Math.max(1, page) - 1) * size };\n};\n\nexport const paginationMeta = (page: number, pageSize: number, total: number): Pagination => ({\n page,\n pageSize,\n total,\n pageCount: Math.ceil(total / pageSize),\n});\n","import { AppError } from '../helpers/appError.js';\n\n/** Errors the content services throw; the central error handler renders them. */\n\nexport const entryNotFound = (id: string) => new AppError(404, 'ENTRY_NOT_FOUND', `No entry ${id}`, { id });\n\nexport const entryLocaleNotFound = (id: string, locale: string, locales: readonly string[]) =>\n new AppError(404, 'ENTRY_LOCALE_NOT_FOUND', `Entry ${id} has no \"${locale}\" version yet`, {\n id,\n locale,\n locales,\n });\n\nexport const contentVersionConflict = (expectedVersion: number | null, currentVersion: number | null) =>\n new AppError(\n 409,\n 'CONTENT_VERSION_CONFLICT',\n 'The entry changed since you loaded it. Reload it, reapply your edit and save again.',\n { expectedVersion, currentVersion },\n );\n\n/** The model (or a component it embeds) was activated in a new version after this request pinned its schema. */\nexport const schemaChanged = () =>\n new AppError(\n 409,\n 'SCHEMA_CHANGED',\n 'The content model changed while saving. Reload the form and save again.',\n );\n\nexport const unknownLocale = (locale: string) =>\n new AppError(422, 'LOCALE_NOT_FOUND', `Locale \"${locale}\" does not exist`, { locale });\n\nexport const singletonExists = (modelKey: string) =>\n new AppError(409, 'SINGLETON_EXISTS', `\"${modelKey}\" is a single type and already has its entry`, {\n modelKey,\n });\n\nexport const entryReferenced = (referrers: readonly unknown[]) =>\n new AppError(\n 409,\n 'ENTRY_REFERENCED',\n 'Other entries still point at this entry. Remove those references first.',\n {\n referrers,\n },\n );\n\nexport const publishingDisabled = (modelKey: string) =>\n new AppError(422, 'PUBLISHING_DISABLED', `\"${modelKey}\" has no draft state: every save is published`, {\n modelKey,\n });\n\nexport const revisionNotFound = (id: string) =>\n new AppError(404, 'REVISION_NOT_FOUND', `No revision ${id}`, { id });\n\nexport const snapshotInvalid = (requested: number, current: number) =>\n new AppError(400, 'SNAPSHOT_INVALID', `Snapshot ${requested} does not exist yet (latest is ${current})`, {\n requested,\n current,\n });\n","import type { Principal } from '../permissions/types.js';\n\n/**\n * How delivery reads are attributed (plan developer-face §5). Delivery tokens are consumers, one key per\n * token; app users are counted together, and so are anonymous callers. Admin reads (admin users and\n * admin-scope API tokens), system work and previews are not consumers and are never counted.\n */\nexport const APP_USERS_PRINCIPAL_KEY = 'app_users';\nexport const ANONYMOUS_PRINCIPAL_KEY = 'anonymous';\n\n/** `token:<id>`, `app_users`, `anonymous`, or null for principals whose reads are not counted. */\nexport const usagePrincipalKey = (principal: Principal): string | null => {\n switch (principal.kind) {\n case 'token':\n return principal.scope === 'delivery' ? `token:${principal.tokenId}` : null;\n case 'appUser':\n return APP_USERS_PRINCIPAL_KEY;\n case 'anonymous':\n return ANONYMOUS_PRINCIPAL_KEY;\n default:\n return null;\n }\n};\n\n/** The UTC day a read is bucketed in, `YYYY-MM-DD`. */\nexport const usageDayOf = (at: Date): string => at.toISOString().slice(0, 10);\n\n/** A populated relation's target field: `<relation field ID>.<target field ID>`. */\nexport const relationFieldPath = (relationFieldId: string, targetFieldId: string): string =>\n `${relationFieldId}.${targetFieldId}`;\n","import {\n BIGINTEGER_PATTERN,\n DATE_PATTERN,\n DECIMAL_PATTERN,\n isStableId,\n SLUG_PATTERN,\n TIME_PATTERN,\n UID_PATTERN,\n type FieldDefinition,\n} from '@shapio/schema';\nimport type { IssueCode } from './issues.js';\n\n/**\n * Value rules for scalar data types (ADR 0001 storage formats). Each returns the canonical stored value or\n * a problem. Canonical forms are what make text sorting and containment equality correct:\n * - datetime: UTC with milliseconds, `YYYY-MM-DDTHH:mm:ss.sssZ` (offsets are converted to UTC);\n * - time: `HH:mm:ss.sss`;\n * - decimal and biginteger: strings (numbers are accepted when they convert exactly).\n */\nexport type ScalarOutcome = { ok: true; value: unknown } | { ok: false; code: IssueCode; message: string };\n\nconst ok = (value: unknown): ScalarOutcome => ({ ok: true, value });\nconst problem = (code: IssueCode, message: string): ScalarOutcome => ({ ok: false, code, message });\n\nconst INPUT_DATETIME =\n /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(\\.\\d{1,9})?)?(Z|[+-]\\d{2}:\\d{2})$/;\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst MAX_EMAIL_LENGTH = 254;\nconst DEFAULT_URL_PROTOCOLS = ['http', 'https', 'mailto', 'tel'];\n\nconst isRealDate = (text: string) => {\n if (!DATE_PATTERN.test(text)) {\n return false;\n }\n const parsed = new Date(`${text}T00:00:00.000Z`);\n return !Number.isNaN(parsed.getTime()) && parsed.toISOString().startsWith(text);\n};\n\nexport const canonicalDate = (value: unknown): string | undefined =>\n typeof value === 'string' && isRealDate(value) ? value : undefined;\n\nexport const canonicalDatetime = (value: unknown): string | undefined => {\n if (typeof value !== 'string') {\n return undefined;\n }\n const match = INPUT_DATETIME.exec(value);\n if (!match || !isRealDate(`${match[1]}-${match[2]}-${match[3]}`)) {\n return undefined;\n }\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();\n};\n\nexport const canonicalTime = (value: unknown): string | undefined => {\n if (typeof value !== 'string' || !TIME_PATTERN.test(value)) {\n return undefined;\n }\n const [hours, minutes, rest = '00'] = value.split(':') as [string, string, string?];\n const [seconds = '00', millis = '000'] = rest.split('.');\n return `${hours}:${minutes}:${seconds}.${millis.padEnd(3, '0')}`;\n};\n\n/** Decimal strings compared exactly (BigInt on a common scale). */\nexport const compareDecimal = (a: string, b: string): number => {\n const scale = (text: string) => (text.split('.')[1] ?? '').length;\n const digits = Math.max(scale(a), scale(b));\n const toBig = (text: string) => {\n const negative = text.startsWith('-');\n const [whole = '0', fraction = ''] = text.replace('-', '').split('.');\n const big = BigInt(`${whole}${fraction.padEnd(digits, '0')}`);\n return negative ? -big : big;\n };\n const difference = toBig(a) - toBig(b);\n return difference === 0n ? 0 : difference > 0n ? 1 : -1;\n};\n\nexport const canonicalDecimal = (value: unknown): string | undefined => {\n if (typeof value === 'number' && Number.isFinite(value)) {\n const text = String(value);\n return DECIMAL_PATTERN.test(text) ? text : undefined;\n }\n return typeof value === 'string' && DECIMAL_PATTERN.test(value) ? value : undefined;\n};\n\nexport const canonicalBigInteger = (value: unknown): string | undefined => {\n if (typeof value === 'number' && Number.isSafeInteger(value)) {\n return String(value);\n }\n return typeof value === 'string' && BIGINTEGER_PATTERN.test(value) ? value : undefined;\n};\n\nconst textLength = (text: string) => Array.from(text).length;\n\nconst checkLength = (text: string, settings: { minLength?: number; maxLength?: number }): ScalarOutcome => {\n const length = textLength(text);\n if (settings.minLength !== undefined && length < settings.minLength) {\n return problem('TOO_SHORT', `must be at least ${settings.minLength} characters`);\n }\n if (settings.maxLength !== undefined && length > settings.maxLength) {\n return problem('TOO_LONG', `must be at most ${settings.maxLength} characters`);\n }\n return ok(text);\n};\n\nconst checkRange = <T>(\n value: T,\n bounds: { min?: T | undefined; max?: T | undefined },\n compare: (a: T, b: T) => number,\n): ScalarOutcome => {\n if (bounds.min !== undefined && compare(value, bounds.min) < 0) {\n return problem('TOO_SMALL', `must be at least ${String(bounds.min)}`);\n }\n if (bounds.max !== undefined && compare(value, bounds.max) > 0) {\n return problem('TOO_LARGE', `must be at most ${String(bounds.max)}`);\n }\n return ok(value);\n};\n\nconst compareText = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);\nconst compareNumber = (a: number, b: number) => a - b;\nconst compareBig = (a: string, b: string) => {\n const difference = BigInt(a) - BigInt(b);\n return difference === 0n ? 0 : difference > 0n ? 1 : -1;\n};\n\nconst patternCache = new Map<string, RegExp | null>();\nconst compilePattern = (source: string): RegExp | null => {\n if (!patternCache.has(source)) {\n try {\n patternCache.set(source, new RegExp(source, 'u'));\n } catch {\n patternCache.set(source, null);\n }\n }\n return patternCache.get(source) ?? null;\n};\n\nconst decimalDigits = (text: string) => {\n const [whole = '', fraction = ''] = text.replace('-', '').split('.');\n return { precision: whole.replace(/^0+(?=\\d)/, '').length + fraction.length, scale: fraction.length };\n};\n\nconst checkUrl = (value: string, protocols: readonly string[] | undefined): ScalarOutcome => {\n if (!URL.canParse(value)) {\n return problem('INVALID_FORMAT', 'must be an absolute URL');\n }\n const protocol = new URL(value).protocol.replace(/:$/, '');\n return (protocols ?? DEFAULT_URL_PROTOCOLS).includes(protocol)\n ? ok(value)\n : problem('INVALID_FORMAT', `must use one of: ${(protocols ?? DEFAULT_URL_PROTOCOLS).join(', ')}`);\n};\n\nconst invalidType = (expected: string) => problem('INVALID_TYPE', `must be ${expected}`);\n\nconst checkString = (field: FieldDefinition, value: unknown): ScalarOutcome => {\n if (typeof value !== 'string') {\n return invalidType('a string');\n }\n switch (field.type) {\n case 'string': {\n const length = checkLength(value, field.settings);\n if (!length.ok || field.settings.pattern === undefined) {\n return length;\n }\n const pattern = compilePattern(field.settings.pattern);\n return pattern && !pattern.test(value)\n ? problem('PATTERN_MISMATCH', 'does not match the required pattern')\n : ok(value);\n }\n case 'text':\n return checkLength(value, field.settings);\n case 'slug':\n return SLUG_PATTERN.test(value)\n ? checkLength(value, field.settings)\n : problem('INVALID_FORMAT', 'must be lower-case words joined by hyphens');\n case 'uid':\n return UID_PATTERN.test(value)\n ? checkLength(value, field.settings)\n : problem('INVALID_FORMAT', 'may only contain letters, digits and _ . ~ -');\n case 'email':\n return EMAIL.test(value) && value.length <= MAX_EMAIL_LENGTH\n ? ok(value)\n : problem('INVALID_FORMAT', 'must be an email address');\n case 'url':\n return checkUrl(value, field.settings.protocols);\n default:\n return invalidType('a string');\n }\n};\n\n/** Checks and canonicalizes one present (non-missing) scalar value. */\nexport const checkScalar = (field: FieldDefinition, value: unknown): ScalarOutcome => {\n switch (field.type) {\n case 'string':\n case 'text':\n case 'slug':\n case 'uid':\n case 'email':\n case 'url':\n return checkString(field, value);\n case 'number':\n return typeof value === 'number' && Number.isFinite(value)\n ? checkRange(value, field.settings, compareNumber)\n : invalidType('a number');\n case 'integer':\n return Number.isSafeInteger(value)\n ? checkRange(value as number, field.settings, compareNumber)\n : invalidType('an integer');\n case 'decimal': {\n const decimal = canonicalDecimal(value);\n if (decimal === undefined) {\n return invalidType('a decimal string, e.g. \"12.50\"');\n }\n const { precision, scale } = decimalDigits(decimal);\n if (field.settings.scale !== undefined && scale > field.settings.scale) {\n return problem('INVALID_FORMAT', `must have at most ${field.settings.scale} decimal places`);\n }\n if (field.settings.precision !== undefined && precision > field.settings.precision) {\n return problem('INVALID_FORMAT', `must have at most ${field.settings.precision} digits`);\n }\n return checkRange(decimal, field.settings, compareDecimal);\n }\n case 'biginteger': {\n const big = canonicalBigInteger(value);\n return big === undefined\n ? invalidType('an integer string, e.g. \"9007199254740993\"')\n : checkRange(big, field.settings, compareBig);\n }\n case 'boolean':\n return typeof value === 'boolean' ? ok(value) : invalidType('true or false');\n case 'date': {\n const date = canonicalDate(value);\n return date === undefined\n ? invalidType('a date, YYYY-MM-DD')\n : checkRange(date, field.settings, compareText);\n }\n case 'datetime': {\n const datetime = canonicalDatetime(value);\n return datetime === undefined\n ? invalidType('an ISO-8601 date-time with a time zone, e.g. 2026-10-01T09:30:00Z')\n : checkRange(\n datetime,\n {\n min: canonicalDatetime(field.settings.min),\n max: canonicalDatetime(field.settings.max),\n },\n compareText,\n );\n }\n case 'time': {\n const time = canonicalTime(value);\n return time === undefined\n ? invalidType('a time, HH:mm[:ss[.sss]]')\n : checkRange(\n time,\n { min: canonicalTime(field.settings.min), max: canonicalTime(field.settings.max) },\n compareText,\n );\n }\n case 'enum': {\n const allowed = new Set(field.settings.values.map((entry) => entry.value));\n if (!field.settings.multiple) {\n return typeof value === 'string' && allowed.has(value)\n ? ok(value)\n : problem('NOT_ALLOWED', 'must be one of the allowed values');\n }\n if (!Array.isArray(value) || !value.every((item) => typeof item === 'string' && allowed.has(item))) {\n return problem('NOT_ALLOWED', 'must be a list of the allowed values');\n }\n return new Set(value).size === value.length ? ok(value) : problem('DUPLICATE', 'lists a value twice');\n }\n default:\n return invalidType(field.type);\n }\n};\n\n/** A single reference (relation target entry or media asset): a stable ID. */\nexport const isReferenceId = (value: unknown): value is string => isStableId(value);\n","import {\n isComponentDefinition,\n isEmptyRichText,\n richTextMediaIds,\n SCALAR_DATA_TYPES,\n type ComponentDefinition,\n type FieldDefinition,\n type SchemaDefinition,\n validateRichText,\n} from '@shapio/schema';\nimport type { ContentData } from '../../db/contentData.js';\nimport type { SchemaById } from '../../schema/snapshot.js';\nimport { findFieldByApiKey, type ContentModel } from '../model.js';\nimport { pointer, type ContentIssue } from './issues.js';\nimport { checkScalar, isReferenceId } from './scalars.js';\n\n/**\n * The content validator (build plan §4.E2). Built from the active model and the components it embeds,\n * compiled once per combination of schema revisions and cached. Values arrive keyed by API keys at the\n * boundary (`fromInput`) and are validated in storage form, keyed by stable field IDs (`validate`).\n *\n * Storage format of structured values:\n * - component: an object keyed by the component's field IDs (an array of them when repeatable);\n * - dynamic zone: an array of such objects, each with `__component` holding the component's stable ID\n * (its API key at the boundary);\n * - relation: the target entry ID (`one`) or an ordered array of IDs (`many`);\n * - media: an asset ID, or an array of them when `multiple`.\n * Missing values (absent, null, empty string, empty list, empty rich text) are not stored.\n * Keys that no field defines (values of removed fields) are preserved untouched (brief §5: field removal\n * keeps values until an explicit cleanup); deprecated fields' values are kept but not validated.\n */\nexport const COMPONENT_KEY = '__component';\n\nexport type RelationReference = { path: string; fieldId: string; targetModelId: string; entryIds: string[] };\n\n/** Media a document points at (media fields and rich-text images), checked against the library on write. */\nexport type MediaReferenceCheck = {\n path: string;\n assetIds: string[];\n allowedKinds: readonly string[] | null;\n};\n\nexport type ValidateOptions = {\n /** Autosave: everything but `required` is enforced, so half-finished drafts can be kept. */\n skipRequired?: boolean;\n};\n\nexport type ValidationOutcome = {\n data: ContentData;\n issues: ContentIssue[];\n relations: RelationReference[];\n media: MediaReferenceCheck[];\n};\n\nexport type ModelValidator = {\n model: ContentModel;\n /** Maps an API-keyed patch to storage form. Unknown or deprecated keys are reported, not dropped silently. */\n fromInput: (input: Readonly<Record<string, unknown>>) => { patch: ContentData; issues: ContentIssue[] };\n /** Validates and canonicalizes a complete document in storage form. */\n validate: (data: Readonly<ContentData>, options?: ValidateOptions) => ValidationOutcome;\n};\n\ntype Walk = ValidationOutcome & { options: ValidateOptions };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nexport const isMissingValue = (value: unknown): boolean =>\n value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);\n\nconst issue = (\n walk: { issues: ContentIssue[] },\n path: string,\n code: ContentIssue['code'],\n message: string,\n) => {\n walk.issues.push({ path, code, message });\n return undefined;\n};\n\ntype Components = ReadonlyMap<string, ComponentDefinition>;\n\nconst checkCount = (walk: Walk, path: string, count: number, settings: { min?: number; max?: number }) => {\n if (settings.min !== undefined && count < settings.min) {\n issue(walk, path, 'TOO_FEW', `needs at least ${settings.min} item(s)`);\n }\n if (settings.max !== undefined && count > settings.max) {\n issue(walk, path, 'TOO_MANY', `allows at most ${settings.max} item(s)`);\n }\n};\n\nconst checkReferences = (walk: Walk, path: string, value: unknown, multiple: boolean) => {\n const ids = multiple ? value : [value];\n if (!Array.isArray(ids) || !ids.every(isReferenceId)) {\n return issue(walk, path, 'INVALID_TYPE', multiple ? 'must be a list of IDs' : 'must be an ID');\n }\n if (new Set(ids).size !== ids.length) {\n return issue(walk, path, 'DUPLICATE', 'lists an ID twice');\n }\n return ids;\n};\n\nconst validateFields = (\n definition: SchemaDefinition,\n data: Readonly<Record<string, unknown>>,\n path: string,\n walk: Walk,\n components: Components,\n): Record<string, unknown> => {\n const known = new Set(definition.fields.map((field) => field.id));\n const output: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (!known.has(key) && key !== COMPONENT_KEY) {\n output[key] = value;\n }\n }\n for (const field of definition.fields) {\n const value = data[field.id];\n if (field.deprecated) {\n if (value !== undefined) {\n output[field.id] = value;\n }\n continue;\n }\n const checked = checkField(field, value, pointer(path, field.apiKey), walk, components);\n if (checked !== undefined) {\n output[field.id] = checked;\n }\n }\n return output;\n};\n\nconst checkComponentObject = (\n component: ComponentDefinition,\n value: unknown,\n path: string,\n walk: Walk,\n components: Components,\n) =>\n isRecord(value)\n ? validateFields(component, value, path, walk, components)\n : issue(walk, path, 'INVALID_TYPE', 'must be an object');\n\nconst checkStructured = (\n field: FieldDefinition,\n value: unknown,\n path: string,\n walk: Walk,\n components: Components,\n): unknown => {\n switch (field.type) {\n case 'component': {\n const component = components.get(field.settings.component);\n if (!component) {\n return issue(walk, path, 'UNKNOWN_COMPONENT', 'refers to a component that does not exist');\n }\n if (!field.settings.repeatable) {\n return checkComponentObject(component, value, path, walk, components);\n }\n if (!Array.isArray(value)) {\n return issue(walk, path, 'INVALID_TYPE', 'must be a list');\n }\n checkCount(walk, path, value.length, field.settings);\n return value.map((item, index) =>\n checkComponentObject(component, item, pointer(path, index), walk, components),\n );\n }\n case 'dynamiczone': {\n if (!Array.isArray(value)) {\n return issue(walk, path, 'INVALID_TYPE', 'must be a list');\n }\n checkCount(walk, path, value.length, field.settings);\n return value.map((item, index) => {\n const at = pointer(path, index);\n const componentId = isRecord(item) ? item[COMPONENT_KEY] : undefined;\n const component =\n typeof componentId === 'string' && field.settings.components.includes(componentId)\n ? components.get(componentId)\n : undefined;\n if (!component) {\n return issue(walk, pointer(at, COMPONENT_KEY), 'UNKNOWN_COMPONENT', 'is not allowed in this zone');\n }\n const fields = checkComponentObject(component, item, at, walk, components);\n return fields ? { [COMPONENT_KEY]: component.id, ...fields } : undefined;\n });\n }\n case 'relation': {\n const ids = checkReferences(walk, path, value, field.settings.cardinality === 'many');\n if (!ids) {\n return undefined;\n }\n if (field.settings.cardinality === 'many') {\n checkCount(walk, path, ids.length, field.settings);\n }\n walk.relations.push({ path, fieldId: field.id, targetModelId: field.settings.target, entryIds: ids });\n return value;\n }\n case 'media': {\n const ids = checkReferences(walk, path, value, field.settings.multiple);\n if (!ids) {\n return undefined;\n }\n if (field.settings.multiple) {\n checkCount(walk, path, ids.length, field.settings);\n }\n walk.media.push({ path, assetIds: ids, allowedKinds: field.settings.allowedKinds ?? null });\n return value;\n }\n case 'richtext': {\n const result = validateRichText(value);\n if (!result.ok) {\n walk.issues.push(\n ...result.problems.map((problem) => ({\n path: `${path}${problem.path}`,\n code: 'INVALID_RICHTEXT' as const,\n message: problem.message,\n })),\n );\n return undefined;\n }\n if (isEmptyRichText(result.document)) {\n // An empty editor is a missing value.\n return field.required && !walk.options.skipRequired\n ? issue(walk, path, 'REQUIRED', 'is required')\n : undefined;\n }\n const images = richTextMediaIds(result.document);\n if (images.length > 0) {\n walk.media.push({ path, assetIds: images, allowedKinds: ['image'] });\n }\n if (field.settings.maxLength !== undefined && result.textLength > field.settings.maxLength) {\n return issue(\n walk,\n path,\n 'TOO_LONG',\n `must be at most ${field.settings.maxLength} characters of text`,\n );\n }\n return result.document;\n }\n case 'json':\n return value;\n default:\n return issue(walk, path, 'INVALID_TYPE', `unsupported type ${field.type}`);\n }\n};\n\nfunction checkField(\n field: FieldDefinition,\n value: unknown,\n path: string,\n walk: Walk,\n components: Components,\n): unknown {\n if (isMissingValue(value)) {\n if (field.required && !walk.options.skipRequired) {\n issue(walk, path, 'REQUIRED', 'is required');\n }\n return undefined;\n }\n if (SCALAR_DATA_TYPES.has(field.type) || (field.type === 'enum' && field.settings.multiple)) {\n const outcome = checkScalar(field, value);\n return outcome.ok ? outcome.value : issue(walk, path, outcome.code, outcome.message);\n }\n return checkStructured(field, value, path, walk, components);\n}\n\n/** API keys → field IDs, recursively through components and dynamic zones. */\nconst mapInput = (\n definition: SchemaDefinition,\n input: Readonly<Record<string, unknown>>,\n path: string,\n issues: ContentIssue[],\n components: Components,\n): Record<string, unknown> => {\n const output: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(input)) {\n if (key === COMPONENT_KEY && isComponentDefinition(definition)) {\n continue;\n }\n const field = findFieldByApiKey(definition.fields, key);\n if (!field) {\n issues.push({\n path: pointer(path, key),\n code: 'UNKNOWN_FIELD',\n message: 'is not a field of this model',\n });\n continue;\n }\n output[field.id] = mapFieldInput(field, value, pointer(path, key), issues, components);\n }\n return output;\n};\n\nconst mapComponentInput = (\n component: ComponentDefinition | undefined,\n value: unknown,\n path: string,\n issues: ContentIssue[],\n components: Components,\n) => (component && isRecord(value) ? mapInput(component, value, path, issues, components) : value);\n\nfunction mapFieldInput(\n field: FieldDefinition,\n value: unknown,\n path: string,\n issues: ContentIssue[],\n components: Components,\n): unknown {\n if (field.type === 'component') {\n const component = components.get(field.settings.component);\n return field.settings.repeatable && Array.isArray(value)\n ? value.map((item, index) =>\n mapComponentInput(component, item, pointer(path, index), issues, components),\n )\n : mapComponentInput(component, value, path, issues, components);\n }\n if (field.type === 'dynamiczone' && Array.isArray(value)) {\n return value.map((item, index) => {\n const at = pointer(path, index);\n const apiKey = isRecord(item) ? item[COMPONENT_KEY] : undefined;\n const component = field.settings.components\n .map((id) => components.get(id))\n .find((candidate) => candidate?.apiKey === apiKey);\n if (!component) {\n issues.push({\n path: pointer(at, COMPONENT_KEY),\n code: 'UNKNOWN_COMPONENT',\n message: 'is not allowed in this zone',\n });\n return item as unknown;\n }\n return {\n [COMPONENT_KEY]: component.id,\n ...mapInput(component, item as Record<string, unknown>, at, issues, components),\n };\n });\n }\n return value;\n}\n\nconst compile = (model: ContentModel): ModelValidator => {\n const components: Components = new Map(\n [...model.components].map(([id, entry]) => [id, entry.definition] as const),\n );\n return {\n model,\n fromInput: (input) => {\n const issues: ContentIssue[] = [];\n return { patch: mapInput(model.definition, input, '', issues, components), issues };\n },\n validate: (data, options = {}) => {\n const walk: Walk = { data: {}, issues: [], relations: [], media: [], options };\n walk.data = validateFields(model.definition, data, '', walk, components);\n return { data: walk.data, issues: walk.issues, relations: walk.relations, media: walk.media };\n },\n };\n};\n\nconst MAX_CACHED_VALIDATORS = 500;\nconst cache = new Map<string, ModelValidator>();\n\n/** The cache key: the model's revision plus every embedded component's revision. */\nconst cacheKey = (snapshot: SchemaById, model: ContentModel) =>\n [\n model.revisionId,\n ...[...model.components.keys()].sort().map((id) => snapshot.byId.get(id)?.revisionId ?? id),\n ].join(':');\n\n/** The compiled validator for a model at the pinned snapshot, cached by schema revision IDs. */\nexport const buildValidator = (snapshot: SchemaById, model: ContentModel): ModelValidator => {\n const key = cacheKey(snapshot, model);\n let validator = cache.get(key);\n if (!validator) {\n if (cache.size >= MAX_CACHED_VALIDATORS) {\n cache.clear();\n }\n validator = compile(model);\n cache.set(key, validator);\n }\n return validator;\n};\n","/**\n * The database dialect this process talks to (ADR 0001, \"Dialect boundary\"). Chosen from DATABASE_URL:\n * `postgres://…`/`postgresql://…`, `mysql://…` or `sqlite:<path>` (`sqlite::memory:` for a throwaway\n * in-memory database).\n * The `db/sql` primitives and the content compiler read `currentDialect()` while building SQL, so it is set\n * once, when the process creates its database handle (`createDb`), before any query is built.\n */\nexport type DialectName = 'postgres' | 'sqlite' | 'mysql';\n\nconst SQLITE_PREFIX = 'sqlite:';\nconst MEMORY = ':memory:';\n\n/** The dialect a DATABASE_URL names, or undefined when it names none of them. */\nexport const dialectOfUrl = (url: string): DialectName | undefined => {\n if (url.startsWith(SQLITE_PREFIX)) {\n return 'sqlite';\n }\n if (url.startsWith('mysql://')) {\n return 'mysql';\n }\n return /^postgres(ql)?:\\/\\//.test(url) ? 'postgres' : undefined;\n};\n\nexport type SqliteLocation = { kind: 'memory' } | { kind: 'file'; path: string };\n\n/**\n * Where a `sqlite:` URL points: `sqlite::memory:` or `sqlite:<path>` (relative to the working directory or\n * absolute; `sqlite:///abs/path` is accepted too). Throws when the path is empty.\n */\nexport const sqliteLocationOfUrl = (url: string): SqliteLocation => {\n const rest = url.slice(SQLITE_PREFIX.length);\n if (rest === MEMORY) {\n return { kind: 'memory' };\n }\n const path = rest.startsWith('//') ? rest.slice(2) : rest;\n if (path === '') {\n throw new Error('A sqlite: DATABASE_URL needs a file path, e.g. sqlite:./shapio.db');\n }\n return { kind: 'file', path };\n};\n\n/** `host:port/database` (PostgreSQL, MySQL) or the file path (SQLite) from DATABASE_URL, never the credentials. */\nexport const describeDatabaseTarget = (connectionString: string): string => {\n if (dialectOfUrl(connectionString) === 'sqlite') {\n const location = sqliteLocationOfUrl(connectionString);\n return location.kind === 'memory' ? 'an in-memory database' : location.path;\n }\n try {\n const url = new URL(connectionString);\n const defaultPort = dialectOfUrl(connectionString) === 'mysql' ? '3306' : '5432';\n return `${url.hostname}:${url.port || defaultPort}${url.pathname}`;\n } catch {\n return 'the configured database';\n }\n};\n\nlet current: DialectName | undefined;\n\n/** The dialect queries are built for. PostgreSQL until a database handle says otherwise. */\nexport const currentDialect = (): DialectName => current ?? 'postgres';\n\n/**\n * Sets the process dialect. `createDb` calls it; one process talks to one dialect, so switching after a\n * handle exists is a bug (`force` is for unit tests that compile SQL for both dialects without a database).\n */\nexport const setCurrentDialect = (\n dialect: DialectName,\n { force = false }: { force?: boolean } = {},\n): void => {\n if (!force && current !== undefined && current !== dialect) {\n throw new Error(`This process already uses ${current}; it cannot also open a ${dialect} database`);\n }\n current = dialect;\n};\n\n/** Runs `fn` with the dialect switched (unit tests of SQL builders only), then restores the previous one. */\nexport const withDialect = <T>(dialect: DialectName, fn: () => T): T => {\n const previous = current;\n current = dialect;\n try {\n return fn();\n } finally {\n current = previous;\n }\n};\n\nexport const isSqlite = (): boolean => currentDialect() === 'sqlite';\n\nexport const isMysql = (): boolean => currentDialect() === 'mysql';\n","/**\n * On MySQL a field index is built on an invisible virtual column holding the field's value expression\n * (`content/compiler/dialect/mysql.ts`); the column is named after the index: `eh_<hash>` → `ev_<hash>`.\n */\nexport const fieldIndexColumnName = (indexName: string): string => indexName.replace(/^eh_/, 'ev_');\n","/** Content heads (package E). Expression indexes live on this table only. */\nexport const CONTENT_HEADS_TABLE = 'entry_heads';\n/** Columns of `entry_heads` the index layout depends on. Package E must keep these names. */\nexport const CONTENT_HEADS_COLUMNS = {\n data: 'data',\n siteId: 'site_id',\n modelId: 'model_id',\n locale: 'locale',\n state: 'state',\n} as const;\n","import { sql, type RawBuilder } from 'kysely';\nimport { toDatetimeText } from '../../../db/mysql/codec.js';\nimport { fieldIndexColumnName } from '../../../db/mysql/fieldIndexColumns.js';\nimport { CONTENT_HEADS_COLUMNS, CONTENT_HEADS_TABLE } from '../heads.js';\nimport type { ContentSqlDialect, FieldIndexDefinition, ListElement, ValueCast } from './types.js';\n\n/**\n * MySQL content SQL (ADR 0001, \"MySQL\"). `data` is a JSON column; there is no containment index and no\n * partial index, so:\n * - each filterable or sortable field gets an invisible virtual column holding its value expression and one\n * index `(site_id, model_id[, locale], state, column)`, built online (`ALGORITHM=INPLACE, LOCK=NONE`; a\n * functional index cannot be). Queries use the expression; MySQL matches it to the column for `=`, ranges\n * and ORDER BY (it does not for `<=>`, so equality is `expr = ? and expr is not null`: false, never null,\n * for a missing value, as containment is);\n * - values are read with `json_value`, which gives SQL NULL for JSON null (`->>` gives the text 'null');\n * text indexes and compares the first 255 characters (an index key is bounded), and equality with a longer\n * value also compares the whole text; numbers compare as DOUBLE, decimal and biginteger strings as\n * DECIMAL(65,30) (parameters are cast the same way, or MySQL does not use the column);\n * - text compares in the session's binary `utf8mb4_0900_bin` collation (case-sensitive, code point order),\n * matched with `locate`, so no LIKE escaping; `$containsi` and search fold both sides with `lower()`;\n * - list membership is `member of` (unindexed); bound lists are `in (?, …)`;\n * - JSON functions return `utf8mb4_bin` text, which MySQL refuses to compare with a literal in the session's\n * collation, so JSON is compared as JSON (`= cast('null' as json)`) or the literal's collation is named;\n * - NULL placement in sorts is written out: `(expr is null), expr`.\n */\nconst DATA = sql.ref(CONTENT_HEADS_COLUMNS.data);\n\n/** Longest text `json_value` returns; longer values read as NULL. */\nconst TEXT_LIMIT = 1_000_000;\n/** Characters of a text value that are indexed, compared in ranges and sorted. */\nexport const INDEXED_TEXT_LENGTH = 255;\n\n/**\n * The JSON path of a field. The `_utf8mb4` introducer gives the literal the character set's own collation\n * whatever the session's: MySQL stores a generated column's expression that way, and only matches a query's\n * expression to the column (to use its index) when every literal in it has the same collation.\n */\nconst path = (fieldId: string) => sql`_utf8mb4${sql.lit(`$.\"${fieldId}\"`)}`;\n\nconst fieldJson = (fieldId: string): RawBuilder<unknown> => sql`json_extract(${DATA}, ${path(fieldId)})`;\nconst fieldText = (fieldId: string): RawBuilder<unknown> =>\n sql`json_value(${DATA}, ${path(fieldId)} returning char(${sql.lit(TEXT_LIMIT)}))`;\n\n/** decimal and biginteger: exact to 35 integer and 30 fractional digits; other numbers are doubles. */\nconst DECIMAL = sql`decimal(65,30)`;\n\nconst fieldValue = (fieldId: string, cast: ValueCast, numericString = false): RawBuilder<unknown> => {\n switch (cast) {\n case 'numeric':\n return numericString\n ? sql`json_value(${DATA}, ${path(fieldId)} returning ${DECIMAL})`\n : sql`json_value(${DATA}, ${path(fieldId)} returning double)`;\n case 'boolean':\n return sql`json_value(${DATA}, ${path(fieldId)} returning char(5))`;\n default:\n return sql`left(${fieldText(fieldId)}, ${sql.lit(INDEXED_TEXT_LENGTH)})`;\n }\n};\n\nconst booleanText = (value: unknown) => (value === true || value === 'true' ? 'true' : 'false');\n\nconst castParameter = (value: unknown, cast: ValueCast, numericString = false): RawBuilder<unknown> => {\n switch (cast) {\n case 'numeric':\n return numericString ? sql`cast(${value} as ${DECIMAL})` : sql`cast(${value} as double)`;\n case 'boolean':\n return sql`${booleanText(value)}`;\n default:\n return sql`${value}`;\n }\n};\n\nconst listValue = (value: string, element: ListElement): string =>\n element === 'timestamp' ? toDatetimeText(value) : value;\n\n/** `expr = ? and expr is not null`: false (not null) for a missing value, and still index-served. */\nconst presentAndEqual = (expression: RawBuilder<unknown>, value: RawBuilder<unknown>) =>\n sql`(${expression} = ${value} and ${expression} is not null)`;\n\nconst textEquals = (fieldId: string, value: unknown): RawBuilder<unknown> => {\n const prefix = fieldValue(fieldId, 'text');\n if (typeof value === 'string' && [...value].length < INDEXED_TEXT_LENGTH) {\n return presentAndEqual(prefix, sql`${value}`);\n }\n return sql`(${prefix} = left(${value}, ${sql.lit(INDEXED_TEXT_LENGTH)}) and ${fieldText(fieldId)} = ${value})`;\n};\n\nconst COLUMN_TYPES: Readonly<Record<ValueCast, RawBuilder<unknown>>> = {\n text: sql`varchar(${sql.lit(INDEXED_TEXT_LENGTH)}) collate utf8mb4_0900_bin`,\n numeric: sql`double`,\n boolean: sql`varchar(5) collate utf8mb4_0900_bin`,\n};\n\nconst addIndexColumn = ({\n name,\n table,\n expression,\n cast,\n numericString,\n}: FieldIndexDefinition): RawBuilder<unknown> =>\n sql`alter table ${sql.table(table)} add column ${sql.id(fieldIndexColumnName(name))} ${numericString ? DECIMAL : COLUMN_TYPES[cast]}\n generated always as (${expression}) virtual invisible, algorithm=inplace, lock=none`;\n\nexport const mysqlContentDialect: ContentSqlDialect = {\n name: 'mysql',\n\n uuid: (value) => sql`${value}`,\n timestamp: (value) => sql`${toDatetimeText(value)}`,\n bool: (value) => (value ? sql`true` : sql`false`),\n oneOf: (column, values, element) =>\n values.length === 0\n ? sql<boolean>`false`\n : sql<boolean>`${column} in (${sql.join(values.map((value) => listValue(value, element)))})`,\n\n fieldValue,\n fieldText,\n fieldJson,\n castParameter,\n\n fieldEquals: (target, value) => {\n if (target.list) {\n // `is not null` first: false, not null, for a missing list (so `$nin` includes it, as on PostgreSQL).\n const json = fieldJson(target.fieldId);\n return sql`(${json} is not null and json_type(${json}) = 'ARRAY' collate utf8mb4_bin and ${value} member of (${json}))`;\n }\n if (target.numericString && target.cast) {\n // Numeric like PostgreSQL (\"12.5\" equals \"12.50\"), as DECIMAL(65,30).\n return sql`(${fieldValue(target.fieldId, target.cast, true)} = ${castParameter(value, target.cast, true)})`;\n }\n if (target.cast === null || target.cast === 'text') {\n return target.cast === null\n ? presentAndEqual(fieldText(target.fieldId), sql`${value}`)\n : textEquals(target.fieldId, value);\n }\n return presentAndEqual(fieldValue(target.fieldId, target.cast), castParameter(value, target.cast));\n },\n fieldMissing: (fieldId) => {\n const json = fieldJson(fieldId);\n return sql`(${json} is null or ${json} = cast('null' as json) or ${json} = cast('\"\"' as json) or ${json} = json_array())`;\n },\n textMatch: (fieldId, operator, text) => {\n const value = fieldText(fieldId);\n switch (operator) {\n case '$startsWith':\n return sql`(locate(${text}, ${value}) = 1)`;\n case '$endsWith':\n return sql`(right(${value}, ${sql.lit([...text].length)}) = ${text})`;\n case '$notContains':\n return sql`(locate(${text}, coalesce(${value}, '')) = 0)`;\n default:\n return sql`(locate(${text}, ${value}) > 0)`;\n }\n },\n containsInsensitive: (fieldId, text) => sql`(locate(lower(${text}), lower(${fieldText(fieldId)})) > 0)`,\n\n localeRank: (chain, column) =>\n sql`(case ${column} ${sql.join(\n chain.map((locale, index) => sql`when ${locale} then ${sql.lit(index)}`),\n sql` `,\n )} end)`,\n sortDirection: (direction) => (direction === 'desc' ? sql`desc` : sql`asc`),\n sortTerm: (expression, direction, nullable) => {\n const order = direction === 'desc' ? sql`desc` : sql`asc`;\n // PostgreSQL's default: nulls sort as the largest value. MySQL sorts them as the smallest.\n return nullable\n ? sql`(${expression} is null) ${order}, ${expression} ${order}`\n : sql`${expression} ${order}`;\n },\n snapshotCte: ({ name, siteId, modelId, seq }) => sql`with ${sql.id(name)} as (\n select pl.entry_id, pl.site_id, pl.model_id, pl.locale, 'published' as state, r.data, 0 as version,\n r.id as revision_id, pl.published_at as updated_at, null as autosaved_at,\n en.created_at as entry_created_at\n from publication_log pl\n join content_revisions r on r.id = pl.revision_id\n join entries en on en.id = pl.entry_id\n where pl.site_id = ${siteId} and pl.model_id = ${modelId}\n and pl.from_seq <= ${seq}\n and (pl.to_seq is null or pl.to_seq > ${seq})\n ) `,\n\n fieldIndexColumn: {\n add: addIndexColumn,\n drop: (name) =>\n sql`alter table ${sql.table(CONTENT_HEADS_TABLE)} drop column ${sql.id(fieldIndexColumnName(name))}, algorithm=inplace, lock=none`,\n },\n createFieldIndex: ({ name, table, localized, modelIdColumn }) => {\n const { siteId, locale, state } = CONTENT_HEADS_COLUMNS;\n const leading = localized\n ? sql`${sql.ref(siteId)}, ${sql.ref(modelIdColumn)}, ${sql.ref(locale)}, ${sql.ref(state)}`\n : sql`${sql.ref(siteId)}, ${sql.ref(modelIdColumn)}, ${sql.ref(state)}`;\n return sql`create index ${sql.id(name)} on ${sql.table(table)} (${leading}, ${sql.id(fieldIndexColumnName(name))}) algorithm=inplace lock=none`;\n },\n createFieldStatistics: () => null,\n dropFieldIndex: (name) =>\n sql`drop index ${sql.id(name)} on ${sql.table(CONTENT_HEADS_TABLE)} algorithm=inplace lock=none`,\n dropFieldStatistics: () => null,\n analyze: (table) => sql`analyze table ${sql.table(table)}`,\n};\n","import { sql, type RawBuilder } from 'kysely';\nimport { CONTENT_HEADS_COLUMNS } from '../heads.js';\nimport type { ContentSqlDialect, ListElement, TextMatchOperator, ValueCast } from './types.js';\n\n/**\n * PostgreSQL content SQL (ADR 0001): JSONB with a GIN `jsonb_path_ops` index serving containment, so\n * equality on a field without an index is emitted as `data @> {...}`; ranges, sorts and equality on indexed\n * fields (filterable or sortable) use per-field B-tree expression indexes built `CONCURRENTLY` with a\n * matching `CREATE STATISTICS` object.\n */\nconst DATA = sql.ref(CONTENT_HEADS_COLUMNS.data);\n\nconst ARRAY_TYPES: Readonly<Record<ListElement, RawBuilder<unknown>>> = {\n uuid: sql`uuid[]`,\n text: sql`text[]`,\n timestamp: sql`timestamptz[]`,\n};\n\nconst MISSING_JSON = sql`('null'::jsonb, '\"\"'::jsonb, '[]'::jsonb)`;\n\n/** LIKE escaping (the default `\\` escape character). */\nexport const escapeLike = (text: string) => text.replace(/[\\\\%_]/g, (char) => `\\\\${char}`);\n\nconst likePattern = (operator: TextMatchOperator | '$containsi', text: string) => {\n const escaped = escapeLike(text);\n switch (operator) {\n case '$startsWith':\n return `${escaped}%`;\n case '$endsWith':\n return `%${escaped}`;\n default:\n return `%${escaped}%`;\n }\n};\n\nconst fieldText = (fieldId: string): RawBuilder<unknown> => sql`(${DATA} ->> ${sql.lit(fieldId)})`;\n\nconst fieldValue = (fieldId: string, cast: ValueCast): RawBuilder<unknown> => {\n const text = fieldText(fieldId);\n switch (cast) {\n case 'numeric':\n return sql`(${text}::numeric)`;\n case 'boolean':\n return sql`(${text}::boolean)`;\n default:\n return text;\n }\n};\n\nconst castParameter = (value: unknown, cast: ValueCast): RawBuilder<unknown> => {\n switch (cast) {\n case 'numeric':\n return sql`${value}::numeric`;\n case 'boolean':\n return sql`${value}::boolean`;\n default:\n return sql`${value}::text`;\n }\n};\n\n/**\n * `data @> $1::jsonb`: the only form the GIN index serves. The document is a parameter, field IDs included,\n * so nothing from the request is ever spliced into the SQL text.\n */\nexport const containment = (document: Readonly<Record<string, unknown>>): RawBuilder<unknown> =>\n sql`(${DATA} @> ${JSON.stringify(document)}::jsonb)`;\n\nexport const postgresContentDialect: ContentSqlDialect = {\n name: 'postgres',\n\n uuid: (value) => sql`${value}::uuid`,\n timestamp: (value) => sql`${value}::timestamptz`,\n bool: (value) => sql`${value}::boolean`,\n oneOf: (column, values, element) => sql<boolean>`${column} = any(${[...values]}::${ARRAY_TYPES[element]})`,\n\n fieldValue,\n fieldText,\n fieldJson: (fieldId) => sql`(${DATA} -> ${sql.lit(fieldId)})`,\n castParameter,\n\n fieldEquals: (target, value) => {\n if (target.numericString && target.cast) {\n // \"12.5\" and \"12.50\" are equal numbers but different JSON strings: compare numerically.\n return sql`(${fieldValue(target.fieldId, target.cast)} = ${castParameter(value, target.cast)})`;\n }\n if (target.indexed && target.cast) {\n // The index expression (and its statistics) serve the filter; a GIN containment match must recheck,\n // and decompress, every candidate row. `is not null` keeps it false, never null, for a missing value,\n // like containment, so `$ne`/`$nin`/`$not` still include missing values (ADR 0001 amendment).\n const expression = fieldValue(target.fieldId, target.cast);\n return sql`(${expression} = ${castParameter(value, target.cast)} and ${expression} is not null)`;\n }\n return containment({ [target.fieldId]: target.list ? [value] : value });\n },\n fieldMissing: (fieldId) => {\n const json = postgresContentDialect.fieldJson(fieldId);\n return sql`(${json} is null or ${json} in ${MISSING_JSON})`;\n },\n textMatch: (fieldId, operator, text) =>\n operator === '$notContains'\n ? sql`(coalesce(${fieldText(fieldId)}, '') not like ${likePattern(operator, text)})`\n : sql`(${fieldText(fieldId)} like ${likePattern(operator, text)})`,\n containsInsensitive: (fieldId, text) =>\n sql`(${fieldText(fieldId)} ilike ${likePattern('$containsi', text)})`,\n\n localeRank: (chain, column) => sql`array_position(${[...chain]}::text[], ${column})`,\n sortDirection: (direction) => (direction === 'desc' ? sql`desc` : sql`asc`),\n snapshotCte: ({ name, siteId, modelId, seq }) => sql`with ${sql.id(name)} as (\n select pl.entry_id, pl.site_id, pl.model_id, pl.locale, 'published'::text as state, r.data, 0 as version,\n r.id as revision_id, pl.published_at as updated_at, null::timestamptz as autosaved_at,\n en.created_at as entry_created_at\n from publication_log pl\n join content_revisions r on r.id = pl.revision_id\n join entries en on en.id = pl.entry_id\n where pl.site_id = ${siteId}::uuid and pl.model_id = ${modelId}\n and pl.from_seq <= ${seq}::bigint\n and (pl.to_seq is null or pl.to_seq > ${seq}::bigint)\n ) `,\n\n createFieldIndex: ({ name, table, leading, expression, modelIdColumn, modelId }) =>\n sql`create index concurrently if not exists ${sql.id(name)} on ${sql.table(table)} (${leading}, ${expression}) where ${sql.ref(modelIdColumn)} = ${modelId}`,\n createFieldStatistics: (name, expression, table) =>\n sql`create statistics if not exists ${sql.id(name)} on ${expression} from ${sql.table(table)}`,\n dropFieldIndex: (name) => sql`drop index concurrently if exists ${sql.id(name)}`,\n dropFieldStatistics: (name) => sql`drop statistics if exists ${sql.id(name)}`,\n analyze: (table) => sql`analyze ${sql.table(table)}`,\n};\n","import { sql, type RawBuilder } from 'kysely';\nimport { toStoredTimestamp } from '../../../db/sql/time.js';\nimport { CONTENT_HEADS_COLUMNS } from '../heads.js';\nimport type { ContentSqlDialect, ListElement, ValueCast } from './types.js';\n\n/**\n * SQLite content SQL (ADR 0001, \"D2: as built\"). `data` is JSON text read with JSON1's `->`/`->>`; there is\n * no GIN index and no containment, so:\n * - one partial expression index per filterable or sortable field serves equality, ranges and sorts, and\n * scalar equality is emitted on that same expression (`expr is ?`, which the planner treats like `=` and\n * which is false, never null, for a missing value, as containment is);\n * - list membership is `json_each` (unindexed; the model's heads are scanned);\n * - LIKE folds ASCII case, so case-sensitive matches use `instr`/`substr`, and case-insensitive ones use\n * `shapio_fold` (JavaScript `toLowerCase`, a deterministic function registered on every connection by\n * the database layer) on the column and the same folding of the parameter in JavaScript;\n * - there are no `::` casts and no array parameters: lists are bound as one JSON text parameter, and\n * timestamps as the stored text (`toStoredTimestamp`), which compares as time;\n * - indexes are built with a plain `CREATE INDEX` (no `CONCURRENTLY`: the build holds the single write lock\n * until it finishes) and there is no `CREATE STATISTICS`.\n */\nconst DATA = sql.ref(CONTENT_HEADS_COLUMNS.data);\n\n/** The name of the case-folding SQL function the database layer registers on each SQLite connection. */\nexport const FOLD_FUNCTION = 'shapio_fold';\n/** What `shapio_fold` computes, for the parameter side (and the function's registration). */\nexport const foldText = (text: string): string => text.toLowerCase();\n\nconst listParameter = (values: readonly string[], element: ListElement): string =>\n JSON.stringify(element === 'timestamp' ? values.map((value) => toStoredTimestamp(value)) : [...values]);\n\nconst fieldText = (fieldId: string): RawBuilder<unknown> => sql`(${DATA} ->> ${sql.lit(fieldId)})`;\nconst fieldJson = (fieldId: string): RawBuilder<unknown> => sql`(${DATA} -> ${sql.lit(fieldId)})`;\n\n/** Booleans extract as 1/0 and numbers as numbers; decimal and biginteger strings need the cast. */\nconst fieldValue = (fieldId: string, cast: ValueCast): RawBuilder<unknown> =>\n cast === 'numeric' ? sql`cast(${fieldText(fieldId)} as numeric)` : fieldText(fieldId);\n\nconst castParameter = (value: unknown, cast: ValueCast): RawBuilder<unknown> => {\n switch (cast) {\n case 'numeric':\n return sql`cast(${value} as numeric)`;\n case 'boolean':\n return sql`${value === true || value === 'true' ? 1 : 0}`;\n default:\n return sql`${value}`;\n }\n};\n\nexport const sqliteContentDialect: ContentSqlDialect = {\n name: 'sqlite',\n\n uuid: (value) => sql`${value}`,\n timestamp: (value) => sql`${toStoredTimestamp(value)}`,\n bool: (value) => (value ? sql`true` : sql`false`),\n oneOf: (column, values, element) =>\n sql<boolean>`${column} in (select value from json_each(${listParameter(values, element)}))`,\n\n fieldValue,\n fieldText,\n fieldJson,\n castParameter,\n\n fieldEquals: (target, value) => {\n if (target.list) {\n const json = fieldJson(target.fieldId);\n return sql`(json_type(${json}) = 'array' and exists (select 1 from json_each(${json}) where value = ${value}))`;\n }\n if (target.numericString && target.cast) {\n // Numeric like PostgreSQL (\"12.5\" equals \"12.50\"); exact to about 15 significant digits.\n return sql`(${fieldValue(target.fieldId, target.cast)} = ${castParameter(value, target.cast)})`;\n }\n const expression = target.cast ? fieldValue(target.fieldId, target.cast) : fieldText(target.fieldId);\n return sql`(${expression} is ${castParameter(value, target.cast ?? 'text')})`;\n },\n fieldMissing: (fieldId) => {\n const json = fieldJson(fieldId);\n return sql`(${json} is null or ${json} in ('null', '\"\"', '[]'))`;\n },\n textMatch: (fieldId, operator, text) => {\n const value = fieldText(fieldId);\n switch (operator) {\n case '$startsWith':\n return sql`(instr(${value}, ${text}) = 1)`;\n case '$endsWith':\n return sql`(substr(${value}, -length(${text})) = ${text})`;\n case '$notContains':\n return sql`(instr(coalesce(${value}, ''), ${text}) = 0)`;\n default:\n return sql`(instr(${value}, ${text}) > 0)`;\n }\n },\n containsInsensitive: (fieldId, text) =>\n sql`(instr(${sql.raw(FOLD_FUNCTION)}(${fieldText(fieldId)}), ${foldText(text)}) > 0)`,\n\n localeRank: (chain, column) =>\n sql`(case ${column} ${sql.join(\n chain.map((locale, index) => sql`when ${locale} then ${sql.lit(index)}`),\n sql` `,\n )} end)`,\n sortDirection: (direction, nullable) => {\n if (!nullable) {\n return direction === 'desc' ? sql`desc` : sql`asc`;\n }\n // PostgreSQL's default: nulls sort as the largest value.\n return direction === 'desc' ? sql`desc nulls first` : sql`asc nulls last`;\n },\n snapshotCte: ({ name, siteId, modelId, seq }) => sql`with ${sql.id(name)} as (\n select pl.entry_id, pl.site_id, pl.model_id, pl.locale, 'published' as state, r.data, 0 as version,\n r.id as revision_id, pl.published_at as updated_at, null as autosaved_at,\n en.created_at as entry_created_at\n from publication_log pl\n join content_revisions r on r.id = pl.revision_id\n join entries en on en.id = pl.entry_id\n where pl.site_id = ${siteId} and pl.model_id = ${modelId}\n and pl.from_seq <= ${seq}\n and (pl.to_seq is null or pl.to_seq > ${seq})\n ) `,\n\n createFieldIndex: ({ name, table, leading, expression, modelIdColumn, modelId }) =>\n sql`create index if not exists ${sql.id(name)} on ${sql.table(table)} (${leading}, ${expression}) where ${sql.ref(modelIdColumn)} = ${modelId}`,\n createFieldStatistics: () => null,\n dropFieldIndex: (name) => sql`drop index if exists ${sql.id(name)}`,\n dropFieldStatistics: () => null,\n analyze: (table) => sql`analyze ${sql.table(table)}`,\n};\n","import { mysqlContentDialect } from './mysql.js';\nimport { postgresContentDialect } from './postgres.js';\nimport { sqliteContentDialect } from './sqlite.js';\nimport type { ContentDialectName, ContentSqlDialect } from './types.js';\n\nexport type { ContentDialectName, ContentSqlDialect } from './types.js';\n\nconst DIALECTS: Readonly<Record<ContentDialectName, ContentSqlDialect>> = {\n postgres: postgresContentDialect,\n sqlite: sqliteContentDialect,\n mysql: mysqlContentDialect,\n};\n\nexport const contentDialectFor = (name: ContentDialectName): ContentSqlDialect => DIALECTS[name];\n","import { currentDialect } from '../../db/dialect.js';\nimport { contentDialectFor } from './dialect/index.js';\nimport type { ContentSqlDialect } from './dialect/types.js';\n\n/**\n * The content dialect of this process's database (`db/dialect.ts`, chosen from `DATABASE_URL`). Every\n * compiler function takes a dialect as an optional last argument that defaults to this one.\n */\nexport const contentDialect = (): ContentSqlDialect => contentDialectFor(currentDialect());\n","import type { FieldDefinition } from '@shapio/schema';\nimport { sql, type RawBuilder } from 'kysely';\nimport { AppError } from '../../helpers/appError.js';\nimport type { FieldMask, Policy, Principal, RowFilter } from '../../permissions/types.js';\nimport { contentDialect } from './currentDialect.js';\nimport type { ContentSqlDialect } from './dialect/types.js';\n\n/**\n * Policies in content queries (ADR 0005): row filters compile to SQL here (and only here); masks decide\n * which fields a caller sees, may filter on and may write. Row-filter conditions are an enumerated set,\n * never free-form expressions.\n */\n\n/** Entry columns row conditions read. `e` is the entries alias of every head query. */\nconst OWNER_COLUMNS = { admin: 'e.created_by_admin_id', appUser: 'e.owner_app_user_id' } as const;\n\nexport const compileRowFilter = (\n filter: RowFilter | null,\n principal: Principal,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null => {\n if (!filter) {\n return null;\n }\n switch (filter.kind) {\n case 'ownedByPrincipal':\n if (principal.kind === 'admin') {\n return sql`${sql.ref(OWNER_COLUMNS.admin)} = ${dialect.uuid(principal.adminUserId)}`;\n }\n if (principal.kind === 'appUser') {\n return sql`${sql.ref(OWNER_COLUMNS.appUser)} = ${dialect.uuid(principal.appUserId)}`;\n }\n // Tokens and anonymous callers own nothing.\n return sql`false`;\n case 'and':\n case 'or': {\n const parts = filter.filters.map((child) => compileRowFilter(child, principal, dialect) ?? sql`true`);\n if (parts.length === 0) {\n return dialect.bool(filter.kind === 'and');\n }\n return sql`(${sql.join(parts, filter.kind === 'and' ? sql` and ` : sql` or `)})`;\n }\n }\n};\n\nexport type EntryOwnership = { created_by_admin_id: string | null; owner_app_user_id: string | null };\n\n/** The same row filter evaluated on one loaded entry (writes check it before touching the entry). */\nexport const matchesRowFilter = (\n filter: RowFilter | null,\n principal: Principal,\n entry: EntryOwnership,\n): boolean => {\n if (!filter) {\n return true;\n }\n switch (filter.kind) {\n case 'ownedByPrincipal':\n return (\n (principal.kind === 'admin' && entry.created_by_admin_id === principal.adminUserId) ||\n (principal.kind === 'appUser' && entry.owner_app_user_id === principal.appUserId)\n );\n case 'and':\n return filter.filters.every((child) => matchesRowFilter(child, principal, entry));\n case 'or':\n return filter.filters.some((child) => matchesRowFilter(child, principal, entry));\n }\n};\n\n/**\n * Whether a mask lets the caller see a field. A wildcard mask covers every live field; deprecated fields\n * stay hidden unless a mask names them explicitly.\n */\nexport const maskAllows = (mask: FieldMask, field: FieldDefinition): boolean =>\n mask.mode === 'all' ? !field.deprecated : mask.fieldIds.includes(field.id);\n\nexport const forbidden = (message: string, details?: Record<string, unknown>) =>\n new AppError(403, 'FORBIDDEN', message, details);\n\n/** Throws 403 unless the policy allows the action. */\nexport const assertAllowed = (policy: Policy, what: string): void => {\n if (!policy.allowed) {\n throw forbidden(`Your role does not allow ${what}`);\n }\n};\n\n/** Writes may only touch fields in the write mask (ADR 0005: 403, never a silent drop). */\nexport const assertWritable = (\n mask: FieldMask,\n fields: readonly FieldDefinition[],\n fieldIds: Iterable<string>,\n) => {\n const byId = new Map(fields.map((field) => [field.id, field]));\n const denied = [...fieldIds].filter((id) => {\n const field = byId.get(id);\n return field !== undefined && !maskAllows(mask, field);\n });\n if (denied.length > 0) {\n throw new AppError(403, 'FORBIDDEN_FIELD', 'Your role may not change some of these fields', {\n fields: denied.map((id) => byId.get(id)?.apiKey ?? id),\n });\n }\n};\n\n/**\n * One condition over the entries row `e` for reads spanning several models (the snapshot diff): an entry\n * passes when its model is readable without a row filter, or its model's row filter holds for it.\n */\nexport const compileModelRowFilters = (\n models: ReadonlyArray<{ modelId: string; rowFilter: RowFilter | null }>,\n principal: Principal,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null => {\n if (models.every((model) => model.rowFilter === null)) {\n return null;\n }\n const open = models.filter((model) => model.rowFilter === null).map((model) => model.modelId);\n const parts = [\n ...(open.length > 0 ? [dialect.oneOf(sql`e.model_id`, open, 'uuid')] : []),\n ...models.flatMap((model) =>\n model.rowFilter === null\n ? []\n : [\n sql`(e.model_id = ${dialect.uuid(model.modelId)} and ${compileRowFilter(model.rowFilter, principal, dialect) ?? sql`true`})`,\n ],\n ),\n ];\n return sql`(${sql.join(parts, sql` or `)})`;\n};\n","import {\n renderRichTextHtml,\n type ComponentDefinition,\n type FieldDefinition,\n type RichTextDocument,\n} from '@shapio/schema';\nimport type { FieldMask } from '../../permissions/types.js';\nimport type { ContentModel } from '../model.js';\nimport { COMPONENT_KEY } from '../validator/index.js';\nimport { maskAllows } from './policy.js';\nimport type { RichTextMode } from './types.js';\n\n/**\n * Storage → API projection with explicit masks (build plan §3.10: generic content routes cannot carry\n * fixed response schemas, so this is where leaks are prevented). Only fields the mask allows, and the\n * caller selected, are emitted; field IDs become API keys; relation IDs are limited to targets the caller\n * may see (published, readable) unless they were populated. Absent values are emitted as null (or [] for\n * lists) so every entry of a model has the same shape.\n */\n\n/** The top-level fields a response carries: readable, live (unless named by the mask) and selected. */\nexport const selectFields = (\n model: ContentModel,\n mask: FieldMask,\n selected: readonly FieldDefinition[] | null,\n): FieldDefinition[] => {\n const allowed = model.definition.fields.filter((field) => maskAllows(mask, field));\n return selected ? allowed.filter((field) => selected.some((choice) => choice.id === field.id)) : allowed;\n};\n\nexport type ProjectionOptions = {\n model: ContentModel;\n fields: readonly FieldDefinition[];\n /** Relation targets the caller may see; null shows every ID (admin reads). */\n visibleTargets: ReadonlySet<string> | null;\n /** Expanded targets: relation field ID → target entry ID → projected entry. */\n populated: ReadonlyMap<string, ReadonlyMap<string, unknown>>;\n /**\n * Delivery reads: the shape of rich-text values (`?richText=`), each also carrying its HTML rendering behind\n * `RICH_TEXT_HTML` for GraphQL's `html` field. Absent: the stored document as is (admin reads, hooks).\n */\n richText?: RichTextMode;\n /** Asset views by ID (media fields become views, missing assets are dropped); null leaves IDs as stored. */\n mediaAssets: ReadonlyMap<string, { url: string }> | null;\n};\n\n/**\n * Renders a delivered rich-text value's HTML on demand (GraphQL's `RichText.html` resolver). A symbol key, so\n * `JSON.stringify` never writes it: REST responses carry `html` only when the request asked for it.\n */\nexport const RICH_TEXT_HTML: unique symbol = Symbol('shapio.richTextHtml');\n\nexport type DeliveredRichText = Record<string, unknown> & { [RICH_TEXT_HTML]?: () => string };\n\nconst projectRichText = (\n document: RichTextDocument,\n mode: RichTextMode,\n options: ProjectionOptions,\n): DeliveredRichText => {\n const render = () => renderRichTextHtml(document, (id) => options.mediaAssets?.get(id)?.url);\n const { doc, ...envelope } = document;\n const shaped: DeliveredRichText =\n mode === 'json'\n ? { ...document }\n : mode === 'html'\n ? { ...envelope, html: render() }\n : { ...envelope, doc, html: render() };\n shaped[RICH_TEXT_HTML] = render;\n return shaped;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isListField = (field: FieldDefinition) =>\n (field.type === 'relation' && field.settings.cardinality === 'many') ||\n (field.type === 'media' && field.settings.multiple) ||\n (field.type === 'component' && field.settings.repeatable) ||\n field.type === 'dynamiczone';\n\nconst projectRelation = (\n field: FieldDefinition & { type: 'relation' },\n value: unknown,\n options: ProjectionOptions,\n topLevel: boolean,\n) => {\n const ids = (Array.isArray(value) ? value : [value]).filter((id): id is string => typeof id === 'string');\n const populated = topLevel ? options.populated.get(field.id) : undefined;\n const shown = ids.flatMap((id) => {\n if (populated) {\n return populated.has(id) ? [populated.get(id)] : [];\n }\n return options.visibleTargets === null || options.visibleTargets.has(id) ? [id] : [];\n });\n return field.settings.cardinality === 'many' ? shown : (shown[0] ?? null);\n};\n\nconst projectComponent = (\n component: ComponentDefinition | undefined,\n value: unknown,\n options: ProjectionOptions,\n): Record<string, unknown> | null =>\n component && isRecord(value)\n ? projectFields(\n component.fields.filter((field) => !field.deprecated),\n value,\n options,\n false,\n )\n : null;\n\nconst isStringOrStrings = (value: unknown) =>\n typeof value === 'string' || (Array.isArray(value) && value.every((item) => typeof item === 'string'));\n\n/**\n * Whether a stored value has the shape the field's current type produces. Old snapshots are served through\n * the current schema (plan developer-face §5): a value written before a type change (text → rich text) is\n * not converted at read time, it is emitted as empty instead of being passed off as the new type.\n */\nconst matchesFieldType = (field: FieldDefinition, value: unknown): boolean => {\n switch (field.type) {\n case 'number':\n case 'integer':\n return typeof value === 'number';\n case 'boolean':\n return typeof value === 'boolean';\n case 'json':\n return true;\n case 'richtext':\n return isRecord(value) && isRecord(value.doc);\n case 'relation':\n case 'media':\n return isStringOrStrings(value);\n case 'enum':\n return field.settings.multiple\n ? Array.isArray(value) && value.every((item) => typeof item === 'string')\n : typeof value === 'string';\n case 'component':\n return field.settings.repeatable ? Array.isArray(value) : isRecord(value);\n case 'dynamiczone':\n return Array.isArray(value);\n default:\n // string, text, slug, email, url, uid, date, datetime, time, decimal, biginteger\n return typeof value === 'string';\n }\n};\n\nconst projectValue = (\n field: FieldDefinition,\n value: unknown,\n options: ProjectionOptions,\n topLevel: boolean,\n): unknown => {\n if (value === undefined || value === null || !matchesFieldType(field, value)) {\n return isListField(field) ? [] : null;\n }\n switch (field.type) {\n case 'relation':\n return projectRelation(field, value, options, topLevel);\n case 'component': {\n const component = options.model.components.get(field.settings.component)?.definition;\n return field.settings.repeatable\n ? (Array.isArray(value) ? value : []).map((item) => projectComponent(component, item, options))\n : projectComponent(component, value, options);\n }\n case 'dynamiczone':\n return (Array.isArray(value) ? value : []).flatMap((item) => {\n const component = isRecord(item)\n ? options.model.components.get(String(item[COMPONENT_KEY]))?.definition\n : undefined;\n const projected = projectComponent(component, item, options);\n return component && projected ? [{ [COMPONENT_KEY]: component.apiKey, ...projected }] : [];\n });\n case 'richtext':\n return options.richText ? projectRichText(value as RichTextDocument, options.richText, options) : value;\n case 'media': {\n if (!options.mediaAssets) {\n return value;\n }\n const assets = (Array.isArray(value) ? value : [value]).flatMap((id) => {\n const asset = typeof id === 'string' ? options.mediaAssets?.get(id) : undefined;\n return asset ? [asset] : [];\n });\n return field.settings.multiple ? assets : (assets[0] ?? null);\n }\n default:\n return value;\n }\n};\n\nfunction projectFields(\n fields: readonly FieldDefinition[],\n data: Readonly<Record<string, unknown>>,\n options: ProjectionOptions,\n topLevel: boolean,\n): Record<string, unknown> {\n const output: Record<string, unknown> = {};\n for (const field of fields) {\n output[field.apiKey] = projectValue(field, data[field.id], options, topLevel);\n }\n return output;\n}\n\n/** A head's data as the API returns it. */\nexport const projectData = (data: Readonly<Record<string, unknown>>, options: ProjectionOptions) =>\n projectFields(options.fields, data, options, true);\n","import type { FieldDefinition } from '@shapio/schema';\nimport { selectFields } from '../content/compiler/select.js';\nimport type { PopulateTree } from '../content/compiler/types.js';\nimport { resolveModelById, type ContentModel } from '../content/model.js';\nimport type { Policy } from '../permissions/types.js';\nimport type { SchemaSnapshot } from '../schema/snapshot.js';\nimport type { FieldRead } from './aggregator.js';\nimport { relationFieldPath } from './keys.js';\n\n/**\n * The fields a REST delivery response carried, as usage reads: the projected top-level fields (`explicit`\n * when the request named them with `fields`, `implicit` when it took the whole model) and, one level deep,\n * the fields of populated relation targets (always `implicit`: populate expands whole targets).\n */\nexport type RestReadShape = {\n snapshot: SchemaSnapshot;\n model: ContentModel;\n policy: Policy;\n query: { fields: readonly FieldDefinition[] | null; populate: PopulateTree };\n /** The caller's read policy for a relation's target model. */\n policyFor: (modelId: string) => Promise<Policy>;\n};\n\nexport const restFieldReads = async ({\n snapshot,\n model,\n policy,\n query,\n policyFor,\n}: RestReadShape): Promise<FieldRead[]> => {\n const fields = selectFields(model, policy.readMask, query.fields);\n const selection = query.fields === null ? 'implicit' : 'explicit';\n const reads: FieldRead[] = fields.map((field) => ({ path: field.id, selection }));\n for (const field of fields) {\n if (field.type !== 'relation' || !query.populate.has(field.id)) {\n continue;\n }\n const target = resolveModelById(snapshot, field.settings.target);\n if (!target) {\n continue;\n }\n const targetPolicy = await policyFor(target.definition.id);\n if (!targetPolicy.allowed) {\n continue;\n }\n for (const targetField of selectFields(target, targetPolicy.readMask, null)) {\n reads.push({ path: relationFieldPath(field.id, targetField.id), selection: 'implicit' });\n }\n }\n return reads;\n};\n","import { createHash } from 'node:crypto';\nimport { isStableId, SCALAR_DATA_TYPES, type DataType, type FieldDefinition } from '@shapio/schema';\nimport { sql, type RawBuilder } from 'kysely';\nimport { contentDialect } from './currentDialect.js';\nimport type {\n ContentSqlDialect,\n EqualityTarget,\n FieldIndexDefinition,\n TextMatchOperator,\n ValueCast,\n} from './dialect/types.js';\nimport { CONTENT_HEADS_COLUMNS, CONTENT_HEADS_TABLE } from './heads.js';\n\n/**\n * The single source of SQL expressions over content values (ADR 0001). The query compiler (package E)\n * and the index builder (schema planner) both call these functions, so an expression index matches the\n * filter or sort that should use it byte for byte. Package E extends this module; never duplicate it.\n *\n * Rules (build plan §3.1):\n * - date, datetime and time are canonical UTC ISO-8601 text: compared and sorted as text, because\n * timestamp casts are not IMMUTABLE and cannot be indexed;\n * - decimal and biginteger are JSON strings cast to numeric; number and integer are JSON numbers cast\n * to numeric (text→numeric is IMMUTABLE, so it can be indexed);\n * - booleans cast to boolean; every other scalar is the raw text value.\n *\n * The spelling of each expression comes from the content dialect (`dialect/`): the same functions build\n * the query and the index on PostgreSQL, and the same functions build them on SQLite.\n *\n * Field IDs are embedded as SQL literals, never parameters, because a parameterised expression cannot\n * match an index expression. That is safe only because field IDs come from the registry and are checked\n * to be UUIDs here; nothing from a request ever reaches these functions unvalidated.\n */\n\nexport { CONTENT_HEADS_COLUMNS, CONTENT_HEADS_TABLE } from './heads.js';\nexport type { ValueCast } from './dialect/types.js';\n\n/** Bump when any expression below changes: index names change with it, so old indexes are rebuilt. */\nexport const EXPRESSION_VERSION = 1;\n\n/**\n * Column layout of field indexes. 1: (locale, state, value). 2 (sites plan §H): `site_id` leads, because\n * every content query names its site. The layout is part of the index name, so a new layout means new\n * indexes; the `fieldIndexLayout` job builds the current layout and drops the old one.\n */\nexport type FieldIndexLayout = 1 | 2;\nexport const FIELD_INDEX_LAYOUT: FieldIndexLayout = 2;\n\nconst NUMERIC_TYPES: ReadonlySet<DataType> = new Set(['number', 'integer', 'decimal', 'biginteger']);\n/** Numbers stored as JSON strings (exact decimals, big integers). */\nconst NUMERIC_STRING_TYPES: ReadonlySet<DataType> = new Set(['decimal', 'biginteger']);\n\n/**\n * Whether a field has a B-tree expression index: filterable or sortable, and not deprecated. The schema\n * planner builds an index for exactly these fields (`indexStepsOf`), and PostgreSQL equality compares on\n * the index expression for them (`EqualityTarget.indexed`).\n */\nexport const hasFieldIndex = (\n field: Pick<FieldDefinition, 'filterable' | 'sortable' | 'deprecated'>,\n): boolean => (field.filterable || field.sortable) && !field.deprecated;\n\nexport const valueCastFor = (type: DataType): ValueCast => {\n if (NUMERIC_TYPES.has(type)) {\n return 'numeric';\n }\n return type === 'boolean' ? 'boolean' : 'text';\n};\n\nexport class UnsafeIdentifierError extends Error {\n constructor(what: string) {\n super(`Refusing to build SQL with ${what}: not a stable ID`);\n this.name = 'UnsafeIdentifierError';\n }\n}\n\nconst assertStableId = (value: string, what: string) => {\n if (!isStableId(value)) {\n throw new UnsafeIdentifierError(what);\n }\n};\n\n/**\n * The expression for one top-level scalar field of a head's `data`, e.g. `((data ->> '0b1c…')::numeric)`\n * on PostgreSQL. Throws for non-scalar types or a malformed field ID.\n */\nexport const fieldValueExpression = (\n fieldId: string,\n type: DataType,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n if (!SCALAR_DATA_TYPES.has(type)) {\n throw new Error(`${type} fields have no scalar expression`);\n }\n return dialect.fieldValue(fieldId, valueCastFor(type), NUMERIC_STRING_TYPES.has(type));\n};\n\n/** `data -> 'fieldId'`: the raw JSON value of a top-level field (any type). */\nexport const fieldJsonExpression = (\n fieldId: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.fieldJson(fieldId);\n};\n\n/** `data ->> 'fieldId'`: a top-level field as text, uncast. */\nexport const fieldTextExpression = (\n fieldId: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.fieldText(fieldId);\n};\n\n/**\n * Equality on a top-level field (lists: \"contains\"). PostgreSQL compares the field's index expression when\n * the field has one (`EqualityTarget.indexed`) and emits containment, which its GIN index serves, otherwise;\n * SQLite and MySQL compare the field's index expression.\n */\nexport const fieldEqualsExpression = (\n target: EqualityTarget,\n value: unknown,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(target.fieldId, 'a field ID');\n return dialect.fieldEquals(target, value);\n};\n\n/** The field's value is missing: absent, null, \"\" or []. */\nexport const fieldMissingExpression = (\n fieldId: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.fieldMissing(fieldId);\n};\n\n/** Case-sensitive text match on a top-level field; `text` is a parameter, never SQL. */\nexport const textMatchExpression = (\n fieldId: string,\n operator: TextMatchOperator,\n text: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.textMatch(fieldId, operator, text);\n};\n\n/** Case-insensitive contains on a top-level field (`$containsi`, search). */\nexport const containsInsensitiveExpression = (\n fieldId: string,\n text: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertStableId(fieldId, 'a field ID');\n return dialect.containsInsensitive(fieldId, text);\n};\n\n/**\n * The model ID as a SQL literal for `model_id = '…'`. Expression indexes are partial on the model, and\n * the planner can only prove a partial index's predicate against a literal, never a parameter.\n */\nexport const modelIdLiteral = (modelId: string): RawBuilder<unknown> => {\n assertStableId(modelId, 'a model ID');\n return sql.lit(modelId);\n};\n\n/** A comparison value cast the way `fieldValueExpression` casts the stored value, so indexes match. */\nexport const castParameter = (\n value: unknown,\n type: DataType,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => dialect.castParameter(value, valueCastFor(type), NUMERIC_STRING_TYPES.has(type));\n\nexport type FieldIndexSpec = {\n modelId: string;\n fieldId: string;\n type: DataType;\n /**\n * Whether the model is localized. Localized models' queries always name a locale, so their indexes lead\n * with it; non-localized models' heads are read without a locale condition, so theirs omit it. Absent\n * means localized (the layout of indexes planned before this flag existed).\n */\n localized?: boolean;\n};\n\n/**\n * Deterministic index name: a hash of the model ID, field ID, value cast, column layout and expression\n * version, so the same field with the same expression and layout always maps to the same index, and a\n * changed one to a new one. 63 bytes is PostgreSQL's identifier limit; this is 3 + 32. `layoutVersion` 1\n * gives the names indexes had before sites (to drop them). A database has one dialect, so the name does\n * not depend on it.\n */\nexport const fieldIndexName = (\n { modelId, fieldId, type, localized = true }: FieldIndexSpec,\n layoutVersion: FieldIndexLayout = FIELD_INDEX_LAYOUT,\n): string => {\n const layout = `${localized ? '' : ':shared'}${layoutVersion === 1 ? '' : `:l${layoutVersion}`}`;\n const digest = createHash('sha256')\n .update(`${modelId}:${fieldId}:${valueCastFor(type)}:v${EXPRESSION_VERSION}${layout}`)\n .digest('hex');\n return `eh_${digest.slice(0, 32)}`;\n};\n\nconst INDEX_NAME_PATTERN = /^eh_([0-9a-f]{32})$/;\n\nconst assertIndexName = (indexName: string): string => {\n const match = INDEX_NAME_PATTERN.exec(indexName);\n if (!match) {\n throw new UnsafeIdentifierError('an index name');\n }\n return match[1] as string;\n};\n\n/**\n * The extended-statistics object that goes with a field index (same hash, `es_` prefix). PostgreSQL keeps\n * no expression statistics for *partial* indexes, so without it every range filter is estimated at a third\n * of the table and the planner ignores the field index.\n */\nexport const fieldStatisticsName = (indexName: string): string => `es_${assertIndexName(indexName)}`;\n\n/**\n * The field index over the model's heads only: (site, locale, state, value) for localized models, (site,\n * state, value) otherwise; the site leads because every query names one (the compiler's base condition).\n * PostgreSQL builds it `CONCURRENTLY` (outside a transaction); SQLite with a plain `CREATE INDEX`, which\n * holds the write lock while it builds. One index serves equality, ranges and sorts on SQLite.\n */\n/** The parts of a field index the dialect spells (`createFieldIndexStatement`). */\nconst fieldIndexDefinition = (spec: FieldIndexSpec, dialect: ContentSqlDialect): FieldIndexDefinition => {\n assertStableId(spec.modelId, 'a model ID');\n const { siteId, modelId, locale, state } = CONTENT_HEADS_COLUMNS;\n const leading =\n spec.localized === false\n ? sql`${sql.ref(siteId)}, ${sql.ref(state)}`\n : sql`${sql.ref(siteId)}, ${sql.ref(locale)}, ${sql.ref(state)}`;\n return {\n name: fieldIndexName(spec),\n table: CONTENT_HEADS_TABLE,\n leading,\n expression: fieldValueExpression(spec.fieldId, spec.type, dialect),\n modelIdColumn: modelId,\n modelId: sql.lit(spec.modelId),\n localized: spec.localized !== false,\n cast: valueCastFor(spec.type),\n numericString: NUMERIC_STRING_TYPES.has(spec.type),\n };\n};\n\nexport const createFieldIndexStatement = (\n spec: FieldIndexSpec,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => dialect.createFieldIndex(fieldIndexDefinition(spec, dialect));\n\n/**\n * Adds the generated column a field index is built on, where the dialect indexes one (MySQL); null\n * elsewhere. Run before `createFieldIndexStatement`.\n */\nexport const createFieldIndexColumnStatement = (\n spec: FieldIndexSpec,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null =>\n dialect.fieldIndexColumn ? dialect.fieldIndexColumn.add(fieldIndexDefinition(spec, dialect)) : null;\n\n/** Drops a field index's generated column (MySQL); null elsewhere. Run after `dropIndexStatement`. */\nexport const dropFieldIndexColumnStatement = (\n indexName: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null => {\n assertIndexName(indexName);\n return dialect.fieldIndexColumn ? dialect.fieldIndexColumn.drop(indexName) : null;\n};\n\n/**\n * `CREATE STATISTICS` on the field's value expression (the same expression the index and queries use);\n * null where the database has no extended statistics (SQLite).\n */\nexport const createFieldStatisticsStatement = (\n spec: FieldIndexSpec,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null =>\n dialect.createFieldStatistics(\n fieldStatisticsName(fieldIndexName(spec)),\n fieldValueExpression(spec.fieldId, spec.type, dialect),\n CONTENT_HEADS_TABLE,\n );\n\n/** Drops a field index (`CONCURRENTLY` on PostgreSQL); names only ever come from `fieldIndexName`. */\nexport const dropIndexStatement = (\n indexName: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n assertIndexName(indexName);\n return dialect.dropFieldIndex(indexName);\n};\n\n/** Drops the statistics object of a field index (`fieldStatisticsName`); null where there is none. */\nexport const dropStatisticsStatement = (\n indexName: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> | null => dialect.dropFieldStatistics(fieldStatisticsName(indexName));\n\n/** Refreshes planner statistics of the heads table (after an index and its statistics object are built). */\nexport const analyzeHeadsStatement = (dialect: ContentSqlDialect = contentDialect()): RawBuilder<unknown> =>\n dialect.analyze(CONTENT_HEADS_TABLE);\n","import { isStableId, type DataType, type FieldDefinition } from '@shapio/schema';\nimport {\n canonicalBigInteger,\n canonicalDate,\n canonicalDatetime,\n canonicalDecimal,\n canonicalTime,\n} from '../validator/scalars.js';\nimport { queryInvalid, type FilterOperator, type FilterTarget } from './types.js';\n\n/**\n * Which operators each data type accepts, and how query text becomes a typed value (ADR 0001):\n * - equality (`$eq $ne $in $nin`) works on every filterable type. On PostgreSQL it compiles to `data @> …`,\n * which the GIN index serves, so no per-field index is needed; a field that has its own index (filterable or\n * sortable) compares on that index's expression instead, which is far cheaper on unselective values. On\n * list values (multiple enums, `many` relations, multiple media) `$eq x` means \"contains x\".\n * - `$null`/`$notNull` test for a missing value (absent, null, \"\" or []).\n * - ranges (`$lt $lte $gt $gte`) need an expression index, so only fields marked filterable or sortable\n * accept them; text matches (`$contains`…) are unindexed scans and need `filterable`.\n * Values are canonicalized exactly like stored values, so `2026-10-01T10:00:00+02:00` finds\n * `2026-10-01T08:00:00.000Z`.\n */\n\ntype TypeFamily =\n 'text' | 'temporal' | 'number' | 'numericString' | 'boolean' | 'list' | 'reference' | 'none';\n\nconst familyOf = (field: FieldDefinition): TypeFamily => {\n switch (field.type) {\n case 'string':\n case 'text':\n case 'slug':\n case 'email':\n case 'url':\n case 'uid':\n return 'text';\n case 'enum':\n return field.settings.multiple ? 'list' : 'text';\n case 'date':\n case 'datetime':\n case 'time':\n return 'temporal';\n case 'number':\n case 'integer':\n return 'number';\n case 'decimal':\n case 'biginteger':\n return 'numericString';\n case 'boolean':\n return 'boolean';\n case 'relation':\n case 'media':\n return 'reference';\n default:\n return 'none';\n }\n};\n\nconst EQUALITY: readonly FilterOperator[] = ['$eq', '$ne', '$in', '$nin', '$null', '$notNull'];\nconst RANGE: readonly FilterOperator[] = ['$lt', '$lte', '$gt', '$gte'];\nconst TEXT_MATCH: readonly FilterOperator[] = [\n '$contains',\n '$notContains',\n '$containsi',\n '$startsWith',\n '$endsWith',\n];\n\nexport const OPERATORS_BY_FAMILY: Readonly<Record<TypeFamily, readonly FilterOperator[]>> = {\n text: [...EQUALITY, ...RANGE, ...TEXT_MATCH],\n temporal: [...EQUALITY, ...RANGE],\n number: [...EQUALITY, ...RANGE],\n numericString: [...EQUALITY, ...RANGE],\n boolean: ['$eq', '$ne', '$null', '$notNull'],\n list: EQUALITY,\n reference: EQUALITY,\n none: [],\n};\n\nexport const isRangeOperator = (operator: FilterOperator) => RANGE.includes(operator);\nexport const isTextMatchOperator = (operator: FilterOperator) => TEXT_MATCH.includes(operator);\nexport const isListOperator = (operator: FilterOperator) => operator === '$in' || operator === '$nin';\nexport const isNullOperator = (operator: FilterOperator) => operator === '$null' || operator === '$notNull';\n\n/** Whether the field holds a list (equality means \"contains\"). */\nexport const isListValued = (field: FieldDefinition): boolean =>\n (field.type === 'enum' && field.settings.multiple) ||\n (field.type === 'relation' && field.settings.cardinality === 'many') ||\n (field.type === 'media' && field.settings.multiple);\n\n/** Types whose stored JSON string is not unique per value, so equality compares numerically. */\nexport const NUMERIC_STRING_TYPES: ReadonlySet<DataType> = new Set(['decimal', 'biginteger']);\n\nconst SYSTEM_OPERATORS: Readonly<Record<string, readonly FilterOperator[]>> = {\n id: ['$eq', '$ne', '$in', '$nin'],\n createdAt: ['$eq', '$ne', '$in', '$nin', ...RANGE],\n updatedAt: ['$eq', '$ne', '$in', '$nin', ...RANGE],\n};\n\n/** Throws 400 unless the operator is allowed for the target. */\nexport const assertOperatorAllowed = (target: FilterTarget, operator: FilterOperator, label: string) => {\n if (target.kind === 'system') {\n if (!(SYSTEM_OPERATORS[target.name] ?? []).includes(operator)) {\n throw queryInvalid(`Operator ${operator} is not supported on \"${label}\"`);\n }\n return;\n }\n const { field } = target;\n if (!OPERATORS_BY_FAMILY[familyOf(field)].includes(operator)) {\n throw queryInvalid(`Operator ${operator} is not supported on \"${label}\" (${field.type})`);\n }\n if (isRangeOperator(operator) && !field.filterable && !field.sortable) {\n throw queryInvalid(`\"${label}\" is not filterable: mark it filterable to use ${operator}`);\n }\n if (isTextMatchOperator(operator) && !field.filterable) {\n throw queryInvalid(`\"${label}\" is not filterable: mark it filterable to use ${operator}`);\n }\n};\n\n/** Whether a field can be sorted on: an expression index exists only for sortable/filterable scalars. */\nexport const isSortable = (field: FieldDefinition) =>\n field.sortable &&\n familyOf(field) !== 'none' &&\n familyOf(field) !== 'list' &&\n familyOf(field) !== 'reference';\n\nconst coerceBoolean = (raw: unknown, label: string): boolean => {\n if (raw === true || raw === 'true') {\n return true;\n }\n if (raw === false || raw === 'false') {\n return false;\n }\n throw queryInvalid(`\"${label}\" needs true or false`);\n};\n\nconst coerceText = (raw: unknown, label: string): string => {\n if (typeof raw !== 'string') {\n throw queryInvalid(`\"${label}\" needs a single text value`);\n }\n return raw;\n};\n\nconst coerceOrFail = <T>(value: T | undefined, label: string, expected: string): T => {\n if (value === undefined) {\n throw queryInvalid(`\"${label}\" needs ${expected}`);\n }\n return value;\n};\n\n/** One comparison value for a field, canonicalized like stored values. */\nconst coerceFieldValue = (field: FieldDefinition, raw: unknown, label: string): unknown => {\n switch (familyOf(field)) {\n case 'number': {\n const number = Number(coerceText(raw, label));\n if (!Number.isFinite(number) || (raw as string).trim() === '') {\n throw queryInvalid(`\"${label}\" needs a number`);\n }\n return number;\n }\n case 'numericString':\n return coerceOrFail(\n field.type === 'decimal'\n ? canonicalDecimal(coerceText(raw, label))\n : canonicalBigInteger(coerceText(raw, label)),\n label,\n 'a number',\n );\n case 'temporal': {\n const text = coerceText(raw, label);\n const canonical =\n field.type === 'date'\n ? canonicalDate(text)\n : field.type === 'datetime'\n ? canonicalDatetime(text)\n : canonicalTime(text);\n return coerceOrFail(canonical, label, `a valid ${field.type}`);\n }\n case 'boolean':\n return coerceBoolean(raw, label);\n case 'reference': {\n const id = coerceText(raw, label);\n if (!isStableId(id)) {\n throw queryInvalid(`\"${label}\" needs an ID`);\n }\n return id;\n }\n default:\n return coerceText(raw, label);\n }\n};\n\nconst coerceSystemValue = (name: string, raw: unknown, label: string): unknown => {\n const text = coerceText(raw, label);\n if (name === 'id') {\n if (!isStableId(text)) {\n throw queryInvalid(`\"${label}\" needs an entry ID`);\n }\n return text;\n }\n return coerceOrFail(canonicalDatetime(text), label, 'an ISO-8601 date-time with a time zone');\n};\n\n/** Turns the raw operand of `operator` into typed value(s); lists for `$in`/`$nin`, a boolean for `$null`. */\nexport const coerceOperand = (\n target: FilterTarget,\n operator: FilterOperator,\n raw: unknown,\n label: string,\n maxListLength: number,\n): unknown => {\n if (isNullOperator(operator)) {\n return coerceBoolean(raw, label);\n }\n const coerceOne = (value: unknown) =>\n target.kind === 'system'\n ? coerceSystemValue(target.name, value, label)\n : coerceFieldValue(target.field, value, label);\n if (isListOperator(operator)) {\n if (!Array.isArray(raw)) {\n throw queryInvalid(`\"${label}\" ${operator} needs a list`);\n }\n if (raw.length > maxListLength) {\n throw queryInvalid(`\"${label}\" ${operator} accepts at most ${maxListLength} values`);\n }\n return raw.map(coerceOne);\n }\n if (isTextMatchOperator(operator)) {\n const text = coerceText(raw, label);\n if (text.length === 0) {\n throw queryInvalid(`\"${label}\" ${operator} needs a non-empty value`);\n }\n return text;\n }\n return coerceOne(raw);\n};\n\n/** Every operator a field accepts right now (documentation and generated filter types). */\nexport const operatorsFor = (field: FieldDefinition): FilterOperator[] =>\n OPERATORS_BY_FAMILY[familyOf(field)].filter(\n (operator) =>\n !(isRangeOperator(operator) && !field.filterable && !field.sortable) &&\n !(isTextMatchOperator(operator) && !field.filterable),\n );\n","import { SCALAR_DATA_TYPES } from '@shapio/schema';\nimport { sql, type RawBuilder } from 'kysely';\nimport type { HeadState } from '../model.js';\nimport { contentDialect } from './currentDialect.js';\nimport type { ContentSqlDialect, EqualityTarget } from './dialect/types.js';\nimport {\n castParameter,\n containsInsensitiveExpression,\n fieldEqualsExpression,\n fieldMissingExpression,\n fieldValueExpression,\n hasFieldIndex,\n modelIdLiteral,\n textMatchExpression,\n valueCastFor,\n} from './expressions.js';\nimport { isListValued, NUMERIC_STRING_TYPES } from './operators.js';\nimport type { EntryListStatus, FilterNode, FilterOperator, FilterTarget } from './types.js';\n\n/**\n * AST → parameterised SQL (Kysely `sql` fragments only). Every value is a bound parameter; the only\n * identifiers are fixed column names and stable field/model IDs that come from the registry and are checked\n * by `expressions.ts`. Head queries alias `entry_heads` (or a snapshot of it) as `h` and `entries` as `e`.\n * Database-specific spelling comes from the content dialect (`dialect/`), passed as the optional last\n * argument of every exported function (the process's dialect by default).\n */\n\n/** Where heads come from: live heads in one state, or published revisions as of a publication sequence. */\nexport type HeadSource = { kind: 'heads'; state: HeadState } | { kind: 'snapshot'; seq: number };\n\n/** Which locale's head serves each entry. */\nexport type LocaleScope =\n /** Non-localized models: each entry has heads in exactly one locale. */\n | { kind: 'any' }\n /** The first locale of the chain that has a head serves the entry (fallback, ADR 0004). */\n | { kind: 'chain'; chain: readonly string[] };\n\nconst RANGE_SQL: Readonly<Partial<Record<FilterOperator, RawBuilder<unknown>>>> = {\n $lt: sql`<`,\n $lte: sql`<=`,\n $gt: sql`>`,\n $gte: sql`>=`,\n $eq: sql`=`,\n $ne: sql`<>`,\n};\n\nconst anyOf = (parts: RawBuilder<unknown>[]): RawBuilder<unknown> =>\n parts.length === 0 ? sql`false` : sql`(${sql.join(parts, sql` or `)})`;\n\ntype FieldTarget = Extract<FilterTarget, { kind: 'field' }>;\n\nconst equalityTarget = ({ field }: FieldTarget): EqualityTarget => {\n const cast = SCALAR_DATA_TYPES.has(field.type) ? valueCastFor(field.type) : null;\n const list = isListValued(field);\n return {\n fieldId: field.id,\n cast,\n list,\n numericString: NUMERIC_STRING_TYPES.has(field.type),\n indexed: cast !== null && !list && hasFieldIndex(field),\n };\n};\n\n/** System filter columns. `createdAt` is the entry's, copied on every head (`h.entry_created_at`). */\nconst SYSTEM_COLUMNS = {\n id: 'h.entry_id',\n createdAt: 'h.entry_created_at',\n updatedAt: 'h.updated_at',\n} as const;\n\n/** Whether a filter names a column of `entries` (the head query's `e`), so its COUNT must join entries. */\nexport const filterReadsEntries = (node: FilterNode): boolean => {\n switch (node.kind) {\n case 'and':\n case 'or':\n return node.nodes.some(filterReadsEntries);\n case 'not':\n return filterReadsEntries(node.node);\n case 'condition':\n return node.target.kind === 'system' && SYSTEM_COLUMNS[node.target.name].startsWith('e.');\n }\n};\n\nconst systemCondition = (\n name: keyof typeof SYSTEM_COLUMNS,\n operator: FilterOperator,\n value: unknown,\n dialect: ContentSqlDialect,\n): RawBuilder<unknown> => {\n const column = sql.ref(SYSTEM_COLUMNS[name]);\n const element = name === 'id' ? 'uuid' : 'timestamp';\n switch (operator) {\n case '$in':\n return sql`(${dialect.oneOf(column, value as string[], element)})`;\n case '$nin':\n return sql`(not (${dialect.oneOf(column, value as string[], element)}))`;\n default: {\n const parameter = name === 'id' ? dialect.uuid(value as string) : dialect.timestamp(value as string);\n return sql`(${column} ${RANGE_SQL[operator] ?? sql`=`} ${parameter})`;\n }\n }\n};\n\nconst fieldCondition = (\n target: FieldTarget,\n operator: FilterOperator,\n value: unknown,\n dialect: ContentSqlDialect,\n): RawBuilder<unknown> => {\n const { field } = target;\n const equals = (item: unknown) => fieldEqualsExpression(equalityTarget(target), item, dialect);\n switch (operator) {\n case '$eq':\n return equals(value);\n case '$ne':\n return sql`(not ${equals(value)})`;\n case '$in':\n return anyOf((value as unknown[]).map(equals));\n case '$nin':\n return sql`(not ${anyOf((value as unknown[]).map(equals))})`;\n case '$null':\n case '$notNull': {\n const missing = fieldMissingExpression(field.id, dialect);\n return (operator === '$null') === value ? missing : sql`(not ${missing})`;\n }\n case '$lt':\n case '$lte':\n case '$gt':\n case '$gte':\n return sql`(${fieldValueExpression(field.id, field.type, dialect)} ${RANGE_SQL[operator]} ${castParameter(value, field.type, dialect)})`;\n case '$contains':\n case '$startsWith':\n case '$endsWith':\n case '$notContains':\n return textMatchExpression(field.id, operator, value as string, dialect);\n case '$containsi':\n return containsInsensitiveExpression(field.id, value as string, dialect);\n }\n};\n\n/** A filter AST as one boolean SQL expression. */\nexport const compileFilter = (\n node: FilterNode,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => {\n const compileChild = (child: FilterNode) => compileFilter(child, dialect);\n switch (node.kind) {\n case 'and':\n return sql`(${sql.join(node.nodes.map(compileChild), sql` and `)})`;\n case 'or':\n return sql`(${sql.join(node.nodes.map(compileChild), sql` or `)})`;\n case 'not':\n return sql`(not ${compileChild(node.node)})`;\n case 'condition':\n return node.target.kind === 'system'\n ? systemCondition(node.target.name, node.operator, node.value, dialect)\n : fieldCondition(node.target, node.operator, node.value, dialect);\n }\n};\n\n/**\n * Admin list status of the served draft head `h`, against the published head of its locale (the same rule as\n * `statusOf`): no published head = draft; the same revision and no autosaved work = published; else modified.\n * Served by the entry_heads primary key.\n */\nexport const compileStatusCondition = (status: EntryListStatus): RawBuilder<unknown> => {\n const published = sql`select 1 from entry_heads p\n where p.entry_id = h.entry_id and p.locale = h.locale and p.state = 'published'`;\n switch (status) {\n case 'draft':\n return sql`not exists (${published})`;\n case 'published':\n return sql`(h.autosaved_at is null and exists (${published} and p.revision_id = h.revision_id))`;\n case 'modified':\n return sql`exists (${published} and (p.revision_id <> h.revision_id or h.autosaved_at is not null))`;\n }\n};\n\n/** Entries an admin user created (admin list `?author=`). */\nexport const compileAuthorCondition = (\n adminUserId: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => sql`e.created_by_admin_id = ${dialect.uuid(adminUserId)}`;\n\n/** Case-insensitive search of one text field. */\nexport const compileSearch = (\n fieldId: string,\n text: string,\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown> => containsInsensitiveExpression(fieldId, text, dialect);\n\nconst SNAPSHOT_CTE = 'content_snapshot';\n\n/** The FROM item for a source, aliased `alias`, and the CTE it needs (snapshots only). */\nconst sourceOf = (source: HeadSource, modelId: string, siteId: string, dialect: ContentSqlDialect) => {\n if (source.kind === 'heads') {\n return { cte: null, table: sql.table('entry_heads') };\n }\n const cte = dialect.snapshotCte({\n name: SNAPSHOT_CTE,\n siteId,\n modelId: modelIdLiteral(modelId),\n seq: source.seq,\n });\n return { cte, table: sql.id(SNAPSHOT_CTE) };\n};\n\nexport type HeadQueryPlan = {\n /**\n * The site whose content is read (sites plan §H). A tenancy boundary, always applied, separate from the\n * ownership row filter in `conditions`. It comes from the service context, never from the request.\n */\n siteId: string;\n modelId: string;\n source: HeadSource;\n locales: LocaleScope;\n /** Extra conditions (filters, search, row filter, ID restriction), all ANDed. */\n conditions: readonly RawBuilder<unknown>[];\n /**\n * Whether any of `conditions` names an `entries` column (`e`: ownership row filters, the admin author filter,\n * `createdAt` filters). Unknown when absent: assumed whenever there are conditions.\n */\n conditionsReadEntries?: boolean;\n orderBy: readonly RawBuilder<unknown>[];\n limit?: number;\n offset?: number;\n};\n\nconst localeConditions = (\n scope: LocaleScope,\n table: RawBuilder<unknown>,\n source: HeadSource,\n dialect: ContentSqlDialect,\n) => {\n if (scope.kind === 'any') {\n return [];\n }\n if (scope.chain.length === 1) {\n return [sql`h.locale = ${scope.chain[0]}`];\n }\n const chain = [...scope.chain];\n const stateMatch = source.kind === 'heads' ? sql` and h2.state = h.state` : sql``;\n return [\n dialect.oneOf(sql`h.locale`, chain, 'text'),\n // The first locale in the chain that has a head serves the entry; later ones are fallbacks.\n sql`not exists (select 1 from ${table} h2 where h2.entry_id = h.entry_id${stateMatch}\n and ${dialect.oneOf(sql`h2.locale`, chain, 'text')}\n and ${dialect.localeRank(chain, sql`h2.locale`)} < ${dialect.localeRank(chain, sql`h.locale`)})`,\n ];\n};\n\nconst whereOf = (plan: HeadQueryPlan, table: RawBuilder<unknown>, dialect: ContentSqlDialect) => {\n const conditions = [\n sql`h.site_id = ${dialect.uuid(plan.siteId)}`,\n sql`h.model_id = ${modelIdLiteral(plan.modelId)}`,\n ...(plan.source.kind === 'heads' ? [sql`h.state = ${plan.source.state}`] : []),\n ...localeConditions(plan.locales, table, plan.source, dialect),\n ...plan.conditions,\n ];\n return sql.join(conditions, sql` and `);\n};\n\n/** Columns every head query returns. */\nexport type HeadRow = {\n entry_id: string;\n locale: string;\n data: Record<string, unknown>;\n version: number;\n revision_id: string;\n updated_at: Date;\n autosaved_at: Date | null;\n created_at: Date;\n entry_updated_at: Date;\n created_by_admin_id: string | null;\n owner_app_user_id: string | null;\n};\n\n/**\n * The COUNT's FROM. Live heads exist only for live entries (deleting an entry removes its heads in the same\n * transaction; entries soft-deleted for having no head have none; `headLiveEntry.int.test.ts` pins it), so a\n * count of heads whose conditions name no `e` column needs no join: it is read from the heads' indexes alone\n * (the model index, or a field index for an indexed filter). A snapshot keeps the join: the publication log\n * still lists entries deleted since.\n */\nconst countFrom = (\n plan: HeadQueryPlan,\n table: RawBuilder<unknown>,\n where: RawBuilder<unknown>,\n joined: RawBuilder<unknown>,\n): RawBuilder<unknown> =>\n plan.source.kind === 'heads' && !(plan.conditionsReadEntries ?? plan.conditions.length > 0)\n ? sql`from ${table} h where ${where}`\n : joined;\n\nconst HEAD_COLUMNS = sql`h.entry_id, h.locale, h.data, h.version, h.revision_id, h.updated_at,\n h.autosaved_at, e.created_at, e.updated_at as entry_updated_at, e.created_by_admin_id, e.owner_app_user_id`;\n\n/** The parts every statement over a plan's heads shares. */\nconst headQueryParts = (plan: HeadQueryPlan, dialect: ContentSqlDialect) => {\n const { cte, table } = sourceOf(plan.source, plan.modelId, plan.siteId, dialect);\n const where = whereOf(plan, table, dialect);\n const from = sql`from ${table} h join entries e on e.id = h.entry_id and e.deleted_at is null where ${where}`;\n const orderBy = plan.orderBy.length > 0 ? sql` order by ${sql.join([...plan.orderBy])}` : sql``;\n const limit = plan.limit !== undefined ? sql` limit ${plan.limit}` : sql``;\n const offset = plan.offset ? sql` offset ${plan.offset}` : sql``;\n return {\n prefix: cte ?? sql``,\n rows: sql`${from}${orderBy}${limit}${offset}`,\n countFrom: countFrom(plan, table, where, from),\n };\n};\n\n/** The SELECT for a plan: rows, plus the matching COUNT for pagination. */\nexport const compileHeadQuery = (plan: HeadQueryPlan, dialect: ContentSqlDialect = contentDialect()) => {\n const { prefix, rows, countFrom: counted } = headQueryParts(plan, dialect);\n return {\n rows: sql<HeadRow>`${prefix}select ${HEAD_COLUMNS}\n ${rows}`,\n count: sql<{\n total: string | number;\n }>`${prefix}select count(*) as total ${counted}`,\n };\n};\n\n/** The page's total (when asked for) and the site's publication sequence, read in the same statement. */\nexport type HeadPageMeta = { page_total: string | number | null; page_seq: string | number | null };\n\n/**\n * One delivery read as a single statement (plan delivery-perf): the page's heads with, on every row, the\n * matching total and the site's current publication sequence (`publication_state.last_seq`), so all three\n * come from one snapshot of the database on every dialect without a transaction. The uncorrelated\n * subqueries run once per statement. An empty page carries no row, so `meta` reads the total and the\n * sequence alone, for callers that read it in the same transaction as `rows`.\n */\nexport const compileHeadPage = (\n plan: HeadQueryPlan,\n options: { total: boolean },\n dialect: ContentSqlDialect = contentDialect(),\n) => {\n const { prefix, rows, countFrom: counted } = headQueryParts(plan, dialect);\n const total = options.total ? sql`(select count(*) ${counted})` : sql`null`;\n const seq = sql`(select ps.last_seq from publication_state ps where ps.site_id = ${dialect.uuid(plan.siteId)})`;\n return {\n rows: sql<\n HeadRow & HeadPageMeta\n >`${prefix}select ${HEAD_COLUMNS}, ${total} as page_total, ${seq} as page_seq\n ${rows}`,\n meta: sql<HeadPageMeta>`${prefix}select ${total} as page_total, ${seq} as page_seq`,\n };\n};\n","import { sql, type RawBuilder } from 'kysely';\nimport { contentDialect } from './currentDialect.js';\nimport type { ContentSqlDialect } from './dialect/types.js';\n\n/**\n * Conditions on the head query's entry (alias `h`, compile.ts) for `HeadQueryPlan.conditions`, so callers\n * outside the compiler never write SQL against its aliases.\n */\n\n/** The head belongs to entry `id`. */\nexport const entryIdIs = (id: string, dialect: ContentSqlDialect = contentDialect()): RawBuilder<boolean> =>\n sql<boolean>`h.entry_id = ${dialect.uuid(id)}`;\n\n/** The head belongs to one of `ids` (one bound parameter, whatever its length). */\nexport const entryIdIn = (\n ids: readonly string[],\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<boolean> => dialect.oneOf(sql`h.entry_id`, ids, 'uuid');\n","import type { FieldDefinition, ModelDefinition } from '@shapio/schema';\nimport { sql, type RawBuilder } from 'kysely';\nimport { contentDialect } from './currentDialect.js';\nimport type { ContentSqlDialect } from './dialect/types.js';\nimport { fieldValueExpression } from './expressions.js';\nimport { isSortable } from './operators.js';\nimport type { SortTerm } from './types.js';\n\n/**\n * ORDER BY for a content query. Field sorts use the same expression as the field's index (so the index can\n * serve them); the entry ID is always the final tie-breaker so pages are stable. Without an explicit sort\n * the model's default sort applies when the caller may read that field, else newest first. `createdAt` is the\n * entry's creation time copied on the head (`h.entry_created_at`), so the default order and its tie-breaker\n * are both head columns, which `entry_heads_entry_order_idx` (site, model, state, `entry_created_at desc`,\n * `entry_id`) serves: a page is read in index order and the scan stops at the limit (ADR 0001, \"Delivery perf\n * 2\").\n */\nconst SYSTEM_SORT_COLUMNS = {\n id: 'h.entry_id',\n createdAt: 'h.entry_created_at',\n updatedAt: 'h.updated_at',\n} as const;\n\nconst termSql = (term: SortTerm, dialect: ContentSqlDialect): RawBuilder<unknown> => {\n // System columns are never null; field values may be missing.\n const nullable = term.target.kind === 'field';\n const expression =\n term.target.kind === 'system'\n ? sql.ref(SYSTEM_SORT_COLUMNS[term.target.name])\n : fieldValueExpression(term.target.field.id, term.target.field.type, dialect);\n if (dialect.sortTerm) {\n return dialect.sortTerm(expression, term.direction, nullable);\n }\n return sql`${expression} ${dialect.sortDirection(term.direction, nullable)}`;\n};\n\nexport const defaultSortTerms = (\n model: ModelDefinition,\n isReadable: (field: FieldDefinition) => boolean,\n): SortTerm[] => {\n const preferred = model.display.defaultSort;\n const field = preferred ? model.fields.find((candidate) => candidate.id === preferred.fieldId) : undefined;\n if (preferred && field && !field.deprecated && isSortable(field) && isReadable(field)) {\n return [{ target: { kind: 'field', field }, direction: preferred.direction }];\n }\n return [{ target: { kind: 'system', name: 'createdAt' }, direction: 'desc' }];\n};\n\nexport const compileOrderBy = (\n terms: readonly SortTerm[],\n dialect: ContentSqlDialect = contentDialect(),\n): RawBuilder<unknown>[] => [...terms.map((term) => termSql(term, dialect)), sql`h.entry_id asc`];\n","import { isDeepStrictEqual } from 'node:util';\nimport type { FieldDefinition, ModelDefinition } from '@shapio/schema';\nimport type { ContentData } from '../db/contentData.js';\nimport type { SchemaById } from '../schema/snapshot.js';\nimport type { LocaleScope } from './compiler/compile.js';\nimport { queryInvalid } from './compiler/types.js';\nimport { isLocalizedField } from './model.js';\n\n/**\n * Localization rules (ADR 0004). Every locale's head holds the complete document; shared (non-localized)\n * fields are fanned out to every existing draft of the entry when saved; publishing is strictly per locale.\n */\n\n/** The locale itself, then its configured fallbacks, then the default locale (always last). */\nexport const fallbackChain = (snapshot: SchemaById, locale: string): string[] => {\n const configured = snapshot.locales.find((candidate) => candidate.code === locale);\n const chain = [locale, ...(configured?.fallbacks ?? []), snapshot.defaultLocale];\n return chain.filter(\n (code, index) => chain.indexOf(code) === index && snapshot.locales.some((l) => l.code === code),\n );\n};\n\n/** The locale a write targets: the requested one for localized models, the default locale otherwise. */\nexport const writeLocaleFor = (\n snapshot: SchemaById,\n model: ModelDefinition,\n requested: string | undefined,\n) => {\n if (!model.localized) {\n return snapshot.defaultLocale;\n }\n const locale = requested ?? snapshot.defaultLocale;\n if (!snapshot.locales.some((candidate) => candidate.code === locale)) {\n throw queryInvalid(`Unknown locale \"${locale}\"`);\n }\n return locale;\n};\n\n/**\n * Which heads serve a read. Non-localized models keep their heads in whichever locale they were created in,\n * so they are read regardless of locale; localized models serve the requested locale or fall back.\n */\nexport const readScopeFor = (\n snapshot: SchemaById,\n model: ModelDefinition,\n requested: string | undefined,\n { fallback }: { fallback: boolean },\n): LocaleScope => {\n if (!model.localized) {\n return { kind: 'any' };\n }\n const locale = requested ?? snapshot.defaultLocale;\n return { kind: 'chain', chain: fallback ? fallbackChain(snapshot, locale) : [locale] };\n};\n\nexport const sharedFields = (model: ModelDefinition): FieldDefinition[] =>\n model.fields.filter((field) => !isLocalizedField(model, field));\n\n/** Copies the shared fields' values (present or absent) from `source` into `target`. */\nexport const withSharedValuesOf = (\n model: ModelDefinition,\n target: Readonly<ContentData>,\n source: Readonly<ContentData>,\n fieldIds?: ReadonlySet<string>,\n): ContentData => {\n const output: ContentData = { ...target };\n for (const field of sharedFields(model)) {\n if (fieldIds && !fieldIds.has(field.id)) {\n continue;\n }\n if (source[field.id] === undefined) {\n delete output[field.id];\n } else {\n output[field.id] = source[field.id];\n }\n }\n return output;\n};\n\n/** Shared fields whose values differ between two documents. */\nexport const changedSharedFieldIds = (\n model: ModelDefinition,\n before: Readonly<ContentData>,\n after: Readonly<ContentData>,\n): Set<string> =>\n new Set(\n sharedFields(model)\n .filter((field) => !isDeepStrictEqual(before[field.id], after[field.id]))\n .map((field) => field.id),\n );\n\nexport type LocaleHeads = {\n locale: string;\n draft?: Readonly<ContentData>;\n published?: Readonly<ContentData>;\n};\n\n/**\n * Published locales whose live shared values are older than their draft's (which always holds the latest\n * shared values). The admin offers to publish these too after a shared field changed (ADR 0004).\n */\nexport const outdatedSharedLocales = (model: ModelDefinition, heads: readonly LocaleHeads[]): string[] =>\n model.localized\n ? heads\n .filter(\n ({ draft, published }) =>\n draft && published && changedSharedFieldIds(model, published, draft).size > 0,\n )\n .map(({ locale }) => locale)\n : [];\n","import { Kysely, PostgresDialect, type PostgresPool, type TransactionBuilder } from 'kysely';\nimport pg from 'pg';\nimport { assertNotNested, withConnectionScope } from './connectionScope.js';\nimport { dialectOfUrl, setCurrentDialect, sqliteLocationOfUrl } from './dialect.js';\nimport { createMysqlDb } from './mysql/index.js';\nimport { DEFAULT_POOL_ACQUIRE_TIMEOUT_MS, pgAcquireError } from './poolAcquire.js';\nimport { createSqliteDb } from './sqlite/index.js';\nimport type { DB } from './types.js';\n\nexport type Database = Kysely<DB>;\n\ntype CreateDbOptions = {\n connectionString: string;\n poolMax: number;\n /**\n * PostgreSQL and MySQL: how long a query waits for a free pooled connection before it fails with a 503\n * (`PoolAcquireTimeoutError`). On PostgreSQL it also bounds opening a new connection (503\n * `DatabaseUnavailableError`). SQLite never waits for a pooled connection: autocommit reads share one\n * reader, and a write that would wait for its own transaction fails at once (`db/sqlite/writeLock.ts`).\n */\n acquireTimeoutMs?: number;\n applicationName?: string;\n /**\n * Called when an idle pooled connection fails (PostgreSQL restarted, a network blip). The pool already\n * discards that connection and opens a new one on demand; this only reports it. Defaults to a process\n * warning, so the error is never silent.\n */\n onIdleConnectionError?: (error: Error) => void;\n /**\n * Tests turn it on. SQLite: fail on computed result columns that look like undecoded timestamps or JSON,\n * so a missing `db/sql/typed` marker is caught. PostgreSQL and MySQL: fail a query that asks the pool for\n * a connection inside an open transaction (`db/connectionScope.ts`).\n */\n strict?: boolean;\n};\n\nconst warnIdleConnectionError = (error: Error) =>\n process.emitWarning(`PostgreSQL connection lost while idle: ${error.message}`);\n\n/**\n * The pg pool as Kysely sees it: an exhausted pool's queue timeout becomes `PoolAcquireTimeoutError`, a\n * connection that cannot be opened in time `DatabaseUnavailableError`, and in strict mode a nested\n * acquisition fails at once.\n */\nconst acquiringPool = (\n pool: pg.Pool,\n poolMax: number,\n acquireTimeoutMs: number,\n strict: boolean,\n): PostgresPool => ({\n options: pool.options,\n connect: async () => {\n if (strict) {\n assertNotNested();\n }\n try {\n return await pool.connect();\n } catch (error) {\n throw pgAcquireError(error, poolMax, acquireTimeoutMs);\n }\n },\n end: () => pool.end(),\n});\n\n/** Kysely on PostgreSQL in strict mode: transactions run in a connection scope (`db/connectionScope.ts`). */\nclass StrictPostgresKysely<DB> extends Kysely<DB> {\n override transaction(): TransactionBuilder<DB> {\n return withConnectionScope(super.transaction());\n }\n}\n\nconst warnIdleMysqlConnectionError = (error: Error) =>\n process.emitWarning(`MySQL connection lost while idle: ${error.message}`);\n\n/**\n * The database handle for DATABASE_URL: PostgreSQL through a `pg` pool, MySQL (`mysql://…`) through a\n * `mysql2` pool, or SQLite (`sqlite:<path>`) through Shapio's `node:sqlite` driver, where `poolMax` is the\n * number of read connections (one connection writes).\n * Sets the process dialect (`db/dialect.ts`) that SQL builders read.\n */\nexport const createDb = ({\n connectionString,\n poolMax,\n acquireTimeoutMs = DEFAULT_POOL_ACQUIRE_TIMEOUT_MS,\n applicationName = 'shapio',\n onIdleConnectionError,\n strict = false,\n}: CreateDbOptions): Database => {\n if (dialectOfUrl(connectionString) === 'sqlite') {\n setCurrentDialect('sqlite');\n return createSqliteDb<DB>({ location: sqliteLocationOfUrl(connectionString), readers: poolMax, strict });\n }\n if (dialectOfUrl(connectionString) === 'mysql') {\n setCurrentDialect('mysql');\n return createMysqlDb<DB>({\n url: connectionString,\n poolMax,\n acquireTimeoutMs,\n applicationName,\n onIdleConnectionError: onIdleConnectionError ?? warnIdleMysqlConnectionError,\n strict,\n });\n }\n setCurrentDialect('postgres');\n const pool = new pg.Pool({\n connectionString,\n max: poolMax,\n // Bounds both the wait for a free connection and the opening of a new one.\n connectionTimeoutMillis: acquireTimeoutMs,\n application_name: applicationName,\n });\n // Without a listener, pg's 'error' event on an idle client would crash the process on a database restart.\n pool.on('error', onIdleConnectionError ?? warnIdleConnectionError);\n // pg-pool drops its own listener while a client is checked out, yet a client whose socket dies still emits\n // 'error' (after rejecting its running query, or before the next query of a transaction). Unheard, that is\n // an uncaught exception that takes the process down. The error is not lost: the query or transaction using\n // the client fails with it, and an idle client reports it through onIdleConnectionError above.\n pool.on('connect', (client) => {\n client.on('error', () => undefined);\n });\n const dialect = new PostgresDialect({\n pool: acquiringPool(pool, poolMax, acquireTimeoutMs, strict),\n // What the pool would use itself; Kysely opens it to cancel a query.\n controlClient: pg.Client,\n });\n return strict ? new StrictPostgresKysely<DB>({ dialect }) : new Kysely<DB>({ dialect });\n};\n\n/**\n * The process-wide database handle used as the default executor by repositories.\n * `buildApp` sets it; tests and child processes each run in their own process, so one handle per process holds.\n */\nexport let db: Database = undefined as unknown as Database;\n\nexport const setDb = (instance: Database): void => {\n db = instance;\n};\n\n/** Whether this process has its database handle yet (a server in the same process set it first). */\nexport const hasDb = (): boolean => (db as Database | undefined) !== undefined;\n\n/** Unsets the process handle if it is `instance` (its owner is closing it); leaves any other handle alone. */\nexport const clearDb = (instance: Database): void => {\n if (db === instance) {\n db = undefined as unknown as Database;\n }\n};\n","import type { Kysely, RawBuilder, Transaction } from 'kysely';\nimport type { HeadPageMeta, HeadRow } from '../content/compiler/compile.js';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/**\n * Executes head queries compiled by `content/compiler` (the only place content SQL is written). Kept as a\n * repository so services never run SQL themselves.\n */\nexport const runHeadQuery = async (query: RawBuilder<HeadRow>, executor: Executor = db): Promise<HeadRow[]> =>\n (await query.execute(executor)).rows;\n\nexport const runCountQuery = async (\n query: RawBuilder<{ total: string | number }>,\n executor: Executor = db,\n): Promise<number> => Number((await query.execute(executor)).rows[0]?.total ?? 0);\n\n/** A page's total (null when not asked for) and the publication sequence its statement saw (null: no site row). */\nexport type PageMeta = { total: number | null; seq: number | null };\n\nconst toPageMeta = (row: HeadPageMeta): PageMeta => ({\n total: row.page_total === null ? null : Number(row.page_total),\n seq: row.page_seq === null ? null : Number(row.page_seq),\n});\n\n/**\n * Runs a head page compiled by `compileHeadPage`: the rows, and the total and sequence carried on them (null\n * when the page is empty; then `runHeadPageMeta` in the same transaction reads them).\n */\nexport const runHeadPage = async (\n query: RawBuilder<HeadRow & HeadPageMeta>,\n executor: Executor = db,\n): Promise<{ rows: HeadRow[]; meta: PageMeta | null }> => {\n const result = (await query.execute(executor)).rows;\n const [first] = result;\n return {\n rows: result.map(({ page_total: _total, page_seq: _seq, ...row }) => row),\n meta: first ? toPageMeta(first) : null,\n };\n};\n\nexport const runHeadPageMeta = async (\n query: RawBuilder<HeadPageMeta>,\n executor: Executor = db,\n): Promise<PageMeta> => {\n const [row] = (await query.execute(executor)).rows;\n return row ? toPageMeta(row) : { total: null, seq: null };\n};\n\n/** Runs `fn` in one REPEATABLE READ, read-only transaction: a response reads one consistent moment. */\nexport const withConsistentRead = <T>(fn: (trx: Transaction<DB>) => Promise<T>, database: Kysely<DB> = db) =>\n database.transaction().setIsolationLevel('repeatable read').setAccessMode('read only').execute(fn);\n","import type { Insertable, Kysely, Selectable, Transaction, Updateable } from 'kysely';\nimport { db } from '../db/index.js';\nimport { containsInsensitive, startsWith } from '../db/sql/text.js';\nimport { timestampCursorText, timestampParam } from '../db/sql/time.js';\nimport type { DB, MediaAssets } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type MediaAssetRow = Selectable<MediaAssets>;\nexport type NewMediaAsset = Insertable<MediaAssets>;\nexport type MediaAssetPatch = Updateable<MediaAssets>;\n\nexport type MediaAssetFilter = {\n /** A folder ID, `null` for the root (no folder), or undefined for every folder. */\n folderId?: string | null;\n /** `image/png`, or a top-level type with a wildcard: `image/*`. */\n mimeType?: string;\n /** Matches the file name, alt text and caption, case-insensitively. */\n search?: string;\n};\n\n/** Keyset cursor: (created_at, id) of the last row, created_at as ISO-8601 with microseconds. */\nexport type MediaAssetCursor = { createdAt: string; id: string };\n\nconst live = (trx: Executor) => trx.selectFrom('media_assets').where('deleted_at', 'is', null);\n\nexport const insert = (asset: NewMediaAsset, trx: Executor = db) =>\n trx.insertInto('media_assets').values(asset).returningAll().executeTakeFirstOrThrow();\n\n/** Any site's asset, deleted or not. Unscoped: jobs and exists-anywhere checks only; requests use `findLiveOnSite`. */\nexport const findById = (id: string, trx: Executor = db) =>\n trx.selectFrom('media_assets').selectAll().where('id', '=', id).executeTakeFirst();\n\n/** A live asset of one site; another site's asset reads as missing (sites plan §H). */\nexport const findLiveOnSite = (siteId: string, id: string, trx: Executor = db) =>\n live(trx).selectAll().where('site_id', '=', siteId).where('id', '=', id).executeTakeFirst();\n\n/** The live assets of one site among `ids`. */\nexport const findLiveManyOnSite = (siteId: string, ids: readonly string[], trx: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : live(trx).selectAll().where('site_id', '=', siteId).where('id', 'in', ids).execute();\n\n/** Locks the row for the rest of the transaction (deleted rows too, so callers can report them). Jobs only. */\nexport const lockById = (id: string, trx: Transaction<DB>) =>\n trx.selectFrom('media_assets').selectAll().where('id', '=', id).forUpdate().executeTakeFirst();\n\n/** Locks one site's asset row (deleted rows too); another site's asset reads as missing. */\nexport const lockOnSite = (siteId: string, id: string, trx: Transaction<DB>) =>\n trx\n .selectFrom('media_assets')\n .selectAll()\n .where('site_id', '=', siteId)\n .where('id', '=', id)\n .forUpdate()\n .executeTakeFirst();\n\nexport const findLiveByStorageKey = (key: string, trx: Executor = db) =>\n live(trx).selectAll().where('storage_key', '=', key).executeTakeFirst();\n\nexport const update = (id: string, patch: MediaAssetPatch, trx: Executor = db) =>\n trx\n .updateTable('media_assets')\n .set({ ...patch, updated_at: new Date() })\n .where('id', '=', id)\n .returningAll()\n .executeTakeFirstOrThrow();\n\n/** Changes made by jobs: only while the asset still has `storageKey` (not replaced, moved or deleted). */\nexport const updateIfStorageKey = (\n id: string,\n storageKey: string,\n patch: MediaAssetPatch,\n trx: Executor = db,\n) =>\n trx\n .updateTable('media_assets')\n .set({ ...patch, updated_at: new Date() })\n .where('id', '=', id)\n .where('storage_key', '=', storageKey)\n .where('deleted_at', 'is', null)\n .returningAll()\n .executeTakeFirst();\n\n/** Metadata edits by people: only when the caller saw the current version (optimistic concurrency). */\nexport const updateIfVersion = (\n siteId: string,\n id: string,\n expectedVersion: number,\n patch: MediaAssetPatch,\n trx: Executor = db,\n) =>\n trx\n .updateTable('media_assets')\n .set((eb) => ({ ...patch, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('site_id', '=', siteId)\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .where('deleted_at', 'is', null)\n .returningAll()\n .executeTakeFirst();\n\nexport const list = (\n siteId: string,\n filter: MediaAssetFilter,\n cursor: MediaAssetCursor | undefined,\n limit: number,\n trx: Executor = db,\n) =>\n live(trx)\n .selectAll()\n .select(timestampCursorText('created_at').as('cursor_at'))\n .where('site_id', '=', siteId)\n .$if(filter.folderId !== undefined, (qb) =>\n filter.folderId === null\n ? qb.where('folder_id', 'is', null)\n : qb.where('folder_id', '=', filter.folderId ?? ''),\n )\n .$if(filter.mimeType !== undefined, (qb) => {\n const mimeType = filter.mimeType ?? '';\n return mimeType.endsWith('/*')\n ? qb.where((eb) => startsWith(eb.ref('mime_type'), mimeType.slice(0, -1)))\n : qb.where('mime_type', '=', mimeType);\n })\n .$if(filter.search !== undefined, (qb) => {\n const search = filter.search ?? '';\n return qb.where((eb) =>\n eb.or([\n containsInsensitive(eb.ref('original_filename'), search),\n containsInsensitive(eb.ref('alt'), search),\n containsInsensitive(eb.ref('caption'), search),\n ]),\n );\n })\n .$if(cursor !== undefined, (qb) =>\n qb.where((eb) => {\n const at = timestampParam(cursor?.createdAt ?? null);\n return eb.or([\n eb('created_at', '<', at),\n eb.and([eb('created_at', '=', at), eb('id', '<', cursor?.id ?? '')]),\n ]);\n }),\n )\n .orderBy('created_at', 'desc')\n .orderBy('id', 'desc')\n .limit(limit)\n .execute();\n\n/** Moves one site's live assets into a folder of that site (or the root); returns the IDs that moved. */\nexport const moveToFolder = async (\n siteId: string,\n ids: readonly string[],\n folderId: string | null,\n trx: Executor = db,\n) =>\n (\n await trx\n .updateTable('media_assets')\n .set((eb) => ({ folder_id: folderId, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('site_id', '=', siteId)\n .where('id', 'in', ids)\n .where('deleted_at', 'is', null)\n .returning('id')\n .execute()\n ).map((row) => row.id);\n\nexport const countLiveInFolder = async (folderId: string, trx: Executor = db) =>\n Number(\n (\n await live(trx)\n .select((eb) => eb.fn.countAll<string>().as('count'))\n .where('folder_id', '=', folderId)\n .executeTakeFirstOrThrow()\n ).count,\n );\n\n/** Live assets on a driver, in ID order after `afterId` (for `shapio media migrate`). */\nexport const listLiveByDriver = (\n driver: string,\n afterId: string | undefined,\n limit: number,\n trx: Executor = db,\n) =>\n live(trx)\n .selectAll()\n .where('storage_driver', '=', driver)\n .$if(afterId !== undefined, (qb) => qb.where('id', '>', afterId ?? ''))\n .orderBy('id')\n .limit(limit)\n .execute();\n\n/** Library alt text and type of one site's live assets among `ids` (content health's alt-text rule). */\nexport const findAltOnSite = (siteId: string, ids: readonly string[], executor: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : executor\n .selectFrom('media_assets')\n .select(['id', 'alt', 'mime_type'])\n .where('site_id', '=', siteId)\n .where('id', 'in', ids)\n .where('deleted_at', 'is', null)\n .execute();\n\n/**\n * Hard-deletes a site's soft-deleted assets (site deletion). Their objects were queued for purge when they\n * were deleted; their variants and references cascade.\n */\nexport const deleteSoftDeletedOfSite = (siteId: string, trx: Executor = db) =>\n trx.deleteFrom('media_assets').where('site_id', '=', siteId).where('deleted_at', 'is not', null).execute();\n","/** Media library limits and timings (package G). Upload size and allowed types are configuration. */\n\n/** Shapio's route for stored objects: public ones at a stable URL, private ones with a signature. */\nexport const MEDIA_FILE_ROUTE_PREFIX = '/api/media/f/';\n/** Where the local driver receives upload bytes (`/api/media/uploads/:grantId`). */\nexport const MEDIA_UPLOAD_ROUTE_PREFIX = '/api/media/uploads/';\n\n/** How long a signed URL for a private asset works. */\nexport const MEDIA_SIGNED_URL_TTL_SECONDS = 60 * 60;\n/** How long an upload grant accepts bytes. */\nexport const MEDIA_UPLOAD_GRANT_TTL_SECONDS = 15 * 60;\n/** How long after a grant expires an uploaded-but-unconfirmed object is kept before it is deleted. */\nexport const MEDIA_UPLOAD_CONFIRM_GRACE_SECONDS = 60 * 60;\n/** Bytes read from the start of an upload to detect its type. */\nexport const MEDIA_SNIFF_SAMPLE_BYTES = 16 * 1024;\n\nexport const MEDIA_PROCESS_JOB = 'media.process';\nexport const MEDIA_PURGE_JOB = 'media.purge';\nexport const MEDIA_RELOCATE_JOB = 'media.relocate';\nexport const MEDIA_GRANT_EXPIRE_JOB = 'media.grant.expire';\nexport const MEDIA_JOB_MAX_ATTEMPTS = 5;\n\n/** Outbox event types (webhook catalogue `media.*`, package H). */\nexport const MEDIA_EVENTS = {\n created: 'media.created',\n updated: 'media.updated',\n deleted: 'media.deleted',\n} as const;\n","import { MEDIA_FILE_ROUTE_PREFIX, MEDIA_SIGNED_URL_TTL_SECONDS } from '../constants/media.js';\nimport type { UrlBuilder } from '../helpers/publicUrl.js';\nimport { encodeKeyPath, type MediaVisibility } from './keys.js';\nimport type { MediaStorage, StorageDriver } from './types.js';\n\nexport type StoredObjectRef = {\n driver: StorageDriver;\n key: string;\n visibility: MediaVisibility;\n filename: string;\n mimeType: string;\n};\n\nexport type ResolvedUrl = { url: string; expiresAt: Date | null };\n\n/**\n * The URL a client should use for a stored object. Public: the CDN/bucket base when configured, otherwise\n * Shapio's stable route. Private: always an expiring signed URL; nothing else reaches private bytes.\n */\nexport const resolveObjectUrl = async (\n storage: MediaStorage,\n urls: UrlBuilder,\n object: StoredObjectRef,\n ttlSeconds = MEDIA_SIGNED_URL_TTL_SECONDS,\n): Promise<ResolvedUrl> => {\n const adapter = storage.get(object.driver);\n if (object.visibility === 'public') {\n return {\n url:\n adapter.publicUrl(object.key) ??\n urls.absoluteUrl(`${MEDIA_FILE_ROUTE_PREFIX}${encodeKeyPath(object.key)}`),\n expiresAt: null,\n };\n }\n const url = await adapter.signedGetUrl(object.key, {\n expiresInSeconds: ttlSeconds,\n filename: object.filename,\n contentType: object.mimeType,\n });\n return { url, expiresAt: new Date(Date.now() + ttlSeconds * 1000) };\n};\n","import type { Insertable, Kysely, Selectable, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB, MediaVariants } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type MediaVariantRow = Selectable<MediaVariants>;\n\nexport const listForAssets = (assetIds: readonly string[], trx: Executor = db) =>\n assetIds.length === 0\n ? Promise.resolve([])\n : trx\n .selectFrom('media_variants')\n .selectAll()\n .where('asset_id', 'in', assetIds)\n .orderBy('asset_id')\n .orderBy('width')\n .execute();\n\n/** Creates (or resets to pending) the variants an asset will get. */\nexport const upsertPending = async (rows: readonly Insertable<MediaVariants>[], trx: Executor = db) => {\n if (rows.length === 0) {\n return;\n }\n await trx\n .insertInto('media_variants')\n .values(rows.map((row) => ({ ...row, status: 'pending' })))\n .onConflict((oc) =>\n oc.columns(['asset_id', 'name']).doUpdateSet((eb) => ({\n width: eb.ref('excluded.width'),\n height: eb.ref('excluded.height'),\n format: eb.ref('excluded.format'),\n mime_type: eb.ref('excluded.mime_type'),\n storage_key: null,\n size_bytes: null,\n checksum_sha256: null,\n status: 'pending',\n error: null,\n updated_at: new Date(),\n })),\n )\n .execute();\n};\n\ntype VariantResult =\n | {\n status: 'ready';\n storageKey: string;\n width: number;\n height: number;\n sizeBytes: number;\n checksumSha256: string;\n }\n | { status: 'failed'; error: string };\n\n/**\n * Records a variant's outcome, but only while the asset still has the original it was made from\n * (`assetStorageKey`); returns false when the asset was replaced, moved or deleted meanwhile.\n */\nexport const recordResult = async (\n assetId: string,\n name: string,\n assetStorageKey: string,\n result: VariantResult,\n trx: Executor = db,\n) => {\n const updated = await trx\n .updateTable('media_variants')\n .set(\n result.status === 'ready'\n ? {\n status: 'ready',\n storage_key: result.storageKey,\n width: result.width,\n height: result.height,\n size_bytes: result.sizeBytes,\n checksum_sha256: result.checksumSha256,\n error: null,\n updated_at: new Date(),\n }\n : { status: 'failed', error: result.error, updated_at: new Date() },\n )\n .where('asset_id', '=', assetId)\n .where('name', '=', name)\n .where((eb) =>\n eb.exists(\n eb\n .selectFrom('media_assets')\n .select('id')\n .where('id', '=', assetId)\n .where('storage_key', '=', assetStorageKey)\n .where('deleted_at', 'is', null),\n ),\n )\n .executeTakeFirst();\n return updated.numUpdatedRows > 0n;\n};\n\n/** Removes an asset's variants; returns the stored keys so the objects can be purged. */\nexport const deleteForAsset = async (assetId: string, trx: Executor = db) =>\n (\n await trx.deleteFrom('media_variants').where('asset_id', '=', assetId).returning('storage_key').execute()\n ).flatMap((row) => (row.storage_key ? [row.storage_key] : []));\n\nexport const updateStorageKey = (id: string, storageKey: string, trx: Executor = db) =>\n trx\n .updateTable('media_variants')\n .set({ storage_key: storageKey, updated_at: new Date() })\n .where('id', '=', id)\n .execute();\n\n/** A ready variant by its key, with what serving it needs from its (live) asset. */\nexport const findServableByStorageKey = (key: string, trx: Executor = db) =>\n trx\n .selectFrom('media_variants')\n .innerJoin('media_assets', 'media_assets.id', 'media_variants.asset_id')\n .select([\n 'media_variants.storage_key',\n 'media_variants.mime_type',\n 'media_variants.size_bytes',\n 'media_variants.name',\n 'media_assets.storage_driver',\n 'media_assets.visibility',\n 'media_assets.original_filename',\n ])\n .where('media_variants.storage_key', '=', key)\n .where('media_variants.status', '=', 'ready')\n .where('media_assets.deleted_at', 'is', null)\n .executeTakeFirst();\n","import type { Database } from '../db/index.js';\nimport type { UrlBuilder } from '../helpers/publicUrl.js';\nimport type { MediaVisibility } from '../media/keys.js';\nimport type { MediaStorage, StorageDriver } from '../media/types.js';\nimport { resolveObjectUrl } from '../media/urls.js';\nimport type { MediaAssetRow } from '../repositories/mediaAssets.js';\nimport * as mediaVariantsRepository from '../repositories/mediaVariants.js';\nimport type { MediaVariantRow } from '../repositories/mediaVariants.js';\n\nexport type MediaVariantView = {\n name: string;\n width: number | null;\n height: number | null;\n format: string;\n mimeType: string;\n sizeBytes: number | null;\n status: 'pending' | 'ready' | 'failed';\n /** Present once the variant is ready. */\n url: string | null;\n};\n\nexport type MediaAssetView = {\n id: string;\n folderId: string | null;\n filename: string;\n mimeType: string;\n sizeBytes: number;\n width: number | null;\n height: number | null;\n checksumSha256: string | null;\n alt: string;\n caption: string;\n focalPoint: { x: number; y: number } | null;\n visibility: MediaVisibility;\n /** `processing` until the checksum, dimensions and variants are done. */\n status: 'processing' | 'ready' | 'failed';\n processingError: string | null;\n storageDriver: StorageDriver;\n url: string;\n /** When `url` stops working (private assets); null for stable public URLs. */\n urlExpiresAt: Date | null;\n variants: MediaVariantView[];\n createdBy: string | null;\n createdAt: Date;\n updatedAt: Date;\n version: number;\n};\n\ntype ViewDependencies = { storage: MediaStorage; urls: UrlBuilder };\n\nconst toVariantView = async (\n deps: ViewDependencies,\n asset: MediaAssetRow,\n variant: MediaVariantRow,\n): Promise<MediaVariantView> => ({\n name: variant.name,\n width: variant.width,\n height: variant.height,\n format: variant.format,\n mimeType: variant.mime_type,\n sizeBytes: variant.size_bytes === null ? null : Number(variant.size_bytes),\n status: variant.status as MediaVariantView['status'],\n url:\n variant.status === 'ready' && variant.storage_key\n ? (\n await resolveObjectUrl(deps.storage, deps.urls, {\n driver: asset.storage_driver as StorageDriver,\n key: variant.storage_key,\n visibility: asset.visibility as MediaVisibility,\n filename: asset.original_filename,\n mimeType: variant.mime_type,\n })\n ).url\n : null,\n});\n\nconst toAssetView = async (\n deps: ViewDependencies,\n asset: MediaAssetRow,\n variants: readonly MediaVariantRow[],\n): Promise<MediaAssetView> => {\n const { url, expiresAt } = await resolveObjectUrl(deps.storage, deps.urls, {\n driver: asset.storage_driver as StorageDriver,\n key: asset.storage_key,\n visibility: asset.visibility as MediaVisibility,\n filename: asset.original_filename,\n mimeType: asset.mime_type,\n });\n return {\n id: asset.id,\n folderId: asset.folder_id,\n filename: asset.original_filename,\n mimeType: asset.mime_type,\n sizeBytes: Number(asset.size_bytes),\n width: asset.width,\n height: asset.height,\n checksumSha256: asset.checksum_sha256,\n alt: asset.alt,\n caption: asset.caption,\n focalPoint:\n asset.focal_x !== null && asset.focal_y !== null ? { x: asset.focal_x, y: asset.focal_y } : null,\n visibility: asset.visibility as MediaVisibility,\n status: asset.status as MediaAssetView['status'],\n processingError: asset.processing_error,\n storageDriver: asset.storage_driver as StorageDriver,\n url,\n urlExpiresAt: expiresAt,\n variants: await Promise.all(variants.map((variant) => toVariantView(deps, asset, variant))),\n createdBy: asset.created_by,\n createdAt: asset.created_at,\n updatedAt: asset.updated_at,\n version: asset.version,\n };\n};\n\n/**\n * API views of assets with their variants (one query for all variants) and URLs: stable for public\n * assets, signed and expiring for private ones. Package E can use this to resolve media fields.\n * `executor`: the caller's open transaction, when there is one.\n */\nexport const toAssetViews = async (\n deps: ViewDependencies,\n assets: readonly MediaAssetRow[],\n executor?: Database,\n): Promise<MediaAssetView[]> => {\n const variants = await mediaVariantsRepository.listForAssets(\n assets.map((asset) => asset.id),\n executor,\n );\n const byAsset = Map.groupBy(variants, (variant) => variant.asset_id);\n return Promise.all(assets.map((asset) => toAssetView(deps, asset, byAsset.get(asset.id) ?? [])));\n};\n\nexport const toAssetViewOne = async (\n deps: ViewDependencies,\n asset: MediaAssetRow,\n): Promise<MediaAssetView> => {\n const [view] = await toAssetViews(deps, [asset]);\n if (!view) {\n throw new Error('Asset view missing');\n }\n return view;\n};\n","import type { FieldDefinition } from '@shapio/schema';\nimport type { Policy } from '../../permissions/types.js';\nimport { resolveModelById, type ContentModel } from '../model.js';\nimport type { ReadEnvironment } from '../read.js';\nimport type { HeadRow } from './compile.js';\nimport type { PopulateTree } from './types.js';\n\n/**\n * Relation expansion (`populate`). Depth is bounded by the parser (QUERY_LIMITS.maxPopulateDepth); each\n * level applies the *target* model's read policy (row filter and read mask) and, for delivery, reads only\n * published targets. A target the caller may not read is left out, never shown partially.\n */\nexport type PopulateEnvironment = ReadEnvironment & {\n fetch: (\n env: ReadEnvironment,\n model: ContentModel,\n policy: Policy,\n ids: readonly string[],\n ) => Promise<HeadRow[]>;\n project: (\n env: ReadEnvironment,\n model: ContentModel,\n policy: Policy,\n rows: readonly HeadRow[],\n query: { fields: null; populate: PopulateTree },\n ) => Promise<Array<{ row: HeadRow; data: Record<string, unknown> }>>;\n policyFor: (env: ReadEnvironment, modelId: string) => Promise<Policy>;\n system: (env: ReadEnvironment, row: HeadRow) => Record<string, unknown>;\n};\n\nconst idsOf = (rows: readonly HeadRow[], fieldId: string): string[] => {\n const ids = new Set<string>();\n for (const row of rows) {\n const value = row.data[fieldId];\n for (const id of Array.isArray(value) ? value : [value]) {\n if (typeof id === 'string') {\n ids.add(id);\n }\n }\n }\n return [...ids];\n};\n\n/** relation field ID → target entry ID → projected target entry. */\nexport const populateRelations = async (\n env: PopulateEnvironment,\n rows: readonly HeadRow[],\n fields: readonly FieldDefinition[],\n tree: PopulateTree,\n): Promise<Map<string, Map<string, unknown>>> => {\n const populated = new Map<string, Map<string, unknown>>();\n for (const [fieldId, subtree] of tree) {\n const field = fields.find((candidate) => candidate.id === fieldId);\n if (!field || field.type !== 'relation') {\n continue;\n }\n const byId = new Map<string, unknown>();\n populated.set(fieldId, byId);\n const target = resolveModelById(env.snapshot, field.settings.target);\n if (!target) {\n continue;\n }\n const policy = await env.policyFor(env, target.definition.id);\n const targetRows = await env.fetch(env, target, policy, idsOf(rows, fieldId));\n for (const { row, data } of await env.project(env, target, policy, targetRows, {\n fields: null,\n populate: subtree,\n })) {\n byId.set(row.entry_id, { ...env.system(env, row), ...data });\n }\n }\n return populated;\n};\n","import { richTextMediaIds, type FieldDefinition, type RichTextDocument } from '@shapio/schema';\nimport type { ContentData } from '../db/contentData.js';\nimport type { ContentModel } from './model.js';\nimport { COMPONENT_KEY } from './validator/index.js';\n\n/**\n * Media a document uses: media field values and rich-text images, also inside components and dynamic zones.\n * Each use is attributed to the top-level field that holds it (package G's `media_references` rows).\n */\nexport type MediaUse = { assetId: string; fieldId: string };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst collect = (\n model: ContentModel,\n fields: readonly FieldDefinition[],\n data: Readonly<Record<string, unknown>>,\n add: (assetId: string) => void,\n) => {\n for (const field of fields) {\n const value = data[field.id];\n if (value === undefined || value === null) {\n continue;\n }\n switch (field.type) {\n case 'media':\n (Array.isArray(value) ? value : [value]).forEach((id) => typeof id === 'string' && add(id));\n break;\n case 'richtext':\n if (isRecord(value) && isRecord(value.doc)) {\n richTextMediaIds(value as RichTextDocument).forEach(add);\n }\n break;\n case 'component': {\n const component = model.components.get(field.settings.component)?.definition;\n for (const item of Array.isArray(value) ? value : [value]) {\n if (component && isRecord(item)) {\n collect(model, component.fields, item, add);\n }\n }\n break;\n }\n case 'dynamiczone':\n for (const item of Array.isArray(value) ? value : []) {\n const component = isRecord(item)\n ? model.components.get(String(item[COMPONENT_KEY]))?.definition\n : undefined;\n if (component && isRecord(item)) {\n collect(model, component.fields, item, add);\n }\n }\n break;\n default:\n break;\n }\n }\n};\n\n/** Every (asset, top-level field) pair a document uses, without repeats. */\nexport const mediaUsesOf = (model: ContentModel, data: Readonly<ContentData>): MediaUse[] => {\n const uses = new Map<string, MediaUse>();\n for (const field of model.definition.fields) {\n collect(model, [field], data, (assetId) =>\n uses.set(`${assetId}|${field.id}`, { assetId, fieldId: field.id }),\n );\n }\n return [...uses.values()];\n};\n\n/** Asset IDs used by the given top-level fields of the documents. */\nexport const mediaIdsOf = (\n model: ContentModel,\n documents: readonly Readonly<ContentData>[],\n fields: readonly FieldDefinition[],\n): Set<string> => {\n const ids = new Set<string>();\n for (const data of documents) {\n collect(model, fields, data, (id) => ids.add(id));\n }\n return ids;\n};\n\n/** The `allowedKinds` family of a MIME type. */\nexport const mediaKindOf = (mimeType: string): string => {\n const [type = '', subtype = ''] = mimeType.toLowerCase().split('/');\n if (type === 'image' || type === 'video' || type === 'audio') {\n return type;\n }\n if (type === 'text' || subtype === 'pdf' || /(msword|officedocument|opendocument|rtf|epub)/.test(subtype)) {\n return 'document';\n }\n return 'other';\n};\n","import type { ComponentDefinition, FieldDefinition } from '@shapio/schema';\nimport type { ContentData } from '../db/contentData.js';\nimport type { ContentModel } from './model.js';\nimport { COMPONENT_KEY } from './validator/index.js';\n\n/**\n * Relations live in the head JSON (field → ordered target entry IDs, ADR 0001). These helpers walk a\n * document, including components and dynamic zones, to derive `relation_edges` rows and to find the\n * targets a response mentions.\n */\nexport type RelationVisit = { field: FieldDefinition & { type: 'relation' }; entryIds: string[] };\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst referenceIds = (value: unknown): string[] =>\n (Array.isArray(value) ? value : [value]).filter((item): item is string => typeof item === 'string');\n\ntype Components = ContentModel['components'];\n\nconst walkFields = (\n fields: readonly FieldDefinition[],\n data: Readonly<Record<string, unknown>>,\n components: Components,\n visit: (relation: RelationVisit) => void,\n) => {\n for (const field of fields) {\n const value = data[field.id];\n if (value === undefined || value === null) {\n continue;\n }\n if (field.type === 'relation') {\n visit({ field, entryIds: referenceIds(value) });\n } else if (field.type === 'component') {\n const component = components.get(field.settings.component)?.definition;\n for (const item of Array.isArray(value) ? value : [value]) {\n if (component && isRecord(item)) {\n walkFields(component.fields, item, components, visit);\n }\n }\n } else if (field.type === 'dynamiczone' && Array.isArray(value)) {\n for (const item of value) {\n const component: ComponentDefinition | undefined = isRecord(item)\n ? components.get(String(item[COMPONENT_KEY]))?.definition\n : undefined;\n if (component && isRecord(item)) {\n walkFields(component.fields, item, components, visit);\n }\n }\n }\n }\n};\n\n/** Calls `visit` for every relation value in a document, in document order. */\nexport const walkRelations = (\n model: ContentModel,\n data: Readonly<ContentData>,\n visit: (relation: RelationVisit) => void,\n topLevelFields: readonly FieldDefinition[] = model.definition.fields,\n) => walkFields(topLevelFields, data, model.components, visit);\n\nexport type RelationEdge = { fieldId: string; position: number; targetEntryId: string };\n\n/** The `relation_edges` rows of one head: per field, targets numbered in document order. */\nexport const edgesOf = (model: ContentModel, data: Readonly<ContentData>): RelationEdge[] => {\n const positions = new Map<string, number>();\n const edges: RelationEdge[] = [];\n walkRelations(model, data, ({ field, entryIds }) => {\n for (const targetEntryId of entryIds) {\n const position = positions.get(field.id) ?? 0;\n positions.set(field.id, position + 1);\n edges.push({ fieldId: field.id, position, targetEntryId });\n }\n });\n return edges;\n};\n\n/** Every target a set of documents mentions, grouped by target model. */\nexport const targetsByModel = (\n model: ContentModel,\n documents: readonly Readonly<ContentData>[],\n topLevelFields?: readonly FieldDefinition[],\n): Map<string, Set<string>> => {\n const targets = new Map<string, Set<string>>();\n for (const data of documents) {\n walkRelations(\n model,\n data,\n ({ field, entryIds }) => {\n const set = targets.get(field.settings.target) ?? new Set<string>();\n entryIds.forEach((id) => set.add(id));\n targets.set(field.settings.target, set);\n },\n topLevelFields,\n );\n }\n return targets;\n};\n","import {\n effectiveTitleField,\n isSeoField,\n resolveSeo,\n SEO_COMPONENT_ID,\n SEO_FIELD_IDS,\n seoDefaultsForLocale,\n type DataType,\n type FieldDefinition,\n type SeoDefaults,\n type SeoFieldKey,\n type SeoKeys,\n} from '@shapio/schema';\nimport type { Policy } from '../permissions/types.js';\nimport type { HeadRow } from './compiler/compile.js';\nimport { maskAllows } from './compiler/policy.js';\nimport { fallbackChain } from './locales.js';\nimport type { ContentModel } from './model.js';\nimport type { ReadEnvironment } from './read.js';\n\n/**\n * `?seo=resolved` (plan seo-fields): the SEO fields a delivered entry carries, with the site's defaults filled\n * in. The built-in component is found by stable ID; its fields by stable ID and the type they were created\n * with, so a renamed field still resolves and a removed or retyped one is passed through untouched.\n */\nexport type SeoReadOptions = {\n defaults: SeoDefaults;\n /** The default social image in the delivered shape, when it is a live public image of the site. */\n image: unknown;\n};\n\nconst EXPECTED_TYPES: Readonly<Record<SeoFieldKey, ReadonlySet<DataType>>> = {\n title: new Set(['string', 'text']),\n description: new Set(['text', 'string']),\n image: new Set(['media']),\n canonical: new Set(['url', 'string']),\n noindex: new Set(['boolean']),\n};\n\nconst keysOf = (model: ContentModel): SeoKeys => {\n const component = model.components.get(SEO_COMPONENT_ID)?.definition;\n const keys: SeoKeys = {};\n for (const [key, id] of Object.entries(SEO_FIELD_IDS) as Array<[SeoFieldKey, string]>) {\n const field = component?.fields.find((candidate) => candidate.id === id && !candidate.deprecated);\n const single = field?.type !== 'media' || !field.settings.multiple;\n if (field && single && EXPECTED_TYPES[key].has(field.type)) {\n keys[key] = field.apiKey;\n }\n }\n return keys;\n};\n\n/** The entry's own title, only when the caller may read the title field (never a hidden value). */\nconst fallbackTitleOf = (model: ContentModel, policy: Policy, row: HeadRow): string | null => {\n const field = effectiveTitleField(model.definition);\n if (!field || !maskAllows(policy.readMask, field)) {\n return null;\n }\n const value = row.data[field.id];\n return typeof value === 'string' ? value : null;\n};\n\n/** The locale whose defaults apply: the head's for localized models, the requested one otherwise. */\nconst defaultsLocaleOf = (env: ReadEnvironment, model: ContentModel, row: HeadRow): string =>\n model.definition.localized ? row.locale : (env.locale ?? env.snapshot.defaultLocale);\n\n/** Whether the selected fields include an SEO field (only those are resolved). */\nexport const seoFieldsOf = (fields: readonly FieldDefinition[]): FieldDefinition[] =>\n fields.filter(isSeoField);\n\n/** Replaces each SEO field's projected value with its resolved value. */\nexport const resolveSeoFields = (\n env: ReadEnvironment,\n options: SeoReadOptions,\n model: ContentModel,\n policy: Policy,\n fields: readonly FieldDefinition[],\n row: HeadRow,\n data: Record<string, unknown>,\n): Record<string, unknown> => {\n const seoFields = seoFieldsOf(fields);\n if (seoFields.length === 0) {\n return data;\n }\n const keys = keysOf(model);\n const context = {\n defaults: seoDefaultsForLocale(\n options.defaults,\n fallbackChain(env.snapshot, defaultsLocaleOf(env, model, row)),\n ),\n image: options.image,\n fallbackTitle: fallbackTitleOf(model, policy, row),\n keys,\n };\n const output = { ...data };\n for (const field of seoFields) {\n output[field.apiKey] = resolveSeo(data[field.apiKey], context);\n }\n return output;\n};\n","import type { FieldDefinition, SeoDefaults } from '@shapio/schema';\nimport type { Kysely, Transaction } from 'kysely';\nimport type { DB } from '../db/types.js';\nimport type { PermissionEvaluator, Policy, Principal } from '../permissions/types.js';\nimport * as contentQueriesRepository from '../repositories/contentQueries.js';\nimport * as mediaAssetsRepository from '../repositories/mediaAssets.js';\nimport type { SchemaSnapshot } from '../schema/snapshot.js';\nimport { toAssetViews, type MediaAssetView } from '../services/mediaViews.js';\nimport { compileHeadQuery, type HeadRow, type HeadSource, type LocaleScope } from './compiler/compile.js';\nimport { entryIdIn } from './compiler/conditions.js';\nimport { compileRowFilter } from './compiler/policy.js';\nimport { populateRelations, type PopulateEnvironment } from './compiler/populate.js';\nimport { projectData, selectFields } from './compiler/select.js';\nimport { DEFAULT_RICH_TEXT_MODE, type PopulateTree, type RichTextMode } from './compiler/types.js';\nimport { readScopeFor } from './locales.js';\nimport { mediaIdsOf } from './media.js';\nimport { resolveModelById, type ContentModel } from './model.js';\nimport { targetsByModel } from './relations.js';\nimport { resolveSeoFields, seoFieldsOf } from './seo.js';\n\n/**\n * The read side shared by the admin and delivery APIs: fetching heads under a policy, deciding which\n * relation targets a caller may see, populating, and projecting through the read mask.\n */\ntype MediaViewDependencies = Parameters<typeof toAssetViews>[0];\n\nexport type ReadEnvironment = {\n executor: Kysely<DB> | Transaction<DB>;\n snapshot: SchemaSnapshot;\n permissions: PermissionEvaluator;\n actor: Principal;\n /** The site read from (sites plan §H): heads, relation targets and media never cross it. */\n siteId: string;\n /** Admin reads see draft heads and every relation ID; delivery reads see published content only. */\n audience: 'admin' | 'delivery';\n source: HeadSource;\n /** Requested locale (the default when absent). */\n locale: string | undefined;\n /** Media storage for asset URLs; without it media fields stay asset IDs. */\n media?: MediaViewDependencies;\n /** Delivery reads: the shape of rich-text values, populated targets included (`?richText=`; the default when absent). */\n richText?: RichTextMode;\n /** Delivery reads with `?seo=resolved`: the site's SEO defaults, merged into SEO fields. */\n seo?: SeoDefaults;\n};\n\n/** Evaluated through the read's executor: a read inside a transaction never asks the pool for a second connection. */\nexport const readPolicy = (env: ReadEnvironment, modelId: string): Promise<Policy> =>\n env.permissions.evaluate(env.actor, { action: 'read', modelId }, env.executor);\n\n/**\n * Heads of the given entries that the policy lets the caller read, served per the locale chain (or per\n * `locales` when given: GraphQL's `fallback: false` and `localizations`).\n */\nexport const fetchHeadsByIds = async (\n env: ReadEnvironment,\n model: ContentModel,\n policy: Policy,\n ids: readonly string[],\n locales?: LocaleScope,\n): Promise<HeadRow[]> => {\n if (ids.length === 0 || !policy.allowed) {\n return [];\n }\n const rowFilter = compileRowFilter(policy.rowFilter, env.actor);\n const { rows } = compileHeadQuery({\n siteId: env.siteId,\n modelId: model.definition.id,\n source: env.source,\n locales: locales ?? readScopeFor(env.snapshot, model.definition, env.locale, { fallback: true }),\n conditions: [entryIdIn(ids), ...(rowFilter ? [rowFilter] : [])],\n orderBy: [],\n });\n return contentQueriesRepository.runHeadQuery(rows, env.executor);\n};\n\n/** Target heads read for relation visibility, by target model, for populate to reuse (same policy and locales). */\ntype FetchedTargets = Map<string, { ids: ReadonlySet<string>; rows: readonly HeadRow[] }>;\n\n/**\n * Relation targets the caller may see in these rows: for delivery, only targets that are published (at the\n * same snapshot) and readable under their model's policy, so a published entry never leaks the ID of a\n * draft (brief §10). Admin reads see every ID (null). The heads read are returned for populate.\n */\nconst relationTargets = async (\n env: ReadEnvironment,\n model: ContentModel,\n rows: readonly HeadRow[],\n fields: readonly FieldDefinition[],\n): Promise<{ visible: Set<string> | null; fetched: FetchedTargets }> => {\n const fetched: FetchedTargets = new Map();\n if (env.audience === 'admin') {\n return { visible: null, fetched };\n }\n const visible = new Set<string>();\n const targets = targetsByModel(\n model,\n rows.map((row) => row.data),\n fields,\n );\n for (const [targetModelId, ids] of targets) {\n const target = resolveModelById(env.snapshot, targetModelId);\n if (!target) {\n continue;\n }\n const policy = await readPolicy(env, targetModelId);\n const found = await fetchHeadsByIds(env, target, policy, [...ids]);\n found.forEach((row) => visible.add(row.entry_id));\n fetched.set(targetModelId, { ids, rows: found });\n }\n return { visible, fetched };\n};\n\n/**\n * `fetchHeadsByIds` for populate, answered from the heads relation visibility already read when it asked for\n * every one of these IDs under the same policy and locale scope (populate's default), else from the database.\n */\nconst fetchReusing =\n (fetched: FetchedTargets): PopulateEnvironment['fetch'] =>\n async (env, model, policy, ids) => {\n const known = fetched.get(model.definition.id);\n if (known && ids.every((id) => known.ids.has(id))) {\n const wanted = new Set(ids);\n return known.rows.filter((row) => wanted.has(row.entry_id));\n }\n return fetchHeadsByIds(env, model, policy, ids);\n };\n\n/**\n * Whether projecting these rows reads anything more (relation targets, populated entries, asset views). When\n * not, a delivery read is complete in the one statement that fetched the rows (plan delivery-perf).\n */\nexport const needsFollowUpReads = (\n env: ReadEnvironment,\n model: ContentModel,\n rows: readonly HeadRow[],\n query: { fields: readonly FieldDefinition[] | null; populate: PopulateTree },\n policy: Policy,\n): boolean => {\n if (query.populate.size > 0) {\n return true;\n }\n const fields = selectFields(model, policy.readMask, query.fields);\n const documents = rows.map((row) => row.data);\n return (\n (env.audience === 'delivery' && targetsByModel(model, documents, fields).size > 0) ||\n needsMediaReads(env, model, rows, fields)\n );\n};\n\n/** Whether projecting these rows reads asset views (media fields, rich-text images, a default SEO image). */\nexport const needsMediaReads = (\n env: ReadEnvironment,\n model: ContentModel,\n rows: readonly HeadRow[],\n fields: readonly FieldDefinition[],\n): boolean =>\n env.media !== undefined &&\n (mediaIdsOf(\n model,\n rows.map((row) => row.data),\n fields,\n ).size > 0 ||\n seoImageIdOf(env, fields) !== undefined);\n\n/** The site's default social image a `?seo=resolved` read needs (when the response carries an SEO field). */\nconst seoImageIdOf = (env: ReadEnvironment, fields: readonly FieldDefinition[]): string | undefined =>\n env.seo?.imageId && seoFieldsOf(fields).length > 0 ? env.seo.imageId : undefined;\n\n/** A default SEO image is shown only while it is a public image (never through a signed URL). */\nexport const isPublicImage = (view: Pick<MediaAssetView, 'visibility' | 'mimeType'>): boolean =>\n view.visibility === 'public' && view.mimeType.startsWith('image/');\n\ntype MediaViews = {\n /** Views by asset ID, in the audience's shape; null leaves IDs as stored. */\n byId: Map<string, { url: string }> | null;\n /** The default social image in the delivered shape, only when it is a live public image. */\n seoImage: unknown;\n};\n\n/**\n * Asset views for the media a response shows (media fields and rich-text images). Private assets get signed,\n * expiring URLs (package G); the read mask already decided whether the caller may see the field at all.\n * Delivery views carry only what a site needs, never storage keys or internal attribution.\n */\nconst loadMediaViews = async (\n env: ReadEnvironment,\n model: ContentModel,\n rows: readonly HeadRow[],\n fields: readonly FieldDefinition[],\n): Promise<MediaViews> => {\n if (!env.media) {\n return { byId: null, seoImage: null };\n }\n const ids = mediaIdsOf(\n model,\n rows.map((row) => row.data),\n fields,\n );\n const seoImageId = seoImageIdOf(env, fields);\n if (seoImageId !== undefined) {\n ids.add(seoImageId);\n }\n if (ids.size === 0) {\n return { byId: new Map(), seoImage: null };\n }\n const assets = await mediaAssetsRepository.findLiveManyOnSite(env.siteId, [...ids], env.executor);\n const views = await toAssetViews(env.media, assets, env.executor);\n const shaped = (view: MediaAssetView) => (env.audience === 'delivery' ? toDeliveryAsset(view) : view);\n // Re-checked on every read: an image made private (or replaced by a non-image) after it was chosen never\n // appears, signed or not (rule 7).\n const seoImage = views.find((view) => view.id === seoImageId && isPublicImage(view));\n return {\n byId: new Map(views.map((view) => [view.id, shaped(view)])),\n seoImage: seoImage ? shaped(seoImage) : null,\n };\n};\n\n/** An asset as delivery shows it: what a site needs, never storage keys or internal attribution. */\nexport const toDeliveryAsset = (view: MediaAssetView) => ({\n id: view.id,\n filename: view.filename,\n mimeType: view.mimeType,\n sizeBytes: view.sizeBytes,\n width: view.width,\n height: view.height,\n alt: view.alt,\n caption: view.caption,\n focalPoint: view.focalPoint,\n url: view.url,\n urlExpiresAt: view.urlExpiresAt?.toISOString() ?? null,\n variants: view.variants\n .filter((variant) => variant.url !== null)\n .map(({ name, width, height, format, mimeType, url }) => ({\n name,\n width,\n height,\n format,\n mimeType,\n url,\n })),\n});\n\nexport type ProjectedEntry = Record<string, unknown>;\n\n/** System attributes first, then the readable, selected fields by API key. */\nexport const systemAttributes = (env: ReadEnvironment, row: HeadRow): Record<string, unknown> => ({\n id: row.entry_id,\n locale: row.locale,\n createdAt: row.created_at.toISOString(),\n updatedAt: row.updated_at.toISOString(),\n ...(env.audience === 'delivery' ? { publishedAt: row.updated_at.toISOString() } : {}),\n});\n\nexport const projectRows = async (\n env: ReadEnvironment,\n model: ContentModel,\n policy: Policy,\n rows: readonly HeadRow[],\n query: { fields: readonly FieldDefinition[] | null; populate: PopulateTree },\n): Promise<Array<{ row: HeadRow; data: ProjectedEntry }>> => {\n const fields = selectFields(model, policy.readMask, query.fields);\n const { visible: visibleTargets, fetched } = await relationTargets(env, model, rows, fields);\n const populateEnv: PopulateEnvironment = {\n ...env,\n project: projectRows,\n fetch: fetchReusing(fetched),\n policyFor: readPolicy,\n system: systemAttributes,\n };\n const populated = await populateRelations(populateEnv, rows, fields, query.populate);\n const media = await loadMediaViews(env, model, rows, fields);\n return rows.map((row) => {\n const data = projectData(row.data, {\n model,\n fields,\n visibleTargets,\n populated,\n ...(env.audience === 'delivery' ? { richText: env.richText ?? DEFAULT_RICH_TEXT_MODE } : {}),\n mediaAssets: media.byId,\n });\n return {\n row,\n data: env.seo\n ? resolveSeoFields(\n env,\n { defaults: env.seo, image: media.seoImage },\n model,\n policy,\n fields,\n row,\n data,\n )\n : data,\n };\n });\n};\n","import { QUERY_LIMITS, queryInvalid } from './types.js';\n\n/**\n * Bracket querystrings (`filters[title][$eq]=x`, `filters[$or][0][slug][$eq]=y`, `fields[0]=title`) to a\n * nested object of strings. Lists stay objects with numeric keys here; `parse.ts` decides where a list is\n * expected. Bounded in parameter count, depth and length so a hostile query cannot cost much.\n */\nexport type QueryTree = { [key: string]: QueryValue };\nexport type QueryValue = string | string[] | QueryTree;\n\nconst SEGMENT = /^\\[([^[\\]]*)\\]/;\nconst MAX_DEPTH = 10;\nconst MAX_KEY_LENGTH = 300;\n\nconst splitKey = (key: string): string[] => {\n const open = key.indexOf('[');\n if (open === -1) {\n return [key];\n }\n const segments = [key.slice(0, open)];\n let rest = key.slice(open);\n while (rest.length > 0) {\n const match = SEGMENT.exec(rest);\n if (!match) {\n throw queryInvalid(`Malformed query parameter \"${key}\"`);\n }\n segments.push(match[1] as string);\n rest = rest.slice(match[0].length);\n }\n return segments;\n};\n\nconst isTree = (value: QueryValue | undefined): value is QueryTree =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst assign = (tree: QueryTree, segments: readonly string[], value: string, key: string) => {\n let node = tree;\n segments.forEach((segment, index) => {\n if (segment === '' || segment === '__proto__' || segment === 'constructor' || segment === 'prototype') {\n throw queryInvalid(`Malformed query parameter \"${key}\"`);\n }\n const last = index === segments.length - 1;\n const existing = Object.hasOwn(node, segment) ? node[segment] : undefined;\n if (last) {\n if (existing === undefined) {\n node[segment] = value;\n } else if (typeof existing === 'string') {\n node[segment] = [existing, value];\n } else if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n throw queryInvalid(`Query parameter \"${key}\" conflicts with another parameter`);\n }\n return;\n }\n if (existing === undefined) {\n const child: QueryTree = Object.create(null) as QueryTree;\n node[segment] = child;\n node = child;\n } else if (isTree(existing)) {\n node = existing;\n } else {\n throw queryInvalid(`Query parameter \"${key}\" conflicts with another parameter`);\n }\n });\n};\n\nexport const parseQueryTree = (search: string): QueryTree => {\n const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);\n const tree: QueryTree = Object.create(null) as QueryTree;\n let count = 0;\n for (const [key, value] of params) {\n count += 1;\n if (count > QUERY_LIMITS.maxParameters) {\n throw queryInvalid(`At most ${QUERY_LIMITS.maxParameters} query parameters are allowed`);\n }\n if (key.length > MAX_KEY_LENGTH || value.length > QUERY_LIMITS.maxValueLength) {\n throw queryInvalid('A query parameter is too long');\n }\n const segments = splitKey(key);\n if (segments.length > MAX_DEPTH) {\n throw queryInvalid(`Query parameter \"${key}\" is nested too deeply`);\n }\n assign(tree, segments, value, key);\n }\n return tree;\n};\n\n/** A list value: repeated parameters, `a,b` comma lists, or `[0]`, `[1]`… keys. */\nexport const toList = (value: QueryValue | undefined, { commas = true } = {}): QueryValue[] => {\n if (value === undefined) {\n return [];\n }\n if (Array.isArray(value)) {\n return value;\n }\n if (typeof value === 'string') {\n return commas ? value.split(',').filter((item) => item.length > 0) : [value];\n }\n const keys = Object.keys(value);\n if (!keys.every((key) => /^\\d+$/.test(key))) {\n throw queryInvalid('Expected a list');\n }\n return keys.sort((a, b) => Number(a) - Number(b)).map((key) => value[key] as QueryValue);\n};\n","import {\n effectiveTitleField,\n isSeoField,\n isStableId,\n TEXT_TITLE_FIELD_TYPES,\n type FieldDefinition,\n type ModelDefinition,\n} from '@shapio/schema';\nimport { findFieldByApiKey } from '../model.js';\nimport { assertOperatorAllowed, coerceOperand, isListOperator, isSortable } from './operators.js';\nimport { toList, type QueryTree, type QueryValue } from './querystring.js';\nimport {\n DEFAULT_RICH_TEXT_MODE,\n ENTRY_LIST_STATUSES,\n FILTER_OPERATORS,\n QUERY_LIMITS,\n queryForbidden,\n queryInvalid,\n PUBLICATION_STATES,\n RICH_TEXT_MODES,\n SEO_MODES,\n SYSTEM_ATTRIBUTES,\n type ContentQuery,\n type EntryListStatus,\n type FilterNode,\n type FilterOperator,\n type FilterTarget,\n type PopulateTree,\n type PublicationState,\n type RichTextMode,\n type SeoMode,\n type SortTerm,\n type SystemAttribute,\n} from './types.js';\n\n/**\n * Querystring → content query AST, allowlisted against the pinned schema (brief §4 \"runtime APIs\"). Every\n * field name must be a live field of the model, every operator must be allowed for its type, and fields\n * outside the caller's read mask are rejected with 403 so filters, sorts and searches can never be used as\n * an oracle for hidden values (ADR 0005). Unknown parameters, fields and operators are 400s.\n */\nexport type ParseContext = {\n model: ModelDefinition;\n /** Locale codes the instance has. */\n locales: readonly string[];\n /** The read mask, per field. */\n isReadable: (field: FieldDefinition) => boolean;\n /** `?snapshot=N` is a delivery feature. */\n allowSnapshot: boolean;\n /** `?status=` and `?author=` are admin list features. */\n allowAdminFilters?: boolean;\n /** `?richText=` is a delivery and preview feature (admin reads return the stored document). */\n allowRichText?: boolean;\n /** `?seo=` is a delivery and preview feature. */\n allowSeo?: boolean;\n /** `?publicationState=` is a delivery feature. */\n allowPublicationState?: boolean;\n /** Target models of relation fields, for nested populate paths. */\n resolveModel: (modelId: string) => ModelDefinition | undefined;\n};\n\nconst TOP_LEVEL_KEYS = new Set([\n 'filters',\n 'sort',\n 'page',\n 'pageSize',\n 'fields',\n 'populate',\n 'locale',\n 'snapshot',\n 'richText',\n 'seo',\n 'publicationState',\n 'q',\n 'status',\n 'author',\n]);\nconst OPERATOR_SET: ReadonlySet<string> = new Set(FILTER_OPERATORS);\n\nconst isTree = (value: QueryValue | undefined): value is QueryTree =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst single = (value: QueryValue | undefined, name: string): string | undefined => {\n if (value === undefined) {\n return undefined;\n }\n if (typeof value !== 'string') {\n throw queryInvalid(`\"${name}\" must be given once`);\n }\n return value;\n};\n\nconst positiveInteger = (value: QueryValue | undefined, name: string, max: number): number | undefined => {\n const text = single(value, name);\n if (text === undefined) {\n return undefined;\n }\n if (!/^\\d{1,15}$/.test(text) || Number(text) < 1 || Number(text) > max) {\n throw queryInvalid(`\"${name}\" must be an integer between 1 and ${max}`);\n }\n return Number(text);\n};\n\ntype FilterState = { context: ParseContext; conditions: number };\n\nconst resolveTarget = (context: ParseContext, key: string): FilterTarget => {\n if ((SYSTEM_ATTRIBUTES as readonly string[]).includes(key)) {\n return { kind: 'system', name: key as SystemAttribute };\n }\n const field = findFieldByApiKey(context.model.fields, key);\n if (!field) {\n throw queryInvalid(`Unknown field \"${key}\"`, { field: key });\n }\n if (!context.isReadable(field)) {\n throw queryForbidden(`You may not filter, sort or search on \"${key}\"`, { field: key });\n }\n return { kind: 'field', field };\n};\n\nconst conditionFor = (\n state: FilterState,\n target: FilterTarget,\n operator: string,\n raw: QueryValue,\n label: string,\n): FilterNode => {\n if (!OPERATOR_SET.has(operator)) {\n throw queryInvalid(`Unknown operator \"${operator}\" on \"${label}\"`);\n }\n const op = operator as FilterOperator;\n state.conditions += 1;\n if (state.conditions > QUERY_LIMITS.maxConditions) {\n throw queryInvalid(`At most ${QUERY_LIMITS.maxConditions} filter conditions are allowed`);\n }\n assertOperatorAllowed(target, op, label);\n if (isTree(raw) && !isListOperator(op)) {\n throw queryInvalid(`\"${label}\" ${op} needs a value`);\n }\n const operand = isListOperator(op) ? toList(raw) : raw;\n if (!isListOperator(op) && Array.isArray(operand)) {\n throw queryInvalid(`\"${label}\" ${op} needs a single value`);\n }\n return {\n kind: 'condition',\n target,\n operator: op,\n value: coerceOperand(target, op, operand, label, QUERY_LIMITS.maxInValues),\n };\n};\n\nconst combine = (kind: 'and' | 'or', nodes: FilterNode[]): FilterNode =>\n nodes.length === 1 ? (nodes[0] as FilterNode) : { kind, nodes };\n\nconst parseFilterTree = (state: FilterState, tree: QueryTree, depth: number): FilterNode => {\n if (depth > QUERY_LIMITS.maxFilterDepth) {\n throw queryInvalid('Filters are nested too deeply');\n }\n const nodes: FilterNode[] = [];\n for (const [key, value] of Object.entries(tree)) {\n if (key === '$and' || key === '$or') {\n const children = toList(value, { commas: false }).map((child) => {\n if (!isTree(child)) {\n throw queryInvalid(`${key} needs a list of filter objects`);\n }\n return parseFilterTree(state, child, depth + 1);\n });\n if (children.length === 0) {\n throw queryInvalid(`${key} needs at least one filter`);\n }\n nodes.push(combine(key === '$and' ? 'and' : 'or', children));\n } else if (key === '$not') {\n if (!isTree(value)) {\n throw queryInvalid('$not needs a filter object');\n }\n nodes.push({ kind: 'not', node: parseFilterTree(state, value, depth + 1) });\n } else if (key.startsWith('$')) {\n throw queryInvalid(`Unknown filter keyword \"${key}\"`);\n } else {\n const target = resolveTarget(state.context, key);\n if (!isTree(value)) {\n nodes.push(conditionFor(state, target, '$eq', value, key));\n continue;\n }\n const operators = Object.entries(value);\n if (operators.length === 0) {\n throw queryInvalid(`\"${key}\" needs an operator`);\n }\n nodes.push(\n combine(\n 'and',\n operators.map(([operator, operand]) => conditionFor(state, target, operator, operand, key)),\n ),\n );\n }\n }\n if (nodes.length === 0) {\n throw queryInvalid('Empty filter');\n }\n return combine('and', nodes);\n};\n\nconst parseSort = (context: ParseContext, value: QueryValue | undefined): SortTerm[] => {\n const items = toList(value);\n if (items.length > QUERY_LIMITS.maxSortTerms) {\n throw queryInvalid(`At most ${QUERY_LIMITS.maxSortTerms} sort terms are allowed`);\n }\n return items.map((item) => {\n if (typeof item !== 'string') {\n throw queryInvalid('sort needs \"field\" or \"field:asc|desc\" values');\n }\n const [key = '', direction = 'asc', extra] = item.split(':');\n if (extra !== undefined || (direction !== 'asc' && direction !== 'desc')) {\n throw queryInvalid(`Invalid sort \"${item}\"`);\n }\n const target = resolveTarget(context, key);\n if (target.kind === 'field' && !isSortable(target.field)) {\n throw queryInvalid(`\"${key}\" is not sortable: mark it sortable first`);\n }\n return { target, direction };\n });\n};\n\nconst parseFields = (context: ParseContext, value: QueryValue | undefined): FieldDefinition[] | null => {\n if (value === undefined) {\n return null;\n }\n return toList(value).map((item) => {\n if (typeof item !== 'string') {\n throw queryInvalid('fields needs a list of field names');\n }\n const target = resolveTarget(context, item);\n if (target.kind === 'system') {\n throw queryInvalid(`\"${item}\" is always returned; list only model fields`);\n }\n return target.field;\n });\n};\n\nconst relationField = (context: ParseContext, model: ModelDefinition, key: string): FieldDefinition => {\n const field = findFieldByApiKey(model.fields, key);\n if (!field || field.type !== 'relation') {\n throw queryInvalid(`\"${key}\" is not a relation of \"${model.apiKey}\" and cannot be populated`);\n }\n if (model.id === context.model.id && !context.isReadable(field)) {\n throw queryForbidden(`You may not populate \"${key}\"`, { field: key });\n }\n return field;\n};\n\ntype MutablePopulate = Map<string, MutablePopulate>;\n\nconst parsePopulate = (context: ParseContext, value: QueryValue | undefined): PopulateTree => {\n const root: MutablePopulate = new Map();\n for (const item of toList(value)) {\n if (typeof item !== 'string') {\n throw queryInvalid('populate needs a list of relation paths');\n }\n if (item === '*') {\n for (const field of context.model.fields) {\n if (field.type === 'relation' && !field.deprecated && context.isReadable(field)) {\n root.set(field.id, root.get(field.id) ?? new Map<string, MutablePopulate>());\n }\n }\n continue;\n }\n const segments = item.split('.');\n if (segments.length > QUERY_LIMITS.maxPopulateDepth) {\n throw queryInvalid(`populate is limited to ${QUERY_LIMITS.maxPopulateDepth} levels`);\n }\n let model: ModelDefinition | undefined = context.model;\n let node = root;\n for (const segment of segments) {\n if (!model) {\n throw queryInvalid(`Cannot populate \"${item}\"`);\n }\n const field = relationField(context, model, segment);\n const next: MutablePopulate = node.get(field.id) ?? new Map<string, MutablePopulate>();\n node.set(field.id, next);\n node = next;\n model = field.type === 'relation' ? context.resolveModel(field.settings.target) : undefined;\n }\n }\n return root;\n};\n\nconst parseSearch = (context: ParseContext, value: QueryValue | undefined): ContentQuery['search'] => {\n const text = single(value, 'q')?.trim();\n if (!text) {\n return null;\n }\n const field = effectiveTitleField(context.model);\n if (!field || !TEXT_TITLE_FIELD_TYPES.has(field.type)) {\n throw queryInvalid(`\"${context.model.apiKey}\" has no text title field to search`);\n }\n if (!context.isReadable(field)) {\n throw queryForbidden('You may not search this model', { field: field.apiKey });\n }\n return { field, text };\n};\n\n/** Parses a bracket querystring tree into a content query for one model. */\nconst STATUS_SET: ReadonlySet<string> = new Set(ENTRY_LIST_STATUSES);\n\n/** `status` and `author` (admin lists only). */\nconst parseAdminFilters = (tree: QueryTree, context: ParseContext) => {\n const status = single(tree.status, 'status');\n const author = single(tree.author, 'author');\n if ((status !== undefined || author !== undefined) && !context.allowAdminFilters) {\n throw queryInvalid('\"status\" and \"author\" are only available on the admin API');\n }\n if (status !== undefined && !STATUS_SET.has(status)) {\n throw queryInvalid(`\"status\" must be one of ${ENTRY_LIST_STATUSES.join(', ')}`);\n }\n if (author !== undefined && !isStableId(author)) {\n throw queryInvalid('\"author\" must be an admin user ID');\n }\n return {\n ...(status !== undefined ? { status: status as EntryListStatus } : {}),\n ...(author !== undefined ? { author } : {}),\n };\n};\n\nconst RICH_TEXT_MODE_SET: ReadonlySet<string> = new Set(RICH_TEXT_MODES);\n\n/** `?richText=json|html|both` on delivery and preview reads; the default there, nothing on admin reads. */\nconst parseRichText = (tree: QueryTree, context: ParseContext): { richText?: RichTextMode } => {\n const value = single(tree.richText, 'richText');\n if (!context.allowRichText) {\n if (value !== undefined) {\n throw queryInvalid('richText is only available on the delivery and preview APIs');\n }\n return {};\n }\n if (value !== undefined && !RICH_TEXT_MODE_SET.has(value)) {\n throw queryInvalid(`\"richText\" must be one of ${RICH_TEXT_MODES.join(', ')}`);\n }\n return { richText: (value as RichTextMode | undefined) ?? DEFAULT_RICH_TEXT_MODE };\n};\n\nconst SEO_MODE_SET: ReadonlySet<string> = new Set(SEO_MODES);\n\n/** `?seo=raw|resolved` on delivery and preview reads; `resolved` needs a readable SEO field on the model. */\nconst parseSeo = (tree: QueryTree, context: ParseContext): { seo?: SeoMode } => {\n const value = single(tree.seo, 'seo');\n if (value === undefined) {\n return {};\n }\n if (!context.allowSeo) {\n throw queryInvalid('seo is only available on the delivery and preview APIs');\n }\n if (!SEO_MODE_SET.has(value)) {\n throw queryInvalid(`\"seo\" must be one of ${SEO_MODES.join(', ')}`);\n }\n if (\n value === 'resolved' &&\n !context.model.fields.some((field) => !field.deprecated && isSeoField(field) && context.isReadable(field))\n ) {\n throw queryInvalid(`\"${context.model.apiKey}\" has no SEO field to resolve`);\n }\n return { seo: value as SeoMode };\n};\n\nconst PUBLICATION_STATE_SET: ReadonlySet<string> = new Set(PUBLICATION_STATES);\n\n/**\n * `?publicationState=published|draft` on delivery reads; whether the caller may read drafts is the service's\n * check.\n */\nconst parsePublicationState = (\n tree: QueryTree,\n context: ParseContext,\n): { publicationState?: PublicationState } => {\n const value = single(tree.publicationState, 'publicationState');\n if (value === undefined) {\n return {};\n }\n if (!context.allowPublicationState) {\n throw queryInvalid('publicationState is only available on the delivery API');\n }\n if (!PUBLICATION_STATE_SET.has(value)) {\n throw queryInvalid(`\"publicationState\" must be one of ${PUBLICATION_STATES.join(', ')}`);\n }\n return { publicationState: value as PublicationState };\n};\n\nexport const parseContentQuery = (tree: QueryTree, context: ParseContext): ContentQuery => {\n for (const key of Object.keys(tree)) {\n if (!TOP_LEVEL_KEYS.has(key)) {\n throw queryInvalid(`Unknown query parameter \"${key}\"`);\n }\n }\n const filters = tree.filters;\n if (filters !== undefined && !isTree(filters)) {\n throw queryInvalid('filters needs field conditions, e.g. filters[title][$eq]=Hello');\n }\n const locale = single(tree.locale, 'locale');\n if (locale !== undefined && !context.locales.includes(locale)) {\n throw queryInvalid(`Unknown locale \"${locale}\"`);\n }\n const snapshot = positiveInteger(tree.snapshot, 'snapshot', Number.MAX_SAFE_INTEGER);\n if (snapshot !== undefined && !context.allowSnapshot) {\n throw queryInvalid('snapshot is only available on the delivery API');\n }\n return {\n filter: filters ? parseFilterTree({ context, conditions: 0 }, filters, 1) : null,\n search: parseSearch(context, tree.q),\n sort: parseSort(context, tree.sort),\n page: positiveInteger(tree.page, 'page', 1_000_000) ?? 1,\n pageSize:\n positiveInteger(tree.pageSize, 'pageSize', QUERY_LIMITS.maxPageSize) ?? QUERY_LIMITS.defaultPageSize,\n fields: parseFields(context, tree.fields),\n populate: parsePopulate(context, tree.populate),\n locale,\n snapshot,\n ...parseRichText(tree, context),\n ...parseSeo(tree, context),\n ...parsePublicationState(tree, context),\n ...parseAdminFilters(tree, context),\n };\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport type { ContentData } from '../db/contentData.js';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type RevisionReason =\n 'create' | 'save' | 'publish' | 'restore' | 'duplicate' | 'localize' | 'conversion';\n\nexport type NewRevision = {\n entryId: string;\n locale: string;\n schemaRevisionId: string;\n data: ContentData;\n reason: RevisionReason;\n authorType: string;\n authorId: string | null;\n parentRevisionId: string | null;\n};\n\nconst SUMMARY_COLUMNS = [\n 'id',\n 'entry_id',\n 'locale',\n 'schema_revision_id',\n 'reason',\n 'author_type',\n 'author_id',\n 'parent_revision_id',\n 'created_at',\n] as const;\n\nexport const insert = (revision: NewRevision, trx: Executor = db) =>\n trx\n .insertInto('content_revisions')\n .values({\n entry_id: revision.entryId,\n locale: revision.locale,\n schema_revision_id: revision.schemaRevisionId,\n // pg serializes objects as JSON (content data is always an object, never an array).\n data: revision.data,\n reason: revision.reason,\n author_type: revision.authorType,\n author_id: revision.authorId,\n parent_revision_id: revision.parentRevisionId,\n })\n .returning(SUMMARY_COLUMNS)\n .executeTakeFirstOrThrow();\n\nexport const findById = (id: string, executor: Executor = db) =>\n executor\n .selectFrom('content_revisions')\n .select([...SUMMARY_COLUMNS, 'data'])\n .where('id', '=', id)\n .executeTakeFirst();\n\n/** Stored data of several revisions at once (titles and covers for the snapshot diff). */\nexport const findDataByIds = (ids: readonly string[], executor: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : executor\n .selectFrom('content_revisions')\n .select(['id', 'data'])\n .where('id', 'in', [...ids])\n .execute();\n\nexport const listForEntry = (\n entryId: string,\n locale: string | null,\n limit: number,\n executor: Executor = db,\n) => {\n let query = executor\n .selectFrom('content_revisions')\n .select(SUMMARY_COLUMNS)\n .where('entry_id', '=', entryId)\n .orderBy('created_at', 'desc')\n .orderBy('id', 'desc')\n .limit(limit);\n if (locale) {\n query = query.where('locale', '=', locale);\n }\n return query.execute();\n};\n\nexport type RevisionSummaryRow = Awaited<ReturnType<typeof listForEntry>>[number];\n\n/** Removes a deleted locale's history (purge follow-up). Revisions' parents never cross locales. */\nexport const deleteForLocale = (locale: string, trx: Executor = db) =>\n trx.deleteFrom('content_revisions').where('locale', '=', locale).executeTakeFirst();\n","import { sql, type Expression, type RawBuilder } from 'kysely';\nimport { isMysql, isSqlite } from '../dialect.js';\nimport { SequenceValue } from '../mysql/parameters.js';\nimport { asBigint, asJson } from './typed.js';\n\n/**\n * Typed values, arrays and sequences (dialect boundary, ADR 0001). Array columns (`text[]`) are read and\n * written as JavaScript arrays at the driver boundary; SQLite stores them as JSON text through its value\n * codec, so plain Kysely reads and writes of those columns stay unchanged.\n */\n\n/** Element types an array parameter may carry. */\nexport type ArrayElementType = 'uuid' | 'text';\n\nconst ARRAY_TYPES = {\n uuid: sql`uuid[]`,\n text: sql`text[]`,\n} as const satisfies Record<ArrayElementType, RawBuilder<unknown>>;\n\n/**\n * A bound value read as a UUID.\n * PostgreSQL: `cast($1 as uuid)`. SQLite: the lowercased text (UUIDs are stored as lowercase text, which\n * compares and sorts like PostgreSQL's uuid; other values compare case-sensitively there). MySQL: the\n * lowercased text too (UUID columns are `CHAR(36)` with a case-insensitive collation).\n */\nexport const uuidParam = (value: string | null): RawBuilder<string> =>\n isSqlite() || isMysql()\n ? sql<string>`${value === null ? null : value.toLowerCase()}`\n : sql<string>`cast(${value} as uuid)`;\n\n/**\n * `value` equals one of `values`, bound as a single parameter (so the statement text and plan do not\n * depend on the list length).\n * PostgreSQL: `value = any($1::uuid[])`. SQLite: `value in (select value from json_each($1))` with the list\n * bound as JSON. MySQL: `value in (?, …)` (its client binds parameters into the text, so there is no plan to\n * keep stable). Contract: false for an empty list; null never matches.\n */\nexport const anyOf = (\n value: Expression<unknown>,\n values: readonly string[],\n type: ArrayElementType,\n): RawBuilder<boolean> => {\n if (isMysql()) {\n return values.length === 0 ? sql<boolean>`false` : sql<boolean>`${value} in (${sql.join([...values])})`;\n }\n return isSqlite()\n ? sql<boolean>`${value} in (select value from json_each(${JSON.stringify(values)}))`\n : sql<boolean>`${value} = any(${[...values]}::${ARRAY_TYPES[type]})`;\n};\n\n/**\n * An array column contains `value`.\n * PostgreSQL: `$1 = any(column)`. SQLite: `exists (select 1 from json_each(column) where value = $1)`.\n * MySQL: `$1 member of (column)` (array columns are JSON).\n * Contract: false for a null or empty array.\n */\nexport const arrayContains = (\n column: Expression<readonly string[] | null>,\n value: string,\n): RawBuilder<boolean> => {\n if (isMysql()) {\n return sql<boolean>`${value} member of (${column})`;\n }\n return isSqlite()\n ? sql<boolean>`exists (select 1 from json_each(${column}) where value = ${value})`\n : sql<boolean>`${value} = any(${column})`;\n};\n\n/**\n * The values of `column` across the group, sorted by the value itself (optionally distinct); null when the\n * group is empty (pair with `emptyArray` in a `coalesce`, and mark the coalesce with `asJson`).\n * PostgreSQL: `array_agg([distinct] col order by col)`. SQLite: `json_group_array([distinct] col order by\n * col)`, which gives `[]` instead of null for an empty group. MySQL: `group_concat` of the JSON-quoted text\n * values, cast to JSON (`JSON_ARRAYAGG` has no ORDER BY); null for an empty group. Contract: a JavaScript\n * array once read.\n */\nexport const sortedArrayAgg = <T>(column: string, options: { distinct?: boolean } = {}): RawBuilder<T[]> => {\n const distinct = options.distinct ? sql`distinct ` : sql``;\n if (isMysql()) {\n return mysqlJsonArrayOf<T[]>(sql`${distinct}json_quote(${sql.ref(column)}) order by ${sql.ref(column)}`);\n }\n return isSqlite()\n ? asJson<T[]>(sql`json_group_array(${distinct}${sql.ref(column)} order by ${sql.ref(column)})`)\n : sql<T[]>`array_agg(${distinct}${sql.ref(column)} order by ${sql.ref(column)})`;\n};\n\n/**\n * An empty array of the given element type.\n * PostgreSQL: `'{}'::uuid[]`. SQLite: `'[]'`. MySQL: `json_array()`. Contract: an empty JavaScript array\n * once read.\n */\nexport const emptyArray = <T>(type: ArrayElementType): RawBuilder<T[]> => {\n if (isMysql()) {\n return sql<T[]>`json_array()`;\n }\n return isSqlite() ? asJson<T[]>(sql`'[]'`) : sql<T[]>`'{}'::${ARRAY_TYPES[type]}`;\n};\n\n/**\n * The value of the first row (by `orderColumn` descending) whose `column` is not null, per group.\n * PostgreSQL: `(array_agg(col order by o desc) filter (where col is not null))[1]`. SQLite: the first\n * element of the matching `json_group_array`. MySQL: the first element of an ordered `group_concat` (which\n * skips nulls), as text. Contract: null when no row of the group has a value. Mark the selected result with\n * its type (e.g. `asBigint`) for SQLite.\n */\nexport const latestNonNull = <T>(column: string, orderColumn: string): RawBuilder<T | null> => {\n if (isMysql()) {\n // group_concat skips nulls; the first element of the JSON array is the latest non-null value.\n return sql<T | null>`json_unquote(json_extract(${mysqlJsonArrayOf(\n sql`json_quote(cast(${sql.ref(column)} as char)) order by ${sql.ref(orderColumn)} desc`,\n )}, '$[0]'))`;\n }\n return isSqlite()\n ? sql<T | null>`(json_group_array(${sql.ref(column)} order by ${sql.ref(orderColumn)} desc)\n filter (where ${sql.ref(column)} is not null)) ->> 0`\n : sql<T | null>`(array_agg(${sql.ref(column)} order by ${sql.ref(orderColumn)} desc)\n filter (where ${sql.ref(column)} is not null))[1]`;\n};\n\n/** Database sequences Shapio uses. */\nexport type SequenceName = 'entry_heads_change_seq';\n\n/**\n * The next value of a database sequence.\n * PostgreSQL: `nextval('name')`. SQLite: `shapio_nextval('name')`, a counter row in `sequences` that rolls\n * back with its transaction (so SQLite numbers have no gaps). MySQL: a parameter the driver fills just before\n * the statement runs, from an AUTO_INCREMENT table (no lock held to commit). Contract: unique and increasing across every\n * transaction (gaps allowed; a value may commit after a higher one). Returned as a string (bigint).\n */\nexport const nextSequenceValue = (name: SequenceName): RawBuilder<string> => {\n if (isMysql()) {\n return sql<string>`${new SequenceValue(name)}`;\n }\n return isSqlite()\n ? asBigint(sql`shapio_nextval(${sql.lit(name)})`)\n : sql<string>`nextval(${sql.lit(name)})`;\n};\n\n/**\n * MySQL: `cast(concat('[', group_concat(<json texts> separator ','), ']') as json)`, an ordered JSON array\n * aggregate (`JSON_ARRAYAGG` takes no ORDER BY); null for an empty group, like PostgreSQL's aggregates.\n * `items` is the inside of `group_concat`: JSON text per row and its ORDER BY.\n */\nexport const mysqlJsonArrayOf = <T>(items: RawBuilder<unknown>): RawBuilder<T> =>\n sql<T>`cast(concat('[', group_concat(${items} separator ','), ']') as json)`;\n","import { sql, type Expression, type RawBuilder } from 'kysely';\nimport { isMysql, isSqlite } from '../dialect.js';\nimport type { JsonArray } from '../types.js';\nimport { asJson } from './typed.js';\nimport { mysqlJsonArrayOf } from './values.js';\n\n/**\n * JSON primitives for Shapio's own JSON columns and aggregates (dialect boundary, ADR 0001). Content\n * values are compiled by `content/compiler`, not here.\n *\n * `flavor` picks PostgreSQL's `jsonb` or `json` functions. Both read back as the same JavaScript value\n * (only key order differs); it exists so existing statements keep their exact SQL. SQLite has one JSON\n * type (JSON text) and ignores it, as does MySQL (`JSON`).\n *\n * On SQLite the values inside a JSON object or array are SQL scalars: text, numbers and null keep their\n * JSON type, a boolean column becomes 0/1 and a JSON column is embedded as a string. Shapio only builds JSON\n * from text, UUID and number columns here.\n */\nexport type JsonFlavor = 'jsonb' | 'json';\n\n/** One ordering term of an aggregate. */\nexport type AggregateOrder = { column: string; direction?: 'asc' | 'desc'; nulls?: 'first' | 'last' };\n\nconst FUNCTIONS = {\n jsonb: { object: sql`jsonb_build_object`, agg: sql`jsonb_agg`, emptyArray: sql`'[]'::jsonb` },\n json: { object: sql`json_build_object`, agg: sql`json_agg`, emptyArray: sql`'[]'::json` },\n} as const;\n\nconst DIRECTIONS = { asc: sql``, desc: sql` desc` } as const;\nconst NULLS = { first: sql` nulls first`, last: sql` nulls last` } as const;\n/** PostgreSQL's default null placement, which SQLite reverses; written out on SQLite. */\nconst DEFAULT_NULLS = { asc: 'last', desc: 'first' } as const;\n\n/** MySQL has no NULLS FIRST/LAST and sorts NULLs as the smallest value: sort on `col is null` first. */\nconst mysqlOrderTerm = ({ column, direction = 'asc', nulls }: AggregateOrder) => {\n const placement = nulls ?? DEFAULT_NULLS[direction];\n const nullsLast = placement === 'last';\n const mysqlNullsLast = direction === 'desc';\n const term = sql`${sql.ref(column)}${DIRECTIONS[direction]}`;\n return nullsLast === mysqlNullsLast\n ? term\n : sql`(${sql.ref(column)} is null)${nullsLast ? sql`` : sql` desc`}, ${term}`;\n};\n\nconst orderTerm = (order: AggregateOrder) => {\n if (isMysql()) {\n return mysqlOrderTerm(order);\n }\n const { column, direction = 'asc', nulls } = order;\n const placement = nulls ?? (isSqlite() ? DEFAULT_NULLS[direction] : undefined);\n return sql`${sql.ref(column)}${DIRECTIONS[direction]}${placement ? NULLS[placement] : sql``}`;\n};\n\nconst pairs = (fields: Readonly<Record<string, Expression<unknown>>>) =>\n sql.join(Object.entries(fields).map(([key, value]) => sql`${sql.lit(key)}, ${value}`));\n\n/**\n * A JSON object from named expressions. Keys are fixed by the caller's code (emitted as literals).\n * PostgreSQL: `jsonb_build_object('key', value, …)`. SQLite and MySQL: `json_object('key', value, …)`.\n * Contract: a plain object once read.\n */\nexport const jsonObject = <T>(\n fields: Readonly<Record<string, Expression<unknown>>>,\n flavor: JsonFlavor = 'jsonb',\n): RawBuilder<T> =>\n isSqlite() || isMysql()\n ? asJson<T>(sql`json_object(${pairs(fields)})`)\n : sql<T>`${FUNCTIONS[flavor].object}(${pairs(fields)})`;\n\n/**\n * The group's values as a JSON array, in `orderBy` order; null when the group is empty (pair with\n * `emptyJsonArray` in a `coalesce`, and mark the coalesce with `asJson`).\n * PostgreSQL: `jsonb_agg(value order by …)`. SQLite: `json_group_array(value order by …)` (`[]` for an empty\n * group). MySQL: an ordered `group_concat` of each value as JSON, cast to JSON (null for an empty group).\n * Contract: an array once read.\n */\nexport const jsonAgg = <T>(\n value: Expression<unknown>,\n orderBy: readonly AggregateOrder[],\n flavor: JsonFlavor = 'jsonb',\n): RawBuilder<T[]> => {\n const order = sql.join(orderBy.map(orderTerm));\n if (isMysql()) {\n // Each value as JSON (`json_extract(json_array(v), '$[0]')` quotes text and keeps JSON as JSON).\n return mysqlJsonArrayOf<T[]>(sql`json_extract(json_array(${value}), '$[0]') order by ${order}`);\n }\n return isSqlite()\n ? asJson<T[]>(sql`json_group_array(${value} order by ${order})`)\n : sql<T[]>`${FUNCTIONS[flavor].agg}(${value} order by ${order})`;\n};\n\n/**\n * An empty JSON array.\n * PostgreSQL: `'[]'::jsonb`. SQLite: `json('[]')`. MySQL: `json_array()`. Contract: an empty JavaScript\n * array once read.\n */\nexport const emptyJsonArray = <T>(flavor: JsonFlavor = 'jsonb'): RawBuilder<T[]> => {\n if (isMysql()) {\n return sql<T[]>`json_array()`;\n }\n return isSqlite() ? asJson<T[]>(sql`json('[]')`) : sql<T[]>`${FUNCTIONS[flavor].emptyArray}`;\n};\n\n/**\n * A JSON array column with `value` appended as one more element (a log that grows by one entry per write).\n * PostgreSQL: `column || $1::jsonb` with `[value]` bound. SQLite: `json_insert(column, '$[#]', json($1))`\n * with `value` bound as JSON. MySQL: `json_array_append(column, '$', cast($1 as json))`. Contract: the\n * stored array gains exactly one element at the end; the column must not be null.\n */\nexport const jsonArrayAppend = (column: string, value: unknown): RawBuilder<JsonArray> =>\n isMysql()\n ? sql<JsonArray>`json_array_append(${sql.ref(column)}, '$', cast(${JSON.stringify(value)} as json))`\n : isSqlite()\n ? sql<JsonArray>`json_insert(${sql.ref(column)}, '$[#]', json(${JSON.stringify(value)}))`\n : sql<JsonArray>`${sql.ref(column)} || ${JSON.stringify([value])}::jsonb`;\n","import {\n compileAuthorCondition,\n compileFilter,\n filterReadsEntries,\n compileHeadQuery,\n compileSearch,\n compileStatusCondition,\n type HeadRow,\n} from '../content/compiler/compile.js';\nimport { paginationMeta, toLimitOffset, type Pagination } from '../content/compiler/paginate.js';\nimport { parseContentQuery } from '../content/compiler/parse.js';\nimport { compileRowFilter, maskAllows } from '../content/compiler/policy.js';\nimport { parseQueryTree } from '../content/compiler/querystring.js';\nimport { compileOrderBy, defaultSortTerms } from '../content/compiler/sort.js';\nimport type { ContentQuery } from '../content/compiler/types.js';\nimport { entryLocaleNotFound, revisionNotFound } from '../content/errors.js';\nimport { outdatedSharedLocales, readScopeFor } from '../content/locales.js';\nimport { resolveModelById, type ContentModel } from '../content/model.js';\nimport { projectRows, type ReadEnvironment } from '../content/read.js';\nimport type { Policy } from '../permissions/types.js';\nimport * as contentQueriesRepository from '../repositories/contentQueries.js';\nimport * as contentRevisionsRepository from '../repositories/contentRevisions.js';\nimport * as entriesRepository from '../repositories/entries.js';\nimport type { EntryRow } from '../repositories/entries.js';\nimport * as entryHeadsRepository from '../repositories/entryHeads.js';\nimport type { HeadRecord } from '../repositories/entryHeads.js';\nimport { assertEntryVisible, modelWithPolicy, type ContentServiceContext } from './contentAccess.js';\nimport { listDetailsFor, type ListAuthor, type ListLocaleStatus } from './contentListDetails.js';\n\n/** Admin reads of content: drafts, per-locale status, revisions. */\n\nexport type EntryStatus = 'draft' | 'published' | 'modified';\n\nexport const statusOf = (\n draft: Pick<HeadRecord, 'revision_id' | 'autosaved_at'>,\n published?: Pick<HeadRecord, 'revision_id'>,\n): EntryStatus => {\n if (!published) {\n return 'draft';\n }\n return published.revision_id === draft.revision_id && !draft.autosaved_at ? 'published' : 'modified';\n};\n\nconst adminEnvironment = (context: ContentServiceContext, locale: string | undefined): ReadEnvironment => ({\n executor: context.db,\n snapshot: context.snapshot,\n permissions: context.permissions,\n actor: context.actor,\n siteId: context.site.id,\n audience: 'admin',\n source: { kind: 'heads', state: 'draft' },\n locale,\n ...(context.media ? { media: context.media } : {}),\n});\n\nconst toHeadRow = (\n entry: EntryRow,\n head: Pick<HeadRecord, 'locale' | 'data' | 'version' | 'revision_id' | 'updated_at' | 'autosaved_at'>,\n): HeadRow => ({\n entry_id: entry.id,\n locale: head.locale,\n data: head.data,\n version: head.version,\n revision_id: head.revision_id,\n updated_at: head.updated_at,\n autosaved_at: head.autosaved_at,\n created_at: entry.created_at,\n entry_updated_at: entry.updated_at,\n created_by_admin_id: entry.created_by_admin_id,\n owner_app_user_id: entry.owner_app_user_id,\n});\n\nexport type LocaleState = {\n locale: string;\n version: number;\n status: EntryStatus;\n publishedAt: string | null;\n /** The published version's shared fields are older than the draft's (ADR 0004 prompt). */\n sharedOutdated: boolean;\n};\n\nconst localeStates = (model: ContentModel, heads: readonly HeadRecord[]): LocaleState[] => {\n const drafts = heads.filter((head) => head.state === 'draft');\n const publishedOf = (locale: string) =>\n heads.find((head) => head.state === 'published' && head.locale === locale);\n const outdated = new Set(\n outdatedSharedLocales(\n model.definition,\n drafts.map((draft) => {\n const published = publishedOf(draft.locale);\n return {\n locale: draft.locale,\n draft: draft.data,\n ...(published ? { published: published.data } : {}),\n };\n }),\n ),\n );\n return drafts.map((draft) => {\n const published = publishedOf(draft.locale);\n return {\n locale: draft.locale,\n version: draft.version,\n status: statusOf(draft, published),\n publishedAt: published?.updated_at.toISOString() ?? null,\n sharedOutdated: outdated.has(draft.locale),\n };\n });\n};\n\nexport type AdminEntryView = {\n id: string;\n model: string;\n locale: string;\n version: number;\n revisionId: string;\n status: EntryStatus;\n /** Saved without full validation (autosave or duplicate): Save or Publish validates it. */\n autosaved: boolean;\n createdAt: string;\n updatedAt: string;\n publishedAt: string | null;\n data: Record<string, unknown>;\n locales: LocaleState[];\n /** Other published locales whose live shared values are older than their drafts. */\n sharedOutdatedLocales: string[];\n};\n\n/** One entry's draft in one locale, with every locale's status. */\nexport const buildAdminEntryView = async (\n context: ContentServiceContext,\n model: ContentModel,\n policy: Policy,\n entry: EntryRow,\n requestedLocale: string | undefined,\n): Promise<AdminEntryView> => {\n const heads = await entryHeadsRepository.findForEntry(entry.id, context.db);\n const drafts = heads.filter((head) => head.state === 'draft');\n const locale = model.definition.localized\n ? (requestedLocale ?? context.snapshot.defaultLocale)\n : drafts[0]?.locale;\n const draft = drafts.find((head) => head.locale === locale);\n if (!draft || locale === undefined) {\n throw entryLocaleNotFound(\n entry.id,\n locale ?? context.snapshot.defaultLocale,\n drafts.map((head) => head.locale),\n );\n }\n const published = heads.find((head) => head.state === 'published' && head.locale === locale);\n const [projected] = await projectRows(\n adminEnvironment(context, locale),\n model,\n policy,\n [toHeadRow(entry, draft)],\n {\n fields: null,\n populate: new Map(),\n },\n );\n const states = localeStates(model, heads);\n return {\n id: entry.id,\n model: model.definition.apiKey,\n locale,\n version: draft.version,\n revisionId: draft.revision_id,\n status: statusOf(draft, published),\n autosaved: draft.autosaved_at !== null,\n createdAt: entry.created_at.toISOString(),\n updatedAt: draft.updated_at.toISOString(),\n publishedAt: published?.updated_at.toISOString() ?? null,\n data: projected?.data ?? {},\n locales: states,\n sharedOutdatedLocales: states\n .filter((state) => state.sharedOutdated && state.locale !== locale)\n .map((state) => state.locale),\n };\n};\n\n/**\n * The entry view returned after a write. The caller may lack `read` (a write-only role): it then sees the\n * fields its read grants allow (possibly none), never more.\n */\nexport const viewAfterWrite = async (\n context: ContentServiceContext,\n model: ContentModel,\n writePolicy: Policy,\n entryId: string,\n locale: string | undefined,\n): Promise<AdminEntryView> => {\n const readPolicy = await context.permissions.evaluate(context.actor, {\n action: 'read',\n modelId: model.definition.id,\n });\n const entry = assertEntryVisible(\n writePolicy,\n context.actor,\n await entriesRepository.findLive(entryId, model.definition.id, context.site.id, context.db),\n entryId,\n );\n return buildAdminEntryView(context, model, readPolicy.allowed ? readPolicy : writePolicy, entry, locale);\n};\n\nexport const getAdminEntry = async (\n context: ContentServiceContext,\n modelKey: string,\n id: string,\n locale: string | undefined,\n): Promise<AdminEntryView> => {\n const { model, policy } = await modelWithPolicy(context, modelKey, 'read');\n const entry = assertEntryVisible(\n policy,\n context.actor,\n await entriesRepository.findLive(id, model.definition.id, context.site.id, context.db),\n id,\n );\n return buildAdminEntryView(context, model, policy, entry, locale);\n};\n\n/** Filters, search and row filter of a parsed query as SQL conditions. */\n/** Whether `queryConditions` names an `entries` column (`HeadQueryPlan.conditionsReadEntries`). */\nexport const queryReadsEntries = (query: ContentQuery, policy: Policy): boolean =>\n (query.filter !== null && filterReadsEntries(query.filter)) ||\n query.author !== undefined ||\n policy.rowFilter !== null;\n\nexport const queryConditions = (context: ContentServiceContext, query: ContentQuery, policy: Policy) => {\n const rowFilter = compileRowFilter(policy.rowFilter, context.actor);\n return [\n ...(query.filter ? [compileFilter(query.filter)] : []),\n ...(query.search ? [compileSearch(query.search.field.id, query.search.text)] : []),\n ...(query.status ? [compileStatusCondition(query.status)] : []),\n ...(query.author ? [compileAuthorCondition(query.author)] : []),\n ...(rowFilter ? [rowFilter] : []),\n ];\n};\n\nexport const parseQueryFor = (\n context: ContentServiceContext,\n model: ContentModel,\n policy: Policy,\n rawQuery: string,\n options: {\n allowSnapshot: boolean;\n allowAdminFilters?: boolean;\n allowRichText?: boolean;\n allowSeo?: boolean;\n allowPublicationState?: boolean;\n },\n): ContentQuery =>\n parseContentQuery(parseQueryTree(rawQuery), {\n model: model.definition,\n locales: context.snapshot.locales.map((locale) => locale.code),\n isReadable: (field) => maskAllows(policy.readMask, field),\n allowSnapshot: options.allowSnapshot,\n allowAdminFilters: options.allowAdminFilters ?? false,\n allowRichText: options.allowRichText ?? false,\n allowSeo: options.allowSeo ?? false,\n allowPublicationState: options.allowPublicationState ?? false,\n resolveModel: (modelId) => resolveModelById(context.snapshot, modelId)?.definition,\n });\n\nexport type AdminListItem = {\n id: string;\n locale: string;\n version: number;\n status: EntryStatus;\n autosaved: boolean;\n createdAt: string;\n updatedAt: string;\n data: Record<string, unknown>;\n author: ListAuthor | null;\n locales?: ListLocaleStatus[];\n};\n\n/** The admin content list: drafts, filtered/sorted/paginated by the same compiler as delivery. */\nexport const listAdminEntries = async (\n context: ContentServiceContext,\n modelKey: string,\n rawQuery: string,\n): Promise<{ items: AdminListItem[]; pagination: Pagination; locale: string }> => {\n const { model, policy } = await modelWithPolicy(context, modelKey, 'read');\n const query = parseQueryFor(context, model, policy, rawQuery, {\n allowSnapshot: false,\n allowAdminFilters: true,\n });\n const { limit, offset } = toLimitOffset(query.page, query.pageSize);\n const sort =\n query.sort.length > 0\n ? query.sort\n : defaultSortTerms(model.definition, (field) => maskAllows(policy.readMask, field));\n const compiled = compileHeadQuery({\n siteId: context.site.id,\n modelId: model.definition.id,\n source: { kind: 'heads', state: 'draft' },\n locales: readScopeFor(context.snapshot, model.definition, query.locale, { fallback: true }),\n conditions: queryConditions(context, query, policy),\n conditionsReadEntries: queryReadsEntries(query, policy),\n orderBy: compileOrderBy(sort),\n limit,\n offset,\n });\n return contentQueriesRepository.withConsistentRead(async (trx) => {\n const rows = await contentQueriesRepository.runHeadQuery(compiled.rows, trx);\n const total = await contentQueriesRepository.runCountQuery(compiled.count, trx);\n const published = await entryHeadsRepository.findPublishedRevisions(\n rows.map((row) => row.entry_id),\n trx,\n );\n const publishedBy = new Map(published.map((head) => [`${head.entry_id}|${head.locale}`, head]));\n const env = { ...adminEnvironment(context, query.locale), executor: trx };\n const projected = await projectRows(env, model, policy, rows, query);\n const details = await listDetailsFor(trx, model, rows, statusOf);\n return {\n items: projected.map(({ row, data }) => ({\n id: row.entry_id,\n locale: row.locale,\n version: row.version,\n status: statusOf(row, publishedBy.get(`${row.entry_id}|${row.locale}`)),\n autosaved: row.autosaved_at !== null,\n createdAt: row.created_at.toISOString(),\n updatedAt: row.updated_at.toISOString(),\n data,\n author: details.authorOf(row),\n ...(model.definition.localized ? { locales: details.localesOf(row.entry_id) ?? [] } : {}),\n })),\n pagination: paginationMeta(query.page, limit, total),\n locale: query.locale ?? context.snapshot.defaultLocale,\n };\n }, context.db);\n};\n\nconst visibleEntry = async (context: ContentServiceContext, modelKey: string, id: string) => {\n const { model, policy } = await modelWithPolicy(context, modelKey, 'read');\n const entry = assertEntryVisible(\n policy,\n context.actor,\n await entriesRepository.findLive(id, model.definition.id, context.site.id, context.db),\n id,\n );\n return { model, policy, entry };\n};\n\nexport const listRevisions = async (\n context: ContentServiceContext,\n modelKey: string,\n id: string,\n locale: string | undefined,\n) => {\n const { entry } = await visibleEntry(context, modelKey, id);\n return contentRevisionsRepository.listForEntry(entry.id, locale ?? null, 200, context.db);\n};\n\n/** One revision with its data projected through the current schema and the caller's read mask. */\nexport const getRevision = async (\n context: ContentServiceContext,\n modelKey: string,\n id: string,\n revisionId: string,\n) => {\n const { model, policy, entry } = await visibleEntry(context, modelKey, id);\n const revision = await contentRevisionsRepository.findById(revisionId, context.db);\n if (!revision || revision.entry_id !== entry.id) {\n throw revisionNotFound(revisionId);\n }\n const row = toHeadRow(entry, {\n locale: revision.locale,\n data: revision.data,\n version: 0,\n revision_id: revision.id,\n updated_at: revision.created_at,\n autosaved_at: null,\n });\n const [projected] = await projectRows(adminEnvironment(context, revision.locale), model, policy, [row], {\n fields: null,\n populate: new Map(),\n });\n return { revision, data: projected?.data ?? {} };\n};\n","import type { Insertable, Kysely, Selectable, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB, Sites } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type SiteRow = Selectable<Sites>;\nexport type NewSite = Pick<Insertable<Sites>, 'key' | 'name'>;\n\nexport const list = (trx: Executor = db) =>\n trx.selectFrom('sites').selectAll().orderBy('is_primary', 'desc').orderBy('name').orderBy('key').execute();\n\n/** How many sites the instance has. */\nexport const count = async (trx: Executor = db): Promise<number> => {\n const row = await trx\n .selectFrom('sites')\n .select((eb) => eb.fn.countAll<number | string | bigint>().as('count'))\n .executeTakeFirst();\n return Number(row?.count ?? 0);\n};\n\nexport const findById = (id: string, trx: Executor = db) =>\n trx.selectFrom('sites').selectAll().where('id', '=', id).executeTakeFirst();\n\nexport const findByKey = (key: string, trx: Executor = db) =>\n trx.selectFrom('sites').selectAll().where('key', '=', key).executeTakeFirst();\n\nexport const findPrimary = (trx: Executor = db) =>\n trx.selectFrom('sites').selectAll().where('is_primary', '=', true).executeTakeFirstOrThrow();\n\nexport const findByIds = (ids: readonly string[], trx: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : trx\n .selectFrom('sites')\n .selectAll()\n .where('id', 'in', ids)\n .orderBy('is_primary', 'desc')\n .orderBy('name')\n .execute();\n\nexport const insert = (site: NewSite, trx: Executor = db) =>\n trx.insertInto('sites').values(site).returningAll().executeTakeFirstOrThrow();\n\n/** Every site starts its own publication sequence at 0 (snapshot numbers are per site). */\nexport const insertPublicationState = (siteId: string, trx: Executor = db) =>\n trx.insertInto('publication_state').values({ site_id: siteId }).execute();\n\n/** Optimistic update: applies only if the stored version is `expectedVersion`; bumps the version. */\nexport const renameIfVersion = (id: string, expectedVersion: number, name: string, trx: Executor = db) =>\n trx\n .updateTable('sites')\n .set((eb) => ({ name, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .returningAll()\n .executeTakeFirst();\n\nexport const lockById = (id: string, trx: Transaction<DB>) =>\n trx.selectFrom('sites').selectAll().where('id', '=', id).forUpdate().executeTakeFirst();\n\n/**\n * What keeps a site from being deleted: live content, media, change sets and app users (counted per kind).\n * Soft-deleted entries, media assets and app users do not count; `deleteSite` purges them with the site.\n */\nexport const countContents = async (siteId: string, trx: Executor = db) => {\n const count = (\n table: 'entries' | 'media_assets' | 'media_folders' | 'change_sets' | 'app_users',\n liveOnly = false,\n ) => {\n let query = trx\n .selectFrom(table)\n .select((eb) => eb.fn.countAll<string>().as('n'))\n .where('site_id', '=', siteId);\n if (liveOnly) {\n query = query.where('deleted_at', 'is', null);\n }\n return query.executeTakeFirstOrThrow().then((row) => Number(row.n));\n };\n const [entries, mediaAssets, mediaFolders, changeSets, appUsers] = await Promise.all([\n count('entries', true),\n count('media_assets', true),\n count('media_folders'),\n count('change_sets'),\n count('app_users', true),\n ]);\n return { entries, mediaAssets, mediaFolders, changeSets, appUsers };\n};\n\nexport const deleteById = (id: string, trx: Executor = db) =>\n trx.deleteFrom('sites').where('id', '=', id).executeTakeFirst();\n\n/** A site's SEO defaults (null until first saved) and the version an update must name. */\nexport const findSeoDefaults = (id: string, trx: Executor = db) =>\n trx\n .selectFrom('sites')\n .select(['id', 'key', 'name', 'version', 'seo_defaults'])\n .where('id', '=', id)\n .executeTakeFirst();\n\n/** Optimistic update of the SEO defaults: applies only at `expectedVersion`; bumps the version. */\nexport const updateSeoDefaultsIfVersion = (\n id: string,\n expectedVersion: number,\n seoDefaults: unknown,\n trx: Executor = db,\n) =>\n trx\n .updateTable('sites')\n .set((eb) => ({\n seo_defaults: JSON.stringify(seoDefaults),\n version: eb('version', '+', 1),\n updated_at: new Date(),\n }))\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .returning(['id', 'key', 'name', 'version', 'seo_defaults'])\n .executeTakeFirst();\n","import { EMPTY_SEO_DEFAULTS, isModelDefinition, SeoDefaultsSchema, type SeoDefaults } from '@shapio/schema';\nimport { Value } from 'typebox/value';\nimport { isPublicImage, toDeliveryAsset, type ReadEnvironment } from '../content/read.js';\nimport type { Database } from '../db/index.js';\nimport { AppError } from '../helpers/appError.js';\nimport { writeOutboxEvent } from '../jobs/outbox.js';\nimport type { PermissionEvaluator, Principal } from '../permissions/types.js';\nimport * as mediaAssetsRepository from '../repositories/mediaAssets.js';\nimport * as sitesRepository from '../repositories/sites.js';\nimport type { SchemaSnapshot } from '../schema/snapshot.js';\nimport type { SiteActorContext, SiteRef } from './actorContext.js';\nimport { recordAudit } from './audit.js';\nimport { toAssetViews } from './mediaViews.js';\n\n/**\n * A site's SEO defaults (plan seo-fields): default texts per locale, a default social image and the Twitter\n * handle, stored on `sites.seo_defaults`. Edited by `site.settings`; delivered on `GET /api/site` and merged\n * into SEO fields by `?seo=resolved` reads.\n */\nexport const SITE_UPDATED_EVENT = 'site.updated';\n\nexport type SiteSeoContext = SiteActorContext & {\n db: Database;\n snapshot: SchemaSnapshot;\n permissions: PermissionEvaluator;\n};\n\nexport type SiteSeoView = { version: number; seo: SeoDefaults };\n\nconst siteNotFound = () => new AppError(404, 'SITE_NOT_FOUND', 'Site not found');\n\n/** Stored defaults, or the empty ones when none were saved (or the stored value no longer fits the schema). */\nconst readDefaults = (stored: unknown): SeoDefaults =>\n stored !== null && Value.Check(SeoDefaultsSchema, stored) ? stored : EMPTY_SEO_DEFAULTS;\n\n/** The defaults a resolved delivery read merges in. */\nexport const loadSeoDefaults = async (siteId: string, executor: Database): Promise<SeoDefaults> =>\n readDefaults((await sitesRepository.findSeoDefaults(siteId, executor))?.seo_defaults ?? null);\n\n/** The caller reads something on this site (the same rule as its content: deny by default). */\nconst assertReadsSite = async (context: Pick<DeliverySiteContext, 'snapshot' | 'permissions' | 'actor'>) => {\n for (const { definition } of context.snapshot.definitions) {\n if (!isModelDefinition(definition)) {\n continue;\n }\n const policy = await context.permissions.evaluate(context.actor, {\n action: 'read',\n modelId: definition.id,\n });\n if (policy.allowed) {\n return;\n }\n }\n throw context.actor.kind === 'anonymous'\n ? new AppError(401, 'UNAUTHENTICATED', 'This site needs a delivery token')\n : new AppError(403, 'FORBIDDEN', 'You may not read content on this site');\n};\n\n/**\n * Reading the defaults needs read access to some content of the site (editors see the template in the entry's\n * search-result preview); changing them needs `site.settings` (the route checks it).\n */\nexport const getSiteSeo = async (context: SiteSeoContext): Promise<SiteSeoView> => {\n await assertReadsSite(context);\n const row = await sitesRepository.findSeoDefaults(context.site.id, context.db);\n if (!row) {\n throw siteNotFound();\n }\n return { version: row.version, seo: readDefaults(row.seo_defaults) };\n};\n\nconst invalid = (code: string, message: string, details?: Record<string, unknown>) =>\n new AppError(400, code, message, details);\n\nconst assertKnownLocales = (snapshot: SchemaSnapshot, seo: SeoDefaults) => {\n const known = new Set(snapshot.locales.map((locale) => locale.code));\n const unknown = Object.keys(seo.locales).filter((code) => !known.has(code));\n if (unknown.length > 0) {\n throw invalid('UNKNOWN_LOCALE', `Unknown locale \"${unknown[0]}\"`, { locales: unknown });\n }\n};\n\n/** The default image must be a live, public image of this site: delivery shows it to anyone. */\nconst assertUsableImage = async (context: SiteSeoContext, imageId: string | null) => {\n if (imageId === null) {\n return;\n }\n const [asset] = await mediaAssetsRepository.findLiveManyOnSite(context.site.id, [imageId], context.db);\n if (!asset) {\n throw invalid('SEO_IMAGE_INVALID', 'The default image is not an asset of this site', { imageId });\n }\n if (!isPublicImage({ visibility: asset.visibility as 'public' | 'private', mimeType: asset.mime_type })) {\n throw invalid('SEO_IMAGE_INVALID', 'The default image must be a public image', { imageId });\n }\n};\n\n/** Without empty locale entries, so a cleared locale leaves nothing behind. */\nconst compact = (seo: SeoDefaults): SeoDefaults => ({\n ...seo,\n locales: Object.fromEntries(\n Object.entries(seo.locales).filter(([, texts]) => Object.keys(texts).length > 0),\n ),\n});\n\n/** Optimistic: fails with VERSION_CONFLICT when the site changed since `expectedVersion` was read. */\nexport const updateSiteSeo = async (\n context: SiteSeoContext,\n input: { expectedVersion: number; seo: SeoDefaults },\n): Promise<SiteSeoView> => {\n const seo = compact(input.seo);\n assertKnownLocales(context.snapshot, seo);\n await assertUsableImage(context, seo.imageId);\n return context.db.transaction().execute(async (trx) => {\n const current = await sitesRepository.findSeoDefaults(context.site.id, trx);\n if (!current) {\n throw siteNotFound();\n }\n const updated = await sitesRepository.updateSeoDefaultsIfVersion(\n context.site.id,\n input.expectedVersion,\n seo,\n trx,\n );\n if (!updated) {\n throw new AppError(409, 'VERSION_CONFLICT', 'The site changed since you loaded it', {\n expectedVersion: input.expectedVersion,\n actualVersion: current.version,\n });\n }\n const metadata = {\n locales: Object.keys(seo.locales),\n imageId: seo.imageId,\n twitterHandle: seo.twitterHandle,\n };\n await recordAudit(trx, {\n ...context,\n action: 'site.seo_update',\n target: { type: 'site', id: context.site.id },\n metadata,\n });\n await writeOutboxEvent(trx, {\n type: SITE_UPDATED_EVENT,\n aggregateType: 'site',\n aggregateId: context.site.id,\n payload: { siteKey: context.site.key, changed: ['seo'] },\n siteId: context.site.id,\n });\n return { version: updated.version, seo: readDefaults(updated.seo_defaults) };\n });\n};\n\nexport type DeliverySiteContext = {\n db: Database;\n snapshot: SchemaSnapshot;\n permissions: PermissionEvaluator;\n actor: Principal;\n site: SiteRef;\n media?: ReadEnvironment['media'];\n};\n\nexport type DeliverySiteView = {\n key: string;\n name: string;\n seo: Omit<SeoDefaults, 'imageId'> & { image: ReturnType<typeof toDeliveryAsset> | null };\n};\n\nconst deliveryImage = async (context: DeliverySiteContext, imageId: string | null) => {\n if (imageId === null || !context.media) {\n return null;\n }\n const assets = await mediaAssetsRepository.findLiveManyOnSite(context.site.id, [imageId], context.db);\n const [view] = await toAssetViews(context.media, assets, context.db);\n return view && isPublicImage(view) ? toDeliveryAsset(view) : null;\n};\n\n/** `GET /api/site`: the site's key, name and SEO defaults, its default image as a delivered asset. */\nexport const getDeliverySite = async (context: DeliverySiteContext): Promise<DeliverySiteView> => {\n await assertReadsSite(context);\n const row = await sitesRepository.findSeoDefaults(context.site.id, context.db);\n if (!row) {\n throw siteNotFound();\n }\n const { imageId, ...seo } = readDefaults(row.seo_defaults);\n return { key: row.key, name: row.name, seo: { ...seo, image: await deliveryImage(context, imageId) } };\n};\n","import { isSeoField, type SeoDefaults } from '@shapio/schema';\nimport type { RawBuilder } from 'kysely';\nimport {\n compileHeadPage,\n type HeadQueryPlan,\n type HeadRow,\n type HeadSource,\n} from '../content/compiler/compile.js';\nimport { entryIdIs } from '../content/compiler/conditions.js';\nimport { toLimitOffset } from '../content/compiler/paginate.js';\nimport { maskAllows } from '../content/compiler/policy.js';\nimport { selectFields } from '../content/compiler/select.js';\nimport { compileOrderBy, defaultSortTerms } from '../content/compiler/sort.js';\nimport type { ContentQuery, RichTextMode } from '../content/compiler/types.js';\nimport { readScopeFor } from '../content/locales.js';\nimport type { ContentModel } from '../content/model.js';\nimport {\n needsFollowUpReads,\n needsMediaReads,\n projectRows,\n systemAttributes,\n type ReadEnvironment,\n} from '../content/read.js';\nimport type { Policy } from '../permissions/types.js';\nimport * as contentQueriesRepository from '../repositories/contentQueries.js';\nimport type { ContentServiceContext } from './contentAccess.js';\nimport { queryConditions, queryReadsEntries } from './contentReads.js';\nimport { loadSeoDefaults } from './siteSeo.js';\n\n/**\n * Delivery-shaped reads (delivery, preview, GraphQL root fields) at one consistent moment (plan\n * delivery-perf). The heads, the page total and the site's publication sequence come from one statement, so\n * a read that needs nothing else (no relation, populate or asset follow-up) runs without a transaction.\n * Anything that needs a follow-up statement runs in one REPEATABLE READ, read-only transaction, so every\n * statement of the response sees the moment `meta.snapshot` names, except a live published read whose only\n * follow-up is relation visibility: that runs on the pool (`readConsistently` explains why it is safe).\n */\n\nexport const deliveryEnvironment = (\n context: ContentServiceContext,\n executor: ReadEnvironment['executor'],\n source: HeadSource,\n locale: string | undefined,\n richText?: RichTextMode,\n seo?: SeoDefaults,\n): ReadEnvironment => ({\n executor,\n snapshot: context.snapshot,\n permissions: context.permissions,\n actor: context.actor,\n siteId: context.site.id,\n audience: 'delivery',\n source,\n locale,\n ...(context.media ? { media: context.media } : {}),\n ...(richText ? { richText } : {}),\n ...(seo ? { seo } : {}),\n});\n\n/**\n * A delivery-shaped read of one model, once the caller is authorised and the query parsed. `source` is\n * published heads, a snapshot, or drafts (previews); `conditions` adds row restrictions on top of the query's\n * own (a preview token's entry scope).\n */\nexport type DeliveryRead = {\n context: ContentServiceContext;\n source: HeadSource;\n model: ContentModel;\n policy: Policy;\n query: ContentQuery;\n fallback: boolean;\n conditions?: readonly RawBuilder<unknown>[];\n /** `?seo=resolved`: the site's SEO defaults (loaded by `withSeoDefaults`). */\n seo?: SeoDefaults;\n};\n\n/** What a read saw: its environment (for projection), the rows, the page total and the publication sequence. */\ntype ConsistentHeads = {\n env: ReadEnvironment;\n rows: HeadRow[];\n /** Null unless the read asked for it. */\n total: number | null;\n seq: number;\n};\n\nconst environmentOf = (read: DeliveryRead, executor: ReadEnvironment['executor']) =>\n deliveryEnvironment(read.context, executor, read.source, read.query.locale, read.query.richText, read.seo);\n\n/**\n * Selected top-level relation or media fields, populate, or a default SEO image to show: the read will need a\n * follow-up statement.\n */\nconst followsUp = (read: DeliveryRead): boolean =>\n read.query.populate.size > 0 ||\n selectFields(read.model, read.policy.readMask, read.query.fields).some(\n (field) =>\n field.type === 'relation' ||\n field.type === 'media' ||\n (Boolean(read.seo?.imageId) && isSeoField(field)),\n );\n\n/**\n * A live read of published heads (no `?snapshot`, not a preview or draft read, no extra conditions) without\n * populate: its only possible follow-up is relation visibility (asset views are checked on the rows).\n */\nconst isLiveVisibilityRead = (read: DeliveryRead): boolean =>\n read.source.kind === 'heads' &&\n read.source.state === 'published' &&\n read.query.populate.size === 0 &&\n (read.conditions?.length ?? 0) === 0;\n\n/**\n * Loads the site's SEO defaults when the query asks for resolved SEO. Read before the heads, outside their\n * consistent moment: defaults are not part of a snapshot (`?snapshot=N&seo=resolved` uses today's).\n */\nconst withSeoDefaults = async (read: DeliveryRead): Promise<DeliveryRead> =>\n read.query.seo === 'resolved'\n ? { ...read, seo: await loadSeoDefaults(read.context.site.id, read.context.db) }\n : read;\n\nconst seqOf = (seq: number | null): number => {\n if (seq === null) {\n throw new Error('The site has no publication state');\n }\n return seq;\n};\n\n/**\n * Runs the plan's statement on the pool when the read cannot need a follow-up, and keeps that result when\n * the rows indeed need none; otherwise reads again inside one REPEATABLE READ transaction. `finish` projects\n * the rows (through the same executor), so its own statements see the same moment. A live published read\n * whose only follow-up is relation visibility also stays on the pool (see below).\n */\nconst readConsistently = async <T>(\n read: DeliveryRead,\n plan: HeadQueryPlan,\n options: { total: boolean },\n finish: (heads: ConsistentHeads) => Promise<T>,\n): Promise<T> => {\n const compiled = compileHeadPage(plan, options);\n const { db } = read.context;\n const live = isLiveVisibilityRead(read);\n if (!followsUp(read) || live) {\n const env = environmentOf(read, db);\n const { rows, meta } = await contentQueriesRepository.runHeadPage(compiled.rows, db);\n const fields = selectFields(read.model, read.policy.readMask, read.query.fields);\n // An empty page carries no meta row; the transaction below reads it consistently.\n if (meta && !needsFollowUpReads(env, read.model, rows, read.query, read.policy)) {\n return finish({ env, rows, total: meta.total, seq: seqOf(meta.seq) });\n }\n // Relation visibility on the pool, without BEGIN … REPEATABLE READ … COMMIT (plan delivery-perf-2, step\n // 4). The heads and `meta.snapshot` come from the one statement above; the visibility fetch is a second\n // statement that may see a later moment. That can only move a target to its newer live state: the fetch\n // itself reads published heads only (and the target's read policy), so a target unpublished or deleted in\n // between is hidden, and one published in between is shown although the snapshot named predates it. It\n // never shows an unpublished target (rule 7). Pinned (`?snapshot=N`), preview and draft reads, populate and\n // asset views keep the transaction, so their responses are exactly the moment they name.\n if (meta && live && !needsMediaReads(env, read.model, rows, fields)) {\n return finish({ env, rows, total: meta.total, seq: seqOf(meta.seq) });\n }\n }\n return contentQueriesRepository.withConsistentRead(async (trx) => {\n const env = environmentOf(read, trx);\n const page = await contentQueriesRepository.runHeadPage(compiled.rows, trx);\n const meta = page.meta ?? (await contentQueriesRepository.runHeadPageMeta(compiled.meta, trx));\n return finish({ env, rows: page.rows, total: meta.total, seq: seqOf(meta.seq) });\n }, db);\n};\n\nconst localesOf = (read: DeliveryRead) =>\n readScopeFor(read.context.snapshot, read.model.definition, read.query.locale, { fallback: read.fallback });\n\nconst toEntries = async (read: DeliveryRead, env: ReadEnvironment, rows: readonly HeadRow[]) =>\n (await projectRows(env, read.model, read.policy, rows, read.query)).map(({ row, data }) => ({\n ...systemAttributes(env, row),\n ...data,\n }));\n\nexport type EntryPage = {\n entries: Array<Record<string, unknown>>;\n limit: number;\n /** The matching total (0 when not asked for). */\n total: number;\n /** The site's publication sequence when the page was read. */\n seq: number;\n};\n\n/**\n * One page of entries in the delivery shape, with the total (`total: false` skips counting: a singleton) and\n * the publication sequence it was read at. `check` runs on the sequence before anything is projected.\n */\nexport const readEntryPage = async (\n unloaded: DeliveryRead,\n options: { total: boolean; check?: (seq: number) => void } = { total: true },\n): Promise<EntryPage> => {\n const read = await withSeoDefaults(unloaded);\n const { context, model, policy, query } = read;\n const { limit, offset } = toLimitOffset(query.page, query.pageSize);\n const sort =\n query.sort.length > 0\n ? query.sort\n : defaultSortTerms(model.definition, (field) => maskAllows(policy.readMask, field));\n const plan: HeadQueryPlan = {\n siteId: context.site.id,\n modelId: model.definition.id,\n source: read.source,\n locales: localesOf(read),\n conditions: [...queryConditions(context, query, policy), ...(read.conditions ?? [])],\n // A caller's extra conditions (a preview token's entry scope) are not inspected: they keep the join.\n conditionsReadEntries: queryReadsEntries(query, policy) || (read.conditions?.length ?? 0) > 0,\n orderBy: compileOrderBy(sort),\n limit,\n offset,\n };\n return readConsistently(read, plan, { total: options.total }, async ({ env, rows, total, seq }) => {\n options.check?.(seq);\n return { entries: await toEntries(read, env, rows), limit, total: total ?? 0, seq };\n });\n};\n\n/** One entry by ID in the delivery shape (undefined when it is not there for this read), and the sequence. */\nexport const readOneEntry = async (\n unloaded: DeliveryRead,\n id: string,\n options: { check?: (seq: number) => void } = {},\n): Promise<{ entry: Record<string, unknown> | undefined; seq: number }> => {\n const read = await withSeoDefaults(unloaded);\n const { context, model, policy, query } = read;\n const plan: HeadQueryPlan = {\n siteId: context.site.id,\n modelId: model.definition.id,\n source: read.source,\n locales: localesOf(read),\n conditions: [\n ...queryConditions(context, { ...query, filter: null, search: null }, policy),\n ...(read.conditions ?? []),\n entryIdIs(id),\n ],\n orderBy: [],\n limit: 1,\n };\n return readConsistently(read, plan, { total: false }, async ({ env, rows, seq }) => {\n options.check?.(seq);\n const [entry] = await toEntries(read, env, rows);\n return { entry, seq };\n });\n};\n","import { AppError } from '../helpers/appError.js';\nimport { READ_DRAFTS_ACTION, type Principal } from '../permissions/types.js';\nimport type { ContentServiceContext } from './contentAccess.js';\n\n/**\n * Who may read draft heads through the delivery API (plan drafts-mode §1). Admin users, admin-scope tokens\n * and Shapio itself always could (GraphQL previews). A delivery token may when its role grants `readDrafts`,\n * which always covers every model, so this one check per request also covers relation targets. App users and\n * anonymous callers never may, whatever a role says (the evaluator denies them too). The caller has already\n * checked `read` on the model.\n */\nconst isAdminPrincipal = (principal: Principal): boolean =>\n principal.kind === 'system' ||\n principal.kind === 'admin' ||\n (principal.kind === 'token' && principal.scope === 'admin');\n\nconst draftsForbidden = (message: string) => new AppError(403, 'DRAFTS_FORBIDDEN', message);\n\nexport const assertMayReadDrafts = async (\n context: ContentServiceContext,\n modelId: string,\n modelKey: string,\n): Promise<void> => {\n const { actor } = context;\n if (isAdminPrincipal(actor)) {\n return;\n }\n if (actor.kind !== 'token') {\n throw draftsForbidden('Drafts need an admin user or a delivery token granted Read drafts');\n }\n const policy = await context.permissions.evaluate(actor, { action: READ_DRAFTS_ACTION, modelId });\n if (!policy.allowed) {\n throw draftsForbidden(\n `This token may not read drafts of \"${modelKey}\"; grant Read drafts on its delivery role`,\n );\n }\n};\n","import type { HeadSource } from '../content/compiler/compile.js';\nimport { paginationMeta, type Pagination } from '../content/compiler/paginate.js';\nimport { queryInvalid, type ContentQuery } from '../content/compiler/types.js';\nimport { entryNotFound, snapshotInvalid } from '../content/errors.js';\nimport { resolveModel, type ContentModel } from '../content/model.js';\nimport { AppError } from '../helpers/appError.js';\nimport type { Policy } from '../permissions/types.js';\nimport type { UsageRecorder } from '../usage/aggregator.js';\nimport { usagePrincipalKey } from '../usage/keys.js';\nimport { restFieldReads } from '../usage/restFieldReads.js';\nimport type { ContentServiceContext } from './contentAccess.js';\nimport { readEntryPage, readOneEntry } from './contentDeliveryReads.js';\nimport { parseQueryFor } from './contentReads.js';\nimport { assertMayReadDrafts } from './draftAccess.js';\n\n/**\n * The delivery API (build plan §4.E6): published heads (drafts only for callers allowed them, plan\n * drafts-mode), through the permission evaluator (delivery tokens, app users, anonymous callers). Each\n * response reads one consistent moment (one statement, or a REPEATABLE READ transaction when it needs more:\n * `contentDeliveryReads.ts`; a live read's relation visibility is the exception, checked against the live\n * state on the pool) and reports the publication sequence it saw, which a site build can pin with\n * `?snapshot=N`.\n */\nexport type DeliveryMeta = {\n /** The requested locale (or the default); each entry's own `locale` says which one served it. */\n locale: string;\n /** Publication sequence number this response reflects. */\n snapshot: number;\n /** Draft reads only (`?publicationState=draft`); published responses carry no such member. */\n publicationState?: 'draft';\n};\n\nexport type DeliveryList = {\n data: Array<Record<string, unknown>>;\n meta: DeliveryMeta & { pagination: Pagination };\n};\nexport type DeliveryItem = { data: Record<string, unknown>; meta: DeliveryMeta };\n\n/**\n * Read variants GraphQL exposes as arguments:\n * - `fallback: false` serves the requested locale only, never a fallback locale (ADR 0004).\n * - `drafts: true` reads draft heads in the delivery shape (GraphQL `publicationState: DRAFT`; REST and\n * `@shapio/local` ask with `?publicationState=draft`). This service checks the caller may (draftAccess.ts).\n */\nexport type DeliveryReadOptions = {\n fallback?: boolean;\n drafts?: boolean;\n /**\n * Field usage counters (REST only). GraphQL counts its own selections once per operation\n * (plugins/graphql.ts), so its root reads through this service pass none.\n */\n usage?: UsageRecorder;\n /**\n * Runs usage recording after the response is sent (REST passes the request's after-response queue); without\n * it the read records before returning.\n */\n defer?: (task: () => Promise<void>) => void;\n};\n\nconst readableModel = async (context: ContentServiceContext, modelKey: string) => {\n const model = resolveModel(context.snapshot, modelKey);\n const policy = await context.permissions.evaluate(context.actor, {\n action: 'read',\n modelId: model.definition.id,\n });\n if (!policy.allowed) {\n throw context.actor.kind === 'anonymous'\n ? new AppError(401, 'UNAUTHENTICATED', 'This content needs a delivery token')\n : new AppError(403, 'FORBIDDEN', `Your token may not read \"${modelKey}\"`);\n }\n return { model, policy };\n};\n\n/** Whether a read asks for drafts: GraphQL's option, or `?publicationState=draft`. */\nconst readsDrafts = (query: ContentQuery, options: DeliveryReadOptions): boolean =>\n options.drafts === true || query.publicationState === 'draft';\n\n/**\n * A draft read must be allowed: `read` was checked already; `readDrafts` (or an admin principal) is checked\n * here.\n */\nconst authorizeDrafts = async (\n context: ContentServiceContext,\n model: ContentModel,\n modelKey: string,\n drafts: boolean,\n): Promise<void> => {\n if (drafts) {\n await assertMayReadDrafts(context, model.definition.id, modelKey);\n }\n};\n\n/** The heads a read serves: drafts, a pinned snapshot, or the published heads. */\nconst sourceOf = (query: ContentQuery, drafts: boolean): HeadSource => {\n if (query.snapshot !== undefined && drafts) {\n throw queryInvalid('snapshot reads published content; it cannot be combined with drafts');\n }\n return drafts\n ? { kind: 'heads', state: 'draft' }\n : query.snapshot !== undefined\n ? { kind: 'snapshot', seq: query.snapshot }\n : { kind: 'heads', state: 'published' };\n};\n\n/** A pinned snapshot must already exist: checked against the sequence the read itself saw. */\nconst snapshotCheck =\n (query: ContentQuery) =>\n (current: number): void => {\n if (query.snapshot !== undefined && query.snapshot > current) {\n throw snapshotInvalid(query.snapshot, current);\n }\n };\n\n/**\n * Counts a successful REST read for field usage (plan developer-face §5): once per request, after the\n * projection, by principal. Admin users and draft reads are never counted.\n */\nconst recordUsage = async (\n context: ContentServiceContext,\n options: DeliveryReadOptions,\n model: ContentModel,\n policy: Policy,\n query: ContentQuery,\n) => {\n const principalKey = usagePrincipalKey(context.actor);\n if (!options.usage || readsDrafts(query, options) || principalKey === null) {\n return;\n }\n const reads = await restFieldReads({\n snapshot: context.snapshot,\n model,\n policy,\n query,\n policyFor: (modelId) => context.permissions.evaluate(context.actor, { action: 'read', modelId }),\n });\n options.usage.recordRequest(context.site.id, principalKey, query.snapshot ?? null);\n options.usage.recordFieldReads(context.site.id, principalKey, model.definition.id, reads);\n};\n\n/** Records usage after the response when the caller can defer it (REST), else before returning. */\nconst afterRead = async (options: DeliveryReadOptions, task: () => Promise<void>) => {\n if (options.defer) {\n options.defer(task);\n return;\n }\n await task();\n};\n\nconst ITEM_PARAMETERS = new Set([\n 'fields',\n 'populate',\n 'locale',\n 'snapshot',\n 'richText',\n 'seo',\n 'publicationState',\n]);\n\nconst DELIVERY_PARSE = {\n allowSnapshot: true,\n allowRichText: true,\n allowSeo: true,\n allowPublicationState: true,\n} as const;\n\n/** Meta every delivery response carries; draft reads add `publicationState` (published ones are unchanged). */\nconst metaOf = (locale: string, snapshot: number, drafts: boolean): DeliveryMeta =>\n drafts ? { locale, snapshot, publicationState: 'draft' } : { locale, snapshot };\n\n/** Reading one entry takes only these query parameters; anything else is refused, not ignored. */\nexport const assertItemParameters = (rawQuery: string, allowed: ReadonlySet<string>): void => {\n const unsupported = [...new URLSearchParams(rawQuery).keys()].find(\n (key) => !allowed.has(key.split('[')[0] ?? key),\n );\n if (unsupported !== undefined) {\n throw queryInvalid(`\"${unsupported}\" is not supported when reading one entry`);\n }\n};\n\n/** `GET /api/content/:modelKey`: a page of a collection, or a singleton's one entry. */\nexport const listDelivery = async (\n context: ContentServiceContext,\n modelKey: string,\n rawQuery: string,\n options: DeliveryReadOptions = {},\n): Promise<DeliveryList | DeliveryItem> => {\n const { model, policy } = await readableModel(context, modelKey);\n const query = parseQueryFor(context, model, policy, rawQuery, DELIVERY_PARSE);\n const drafts = readsDrafts(query, options);\n const source = sourceOf(query, drafts);\n await authorizeDrafts(context, model, modelKey, drafts);\n const locale = query.locale ?? context.snapshot.defaultLocale;\n const singleton = model.definition.kind === 'singleton';\n const { entries, limit, total, seq } = await readEntryPage(\n { context, source, model, policy, query, fallback: options.fallback ?? true },\n { total: !singleton, check: snapshotCheck(query) },\n );\n const snapshot = query.snapshot ?? seq;\n let result: DeliveryList | DeliveryItem;\n if (singleton) {\n const [entry] = entries;\n if (!entry) {\n throw entryNotFound(modelKey);\n }\n result = { data: entry, meta: metaOf(locale, snapshot, drafts) };\n } else {\n result = {\n data: entries,\n meta: { ...metaOf(locale, snapshot, drafts), pagination: paginationMeta(query.page, limit, total) },\n };\n }\n await afterRead(options, () => recordUsage(context, options, model, policy, query));\n return result;\n};\n\n/**\n * `GET /api/content/:modelKey/:id`: one published entry (404 for unpublished, unknown or unreadable entries);\n * with drafts, the entry's draft.\n */\nexport const getDelivery = async (\n context: ContentServiceContext,\n modelKey: string,\n id: string,\n rawQuery: string,\n options: DeliveryReadOptions = {},\n): Promise<DeliveryItem> => {\n assertItemParameters(rawQuery, ITEM_PARAMETERS);\n const { model, policy } = await readableModel(context, modelKey);\n const query = parseQueryFor(context, model, policy, rawQuery, DELIVERY_PARSE);\n const drafts = readsDrafts(query, options);\n const source = sourceOf(query, drafts);\n await authorizeDrafts(context, model, modelKey, drafts);\n const { entry, seq } = await readOneEntry(\n { context, source, model, policy, query, fallback: options.fallback ?? true },\n id,\n { check: snapshotCheck(query) },\n );\n if (!entry) {\n throw entryNotFound(id);\n }\n await afterRead(options, () => recordUsage(context, options, model, policy, query));\n return {\n data: entry,\n meta: metaOf(query.locale ?? context.snapshot.defaultLocale, query.snapshot ?? seq, drafts),\n };\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\nimport { entrySiteOf } from './entries.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** Why a publication sequence number was taken (the `publication_snapshots` ledger). */\nexport type SnapshotSource =\n 'publish' | 'unpublish' | 'delete' | 'schedule' | 'change_set' | 'schema' | 'conversion' | 'import';\n\nexport type SnapshotMeta = {\n source: SnapshotSource;\n changeSetId?: string | null;\n /** Who caused it, in audit vocabulary (`schema/planner/actor.ts actorColumns`). */\n actor?: { type: string; id: string | null } | null;\n};\n\n/**\n * The publication sequence and log behind `?snapshot=N` (ADR 0001). The sequence is one row incremented\n * inside each publishing transaction: its row lock orders publications by commit, so a reader that sees N\n * also sees every publication before N. Take it as late as possible in the transaction (it serialises every\n * publication instance-wide until commit), and never ahead of the transaction that uses it.\n *\n * Every number also gets a `publication_snapshots` ledger row (why, who, the schema version live with it),\n * written in the same transaction.\n */\nexport const nextSeq = async (trx: Transaction<DB>, siteId: string, meta: SnapshotMeta): Promise<number> => {\n // One sequence per site (sites plan §H): publishing on one site never waits for another's.\n const row = await trx\n .updateTable('publication_state')\n .set((eb) => ({ last_seq: eb('last_seq', '+', '1') }))\n .where('site_id', '=', siteId)\n .returning('last_seq')\n .executeTakeFirstOrThrow();\n const seq = Number(row.last_seq);\n await trx\n .insertInto('publication_snapshots')\n .values((eb) => ({\n site_id: siteId,\n seq: String(seq),\n schema_version: eb.selectFrom('system_versions').select('schema_version'),\n source: meta.source,\n change_set_id: meta.changeSetId ?? null,\n actor_type: meta.actor?.type ?? null,\n actor_id: meta.actor?.id ?? null,\n }))\n .execute();\n return seq;\n};\n\n/**\n * One sequence number per transaction, taken on first use: everything a batch publishes, unpublishes or\n * converts shares it, so the batch is exactly one snapshot.\n */\nexport type SeqAllocator = {\n next: () => Promise<number>;\n /** The number if one was taken already. */\n taken: () => number | undefined;\n};\n\nexport const createSeqAllocator = (\n trx: Transaction<DB>,\n siteId: string,\n meta: SnapshotMeta,\n): SeqAllocator => {\n let seq: Promise<number> | undefined;\n let value: number | undefined;\n return {\n next: () => {\n seq ??= nextSeq(trx, siteId, meta).then((taken) => {\n value = taken;\n return taken;\n });\n return seq;\n },\n taken: () => value,\n };\n};\n\nexport const currentSeq = async (siteId: string, executor: Executor = db): Promise<number> => {\n const row = await executor\n .selectFrom('publication_state')\n .select('last_seq')\n .where('site_id', '=', siteId)\n .executeTakeFirstOrThrow();\n return Number(row.last_seq);\n};\n\n/** Ends the live period of (entry, locale) at `seq`, if one is open. */\nexport const close = (entryId: string, locale: string | null, seq: number, trx: Executor = db) => {\n let query = trx\n .updateTable('publication_log')\n .set({ to_seq: String(seq) })\n .where('entry_id', '=', entryId)\n .where('to_seq', 'is', null);\n if (locale !== null) {\n query = query.where('locale', '=', locale);\n }\n return query.executeTakeFirst();\n};\n\nexport const open = (\n row: { entryId: string; modelId: string; locale: string; revisionId: string; seq: number; now: Date },\n trx: Executor = db,\n) =>\n trx\n .insertInto('publication_log')\n .values({\n entry_id: row.entryId,\n // The log is per site (snapshot numbers are): a copy of the entry's site.\n site_id: entrySiteOf(trx, row.entryId),\n model_id: row.modelId,\n locale: row.locale,\n revision_id: row.revisionId,\n from_seq: String(row.seq),\n published_at: row.now,\n })\n .execute();\n\nexport const hasOpen = async (entryId: string, executor: Executor = db): Promise<boolean> => {\n const row = await executor\n .selectFrom('publication_log')\n .select('id')\n .where('entry_id', '=', entryId)\n .where('to_seq', 'is', null)\n .executeTakeFirst();\n return row !== undefined;\n};\n\nexport const removeForLocale = (locale: string, trx: Executor = db) =>\n trx.deleteFrom('publication_log').where('locale', '=', locale).executeTakeFirst();\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** The global schema version: the durable value every instance compares its cached snapshot against. */\nexport const getSchemaVersion = async (executor: Executor = db): Promise<number> => {\n const row = await executor.selectFrom('system_versions').select('schema_version').executeTakeFirstOrThrow();\n return row.schema_version;\n};\n\n/** Increments the global schema version in the caller's transaction and returns the new value. */\nexport const bumpSchemaVersion = async (trx: Executor, now: Date): Promise<number> => {\n const row = await trx\n .updateTable('system_versions')\n .set((eb) => ({ schema_version: eb('schema_version', '+', eb.lit(1)), updated_at: now }))\n .returning('schema_version')\n .executeTakeFirstOrThrow();\n return row.schema_version;\n};\n\n/** Records the Shapio release this database is served by (in-process readers compare their own with it). */\nexport const setRelease = async (release: string, executor: Executor = db): Promise<void> => {\n await executor.updateTable('system_versions').set({ release }).execute();\n};\n","import { sql, type Kysely, type RawBuilder, type Transaction } from 'kysely';\nimport { isMysql, isSqlite } from './dialect.js';\nimport { mysqlJsonArrayOf } from './sql/values.js';\nimport type { DB } from './types.js';\n\n/**\n * The snapshot diff query over `publication_log` (PostgreSQL-specific SQL lives in `db/`, the content\n * compiler and migrations; CONTRIBUTING.md). repositories/snapshotDiff.ts is its typed wrapper and documents the\n * classification. Rows come back in entry-ID order, at most `maxRows` of them, with each entry's changed\n * locales aggregated as JSON.\n */\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type SnapshotDiffLocaleRow = {\n locale: string;\n change: 'published' | 'updated' | 'unpublished';\n fromRevisionId: string | null;\n toRevisionId: string | null;\n};\n\nexport type SnapshotDiffRow = { entry_id: string; model_id: string; locales: SnapshotDiffLocaleRow[] };\n\nexport type SnapshotDiffParams = {\n /** The site whose publication log is diffed (snapshot numbers are per site). */\n siteId: string;\n from: number;\n to: number;\n /** Keyset cursor: only entries with a greater ID. */\n after: string | undefined;\n /** Only these models; null = every model. */\n modelIds: readonly string[] | null;\n /** A compiled condition over the `entries` row aliased `e`. */\n rowFilter: RawBuilder<unknown> | null;\n maxRows: number;\n};\n\n/**\n * The dialect's spelling of the few typed pieces (PostgreSQL casts, arrays and `jsonb_agg`; SQLite binds\n * plain values, expands the model list with `json_each` and aggregates with `json_group_array`, whose text\n * result is parsed here).\n */\ntype DiffDialect = {\n uuid: (value: string) => RawBuilder<unknown>;\n seq: (value: number) => RawBuilder<unknown>;\n inModels: (modelIds: readonly string[]) => RawBuilder<unknown>;\n /** `a <op> b`: the two revision IDs differ (nulls compare as values). */\n distinct: (a: RawBuilder<unknown>, b: RawBuilder<unknown>) => RawBuilder<unknown>;\n /** The JSON objects of a group as an array ordered by locale. */\n aggregate: (object: RawBuilder<unknown>) => RawBuilder<unknown>;\n object: RawBuilder<unknown>;\n /** The classification column's name (`change` is reserved on MySQL). */\n change: RawBuilder<unknown>;\n};\n\nconst POSTGRES: DiffDialect = {\n uuid: (value: string) => sql`${value}::uuid`,\n seq: (value: number) => sql`${value}::bigint`,\n inModels: (modelIds: readonly string[]) => sql`pl.model_id = any(${[...modelIds]}::uuid[])`,\n distinct: (a, b) => sql`${a} is distinct from ${b}`,\n aggregate: (object) => sql`jsonb_agg(\n ${object}\n order by ch.locale\n )`,\n object: sql`jsonb_build_object`,\n change: sql`change`,\n};\n\nconst SQLITE: DiffDialect = {\n uuid: (value) => sql`${value.toLowerCase()}`,\n seq: (value) => sql`${value}`,\n inModels: (modelIds) => sql`pl.model_id in (select value from json_each(${JSON.stringify(modelIds)}))`,\n distinct: (a, b) => sql`${a} is not ${b}`,\n aggregate: (object) => sql`json_group_array(\n ${object}\n order by ch.locale\n )`,\n object: sql`json_object`,\n change: sql`change`,\n};\n\n/** MySQL: `<=>` is null-safe equality; JSON_ARRAYAGG takes no ORDER BY (`mysqlJsonArrayOf`). */\nconst MYSQL: DiffDialect = {\n uuid: (value) => sql`${value.toLowerCase()}`,\n seq: (value) => sql`${value}`,\n inModels: (modelIds) =>\n modelIds.length === 0 ? sql`false` : sql`pl.model_id in (${sql.join([...modelIds])})`,\n distinct: (a, b) => sql`not (${a} <=> ${b})`,\n aggregate: (object) => mysqlJsonArrayOf(sql`${object} order by ch.locale`),\n object: sql`json_object`,\n change: sql`\\`change\\``,\n};\n\nconst modelCondition = (dialect: DiffDialect, modelIds: readonly string[] | null) =>\n modelIds === null ? sql`true` : dialect.inModels(modelIds);\n\nconst afterCondition = (dialect: DiffDialect, after: string | undefined) =>\n after === undefined ? sql`true` : sql`pl.entry_id > ${dialect.uuid(after)}`;\n\n/** The revision of (entry, locale) live at `seq`, as a lateral subquery over `c`. */\nconst liveAt = (dialect: DiffDialect, siteId: string, seq: number) => sql`(\n select p.revision_id from publication_log p\n where p.site_id = ${dialect.uuid(siteId)} and p.entry_id = c.entry_id and p.locale = c.locale\n and p.from_seq <= ${dialect.seq(seq)} and (p.to_seq is null or p.to_seq > ${dialect.seq(seq)})\n order by p.from_seq desc\n limit 1\n)`;\n\nexport const selectSnapshotDiffRows = async (\n params: SnapshotDiffParams,\n executor: Executor,\n): Promise<SnapshotDiffRow[]> => {\n const dialect = isMysql() ? MYSQL : isSqlite() ? SQLITE : POSTGRES;\n const { from, to } = params;\n const scope = sql`pl.site_id = ${dialect.uuid(params.siteId)} and ${modelCondition(dialect, params.modelIds)}\n and ${afterCondition(dialect, params.after)}`;\n const result = await sql<SnapshotDiffRow>`\n with candidates as (\n select pl.entry_id, pl.locale from publication_log pl\n where pl.from_seq > ${dialect.seq(from)} and pl.from_seq <= ${dialect.seq(to)} and ${scope}\n union\n select pl.entry_id, pl.locale from publication_log pl\n where pl.to_seq > ${dialect.seq(from)} and pl.to_seq <= ${dialect.seq(to)} and ${scope}\n ),\n states as (\n select c.entry_id, c.locale, ${liveAt(dialect, params.siteId, from)} as from_revision_id,\n ${liveAt(dialect, params.siteId, to)} as to_revision_id\n from candidates c\n ),\n changes as (\n select s.*,\n case\n when s.from_revision_id is null then 'published'\n when s.to_revision_id is null then 'unpublished'\n else 'updated'\n end as ${dialect.change}\n from states s\n where ${dialect.distinct(sql`s.from_revision_id`, sql`s.to_revision_id`)}\n )\n select ch.entry_id, e.model_id,\n ${dialect.aggregate(sql`${dialect.object}(\n 'locale', ch.locale,\n 'change', ch.${dialect.change},\n 'fromRevisionId', ch.from_revision_id,\n 'toRevisionId', ch.to_revision_id\n )`)} as locales\n from changes ch\n join entries e on e.id = ch.entry_id\n where ${params.rowFilter ?? sql`true`}\n group by ch.entry_id, e.model_id\n order by ch.entry_id\n limit ${params.maxRows}\n `.execute(executor);\n return result.rows.map((row) =>\n typeof row.locales === 'string'\n ? { ...row, locales: JSON.parse(row.locales) as SnapshotDiffLocaleRow[] }\n : row,\n );\n};\n","import type { Kysely, RawBuilder, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport { selectSnapshotDiffRows } from '../db/snapshotDiffQuery.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/**\n * The snapshot diff (plan developer-face §5) of one site's ledger: which of its (entry, locale) pairs serve different live content at\n * publication sequence `to` than at `from`, in one query over `publication_log` (db/snapshotDiffQuery.ts).\n *\n * Candidates are the pairs whose live period opened (`from_seq`) or closed (`to_seq`) in `(from, to]`; only\n * those can differ. For each, the revision live at `from` and at `to` is \"the row whose period contains\n * the sequence number\" (`from_seq <= N and (to_seq is null or to_seq > N)`), the same rule `?snapshot=N`\n * reads with (content/compiler/compile.ts). Classification:\n *\n * | live at `from` | live at `to` | change |\n * |----------------|--------------------|---------------|\n * | no | yes | `published` |\n * | yes | no | `unpublished` |\n * | yes (R1) | yes (R2, R2 ≠ R1) | `updated` |\n * | yes (R) | yes (same R) | dropped |\n * | no | no | dropped |\n *\n * Entries are returned in entry-ID order with their changed locales grouped, paginated by keyset on the\n * entry ID (`after`). A page holds at most `limit` entries; `nextAfter` is set when more follow.\n *\n * Entry deletion closes the entry's live periods (it reads as `unpublished`); locale deletion removes that\n * locale's rows (history is rewritten, so a later diff no longer mentions the locale).\n */\nexport type SnapshotChangeKind = 'published' | 'updated' | 'unpublished';\n\nexport type SnapshotLocaleChange = {\n locale: string;\n change: SnapshotChangeKind;\n /** The revision live at `from` (null when the locale was not live then). */\n fromRevisionId: string | null;\n /** The revision live at `to` (null when the locale is not live then). */\n toRevisionId: string | null;\n};\n\nexport type SnapshotEntryChange = {\n entryId: string;\n modelId: string;\n locales: SnapshotLocaleChange[];\n};\n\nexport type SnapshotDiffQuery = {\n /** The site whose ledger is diffed (snapshot numbers are per site). */\n siteId: string;\n from: number;\n to: number;\n /** Keyset cursor: only entries with an ID greater than this. */\n after?: string | undefined;\n /** Entries per page (the caller bounds it). */\n limit: number;\n /** Only these models (the ones the caller may read); null = every model. */\n modelIds: readonly string[] | null;\n /**\n * A compiled row filter over the `entries` row aliased `e` (content/compiler/policy.ts\n * `compileRowFilter`). It reads the entry as it is now, which for a diff ending at the current snapshot\n * is the state at `to`.\n */\n rowFilter?: RawBuilder<unknown> | null;\n};\n\nexport type SnapshotDiffPage = { items: SnapshotEntryChange[]; nextAfter: string | null };\n\nexport const listChanges = async (\n query: SnapshotDiffQuery,\n executor: Executor = db,\n): Promise<SnapshotDiffPage> => {\n if (query.to <= query.from || (query.modelIds !== null && query.modelIds.length === 0)) {\n return { items: [], nextAfter: null };\n }\n const rows = await selectSnapshotDiffRows(\n {\n siteId: query.siteId,\n from: query.from,\n to: query.to,\n after: query.after,\n modelIds: query.modelIds,\n rowFilter: query.rowFilter ?? null,\n maxRows: query.limit + 1,\n },\n executor,\n );\n const page = rows.slice(0, query.limit);\n return {\n items: page.map((row) => ({ entryId: row.entry_id, modelId: row.model_id, locales: row.locales })),\n nextAfter: rows.length > query.limit ? (page.at(-1)?.entry_id ?? null) : null,\n };\n};\n\n/**\n * The schema version recorded for each publication sequence number in the `publication_snapshots`\n * ledger; numbers without a ledger row (snapshot 0, rows from before the ledger) are absent.\n */\nexport const schemaVersionsAt = async (\n siteId: string,\n seqs: readonly number[],\n executor: Executor = db,\n): Promise<Map<number, number | null>> => {\n if (seqs.length === 0) {\n return new Map();\n }\n const rows = await executor\n .selectFrom('publication_snapshots')\n .select(['seq', 'schema_version'])\n .where('site_id', '=', siteId)\n .where(\n 'seq',\n 'in',\n seqs.map((seq) => String(seq)),\n )\n .execute();\n return new Map(rows.map((row) => [Number(row.seq), row.schema_version]));\n};\n\n/** When the site's snapshot `seq` was created, from the ledger (undefined without a ledger row). */\nexport const snapshotCreatedAt = async (\n siteId: string,\n seq: number,\n executor: Executor = db,\n): Promise<Date | undefined> =>\n (\n await executor\n .selectFrom('publication_snapshots')\n .select('created_at')\n .where('site_id', '=', siteId)\n .where('seq', '=', String(seq))\n .executeTakeFirst()\n )?.created_at;\n","import {\n effectiveLayout,\n effectiveTitleField,\n isModelDefinition,\n routeKeyOf,\n type ModelDefinition,\n} from '@shapio/schema';\nimport type { Transaction } from 'kysely';\nimport { compileModelRowFilters, maskAllows } from '../content/compiler/policy.js';\nimport { queryInvalid } from '../content/compiler/types.js';\nimport { snapshotInvalid } from '../content/errors.js';\nimport type { DB } from '../db/types.js';\nimport { AppError } from '../helpers/appError.js';\nimport type { FieldMask, RowFilter } from '../permissions/types.js';\nimport * as contentQueriesRepository from '../repositories/contentQueries.js';\nimport * as contentRevisionsRepository from '../repositories/contentRevisions.js';\nimport * as publicationsRepository from '../repositories/publications.js';\nimport * as schemaVersionsRepository from '../repositories/schemaVersions.js';\nimport * as snapshotDiffRepository from '../repositories/snapshotDiff.js';\nimport type { SnapshotChangeKind, SnapshotEntryChange } from '../repositories/snapshotDiff.js';\nimport type { ContentServiceContext } from './contentAccess.js';\n\n/**\n * The snapshot diff API (plan developer-face §5): which entries' live content changed between two\n * publication sequence numbers, for incremental site builds. Delivery principals see only models they may\n * read. Row filters (e.g. `ownedByPrincipal`) are evaluated on the entry as it is now, i.e. at `to` when\n * `to` is the current snapshot; an entry the caller may read now but not at `from` (or the reverse) is\n * judged by its state now. The response names entries and locales, never values, so read masks do not\n * apply: an entry whose only change is in a field the caller cannot read is still listed as `updated`.\n *\n * Old snapshots are diffed as recorded. When the schema version differs between the two ends, clients\n * should rebuild the affected models rather than trust per-entry changes (fields may have been added,\n * converted or removed without new publications); locale deletion rewrites history.\n */\nexport const SNAPSHOT_CHANGES_DEFAULT_LIMIT = 100;\nexport const SNAPSHOT_CHANGES_MAX_LIMIT = 500;\n\nexport type SnapshotChangeView = {\n id: string;\n modelId: string;\n /** The model's API ID (GraphQL, admin API). */\n modelKey: string;\n /** The model's delivery route key (`/api/content/<routeKey>`). */\n routeKey: string;\n /**\n * The entry's title from its revision live at `to` (at `from` when it was unpublished), in the default\n * locale when that changed, else the first changed locale; null without a title field, a readable one\n * or a value.\n */\n title: string | null;\n /** The cover image's media asset ID from the same revision, when the model has a readable cover. */\n coverMediaId: string | null;\n locales: Array<{\n locale: string;\n change: SnapshotChangeKind;\n /** The revision live at `to`, or the one live at `from` for `unpublished` (open it in the admin). */\n revisionId: string | null;\n }>;\n};\n\nexport type SnapshotChangesPage = {\n from: number;\n to: number;\n /** The schema version recorded with each end's snapshot; null when none was recorded. */\n schemaVersions: { from: number | null; to: number | null };\n items: SnapshotChangeView[];\n /** Pass as `after` for the next page; null on the last page. */\n nextCursor: string | null;\n};\n\nexport type SnapshotChangesQuery = { from: number; to?: number; after?: string; limit?: number };\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ntype ReadableModel = { definition: ModelDefinition; rowFilter: RowFilter | null; readMask: FieldMask };\n\n/**\n * Models the caller may read. An authenticated caller (session, token, app user) with none (or an instance\n * with no models yet) gets empty results, never an error. Anonymous callers with nothing readable get 401,\n * as the delivery routes answer them when the `public` role grants nothing.\n */\nconst readableModels = async (context: ContentServiceContext): Promise<ReadableModel[]> => {\n const models = context.snapshot.definitions\n .map((active) => active.definition)\n .filter((definition): definition is ModelDefinition => isModelDefinition(definition));\n const readable: ReadableModel[] = [];\n for (const definition of models) {\n const policy = await context.permissions.evaluate(context.actor, {\n action: 'read',\n modelId: definition.id,\n });\n if (policy.allowed) {\n readable.push({ definition, rowFilter: policy.rowFilter, readMask: policy.readMask });\n }\n }\n if (readable.length === 0 && context.actor.kind === 'anonymous') {\n throw new AppError(401, 'UNAUTHENTICATED', 'Snapshots need a delivery token');\n }\n return readable;\n};\n\ntype Describe = (item: SnapshotEntryChange) => { title: string | null; coverMediaId: string | null };\n\n/** The revision an entry is described from: the default locale's when it changed, else the first. */\nconst revisionOf = (item: SnapshotEntryChange, defaultLocale: string): string | null => {\n const locale = item.locales.find((candidate) => candidate.locale === defaultLocale) ?? item.locales[0];\n return locale ? (locale.toRevisionId ?? locale.fromRevisionId) : null;\n};\n\n/**\n * Title and cover per changed entry, read from one revision each (one query for the page). Fields the\n * caller's read mask hides are left out.\n */\nconst describeEntries = async (\n items: readonly SnapshotEntryChange[],\n models: ReadonlyMap<string, ReadableModel>,\n defaultLocale: string,\n executor: Transaction<DB>,\n): Promise<Describe> => {\n const revisionIds = [...new Set(items.flatMap((item) => revisionOf(item, defaultLocale) ?? []))];\n const rows = await contentRevisionsRepository.findDataByIds(revisionIds, executor);\n const dataById = new Map(rows.map((row) => [row.id, row.data as Record<string, unknown>]));\n return (item) => {\n const model = models.get(item.modelId);\n const revisionId = revisionOf(item, defaultLocale);\n const data = revisionId ? dataById.get(revisionId) : undefined;\n if (!model || !data) {\n return { title: null, coverMediaId: null };\n }\n const titleField = effectiveTitleField(model.definition);\n const cover = effectiveLayout(model.definition).cover;\n const title = titleField && maskAllows(model.readMask, titleField) ? data[titleField.id] : undefined;\n const coverId = cover && maskAllows(model.readMask, cover) ? data[cover.id] : undefined;\n return {\n title:\n typeof title === 'string' && title.trim() !== ''\n ? title\n : typeof title === 'number'\n ? String(title)\n : null,\n coverMediaId: typeof coverId === 'string' ? coverId : null,\n };\n };\n};\n\nconst boundedLimit = (limit: number | undefined) =>\n Math.min(Math.max(limit ?? SNAPSHOT_CHANGES_DEFAULT_LIMIT, 1), SNAPSHOT_CHANGES_MAX_LIMIT);\n\n/** `GET /api/snapshots/changes`, GraphQL `_changes`: one page of entries changed between `from` and `to`. */\nexport const listSnapshotChanges = async (\n context: ContentServiceContext,\n query: SnapshotChangesQuery,\n): Promise<SnapshotChangesPage> => {\n if (query.after !== undefined && !UUID.test(query.after)) {\n throw queryInvalid('\"after\" must be a cursor returned by a previous page');\n }\n const readable = await readableModels(context);\n const byId = new Map(readable.map((model) => [model.definition.id, model]));\n return contentQueriesRepository.withConsistentRead(async (trx) => {\n const current = await publicationsRepository.currentSeq(context.site.id, trx);\n const to = query.to ?? current;\n if (to > current) {\n throw snapshotInvalid(to, current);\n }\n if (query.from < 0 || query.from > to) {\n throw queryInvalid('\"from\" must be between 0 and \"to\"');\n }\n const page = await snapshotDiffRepository.listChanges(\n {\n siteId: context.site.id,\n from: query.from,\n to,\n after: query.after,\n limit: boundedLimit(query.limit),\n modelIds: [...byId.keys()],\n rowFilter: compileModelRowFilters(\n readable.map((model) => ({ modelId: model.definition.id, rowFilter: model.rowFilter })),\n context.actor,\n ),\n },\n trx,\n );\n const versions = await snapshotDiffRepository.schemaVersionsAt(context.site.id, [query.from, to], trx);\n const describe = await describeEntries(page.items, byId, context.snapshot.defaultLocale, trx);\n return {\n from: query.from,\n to,\n schemaVersions: { from: versions.get(query.from) ?? null, to: versions.get(to) ?? null },\n items: page.items.flatMap((item) => {\n const model = byId.get(item.modelId)?.definition;\n return model\n ? [\n {\n id: item.entryId,\n modelId: item.modelId,\n modelKey: model.apiKey,\n routeKey: routeKeyOf(model),\n ...describe(item),\n locales: item.locales.map(({ locale, change, fromRevisionId, toRevisionId }) => ({\n locale,\n change,\n revisionId: toRevisionId ?? fromRevisionId,\n })),\n },\n ]\n : [];\n }),\n nextCursor: page.nextAfter,\n };\n }, context.db);\n};\n\nexport type CurrentSnapshot = {\n /** The latest publication sequence number (what an unpinned read serves). */\n snapshot: number;\n /** The active schema version (`system_versions`; what delivery serves now). */\n schemaVersion: number;\n /** When the latest snapshot was published; null when the ledger has no row for it (snapshot 0). */\n publishedAt: Date | null;\n};\n\n/** `GET /api/snapshots/current`, GraphQL `_snapshot`. */\nexport const currentSnapshot = async (context: ContentServiceContext): Promise<CurrentSnapshot> => {\n await readableModels(context);\n return contentQueriesRepository.withConsistentRead(async (trx) => {\n const snapshot = await publicationsRepository.currentSeq(context.site.id, trx);\n const publishedAt = await snapshotDiffRepository.snapshotCreatedAt(context.site.id, snapshot, trx);\n // The active version (system_versions), read in the same moment as the sequence. Not the ledger's:\n // metadata-only activations (a label rename) take no snapshot number, so the newest ledger row can\n // carry an older schema version than the live one.\n const schemaVersion = await schemaVersionsRepository.getSchemaVersion(trx);\n return { snapshot, schemaVersion, publishedAt: publishedAt ?? null };\n }, context.db);\n};\n","import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';\n\nconst TOKEN_BYTES = 32;\n\n/** A fresh secret: 32 random bytes, base64url (43 characters). */\nexport const generateToken = (): string => randomBytes(TOKEN_BYTES).toString('base64url');\n\n/**\n * What we store for a token: its SHA-256, hex. Tokens are high-entropy random values, so a fast hash is\n * enough (no salt or stretching needed); a leaked table does not reveal usable tokens.\n */\nexport const hashToken = (token: string): string => createHash('sha256').update(token, 'utf8').digest('hex');\n\n/** Constant-time comparison of two strings (unequal lengths return false without comparing). */\nexport const safeEqual = (a: string, b: string): boolean => {\n const left = Buffer.from(a, 'utf8');\n const right = Buffer.from(b, 'utf8');\n return left.length === right.length && timingSafeEqual(left, right);\n};\n\n/** Constant-time check that `token` hashes to `expectedHash`. */\nexport const verifyTokenHash = (token: string, expectedHash: string): boolean =>\n safeEqual(hashToken(token), expectedHash);\n","import type { PermissionEvaluator, PermissionExecutor, Policy, Principal } from './types.js';\n\n/**\n * One request's evaluator, memoized per (principal, action, model): a request touches the same models many\n * times (REST: the read, relation visibility, populate, usage; GraphQL: lists, relation batches,\n * localizations), and evaluates each policy once (ADR 0005). Calls for another principal bypass the memo.\n * A policy still being evaluated is shared only on the executor that evaluates it: one evaluated inside a\n * read transaction is never awaited from another transaction. Once settled, every executor reuses it.\n */\nexport const memoizePermissions = (base: PermissionEvaluator, principal: Principal): PermissionEvaluator => {\n const settled = new Map<string, Policy>();\n const pooled = new Map<string, Promise<Policy>>();\n const byTransaction = new WeakMap<PermissionExecutor, Map<string, Promise<Policy>>>();\n const pendingFor = (executor: PermissionExecutor | undefined) => {\n if (!executor) {\n return pooled;\n }\n let pending = byTransaction.get(executor);\n if (!pending) {\n pending = new Map();\n byTransaction.set(executor, pending);\n }\n return pending;\n };\n return {\n evaluate: (caller, request, executor) => {\n if (caller !== principal) {\n return base.evaluate(caller, request, executor);\n }\n const key = `${request.action}:${request.modelId}`;\n const done = settled.get(key);\n if (done) {\n return Promise.resolve(done);\n }\n const pending = pendingFor(executor);\n let policy = pending.get(key);\n if (!policy) {\n policy = base.evaluate(caller, request, executor);\n pending.set(key, policy);\n // A failure reaches the caller through `policy`; here it only keeps the policy out of `settled`.\n policy.then(\n (value) => settled.set(key, value),\n () => undefined,\n );\n }\n return policy;\n },\n canPerform: (caller, action, executor) => base.canPerform(caller, action, executor),\n canPerformOnSite: (caller, action, siteId, executor) =>\n base.canPerformOnSite(caller, action, siteId, executor),\n };\n};\n","import { memoizePermissions } from './memo.js';\nimport type { KnownVersions, PermissionEvaluator, PermissionExecutor, Principal } from './types.js';\n\n/**\n * Where a scope's durable versions come from. HTTP reads them lazily, once per request (`plugins/requestState.ts`):\n * `read` reads them (or returns the read already made) and `isRead` says whether a read was started. A caller\n * that already holds them passes the versions themselves.\n */\nexport type ScopeVersions = KnownVersions | { read: () => Promise<KnownVersions>; isRead: () => boolean };\n\nconst isLazy = (\n versions: ScopeVersions,\n): versions is { read: () => Promise<KnownVersions>; isRead: () => boolean } => 'read' in versions;\n\n/**\n * The evaluator at the scope's versions. Inside a transaction before a lazy scope read its versions, the\n * unbound evaluator checks the versions itself through the transaction, so no pooled connection is asked\n * for while the transaction holds one.\n */\nconst boundEvaluator = async (\n base: PermissionEvaluator,\n versions: ScopeVersions,\n executor: PermissionExecutor | undefined,\n): Promise<PermissionEvaluator> => {\n if (!isLazy(versions)) {\n return base.atVersions?.(versions) ?? base;\n }\n if (executor && !versions.isRead()) {\n return base;\n }\n return base.atVersions?.(await versions.read()) ?? base;\n};\n\n/**\n * One scope's evaluator (an HTTP request, an in-process delivery call): checked against the scope's durable\n * versions (ADR 0005: a revoke applies from the next scope, not mid-scope) and memoized per (principal, action,\n * model) for the scope (`memo.ts`).\n */\nexport const scopePermissions = (\n base: PermissionEvaluator,\n principal: Principal,\n versions: ScopeVersions,\n): PermissionEvaluator =>\n memoizePermissions(\n {\n evaluate: async (caller, policyRequest, executor) =>\n (await boundEvaluator(base, versions, executor)).evaluate(caller, policyRequest, executor),\n canPerform: async (caller, action, executor) =>\n (await boundEvaluator(base, versions, executor)).canPerform(caller, action, executor),\n canPerformOnSite: async (caller, action, siteId, executor) =>\n (await boundEvaluator(base, versions, executor)).canPerformOnSite(caller, action, siteId, executor),\n },\n principal,\n );\n","import { AppError } from '../helpers/appError.js';\nimport type { AdminIdentity, AdminPrincipal, Principal, RoleAssignment, TokenPrincipal } from './types.js';\n\n/**\n * Narrowing principals to one site (sites plan §H). Roles are instance-wide; assignments say where a role\n * applies. On a site, an admin holds the roles assigned on that site plus the roles assigned on every site.\n * Network actions only ever see the roles assigned on every site.\n */\n\nconst unique = (ids: readonly string[]) => [...new Set(ids)].sort();\n\n/** Roles assigned on every site. */\nexport const networkRolesOf = (assignments: readonly RoleAssignment[]): string[] =>\n unique(\n assignments.filter((assignment) => assignment.siteId === null).map((assignment) => assignment.roleId),\n );\n\n/** Roles that apply on `siteId`: those assigned there and those assigned on every site. */\nexport const siteRolesOf = (assignments: readonly RoleAssignment[], siteId: string | null): string[] =>\n unique(\n assignments\n .filter((assignment) => assignment.siteId === null || assignment.siteId === siteId)\n .map((assignment) => assignment.roleId),\n );\n\n/** Sites the admin holds a role on specifically (not counting roles assigned on every site). */\nexport const assignedSiteIdsOf = (assignments: readonly RoleAssignment[]): string[] =>\n unique(assignments.flatMap((assignment) => (assignment.siteId === null ? [] : [assignment.siteId])));\n\n/** The admin principal for one site, or for a network route (`siteId` null: network roles only). */\nexport const narrowToSite = (identity: AdminIdentity, siteId: string | null): AdminPrincipal => ({\n kind: 'admin',\n adminUserId: identity.adminUserId,\n sessionId: identity.sessionId,\n assignments: identity.assignments,\n siteId,\n roleIds: siteId === null ? networkRolesOf(identity.assignments) : siteRolesOf(identity.assignments, siteId),\n networkRoleIds: networkRolesOf(identity.assignments),\n});\n\n/** The identity behind an admin principal (to narrow it again to another site). */\nexport const identityOf = (principal: AdminPrincipal): AdminIdentity => ({\n adminUserId: principal.adminUserId,\n sessionId: principal.sessionId,\n assignments: principal.assignments,\n});\n\n/** A credential (token, app-user session, link or code) used on another site than its own. */\nexport const siteMismatch = () =>\n new AppError(403, 'SITE_MISMATCH', 'This credential belongs to another site than the one requested');\n\n/**\n * Re-narrows a principal to the request's site. Admins get that site's roles; anonymous callers get the site\n * (its `public` bindings apply). Site credentials (a site token, an app user) act on their own site only:\n * another site is refused (403 `SITE_MISMATCH`), whatever resolved it. A network token's role applies on\n * every site.\n */\nexport const principalForSite = (principal: Principal, siteId: string | null): Principal => {\n switch (principal.kind) {\n case 'admin':\n return narrowToSite(identityOf(principal), siteId);\n case 'anonymous':\n return { kind: 'anonymous', siteId };\n case 'appUser':\n if (principal.siteId !== siteId) {\n throw siteMismatch();\n }\n return principal;\n case 'token':\n if (principal.siteId !== null && principal.siteId !== siteId) {\n throw siteMismatch();\n }\n return principal;\n case 'system':\n return principal;\n }\n};\n\n/** The roles a token holds for network actions: only a network token's (site tokens never reach the network). */\nexport const tokenNetworkRoleIds = (token: TokenPrincipal): readonly string[] =>\n token.siteId === null ? [token.roleId] : [];\n","/** `system_settings` key of the delivery descriptor the server writes at start (services/deliveryDescriptor.ts). */\nexport const DELIVERY_DESCRIPTOR_SETTING = 'delivery_descriptor';\n","import type { JoinBuilder, Kysely, Transaction } from 'kysely';\nimport { DELIVERY_DESCRIPTOR_SETTING } from '../constants/delivery.js';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** Which site a request is about: by ID (the credential's site), by key (the site it names) or the primary. */\nexport type SiteLookup = { by: 'id'; id: string } | { by: 'key'; key: string } | { by: 'primary' };\n\nexport type RequestStateRow = {\n schemaVersion: number;\n permissionsVersion: number;\n /** The looked-up site; undefined when none matches or no lookup was asked for. */\n site: { id: string; key: string } | undefined;\n};\n\n/** The site a lookup names, joined to the one `system_versions` row (at most one row matches). */\nconst siteJoin = (join: JoinBuilder<DB, 'system_versions' | 'sites'>, lookup: SiteLookup) => {\n switch (lookup.by) {\n case 'id':\n return join.on('sites.id', '=', lookup.id);\n case 'key':\n return join.on('sites.key', '=', lookup.key);\n case 'primary':\n return join.on('sites.is_primary', '=', true);\n }\n};\n\n/**\n * Everything a request checks before its work, in one statement: the durable schema and permissions versions\n * (`system_versions`, one row) and, when asked, the request's site. Read once per request on the pool, before\n * any transaction; caches compare against these versions for the rest of the request.\n */\nexport const read = async (lookup: SiteLookup | undefined, trx: Executor = db): Promise<RequestStateRow> => {\n if (!lookup) {\n const row = await trx\n .selectFrom('system_versions')\n .select(['schema_version', 'permissions_version'])\n .executeTakeFirstOrThrow();\n return {\n schemaVersion: row.schema_version,\n permissionsVersion: row.permissions_version,\n site: undefined,\n };\n }\n const row = await trx\n .selectFrom('system_versions')\n .leftJoin('sites', (join) => siteJoin(join, lookup))\n .select([\n 'system_versions.schema_version',\n 'system_versions.permissions_version',\n 'sites.id as site_id',\n 'sites.key as site_key',\n ])\n .executeTakeFirstOrThrow();\n return {\n schemaVersion: row.schema_version,\n permissionsVersion: row.permissions_version,\n site: row.site_id !== null && row.site_key !== null ? { id: row.site_id, key: row.site_key } : undefined,\n };\n};\n\n/** What an in-process delivery call reads besides the request state (plan next-in-process §2). */\nexport type DeliveryStateRow = RequestStateRow & {\n /** The release the server last started with; null before a server of this release started. */\n release: string | null;\n /** The stored delivery descriptor (JSON text); null before a server of this release started. */\n descriptor: string | null;\n /** The token's revocation and expiry; null when no token was asked about or the token no longer exists. */\n token: { revokedAt: Date | null; expiresAt: Date | null } | null;\n};\n\n/**\n * The request state for an in-process delivery call, in the same one statement: the versions and the site,\n * plus the server's release and delivery descriptor, and (for a token already resolved) whether it is still\n * live, so a call is this statement and its read.\n */\nexport const readForDelivery = async (\n input: { site: SiteLookup; tokenId?: string },\n trx: Executor = db,\n): Promise<DeliveryStateRow> => {\n const { tokenId } = input;\n const row = await trx\n .selectFrom('system_versions')\n .leftJoin('sites', (join) => siteJoin(join, input.site))\n .leftJoin('system_settings', (join) => join.on('system_settings.key', '=', DELIVERY_DESCRIPTOR_SETTING))\n .select([\n 'system_versions.schema_version',\n 'system_versions.permissions_version',\n 'system_versions.release',\n 'sites.id as site_id',\n 'sites.key as site_key',\n 'system_settings.value as descriptor',\n ])\n .$if(tokenId !== undefined, (query) =>\n query\n .leftJoin('api_tokens', (join) => join.on('api_tokens.id', '=', tokenId ?? ''))\n .select([\n 'api_tokens.id as token_id',\n 'api_tokens.revoked_at as token_revoked_at',\n 'api_tokens.expires_at as token_expires_at',\n ]),\n )\n .executeTakeFirstOrThrow();\n return {\n schemaVersion: row.schema_version,\n permissionsVersion: row.permissions_version,\n site: row.site_id !== null && row.site_key !== null ? { id: row.site_id, key: row.site_key } : undefined,\n release: row.release,\n descriptor: row.descriptor,\n token:\n row.token_id === undefined || row.token_id === null\n ? null\n : { revokedAt: row.token_revoked_at ?? null, expiresAt: row.token_expires_at ?? null },\n };\n};\n","import type { Insertable, Kysely, Selectable, Transaction, Updateable } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { AdminRolePermissions, AdminRoles, DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type AdminRoleRow = Selectable<AdminRoles>;\nexport type NewAdminRole = Insertable<AdminRoles>;\nexport type AdminRolePermissionRow = Selectable<AdminRolePermissions>;\nexport type NewAdminRolePermission = Insertable<AdminRolePermissions>;\n\nconst PERMISSION_COLUMNS = ['role_id', 'action', 'model_id', 'condition', 'field_ids'] as const;\n\n/** A grant as stored, without bookkeeping columns. */\nexport type PermissionGrantRow = Pick<AdminRolePermissionRow, (typeof PERMISSION_COLUMNS)[number]>;\n\nexport const listRoles = (trx: Executor = db) =>\n trx.selectFrom('admin_roles').selectAll().orderBy('is_system', 'desc').orderBy('name').execute();\n\nexport const findById = (id: string, trx: Executor = db) =>\n trx.selectFrom('admin_roles').selectAll().where('id', '=', id).executeTakeFirst();\n\nexport const findByIds = (ids: readonly string[], trx: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : trx.selectFrom('admin_roles').selectAll().where('id', 'in', ids).execute();\n\nexport const findByKeys = (keys: readonly string[], trx: Executor = db) =>\n keys.length === 0\n ? Promise.resolve([])\n : trx.selectFrom('admin_roles').selectAll().where('key', 'in', keys).execute();\n\n/** Inserts a role unless one with the same key exists; returns the inserted row or undefined. */\nexport const insertIfAbsent = (role: NewAdminRole, trx: Executor = db) =>\n trx\n .insertInto('admin_roles')\n .values(role)\n .onConflict((oc) => oc.column('key').doNothing())\n .returningAll()\n .executeTakeFirst();\n\nexport const insert = (role: NewAdminRole, trx: Executor = db) =>\n trx.insertInto('admin_roles').values(role).returningAll().executeTakeFirstOrThrow();\n\n/** Optimistic update: applies only if the stored version is `expectedVersion`; bumps the version. */\nexport const updateIfVersion = (\n id: string,\n expectedVersion: number,\n changes: Updateable<AdminRoles>,\n trx: Executor = db,\n) =>\n trx\n .updateTable('admin_roles')\n .set((eb) => ({ ...changes, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .returningAll()\n .executeTakeFirst();\n\nexport const deleteById = (id: string, trx: Executor = db) =>\n trx.deleteFrom('admin_roles').where('id', '=', id).executeTakeFirst();\n\n/** Every grant of every role; the permission cache loads them all at once (the table is small). */\nexport const listAllPermissions = (trx: Executor = db) =>\n trx.selectFrom('admin_role_permissions').select(PERMISSION_COLUMNS).execute();\n\nexport const listPermissionsForRoles = (roleIds: readonly string[], trx: Executor = db) =>\n roleIds.length === 0\n ? Promise.resolve([])\n : trx\n .selectFrom('admin_role_permissions')\n .select(PERMISSION_COLUMNS)\n .where('role_id', 'in', roleIds)\n .orderBy('action')\n .orderBy('model_id')\n .execute();\n\nexport const insertPermissionsIfAbsent = (rows: readonly NewAdminRolePermission[], trx: Executor = db) =>\n rows.length === 0\n ? Promise.resolve([])\n : trx\n .insertInto('admin_role_permissions')\n .values([...rows])\n .onConflict((oc) => oc.columns(['role_id', 'action', 'model_id']).doNothing())\n .returning('id')\n .execute();\n\nexport const deletePermissionsForRole = (roleId: string, trx: Executor = db) =>\n trx.deleteFrom('admin_role_permissions').where('role_id', '=', roleId).execute();\n\nexport const insertPermissions = (rows: readonly NewAdminRolePermission[], trx: Executor = db) =>\n rows.length === 0\n ? Promise.resolve([])\n : trx\n .insertInto('admin_role_permissions')\n .values([...rows])\n .returning('id')\n .execute();\n\n/** How many admin users and API tokens hold each role (for delete protection and list views). */\nexport const countHolders = async (roleId: string, trx: Executor = db) => {\n const row = await trx\n .selectNoFrom((eb) => [\n eb\n .selectFrom('admin_user_roles')\n .select((s) => s.fn.countAll<string>().as('n'))\n .where('role_id', '=', roleId)\n .as('users'),\n eb\n .selectFrom('api_tokens')\n .select((s) => s.fn.countAll<string>().as('n'))\n .where('role_id', '=', roleId)\n .where('revoked_at', 'is', null)\n .as('tokens'),\n ])\n .executeTakeFirstOrThrow();\n return { users: Number(row.users ?? 0), tokens: Number(row.tokens ?? 0) };\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport { currentTimestamp } from '../db/sql/time.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** The durable permissions version; caches compare against it (build plan §3.4). */\nexport const getPermissionsVersion = async (trx: Executor = db): Promise<number> => {\n const row = await trx.selectFrom('system_versions').select('permissions_version').executeTakeFirstOrThrow();\n return row.permissions_version;\n};\n\n/** Call in the same transaction as every permission change, so caches reload exactly when it commits. */\nexport const bumpPermissionsVersion = (trx: Executor = db) =>\n trx\n .updateTable('system_versions')\n .set((eb) => ({\n permissions_version: eb('permissions_version', '+', eb.lit(1)),\n updated_at: currentTimestamp(),\n }))\n .execute();\n","import type { Kysely } from 'kysely';\nimport type { DB } from '../db/types.js';\nimport * as adminRolesRepository from '../repositories/adminRoles.js';\nimport * as permissionsVersionRepository from '../repositories/permissionsVersion.js';\nimport { CONTENT_ACTIONS, GLOBAL_ACTIONS, type ContentAction, type GlobalAction } from './types.js';\n\nexport const SYSTEM_ROLE_KEYS = {\n owner: 'owner',\n admin: 'admin',\n editor: 'editor',\n readOnly: 'read-only',\n} as const;\n\ntype SystemRoleDefinition = {\n key: string;\n name: string;\n description: string;\n /** Granted on every model (model_id null), every field. */\n contentActions: readonly ContentAction[];\n globalActions: readonly GlobalAction[];\n};\n\n/**\n * Built-in roles. They cannot be edited or deleted; custom roles cover everything else. `admin` differs\n * from `owner` only in what the services allow around the owner role (only owners grant or remove it).\n */\nexport const SYSTEM_ROLES: readonly SystemRoleDefinition[] = [\n {\n key: SYSTEM_ROLE_KEYS.owner,\n name: 'Owner',\n description: 'Full access, including managing owners.',\n contentActions: CONTENT_ACTIONS,\n globalActions: GLOBAL_ACTIONS,\n },\n {\n key: SYSTEM_ROLE_KEYS.admin,\n name: 'Admin',\n description: 'Full access to content, models, users, roles and tokens. Cannot manage owners.',\n contentActions: CONTENT_ACTIONS,\n globalActions: GLOBAL_ACTIONS,\n },\n {\n key: SYSTEM_ROLE_KEYS.editor,\n name: 'Editor',\n description: 'Creates, edits, publishes and deletes content in every model.',\n contentActions: ['read', 'create', 'update', 'delete', 'publish'],\n // Editors group publications into change sets and (re)trigger site builds; webhooks, deployment\n // connections and the jobs view stay with admins.\n globalActions: ['media.read', 'media.write', 'changes.manage', 'changes.ship', 'deployments.trigger'],\n },\n {\n key: SYSTEM_ROLE_KEYS.readOnly,\n name: 'Read-only',\n description: 'Reads all content, changes nothing.',\n contentActions: ['read'],\n globalActions: ['media.read'],\n },\n];\n\n/**\n * Creates any missing built-in role and grant. Idempotent and safe to run from several instances at once\n * (conflicts are ignored); bumps the permissions version only when something was added. Runs at startup\n * and before `shapio admin create`.\n */\nexport const ensureSystemRoles = async (database: Kysely<DB>): Promise<void> => {\n await database.transaction().execute(async (trx) => {\n let changed = false;\n for (const definition of SYSTEM_ROLES) {\n const inserted = await adminRolesRepository.insertIfAbsent(\n {\n key: definition.key,\n name: definition.name,\n description: definition.description,\n kind: 'admin',\n is_system: true,\n },\n trx,\n );\n changed ||= inserted !== undefined;\n const [role] = await adminRolesRepository.findByKeys([definition.key], trx);\n if (!role) {\n throw new Error(`System role ${definition.key} is missing after insert`);\n }\n const actions = [...definition.contentActions, ...definition.globalActions];\n const added = await adminRolesRepository.insertPermissionsIfAbsent(\n actions.map((action) => ({\n role_id: role.id,\n action,\n model_id: null,\n condition: null,\n field_ids: null,\n })),\n trx,\n );\n changed ||= added.length > 0;\n }\n if (changed) {\n await permissionsVersionRepository.bumpPermissionsVersion(trx);\n }\n });\n};\n","import type { Insertable, Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { ApiTokens, DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type NewApiToken = Insertable<ApiTokens>;\n\nconst LIST_COLUMNS = [\n 'api_tokens.id',\n 'api_tokens.name',\n 'api_tokens.token_prefix',\n 'api_tokens.role_id',\n 'api_tokens.created_by',\n 'api_tokens.expires_at',\n 'api_tokens.last_used_at',\n 'api_tokens.revoked_at',\n 'api_tokens.created_at',\n 'api_tokens.site_id',\n 'admin_roles.kind as role_kind',\n] as const;\n\nconst withRole = (trx: Executor) =>\n trx\n .selectFrom('api_tokens')\n .innerJoin('admin_roles', 'admin_roles.id', 'api_tokens.role_id')\n .select(LIST_COLUMNS);\n\nexport const insert = (token: NewApiToken, trx: Executor = db) =>\n trx.insertInto('api_tokens').values(token).returning('id').executeTakeFirstOrThrow();\n\n/** One site's tokens, plus network tokens (no site) when `includeNetwork` is set. Newest first. */\nexport const listForSite = (siteId: string, includeNetwork: boolean, trx: Executor = db) =>\n withRole(trx)\n .where((eb) =>\n includeNetwork\n ? eb.or([eb('api_tokens.site_id', '=', siteId), eb('api_tokens.site_id', 'is', null)])\n : eb('api_tokens.site_id', '=', siteId),\n )\n .orderBy('api_tokens.created_at', 'desc')\n .orderBy('api_tokens.id', 'desc')\n .execute();\n\nexport const findById = (id: string, trx: Executor = db) =>\n withRole(trx).where('api_tokens.id', '=', id).executeTakeFirst();\n\n/** A usable token for this hash: not revoked. Expiry is checked by the caller. */\nexport const findLiveByHash = (tokenHash: string, trx: Executor = db) =>\n withRole(trx)\n .where('api_tokens.token_hash', '=', tokenHash)\n .where('api_tokens.revoked_at', 'is', null)\n .executeTakeFirst();\n\nexport const touchLastUsed = (id: string, now: Date, staleBefore: Date, trx: Executor = db) =>\n trx\n .updateTable('api_tokens')\n .set({ last_used_at: now })\n .where('id', '=', id)\n .where((eb) => eb.or([eb('last_used_at', 'is', null), eb('last_used_at', '<', staleBefore)]))\n .execute();\n\n/**\n * Revokes a live token of one site, or a network token (no site) when `includeNetwork` is set. Another\n * site's token is left alone (false, as for an unknown token).\n */\nexport const revoke = async (\n id: string,\n scope: { siteId: string; includeNetwork: boolean },\n now: Date,\n trx: Executor = db,\n): Promise<boolean> => {\n const result = await trx\n .updateTable('api_tokens')\n .set({ revoked_at: now, updated_at: now })\n .where('id', '=', id)\n .where('revoked_at', 'is', null)\n .where((eb) =>\n scope.includeNetwork\n ? eb.or([eb('site_id', '=', scope.siteId), eb('site_id', 'is', null)])\n : eb('site_id', '=', scope.siteId),\n )\n .executeTakeFirst();\n return result.numUpdatedRows > 0n;\n};\n\n/** Revoked tokens hold no permissions; they are removed with the role they referenced. */\nexport const deleteRevokedForRole = (roleId: string, trx: Executor = db) =>\n trx.deleteFrom('api_tokens').where('role_id', '=', roleId).where('revoked_at', 'is not', null).execute();\n","import type { Transaction } from 'kysely';\nimport {\n API_TOKEN_DISPLAY_LENGTH,\n API_TOKEN_PREFIX,\n LAST_SEEN_WRITE_INTERVAL_MS,\n} from '../constants/auth.js';\nimport { db, type Database } from '../db/index.js';\nimport type { DB } from '../db/types.js';\nimport { AppError } from '../helpers/appError.js';\nimport { generateToken, hashToken } from '../helpers/tokens.js';\nimport { SYSTEM_ROLE_KEYS } from '../permissions/seedRoles.js';\nimport type { PermissionEvaluator, PermissionExecutor, TokenPrincipal } from '../permissions/types.js';\nimport * as adminRolesRepository from '../repositories/adminRoles.js';\nimport * as apiTokensRepository from '../repositories/apiTokens.js';\nimport type { SiteActorContext } from './actorContext.js';\nimport { recordAudit } from './audit.js';\n\nexport type ApiTokenView = {\n id: string;\n name: string;\n /** The first characters of the token, for telling tokens apart. */\n tokenPrefix: string;\n roleId: string;\n scope: 'admin' | 'delivery';\n /** The token's site; null for a network admin token (its role applies on every site). */\n siteId: string | null;\n createdBy: string | null;\n expiresAt: Date | null;\n lastUsedAt: Date | null;\n revokedAt: Date | null;\n createdAt: Date;\n};\n\ntype ApiTokenListRow = Awaited<ReturnType<typeof apiTokensRepository.listForSite>>[number];\n\nconst scopeOf = (roleKind: string): 'admin' | 'delivery' => (roleKind === 'delivery' ? 'delivery' : 'admin');\n\nconst toApiTokenView = (row: ApiTokenListRow): ApiTokenView => ({\n id: row.id,\n name: row.name,\n tokenPrefix: row.token_prefix,\n roleId: row.role_id,\n scope: scopeOf(row.role_kind),\n siteId: row.site_id,\n createdBy: row.created_by,\n expiresAt: row.expires_at,\n lastUsedAt: row.last_used_at,\n revokedAt: row.revoked_at,\n createdAt: row.created_at,\n});\n\nexport const isApiTokenFormat = (value: string): boolean => value.startsWith(API_TOKEN_PREFIX);\n\n/**\n * Network tokens (no site) are visible and revocable only by admins who could mint them (`users.manage`, a\n * network action); everyone else sees their site's tokens only.\n */\nconst managesNetworkTokens = (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n executor?: PermissionExecutor,\n) => permissions.canPerform(context.actor, 'users.manage', executor);\n\n/** The request site's tokens, plus network tokens for those who manage them. */\nexport const listApiTokens = async (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n): Promise<ApiTokenView[]> =>\n (\n await apiTokensRepository.listForSite(context.site.id, await managesNetworkTokens(context, permissions))\n ).map(toApiTokenView);\n\ntype CreateApiTokenInput = {\n name: string;\n roleId: string;\n expiresAt: Date | null;\n /** A network admin token (every site and network actions); omitted or false: a token of the request's site. */\n network?: boolean | undefined;\n};\n\n/**\n * Whether the new token is a network token (sites plan §H). Tokens belong to the request's site unless the\n * creator asks for a network token explicitly. A network admin token carries its role on every site and\n * into network actions, so only someone who may assign roles on every site (`users.manage`, a network\n * action) may mint one. Delivery tokens always belong to a site.\n */\nconst isNetworkToken = async (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n roleKind: string,\n requested: boolean | undefined,\n trx: Transaction<DB>,\n): Promise<boolean> => {\n if (roleKind === 'delivery') {\n if (requested === true) {\n throw new AppError(400, 'INVALID_TOKEN_SITE', 'Delivery tokens always belong to one site');\n }\n return false;\n }\n if (requested !== true) {\n return false;\n }\n if (!(await managesNetworkTokens(context, permissions, trx))) {\n throw new AppError(\n 403,\n 'FORBIDDEN',\n 'Only admins who manage users on every site can create network tokens',\n );\n }\n return true;\n};\n\n/**\n * Creates a token bound to one role. The value is returned once and stored only as a hash. The owner\n * role cannot be bound to a token: owner powers stay with people.\n */\nexport const createApiToken = async (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n input: CreateApiTokenInput,\n): Promise<{ token: string; apiToken: ApiTokenView }> => {\n const now = new Date();\n if (input.expiresAt !== null && input.expiresAt.getTime() <= now.getTime()) {\n throw new AppError(400, 'INVALID_EXPIRY', 'The expiry must be in the future');\n }\n const token = `${API_TOKEN_PREFIX}${generateToken()}`;\n return db.transaction().execute(async (trx) => {\n const role = await adminRolesRepository.findById(input.roleId, trx);\n if (!role) {\n throw new AppError(400, 'INVALID_ROLES', 'The role does not exist');\n }\n if (role.key === SYSTEM_ROLE_KEYS.owner) {\n throw new AppError(400, 'INVALID_ROLES', 'API tokens cannot hold the owner role');\n }\n const network = await isNetworkToken(context, permissions, role.kind, input.network, trx);\n const { id } = await apiTokensRepository.insert(\n {\n name: input.name.trim(),\n token_hash: hashToken(token),\n token_prefix: token.slice(0, API_TOKEN_DISPLAY_LENGTH),\n role_id: role.id,\n site_id: network ? null : context.site.id,\n created_by: context.actor.kind === 'admin' ? context.actor.adminUserId : null,\n expires_at: input.expiresAt,\n },\n trx,\n );\n await recordAudit(trx, {\n ...context,\n action: 'api_token.create',\n target: { type: 'api_token', id },\n metadata: {\n name: input.name,\n roleId: role.id,\n siteId: network ? null : context.site.id,\n expiresAt: input.expiresAt?.toISOString() ?? null,\n },\n });\n const row = await apiTokensRepository.findById(id, trx);\n if (!row) {\n throw new Error('API token vanished after insert');\n }\n return { token, apiToken: toApiTokenView(row) };\n });\n};\n\n/** Revokes a token of the request's site (or a network token, for those who manage them). */\nexport const revokeApiToken = async (\n context: SiteActorContext,\n permissions: PermissionEvaluator,\n id: string,\n): Promise<void> => {\n const scope = { siteId: context.site.id, includeNetwork: await managesNetworkTokens(context, permissions) };\n await db.transaction().execute(async (trx) => {\n if (!(await apiTokensRepository.revoke(id, scope, new Date(), trx))) {\n throw new AppError(404, 'NOT_FOUND', 'Active API token not found');\n }\n await recordAudit(trx, { ...context, action: 'api_token.revoke', target: { type: 'api_token', id } });\n });\n};\n\ntype ResolveTokenOptions = {\n /** Records the token's last use (default). In-process delivery reads never write (plan next-in-process). */\n recordUse?: boolean;\n executor?: Database;\n};\n\n/** Whether a token with this expiry has expired at `now`. */\nexport const isTokenExpired = (expiresAt: Date | null, now: Date): boolean =>\n expiresAt !== null && expiresAt.getTime() <= now.getTime();\n\n/** The principal for a bearer API token, or undefined when unknown, revoked or expired. */\nexport const resolveApiToken = async (\n value: string,\n now = new Date(),\n { recordUse = true, executor = db }: ResolveTokenOptions = {},\n): Promise<TokenPrincipal | undefined> => {\n const row = await apiTokensRepository.findLiveByHash(hashToken(value), executor);\n if (!row || isTokenExpired(row.expires_at, now)) {\n return undefined;\n }\n const staleBefore = new Date(now.getTime() - LAST_SEEN_WRITE_INTERVAL_MS);\n // Skips the statement when the row just read is recent; the WHERE clause still guards concurrent writers.\n if (recordUse && (row.last_used_at === null || row.last_used_at.getTime() < staleBefore.getTime())) {\n await apiTokensRepository.touchLastUsed(row.id, now, staleBefore, executor);\n }\n return {\n kind: 'token',\n tokenId: row.id,\n scope: scopeOf(row.role_kind),\n roleId: row.role_id,\n siteId: row.site_id,\n };\n};\n","import type { Transaction } from 'kysely';\nimport type { ContentData } from '../db/contentData.js';\nimport type { DB } from '../db/types.js';\nimport type { Principal } from '../permissions/types.js';\nimport type { SiteRef } from '../services/actorContext.js';\nimport type { ContentModel } from './model.js';\n\n/**\n * Lifecycle hook points (ADR 0009). The content services call every hook point inside the owning\n * transaction; a hook that throws aborts the write. This registry is the engine only: the extension\n * runtime (extensions/hooks.ts) registers project hooks on it, running `before*` hooks right here in the\n * transaction and turning `after*` hook points into an outbox event, so project `after*` hooks run after\n * commit as jobs and can never roll the write back.\n */\nexport const BEFORE_EVENTS = ['beforeCreate', 'beforeUpdate', 'beforePublish', 'beforeDelete'] as const;\nexport const AFTER_EVENTS = ['afterCreate', 'afterUpdate', 'afterPublish', 'afterDelete'] as const;\nexport const LIFECYCLE_EVENTS = [...BEFORE_EVENTS, ...AFTER_EVENTS] as const;\n\nexport type BeforeEvent = (typeof BEFORE_EVENTS)[number];\nexport type AfterEvent = (typeof AFTER_EVENTS)[number];\nexport type LifecycleEvent = BeforeEvent | AfterEvent;\n\n/** The state of the entry the hook is about: its draft, its published head, or a deleted entry. */\nexport type LifecycleEntryState = 'draft' | 'published' | 'deleted';\n\nexport type LifecycleContext = {\n trx: Transaction<DB>;\n /** The site the entry belongs to (sites plan §H; ADR 0009 note). */\n site: SiteRef;\n model: ContentModel;\n entryId: string;\n locale: string | null;\n /** The document in storage form (keyed by field IDs); absent for deletes. */\n data?: Readonly<ContentData>;\n /** The document before the change (the previous draft, or the live version being replaced). */\n before?: Readonly<ContentData>;\n actor: Principal;\n};\n\nexport type LifecycleHook = (context: LifecycleContext) => Promise<void> | void;\n\nexport type ContentHooks = {\n on: (event: LifecycleEvent, hook: LifecycleHook) => () => void;\n run: (event: LifecycleEvent, context: LifecycleContext) => Promise<void>;\n};\n\nexport const ENTRY_STATE_BY_EVENT: Readonly<Record<LifecycleEvent, LifecycleEntryState>> = {\n beforeCreate: 'draft',\n afterCreate: 'draft',\n beforeUpdate: 'draft',\n afterUpdate: 'draft',\n beforePublish: 'published',\n afterPublish: 'published',\n beforeDelete: 'deleted',\n afterDelete: 'deleted',\n};\n\nexport const createContentHooks = (): ContentHooks => {\n const hooks = new Map<LifecycleEvent, LifecycleHook[]>();\n return {\n on: (event, hook) => {\n hooks.set(event, [...(hooks.get(event) ?? []), hook]);\n return () =>\n hooks.set(\n event,\n (hooks.get(event) ?? []).filter((candidate) => candidate !== hook),\n );\n },\n run: async (event, context) => {\n for (const hook of hooks.get(event) ?? []) {\n await hook(context);\n }\n },\n };\n};\n","import { createContentHooks, type ContentHooks } from '../content/hooks.js';\nimport type { Database } from '../db/index.js';\nimport type { PermissionEvaluator, Principal } from '../permissions/types.js';\nimport type { NetworkSchema } from '../schema/snapshot.js';\nimport type { SiteRef } from './actorContext.js';\nimport type { ContentServiceContext } from './contentAccess.js';\n\n/** What every caller of the content services has: an HTTP request, an extension service, an in-process read. */\nexport type ContentContextInput = {\n db: Database;\n /** The full schema the caller is pinned to; the context sees the site's view of it. */\n network: NetworkSchema;\n /** The caller's evaluator (memoized and version-bound for a request: `permissions/scoped.ts`). */\n permissions: PermissionEvaluator;\n actor: Principal;\n site: SiteRef;\n /** Lifecycle hooks for writes. Omitted: none (reads never reach a hook point). */\n hooks?: ContentHooks;\n media?: ContentServiceContext['media'];\n requestId?: string;\n ip?: string;\n};\n\n/**\n * The content service context for one call, the same however the call arrived: the site's view of the pinned\n * schema (plan site-schema: shared definitions and the site's own), scoped to the site.\n */\nexport const buildContentContext = ({\n network,\n hooks,\n media,\n requestId,\n ip,\n ...rest\n}: ContentContextInput): ContentServiceContext => ({\n ...rest,\n snapshot: network.forSite(rest.site.id),\n hooks: hooks ?? createContentHooks(),\n ...(media ? { media } : {}),\n ...(requestId !== undefined ? { requestId } : {}),\n ...(ip !== undefined ? { ip } : {}),\n});\n","import type { Kysely, Selectable, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB, SchemaRevisions } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type SchemaRevisionRow = Selectable<SchemaRevisions>;\n\n/** The active revision of every live (not deleted) model and component. */\nexport const findActiveDefinitions = (executor: Executor = db) =>\n executor\n .selectFrom('model_active_versions as active')\n .innerJoin('models', 'models.id', 'active.model_id')\n .innerJoin('schema_revisions as revision', 'revision.id', 'active.revision_id')\n .where('models.deleted_at', 'is', null)\n .select([\n 'models.id as modelId',\n 'models.kind as kind',\n 'models.site_id as siteId',\n 'active.version as version',\n 'active.revision_id as revisionId',\n 'active.activated_at as activatedAt',\n 'revision.hash as hash',\n 'revision.definition as definition',\n ])\n .orderBy('models.created_at')\n .orderBy('models.id')\n .execute();\n\nexport type ActiveDefinitionRow = Awaited<ReturnType<typeof findActiveDefinitions>>[number];\n\nexport const findModelById = (id: string, executor: Executor = db) =>\n executor.selectFrom('models').selectAll().where('id', '=', id).executeTakeFirst();\n\n/** `siteId` null: a shared definition (every site); otherwise the one site it belongs to. */\nexport const insertModel = (\n model: { id: string; kind: string; apiKey: string; siteId: string | null },\n trx: Executor = db,\n) =>\n trx\n .insertInto('models')\n .values({ id: model.id, kind: model.kind, api_key: model.apiKey, site_id: model.siteId })\n .execute();\n\n/** Definitions of one site that are not deleted (active or still pending their first activation). */\nexport const countSiteDefinitions = async (siteId: string, executor: Executor = db): Promise<number> => {\n const row = await executor\n .selectFrom('models')\n .select((eb) => eb.fn.countAll<number | string | bigint>().as('count'))\n .where('site_id', '=', siteId)\n .where('deleted_at', 'is', null)\n .executeTakeFirst();\n return Number(row?.count ?? 0);\n};\n\n/** A site's deleted definitions (what `deleteSite` purges with the site). */\nexport const findDeletedIdsOfSite = async (siteId: string, executor: Executor = db): Promise<string[]> =>\n (\n await executor\n .selectFrom('models')\n .select('id')\n .where('site_id', '=', siteId)\n .where('deleted_at', 'is not', null)\n .execute()\n ).map((row) => row.id);\n\n/**\n * Purges deleted definitions with their history (`deleteSite`, once the site has no live definition and the\n * definitions' deleted entries are purged): change rows, revisions, then the models rows.\n */\nexport const purgeDefinitions = async (ids: readonly string[], trx: Executor = db): Promise<void> => {\n if (ids.length === 0) {\n return;\n }\n await trx.deleteFrom('schema_change_jobs').where('model_id', 'in', ids).execute();\n await trx.deleteFrom('model_active_versions').where('model_id', 'in', ids).execute();\n // Newest first, one row at a time: a revision's parent always has a lower version, and MySQL checks the\n // self-reference row by row.\n const revisions = await trx\n .selectFrom('schema_revisions')\n .select('id')\n .where('model_id', 'in', ids)\n .orderBy('version', 'desc')\n .execute();\n for (const revision of revisions) {\n await trx.deleteFrom('schema_revisions').where('id', '=', revision.id).execute();\n }\n await trx.deleteFrom('models').where('id', 'in', ids).execute();\n};\n\n/**\n * Keeps the denormalised kind, API key and scope in step with the active revision; also undeletes. The scope\n * only differs from the stored one for a scope change (`ActivationItem.moveScope`).\n */\nexport const markModelActive = (\n model: { id: string; kind: string; apiKey: string; siteId: string | null; now: Date },\n trx: Executor = db,\n) =>\n trx\n .updateTable('models')\n .set({\n kind: model.kind,\n api_key: model.apiKey,\n site_id: model.siteId,\n updated_at: model.now,\n deleted_at: null,\n })\n .where('id', '=', model.id)\n .execute();\n\nexport const softDeleteModel = (id: string, now: Date, trx: Executor = db) =>\n trx.updateTable('models').set({ deleted_at: now, updated_at: now }).where('id', '=', id).execute();\n\nexport const findActivePointer = (modelId: string, executor: Executor = db) =>\n executor.selectFrom('model_active_versions').selectAll().where('model_id', '=', modelId).executeTakeFirst();\n\nexport const upsertActivePointer = (\n pointer: { modelId: string; revisionId: string; version: number; schemaVersion: number; now: Date },\n trx: Executor = db,\n) =>\n trx\n .insertInto('model_active_versions')\n .values({\n model_id: pointer.modelId,\n revision_id: pointer.revisionId,\n version: pointer.version,\n schema_version: pointer.schemaVersion,\n activated_at: pointer.now,\n })\n .onConflict((oc) =>\n oc.column('model_id').doUpdateSet({\n revision_id: pointer.revisionId,\n version: pointer.version,\n schema_version: pointer.schemaVersion,\n activated_at: pointer.now,\n }),\n )\n .execute();\n\nexport const deleteActivePointer = (modelId: string, trx: Executor = db) =>\n trx.deleteFrom('model_active_versions').where('model_id', '=', modelId).execute();\n\nexport const findLatestRevisionVersion = async (\n modelId: string,\n executor: Executor = db,\n): Promise<number> => {\n const row = await executor\n .selectFrom('schema_revisions')\n .select((eb) => eb.fn.max('version').as('version'))\n .where('model_id', '=', modelId)\n .executeTakeFirst();\n return row?.version ?? 0;\n};\n\nexport type NewRevision = {\n modelId: string;\n version: number;\n definition: unknown;\n hash: string;\n parentRevisionId: string | null;\n createdByType: string;\n createdById: string | null;\n};\n\nexport const insertRevision = (revision: NewRevision, trx: Executor = db) =>\n trx\n .insertInto('schema_revisions')\n .values({\n model_id: revision.modelId,\n version: revision.version,\n definition: JSON.stringify(revision.definition),\n hash: revision.hash,\n parent_revision_id: revision.parentRevisionId,\n created_by_type: revision.createdByType,\n created_by_id: revision.createdById,\n })\n .returning(['id', 'version', 'created_at'])\n .executeTakeFirstOrThrow();\n\nexport const findRevisionById = (id: string, executor: Executor = db) =>\n executor.selectFrom('schema_revisions').selectAll().where('id', '=', id).executeTakeFirst();\n\n/** History of one model, newest first, without the definitions (fetch one revision for its body). */\nexport const listRevisions = (modelId: string, limit: number, executor: Executor = db) =>\n executor\n .selectFrom('schema_revisions')\n .select(['id', 'version', 'hash', 'parent_revision_id', 'created_by_type', 'created_by_id', 'created_at'])\n .where('model_id', '=', modelId)\n .orderBy('version', 'desc')\n .limit(limit)\n .execute();\n\n/** The newest revision of a model, active or not (used to restore a deleted definition). */\nexport const findLatestRevision = (modelId: string, executor: Executor = db) =>\n executor\n .selectFrom('schema_revisions')\n .selectAll()\n .where('model_id', '=', modelId)\n .orderBy('version', 'desc')\n .limit(1)\n .executeTakeFirst();\n","import type { Transaction } from 'kysely';\nimport { LOCK_NAMESPACE } from '../../constants/lockKeys.js';\nimport { acquireXactLock, lockKeyFromId } from '../../db/advisoryLocks.js';\nimport type { DB } from '../../db/types.js';\n\n/**\n * Serialises every schema activation (and the start of every planned change) instance-wide. Activations are\n * rare, and holding this lock while re-validating the whole schema closes races between changes to\n * *different* definitions (e.g. two renames to colliding GraphQL names). It lives in the model namespace;\n * a collision with a model's hash only costs extra serialisation.\n */\nexport const SCHEMA_ACTIVATION_LOCK_KEY = lockKeyFromId('shapio:schema-activation');\n\n/**\n * Takes the global schema lock, then the exclusive per-model lock of each affected model in key order (the\n * same order entry writes use for their shared locks, so no deadlock). Entry writes to these models wait\n * until the activation commits (ADR 0002, transitional write policy).\n */\nexport const lockForActivation = async (trx: Transaction<DB>, modelIds: readonly string[]): Promise<void> => {\n await acquireXactLock(trx, LOCK_NAMESPACE.model, SCHEMA_ACTIVATION_LOCK_KEY);\n const keys = [...new Set(modelIds.map(lockKeyFromId))].sort((a, b) => a - b);\n for (const key of keys) {\n await acquireXactLock(trx, LOCK_NAMESPACE.model, key);\n }\n};\n\nexport const lockSchema = (trx: Transaction<DB>) =>\n acquireXactLock(trx, LOCK_NAMESPACE.model, SCHEMA_ACTIVATION_LOCK_KEY);\n","import { db } from '../db/index.js';\nimport type { Database } from '../db/index.js';\nimport { isUniqueViolation } from '../db/sql/errors.js';\nimport { AppError } from '../helpers/appError.js';\nimport { assignedSiteIdsOf } from '../permissions/sites.js';\nimport type { AdminPrincipal, TokenPrincipal } from '../permissions/types.js';\nimport * as appUsersRepository from '../repositories/appUsers.js';\nimport * as contentPurgeRepository from '../repositories/contentPurge.js';\nimport * as mediaAssetsRepository from '../repositories/mediaAssets.js';\nimport type { SiteLookup } from '../repositories/requestState.js';\nimport * as schemaModelsRepository from '../repositories/schemaModels.js';\nimport * as sitesRepository from '../repositories/sites.js';\nimport type { SiteRow } from '../repositories/sites.js';\nimport { lockSchema } from '../schema/planner/locks.js';\nimport type { ActorContext, SiteRef } from './actorContext.js';\nimport { recordAudit } from './audit.js';\n\n/**\n * Sites (plan agentic-ecosystem §H, ADR 0011): the tenants of content. The schema, locales, admin users and\n * roles are shared; everything a site publishes is its own. Exactly one site is primary: requests that name\n * no site go to it, and it can never be deleted.\n */\nexport type SiteView = {\n id: string;\n key: string;\n name: string;\n isPrimary: boolean;\n version: number;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type SiteSummaryView = Pick<SiteView, 'id' | 'key' | 'name' | 'isPrimary'>;\n\nexport const toSiteView = (row: SiteRow): SiteView => ({\n id: row.id,\n key: row.key,\n name: row.name,\n isPrimary: row.is_primary,\n version: row.version,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n});\n\nexport const toSiteSummary = (row: SiteRow): SiteSummaryView => ({\n id: row.id,\n key: row.key,\n name: row.name,\n isPrimary: row.is_primary,\n});\n\nexport const toSiteRef = (row: Pick<SiteRow, 'id' | 'key'>): SiteRef => ({ id: row.id, key: row.key });\n\nconst siteNotFound = () => new AppError(404, 'SITE_NOT_FOUND', 'Site not found');\n\n/** A site's summary by ID (`me` for a site the admin holds no role on: they see its name, nothing else). */\n/** How many sites the instance has, whatever roles the caller holds (one: per-site schema UI is moot). */\nexport const countSites = (): Promise<number> => sitesRepository.count();\n\nexport const getSiteSummary = async (siteId: string): Promise<SiteSummaryView> => {\n const site = await sitesRepository.findById(siteId);\n if (!site) {\n throw siteNotFound();\n }\n return toSiteSummary(site);\n};\n\n/** The reference of a site by ID (jobs and deferred work carry the ID of the site they belong to). */\nexport const getSiteRef = async (siteId: string, executor: Database = db): Promise<SiteRef> => {\n const site = await sitesRepository.findById(siteId, executor);\n if (!site) {\n throw siteNotFound();\n }\n return toSiteRef(site);\n};\n\n/**\n * The sites a principal works on: every site for a role assigned on every site (or a network token),\n * otherwise the sites it holds a role on. Used by `me` and the site switcher.\n */\nexport const listAccessibleSites = async (principal: AdminPrincipal | TokenPrincipal): Promise<SiteRow[]> => {\n if (principal.kind === 'token') {\n return principal.siteId === null ? sitesRepository.list() : sitesRepository.findByIds([principal.siteId]);\n }\n return principal.networkRoleIds.length > 0\n ? sitesRepository.list()\n : sitesRepository.findByIds(assignedSiteIdsOf(principal.assignments));\n};\n\nexport const listSites = async (principal: AdminPrincipal | TokenPrincipal): Promise<SiteView[]> =>\n (await listAccessibleSites(principal)).map(toSiteView);\n\nexport const getSite = async (principal: AdminPrincipal | TokenPrincipal, id: string): Promise<SiteView> => {\n const site = (await listAccessibleSites(principal)).find((row) => row.id === id);\n if (!site) {\n throw siteNotFound();\n }\n return toSiteView(site);\n};\n\n/** Every site, primary first (`shapio sites list`, which runs on the server host without a principal). */\nexport const listAllSites = async (database: Database = db): Promise<SiteView[]> =>\n (await sitesRepository.list(database)).map(toSiteView);\n\n/** A new site starts empty: its own publication sequence at 0, and no app roles bound (deny by default). */\nexport const createSite = async (\n context: ActorContext,\n input: { key: string; name: string },\n database: Database = db,\n): Promise<SiteView> => {\n try {\n return await database.transaction().execute(async (trx) => {\n const site = await sitesRepository.insert({ key: input.key, name: input.name.trim() }, trx);\n await sitesRepository.insertPublicationState(site.id, trx);\n await recordAudit(trx, {\n ...context,\n action: 'site.create',\n target: { type: 'site', id: site.id },\n metadata: { key: site.key, name: site.name },\n });\n return toSiteView(site);\n });\n } catch (error) {\n if (isUniqueViolation(error)) {\n throw new AppError(409, 'SITE_KEY_TAKEN', 'A site with this key already exists', undefined, {\n cause: error,\n });\n }\n throw error;\n }\n};\n\n/** Optimistic: fails with VERSION_CONFLICT when the site changed since `expectedVersion` was read. */\nexport const renameSite = async (\n context: ActorContext,\n id: string,\n input: { expectedVersion: number; name: string },\n): Promise<SiteView> =>\n db.transaction().execute(async (trx) => {\n const current = await sitesRepository.findById(id, trx);\n if (!current) {\n throw siteNotFound();\n }\n const updated = await sitesRepository.renameIfVersion(id, input.expectedVersion, input.name.trim(), trx);\n if (!updated) {\n throw new AppError(409, 'VERSION_CONFLICT', 'The site changed since you loaded it', {\n expectedVersion: input.expectedVersion,\n actualVersion: current.version,\n });\n }\n await recordAudit(trx, {\n ...context,\n action: 'site.update',\n target: { type: 'site', id },\n metadata: { name: { from: current.name, to: updated.name } },\n });\n return toSiteView(updated);\n });\n\n/**\n * Deletes an empty site: no live entries, media or app users, no folders, change sets or definitions of its\n * own. Its soft-deleted entries (with their history), definitions, media assets and app users are purged first; its tokens, webhooks, deployment\n * connections, role assignments, app role bindings, snapshot ledger and usage counters go with it.\n */\nexport const deleteSite = async (context: ActorContext, id: string): Promise<void> => {\n await db.transaction().execute(async (trx) => {\n const site = await sitesRepository.lockById(id, trx);\n if (!site) {\n throw siteNotFound();\n }\n if (site.is_primary) {\n throw new AppError(409, 'SITE_IS_PRIMARY', 'The primary site cannot be deleted');\n }\n // Serialised with schema changes, so no definition can be created on the site meanwhile.\n await lockSchema(trx);\n const contents = {\n ...(await sitesRepository.countContents(id, trx)),\n // The site's own content types and components (plan site-schema): delete or share them first.\n definitions: await schemaModelsRepository.countSiteDefinitions(id, trx),\n };\n if (Object.values(contents).some((count) => count > 0)) {\n throw new AppError(\n 409,\n 'SITE_NOT_EMPTY',\n 'Only an empty site can be deleted: remove its entries, media, change sets, app users and content types first',\n contents,\n );\n }\n await contentPurgeRepository.purgeDeletedEntriesOfSite(id, trx);\n const deletedDefinitions = await schemaModelsRepository.findDeletedIdsOfSite(id, trx);\n await contentPurgeRepository.purgeDeletedEntriesOfModels(deletedDefinitions, trx);\n await schemaModelsRepository.purgeDefinitions(deletedDefinitions, trx);\n await mediaAssetsRepository.deleteSoftDeletedOfSite(id, trx);\n await appUsersRepository.deleteSoftDeletedOfSite(id, trx);\n await sitesRepository.deleteById(id, trx);\n await recordAudit(trx, {\n ...context,\n action: 'site.delete',\n target: { type: 'site', id },\n metadata: { key: site.key, name: site.name },\n });\n });\n};\n\nexport type SiteResolutionInput = {\n /** The site the credential belongs to (a site token); undefined when the credential names none. */\n credentialSiteId: string | undefined;\n /** The site key the request names (`Shapio-Site` header or `?site=`). */\n requestedKey: string | undefined;\n};\n\n/** Reads the site a lookup names; undefined when none matches. */\nexport type SiteReader = (lookup: SiteLookup) => Promise<SiteRef | undefined>;\n\n/** The site a lookup names, read on its own (a request whose state was read without its site). */\nexport const findSite: SiteReader = async (lookup) => {\n switch (lookup.by) {\n case 'id':\n return sitesRepository.findById(lookup.id).then((row) => row && toSiteRef(row));\n case 'key':\n return sitesRepository.findByKey(lookup.key).then((row) => row && toSiteRef(row));\n case 'primary':\n return toSiteRef(await sitesRepository.findPrimary());\n }\n};\n\n/**\n * Which site a request is about (sites plan §H): the credential's site, else the site the request names,\n * else the primary site. A request that names a different site than its credential is refused\n * (403 `SITE_MISMATCH`), never redirected; an unknown key is 404 `SITE_NOT_FOUND`.\n * `read`: where the site is read; a request passes its once-per-request state read\n * (`repositories/requestState.ts`), which reads the site with the versions in one statement.\n */\nexport const resolveSite = async (\n { credentialSiteId, requestedKey }: SiteResolutionInput,\n read: SiteReader = findSite,\n): Promise<SiteRef> => {\n if (credentialSiteId !== undefined) {\n const site = await read({ by: 'id', id: credentialSiteId });\n if (!site) {\n throw siteNotFound();\n }\n if (requestedKey !== undefined && requestedKey !== site.key) {\n throw new AppError(\n 403,\n 'SITE_MISMATCH',\n `This credential belongs to site \"${site.key}\", not \"${requestedKey}\"`,\n );\n }\n return site;\n }\n if (requestedKey !== undefined) {\n const site = await read({ by: 'key', key: requestedKey });\n if (!site) {\n throw new AppError(404, 'SITE_NOT_FOUND', `No site has the key \"${requestedKey}\"`);\n }\n return site;\n }\n const primary = await read({ by: 'primary' });\n if (!primary) {\n // Every instance has exactly one primary site (migrations create it; it can never be deleted).\n throw new Error('No primary site');\n }\n return primary;\n};\n","import { SHAPIO_VERSION } from '../constants/version.js';\nimport { AppError } from '../helpers/appError.js';\nimport { hashToken } from '../helpers/tokens.js';\nimport { scopePermissions } from '../permissions/scoped.js';\nimport { principalForSite } from '../permissions/sites.js';\nimport type { Principal } from '../permissions/types.js';\nimport * as requestStateRepository from '../repositories/requestState.js';\nimport type { DeliveryStateRow, SiteLookup } from '../repositories/requestState.js';\nimport { isTokenExpired, resolveApiToken } from '../services/apiTokens.js';\nimport type { ContentServiceContext } from '../services/contentAccess.js';\nimport { buildContentContext } from '../services/contentContext.js';\nimport { resolveSite } from '../services/sites.js';\nimport { ShapioVersionSkewError } from './errors.js';\nimport type { DeliveryRuntime } from './runtime.js';\n\n/** Who an in-process call reads as, and which site it reads. */\nexport type CallCredentials = {\n /** A delivery API token; none reads as an anonymous caller (the site's `public` role). */\n token?: string;\n /** The site key; default the token's site, else the primary site (as over HTTP). */\n site?: string;\n};\n\nconst ANONYMOUS: Principal = Object.freeze({ kind: 'anonymous', siteId: null });\n\nconst invalidToken = () => new AppError(401, 'INVALID_TOKEN', 'The API token is invalid, expired or revoked');\n\n/**\n * Runs a statement of the call's preamble, mapping a failure on a database that predates this release (a\n * statement names a column or table it lacks) to a version skew rather than an SQL error.\n */\nconst guardingSkew = async <T>(runtime: DeliveryRuntime, statement: () => Promise<T>): Promise<T> => {\n try {\n return await statement();\n } catch (error) {\n if (error instanceof AppError) {\n throw error;\n }\n // Loaded only here: the migration list is not needed on the read path.\n const { getPendingMigrations } = await import('../db/migrator.js');\n if ((await getPendingMigrations(runtime.db)).length > 0) {\n throw new ShapioVersionSkewError(null, SHAPIO_VERSION, { cause: error });\n }\n throw error;\n }\n};\n\n/** The one state statement: versions, site, release, descriptor and the token's liveness. */\nconst readState = (\n runtime: DeliveryRuntime,\n site: SiteLookup,\n tokenId: string | undefined,\n): Promise<DeliveryStateRow> =>\n guardingSkew(runtime, () =>\n requestStateRepository.readForDelivery(\n { site, ...(tokenId !== undefined ? { tokenId } : {}) },\n runtime.db,\n ),\n );\n\n/**\n * The caller: anonymous, or a delivery token resolved once per runtime (read-only: in-process reads never\n * record a token's use) and checked for liveness on every call by the state read. Admin-scope tokens are\n * refused: in-process reads are delivery reads only (rule 7).\n */\nconst callerOf = async (\n runtime: DeliveryRuntime,\n token: string | undefined,\n): Promise<{ principal: Principal; tokenKey?: string; tokenId?: string }> => {\n if (token === undefined) {\n return { principal: ANONYMOUS };\n }\n const tokenKey = hashToken(token);\n let principal = runtime.tokens.get(tokenKey);\n if (!principal) {\n const resolved = await guardingSkew(runtime, () =>\n resolveApiToken(token, new Date(), { recordUse: false, executor: runtime.db }),\n );\n if (!resolved) {\n throw invalidToken();\n }\n if (resolved.scope !== 'delivery') {\n throw new AppError(\n 403,\n 'DELIVERY_TOKEN_REQUIRED',\n 'In-process reads take a delivery token (or none); this is an admin API token',\n );\n }\n runtime.tokens.set(tokenKey, resolved);\n principal = resolved;\n }\n return { principal, tokenKey, tokenId: principal.tokenId };\n};\n\n/**\n * Opens one in-process call the way the HTTP API opens a request: one statement reads the schema and\n * permissions versions, the site, the server's release and descriptor, and the token's liveness; the call\n * then sees the schema and the permissions at those versions (a change committed later applies from the next\n * call, so a model changed in the admin shows on the next call without a restart: rule 1).\n */\nexport const openCallScope = async (\n runtime: DeliveryRuntime,\n { token, site: requestedKey }: CallCredentials,\n): Promise<ContentServiceContext> => {\n const caller = await callerOf(runtime, token);\n let state: DeliveryStateRow | undefined;\n const site = await resolveSite(\n {\n credentialSiteId:\n caller.principal.kind === 'token' ? (caller.principal.siteId ?? undefined) : undefined,\n requestedKey,\n },\n async (lookup) => {\n state = await readState(runtime, lookup, caller.tokenId);\n // Before anything else of the row is trusted: another release may mean other semantics.\n if (state.release !== SHAPIO_VERSION) {\n throw new ShapioVersionSkewError(state.release, SHAPIO_VERSION);\n }\n return state.site;\n },\n );\n if (!state) {\n throw new Error('The site was resolved without reading the request state');\n }\n if (caller.tokenKey !== undefined) {\n const live = state.token;\n if (!live || live.revokedAt !== null || isTokenExpired(live.expiresAt, new Date())) {\n runtime.tokens.delete(caller.tokenKey);\n throw invalidToken();\n }\n }\n if (state.descriptor === null) {\n throw new AppError(503, 'SERVER_NOT_STARTED', 'Start the Shapio server of this release once');\n }\n const versions = {\n schemaVersion: state.schemaVersion,\n permissionsVersion: state.permissionsVersion,\n };\n const principal = principalForSite(caller.principal, site.id);\n const [network, media] = await Promise.all([\n runtime.registry.getSnapshot(undefined, versions),\n runtime.mediaFor(state.descriptor),\n ]);\n return buildContentContext({\n db: runtime.db,\n network,\n permissions: scopePermissions(runtime.permissions, principal, versions),\n actor: principal,\n site,\n media,\n });\n};\n","import { createClient, ShapioApiError, type RequestFn, type RequestOptions } from '@shapio/client';\nimport { Value } from 'typebox/value';\nimport { SITE_QUERY_PARAMETER } from '../constants/sites.js';\nimport { apiKeyOfRoute } from '../content/model.js';\nimport { AppError } from '../helpers/appError.js';\nimport type { ErrorLike } from '../helpers/errorResponse.js';\nimport { withoutSiteParameter } from '../helpers/siteQuery.js';\nimport { SnapshotChangesQuerySchema, type SnapshotChangesQuery } from '../routes/snapshots/schemas.js';\nimport type { ContentServiceContext } from '../services/contentAccess.js';\nimport { getDelivery, listDelivery } from '../services/contentDelivery.js';\nimport { getDeliverySite } from '../services/siteSeo.js';\nimport { currentSnapshot, listSnapshotChanges } from '../services/snapshotChanges.js';\nimport { ShapioVersionSkewError, toShapioApiError } from './errors.js';\nimport type { DeliveryRuntime } from './runtime.js';\nimport { openCallScope, type CallCredentials } from './scope.js';\n\n/**\n * The delivery API as a `RequestFn` (`@shapio/client`'s transport): the client's delivery, site and snapshot\n * groups call it with the same paths they send over HTTP, and it answers them in process, with the same\n * bodies, and errors as the same `ShapioApiError`. Paths:\n *\n * - `GET /api/content/:routeKey` (a collection's page, or a singleton's entry)\n * - `GET /api/content/:routeKey/:id`\n * - `GET /api/site`\n * - `GET /api/snapshots/current`\n * - `GET /api/snapshots/changes`\n *\n * Anything else (writes, previews, the admin API, GraphQL) is 404 `NOT_AVAILABLE_IN_PROCESS`.\n */\nexport type DeliveryRoute =\n | { kind: 'list'; routeKey: string }\n | { kind: 'item'; routeKey: string; id: string }\n | { kind: 'site' }\n | { kind: 'snapshotCurrent' }\n | { kind: 'snapshotChanges' };\n\n/** A matched path: the route, its querystring without `site`, and the site the path names, if any. */\nexport type MatchedRequest = { route: DeliveryRoute; query: string; site: string | undefined };\n\nconst decodeSegment = (segment: string): string | undefined => {\n try {\n return decodeURIComponent(segment);\n } catch {\n return undefined;\n }\n};\n\nconst routeOf = (segments: readonly string[]): DeliveryRoute | undefined => {\n const [api, group, first, second, ...rest] = segments;\n if (api !== 'api' || rest.length > 0) {\n return undefined;\n }\n if (group === 'content' && first) {\n const routeKey = decodeSegment(first);\n if (routeKey === undefined) {\n return undefined;\n }\n if (second === undefined) {\n return { kind: 'list', routeKey };\n }\n const id = decodeSegment(second);\n return id ? { kind: 'item', routeKey, id } : undefined;\n }\n if (group === 'site' && first === undefined) {\n return { kind: 'site' };\n }\n if (group === 'snapshots' && second === undefined) {\n if (first === 'current') {\n return { kind: 'snapshotCurrent' };\n }\n if (first === 'changes') {\n return { kind: 'snapshotChanges' };\n }\n }\n return undefined;\n};\n\n/** Matches a delivery read; undefined for any other method or path. */\nexport const matchDeliveryRequest = (method: string, path: string): MatchedRequest | undefined => {\n if (method.toUpperCase() !== 'GET') {\n return undefined;\n }\n const index = path.indexOf('?');\n const pathname = index === -1 ? path : path.slice(0, index);\n const rawQuery = index === -1 ? '' : path.slice(index + 1);\n if (!pathname.startsWith('/')) {\n return undefined;\n }\n const route = routeOf(pathname.slice(1).split('/'));\n if (!route) {\n return undefined;\n }\n // An empty `?site=` names no site, as over HTTP.\n const site = new URLSearchParams(rawQuery).get(SITE_QUERY_PARAMETER) || undefined;\n return { route, query: withoutSiteParameter(rawQuery), site };\n};\n\nconst notAvailable = (method: string, path: string) =>\n new ShapioApiError(404, {\n error: {\n code: 'NOT_AVAILABLE_IN_PROCESS',\n message: `${method.toUpperCase()} ${path.split('?')[0] ?? path} is not available in process; only delivery reads are`,\n },\n });\n\n/** The site a call reads: the path's `?site=` or the credentials' site; both must agree. */\nconst siteOf = (pathSite: string | undefined, credentialsSite: string | undefined): string | undefined => {\n if (pathSite !== undefined && credentialsSite !== undefined && pathSite !== credentialsSite) {\n throw new AppError(403, 'SITE_MISMATCH', 'The site option and ?site= name different sites');\n }\n return pathSite ?? credentialsSite;\n};\n\n/** `?from&to&after&limit`, coerced and checked against the HTTP route's own schema. */\nconst snapshotChangesQuery = (rawQuery: string): SnapshotChangesQuery => {\n const value = Value.Convert(SnapshotChangesQuerySchema, Object.fromEntries(new URLSearchParams(rawQuery)));\n if (Value.Check(SnapshotChangesQuerySchema, value)) {\n return value;\n }\n const validation = Value.Errors(SnapshotChangesQuerySchema, value).map(\n ({ instancePath, message, params }) => ({ instancePath, message, params }),\n );\n const [first] = validation;\n const failure: ErrorLike = Object.assign(\n new Error(`querystring${first?.instancePath ?? ''} ${first?.message ?? 'is invalid'}`),\n { statusCode: 400, validation },\n );\n throw failure;\n};\n\n/**\n * Values exactly as the HTTP API sends them: a JSON round trip, as the server serializes them. Dates become ISO\n * strings, `undefined` members disappear, and so do symbol-keyed members (a delivered rich-text value carries\n * its HTML renderer for GraphQL behind a symbol), so a caller gets plain data that holds nothing else.\n */\nconst asJson = (value: unknown): unknown => JSON.parse(JSON.stringify(value)) as unknown;\n\nconst dispatch = async (\n context: ContentServiceContext,\n { route, query }: MatchedRequest,\n): Promise<unknown> => {\n switch (route.kind) {\n case 'list':\n return listDelivery(context, apiKeyOfRoute(context.snapshot, route.routeKey), query);\n case 'item':\n return getDelivery(context, apiKeyOfRoute(context.snapshot, route.routeKey), route.id, query);\n case 'site':\n return getDeliverySite(context);\n case 'snapshotCurrent':\n return currentSnapshot(context);\n case 'snapshotChanges':\n return listSnapshotChanges(context, snapshotChangesQuery(query));\n }\n};\n\n/**\n * A `RequestFn` that answers delivery reads in this process, as `credentials` (a delivery token or none, and\n * a site). With `onVersionSkew: 'http'`, a call refused for a release mismatch is sent to the server instead.\n */\nexport const createInProcessRequest = (runtime: DeliveryRuntime, credentials: CallCredentials): RequestFn => {\n let fallback: RequestFn | undefined;\n let warnedSkew = false;\n const fallbackRequest = (): RequestFn | undefined => {\n if (runtime.onVersionSkew !== 'http' || runtime.fallbackUrl === undefined) {\n return undefined;\n }\n fallback ??= createClient({\n baseUrl: runtime.fallbackUrl,\n ...(credentials.token !== undefined ? { token: credentials.token } : {}),\n ...(credentials.site !== undefined ? { site: credentials.site } : {}),\n }).request;\n return fallback;\n };\n\n return async <T>(path: string, options: RequestOptions = {}): Promise<T> => {\n const method = options.method ?? 'GET';\n const matched = matchDeliveryRequest(method, path);\n if (!matched) {\n throw notAvailable(method, path);\n }\n try {\n const site = siteOf(matched.site, credentials.site);\n const context = await openCallScope(runtime, {\n ...(credentials.token !== undefined ? { token: credentials.token } : {}),\n ...(site !== undefined ? { site } : {}),\n });\n const result = asJson(await dispatch(context, matched)) as T;\n warnedSkew = false;\n return result;\n } catch (error) {\n const http = error instanceof ShapioVersionSkewError ? fallbackRequest() : undefined;\n if (http && error instanceof ShapioVersionSkewError) {\n if (!warnedSkew) {\n warnedSkew = true;\n runtime.log.warn(\n { serverRelease: error.serverRelease, libraryRelease: error.libraryRelease },\n 'release mismatch; reading over HTTP until the releases match',\n );\n }\n return http<T>(path, options);\n }\n throw toShapioApiError(error, runtime.log);\n }\n };\n};\n","/**\n * The ONLY place Shapio builds absolute URLs (media, preview, OAuth callbacks, emails, OpenAPI servers,\n * redirects). Everything derives from PUBLIC_URL (the origin) and BASE_PATH (the sub-path), so Shapio\n * works on any domain, port or sub-path without a reverse proxy rewriting links. ESLint forbids\n * building `http(s)://` strings anywhere else in apps/api/src.\n */\nexport type UrlBuilder = {\n /** Origin without a trailing slash, e.g. `https://cms.example.com`. */\n readonly publicUrl: string;\n /** `''` at the root, otherwise `/cms`. */\n readonly basePath: string;\n /** App-relative path → path on this server: `/api/ready` → `/cms/api/ready`. For routes and Location headers. */\n withBasePath: (path: string) => string;\n /** App-relative path → absolute URL: `/api/ready` → `https://cms.example.com/cms/api/ready`. */\n absoluteUrl: (path: string) => string;\n /** A request's raw URL (already including BASE_PATH) → absolute URL on the public origin. For redirects. */\n originUrl: (rawUrl: string) => string;\n};\n\nexport class InvalidUrlPathError extends Error {\n constructor(path: string) {\n super(`Expected an app-relative path starting with a single \"/\", got ${JSON.stringify(path)}`);\n this.name = 'InvalidUrlPathError';\n }\n}\n\nconst assertRelativePath = (path: string): void => {\n // `//host` would be protocol-relative: an open redirect waiting to happen.\n if (!path.startsWith('/') || path.startsWith('//')) {\n throw new InvalidUrlPathError(path);\n }\n};\n\nexport const createUrlBuilder = ({\n publicUrl,\n basePath,\n}: {\n publicUrl: string;\n basePath: string;\n}): UrlBuilder => {\n const origin = publicUrl.replace(/\\/+$/, '');\n const withBasePath = (path: string) => {\n assertRelativePath(path);\n return `${basePath}${path}`;\n };\n return {\n publicUrl: origin,\n basePath,\n withBasePath,\n absoluteUrl: (path) => `${origin}${withBasePath(path)}`,\n originUrl: (rawUrl) => {\n const safe = rawUrl.startsWith('/') ? rawUrl.replace(/^\\/+/, '/') : '/';\n return `${origin}${safe}`;\n },\n };\n};\n\ntype LoopbackTarget = { port: number; basePath: string; https: boolean };\n\n/**\n * URL of this server on the loopback interface (for health probes from inside the host or container),\n * which deliberately bypasses PUBLIC_URL.\n */\nexport const loopbackUrl = ({ port, basePath, https }: LoopbackTarget, path: string): string => {\n assertRelativePath(path);\n return `${https ? 'https' : 'http'}://127.0.0.1:${port}${basePath}${path}`;\n};\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\n/**\n * HMAC signatures for media capabilities, keyed by the instance signing secret (`app.signingSecret`).\n * Each purpose has its own prefix so a signature for one can never be replayed as another.\n */\ntype SignedPurpose = 'media-get' | 'media-upload';\n\nconst sign = (secret: string, purpose: SignedPurpose, subject: string, expiresAt: number): string =>\n createHmac('sha256', secret).update(`${purpose}\\n${subject}\\n${expiresAt}`).digest('base64url');\n\nconst safeEqual = (a: string, b: string): boolean => {\n const left = Buffer.from(a);\n const right = Buffer.from(b);\n return left.length === right.length && timingSafeEqual(left, right);\n};\n\nexport type SignatureCheck = 'valid' | 'invalid' | 'expired';\n\nconst verify = (\n secret: string,\n purpose: SignedPurpose,\n subject: string,\n expiresAt: number,\n signature: string,\n now: number,\n): SignatureCheck => {\n if (!Number.isSafeInteger(expiresAt) || !safeEqual(sign(secret, purpose, subject, expiresAt), signature)) {\n return 'invalid';\n }\n return expiresAt * 1000 <= now ? 'expired' : 'valid';\n};\n\n/** `expiresAt` is in Unix seconds. */\nexport const signMediaGet = (secret: string, key: string, expiresAt: number): string =>\n sign(secret, 'media-get', key, expiresAt);\n\nexport const verifyMediaGet = (\n secret: string,\n key: string,\n expiresAt: number,\n signature: string,\n now = Date.now(),\n): SignatureCheck => verify(secret, 'media-get', key, expiresAt, signature, now);\n\n/** The capability to upload bytes for one grant (local driver), sent as a form field like an S3 policy. */\nexport const signUploadGrant = (secret: string, grantId: string, expiresAt: number): string =>\n sign(secret, 'media-upload', grantId, expiresAt);\n\nexport const verifyUploadGrant = (\n secret: string,\n grantId: string,\n expiresAt: number,\n signature: string,\n now = Date.now(),\n): SignatureCheck => verify(secret, 'media-upload', grantId, expiresAt, signature, now);\n","import { randomBytes } from 'node:crypto';\nimport { createReadStream, createWriteStream } from 'node:fs';\nimport { mkdir, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';\nimport { dirname, relative, resolve, sep } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { MEDIA_FILE_ROUTE_PREFIX } from '../constants/media.js';\nimport type { UrlBuilder } from '../helpers/publicUrl.js';\nimport { InvalidStorageKeyError, ObjectNotFoundError } from './errors.js';\nimport { encodeKeyPath, isValidStorageKey } from './keys.js';\nimport { signMediaGet } from './signing.js';\nimport type { StorageAdapter } from './types.js';\n\nexport type LocalAdapterOptions = {\n /** MEDIA_PATH, absolute. */\n root: string;\n urls: UrlBuilder;\n /** Needed to sign private URLs; jobs and the CLI never sign, so they may omit it. */\n signingSecret: string | undefined;\n publicBaseUrl: string | undefined;\n};\n\nconst isNotFound = (error: unknown) =>\n error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT';\n\n/**\n * Files under MEDIA_PATH, served by Shapio itself: public objects at a stable URL, private ones only with\n * an HMAC-signed, expiring URL. Writes go to a temporary file and are renamed into place, so a reader never\n * sees half a file.\n */\nexport const createLocalAdapter = ({\n root,\n urls,\n signingSecret,\n publicBaseUrl,\n}: LocalAdapterOptions): StorageAdapter => {\n const pathOf = (key: string): string => {\n const path = resolve(root, key);\n if (!isValidStorageKey(key) || relative(root, path).startsWith('..') || !path.startsWith(root + sep)) {\n throw new InvalidStorageKeyError(key);\n }\n return path;\n };\n\n const statOrThrow = async (key: string) => {\n try {\n return await stat(pathOf(key));\n } catch (error) {\n throw isNotFound(error) ? new ObjectNotFoundError(key, { cause: error }) : error;\n }\n };\n\n /** Removes now-empty directories up to the asset folder; a non-empty one stops the walk. */\n const pruneEmptyParents = async (path: string) => {\n let directory = dirname(path);\n while (directory.startsWith(root + sep) && relative(root, directory).split(sep).length > 1) {\n try {\n await rmdir(directory);\n } catch {\n return;\n }\n directory = dirname(directory);\n }\n };\n\n return {\n driver: 'local',\n put: async (key, body) => {\n const path = pathOf(key);\n await mkdir(dirname(path), { recursive: true });\n const temporary = `${path}.${randomBytes(6).toString('hex')}.tmp`;\n try {\n if (Buffer.isBuffer(body)) {\n await writeFile(temporary, body);\n } else {\n await pipeline(body, createWriteStream(temporary));\n }\n await rename(temporary, path);\n } catch (error) {\n await rm(temporary, { force: true });\n throw error;\n }\n },\n getStream: async (key, range) => {\n await statOrThrow(key);\n return createReadStream(pathOf(key), range ? { start: range.start, end: range.end } : {});\n },\n head: async (key) => {\n try {\n const info = await statOrThrow(key);\n return { size: info.size, contentType: undefined };\n } catch (error) {\n if (error instanceof ObjectNotFoundError) {\n return undefined;\n }\n throw error;\n }\n },\n exists: async (key) => {\n try {\n await statOrThrow(key);\n return true;\n } catch (error) {\n if (error instanceof ObjectNotFoundError) {\n return false;\n }\n throw error;\n }\n },\n delete: async (key) => {\n const path = pathOf(key);\n await rm(path, { force: true });\n await pruneEmptyParents(path);\n },\n signedGetUrl: async (key, { expiresInSeconds }) => {\n if (!signingSecret) {\n throw new Error('The local media adapter was created without a signing secret');\n }\n pathOf(key);\n const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;\n const signature = signMediaGet(signingSecret, key, expiresAt);\n return urls.absoluteUrl(\n `${MEDIA_FILE_ROUTE_PREFIX}${encodeKeyPath(key)}?expires=${expiresAt}&signature=${signature}`,\n );\n },\n publicUrl: (key) => (publicBaseUrl ? `${publicBaseUrl}/${encodeKeyPath(key)}` : undefined),\n };\n};\n","import type { StorageConfig } from '../config/index.js';\nimport type { UrlBuilder } from '../helpers/publicUrl.js';\nimport { createLocalAdapter } from './localAdapter.js';\nimport type { MediaStorage, StorageAdapter, StorageDriver } from './types.js';\n\nexport type MediaStorageDependencies = {\n urls: UrlBuilder;\n /** Required by the API (private URLs); jobs and the CLI never sign URLs. */\n signingSecret?: string;\n};\n\nexport class StorageNotConfiguredError extends Error {\n constructor(driver: StorageDriver) {\n super(\n driver === 's3'\n ? 'Media is stored on S3 but STORAGE_S3_BUCKET is not set'\n : `Storage driver ${driver} is not configured`,\n );\n this.name = 'StorageNotConfiguredError';\n }\n}\n\n/**\n * Every adapter this instance can reach. Local disk is always available; S3 whenever its bucket is set,\n * whatever STORAGE_DRIVER says, so assets keep being served while `shapio media migrate` moves them. The S3\n * adapter (and the AWS SDK) is loaded only then.\n */\nexport const createMediaStorage = async (\n config: StorageConfig,\n deps: MediaStorageDependencies,\n): Promise<MediaStorage> => {\n const adapters = new Map<StorageDriver, StorageAdapter>();\n adapters.set(\n 'local',\n createLocalAdapter({\n root: config.mediaPath,\n urls: deps.urls,\n signingSecret: deps.signingSecret,\n publicBaseUrl: config.publicBaseUrl,\n }),\n );\n if (config.s3) {\n const { createS3Adapter } = await import('./s3Adapter.js');\n adapters.set('s3', createS3Adapter({ config: config.s3, publicBaseUrl: config.publicBaseUrl }));\n }\n const get = (driver: StorageDriver): StorageAdapter => {\n const adapter = adapters.get(driver);\n if (!adapter) {\n throw new StorageNotConfiguredError(driver);\n }\n return adapter;\n };\n return { active: get(config.driver), get, has: (driver) => adapters.has(driver) };\n};\n","import type { Insertable, Kysely, Selectable, Transaction, Updateable } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { AppRolePermissions, AppRoles, DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport type AppRoleRow = Selectable<AppRoles>;\nexport type NewAppRole = Insertable<AppRoles>;\nexport type NewAppRolePermission = Insertable<AppRolePermissions>;\n\nconst PERMISSION_COLUMNS = ['role_id', 'action', 'model_id', 'condition', 'field_ids'] as const;\n\n/** A grant as stored, without bookkeeping columns (the same shape as admin role grants). */\nexport type AppPermissionGrantRow = Pick<Selectable<AppRolePermissions>, (typeof PERMISSION_COLUMNS)[number]>;\n\nexport const listRoles = (trx: Executor = db) =>\n trx.selectFrom('app_roles').selectAll().orderBy('is_system', 'desc').orderBy('name').execute();\n\nexport const findById = (id: string, trx: Executor = db) =>\n trx.selectFrom('app_roles').selectAll().where('id', '=', id).executeTakeFirst();\n\nexport const findByIds = (ids: readonly string[], trx: Executor = db) =>\n ids.length === 0\n ? Promise.resolve([])\n : trx.selectFrom('app_roles').selectAll().where('id', 'in', ids).execute();\n\nexport const insert = (role: NewAppRole, trx: Executor = db) =>\n trx.insertInto('app_roles').values(role).returningAll().executeTakeFirstOrThrow();\n\n/** Optimistic update: applies only if the stored version is `expectedVersion`; bumps the version. */\nexport const updateIfVersion = (\n id: string,\n expectedVersion: number,\n changes: Updateable<AppRoles>,\n trx: Executor = db,\n) =>\n trx\n .updateTable('app_roles')\n .set((eb) => ({ ...changes, version: eb('version', '+', 1), updated_at: new Date() }))\n .where('id', '=', id)\n .where('version', '=', expectedVersion)\n .returningAll()\n .executeTakeFirst();\n\nexport const deleteById = (id: string, trx: Executor = db) =>\n trx.deleteFrom('app_roles').where('id', '=', id).executeTakeFirst();\n\n/** Every grant of every app role; the permission cache loads them with the admin grants (small table). */\nexport const listAllPermissions = (trx: Executor = db) =>\n trx.selectFrom('app_role_permissions').select(PERMISSION_COLUMNS).execute();\n\nexport const listPermissionsForRoles = (roleIds: readonly string[], trx: Executor = db) =>\n roleIds.length === 0\n ? Promise.resolve([])\n : trx\n .selectFrom('app_role_permissions')\n .select(PERMISSION_COLUMNS)\n .where('role_id', 'in', roleIds)\n .orderBy('action')\n .orderBy('model_id')\n .execute();\n\nexport const deletePermissionsForRole = (roleId: string, trx: Executor = db) =>\n trx.deleteFrom('app_role_permissions').where('role_id', '=', roleId).execute();\n\nexport const insertPermissions = (rows: readonly NewAppRolePermission[], trx: Executor = db) =>\n rows.length === 0\n ? Promise.resolve([])\n : trx\n .insertInto('app_role_permissions')\n .values([...rows])\n .returning('id')\n .execute();\n\n/** How many live app users hold the role (delete protection, list views). */\nexport const countHolders = async (roleId: string, trx: Executor = db): Promise<number> => {\n const row = await trx\n .selectFrom('app_user_roles')\n .innerJoin('app_users', 'app_users.id', 'app_user_roles.app_user_id')\n .select((eb) => eb.fn.countAll<string>().as('n'))\n .where('app_user_roles.role_id', '=', roleId)\n .where('app_users.deleted_at', 'is', null)\n .executeTakeFirstOrThrow();\n return Number(row.n);\n};\n\n/** Live holders per role, for list views (roles nobody holds are absent). */\nexport const countHoldersByRole = async (trx: Executor = db): Promise<Map<string, number>> => {\n const rows = await trx\n .selectFrom('app_user_roles')\n .innerJoin('app_users', 'app_users.id', 'app_user_roles.app_user_id')\n .select((eb) => ['app_user_roles.role_id', eb.fn.countAll<string>().as('n')])\n .where('app_users.deleted_at', 'is', null)\n .groupBy('app_user_roles.role_id')\n .execute();\n return new Map(rows.map((row) => [row.role_id, Number(row.n)]));\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\n/** Who a binding applies to: anonymous callers (`public`) or every signed-in app user (`authenticated`). */\nexport type AppRoleAudience = 'public' | 'authenticated';\n\nexport type SiteAppRoleBinding = { site_id: string; audience: string; role_id: string };\n\n/** Every binding on every site (the permission cache holds them all, like the grants). */\nexport const listAll = (trx: Executor = db): Promise<SiteAppRoleBinding[]> =>\n trx.selectFrom('site_app_roles').select(['site_id', 'audience', 'role_id']).execute();\n\nexport const listForSite = (siteId: string, trx: Executor = db): Promise<SiteAppRoleBinding[]> =>\n trx\n .selectFrom('site_app_roles')\n .select(['site_id', 'audience', 'role_id'])\n .where('site_id', '=', siteId)\n .orderBy('audience')\n .orderBy('role_id')\n .execute();\n\n/** Replaces the roles bound to one audience on one site. */\nexport const replaceForAudience = async (\n siteId: string,\n audience: AppRoleAudience,\n roleIds: readonly string[],\n trx: Executor = db,\n): Promise<void> => {\n await trx\n .deleteFrom('site_app_roles')\n .where('site_id', '=', siteId)\n .where('audience', '=', audience)\n .execute();\n if (roleIds.length > 0) {\n await trx\n .insertInto('site_app_roles')\n .values(roleIds.map((roleId) => ({ site_id: siteId, audience, role_id: roleId })))\n .execute();\n }\n};\n","import type { Kysely } from 'kysely';\nimport type { DB } from '../db/types.js';\nimport * as adminRolesRepository from '../repositories/adminRoles.js';\nimport * as appRolesRepository from '../repositories/appRoles.js';\nimport * as permissionsVersionRepository from '../repositories/permissionsVersion.js';\nimport * as siteAppRolesRepository from '../repositories/siteAppRoles.js';\nimport type { AppRoleAudience } from '../repositories/siteAppRoles.js';\nimport type { Grant } from './policy.js';\nimport {\n CONTENT_ACTIONS,\n DELIVERY_ACTIONS,\n GLOBAL_ACTIONS,\n type KnownVersions,\n type PermissionExecutor,\n} from './types.js';\n\n/**\n * Where the evaluator gets grants from. The database-backed cache in production, a fixed list in unit tests.\n * `getSiteAppRoleIds`: the app roles a site binds to anonymous callers (`public`) or to every signed-in app\n * user (`authenticated`); a site that binds nothing grants them nothing (sites plan §H).\n */\nexport type GrantSource = {\n getGrants: (\n roleIds: readonly string[],\n executor?: PermissionExecutor,\n versions?: KnownVersions,\n ) => Promise<readonly Grant[]>;\n getSiteAppRoleIds: (\n siteId: string,\n audience: AppRoleAudience,\n executor?: PermissionExecutor,\n versions?: KnownVersions,\n ) => Promise<readonly string[]>;\n};\n\ntype Snapshot = {\n version: number;\n byRole: ReadonlyMap<string, readonly Grant[]>;\n /** `<siteId>:<audience>` → bound app role IDs. */\n bindings: ReadonlyMap<string, readonly string[]>;\n};\n\nconst bindingKey = (siteId: string, audience: string) => `${siteId}:${audience}`;\n\nconst groupBindings = (rows: readonly siteAppRolesRepository.SiteAppRoleBinding[]) => {\n const bindings = new Map<string, string[]>();\n for (const row of rows) {\n const key = bindingKey(row.site_id, row.audience);\n bindings.set(key, [...(bindings.get(key) ?? []), row.role_id]);\n }\n return bindings;\n};\n\nconst KNOWN_ACTIONS: ReadonlySet<string> = new Set([\n ...CONTENT_ACTIONS,\n ...DELIVERY_ACTIONS,\n ...GLOBAL_ACTIONS,\n]);\n\nconst KNOWN_CONDITIONS: ReadonlySet<string> = new Set<NonNullable<Grant['condition']>>(['ownedByPrincipal']);\n\n/**\n * A stored grant as this build understands it. Rows written by a newer Shapio may name an action or a row\n * condition this build does not know; they grant nothing here (fail closed). An unknown condition must never\n * read as \"no condition\", which would widen the grant to every row.\n */\nexport const toGrant = (row: adminRolesRepository.PermissionGrantRow): Grant | undefined => {\n if (!KNOWN_ACTIONS.has(row.action)) {\n return undefined;\n }\n if (row.condition !== null && !KNOWN_CONDITIONS.has(row.condition)) {\n return undefined;\n }\n return {\n roleId: row.role_id,\n action: row.action as Grant['action'],\n modelId: row.model_id,\n condition: row.condition as Grant['condition'],\n fieldIds: row.field_ids,\n };\n};\n\nconst groupByRole = (grants: readonly Grant[]): Map<string, Grant[]> => {\n const byRole = new Map<string, Grant[]>();\n for (const grant of grants) {\n const list = byRole.get(grant.roleId) ?? [];\n list.push(grant);\n byRole.set(grant.roleId, list);\n }\n return byRole;\n};\n\n/**\n * Every role's grants, held in memory and reloaded when `system_versions.permissions_version` moves.\n * The version is checked on every lookup: read here, or read once by the request (`versions`, see\n * `KnownVersions`). That durable check is what makes a permission change visible to every instance from the\n * next request (NOTIFY would only be an optimisation). Lookups on the pool share one reload; a\n * lookup inside a transaction reads (and, when stale, reloads) through that transaction alone, never\n * through a reload running on another request's transaction.\n */\nexport const createPermissionCache = (database: Kysely<DB>): GrantSource => {\n let snapshot: Snapshot | undefined;\n let reloading: Promise<Snapshot> | undefined;\n\n const reload = async (version: number, executor: PermissionExecutor): Promise<Snapshot> => {\n // Read after the version: grants are at least as new as the version they are filed under, and a\n // change that commits in between bumps the version again, so the next lookup reloads.\n // Admin and app role IDs are distinct UUIDs; principals only ever carry roles of their own kind.\n const rows = [\n ...(await adminRolesRepository.listAllPermissions(executor)),\n ...(await appRolesRepository.listAllPermissions(executor)),\n ];\n const grants = rows.map(toGrant).filter((grant): grant is Grant => grant !== undefined);\n const bindings = groupBindings(await siteAppRolesRepository.listAll(executor));\n return { version, byRole: groupByRole(grants), bindings };\n };\n\n const sharedReload = (version: number): Promise<Snapshot> =>\n (reloading ??= reload(version, database).finally(() => {\n reloading = undefined;\n }));\n\n const current = async (executor?: PermissionExecutor, versions?: KnownVersions): Promise<Snapshot> => {\n const version =\n versions?.permissionsVersion ??\n (await permissionsVersionRepository.getPermissionsVersion(executor ?? database));\n if (snapshot && snapshot.version === version) {\n return snapshot;\n }\n const loaded = await (executor ? reload(version, executor) : sharedReload(version));\n if (!snapshot || loaded.version >= snapshot.version) {\n snapshot = loaded;\n }\n return loaded;\n };\n\n return {\n getGrants: async (roleIds, executor, versions) => {\n if (roleIds.length === 0) {\n return [];\n }\n const { byRole } = await current(executor, versions);\n return roleIds.flatMap((roleId) => byRole.get(roleId) ?? []);\n },\n getSiteAppRoleIds: async (siteId, audience, executor, versions) => {\n const { bindings } = await current(executor, versions);\n return bindings.get(bindingKey(siteId, audience)) ?? [];\n },\n };\n};\n","import type { ContentAction } from './types.js';\n\n/**\n * Built-in app roles (package I). Their IDs are fixed (seeded by migration 20261001180000_create_app_users),\n * so the evaluator can apply them without a lookup: `public` to every anonymous caller, `authenticated` to\n * every signed-in app user on top of their custom roles.\n */\nexport const APP_ROLE_IDS = {\n public: '00000000-0000-4000-a000-000000000001',\n authenticated: '00000000-0000-4000-a000-000000000002',\n} as const;\n\nexport const APP_ROLE_KEYS = { public: 'public', authenticated: 'authenticated' } as const;\n\n/**\n * What app roles may grant: content actions only. Schema management and every instance-level action stay\n * with admin roles, whatever an app role row says.\n */\nexport const APP_CONTENT_ACTIONS = [\n 'read',\n 'create',\n 'update',\n 'delete',\n 'publish',\n] as const satisfies readonly ContentAction[];\nexport type AppContentAction = (typeof APP_CONTENT_ACTIONS)[number];\n","import {\n DENIED_POLICY,\n type FieldMask,\n type GlobalAction,\n type KnownVersions,\n type ModelAction,\n type PermissionExecutor,\n type Policy,\n type PolicyRequest,\n type RowCondition,\n type RowFilter,\n} from './types.js';\n\n/** One permission row of a role: an action on one model (or every model when `modelId` is null). */\nexport type Grant = {\n roleId: string;\n action: ModelAction | GlobalAction;\n /** null = every model, including ones created later. Always null for global actions. */\n modelId: string | null;\n condition: RowCondition['kind'] | null;\n /** null = every field the principal's audience may see; otherwise exactly these stable field IDs. */\n fieldIds: readonly string[] | null;\n};\n\n/**\n * Who the policy is for. Admin principals (admin users, admin-scope tokens) see every field their grants\n * allow. Delivery principals (delivery tokens, app users, anonymous callers) see only fields with\n * `public: true` unless a grant names a non-public field explicitly (build plan §3.4).\n */\nexport type Audience = 'admin' | 'delivery';\n\nexport type ModelField = { id: string; public: boolean };\n\n/**\n * The one port from permissions to the schema registry (package D implements it from the active schema).\n * `getModelFields`: field definitions for `public` semantics; `undefined` means the model does not exist,\n * which denies delivery access. `hasModel`: whether a model or component ID exists, so role grants can\n * only name real models.\n */\nexport type FieldVisibilityLookup = {\n getModelFields: (\n modelId: string,\n executor?: PermissionExecutor,\n versions?: KnownVersions,\n ) => Promise<readonly ModelField[] | undefined>;\n hasModel: (modelId: string, executor?: PermissionExecutor, versions?: KnownVersions) => Promise<boolean>;\n /**\n * The site a definition belongs to: null when it is shared (or unknown). Schema management of a site's\n * definition is granted by roles held on that site (plan site-schema, rule 4).\n */\n getModelSite: (\n modelId: string,\n executor?: PermissionExecutor,\n versions?: KnownVersions,\n ) => Promise<string | null>;\n};\n\n/** Used until the schema registry is wired in: no model is known, so delivery principals see nothing. */\nexport const NO_FIELD_VISIBILITY: FieldVisibilityLookup = {\n getModelFields: () => Promise.resolve(undefined),\n hasModel: () => Promise.resolve(false),\n getModelSite: () => Promise.resolve(null),\n};\n\nconst EMPTY_MASK: FieldMask = { mode: 'only', fieldIds: [] };\n\nexport const ALLOW_ALL_POLICY: Policy = Object.freeze<Policy>({\n allowed: true,\n rowFilter: null,\n readMask: { mode: 'all' },\n writeMask: { mode: 'all' },\n});\n\nconst WRITE_ACTIONS: ReadonlySet<ModelAction> = new Set<ModelAction>(['create', 'update']);\n\nconst grantsFor = (grants: readonly Grant[], action: ModelAction, modelId: string) =>\n grants.filter((grant) => grant.action === action && (grant.modelId === null || grant.modelId === modelId));\n\n/** Rows: the union of what each grant allows. Any unconditional grant means no restriction. */\nexport const combineRowFilters = (grants: readonly Grant[]): RowFilter | null => {\n if (grants.some((grant) => grant.condition === null)) {\n return null;\n }\n const kinds = [...new Set(grants.map((grant) => grant.condition))].filter(\n (kind): kind is RowCondition['kind'] => kind !== null,\n );\n const filters: RowFilter[] = kinds.map((kind) => ({ kind }));\n if (filters.length === 1 && filters[0]) {\n return filters[0];\n }\n return { kind: 'or', filters };\n};\n\n/**\n * Fields: the union of what each grant allows. For delivery audiences a wildcard grant expands to the\n * model's public fields, so `public: false` fields stay hidden unless a grant names them. Delivery masks\n * are always explicit lists, so a field added after evaluation is never exposed by accident.\n */\nexport const combineFieldMasks = (\n grants: readonly Grant[],\n audience: Audience,\n fields: readonly ModelField[] | undefined,\n): FieldMask => {\n if (grants.length === 0) {\n return EMPTY_MASK;\n }\n const hasWildcard = grants.some((grant) => grant.fieldIds === null);\n if (audience === 'admin' && hasWildcard) {\n return { mode: 'all' };\n }\n const named = new Set(grants.flatMap((grant) => grant.fieldIds ?? []));\n if (audience === 'delivery') {\n const known = fields ?? [];\n const visible = known.filter((field) => named.has(field.id) || (hasWildcard && field.public));\n return { mode: 'only', fieldIds: visible.map((field) => field.id) };\n }\n return { mode: 'only', fieldIds: [...named].sort() };\n};\n\n/**\n * The policy for one (action, model) request from a principal's grants. Pure: no I/O, so it is unit- and\n * property-tested directly. Deny by default: no matching grant, no access.\n */\nexport const buildPolicy = (\n grants: readonly Grant[],\n request: PolicyRequest,\n audience: Audience,\n fields: readonly ModelField[] | undefined,\n): Policy => {\n if (audience === 'delivery' && fields === undefined) {\n // Unknown model: without its field definitions no delivery mask can be computed safely.\n return DENIED_POLICY;\n }\n const matching = grantsFor(grants, request.action, request.modelId);\n if (matching.length === 0) {\n return DENIED_POLICY;\n }\n const readGrants = request.action === 'read' ? matching : grantsFor(grants, 'read', request.modelId);\n return {\n allowed: true,\n rowFilter: combineRowFilters(matching),\n readMask: combineFieldMasks(readGrants, audience, fields),\n writeMask: WRITE_ACTIONS.has(request.action) ? combineFieldMasks(matching, audience, fields) : EMPTY_MASK,\n };\n};\n\n/** Global actions are granted only by a grant on that action with no model. */\nexport const allowsGlobalAction = (grants: readonly Grant[], action: GlobalAction): boolean =>\n grants.some((grant) => grant.action === action && grant.modelId === null);\n\n/**\n * A fixed lookup (model ID → fields), for tests and tools that have no schema registry. `hasModel` accepts\n * every ID unless `knownModelsOnly` is set, so tests can grant on models they never define.\n */\nexport const createStaticFieldVisibility = (\n models: Readonly<Record<string, readonly ModelField[]>>,\n {\n knownModelsOnly = false,\n modelSites = {},\n }: { knownModelsOnly?: boolean; modelSites?: Readonly<Record<string, string>> } = {},\n): FieldVisibilityLookup => ({\n getModelFields: (modelId) => Promise.resolve(Object.hasOwn(models, modelId) ? models[modelId] : undefined),\n hasModel: (modelId) => Promise.resolve(!knownModelsOnly || Object.hasOwn(models, modelId)),\n getModelSite: (modelId) =>\n Promise.resolve(Object.hasOwn(modelSites, modelId) ? (modelSites[modelId] ?? null) : null),\n});\n","import { APP_CONTENT_ACTIONS } from './appRoles.js';\nimport type { GrantSource } from './cache.js';\nimport {\n ALLOW_ALL_POLICY,\n allowsGlobalAction,\n buildPolicy,\n type Audience,\n type FieldVisibilityLookup,\n} from './policy.js';\nimport { tokenNetworkRoleIds } from './sites.js';\nimport {\n DELIVERY_ACTIONS,\n DENIED_POLICY,\n NETWORK_ACTIONS,\n NETWORK_CONTENT_ACTIONS,\n SITE_GRANTABLE_ACTIONS,\n type GlobalAction,\n type KnownVersions,\n type ModelAction,\n type PermissionEvaluator,\n type PermissionExecutor,\n type Principal,\n} from './types.js';\n\nconst NETWORK_ACTION_SET: ReadonlySet<GlobalAction> = new Set(NETWORK_ACTIONS);\n\ntype PrincipalRoles = {\n /** Roles that apply on the request's site (content and site actions). */\n roleIds: readonly string[];\n /** Roles that apply to the whole instance (network actions); a role held on one site is never here. */\n networkRoleIds: readonly string[];\n audience: Audience;\n /** Per-model actions the principal kind may ever perform; `all` = whatever its roles grant. */\n actions: ReadonlySet<ModelAction> | 'all';\n};\n\n/** Delivery tokens read, and read drafts where their role grants `readDrafts`. */\nconst DELIVERY_TOKEN_ACTIONS: ReadonlySet<ModelAction> = new Set(DELIVERY_ACTIONS);\n/** App users and anonymous callers: never `readDrafts`, whatever a role says. */\nconst APP_ACTIONS: ReadonlySet<ModelAction> = new Set(APP_CONTENT_ACTIONS);\n\n/**\n * Which roles a principal holds and how its masks are computed. App users hold the custom app roles assigned\n * to them plus the roles their site binds to `authenticated`; anonymous callers hold the roles the request's\n * site binds to `public` (`site_app_roles`: a site that binds nothing grants nothing, and an anonymous caller\n * outside a site holds no role). Both are delivery audiences: they see `public: false` fields only where a\n * grant names them.\n */\nconst rolesOf = async (\n principal: Principal,\n grants: GrantSource,\n executor: PermissionExecutor | undefined,\n versions: KnownVersions | undefined,\n): Promise<PrincipalRoles | 'all'> => {\n switch (principal.kind) {\n case 'system':\n return 'all';\n case 'admin':\n return {\n roleIds: principal.roleIds,\n networkRoleIds: principal.networkRoleIds,\n audience: 'admin',\n actions: 'all',\n };\n case 'token':\n return principal.scope === 'admin'\n ? {\n roleIds: [principal.roleId],\n networkRoleIds: tokenNetworkRoleIds(principal),\n audience: 'admin',\n actions: 'all',\n }\n : // Delivery tokens only ever read (drafts included when granted), whatever their role says.\n {\n roleIds: [principal.roleId],\n networkRoleIds: [],\n audience: 'delivery',\n actions: DELIVERY_TOKEN_ACTIONS,\n };\n case 'appUser':\n return {\n roleIds: [\n ...(await grants.getSiteAppRoleIds(principal.siteId, 'authenticated', executor, versions)),\n ...principal.roleIds,\n ],\n networkRoleIds: [],\n audience: 'delivery',\n actions: APP_ACTIONS,\n };\n case 'anonymous':\n return {\n roleIds:\n principal.siteId === null\n ? []\n : await grants.getSiteAppRoleIds(principal.siteId, 'public', executor, versions),\n networkRoleIds: [],\n audience: 'delivery',\n actions: APP_ACTIONS,\n };\n }\n};\n\ntype EvaluatorDependencies = { grants: GrantSource; fields: FieldVisibilityLookup };\n\n/**\n * Whether the principal acts on `siteId`: an admin narrowed to it, a site token of it, or a network token\n * (whose role applies on every site). Never an app user or anonymous caller (schema actions are admin-only).\n */\nconst actsOnSite = (principal: Principal, siteId: string): boolean => {\n switch (principal.kind) {\n case 'admin':\n return principal.siteId === siteId;\n case 'token':\n return principal.scope === 'admin' && (principal.siteId === null || principal.siteId === siteId);\n default:\n return false;\n }\n};\n\n/**\n * The one permission evaluator (ADR 0005): principal → roles → grants → Policy, for REST and GraphQL.\n * Deny by default at every step. `atVersions` gives the same evaluator for one request, whose caches are\n * checked against the versions the request read once instead of read again on every call.\n */\nexport const createPermissionEvaluator = ({ grants, fields }: EvaluatorDependencies): PermissionEvaluator => {\n const at = (versions: KnownVersions | undefined): PermissionEvaluator => ({\n evaluate: async (principal, request, executor) => {\n const roles = await rolesOf(principal, grants, executor, versions);\n if (roles === 'all') {\n return ALLOW_ALL_POLICY;\n }\n if (roles.actions !== 'all' && !roles.actions.has(request.action)) {\n return DENIED_POLICY;\n }\n // Schema management of a shared definition: network roles only (sites plan §H). Of a site's own\n // definition: the roles that apply on that site, when the principal acts there (plan site-schema).\n let roleIds = roles.roleIds;\n if (NETWORK_CONTENT_ACTIONS.has(request.action)) {\n const modelSite = await fields.getModelSite(request.modelId, executor, versions);\n roleIds =\n modelSite !== null && actsOnSite(principal, modelSite) ? roles.roleIds : roles.networkRoleIds;\n }\n const held = await grants.getGrants(roleIds, executor, versions);\n if (held.length === 0) {\n return DENIED_POLICY;\n }\n const modelFields =\n roles.audience === 'delivery'\n ? await fields.getModelFields(request.modelId, executor, versions)\n : undefined;\n return buildPolicy(held, request, roles.audience, modelFields);\n },\n canPerform: async (principal, action, executor) => {\n const roles = await rolesOf(principal, grants, executor, versions);\n if (roles === 'all') {\n return true;\n }\n if (roles.audience === 'delivery') {\n return false;\n }\n // Network actions only count roles assigned on every site, so a site role never reaches the network.\n const roleIds = NETWORK_ACTION_SET.has(action) ? roles.networkRoleIds : roles.roleIds;\n return allowsGlobalAction(await grants.getGrants(roleIds, executor, versions), action);\n },\n canPerformOnSite: async (principal, action, siteId, executor) => {\n const roles = await rolesOf(principal, grants, executor, versions);\n if (roles === 'all') {\n return true;\n }\n if (roles.audience === 'delivery') {\n return false;\n }\n const counted =\n !NETWORK_ACTION_SET.has(action) ||\n (SITE_GRANTABLE_ACTIONS.has(action) && actsOnSite(principal, siteId))\n ? roles.roleIds\n : roles.networkRoleIds;\n return allowsGlobalAction(await grants.getGrants(counted, executor, versions), action);\n },\n atVersions: at,\n });\n return at(undefined);\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nexport const findValue = async (key: string, trx: Executor = db): Promise<string | undefined> =>\n (await trx.selectFrom('system_settings').select('value').where('key', '=', key).executeTakeFirst())?.value;\n\n/** Inserts the setting unless it exists; returns true when this call created it. */\nexport const insertIfAbsent = async (key: string, value: string, trx: Executor = db): Promise<boolean> => {\n const inserted = await trx\n .insertInto('system_settings')\n .values({ key, value })\n .onConflict((oc) => oc.column('key').doNothing())\n .returning('key')\n .executeTakeFirst();\n return inserted !== undefined;\n};\n\n/** Creates or replaces a setting. */\nexport const upsertValue = async (key: string, value: string, trx: Executor = db): Promise<void> => {\n await trx\n .insertInto('system_settings')\n .values({ key, value })\n .onConflict((oc) => oc.column('key').doUpdateSet({ value, updated_at: new Date() }))\n .execute();\n};\n","import type { FieldVisibilityLookup, ModelField } from '../permissions/policy.js';\nimport type { KnownVersions, PermissionExecutor } from '../permissions/types.js';\nimport type { SchemaRegistry } from './registry.js';\n\n/**\n * The permission evaluator's view of field visibility (`public` flags), served from the active schema.\n * Uses the durable version check on every call (or the version the request read once, `versions`), so a\n * field made non-public is hidden on every instance from the next request, whether or not it got the\n * notification. Deprecated fields are reported as non-public: they are hidden from the API unless a role\n * names them explicitly. Components are not delivery models.\n */\nexport const createSchemaFieldVisibility = (registry: SchemaRegistry): FieldVisibilityLookup => {\n const activeModel = async (\n modelId: string,\n executor: PermissionExecutor | undefined,\n versions: KnownVersions | undefined,\n ) => {\n const active = (await registry.getSnapshot(executor, versions)).byId.get(modelId);\n return active && active.definition.kind !== 'component' ? active.definition : undefined;\n };\n return {\n getModelFields: async (modelId, executor, versions) =>\n (await activeModel(modelId, executor, versions))?.fields.map((field): ModelField => ({\n id: field.id,\n public: field.public && !field.deprecated,\n })),\n hasModel: async (modelId, executor, versions) =>\n (await activeModel(modelId, executor, versions)) !== undefined,\n // Components too: managing a site's component is a site's schema permission like its models'.\n getModelSite: async (modelId, executor, versions) =>\n (await registry.getSnapshot(executor, versions)).scopeOf(modelId) ?? null,\n };\n};\n","import type { Kysely, Transaction } from 'kysely';\nimport { db } from '../db/index.js';\nimport type { DB } from '../db/types.js';\n\ntype Executor = Kysely<DB> | Transaction<DB>;\n\nconst COLUMNS = ['code', 'label', 'is_default', 'fallbacks', 'created_at', 'updated_at'] as const;\n\nexport const list = (executor: Executor = db) =>\n executor.selectFrom('locales').select(COLUMNS).orderBy('is_default', 'desc').orderBy('code').execute();\n\nexport type LocaleRow = Awaited<ReturnType<typeof list>>[number];\n\nexport const findByCode = (code: string, executor: Executor = db) =>\n executor.selectFrom('locales').select(COLUMNS).where('code', '=', code).executeTakeFirst();\n\nexport const insert = (locale: { code: string; label: string; fallbacks: string[] }, trx: Executor = db) =>\n trx\n .insertInto('locales')\n .values({ code: locale.code, label: locale.label, fallbacks: locale.fallbacks, is_default: false })\n .returning(COLUMNS)\n .executeTakeFirstOrThrow();\n\nexport const update = (\n code: string,\n changes: { label: string; fallbacks: string[]; now: Date },\n trx: Executor = db,\n) =>\n trx\n .updateTable('locales')\n .set({ label: changes.label, fallbacks: changes.fallbacks, updated_at: changes.now })\n .where('code', '=', code)\n .returning(COLUMNS)\n .executeTakeFirst();\n\n/** Makes `code` the only default locale. Clears the old default first (single-default unique index). */\nexport const setDefault = async (code: string, now: Date, trx: Executor = db) => {\n await trx\n .updateTable('locales')\n .set({ is_default: false, updated_at: now })\n .where('is_default', '=', true)\n .execute();\n await trx\n .updateTable('locales')\n .set({ is_default: true, updated_at: now })\n .where('code', '=', code)\n .execute();\n};\n\nexport const remove = (code: string, trx: Executor = db) =>\n trx.deleteFrom('locales').where('code', '=', code).executeTakeFirst();\n\n/** Removes `code` from every other locale's fallback chain. */\nexport const removeFromFallbacks = async (code: string, now: Date, trx: Executor = db) => {\n const rows = await trx.selectFrom('locales').select(['code', 'fallbacks']).execute();\n for (const row of rows.filter((candidate) => candidate.fallbacks.includes(code))) {\n await trx\n .updateTable('locales')\n .set({ fallbacks: row.fallbacks.filter((entry) => entry !== code), updated_at: now })\n .where('code', '=', row.code)\n .execute();\n }\n};\n","import {\n findDependentModels,\n isComponentDefinition,\n routeKeyOf,\n type LocaleDefinition,\n type SchemaDefinition,\n} from '@shapio/schema';\n\n/** One definition as it is active right now. */\nexport type ActiveDefinition = {\n definition: SchemaDefinition;\n /** The site the definition belongs to; null when it is shared by every site (plan site-schema). */\n siteId: string | null;\n /** Per-model version: the optimistic-concurrency token for edits. */\n version: number;\n revisionId: string;\n hash: string;\n activatedAt: Date;\n};\n\n/**\n * Two models whose route keys are equal. The validator refuses this on save, but a collection stored before\n * plural API IDs gets a derived plural on read, which can equal another model's API ID (`post` → `posts`).\n */\nexport type RouteKeyCollision = { routeKey: string; servedApiKey: string; hiddenApiKey: string };\n\n/**\n * What every schema exposes: lookups by stable ID, the locales, and scopes. Code that only resolves by ID\n * (content checks, validators, conversions) takes this, so it works on the full set and on a site's view.\n */\nexport type SchemaById = {\n version: number;\n definitions: readonly ActiveDefinition[];\n byId: ReadonlyMap<string, ActiveDefinition>;\n /** The site a definition belongs to: null when shared, undefined when no such definition is active. */\n scopeOf: (id: string) => string | null | undefined;\n locales: readonly LocaleDefinition[];\n defaultLocale: string;\n /** Models whose entries embed this component, directly or through other components. */\n dependentModelIds: (componentId: string) => string[];\n};\n\n/**\n * The whole active schema at one global version: every site's definitions and the shared ones. It has no\n * lookups by API ID on purpose: two sites may each have a `post`, so a key only means something in a view.\n * The registry loads and caches this; requests read a view of it (`forSite`).\n */\nexport type NetworkSchema = SchemaById & {\n /** A site's view: the shared definitions and that site's (memoised per site). */\n forSite: (siteId: string) => SchemaSnapshot;\n /** The shared definitions alone (network routes, the Network pages). */\n shared: () => SchemaSnapshot;\n};\n\n/**\n * One site's view of the schema at one global version (or the shared definitions alone, `siteId` null).\n * Requests pin one for their whole lifetime, so a model never changes shape halfway through a request.\n */\nexport type SchemaSnapshot = SchemaById & {\n /** The site this view is for; null for the shared definitions alone. */\n siteId: string | null;\n /** Collections and singletons by exact API key (the admin content API and GraphQL resolve here). */\n modelsByApiKey: ReadonlyMap<string, ActiveDefinition>;\n /**\n * Collections by plural API ID and singletons by API ID (`routeKeyOf`): the delivery and preview routes\n * resolve `/api/content/:modelKey` here.\n */\n modelsByRouteKey: ReadonlyMap<string, ActiveDefinition>;\n /** Route keys claimed twice; the registry logs them. Empty for any schema the validator accepted. */\n routeKeyCollisions: readonly RouteKeyCollision[];\n componentsByApiKey: ReadonlyMap<string, ActiveDefinition>;\n /** The full set at the same version (planning, validation across sites). */\n network: NetworkSchema;\n};\n\n/**\n * On a collision the model whose API ID is the route key keeps it (a singleton keeps the URL it always\n * had); otherwise the first definition does. Deterministic, so every instance serves the same model.\n */\nconst indexByRouteKey = (definitions: readonly ActiveDefinition[]) => {\n const modelsByRouteKey = new Map<string, ActiveDefinition>();\n const routeKeyCollisions: RouteKeyCollision[] = [];\n for (const active of definitions) {\n if (isComponentDefinition(active.definition)) {\n continue;\n }\n const routeKey = routeKeyOf(active.definition);\n const existing = modelsByRouteKey.get(routeKey);\n if (!existing) {\n modelsByRouteKey.set(routeKey, active);\n continue;\n }\n const takesOver = active.definition.apiKey === routeKey;\n const [served, hidden] = takesOver ? [active, existing] : [existing, active];\n modelsByRouteKey.set(routeKey, served);\n routeKeyCollisions.push({\n routeKey,\n servedApiKey: served.definition.apiKey,\n hiddenApiKey: hidden.definition.apiKey,\n });\n }\n return { modelsByRouteKey, routeKeyCollisions };\n};\n\nconst byIdPart = (\n version: number,\n definitions: readonly ActiveDefinition[],\n locales: readonly LocaleDefinition[],\n): SchemaById => {\n const byId = new Map(definitions.map((active) => [active.definition.id, active]));\n const all = definitions.map((active) => active.definition);\n const dependents = new Map<string, string[]>();\n return {\n version,\n definitions,\n byId,\n scopeOf: (id) => byId.get(id)?.siteId,\n locales,\n defaultLocale: locales.find((locale) => locale.isDefault)?.code ?? 'en',\n dependentModelIds: (componentId: string) => {\n let ids = dependents.get(componentId);\n if (!ids) {\n ids = findDependentModels(all, componentId).map((definition) => definition.id);\n dependents.set(componentId, ids);\n }\n return ids;\n },\n };\n};\n\nconst buildView = (\n network: NetworkSchema,\n siteId: string | null,\n definitions: readonly ActiveDefinition[],\n): SchemaSnapshot => {\n const modelsByApiKey = new Map<string, ActiveDefinition>();\n const componentsByApiKey = new Map<string, ActiveDefinition>();\n for (const active of definitions) {\n (isComponentDefinition(active.definition) ? componentsByApiKey : modelsByApiKey).set(\n active.definition.apiKey,\n active,\n );\n }\n const { modelsByRouteKey, routeKeyCollisions } = indexByRouteKey(definitions);\n return Object.freeze({\n ...byIdPart(network.version, definitions, network.locales),\n siteId,\n modelsByApiKey,\n modelsByRouteKey,\n routeKeyCollisions,\n componentsByApiKey,\n network,\n });\n};\n\n/** The full schema at one version; views are built on first use and kept for the snapshot's lifetime. */\nexport const buildNetworkSchema = (\n version: number,\n definitions: readonly ActiveDefinition[],\n locales: readonly LocaleDefinition[],\n): NetworkSchema => {\n const views = new Map<string | null, SchemaSnapshot>();\n const viewOf = (siteId: string | null): SchemaSnapshot => {\n let view = views.get(siteId);\n if (!view) {\n view = buildView(\n network,\n siteId,\n definitions.filter((active) => active.siteId === null || active.siteId === siteId),\n );\n views.set(siteId, view);\n }\n return view;\n };\n const network: NetworkSchema = Object.freeze({\n ...byIdPart(version, definitions, locales),\n forSite: (siteId: string) => viewOf(siteId),\n shared: () => viewOf(null),\n });\n return network;\n};\n\n/**\n * A view built straight from definitions (tests, generated references, proposed schemas): the view of\n * `siteId` (the shared definitions alone when null) over a full set made of `definitions`.\n */\nexport const buildSnapshot = (\n version: number,\n definitions: readonly ActiveDefinition[],\n locales: readonly LocaleDefinition[],\n siteId: string | null = null,\n): SchemaSnapshot => {\n const network = buildNetworkSchema(version, definitions, locales);\n return siteId === null ? network.shared() : network.forSite(siteId);\n};\n\n/** The sites whose views hold a definition of `definitions` (null: the shared definitions). */\nexport const siteIdsOf = (definitions: readonly ActiveDefinition[]): string[] => [\n ...new Set(definitions.flatMap((active) => (active.siteId === null ? [] : [active.siteId]))),\n];\n","import { hashDefinition, withDerivedPlural, type SchemaDefinition } from '@shapio/schema';\n\n/**\n * Reads a definition stored on a `schema_revisions` row. Revisions are immutable, so a collection saved\n * before plural API IDs existed has no `pluralApiKey`; it gets the value normalizing would fill\n * (`@shapio/schema` `withDerivedPlural`), so the snapshot, pull files and GraphQL agree before anyone saves.\n * Every read of a stored definition goes through here.\n */\nexport const readStoredDefinition = (stored: unknown): SchemaDefinition =>\n withDerivedPlural(stored as SchemaDefinition).definition;\n\nexport type StoredDefinition = { definition: SchemaDefinition; hash: string };\n\n/**\n * A stored definition with a hash that matches it: the stored hash, or, when a plural API ID was filled,\n * the hash of the filled definition (the hash a pull of it records, so pull → apply shows no change).\n */\nexport const readStoredRevision = async (stored: unknown, hash: string): Promise<StoredDefinition> => {\n const { definition, filled } = withDerivedPlural(stored as SchemaDefinition);\n return { definition, hash: filled ? await hashDefinition(definition) : hash };\n};\n","import type { LocaleDefinition } from '@shapio/schema';\nimport type { Kysely, Transaction } from 'kysely';\nimport type { Database } from '../db/index.js';\nimport type { DB } from '../db/types.js';\nimport * as localesRepository from '../repositories/locales.js';\nimport type { LocaleRow } from '../repositories/locales.js';\nimport * as schemaModelsRepository from '../repositories/schemaModels.js';\nimport * as schemaVersionsRepository from '../repositories/schemaVersions.js';\nimport { buildNetworkSchema, type ActiveDefinition, type NetworkSchema } from './snapshot.js';\nimport { readStoredRevision } from './storedDefinition.js';\n\nexport const toLocaleDefinition = (row: LocaleRow): LocaleDefinition => ({\n code: row.code,\n label: row.label,\n isDefault: row.is_default,\n fallbacks: row.fallbacks,\n});\n\nexport const toActiveDefinition = async (\n row: schemaModelsRepository.ActiveDefinitionRow,\n): Promise<ActiveDefinition> => ({\n ...(await readStoredRevision(row.definition, row.hash)),\n siteId: row.siteId,\n version: row.version,\n revisionId: row.revisionId,\n activatedAt: row.activatedAt,\n});\n\nexport const toActiveDefinitions = (\n rows: readonly schemaModelsRepository.ActiveDefinitionRow[],\n): Promise<ActiveDefinition[]> => Promise.all(rows.map(toActiveDefinition));\n\n/** The schema as the executor sees it (inside an activation transaction: the state right after the flip). */\nexport const readSnapshot = async (executor: Kysely<DB> | Transaction<DB>): Promise<NetworkSchema> => {\n const version = await schemaVersionsRepository.getSchemaVersion(executor);\n const rows = await schemaModelsRepository.findActiveDefinitions(executor);\n const locales = await localesRepository.list(executor);\n return buildNetworkSchema(version, await toActiveDefinitions(rows), locales.map(toLocaleDefinition));\n};\n\n/**\n * Reads the global version, every active definition and the locales in one REPEATABLE READ transaction,\n * so the snapshot is exactly the state at that version even if an activation commits meanwhile.\n */\nexport const loadSnapshot = (db: Database): Promise<NetworkSchema> =>\n db\n .transaction()\n .setIsolationLevel('repeatable read')\n .setAccessMode('read only')\n .execute((trx) => readSnapshot(trx));\n","import type { FastifyBaseLogger } from 'fastify';\nimport type { Database } from '../db/index.js';\nimport * as schemaVersionsRepository from '../repositories/schemaVersions.js';\nimport { loadSnapshot, readSnapshot } from './loadSnapshot.js';\nimport { siteIdsOf, type NetworkSchema } from './snapshot.js';\n\nexport type SchemaChangeListener = (snapshot: NetworkSchema) => void;\n\nexport type SchemaRegistry = {\n /**\n * The snapshot for the current global schema version. Costs one primary-key read of\n * `system_versions` when the cache is current; reloads (single-flight) when it is not. This durable\n * check is what keeps every instance correct; notifications only make reloads happen sooner.\n * `executor`: the caller's open transaction, which the check (and a reload, when the cache is stale) then\n * reads through, so it never waits for a second pooled connection while holding one.\n * `versions.schemaVersion`: the version the request already read (`repositories/requestState.ts`); the\n * check compares against it instead of reading it again, and still reloads when the cache is older.\n */\n getSnapshot: (executor?: Database, versions?: { schemaVersion?: number }) => Promise<NetworkSchema>;\n /** The cached snapshot without any check (may be stale or absent). */\n peek: () => NetworkSchema | undefined;\n /** A hint that the version moved (NOTIFY). Reloads in the background; never throws. */\n hint: (version?: number) => void;\n /** Called with each newer snapshot once loaded (GraphQL/OpenAPI regeneration, admin live refresh). */\n onChange: (listener: SchemaChangeListener) => () => void;\n /**\n * Ignores later hints and waits for a background reload still in flight. Call on shutdown before the\n * database is closed: a reload that is still acquiring a pooled connection when the pool ends never\n * returns it, and the pool's `end()` then waits forever.\n */\n close: () => Promise<void>;\n};\n\ntype RegistryOptions = { db: Database; log: FastifyBaseLogger };\n\nexport const createSchemaRegistry = ({ db, log }: RegistryOptions): SchemaRegistry => {\n let cached: NetworkSchema | undefined;\n let loading: Promise<NetworkSchema> | undefined;\n let closed = false;\n /** Reloads started by `hint`, which nobody else awaits. */\n const background = new Set<Promise<void>>();\n const listeners = new Set<SchemaChangeListener>();\n\n const publish = (snapshot: NetworkSchema) => {\n if (cached && cached.version >= snapshot.version) {\n return cached;\n }\n cached = snapshot;\n // Route keys only collide within a view (two sites may each serve `/posts`).\n const views = [snapshot.shared(), ...siteIdsOf(snapshot.definitions).map((id) => snapshot.forSite(id))];\n for (const view of views) {\n for (const collision of view.routeKeyCollisions) {\n log.error(\n { ...collision, siteId: view.siteId, schemaVersion: snapshot.version },\n 'two models share a delivery route key; set a plural API ID on the hidden one',\n );\n }\n }\n for (const listener of listeners) {\n try {\n listener(snapshot);\n } catch (error) {\n log.error({ err: error, schemaVersion: snapshot.version }, 'schema change listener failed');\n }\n }\n return snapshot;\n };\n\n /** Single-flight: concurrent callers share one load; a load that turns out too old is repeated. */\n const loadAtLeast = async (version: number): Promise<NetworkSchema> => {\n for (;;) {\n loading ??= loadSnapshot(db).finally(() => {\n loading = undefined;\n });\n const snapshot = publish(await loading);\n if (snapshot.version >= version) {\n return snapshot;\n }\n }\n };\n\n return {\n getSnapshot: async (executor, versions) => {\n const version =\n versions?.schemaVersion ?? (await schemaVersionsRepository.getSchemaVersion(executor ?? db));\n if (cached && cached.version >= version) {\n return cached;\n }\n // Through the caller's transaction: this caller's own read, not cached, since a transaction that is\n // not REPEATABLE READ can see an activation commit between its reads. The pool path refreshes the cache.\n return executor ? readSnapshot(executor) : loadAtLeast(version);\n },\n peek: () => cached,\n hint: (version) => {\n if (closed || (version !== undefined && cached && cached.version >= version)) {\n return;\n }\n const reload = loadAtLeast(version ?? 0).then(\n () => undefined,\n (error: unknown) => {\n log.warn({ err: error }, 'schema snapshot refresh failed; the next request will retry');\n },\n );\n background.add(reload);\n void reload.finally(() => background.delete(reload));\n },\n onChange: (listener) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n close: async () => {\n closed = true;\n // Each reload settles without throwing (failures are logged above).\n await Promise.all(background);\n },\n };\n};\n","import { Type, type Static } from 'typebox';\nimport { Value } from 'typebox/value';\nimport type { StorageConfig } from '../config/index.js';\nimport { DELIVERY_DESCRIPTOR_SETTING } from '../constants/delivery.js';\nimport type { Database } from '../db/index.js';\nimport * as schemaVersionsRepository from '../repositories/schemaVersions.js';\nimport * as systemSettingsRepository from '../repositories/systemSettings.js';\n\n/**\n * What an in-process delivery reader (plan next-in-process §2) needs from the server's configuration to answer\n * exactly as the server does, without its environment: where Shapio is served (absolute media URLs) and where\n * media lives. Never a secret: S3 credentials and the signing secret are the reader's own options.\n */\nconst DeliveryDescriptorSchema = Type.Object({\n format: Type.Literal(1),\n publicUrl: Type.String(),\n basePath: Type.String(),\n media: Type.Object({\n publicBaseUrl: Type.Union([Type.String(), Type.Null()]),\n s3: Type.Union([\n Type.Object({\n bucket: Type.String(),\n region: Type.Union([Type.String(), Type.Null()]),\n endpoint: Type.Union([Type.String(), Type.Null()]),\n forcePathStyle: Type.Boolean(),\n }),\n Type.Null(),\n ]),\n }),\n});\n\nexport type DeliveryDescriptor = Static<typeof DeliveryDescriptorSchema>;\n\ntype DescriptorSource = {\n server: { publicUrl: string; basePath: string };\n storage: Pick<StorageConfig, 'publicBaseUrl' | 's3'>;\n};\n\nexport const toDeliveryDescriptor = ({ server, storage }: DescriptorSource): DeliveryDescriptor => ({\n format: 1,\n publicUrl: server.publicUrl,\n basePath: server.basePath,\n media: {\n publicBaseUrl: storage.publicBaseUrl ?? null,\n s3: storage.s3\n ? {\n bucket: storage.s3.bucket,\n region: storage.s3.region ?? null,\n endpoint: storage.s3.endpoint ?? null,\n forcePathStyle: storage.s3.forcePathStyle,\n }\n : null,\n },\n});\n\nexport class InvalidDeliveryDescriptorError extends Error {\n constructor(options?: ErrorOptions) {\n super('The delivery descriptor stored by the Shapio server is not readable by this release', options);\n this.name = 'InvalidDeliveryDescriptorError';\n }\n}\n\n/** Parses the stored descriptor; throws when it is not in a format this release reads. */\nexport const parseDeliveryDescriptor = (stored: string): DeliveryDescriptor => {\n let value: unknown;\n try {\n value = JSON.parse(stored);\n } catch (error) {\n throw new InvalidDeliveryDescriptorError({ cause: error });\n }\n if (!Value.Check(DeliveryDescriptorSchema, value)) {\n throw new InvalidDeliveryDescriptorError();\n }\n return value;\n};\n\n/**\n * Called by the server at every start, after migrating: records its release and its delivery descriptor\n * (rewritten only when it changed), in one transaction.\n */\nexport const publishServerRelease = async (\n database: Database,\n release: string,\n source: DescriptorSource,\n): Promise<void> => {\n const descriptor = JSON.stringify(toDeliveryDescriptor(source));\n await database.transaction().execute(async (trx) => {\n await schemaVersionsRepository.setRelease(release, trx);\n if ((await systemSettingsRepository.findValue(DELIVERY_DESCRIPTOR_SETTING, trx)) !== descriptor) {\n await systemSettingsRepository.upsertValue(DELIVERY_DESCRIPTOR_SETTING, descriptor, trx);\n }\n });\n};\n","import type { FastifyBaseLogger } from 'fastify';\nimport type { Kysely } from 'kysely';\nimport type { DB } from '../db/types.js';\nimport { generateToken } from '../helpers/tokens.js';\nimport * as systemSettingsRepository from '../repositories/systemSettings.js';\n\nexport const SIGNING_SECRET_SETTING = 'signing_secret';\n\n/**\n * The instance's HMAC signing secret (preview tokens, webhook signatures). SESSION_SECRET always wins.\n * Without it, the secret is generated once and stored in `system_settings`, so it survives restarts and\n * every instance on the database agrees: concurrent first boots race on one insert and all re-read the\n * winner. The value is never logged.\n */\nexport const resolveSigningSecret = async (\n database: Kysely<DB>,\n configured: string | undefined,\n log: Pick<FastifyBaseLogger, 'info'>,\n): Promise<string> => {\n if (configured) {\n return configured;\n }\n const stored = await systemSettingsRepository.findValue(SIGNING_SECRET_SETTING, database);\n if (stored) {\n return stored;\n }\n const created = await systemSettingsRepository.insertIfAbsent(\n SIGNING_SECRET_SETTING,\n generateToken(),\n database,\n );\n const value = await systemSettingsRepository.findValue(SIGNING_SECRET_SETTING, database);\n if (!value) {\n throw new Error('The signing secret could not be stored');\n }\n if (created) {\n log.info('Generated signing secret; set SESSION_SECRET to pin it');\n }\n return value;\n};\n","import { resolve } from 'node:path';\nimport type { FastifyBaseLogger } from 'fastify';\nimport { pino, type Logger } from 'pino';\nimport type { StorageConfig } from '../config/index.js';\nimport { describeDatabaseTarget, dialectOfUrl } from '../db/dialect.js';\nimport { clearDb, createDb, hasDb, setDb, type Database } from '../db/index.js';\nimport { createUrlBuilder } from '../helpers/publicUrl.js';\nimport { createMediaStorage } from '../media/storage.js';\nimport { createPermissionCache } from '../permissions/cache.js';\nimport { createPermissionEvaluator } from '../permissions/evaluator.js';\nimport type { PermissionEvaluator, TokenPrincipal } from '../permissions/types.js';\nimport * as systemSettingsRepository from '../repositories/systemSettings.js';\nimport { createSchemaFieldVisibility } from '../schema/fieldVisibility.js';\nimport { createSchemaRegistry, type SchemaRegistry } from '../schema/registry.js';\nimport type { ContentServiceContext } from '../services/contentAccess.js';\nimport { parseDeliveryDescriptor, type DeliveryDescriptor } from '../services/deliveryDescriptor.js';\nimport { SIGNING_SECRET_SETTING } from '../services/signingSecret.js';\nimport { DeliveryRuntimeError } from './errors.js';\n\n/**\n * In-process delivery (plan next-in-process §2): the delivery API's reads as function calls in the reading\n * process (a Next.js server, an SSR site, a script), with the server's permissions, shapes and schema. Only\n * the database URL is required: the server records its release and a delivery descriptor (public URL, media\n * locations) at every start, and every call reads both with its request state.\n */\nexport type DeliveryRuntimeOptions = {\n /** The Shapio database: a `postgres://` or `mysql://` URL. SQLite is single-process and refused. */\n databaseUrl: string;\n /** Pooled connections (default 4). Serverless functions: 1 or 2. `next build` opens one pool per worker. */\n poolMax?: number;\n /** How long a read waits for a pooled connection before it fails with 503 (default 10 s, as the server). */\n acquireTimeoutMs?: number;\n /**\n * Signs URLs of private media stored on the server's disk. Default: the secret the server generated and\n * stored; pass the server's SESSION_SECRET when it sets one.\n */\n signingSecret?: string;\n /** Presigns URLs of private media on S3. Default: the AWS SDK's own credential chain (environment, profile). */\n s3Credentials?: { accessKeyId: string; secretAccessKey: string };\n /**\n * When the server's release differs from this one: `throw` (default) a `ShapioVersionSkewError`, or `http`:\n * read through the HTTP API at `fallbackUrl` until the releases match again.\n */\n onVersionSkew?: 'throw' | 'http';\n /** The Shapio server's URL (with BASE_PATH) for `onVersionSkew: 'http'`. */\n fallbackUrl?: string;\n /** Where failures are logged (pino). Default: warnings and errors to stdout. */\n logger?: Logger;\n};\n\n/** Media dependencies for one stored descriptor (rebuilt only when the server's descriptor changes). */\ntype MediaDependencies = NonNullable<ContentServiceContext['media']>;\n\nexport type DeliveryRuntime = {\n readonly databaseUrl: string;\n readonly db: Database;\n readonly registry: SchemaRegistry;\n /** The unscoped evaluator; each call scopes it to its principal and versions (`permissions/scoped.ts`). */\n readonly permissions: PermissionEvaluator;\n readonly log: Logger;\n readonly onVersionSkew: 'throw' | 'http';\n readonly fallbackUrl: string | undefined;\n /** Resolved delivery tokens by token hash; each call still checks the token is live. */\n readonly tokens: Map<string, TokenPrincipal>;\n /** Media storage and URLs for the stored descriptor text. */\n mediaFor: (descriptor: string) => Promise<MediaDependencies>;\n /**\n * Registers one more user of this shared runtime (a local client); the returned function releases it, and\n * the last release closes the runtime.\n */\n retain: () => () => Promise<void>;\n /** Closes the pool now, whoever else uses it. The runtime cannot be used afterwards; a new one may be created. */\n close: () => Promise<void>;\n};\n\n/** One runtime per database per process, kept on `globalThis` so a re-evaluated module (dev reloads, a\n * bundler's second copy) never opens a second pool. */\nconst RUNTIMES = Symbol.for('shapio.delivery');\n\nconst runtimes = (): Map<string, DeliveryRuntime> => {\n const holder = globalThis as typeof globalThis & { [RUNTIMES]?: Map<string, DeliveryRuntime> };\n holder[RUNTIMES] ??= new Map();\n return holder[RUNTIMES];\n};\n\nconst DEFAULT_POOL_MAX = 4;\n\n/** Local media is never read from disk here; the adapter only builds URLs. Any file access fails loudly. */\nconst NO_LOCAL_MEDIA_ROOT = resolve('/', '.shapio-in-process-has-no-media');\n\nconst storageConfigOf = (\n descriptor: DeliveryDescriptor,\n credentials: DeliveryRuntimeOptions['s3Credentials'],\n): StorageConfig => {\n const { s3 } = descriptor.media;\n return {\n driver: 'local',\n mediaPath: NO_LOCAL_MEDIA_ROOT,\n s3: s3\n ? {\n bucket: s3.bucket,\n region: s3.region ?? undefined,\n endpoint: s3.endpoint ?? undefined,\n forcePathStyle: s3.forcePathStyle,\n accessKeyId: credentials?.accessKeyId,\n secretAccessKey: credentials?.secretAccessKey,\n }\n : undefined,\n publicBaseUrl: descriptor.media.publicBaseUrl ?? undefined,\n maxUploadBytes: 0,\n allowedTypes: [],\n };\n};\n\nconst assertSupported = (options: DeliveryRuntimeOptions) => {\n const dialect = dialectOfUrl(options.databaseUrl);\n if (dialect === 'sqlite') {\n throw new DeliveryRuntimeError(\n 'In-process delivery needs PostgreSQL or MySQL: a SQLite database is served by one process only. ' +\n \"Read over HTTP with @shapio/client's createClient instead\",\n );\n }\n if (dialect === undefined) {\n throw new DeliveryRuntimeError('databaseUrl must be a postgres:// or mysql:// URL');\n }\n if (options.onVersionSkew === 'http' && !options.fallbackUrl) {\n throw new DeliveryRuntimeError(\"onVersionSkew: 'http' needs fallbackUrl, the Shapio server's URL\");\n }\n};\n\nconst openRuntime = (options: DeliveryRuntimeOptions): DeliveryRuntime => {\n const log = options.logger ?? pino({ level: 'warn' });\n const db = createDb({\n connectionString: options.databaseUrl,\n poolMax: options.poolMax ?? DEFAULT_POOL_MAX,\n ...(options.acquireTimeoutMs !== undefined ? { acquireTimeoutMs: options.acquireTimeoutMs } : {}),\n applicationName: 'shapio-in-process',\n onIdleConnectionError: (error) =>\n log.warn({ err: error }, 'database connection lost; it will be replaced on the next query'),\n });\n // Repositories default to the process handle. A Shapio server in this process keeps its own (same\n // database); otherwise this pool becomes it.\n const ownsProcessHandle = !hasDb();\n if (ownsProcessHandle) {\n setDb(db);\n }\n const registryLog = log.child({ component: 'schema-registry' }) as unknown as FastifyBaseLogger;\n const registry = createSchemaRegistry({ db, log: registryLog });\n const permissions = createPermissionEvaluator({\n grants: createPermissionCache(db),\n fields: createSchemaFieldVisibility(registry),\n });\n\n let media: { descriptor: string; dependencies: Promise<MediaDependencies> } | undefined;\n let users = 0;\n let closing: Promise<void> | undefined;\n const buildMedia = async (stored: string): Promise<MediaDependencies> => {\n const descriptor = parseDeliveryDescriptor(stored);\n const urls = createUrlBuilder(descriptor);\n const signingSecret =\n options.signingSecret ?? (await systemSettingsRepository.findValue(SIGNING_SECRET_SETTING, db));\n const storage = await createMediaStorage(storageConfigOf(descriptor, options.s3Credentials), {\n urls,\n ...(signingSecret !== undefined ? { signingSecret } : {}),\n });\n return { storage, urls };\n };\n\n const runtime: DeliveryRuntime = {\n databaseUrl: options.databaseUrl,\n db,\n registry,\n permissions,\n log,\n onVersionSkew: options.onVersionSkew ?? 'throw',\n fallbackUrl: options.fallbackUrl,\n tokens: new Map(),\n mediaFor: (descriptor) => {\n if (media?.descriptor !== descriptor) {\n const dependencies = buildMedia(descriptor);\n media = { descriptor, dependencies };\n // A failed build is retried by the next call instead of being cached.\n dependencies.catch(() => {\n if (media?.dependencies === dependencies) {\n media = undefined;\n }\n });\n }\n return media.dependencies;\n },\n retain: () => {\n users += 1;\n let released = false;\n return async () => {\n if (released) {\n return;\n }\n released = true;\n users -= 1;\n if (users === 0) {\n await runtime.close();\n }\n };\n },\n close: () => {\n closing ??= (async () => {\n if (runtimes().get(options.databaseUrl) === runtime) {\n runtimes().delete(options.databaseUrl);\n }\n await registry.close();\n await db.destroy();\n if (ownsProcessHandle) {\n clearDb(db);\n }\n })();\n return closing;\n },\n };\n return runtime;\n};\n\n/**\n * The in-process delivery runtime for a database: created once per process and shared (the same URL returns\n * the same runtime; the first caller's options apply). A process reads one Shapio database: another URL is\n * refused. Nothing is read until the first call, so creating it never fails on a database that is down.\n */\nexport const createDeliveryRuntime = (options: DeliveryRuntimeOptions): DeliveryRuntime => {\n assertSupported(options);\n const all = runtimes();\n const existing = all.get(options.databaseUrl);\n if (existing) {\n return existing;\n }\n const [other] = all.keys();\n if (other !== undefined) {\n throw new DeliveryRuntimeError(\n `This process already reads ${describeDatabaseTarget(other)} in process; one Shapio database per process`,\n );\n }\n const created = openRuntime(options);\n all.set(options.databaseUrl, created);\n return created;\n};\n","import { createDeliveryClient, type ShapioDeliveryClient } from '@shapio/client';\nimport * as delivery from '@shapio/cms/delivery';\nimport { assertNodeRuntime } from './nodeRuntime.js';\nimport type {\n DeliveryRuntimeError as DeliveryRuntimeErrorInstance,\n LocalClientOptions,\n ShapioVersionSkewError as ShapioVersionSkewErrorInstance,\n} from './types.js';\n\nexport type { LocalClientOptions } from './types.js';\n\n/** The Shapio release this package reads; the server must run the same one. */\nexport const SHAPIO_VERSION: string = delivery.SHAPIO_VERSION;\n\n/** Thrown (or 503 `VERSION_SKEW` caught as `ShapioApiError`) when the server runs another release. */\nexport const ShapioVersionSkewError: {\n new (serverRelease: string | null, libraryRelease: string): ShapioVersionSkewErrorInstance;\n readonly prototype: ShapioVersionSkewErrorInstance;\n} = delivery.ShapioVersionSkewError;\nexport type ShapioVersionSkewError = ShapioVersionSkewErrorInstance;\n\n/** Thrown by `createLocalClient` for options it cannot serve (SQLite, an unknown URL, a second database). */\nexport const DeliveryRuntimeError: {\n new (message: string): DeliveryRuntimeErrorInstance;\n readonly prototype: DeliveryRuntimeErrorInstance;\n} = delivery.DeliveryRuntimeError;\nexport type DeliveryRuntimeError = DeliveryRuntimeErrorInstance;\n\nexport type LocalClient = ShapioDeliveryClient & {\n /**\n * Stops this client. The database pool is shared by every local client of the process and closes when the\n * last one is closed. Long-running servers never need to call it; scripts and tests do.\n */\n close: () => Promise<void>;\n};\n\n/**\n * Shapio's delivery API as function calls in this process: the `delivery`, `site` and `snapshots` groups of\n * `@shapio/client`, with the same paths, permissions, response shapes and `ShapioApiError`s, answered from the\n * database without an HTTP hop. Reads as `token` (a delivery API token) or, without one, as an anonymous\n * caller (the site's `public` role). Model changes made in the admin show on the next call. `drafts: true`\n * reads drafts instead (drafts mode), when the token's role grants Read drafts.\n *\n * Needs PostgreSQL or MySQL, the Node.js runtime, and the same Shapio release as the server (`SHAPIO_VERSION`);\n * see `onVersionSkew`. Nothing is read until the first call.\n */\nexport const createLocalClient = ({\n token,\n site,\n drafts,\n ...runtimeOptions\n}: LocalClientOptions): LocalClient => {\n assertNodeRuntime();\n const runtime = delivery.createDeliveryRuntime(runtimeOptions);\n const release = runtime.retain();\n const request = delivery.createInProcessRequest(runtime, {\n ...(token !== undefined ? { token } : {}),\n ...(site !== undefined ? { site } : {}),\n });\n return {\n ...createDeliveryClient(request, {\n ...(site !== undefined ? { site } : {}),\n ...(drafts !== undefined ? { drafts } : {}),\n }),\n close: release,\n };\n};\n"],"mappings":"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"}
|