@phtngyn/cms 0.1.0-beta.1 → 0.1.0-beta.3

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.
Files changed (143) hide show
  1. package/README.md +212 -12
  2. package/bin/cms.mjs +2 -0
  3. package/package.json +30 -15
  4. package/src/{Configuration-DyKo0sLS.js → Configuration-CDMYfTre.js} +26 -147
  5. package/src/Configuration-CDMYfTre.js.map +1 -0
  6. package/src/Configuration.d.ts +24 -8
  7. package/src/Configuration.js +0 -1
  8. package/src/Configuration.js.map +1 -1
  9. package/src/ContentTransfer-C3Av9sWz.js +53 -0
  10. package/src/ContentTransfer-C3Av9sWz.js.map +1 -0
  11. package/src/{DocumentEngine-DYObwMDY.js → DocumentEngine-J7g-emdf.js} +75 -27
  12. package/src/DocumentEngine-J7g-emdf.js.map +1 -0
  13. package/src/DocumentModel.d.ts +8 -9
  14. package/src/DocumentModel.js +13 -3
  15. package/src/DocumentModel.js.map +1 -1
  16. package/src/Forms-BsJMev5_.d.ts +74 -0
  17. package/src/Forms-CfcI3tPU.js +79 -0
  18. package/src/Forms-CfcI3tPU.js.map +1 -0
  19. package/src/Forms-Dj6NuW66.js +367 -0
  20. package/src/Forms-Dj6NuW66.js.map +1 -0
  21. package/src/Forms.d.ts +51 -0
  22. package/src/Forms.js +13 -0
  23. package/src/Forms.js.map +1 -0
  24. package/src/{HttpPaths-BbS21Jy8.js → HttpPaths-c9nLZ9H2.js} +6 -2
  25. package/src/HttpPaths-c9nLZ9H2.js.map +1 -0
  26. package/src/I18n.d.ts +20 -0
  27. package/src/I18n.js +1 -0
  28. package/src/Module.js +59 -14
  29. package/src/Module.js.map +1 -1
  30. package/src/S3MediaObjects-CpPSzJto.js +2777 -0
  31. package/src/S3MediaObjects-CpPSzJto.js.map +1 -0
  32. package/src/Seo-402YumpT.d.ts +198 -0
  33. package/src/cli/content.js +258 -0
  34. package/src/cli/content.js.map +1 -0
  35. package/src/cli.js +71 -0
  36. package/src/cli.js.map +1 -0
  37. package/src/{connection-CBTY-TzB.js → connection-CBAy2qGy.js} +5 -7
  38. package/src/connection-CBAy2qGy.js.map +1 -0
  39. package/src/{installation-DeuBiFRE.js → installation-CNfIriVm.js} +1007 -2246
  40. package/src/installation-CNfIriVm.js.map +1 -0
  41. package/src/internal/authoring-catalog.js +10 -4
  42. package/src/internal/authoring-catalog.js.map +1 -1
  43. package/src/internal/authoring-compiler.js +0 -1
  44. package/src/internal/authoring-compiler.js.map +1 -1
  45. package/src/internal/authoring-metadata.js +4 -4
  46. package/src/internal/authoring-metadata.js.map +1 -1
  47. package/src/internal/cms-config.js +0 -1
  48. package/src/internal/cms-config.js.map +1 -1
  49. package/src/internal/configuration.js +26 -17
  50. package/src/internal/configuration.js.map +1 -1
  51. package/src/internal/document-model.js +69 -21
  52. package/src/internal/document-model.js.map +1 -1
  53. package/src/internal/form-capabilities.js +12 -0
  54. package/src/internal/form-capabilities.js.map +1 -0
  55. package/src/internal/i18n.js +47 -0
  56. package/src/internal/i18n.js.map +1 -0
  57. package/src/internal/studio-app.js +2 -2
  58. package/src/internal/studio-app.js.map +1 -1
  59. package/src/{location-DtYv2aes.js → location-BTqdfIzX.js} +2 -3
  60. package/src/location-BTqdfIzX.js.map +1 -0
  61. package/src/{metadata-DE1Mc2td.js → metadata-BEZPWh-3.js} +3 -72
  62. package/src/metadata-BEZPWh-3.js.map +1 -0
  63. package/src/{models-C3witQBK.js → models-B4IoTmp7.js} +3 -2
  64. package/src/models-B4IoTmp7.js.map +1 -0
  65. package/src/{models-BkMgeaOL.js → models-Bah5B3bT.js} +205 -51
  66. package/src/models-Bah5B3bT.js.map +1 -0
  67. package/src/runtime/blocks.js +22 -9
  68. package/src/runtime/blocks.js.map +1 -1
  69. package/src/runtime/collaboration.js.map +1 -1
  70. package/src/runtime/delivery/DocumentRenderer.js +7 -4
  71. package/src/runtime/delivery/DocumentRenderer.js.map +1 -1
  72. package/src/runtime/delivery/blocks.js +3 -1
  73. package/src/runtime/delivery/blocks.js.map +1 -1
  74. package/src/runtime/delivery/context.js +1 -1
  75. package/src/runtime/delivery/document-page.js +12 -12
  76. package/src/runtime/delivery/document-page.js.map +1 -1
  77. package/src/runtime/delivery/document.js +4 -2
  78. package/src/runtime/delivery/document.js.map +1 -1
  79. package/src/runtime/delivery/error.js.map +1 -1
  80. package/src/runtime/delivery/plugin.js +28 -2
  81. package/src/runtime/delivery/plugin.js.map +1 -1
  82. package/src/runtime/delivery/types.d.ts +5 -0
  83. package/src/runtime/delivery/useCmsDocumentSeo.d.ts +59 -0
  84. package/src/runtime/delivery/useCmsDocumentSeo.js +101 -0
  85. package/src/runtime/delivery/useCmsDocumentSeo.js.map +1 -0
  86. package/src/runtime/delivery/useDocument.js +2 -2
  87. package/src/runtime/delivery/useDocument.js.map +1 -1
  88. package/src/runtime/delivery/useDocumentQuery.js +2 -2
  89. package/src/runtime/delivery/useDocumentQuery.js.map +1 -1
  90. package/src/runtime/forms/document.js +38 -0
  91. package/src/runtime/forms/document.js.map +1 -0
  92. package/src/runtime/forms/useCmsForm.js +190 -0
  93. package/src/runtime/forms/useCmsForm.js.map +1 -0
  94. package/src/runtime/i18n/translator.js +51 -0
  95. package/src/runtime/i18n/translator.js.map +1 -0
  96. package/src/runtime/i18n/useCmsI18n.d.ts +11 -0
  97. package/src/runtime/i18n/useCmsI18n.js +37 -0
  98. package/src/runtime/i18n/useCmsI18n.js.map +1 -0
  99. package/src/runtime/installation.js +1 -1
  100. package/src/runtime/plugin.js +3 -3
  101. package/src/runtime/plugin.js.map +1 -1
  102. package/src/runtime/preview/comark-document.js +0 -1
  103. package/src/runtime/preview/comark-document.js.map +1 -1
  104. package/src/runtime/preview/connection.js +1 -1
  105. package/src/runtime/preview/controller.js +10 -3
  106. package/src/runtime/preview/controller.js.map +1 -1
  107. package/src/runtime/preview/overlay.js.map +1 -1
  108. package/src/runtime/preview/plugin.client.js +59 -48
  109. package/src/runtime/preview/plugin.client.js.map +1 -1
  110. package/src/runtime/preview/protocol.js.map +1 -1
  111. package/src/runtime/preview/store.js +1 -1
  112. package/src/runtime/preview/targets.js +0 -1
  113. package/src/runtime/preview/targets.js.map +1 -1
  114. package/src/runtime/sitemap-entries.js +17 -0
  115. package/src/runtime/sitemap-entries.js.map +1 -0
  116. package/src/runtime/sitemap.js +24 -0
  117. package/src/runtime/sitemap.js.map +1 -0
  118. package/src/runtime/studio-app.js.map +1 -1
  119. package/src/runtime/studio-entry.js +2 -2
  120. package/src/runtime/studio-entry.js.map +1 -1
  121. package/src/runtime/studio-navigation.js +1 -1
  122. package/src/scalars-B2lgGUnj.js +26 -0
  123. package/src/scalars-B2lgGUnj.js.map +1 -0
  124. package/src/{store-CfFFx9Ua.js → store-b2CSXgMt.js} +12 -7
  125. package/src/store-b2CSXgMt.js.map +1 -0
  126. package/studio/{CreateDocumentDialog--L9lTfx6.js → CreateDocumentDialog-GGey1uBc.js} +44 -44
  127. package/studio/{collaboration-DUuu678R.js → collaboration-DcK2F6v9.js} +7 -6
  128. package/studio/{documentPlanning-BXX04Woe.js → documentPlanning-DRErYmBU.js} +7340 -6469
  129. package/studio/{editor-PexnXkJY.js → editor-Bzlh3zXt.js} +7965 -7955
  130. package/studio/host.css +1 -1
  131. package/studio/host.js +10416 -7981
  132. package/src/Configuration-DyKo0sLS.js.map +0 -1
  133. package/src/DocumentEngine-DYObwMDY.js.map +0 -1
  134. package/src/HttpPaths-BbS21Jy8.js.map +0 -1
  135. package/src/StudioHttp-jR2-bc01.js +0 -718
  136. package/src/StudioHttp-jR2-bc01.js.map +0 -1
  137. package/src/connection-CBTY-TzB.js.map +0 -1
  138. package/src/installation-DeuBiFRE.js.map +0 -1
  139. package/src/location-DtYv2aes.js.map +0 -1
  140. package/src/metadata-DE1Mc2td.js.map +0 -1
  141. package/src/models-BkMgeaOL.js.map +0 -1
  142. package/src/models-C3witQBK.js.map +0 -1
  143. package/src/store-CfFFx9Ua.js.map +0 -1
@@ -1,31 +1,31 @@
1
- import { c as STUDIO_API_PATH, d as STUDIO_COLLABORATION_PATH, i as CMS_LIVENESS_PATH, s as CMS_READINESS_PATH, t as CMS_AUTHENTICATION_PATH } from "./HttpPaths-BbS21Jy8.js";
2
- import { $ as MediaName, A as RemoveDocumentInput, B as StudioActor, C as MarkdownNode, Ct as Slug, D as PublishedDocumentPage, F as AccessDenied, G as MediaByteSize, H as StudioRequestContext, I as AuthenticationRequired, J as MediaCursor, K as MediaCleanupReport, L as DOCUMENT_ACTIONS, M as UndeleteDocumentInput, N as UnpublishDocumentInput, P as UpdateDocumentInput, Q as MediaInvalid, R as MEDIA_ACTIONS, S as DuplicateDocumentInput, T as PublishDocumentInput, U as ImageMimeType, V as StudioEntryDenied, W as MediaAsset, X as MediaId, Y as MediaFolder, Z as MediaIntegrityReport, _ as DocumentSource, _t as DocumentVersion, a as DOCUMENT_REVISION_ACTIONS, at as MediaReferenced, b as DocumentSurround, bt as RelativeDocumentPath, c as DocumentImportInput, ct as MediaUpload, et as MediaNotFound, f as DocumentMutationResult, g as DocumentRevisionAction, gt as DocumentId, h as DocumentQueryScalar, ht as DocumentHistoryCursor, it as MediaReference, j as RestoreDocumentInput, l as DocumentImportReport, mt as DocumentGeneration, n as CmsMarkdownDocument, nt as MediaPage, o as DeletedDocument, ot as MediaStatus, p as DocumentNavigationItem, pt as RequestId, q as MediaConflict, st as MediaTooLarge, tt as MediaObjectKey, vt as EditorNodeId, w as MoveDocumentInput, wt as SourcePath, x as DocumentValidationResult, xt as RevisionId, y as DocumentSummary, yt as PublicPath, z as Role } from "./models-BkMgeaOL.js";
3
- import { a as normalizeDocumentRelativePath, c as resolveDocumentAddress, i as localizeDocumentPath, n as referenceTarget, o as normalizePublicPath, r as DocumentCompilationFailed, s as parseDocumentAddress, t as createDocumentEngine } from "./DocumentEngine-DYObwMDY.js";
4
- import { A as ACTIVITY_ACTIONS, B as ActivityTargetKind, C as CollaborationTicket, D as DocumentRevision, E as DocumentHistoryInvalidCursor, F as ActivityEntry, G as DocumentVersionConflict, H as DocumentNotFound, I as ActivityEntryDraft, K as PublishedDocumentNotFound, L as ActivityId, M as ActivityContext, N as ActivityCursor, O as DocumentRevisionComparison, P as ActivityCursorInvalid, R as ActivityPage, S as CollaborationServerMessage, T as DocumentHistoryComparisonTooLarge, U as DocumentPathConflict, V as DocumentMediaUnavailable, W as DocumentValidationFailed, _ as UserNotFound, b as UserSelfRoleChangeDenied, c as StudioInternalError, d as InitializationProofInvalid, f as InitializationState, g as UserAlreadyExists, h as UserAdministrationDenied, i as StudioBadRequest, j as ActivityAction, k as DocumentRevisionPage, l as studioInternalError, m as User, n as StudioApi, o as StudioRequestConflict, p as CreateUserInput, r as StudioAuthentication, s as StudioSchemaError, t as CurrentStudioRequest, u as InitializationAlreadyCompleted, v as UserPage, w as StateVector, x as CollaborationClientMessage, y as UserSelfRemovalDenied, z as ActivityTarget } from "./StudioHttp-jR2-bc01.js";
5
- import { a as createEditorNodeId, c as createCmsComarkRenderData, d as toComarkDocument, i as withoutEditorMetadata, o as editorNodeId, r as identifiedAttributes, t as assertCanonicalEditorContent, u as mutableJsonObject } from "./metadata-DE1Mc2td.js";
6
- import { C as finiteNumberAttribute, D as projectCmsEditorComponentAttributes, S as compactJsonAttributes, T as encodeCmsEditorComponentAttributes, _ as comarkTagForEditorMark, b as encodeEditorImageAttributes, d as EditorRenderFailed, g as EDITOR_MARK_TYPES, h as comarkToEditor, u as EditorDocumentInvalid, v as isEditorMark, w as nonEmptyStringAttribute, x as normalizeEditorImageLayout, y as EDITOR_IMAGE_LAYOUT_ATTRIBUTE } from "./Configuration-DyKo0sLS.js";
7
- import { Cause, Clock, Context, Crypto, Data, DateTime, Deferred, Effect, Encoding, Exit, Fiber, Layer, Option, Predicate, PubSub, Pull, Queue, Redacted, Ref, Result, Schedule, Schema, Scope, Semaphore, Stream } from "effect";
1
+ import { a as EditorNodeId, c as RevisionId, d as SourcePath, i as DocumentVersion, r as DocumentId, t as DocumentGeneration } from "./scalars-B2lgGUnj.js";
2
+ import { i as FormResult, n as FORM_SUBMISSION_MAX_BYTES } from "./Forms-CfcI3tPU.js";
3
+ import { a as encodeCmsEditorComponentAttributes, d as createCmsComarkRenderData, i as validateFormValues, t as documentForms } from "./Forms-Dj6NuW66.js";
4
+ import { $ as StudioActor, A as UnpublishDocumentInput, B as SeoMediaUnavailable, D as RemoveDocumentInput, Et as RequestId, H as SeoRevision, J as AccessDenied, K as SeoVersionConflict, O as RestoreDocumentInput, Q as Role, S as PublishDocumentInput, Y as AuthenticationRequired, _t as MediaReference, a as DOCUMENT_REVISION_ACTIONS, at as MediaCleanupReport, b as MarkdownNode, bt as MediaTooLarge, ct as MediaFolder, dt as MediaInvalid, et as StudioEntryDenied, ft as MediaName, ht as MediaPage, it as MediaByteSize, j as UpdateDocumentInput, k as UndeleteDocumentInput, l as DocumentMutationResult, lt as MediaId, m as DocumentSource, mt as MediaObjectKey, n as CmsMarkdownDocument, nt as ImageMimeType, o as DeletedDocument, ot as MediaConflict, p as DocumentRevisionAction, pt as MediaNotFound, rt as MediaAsset, st as MediaCursor, tt as StudioRequestContext, ut as MediaIntegrityReport, vt as MediaReferenced, x as MoveDocumentInput, xt as MediaUpload, y as DuplicateDocumentInput, yt as MediaStatus, z as SeoConfirmationRequired } from "./models-Bah5B3bT.js";
5
+ import { $ as DocumentRevision, A as StudioBadRequest, B as User, C as DocumentStore, D as CurrentStudioRequest, E as defaultAccessControlLayer, F as studioInternalError, G as UserSelfRemovalDenied, H as UserAlreadyExists, I as InitializationAlreadyCompleted, J as CollaborationServerMessage, K as UserSelfRoleChangeDenied, L as InitializationProofInvalid, M as StudioRequestConflict, N as StudioSchemaError, O as StudioApi, P as StudioInternalError, Q as DocumentHistoryInvalidCursor, R as InitializationState, S as DocumentPersistenceError, T as AccessControl, U as UserNotFound, V as UserAdministrationDenied, W as UserPage, X as StateVector, Y as CollaborationTicket, Z as DocumentHistoryComparisonTooLarge, _ as Documents, a as mediaObjectPrefix, at as ActivityEntry, b as cursorCodec, c as Seo, ct as ActivityPage, d as seoDeliveryLayer, dt as DocumentMediaUnavailable, et as ACTIVITY_ACTIONS, f as seoLayer, ft as DocumentNotFound, g as DocumentPreviews, gt as PublishedDocumentNotFound, h as DocumentMaintenance, ht as DocumentVersionConflict, i as MediaObjectStore, it as ActivityCursorInvalid, k as StudioAuthentication, l as SeoDelivery, lt as ActivityTarget, m as SeoStore, mt as DocumentValidationFailed, n as s3MediaObjectStoreLayer, nt as ActivityContext, o as PreviewSeo, ot as ActivityEntryDraft, p as SeoPersistenceError, pt as DocumentPathConflict, q as CollaborationClientMessage, r as MediaPersistenceError, rt as ActivityCursor, s as PublishedSeo, st as ActivityId, tt as ActivityAction, u as makeDefaultSeoOptions, ut as ActivityTargetKind, v as PublishedDocuments, w as renderEditorMarkdown, x as DocumentGenerationConflict, y as documentsLayer, z as CreateUserInput } from "./S3MediaObjects-CpPSzJto.js";
6
+ import { a as CMS_FORM_SUBMIT_PATH, l as CMS_READINESS_PATH, o as CMS_LIVENESS_PATH, p as STUDIO_COLLABORATION_PATH, t as CMS_AUTHENTICATION_PATH, u as STUDIO_API_PATH } from "./HttpPaths-c9nLZ9H2.js";
7
+ import { a as createEditorNodeId, o as editorNodeId, r as identifiedAttributes, t as assertCanonicalEditorContent } from "./metadata-BEZPWh-3.js";
8
+ import { C as finiteNumberAttribute, S as compactJsonAttributes, _ as comarkTagForEditorMark, b as encodeEditorImageAttributes, g as EDITOR_MARK_TYPES, h as comarkToEditor, u as EditorDocumentInvalid, v as isEditorMark, w as nonEmptyStringAttribute, x as normalizeEditorImageLayout, y as EDITOR_IMAGE_LAYOUT_ATTRIBUTE } from "./Configuration-CDMYfTre.js";
9
+ import { o as ContentTransferConflict } from "./ContentTransfer-C3Av9sWz.js";
10
+ import { Cause, Clock, Context, Crypto, Data, DateTime, Deferred, Effect, Encoding, Exit, Fiber, Layer, Option, Predicate, PubSub, Pull, Queue, Redacted, Ref, Result, Schedule, Schema, Scope, Semaphore, Stream, flow } from "effect";
8
11
  import { Extension, Node, getSchema, mergeAttributes } from "@tiptap/core";
9
12
  import { prosemirrorJSONToYDoc, ySyncPluginKey, yXmlFragmentToProseMirrorRootNode } from "@tiptap/y-tiptap";
10
- import { NodeStream } from "@effect/platform-node-shared";
11
- import { HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse, Multipart } from "effect/unstable/http";
13
+ import { FindMyWay, HttpEffect, HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse, Multipart } from "effect/unstable/http";
12
14
  import { HttpApiBuilder, HttpApiMiddleware } from "effect/unstable/httpapi";
13
- import * as NodeCrypto$1 from "@effect/platform-node-shared/NodeCrypto";
15
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto";
16
+ import { createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
14
17
  import { betterAuth } from "better-auth";
15
18
  import { createAccessControl } from "better-auth/plugins/access";
16
19
  import { admin } from "better-auth/plugins/admin";
17
20
  import { defaultStatements } from "better-auth/plugins/admin/access";
18
- import { APIError, createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
19
21
  import { Hocuspocus, IncomingMessage, MessageType, shouldSkipStoreHooks } from "@hocuspocus/server";
20
- import { renderMarkdown } from "comark/render";
21
- import { diffArrays } from "diff";
22
22
  import StarterKit from "@tiptap/starter-kit";
23
23
  import * as Y from "yjs";
24
24
  import { Doc, applyUpdate, encodeStateAsUpdate, encodeStateVector } from "yjs";
25
- import { CopyObjectCommand, CreateBucketCommand, DeleteObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
25
+ import { RateLimiter } from "effect/unstable/persistence";
26
26
  import { PgClient } from "@effect/sql-pg";
27
27
  import { isDeepStrictEqual } from "node:util";
28
- import { and, asc, count, desc, eq, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
28
+ import { and, asc, count, desc, eq, gt, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
29
29
  import * as PgDrizzle from "drizzle-orm/effect-postgres";
30
30
  import { createInsertSchema, createSelectSchema } from "drizzle-orm/effect-schema";
31
31
  import { alias, bigint, boolean, bytea, check, foreignKey, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
@@ -58,50 +58,6 @@ function activityEntryFromDraft(id, draft, occurredAt) {
58
58
  });
59
59
  }
60
60
 
61
- //#endregion
62
- //#region ../server/src/internal/access-control.ts
63
- const VIEWER_ACTIONS = /* @__PURE__ */ new Set([
64
- "document.browse",
65
- "document.read",
66
- "document.trash",
67
- "document.history",
68
- "media.read",
69
- "media.search",
70
- "media.integrity"
71
- ]);
72
- const EDITOR_ACTIONS = /* @__PURE__ */ new Set([...DOCUMENT_ACTIONS, ...MEDIA_ACTIONS.filter((action) => action !== "media.cleanup")]);
73
- var AccessControl = class extends Context.Service()("@internal/server/AccessControl") {};
74
- const defaultAccessControlLayer = Layer.succeed(AccessControl, AccessControl.of({ require: Effect.fn("AccessControl.require")(function* (context, decision) {
75
- if (isAllowed(context, decision.action)) return;
76
- return yield* new AccessDenied({
77
- action: decision.action,
78
- actorId: context.actor.id,
79
- resource: decision.resource
80
- });
81
- }) }));
82
- function isAllowed(context, action) {
83
- return context.actor.roles.some((role) => {
84
- if (role === "administrator") return true;
85
- return role === "editor" && EDITOR_ACTIONS.has(action) || role === "viewer" && VIEWER_ACTIONS.has(action);
86
- });
87
- }
88
-
89
- //#endregion
90
- //#region ../server/src/internal/cursor.ts
91
- /** Owns the opaque base64url JSON representation shared by keyset cursors. */
92
- function cursorCodec(schema, makeCursor, invalid) {
93
- const decodeJson = Schema.decodeUnknownResult(schema);
94
- const encodeJson = Schema.encodeSync(schema);
95
- return {
96
- decode: (cursor) => {
97
- const decoded = Encoding.decodeBase64UrlString(cursor);
98
- if (Result.isFailure(decoded)) return Result.fail(invalid());
99
- return decodeJson(decoded.success).pipe(Result.mapError(invalid));
100
- },
101
- encode: (payload) => makeCursor(Encoding.encodeBase64Url(encodeJson(payload)))
102
- };
103
- }
104
-
105
61
  //#endregion
106
62
  //#region ../server/src/internal/activity/activity.ts
107
63
  var ActivityUnavailable = class extends Schema.TaggedError()("Activity.Unavailable", {
@@ -109,11 +65,11 @@ var ActivityUnavailable = class extends Schema.TaggedError()("Activity.Unavailab
109
65
  cause: Schema.Defect()
110
66
  }) {};
111
67
  var Activity = class extends Context.Service()("@internal/server/Activity") {};
112
- const CursorPayload$2 = Schema.Struct({
68
+ const CursorPayload$1 = Schema.Struct({
113
69
  id: ActivityId,
114
70
  occurredAt: Schema.DateTimeUtcFromString
115
71
  });
116
- const cursors$2 = cursorCodec(Schema.fromJsonString(CursorPayload$2), (value) => ActivityCursor.make(value), () => new ActivityCursorInvalid());
72
+ const cursors$1 = cursorCodec(Schema.fromJsonString(CursorPayload$1), (value) => ActivityCursor.make(value), () => new ActivityCursorInvalid());
117
73
  const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
118
74
  const accessControl = yield* AccessControl;
119
75
  const storage = yield* ActivityStore;
@@ -122,7 +78,7 @@ const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
122
78
  action: "activity.read",
123
79
  resource: { kind: "activity" }
124
80
  });
125
- const cursor = query.cursor ? yield* Effect.fromResult(cursors$2.decode(query.cursor)) : void 0;
81
+ const cursor = query.cursor ? yield* Effect.fromResult(cursors$1.decode(query.cursor)) : void 0;
126
82
  const page = yield* storage.list({
127
83
  action: query.action,
128
84
  actorId: query.actorId,
@@ -138,7 +94,7 @@ const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
138
94
  };
139
95
  return page.next ? ActivityPage.make({
140
96
  ...output,
141
- nextCursor: cursors$2.encode(page.next)
97
+ nextCursor: cursors$1.encode(page.next)
142
98
  }) : ActivityPage.make(output);
143
99
  });
144
100
  return Activity.of({ list });
@@ -254,7 +210,6 @@ const ROUND = [
254
210
  3204031479,
255
211
  3329325298
256
212
  ];
257
- /** Portable synchronous SHA-256 for deterministic journal fingerprints and secret redaction. */
258
213
  function sha256Hex(value) {
259
214
  const input = new TextEncoder().encode(value);
260
215
  const bitLength = BigInt(input.length) * 8n;
@@ -334,14 +289,14 @@ const PendingChange = ChangeDraft.pipe(Schema.fieldsAssign({
334
289
  updatedAt: Schema.DateTimeUtcFromString
335
290
  }));
336
291
  function changeFingerprint(change) {
337
- const encoded = Schema.encodeSync(ChangeDraft)(change);
292
+ const encoded = Result.getOrThrow(Schema.encodeResult(ChangeDraft)(change));
338
293
  return sha256Hex(JSON.stringify(canonicalJson(encoded)));
339
294
  }
340
- const isJsonObject$5 = Schema.is(Schema.JsonObject);
341
- const decodeJson$2 = Schema.decodeUnknownSync(Schema.Json);
295
+ const isJsonObject$4 = Schema.is(Schema.JsonObject);
296
+ const decodeJson$2 = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
342
297
  function canonicalJson(value) {
343
298
  if (Array.isArray(value)) return value.map(canonicalJson);
344
- if (!isJsonObject$5(value)) return value;
299
+ if (!isJsonObject$4(value)) return value;
345
300
  return Object.fromEntries(Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, canonicalJson(decodeJson$2(value[key]))]));
346
301
  }
347
302
 
@@ -349,17 +304,17 @@ function canonicalJson(value) {
349
304
  //#region ../server/src/internal/change-journal/change-journal.ts
350
305
  const ChangeBegin = Data.taggedEnum();
351
306
  function requireUnresolvedChange(result) {
352
- switch (result._tag) {
353
- case "Unresolved": return Effect.succeed(result.change);
354
- case "Recorded": return Effect.fail(new ChangeAlreadyResolved({
355
- changeId: result.entry.id,
307
+ return ChangeBegin.$match(result, {
308
+ Unresolved: ({ change }) => Effect.succeed(change),
309
+ Recorded: ({ entry }) => Effect.fail(new ChangeAlreadyResolved({
310
+ changeId: entry.id,
356
311
  outcome: "applied"
357
- }));
358
- case "Cancelled": return Effect.fail(new ChangeAlreadyResolved({
359
- changeId: result.changeId,
312
+ })),
313
+ Cancelled: ({ changeId }) => Effect.fail(new ChangeAlreadyResolved({
314
+ changeId,
360
315
  outcome: "cancelled"
361
- }));
362
- }
316
+ }))
317
+ });
363
318
  }
364
319
  var ChangeJournal = class extends Context.Service()("@internal/server/ChangeJournal") {};
365
320
  const findPendingChange = Effect.fn("ChangeJournal.findPending")(function* (journal, subject) {
@@ -424,7 +379,7 @@ function initialAdministratorDraft(identity, intent) {
424
379
  action: "user.created",
425
380
  context: identity.context,
426
381
  id: identity.id,
427
- intent: Schema.encodeSync(InitialAdministratorIntent)(intent),
382
+ intent: Result.getOrThrow(Schema.encodeResult(InitialAdministratorIntent)(intent)),
428
383
  scope: "initialization",
429
384
  subject: userEmailSubject(intent.normalizedEmail)
430
385
  });
@@ -566,7 +521,7 @@ function userChangeDraft(identity, intent) {
566
521
  action: userChangeAction(intent),
567
522
  context: identity.context,
568
523
  id: identity.id,
569
- intent: Schema.encodeSync(UserChangeIntent)(intent),
524
+ intent: Result.getOrThrow(Schema.encodeResult(UserChangeIntent)(intent)),
570
525
  scope: "users",
571
526
  subject: userEmailSubject(intent.kind === "create" ? intent.normalizedEmail : intent.email)
572
527
  };
@@ -732,12 +687,22 @@ function appliedResolution(change, intent, userId) {
732
687
  //#region ../auth/src/internal/user.ts
733
688
  const cmsUserCreationChangeField = "cmsCreationChangeId";
734
689
  const cmsUserSessionsRevokedChangeField = "cmsSessionsRevokedChangeId";
690
+ const BetterAuthUser = Schema.Struct({
691
+ banned: Schema.optionalKey(Schema.NullOr(Schema.Boolean)),
692
+ createdAt: Schema.Date,
693
+ email: Schema.String,
694
+ emailVerified: Schema.Boolean,
695
+ id: Schema.String,
696
+ name: Schema.String,
697
+ role: Schema.optionalKey(Schema.NullOr(Schema.String)),
698
+ updatedAt: Schema.Date
699
+ });
735
700
  const BetterAuthUserRecoveryEvidence = Schema.Struct({
736
701
  [cmsUserCreationChangeField]: Schema.optionalKey(Schema.UndefinedOr(Schema.NullOr(ActivityId))),
737
702
  [cmsUserSessionsRevokedChangeField]: Schema.optionalKey(Schema.UndefinedOr(Schema.NullOr(ActivityId)))
738
703
  });
739
- function decodeBetterAuthUser(user) {
740
- return Schema.decodeUnknownEffect(User)({
704
+ function decodeBetterAuthUser(value) {
705
+ return Schema.decodeEffect(BetterAuthUser)(value).pipe(Effect.flatMap((user) => Schema.decodeUnknownEffect(User)({
741
706
  banned: user.banned === true,
742
707
  createdAt: user.createdAt.toISOString(),
743
708
  email: user.email,
@@ -746,7 +711,7 @@ function decodeBetterAuthUser(user) {
746
711
  name: user.name,
747
712
  role: user.role,
748
713
  updatedAt: user.updatedAt.toISOString()
749
- });
714
+ })));
750
715
  }
751
716
  function decodeBetterAuthActor(user) {
752
717
  return Schema.decodeEffect(StudioActor)(betterAuthActorInput(user));
@@ -783,12 +748,12 @@ function betterAuthActivityWriter(storage, crypto) {
783
748
  return yield* storage.append(activityEntryFromDraft(id, draft, yield* DateTime.now));
784
749
  });
785
750
  }
786
- function betterAuthActivityOptions(writeActivity, crypto) {
751
+ function betterAuthActivityOptions(writeActivity, crypto, runPromise) {
787
752
  const capturedActors = /* @__PURE__ */ new WeakMap();
788
- const write = (action, actor) => Effect.runPromise(crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((requestId) => activityDraft(action, actor, requestId)), Effect.flatMap(writeActivity), Effect.asVoid, Effect.catchCause(() => Effect.logWarning("Authentication Activity could not be recorded.", { action }).pipe(Effect.ignoreCause))));
753
+ const write = (action, actor) => runPromise(crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((requestId) => activityDraft(action, actor, requestId)), Effect.flatMap(writeActivity), Effect.asVoid, Effect.catchCauseIf((cause) => Cause.interruptors(cause).size === 0, () => Effect.logWarning("Authentication Activity could not be recorded.", { action }).pipe(Effect.ignoreCause))));
789
754
  const before = createAuthMiddleware((context) => {
790
755
  if (context.path !== "/sign-out" && context.path !== "/change-password") return Promise.resolve();
791
- return Effect.runPromise(Effect.tryPromise(() => getSessionFromCtx(context, { disableRefresh: true })).pipe(Effect.orElseSucceed(() => null), Effect.tap((session) => Effect.sync(() => {
756
+ return runPromise(Effect.tryPromise(() => getSessionFromCtx(context, { disableRefresh: true })).pipe(Effect.orElseSucceed(() => null), Effect.tap((session) => Effect.sync(() => {
792
757
  const actor = session?.user ? decodeActor(session.user) : void 0;
793
758
  if (actor) capturedActors.set(context.context, actor);
794
759
  })), Effect.asVoid));
@@ -832,7 +797,149 @@ function activityDraft(action, actor, requestId) {
832
797
  }
833
798
  function decodeActor(user) {
834
799
  const option = decodeBetterAuthActorOption(user);
835
- return option._tag === "Some" ? option.value : void 0;
800
+ return Option.getOrUndefined(option);
801
+ }
802
+
803
+ //#endregion
804
+ //#region ../auth/src/internal/better-auth.ts
805
+ const cmsBetterAuthAccess = createAccessControl(defaultStatements);
806
+ const cmsBetterAuthRoles = {
807
+ administrator: cmsBetterAuthAccess.newRole({
808
+ session: ["list", "revoke"],
809
+ user: [
810
+ "create",
811
+ "list",
812
+ "get",
813
+ "set-role",
814
+ "delete",
815
+ "update"
816
+ ]
817
+ }),
818
+ editor: cmsBetterAuthAccess.newRole({
819
+ session: [],
820
+ user: []
821
+ }),
822
+ viewer: cmsBetterAuthAccess.newRole({
823
+ session: [],
824
+ user: []
825
+ })
826
+ };
827
+ const timestampFields = {
828
+ createdAt: "created_at",
829
+ updatedAt: "updated_at"
830
+ };
831
+ function createCmsBetterAuthOptions(options, activity) {
832
+ return {
833
+ advanced: { database: {
834
+ generateId: "uuid",
835
+ joins: true
836
+ } },
837
+ appName: "CMS Studio",
838
+ basePath: options.basePath ?? "/api/cms/auth",
839
+ baseURL: options.baseURL,
840
+ database: options.database,
841
+ emailAndPassword: {
842
+ disableSignUp: true,
843
+ enabled: true,
844
+ ...activity?.emailAndPassword
845
+ },
846
+ hooks: activity?.hooks,
847
+ user: {
848
+ additionalFields: {
849
+ [cmsUserCreationChangeField]: hiddenChangeField("cms_creation_change_id"),
850
+ [cmsUserSessionsRevokedChangeField]: hiddenChangeField("cms_sessions_revoked_change_id")
851
+ },
852
+ modelName: "auth_users",
853
+ fields: {
854
+ ...timestampFields,
855
+ emailVerified: "email_verified"
856
+ }
857
+ },
858
+ session: {
859
+ cookieCache: {
860
+ enabled: false,
861
+ strategy: "jwt"
862
+ },
863
+ modelName: "auth_sessions",
864
+ fields: {
865
+ ...timestampFields,
866
+ expiresAt: "expires_at",
867
+ ipAddress: "ip_address",
868
+ userAgent: "user_agent",
869
+ userId: "user_id"
870
+ }
871
+ },
872
+ account: {
873
+ modelName: "auth_accounts",
874
+ fields: {
875
+ accessToken: "access_token",
876
+ accessTokenExpiresAt: "access_token_expires_at",
877
+ accountId: "account_id",
878
+ ...timestampFields,
879
+ idToken: "id_token",
880
+ providerId: "provider_id",
881
+ refreshToken: "refresh_token",
882
+ refreshTokenExpiresAt: "refresh_token_expires_at",
883
+ userId: "user_id"
884
+ }
885
+ },
886
+ verification: {
887
+ modelName: "auth_verifications",
888
+ fields: {
889
+ ...timestampFields,
890
+ expiresAt: "expires_at"
891
+ }
892
+ },
893
+ rateLimit: {
894
+ enabled: true,
895
+ storage: "database",
896
+ modelName: "auth_rate_limits",
897
+ fields: { lastRequest: "last_request" }
898
+ },
899
+ plugins: [admin({
900
+ ac: cmsBetterAuthAccess,
901
+ adminRoles: ["administrator"],
902
+ defaultRole: "editor",
903
+ roles: cmsBetterAuthRoles,
904
+ schema: {
905
+ session: { fields: { impersonatedBy: "impersonated_by" } },
906
+ user: { fields: {
907
+ banExpires: "ban_expires",
908
+ banReason: "ban_reason",
909
+ banned: "banned",
910
+ role: "role"
911
+ } }
912
+ }
913
+ })],
914
+ secret: options.secret
915
+ };
916
+ }
917
+ function createCmsBetterAuth(options, activity) {
918
+ return betterAuth(createCmsBetterAuthOptions(options, activity));
919
+ }
920
+ function hiddenChangeField(fieldName) {
921
+ return {
922
+ fieldName,
923
+ input: false,
924
+ required: false,
925
+ returned: false,
926
+ type: "string"
927
+ };
928
+ }
929
+
930
+ //#endregion
931
+ //#region ../auth/src/internal/handler.ts
932
+ function rejectExternalAdminRoutes(handler, basePath) {
933
+ const mountedAt = canonicalBasePath(basePath);
934
+ return (request) => {
935
+ const path = new URL(request.url).pathname;
936
+ const relative = mountedAt === "/" ? path : path === mountedAt ? "/" : path.startsWith(`${mountedAt}/`) ? path.slice(mountedAt.length) : path;
937
+ if (relative === "/admin" || relative.startsWith("/admin/")) return Promise.resolve(new Response(null, { status: 404 }));
938
+ return handler(request);
939
+ };
940
+ }
941
+ function canonicalBasePath(basePath) {
942
+ return (basePath.startsWith("/") ? basePath : `/${basePath}`).replace(/\/+$/u, "") || "/";
836
943
  }
837
944
 
838
945
  //#endregion
@@ -915,9 +1022,24 @@ function matches(code, ...candidates) {
915
1022
  return code !== void 0 && candidates.some((candidate) => candidate.code === code);
916
1023
  }
917
1024
  function apiErrorCode(cause) {
918
- if (!(cause instanceof APIError)) return void 0;
919
- const body = Schema.decodeUnknownOption(Schema.Struct({ code: Schema.String }))(cause.body);
920
- return body._tag === "Some" ? body.value.code : void 0;
1025
+ if (!(cause instanceof Error) || cause.name !== "APIError") return void 0;
1026
+ const apiError = Schema.decodeUnknownOption(Schema.Struct({
1027
+ body: Schema.Struct({ code: Schema.String }),
1028
+ statusCode: Schema.Int
1029
+ }))(cause);
1030
+ return Option.map(apiError, ({ body }) => body.code).pipe(Option.getOrUndefined);
1031
+ }
1032
+
1033
+ //#endregion
1034
+ //#region ../auth/src/internal/provider.ts
1035
+ function providerCall(operation, request, onError) {
1036
+ return Effect.tryPromise({
1037
+ catch: (cause) => onError(operation, cause),
1038
+ try: request
1039
+ }).pipe(Effect.withSpan(operation));
1040
+ }
1041
+ function providerAdapter(auth, operation, onError) {
1042
+ return providerCall(operation, () => auth.$context, onError).pipe(Effect.map((context) => context.internalAdapter));
921
1043
  }
922
1044
 
923
1045
  //#endregion
@@ -926,13 +1048,10 @@ function betterAuthIdentity(auth) {
926
1048
  const authorityHeaders = /* @__PURE__ */ new WeakMap();
927
1049
  const authenticate = Effect.fn("BetterAuth.Authentication.authenticate")(function* (headers) {
928
1050
  const providerHeaders = new globalThis.Headers(headers);
929
- const session = yield* Effect.tryPromise({
930
- catch: () => studioInternalError(),
931
- try: () => auth.api.getSession({
932
- headers: providerHeaders,
933
- query: { disableCookieCache: true }
934
- })
935
- });
1051
+ const session = yield* providerCall("BetterAuth.getSession", () => auth.api.getSession({
1052
+ headers: providerHeaders,
1053
+ query: { disableCookieCache: true }
1054
+ }), () => studioInternalError());
936
1055
  if (!session?.user) return null;
937
1056
  const actor = yield* decodeBetterAuthActor(session.user).pipe(Effect.mapError(() => studioInternalError()));
938
1057
  const authority = issueIdentityAuthority();
@@ -1026,16 +1145,13 @@ function betterAuthIdentity(auth) {
1026
1145
  const apply = (changeId) => applyOperation(auth, "BetterAuth.revokeUserSessions", command, () => auth.api.revokeUserSessions({
1027
1146
  body: { userId: command.userId },
1028
1147
  headers
1029
- })).pipe(Effect.flatMap(() => Effect.tryPromise({
1030
- catch: (cause) => IdentityApplyFailure.Unknown({ error: unavailable$4("BetterAuth.adminUpdateUser.sessionRevocationReceipt", cause) }),
1031
- try: () => auth.api.adminUpdateUser({
1032
- body: {
1033
- data: { [cmsUserSessionsRevokedChangeField]: changeId },
1034
- userId: command.userId
1035
- },
1036
- headers
1037
- })
1038
- })), Effect.as(user));
1148
+ })).pipe(Effect.flatMap(() => providerCall("BetterAuth.adminUpdateUser.sessionRevocationReceipt", () => auth.api.adminUpdateUser({
1149
+ body: {
1150
+ data: { [cmsUserSessionsRevokedChangeField]: changeId },
1151
+ userId: command.userId
1152
+ },
1153
+ headers
1154
+ }), (operation, cause) => IdentityApplyFailure.Unknown({ error: unavailable$4(operation, cause) }))), Effect.as(user));
1039
1155
  return IdentityPreparation.Prepared({
1040
1156
  apply,
1041
1157
  intent,
@@ -1053,22 +1169,13 @@ function betterAuthIdentity(auth) {
1053
1169
  };
1054
1170
  }
1055
1171
  function prepareOperation(auth, operation, request) {
1056
- return Effect.tryPromise({
1057
- catch: (cause) => prepareError(auth.$ERROR_CODES, operation, cause),
1058
- try: request
1059
- });
1172
+ return providerCall(operation, request, (name, cause) => prepareError(auth.$ERROR_CODES, name, cause));
1060
1173
  }
1061
1174
  function prepareUserOperation(auth, operation, userId, request) {
1062
- return Effect.tryPromise({
1063
- catch: (cause) => prepareError(auth.$ERROR_CODES, operation, cause, userId),
1064
- try: request
1065
- });
1175
+ return providerCall(operation, request, (name, cause) => prepareError(auth.$ERROR_CODES, name, cause, userId));
1066
1176
  }
1067
1177
  function applyOperation(auth, operation, command, request) {
1068
- return Effect.tryPromise({
1069
- catch: (cause) => applyError(auth.$ERROR_CODES, operation, command, cause),
1070
- try: request
1071
- });
1178
+ return providerCall(operation, request, (name, cause) => applyError(auth.$ERROR_CODES, name, command, cause));
1072
1179
  }
1073
1180
  function requireAuthority(authorityHeaders, authority) {
1074
1181
  const headers = authorityHeaders.get(authority);
@@ -1106,13 +1213,10 @@ function betterAuthInitialIdentity(auth) {
1106
1213
  });
1107
1214
  const intent = initialAdministratorIntent(input);
1108
1215
  return {
1109
- apply: Effect.tryPromise({
1110
- catch: (cause) => initialIdentityError(auth.$ERROR_CODES, "BetterAuth.initialization.createUser", createInput, cause),
1111
- try: () => auth.api.createUser({ body: {
1112
- data: { emailVerified: true },
1113
- ...createInput
1114
- } })
1115
- }).pipe(Effect.flatMap((result) => decodeBetterAuthUser(result.user).pipe(Effect.mapError((cause) => InitialIdentityFailure.Unknown({ error: initializationUnavailable$1("BetterAuth.initialization.createUser", cause) }))))),
1216
+ apply: providerCall("BetterAuth.initialization.createUser", () => auth.api.createUser({ body: {
1217
+ data: { emailVerified: true },
1218
+ ...createInput
1219
+ } }), (operation, cause) => initialIdentityError(auth.$ERROR_CODES, operation, createInput, cause)).pipe(Effect.flatMap((result) => decodeBetterAuthUser(result.user).pipe(Effect.mapError((cause) => InitialIdentityFailure.Unknown({ error: initializationUnavailable$1("BetterAuth.initialization.createUser", cause) }))))),
1116
1220
  intent
1117
1221
  };
1118
1222
  } });
@@ -1121,22 +1225,13 @@ function betterAuthInitialIdentity(auth) {
1121
1225
  //#endregion
1122
1226
  //#region ../auth/src/internal/recovery.ts
1123
1227
  function betterAuthRecovery(auth) {
1124
- const userAdapter = Effect.tryPromise({
1125
- catch: (cause) => unavailable$4("BetterAuth.recovery.context", cause),
1126
- try: () => auth.$context
1127
- }).pipe(Effect.map((context) => context.internalAdapter));
1128
- const initialAdapter = Effect.tryPromise({
1129
- catch: (cause) => initializationUnavailable$1("BetterAuth.initialization.recovery.context", cause),
1130
- try: () => auth.$context
1131
- }).pipe(Effect.map((context) => context.internalAdapter));
1228
+ const userAdapter = providerAdapter(auth, "BetterAuth.recovery.context", unavailable$4);
1229
+ const initialAdapter = providerAdapter(auth, "BetterAuth.initialization.recovery.context", initializationUnavailable$1);
1132
1230
  const users = IdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeUserChange")(function* (intent) {
1133
1231
  const adapter = yield* userAdapter;
1134
1232
  switch (intent.kind) {
1135
1233
  case "create": {
1136
- const result = yield* Effect.tryPromise({
1137
- catch: (cause) => unavailable$4("BetterAuth.recovery.findUserByEmail", cause),
1138
- try: () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true })
1139
- });
1234
+ const result = yield* providerCall("BetterAuth.recovery.findUserByEmail", () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true }), unavailable$4);
1140
1235
  if (!result) return {
1141
1236
  creationChangeId: null,
1142
1237
  credentialReady: false,
@@ -1178,10 +1273,7 @@ function betterAuthRecovery(auth) {
1178
1273
  return {
1179
1274
  initial: InitialIdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeInitialAdministrator")(function* (intent) {
1180
1275
  const adapter = yield* initialAdapter;
1181
- const result = yield* Effect.tryPromise({
1182
- catch: (cause) => initializationUnavailable$1("BetterAuth.recovery.findInitialAdministrator", cause),
1183
- try: () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true })
1184
- });
1276
+ const result = yield* providerCall("BetterAuth.recovery.findInitialAdministrator", () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true }), initializationUnavailable$1);
1185
1277
  if (!result) return {
1186
1278
  credentialReady: false,
1187
1279
  user: null
@@ -1204,1580 +1296,156 @@ function findUser(adapter, userId) {
1204
1296
  return findRawUser(adapter, userId).pipe(Effect.flatMap((user) => user ? decodeUser(user, "BetterAuth.recovery.findUserById") : Effect.succeed(null)));
1205
1297
  }
1206
1298
  function findRawUser(adapter, userId) {
1207
- return Effect.tryPromise({
1208
- catch: (cause) => unavailable$4("BetterAuth.recovery.findUserById", cause),
1209
- try: () => adapter.findUserById(userId)
1210
- });
1299
+ return providerCall("BetterAuth.recovery.findUserById", () => adapter.findUserById(userId), unavailable$4);
1211
1300
  }
1212
1301
  function findSessionFingerprints(adapter, userId) {
1213
- return Effect.tryPromise({
1214
- catch: (cause) => unavailable$4("BetterAuth.recovery.listSessions", cause),
1215
- try: () => adapter.listSessions(userId)
1216
- }).pipe(Effect.map((sessions) => sessions.map((session) => sessionFingerprint(session.id))));
1302
+ return providerCall("BetterAuth.recovery.listSessions", () => adapter.listSessions(userId), unavailable$4).pipe(Effect.map((sessions) => sessions.map((session) => sessionFingerprint(session.id))));
1217
1303
  }
1218
1304
 
1219
1305
  //#endregion
1220
- //#region ../auth/src/internal/better-auth.ts
1221
- const cmsBetterAuthAccess = createAccessControl(defaultStatements);
1222
- const cmsBetterAuthRoles = {
1223
- administrator: cmsBetterAuthAccess.newRole({
1224
- session: ["list", "revoke"],
1225
- user: [
1226
- "create",
1227
- "list",
1228
- "get",
1229
- "set-role",
1230
- "delete",
1231
- "update"
1232
- ]
1233
- }),
1234
- editor: cmsBetterAuthAccess.newRole({
1235
- session: [],
1236
- user: []
1237
- }),
1238
- viewer: cmsBetterAuthAccess.newRole({
1239
- session: [],
1240
- user: []
1241
- })
1242
- };
1243
- const timestampFields = {
1244
- createdAt: "created_at",
1245
- updatedAt: "updated_at"
1246
- };
1247
- function createCmsBetterAuthOptions(options, activity) {
1248
- return {
1249
- advanced: { database: {
1250
- generateId: "uuid",
1251
- joins: true
1252
- } },
1253
- appName: "CMS Studio",
1254
- basePath: options.basePath ?? "/api/cms/auth",
1255
- baseURL: options.baseURL,
1256
- database: options.database,
1257
- emailAndPassword: {
1258
- disableSignUp: true,
1259
- enabled: true,
1260
- ...activity?.emailAndPassword
1261
- },
1262
- hooks: activity?.hooks,
1263
- user: {
1264
- additionalFields: {
1265
- [cmsUserCreationChangeField]: hiddenChangeField("cms_creation_change_id"),
1266
- [cmsUserSessionsRevokedChangeField]: hiddenChangeField("cms_sessions_revoked_change_id")
1267
- },
1268
- modelName: "auth_users",
1269
- fields: {
1270
- ...timestampFields,
1271
- emailVerified: "email_verified"
1272
- }
1273
- },
1274
- session: {
1275
- cookieCache: {
1276
- enabled: false,
1277
- strategy: "jwt"
1278
- },
1279
- modelName: "auth_sessions",
1280
- fields: {
1281
- ...timestampFields,
1282
- expiresAt: "expires_at",
1283
- ipAddress: "ip_address",
1284
- userAgent: "user_agent",
1285
- userId: "user_id"
1286
- }
1287
- },
1288
- account: {
1289
- modelName: "auth_accounts",
1290
- fields: {
1291
- accessToken: "access_token",
1292
- accessTokenExpiresAt: "access_token_expires_at",
1293
- accountId: "account_id",
1294
- ...timestampFields,
1295
- idToken: "id_token",
1296
- providerId: "provider_id",
1297
- refreshToken: "refresh_token",
1298
- refreshTokenExpiresAt: "refresh_token_expires_at",
1299
- userId: "user_id"
1300
- }
1301
- },
1302
- verification: {
1303
- modelName: "auth_verifications",
1304
- fields: {
1305
- ...timestampFields,
1306
- expiresAt: "expires_at"
1307
- }
1308
- },
1309
- rateLimit: {
1310
- enabled: true,
1311
- storage: "database",
1312
- modelName: "auth_rate_limits",
1313
- fields: { lastRequest: "last_request" }
1314
- },
1315
- plugins: [admin({
1316
- ac: cmsBetterAuthAccess,
1317
- adminRoles: ["administrator"],
1318
- defaultRole: "editor",
1319
- roles: cmsBetterAuthRoles,
1320
- schema: {
1321
- session: { fields: { impersonatedBy: "impersonated_by" } },
1322
- user: { fields: {
1323
- banExpires: "ban_expires",
1324
- banReason: "ban_reason",
1325
- banned: "banned",
1326
- role: "role"
1327
- } }
1328
- }
1329
- })],
1330
- secret: options.secret
1331
- };
1332
- }
1333
- function createCmsBetterAuth(options, activity) {
1334
- return betterAuth(createCmsBetterAuthOptions(options, activity));
1335
- }
1306
+ //#region ../auth/src/BetterAuth.ts
1336
1307
  function betterAuthLayer(options) {
1308
+ if (options.basePath !== void 0 && options.basePath !== "/api/cms/auth") throw new Error(`Authentication basePath must be ${CMS_AUTHENTICATION_PATH}.`);
1337
1309
  return Layer.effectContext(Effect.gen(function* () {
1338
1310
  const crypto = yield* Crypto.Crypto;
1339
1311
  const storage = yield* ActivityStore;
1340
- const auth = createCmsBetterAuth(options, betterAuthActivityOptions(betterAuthActivityWriter(storage, crypto), crypto));
1312
+ const runPromise = Effect.runPromiseWith(yield* Effect.context());
1313
+ const auth = createCmsBetterAuth(options, betterAuthActivityOptions(betterAuthActivityWriter(storage, crypto), crypto, runPromise));
1341
1314
  const identity = betterAuthIdentity(auth);
1342
1315
  const recovery = betterAuthRecovery(auth);
1343
1316
  const initialization = betterAuthInitialIdentity(auth);
1344
1317
  const basePath = options.basePath ?? "/api/cms/auth";
1345
- const handler = AuthenticationHandler.of({ handle: rejectExternalAdminRoutes(auth.handler, basePath) });
1346
- return Context.empty().pipe(Context.add(Authentication, identity.authentication), Context.add(IdentityAdministration, identity.administration), Context.add(IdentityRecovery, recovery.users), Context.add(InitialIdentity, initialization), Context.add(InitialIdentityRecovery, recovery.initial), Context.add(AuthenticationHandler, handler));
1347
- }));
1348
- }
1349
- function rejectExternalAdminRoutes(handler, basePath) {
1350
- const mountedAt = canonicalBasePath(basePath);
1351
- return (request) => {
1352
- const path = new URL(request.url).pathname;
1353
- const relative = mountedAt === "/" ? path : path === mountedAt ? "/" : path.startsWith(`${mountedAt}/`) ? path.slice(mountedAt.length) : path;
1354
- if (relative === "/admin" || relative.startsWith("/admin/")) return Promise.resolve(new Response(null, { status: 404 }));
1355
- return handler(request);
1356
- };
1357
- }
1358
- function canonicalBasePath(basePath) {
1359
- return (basePath.startsWith("/") ? basePath : `/${basePath}`).replace(/\/+$/u, "") || "/";
1360
- }
1361
- function hiddenChangeField(fieldName) {
1362
- return {
1363
- fieldName,
1364
- input: false,
1365
- required: false,
1366
- returned: false,
1367
- type: "string"
1368
- };
1369
- }
1370
-
1371
- //#endregion
1372
- //#region ../server/src/adapter/Readiness.ts
1373
- var ReadinessFailed = class extends Schema.TaggedError()("Cms.ReadinessFailed", { cause: Schema.Defect() }) {};
1374
- var Readiness = class extends Context.Service()("@internal/server/Readiness") {};
1375
-
1376
- //#endregion
1377
- //#region ../server/src/internal/working-documents/working-document-model.ts
1378
- const WorkingDocumentGenerationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("WorkingDocumentGenerationId"));
1379
- const WorkingDocumentUpdateSequence = Schema.Int.check(Schema.isGreaterThan(0)).pipe(Schema.brand("WorkingDocumentUpdateSequence"));
1380
- const WorkingDocumentHighWatermark = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.brand("WorkingDocumentHighWatermark"));
1381
- var WorkingDocumentPersistenceError = class extends Schema.TaggedError()("WorkingDocuments.PersistenceError", {
1382
- operation: Schema.NonEmptyString,
1383
- cause: Schema.Defect()
1384
- }) {};
1385
- var WorkingDocumentSuperseded = class extends Schema.TaggedError()("WorkingDocuments.Superseded", {
1386
- documentId: DocumentId,
1387
- expectedGeneration: WorkingDocumentGenerationId,
1388
- actualGeneration: Schema.optionalKey(WorkingDocumentGenerationId)
1389
- }) {};
1390
- var WorkingDocumentConflict = class extends Schema.TaggedError()("WorkingDocuments.Conflict", {
1391
- documentId: DocumentId,
1392
- operation: Schema.NonEmptyString,
1393
- message: Schema.NonEmptyString
1394
- }) {};
1395
-
1396
- //#endregion
1397
- //#region ../server/src/internal/working-documents/working-document.ts
1398
- /** Durable identity for one exact recoverable Working Document generation. */
1399
- const WorkingDocumentRef = Schema.Struct({
1400
- documentId: DocumentId,
1401
- generationId: WorkingDocumentGenerationId
1402
- });
1403
- var WorkingDocumentInvalid = class extends Schema.TaggedError()("WorkingDocuments.Invalid", {
1404
- documentId: DocumentId,
1405
- message: Schema.NonEmptyString,
1406
- cause: Schema.Defect()
1407
- }) {};
1408
- var WorkingDocumentUnavailable = class extends Schema.TaggedError()("WorkingDocuments.Unavailable", {
1409
- operation: Schema.NonEmptyString,
1410
- cause: Schema.Defect()
1411
- }) {};
1412
- /** An authored or working baseline committed, but the publish workflow still needs recovery. */
1413
- var WorkingDocumentCheckpointed = class extends Schema.TaggedError()("WorkingDocuments.Checkpointed", {
1414
- checkpointVersion: DocumentVersion,
1415
- documentId: DocumentId,
1416
- result: Schema.optionalKey(DocumentMutationResult),
1417
- stateVector: Schema.Uint8Array,
1418
- cause: Schema.Defect()
1419
- }) {};
1420
- /** Signals that one exact Working Document generation changed or became invalid. */
1421
- const WorkingDocumentChange = Schema.Struct({
1422
- kind: Schema.Literals(["updated", "invalidated"]),
1423
- ref: WorkingDocumentRef
1424
- });
1425
- var WorkingDocuments = class extends Context.Service()("@internal/server/WorkingDocuments") {};
1426
-
1427
- //#endregion
1428
- //#region ../document/src/internal/editor/render.ts
1429
- const renderEditorMarkdown = Effect.fn("EditorDocument.renderMarkdown")((document) => {
1430
- return Effect.tryPromise({
1431
- try: () => renderMarkdown(toComarkDocument(withoutEditorMetadata(document))),
1432
- catch: (cause) => new EditorRenderFailed({ cause })
1433
- });
1434
- });
1435
-
1436
- //#endregion
1437
- //#region ../document/src/internal/document-engine/relocate-links.ts
1438
- /** Rewrites only semantic page links, preserving bindings and unrelated content. */
1439
- function rewriteDocumentRoutes(document, destinations, components = []) {
1440
- let changed = false;
1441
- function link(value) {
1442
- const target = referenceTarget("a", /* @__PURE__ */ new Map([["href", value]]), document.path, void 0);
1443
- if (target?.kind !== "document" || !Predicate.isString(value)) return value;
1444
- const destination = destinations.get(target.path);
1445
- const sourceMoved = destinations.has(document.path);
1446
- if (!destination && (!sourceMoved || value.startsWith("/"))) return value;
1447
- const suffix = value.match(/[?#].*$/u)?.[0] ?? "";
1448
- const next = `${destination ?? target.path}${suffix}`;
1449
- if (next !== value) changed = true;
1450
- return next;
1451
- }
1452
- function properties(fields, values) {
1453
- const result = mutableJsonObject(values);
1454
- for (const field of fields) {
1455
- const value = values[field.name] === void 0 ? field.defaultValue : values[field.name];
1456
- if (field.input === "link") {
1457
- const next = link(value);
1458
- if (next !== void 0 && next !== value) result[field.name] = next;
1459
- } else if (field.fields) {
1460
- const next = Array.isArray(value) ? value.map((item) => Schema.is(Schema.JsonObject)(item) ? properties(field.fields ?? [], item) : item) : Schema.is(Schema.JsonObject)(value) ? properties(field.fields, value) : value;
1461
- if (next !== void 0 && JSON.stringify(next) !== JSON.stringify(value)) result[field.name] = next;
1462
- }
1463
- }
1464
- return result;
1465
- }
1466
- function visit(node, renderData) {
1467
- if (Predicate.isString(node) || node[0] === null) return node;
1468
- const [tag, attributes, ...children] = node;
1469
- const projection = projectCmsEditorComponentAttributes(attributes, renderData);
1470
- let props = properties(components.find((component) => component.name.toLowerCase() === tag.toLowerCase())?.authoring.props ?? [], projection.props);
1471
- if (tag === "a") {
1472
- const href = link(projection.props.href);
1473
- if (href !== void 0) props = Object.assign({}, props, { href });
1474
- }
1475
- return [
1476
- tag,
1477
- JSON.stringify(props) === JSON.stringify(projection.props) ? attributes : encodeCmsEditorComponentAttributes(props, projection.bindings),
1478
- ...children.map((child) => visit(child, projection.renderData))
1479
- ];
1480
- }
1481
- const nodes = document.body.nodes.map((node) => visit(node, createCmsComarkRenderData(document.body)));
1482
- return changed ? Object.assign({}, document.body, { nodes }) : void 0;
1483
- }
1484
-
1485
- //#endregion
1486
- //#region ../document/src/DocumentPlanning.ts
1487
- /** Derives one canonical Source path and public path without performing persistence. */
1488
- function planDocumentCreation(configuration, input) {
1489
- return Result.gen(function* () {
1490
- const collection = configuration.types.find((candidate) => candidate.collection === input.collection);
1491
- if (!collection) return yield* invalid$2(`Document collection "${input.collection}" does not exist.`, "collection");
1492
- if (!collection.canCreate) return yield* invalid$2(`Document collection "${input.collection}" does not allow creation.`, "collection");
1493
- const title = input.title.trim();
1494
- if (!title) return yield* invalid$2("A document title is required.", "title");
1495
- const locale = yield* resolveLocale(configuration, input.locale);
1496
- const address = yield* parseDocumentAddress(collection.addressPattern).pipe(Result.mapError(addressInvalid));
1497
- const relativePath = yield* resolveRelativePath(collection, input.location);
1498
- const data = Object.fromEntries(address.tokens.flatMap((token) => {
1499
- if (token.name === "slug" && relativePath) return [[token.name, relativePath.split("/").at(-1) ?? relativePath]];
1500
- return [];
1501
- }));
1502
- const relativeKey = yield* normalizePublicPath(relativePath).pipe(Result.mapError(addressInvalid));
1503
- const key = yield* resolveDocumentAddress(address, relativeKey, data).pipe(Result.mapError(addressInvalid));
1504
- const sourcePath = documentSourcePath(collection, relativePath, locale);
1505
- const path = localizeDocumentPath(key, locale, configuration.locales);
1506
- return Object.assign({
1507
- collection,
1508
- data,
1509
- key,
1510
- path,
1511
- sourcePath,
1512
- title
1513
- }, locale === void 0 ? void 0 : { locale });
1514
- });
1515
- }
1516
- /** Creates the canonical initial editor source, including configured field defaults. */
1517
- const createInitialDocumentSource = Effect.fn("DocumentPlanning.createInitialSource")(function* (plan) {
1518
- const body = CmsMarkdownDocument.make({
1519
- frontmatter: initialFrontmatter(plan),
1520
- meta: {},
1521
- nodes: [[
1522
- "h1",
1523
- {},
1524
- plan.title
1525
- ]]
1526
- });
1527
- return {
1528
- body,
1529
- markdown: yield* renderEditorMarkdown(body)
1530
- };
1531
- });
1532
- /** Derives the canonical destination Source path while preserving the Document's collection and locale. */
1533
- function planDocumentMove(configuration, document, location) {
1534
- return Result.gen(function* () {
1535
- const collection = configuration.types.find((candidate) => candidate.collection === document.collection);
1536
- if (!collection) return yield* invalid$2(`Document collection "${document.collection}" does not exist.`, "documentId");
1537
- return documentSourcePath(collection, yield* resolveRelativePath(collection, location), document.locale);
1538
- });
1539
- }
1540
- /** Derives a collection-relative hierarchy path from a canonical document summary. */
1541
- function documentRelativePath(collection, entry) {
1542
- let sourcePath = entry.sourcePath;
1543
- if (entry.document.locale && sourcePath.startsWith(`${entry.document.locale}/`)) sourcePath = sourcePath.slice(entry.document.locale.length + 1);
1544
- if (collection.directory !== "." && sourcePath.startsWith(`${collection.directory}/`)) sourcePath = sourcePath.slice(collection.directory.length + 1);
1545
- sourcePath = sourcePath.replace(/\.(?:md|mdc)$/iu, "");
1546
- return sourcePath.toLocaleLowerCase() === "index" ? void 0 : sourcePath.replace(/\/index$/iu, "");
1547
- }
1548
- function documentSourcePath(collection, relativePath, locale) {
1549
- const sourceName = relativePath ? `${relativePath}.md` : "index.md";
1550
- return SourcePath.make([
1551
- locale,
1552
- collection.directory === "." ? void 0 : collection.directory,
1553
- sourceName
1554
- ].filter((value) => value !== void 0).join("/"));
1555
- }
1556
- function initialFrontmatter(plan) {
1557
- let frontmatter = {
1558
- ...plan.data,
1559
- title: plan.title
1560
- };
1561
- for (const field of plan.collection.authoringFields) if (field.defaultValue !== void 0 && nestedValue$1(frontmatter, field.path) === void 0) frontmatter = setNestedValue(frontmatter, field.path.split("."), field.defaultValue);
1562
- return frontmatter;
1563
- }
1564
- function nestedValue$1(value, path) {
1565
- return path.split(".").reduce((current, segment) => isJsonObject$4(current) ? current[segment] : void 0, value);
1566
- }
1567
- function setNestedValue(target, path, value) {
1568
- const [segment, ...rest] = path;
1569
- if (!segment) return target;
1570
- if (rest.length === 0) return Object.assign({}, target, { [segment]: value });
1571
- const current = target[segment];
1572
- return Object.assign({}, target, { [segment]: setNestedValue(isJsonObject$4(current) ? current : {}, rest, value) });
1573
- }
1574
- const isJsonObject$4 = Schema.is(Schema.JsonObject);
1575
- function resolveLocale(configuration, requested) {
1576
- const locales = configuration.locales;
1577
- if (!locales) return requested === void 0 ? Result.succeed(void 0) : invalid$2(`Document locale "${requested}" is not configured.`, "locale");
1578
- const locale = requested ?? locales.defaultLocale;
1579
- return locales.locales.includes(locale) ? Result.succeed(locale) : invalid$2(`Document locale "${locale}" does not exist.`, "locale");
1580
- }
1581
- function resolveRelativePath(collection, location) {
1582
- if (location.kind === "home") return collection.addressPattern === "/:path*" ? Result.succeed("") : invalid$2("Only a root catch-all collection can use the home location.", "location");
1583
- if (location.parent !== void 0 && !collection.hierarchy) return invalid$2(`Document collection "${collection.collection}" does not allow hierarchy.`, "location.parent");
1584
- return normalizeDocumentRelativePath([location.parent, location.slug].filter((value) => value !== void 0).join("/")).pipe(Result.mapError((error) => addressInvalid({
1585
- ...error,
1586
- field: "location"
1587
- })));
1588
- }
1589
- function addressInvalid(error) {
1590
- return new DocumentValidationFailed({ issues: [validationIssue(error.message, error.field)] });
1591
- }
1592
- function invalid$2(message, field) {
1593
- return Result.fail(new DocumentValidationFailed({ issues: [validationIssue(message, field)] }));
1594
- }
1595
- function validationIssue(message, field) {
1596
- return field === void 0 ? {
1597
- code: "document.invalid",
1598
- message,
1599
- severity: "error"
1600
- } : {
1601
- code: "document.invalid",
1602
- field,
1603
- message,
1604
- severity: "error"
1605
- };
1606
- }
1607
- /** Updates route-owned frontmatter when relocating a page in a slug-addressed collection. */
1608
- const relocateDocumentMarkdown = Effect.fn("DocumentPlanning.relocateMarkdown")(function* (configuration, document, location, markdown) {
1609
- const collection = configuration.types.find((type) => type.collection === document.collection);
1610
- if (!collection || !collection.addressPattern.includes(":slug") || location.kind === "home") return markdown;
1611
- return yield* renderEditorMarkdown({
1612
- ...document.body,
1613
- frontmatter: {
1614
- ...document.body.frontmatter,
1615
- slug: location.slug
1616
- }
1617
- });
1618
- });
1619
-
1620
- //#endregion
1621
- //#region ../server/src/internal/documents/document-state.ts
1622
- const isDocumentCompilationFailed = Schema.is(DocumentCompilationFailed);
1623
- function makeDocumentState(authored, published, snapshot) {
1624
- const activeById = /* @__PURE__ */ new Map();
1625
- for (const source of snapshot.active) {
1626
- if (activeById.has(source.documentId)) throw new Error(`Document "${source.documentId}" appears more than once in the repository snapshot.`);
1627
- activeById.set(source.documentId, source);
1628
- }
1629
- const authoredById = /* @__PURE__ */ new Map();
1630
- for (const document of authored.documents) {
1631
- if (authoredById.has(document.id)) throw new Error(`Compiled document "${document.id}" appears more than once in the authored catalog.`);
1632
- if (!activeById.has(document.id)) throw new Error(`Compiled document "${document.id}" is missing from the repository snapshot.`);
1633
- authoredById.set(document.id, document);
1634
- }
1635
- for (const documentId of activeById.keys()) if (!authoredById.has(documentId)) throw new Error(`Compiled document "${documentId}" is missing from the repository snapshot.`);
1636
- return {
1637
- activeById,
1638
- authored,
1639
- authoredById,
1640
- published,
1641
- snapshot
1642
- };
1643
- }
1644
- const compileDocumentState = Effect.fn("DocumentState.compile")(function* (engine, snapshot, previous) {
1645
- return makeDocumentState(yield* engine.compileAll({
1646
- previous: previous?.authored,
1647
- sources: authoredSources(snapshot)
1648
- }), yield* engine.compileAll({
1649
- previous: previous?.published,
1650
- sources: publishedSources(snapshot)
1651
- }), snapshot);
1652
- });
1653
- function authoredSources(snapshot) {
1654
- return snapshot.active.map((source) => ({
1655
- documentId: source.documentId,
1656
- draft: source.publishedVersion !== source.version,
1657
- markdown: source.markdown,
1658
- sourcePath: source.sourcePath
1659
- }));
1660
- }
1661
- function publishedSources(snapshot) {
1662
- return snapshot.published.map((source) => ({
1663
- documentId: source.documentId,
1664
- draft: false,
1665
- markdown: source.markdown,
1666
- sourcePath: source.sourcePath
1667
- }));
1668
- }
1669
- const compileValidCatalog = Effect.fn("DocumentState.compileValid")(function* (engine, sources, previous) {
1670
- const result = yield* Effect.result(engine.compileAll({
1671
- previous,
1672
- sources
1673
- }));
1674
- if (Result.isFailure(result)) return yield* new DocumentValidationFailed({ issues: compilationIssues(result.failure) });
1675
- const errors = result.success.issues.filter((issue) => issue.severity === "error");
1676
- if (errors.length > 0) return yield* new DocumentValidationFailed({ issues: errors });
1677
- return result.success;
1678
- });
1679
- const validateDocumentSources = Effect.fn("DocumentState.validate")(function* (engine, sources, sourcePath, previous) {
1680
- const result = yield* Effect.result(engine.compileAll({
1681
- previous,
1682
- sources
1683
- }));
1684
- if (Result.isFailure(result)) return DocumentValidationResult.make({
1685
- issues: compilationIssues(result.failure),
1686
- valid: false
1687
- });
1688
- const catalog = result.success;
1689
- const document = catalog.documents.find((candidate) => candidate.source.path === sourcePath);
1690
- const issues = catalog.issues;
1691
- const validation = {
1692
- issues,
1693
- valid: document !== void 0 && !issues.some((issue) => issue.severity === "error")
1694
- };
1695
- return DocumentValidationResult.make(Object.assign(validation, document === void 0 ? void 0 : { document }));
1696
- });
1697
- function authoringEntries(state, query) {
1698
- return state.snapshot.active.flatMap((source) => {
1699
- const document = state.authoredById.get(source.documentId);
1700
- if (!document) throw new Error(`Compiled document "${source.documentId}" is missing from the repository snapshot.`);
1701
- if (query.collection !== void 0 && document.collection !== query.collection) return [];
1702
- if (query.locale !== void 0 && document.locale !== query.locale) return [];
1703
- return [DocumentSummary.make({
1704
- ...browseMetadata(state, source),
1705
- document,
1706
- sourcePath: source.sourcePath,
1707
- version: source.version
1708
- })];
1709
- });
1710
- }
1711
- function deletedEntries(state) {
1712
- return state.snapshot.deleted.map((source) => DeletedDocument.make({
1713
- ...source.title ? { title: source.title } : void 0,
1714
- ...source.path ? { path: source.path } : void 0,
1715
- ...source.trashBatchId ? { trashBatchId: source.trashBatchId } : void 0,
1716
- documentId: source.documentId,
1717
- deletedAt: source.deletedAt,
1718
- sourcePath: source.sourcePath,
1719
- version: source.version
1720
- }));
1721
- }
1722
- function entryById(state, documentId) {
1723
- const source = state.activeById.get(documentId);
1724
- const document = state.authoredById.get(documentId);
1725
- return source && document ? DocumentSummary.make({
1726
- ...browseMetadata(state, source),
1727
- document,
1728
- sourcePath: source.sourcePath,
1729
- version: source.version
1730
- }) : void 0;
1731
- }
1732
- function compilationIssues(error) {
1733
- if (isDocumentCompilationFailed(error)) return error.issues;
1734
- return [{
1735
- code: "markdown.parse",
1736
- message: errorMessage(error.cause),
1737
- severity: "error",
1738
- sourcePath: error.sourcePath
1739
- }];
1740
- }
1741
- function errorMessage(cause) {
1742
- return cause instanceof Error && cause.message ? cause.message : "Markdown could not be parsed.";
1743
- }
1744
- function browseMetadata(state, source) {
1745
- const live = state.published.documents.find((document) => document.id === source.documentId);
1746
- const metadata = {};
1747
- if (source.createdBy) metadata.createdBy = source.createdBy;
1748
- if (source.editedBy) metadata.editedBy = source.editedBy;
1749
- if (source.editedAt) metadata.editedAt = source.editedAt;
1750
- if (source.publishedVersion) metadata.publishedVersion = source.publishedVersion;
1751
- if (live) metadata.publishedPath = live.path;
1752
- return metadata;
1753
- }
1754
-
1755
- //#endregion
1756
- //#region ../server/src/internal/documents/document-tree-mutation.ts
1757
- /** Selects by route boundaries, never by title or a raw prefix such as /about matching /about-us. */
1758
- function subtreeIds(state, documentId) {
1759
- const parent = state.authoredById.get(documentId);
1760
- if (!parent) return [documentId];
1761
- return state.authored.documents.filter((page) => page.id === parent.id || parent.key !== "/" && page.collection === parent.collection && page.locale === parent.locale && page.key.startsWith(`${parent.key}/`)).map((page) => page.id);
1762
- }
1763
- const prepareTreeMutation = Effect.fn("Documents.prepareTreeMutation")(function* (dependencies, state, command) {
1764
- const { configuration, engine } = dependencies;
1765
- const input = command.input;
1766
- const root = command.action === "document.undelete" ? state.snapshot.deleted.find((source) => source.documentId === input.documentId) : state.activeById.get(input.documentId);
1767
- if (!root) return yield* new DocumentNotFound({ documentId: input.documentId });
1768
- if (root.version !== input.expectedVersion) return yield* new DocumentVersionConflict({
1769
- documentId: root.documentId,
1770
- actualVersion: root.version,
1771
- expectedVersion: input.expectedVersion
1772
- });
1773
- let sources;
1774
- if (command.action === "document.undelete") {
1775
- const deleted = state.snapshot.deleted.find((source) => source.documentId === root.documentId);
1776
- const prefix = root.sourcePath.replace(/(?:\/index)?\.(?:md|mdc)$/iu, "");
1777
- sources = state.snapshot.deleted.filter((source) => source.documentId === root.documentId || deleted && deleted.trashBatchId && source.trashBatchId === deleted.trashBatchId && source.sourcePath.startsWith(`${prefix}/`));
1778
- } else {
1779
- const ids = new Set(command.action === "document.duplicate" && !command.input.includeChildren ? [root.documentId] : subtreeIds(state, root.documentId));
1780
- sources = state.snapshot.active.filter((source) => ids.has(source.documentId));
1781
- }
1782
- sources = [root, ...sources.filter((source) => source.documentId !== root.documentId)];
1783
- const plans = [];
1784
- const parent = state.authoredById.get(root.documentId);
1785
- for (const source of sources) {
1786
- if (command.action === "document.remove" || command.action === "document.undelete") {
1787
- const page = state.authoredById.get(source.documentId);
1788
- plans.push({
1789
- ...command.action === "document.remove" ? {
1790
- trashBatchId: root.version,
1791
- title: page?.title,
1792
- path: page?.path
1793
- } : void 0,
1794
- action: command.action,
1795
- documentId: source.documentId,
1796
- expectedVersion: source.version,
1797
- markdown: source.markdown,
1798
- sourcePath: source.sourcePath
1799
- });
1800
- continue;
1801
- }
1802
- const page = state.authoredById.get(source.documentId);
1803
- const collection = configuration.types.find((type) => type.collection === page?.collection);
1804
- if (!page || !parent || !collection) return yield* new DocumentNotFound({ documentId: source.documentId });
1805
- if (command.action === "document.duplicate" && !collection.canCreate) return yield* invalid$1("This collection does not allow new pages.");
1806
- const destination = command.input.location;
1807
- if (parent.key === "/" && command.action === "document.move") return yield* invalid$1("The home page cannot be moved.");
1808
- const rootRelative = documentRelativePath(collection, {
1809
- document: parent,
1810
- sourcePath: root.sourcePath
1811
- }) ?? "";
1812
- const nextRelative = destination.kind === "home" ? "" : [destination.parent, destination.slug].filter(Boolean).join("/");
1813
- if (rootRelative && (nextRelative === rootRelative || nextRelative.startsWith(`${rootRelative}/`))) return yield* invalid$1("Choose a destination outside this page and its subpages.");
1814
- const relative = documentRelativePath(collection, {
1815
- document: page,
1816
- sourcePath: source.sourcePath
1817
- }) ?? "";
1818
- const segments = `${nextRelative}${source.documentId === root.documentId ? "" : relative.slice(rootRelative.length)}`.split("/").filter(Boolean);
1819
- const slug = segments.pop();
1820
- const location = slug ? Object.assign({
1821
- kind: "entry",
1822
- slug: yield* decodeSlug(slug)
1823
- }, segments.length ? { parent: yield* decodeParent(segments.join("/")) } : void 0) : { kind: "home" };
1824
- const sourcePath = yield* Effect.fromResult(planDocumentMove(configuration, page, location));
1825
- const markdown = yield* relocateDocumentMarkdown(configuration, page, location, source.markdown).pipe(Effect.mapError(() => invalid$1("The page could not be copied to its new URL.")));
1826
- if (command.action === "document.duplicate") {
1827
- const documentId = yield* command.nextDocumentId;
1828
- plans.push({
1829
- action: "document.create",
1830
- documentId,
1831
- markdown,
1832
- sourcePath
1833
- });
1834
- } else plans.push({
1835
- action: "document.move",
1836
- documentId: source.documentId,
1837
- expectedVersion: source.version,
1838
- markdown,
1839
- sourcePath,
1840
- publishedVersion: source.publishedVersion
1841
- });
1842
- }
1843
- const changed = new Set(plans.map((plan) => plan.documentId));
1844
- const nextSources = authoredSources(state.snapshot).filter((source) => !changed.has(source.documentId));
1845
- if (command.action !== "document.remove") for (const plan of plans) {
1846
- if (nextSources.some((source) => source.sourcePath === plan.sourcePath)) return yield* new DocumentPathConflict({ sourcePath: plan.sourcePath });
1847
- nextSources.push({
1848
- documentId: plan.documentId,
1849
- draft: true,
1850
- markdown: plan.markdown,
1851
- sourcePath: plan.sourcePath
1852
- });
1853
- }
1854
- if (command.action === "document.move" || command.action === "document.duplicate") {
1855
- const preliminary = yield* engine.compileAll({
1856
- sources: nextSources,
1857
- previous: state.authored
1858
- });
1859
- const destinations = /* @__PURE__ */ new Map();
1860
- for (let index = 0; index < sources.length; index++) {
1861
- const original = sources[index];
1862
- const plan = plans[index];
1863
- const oldPage = original && state.authoredById.get(original.documentId);
1864
- const newPage = plan && preliminary.documents.find((document) => document.id === plan.documentId);
1865
- if (oldPage && newPage) destinations.set(oldPage.path, newPage.path);
1866
- }
1867
- const originals = command.action === "document.move" ? state.snapshot.active : sources;
1868
- for (const source of originals) {
1869
- const page = state.authoredById.get(source.documentId);
1870
- if (!page) continue;
1871
- const body = rewriteDocumentRoutes(page, destinations, dependencies.components);
1872
- if (!body) continue;
1873
- const index = command.action === "document.duplicate" ? sources.findIndex((item) => item.documentId === source.documentId) : plans.findIndex((plan) => plan.documentId === source.documentId);
1874
- const existing = index >= 0 ? plans[index] : void 0;
1875
- const projected = existing && preliminary.documents.find((document) => document.id === existing.documentId);
1876
- const markdown = yield* renderEditorMarkdown(Object.assign({}, body, { frontmatter: projected?.body.frontmatter ?? body.frontmatter })).pipe(Effect.mapError(() => invalid$1("Page links could not be updated.")));
1877
- if (existing) plans[index] = {
1878
- ...existing,
1879
- markdown
1880
- };
1881
- else plans.push({
1882
- action: "document.update",
1883
- documentId: source.documentId,
1884
- expectedVersion: source.version,
1885
- sourcePath: source.sourcePath,
1886
- markdown,
1887
- publishedVersion: source.publishedVersion
1888
- });
1889
- }
1890
- for (const plan of plans) {
1891
- const index = nextSources.findIndex((source) => source.documentId === plan.documentId);
1892
- const updated = {
1893
- documentId: plan.documentId,
1894
- sourcePath: plan.sourcePath,
1895
- markdown: plan.markdown,
1896
- draft: true
1897
- };
1898
- if (index >= 0) nextSources[index] = updated;
1899
- else nextSources.push(updated);
1900
- }
1901
- }
1902
- const first = plans[0];
1903
- if (!first) return yield* Effect.die(/* @__PURE__ */ new Error("Empty subtree mutation."));
1904
- return {
1905
- authored: yield* compileValidCatalog(engine, nextSources, state.authored),
1906
- published: command.action === "document.remove" ? yield* compileValidCatalog(engine, publishedSources(state.snapshot).filter((source) => !changed.has(source.documentId)), state.published) : state.published,
1907
- commitPlan: first,
1908
- additionalPlans: plans.slice(1)
1909
- };
1910
- });
1911
- const decodeSlug = (value) => Schema.decodeEffect(Slug)(value).pipe(Effect.mapError(() => invalid$1("Invalid URL ending.")));
1912
- const decodeParent = (value) => Schema.decodeEffect(RelativeDocumentPath)(value).pipe(Effect.mapError(() => invalid$1("Invalid parent URL.")));
1913
- function invalid$1(message) {
1914
- return new DocumentValidationFailed({ issues: [{
1915
- code: "document.invalid",
1916
- severity: "error",
1917
- message
1918
- }] });
1919
- }
1920
-
1921
- //#endregion
1922
- //#region ../server/src/internal/documents/document-lifecycle.ts
1923
- const PreparedDocumentMutation = Data.taggedEnum();
1924
- const prepareDocumentMutation = Effect.fn("DocumentLifecycle.prepare")(function* (dependencies, state, command) {
1925
- const { engine, repository } = dependencies;
1926
- if (command.action === "document.move" || command.action === "document.remove" || command.action === "document.undelete" || command.action === "document.duplicate") return PreparedDocumentMutation.Commit(yield* prepareTreeMutation(dependencies, state, command));
1927
- const authored = authoredSources(state.snapshot);
1928
- const published = publishedSources(state.snapshot);
1929
- switch (command.action) {
1930
- case "document.create": {
1931
- yield* ensurePathAvailable(state, command.sourcePath);
1932
- const nextAuthored = [...authored, {
1933
- documentId: command.documentId,
1934
- draft: true,
1935
- markdown: command.markdown,
1936
- sourcePath: command.sourcePath
1937
- }];
1938
- return PreparedDocumentMutation.Commit({
1939
- authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
1940
- published: state.published,
1941
- commitPlan: {
1942
- action: command.action,
1943
- documentId: command.documentId,
1944
- markdown: command.markdown,
1945
- sourcePath: command.sourcePath
1946
- }
1947
- });
1948
- }
1949
- case "document.update": {
1950
- const current = yield* requireActive(state, command.input.documentId);
1951
- yield* requireVersion(current, command.input.expectedVersion);
1952
- const nextAuthored = replaceSource(authored, {
1953
- documentId: current.documentId,
1954
- draft: true,
1955
- markdown: command.input.markdown,
1956
- sourcePath: current.sourcePath
1957
- });
1958
- return PreparedDocumentMutation.Commit({
1959
- authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
1960
- published: state.published,
1961
- commitPlan: {
1962
- action: command.action,
1963
- documentId: current.documentId,
1964
- expectedVersion: command.input.expectedVersion,
1965
- markdown: command.input.markdown,
1966
- publishedVersion: current.publishedVersion,
1967
- sourcePath: current.sourcePath
1968
- }
1969
- });
1970
- }
1971
- case "document.restore": {
1972
- const current = yield* requireActive(state, command.input.documentId);
1973
- yield* requireVersion(current, command.input.expectedVersion);
1974
- const historical = yield* repository.revision(current.documentId, command.input.revisionId);
1975
- const nextAuthored = replaceSource(authored, {
1976
- documentId: current.documentId,
1977
- draft: true,
1978
- markdown: historical.markdown,
1979
- sourcePath: current.sourcePath
1980
- });
1981
- return PreparedDocumentMutation.Commit({
1982
- authored: yield* compileValidCatalog(engine, nextAuthored, state.authored),
1983
- published: state.published,
1984
- commitPlan: {
1985
- action: command.action,
1986
- documentId: current.documentId,
1987
- expectedVersion: command.input.expectedVersion,
1988
- markdown: historical.markdown,
1989
- publishedVersion: current.publishedVersion,
1990
- revisionId: command.input.revisionId,
1991
- sourcePath: current.sourcePath
1992
- }
1993
- });
1994
- }
1995
- case "document.unpublish": {
1996
- const current = yield* requireActive(state, command.input.documentId);
1997
- yield* requireVersion(current, command.input.expectedVersion);
1998
- return PreparedDocumentMutation.Commit({
1999
- authored: yield* compileValidCatalog(engine, replaceSource(authored, {
2000
- documentId: current.documentId,
2001
- draft: true,
2002
- markdown: current.markdown,
2003
- sourcePath: current.sourcePath
2004
- }), state.authored),
2005
- published: yield* compileValidCatalog(engine, withoutDocument(published, current.documentId), state.published),
2006
- commitPlan: {
2007
- action: command.action,
2008
- documentId: current.documentId,
2009
- expectedVersion: current.version,
2010
- markdown: current.markdown,
2011
- sourcePath: current.sourcePath,
2012
- revisionId: current.revisionId
2013
- }
2014
- });
2015
- }
2016
- case "document.publish": {
2017
- const current = yield* requireActive(state, command.input.documentId);
2018
- yield* requireVersion(current, command.input.expectedVersion);
2019
- if (current.publishedVersion === current.version) {
2020
- const entry = entryById(state, current.documentId);
2021
- const result = {
2022
- action: "document.publish",
2023
- affectedDocumentIds: [current.documentId],
2024
- documentId: current.documentId,
2025
- generation: state.snapshot.generation,
2026
- revisionId: current.revisionId,
2027
- sourcePath: current.sourcePath,
2028
- version: current.version
2029
- };
2030
- return PreparedDocumentMutation.Unchanged({ result: DocumentMutationResult.make(Object.assign(result, entry === void 0 ? void 0 : { entry })) });
2031
- }
2032
- const source = {
2033
- documentId: current.documentId,
2034
- draft: false,
2035
- markdown: current.markdown,
2036
- sourcePath: current.sourcePath
2037
- };
2038
- return PreparedDocumentMutation.Commit({
2039
- authored: yield* compileValidCatalog(engine, replaceSource(authored, source), state.authored),
2040
- published: yield* compileValidCatalog(engine, replaceSource(published, source), state.published),
2041
- commitPlan: {
2042
- action: command.action,
2043
- documentId: current.documentId,
2044
- expectedVersion: command.input.expectedVersion,
2045
- markdown: current.markdown,
2046
- revisionId: current.revisionId,
2047
- sourcePath: current.sourcePath
2048
- }
2049
- });
2050
- }
2051
- }
2052
- });
2053
- const validateDocumentUpdate = Effect.fn("DocumentLifecycle.validateUpdate")(function* (engine, state, input) {
2054
- const current = yield* requireActive(state, input.documentId);
2055
- yield* requireVersion(current, input.expectedVersion);
2056
- const source = {
2057
- documentId: current.documentId,
2058
- draft: true,
2059
- markdown: input.markdown,
2060
- sourcePath: current.sourcePath
2061
- };
2062
- return yield* validateDocumentSources(engine, replaceSource(authoredSources(state.snapshot), source), current.sourcePath, state.authored);
2063
- });
2064
- function requireActive(state, documentId) {
2065
- const source = state.activeById.get(documentId);
2066
- return source ? Effect.succeed(source) : Effect.fail(new DocumentNotFound({ documentId }));
2067
- }
2068
- function requireVersion(source, expectedVersion) {
2069
- return source.version === expectedVersion ? Effect.void : Effect.fail(new DocumentVersionConflict({
2070
- actualVersion: source.version,
2071
- documentId: source.documentId,
2072
- expectedVersion
1318
+ const handler = AuthenticationHandler.of({ handle: rejectExternalAdminRoutes(auth.handler, basePath) });
1319
+ return Context.empty().pipe(Context.add(Authentication, identity.authentication), Context.add(IdentityAdministration, identity.administration), Context.add(IdentityRecovery, recovery.users), Context.add(InitialIdentity, initialization), Context.add(InitialIdentityRecovery, recovery.initial), Context.add(AuthenticationHandler, handler));
2073
1320
  }));
2074
1321
  }
2075
- function ensurePathAvailable(state, sourcePath, exceptDocumentId) {
2076
- return state.snapshot.active.some((source) => source.sourcePath === sourcePath && source.documentId !== exceptDocumentId) ? Effect.fail(new DocumentPathConflict({ sourcePath })) : Effect.void;
2077
- }
2078
- function replaceSource(sources, source) {
2079
- return [...sources.filter((candidate) => candidate.documentId !== source.documentId), source];
2080
- }
2081
- function withoutDocument(sources, documentId) {
2082
- return sources.filter((source) => source.documentId !== documentId);
2083
- }
2084
-
2085
- //#endregion
2086
- //#region ../server/src/internal/documents/document-media-references.ts
2087
- /** Projects the durable authored/published media-reference union without reparsing documents. */
2088
- function projectMediaReferences(authored, published, documentId) {
2089
- const references = [...referencesFromCatalog(authored, documentId), ...referencesFromCatalog(published, documentId)];
2090
- return [...new Map(references.map((reference) => [referenceIdentity(reference), reference])).values()];
2091
- }
2092
- function referencesFromCatalog(catalog, documentId) {
2093
- return catalog.references.all.filter((reference) => reference.target.kind === "media" && (documentId === void 0 || reference.source.documentId === documentId)).map(toMediaReference);
2094
- }
2095
- function toMediaReference(reference) {
2096
- if (reference.target.kind !== "media") throw new Error("Expected a media reference.");
2097
- return MediaReference.make({
2098
- documentId: reference.source.documentId,
2099
- nodePath: reference.nodePath,
2100
- objectKey: MediaObjectKey.make(reference.target.key),
2101
- path: reference.source.path,
2102
- sourcePath: reference.source.sourcePath
2103
- });
2104
- }
2105
- function referenceIdentity(reference) {
2106
- return `${reference.documentId}:${reference.sourcePath}:${reference.nodePath.join(".")}:${reference.objectKey}`;
2107
- }
2108
1322
 
2109
1323
  //#endregion
2110
- //#region ../server/src/internal/documents/document-import.ts
2111
- /** Imports configured or migrated Sources through the normal Document lifecycle. */
2112
- const importDocumentSources = Effect.fn("Documents.importSources")(function* (dependencies, input, activity) {
2113
- const report = {
2114
- created: 0,
2115
- preserved: 0,
2116
- published: 0,
2117
- unchanged: 0,
2118
- updated: 0
2119
- };
2120
- for (const source of input.sources) {
2121
- const plan = planDocumentImport(yield* dependencies.current(source.sourcePath), source, input.mode);
2122
- switch (plan.kind) {
2123
- case "create": {
2124
- const created = yield* dependencies.runMutation({
2125
- action: "document.create",
2126
- documentId: yield* dependencies.nextDocumentId,
2127
- markdown: source.markdown,
2128
- sourcePath: source.sourcePath
2129
- }, activity, input.reason);
2130
- report.created += 1;
2131
- if (plan.publish) {
2132
- yield* publish(dependencies, created, input, activity);
2133
- report.published += 1;
2134
- }
2135
- break;
2136
- }
2137
- case "preserve":
2138
- case "unchanged":
2139
- report[plan.kind === "preserve" ? "preserved" : "unchanged"] += 1;
2140
- break;
2141
- case "publish":
2142
- yield* publish(dependencies, plan.current, input, activity);
2143
- report.published += 1;
2144
- break;
2145
- case "update": {
2146
- const updated = yield* dependencies.runMutation({
2147
- action: "document.update",
2148
- input: {
2149
- documentId: plan.current.documentId,
2150
- expectedVersion: plan.current.version,
2151
- markdown: source.markdown,
2152
- reason: input.reason
1324
+ //#region ../platform-node/src/internal/http/form-ingress.ts
1325
+ function handleFormRequest(request, handle) {
1326
+ return Effect.runPromise(Effect.gen(function* () {
1327
+ const ingress = yield* Effect.gen(function* () {
1328
+ const origin = request.headers.get("origin");
1329
+ if (origin && origin !== new URL(request.url).origin) return Response.json({ error: "Origin rejected." }, { status: 403 });
1330
+ if (request.method !== "POST") return new Response(null, { status: 405 });
1331
+ if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response(null, { status: 415 });
1332
+ const reader = request.body?.getReader();
1333
+ if (!reader) return new Response(null, { status: 400 });
1334
+ const chunks = [];
1335
+ let size = 0;
1336
+ try {
1337
+ while (true) {
1338
+ const chunk = yield* Effect.tryPromise(() => reader.read());
1339
+ if (chunk.done) break;
1340
+ size += chunk.value.byteLength;
1341
+ if (size > 131072) {
1342
+ yield* Effect.tryPromise(() => reader.cancel()).pipe(Effect.ignore);
1343
+ return new Response(null, { status: 413 });
2153
1344
  }
2154
- }, activity, input.reason);
2155
- report.updated += 1;
2156
- if (plan.publish) {
2157
- yield* publish(dependencies, updated, input, activity);
2158
- report.published += 1;
1345
+ chunks.push(chunk.value);
2159
1346
  }
2160
- break;
1347
+ } finally {
1348
+ reader.releaseLock();
2161
1349
  }
2162
- }
2163
- }
2164
- return DocumentImportReport.make(report);
2165
- });
2166
- /** Plans one deterministic import without performing persistence or generating identities. */
2167
- function planDocumentImport(current, source, mode) {
2168
- if (!current) return {
2169
- kind: "create",
2170
- publish: source.published
2171
- };
2172
- if (current.markdown !== source.markdown) return mode === "overwrite" ? {
2173
- current,
2174
- kind: "update",
2175
- publish: source.published
2176
- } : { kind: "preserve" };
2177
- if (source.published && current.publishedVersion !== current.version) return {
2178
- current,
2179
- kind: "publish"
2180
- };
2181
- return { kind: "unchanged" };
2182
- }
2183
- function publish(dependencies, source, input, activity) {
2184
- return dependencies.runMutation({
2185
- action: "document.publish",
2186
- input: {
2187
- documentId: source.documentId,
2188
- expectedVersion: source.version,
2189
- reason: input.reason
2190
- }
2191
- }, activity, input.reason);
1350
+ const body = new Uint8Array(size);
1351
+ let offset = 0;
1352
+ for (const chunk of chunks) {
1353
+ body.set(chunk, offset);
1354
+ offset += chunk.byteLength;
1355
+ }
1356
+ return body;
1357
+ }).pipe(Effect.orElseSucceed(() => new Response(null, { status: 400 })));
1358
+ if (ingress instanceof Response) return ingress;
1359
+ return yield* Effect.promise(() => handle(new Request(request.url, {
1360
+ method: "POST",
1361
+ headers: request.headers,
1362
+ body: ingress,
1363
+ signal: request.signal
1364
+ })));
1365
+ }).pipe(Effect.map((response) => {
1366
+ response.headers.set("cache-control", "no-store");
1367
+ return response;
1368
+ })));
2192
1369
  }
2193
1370
 
2194
1371
  //#endregion
2195
- //#region ../server/src/internal/documents/document-store.ts
2196
- var DocumentStore = class extends Context.Service()("@internal/server/DocumentStore") {};
1372
+ //#region ../server/src/Readiness.ts
1373
+ var ReadinessFailed = class extends Schema.TaggedError()("Cms.ReadinessFailed", { cause: Schema.Defect() }) {};
1374
+ var Readiness = class extends Context.Service()("@internal/server/Readiness") {};
1375
+ /** A stale unresolved external change makes the installation unavailable until reconciliation resolves it. */
1376
+ const readinessLayer = Layer.effect(Readiness, Effect.gen(function* () {
1377
+ const journal = yield* ChangeJournal;
1378
+ const check = Effect.gen(function* () {
1379
+ const now = yield* DateTime.now;
1380
+ const pending = yield* journal.pending({
1381
+ before: DateTime.subtractDuration(now, "30 seconds"),
1382
+ limit: 1
1383
+ }).pipe(Effect.mapError((cause) => new ReadinessFailed({ cause })));
1384
+ const oldest = pending.changes[0];
1385
+ if (pending.total === 0) return;
1386
+ if (oldest === void 0) return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("The pending change journal returned no change.") });
1387
+ yield* Effect.logWarning("CMS readiness found a stale pending change.", {
1388
+ action: oldest.action,
1389
+ ageMillis: DateTime.toEpochMillis(now) - DateTime.toEpochMillis(oldest.updatedAt),
1390
+ changeId: oldest.id,
1391
+ phase: "pending"
1392
+ });
1393
+ return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("A stale change remains pending.") });
1394
+ }).pipe(Effect.withSpan("Installation.readiness"));
1395
+ return Readiness.of({ check });
1396
+ }));
2197
1397
 
2198
1398
  //#endregion
2199
- //#region ../server/src/internal/documents/document-failures.ts
2200
- var DocumentPersistenceError = class extends Schema.TaggedError()("Documents.PersistenceError", {
1399
+ //#region ../server/src/internal/working-documents/working-document-model.ts
1400
+ const WorkingDocumentGenerationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("WorkingDocumentGenerationId"));
1401
+ const WorkingDocumentUpdateSequence = Schema.Int.check(Schema.isGreaterThan(0)).pipe(Schema.brand("WorkingDocumentUpdateSequence"));
1402
+ const WorkingDocumentHighWatermark = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.brand("WorkingDocumentHighWatermark"));
1403
+ var WorkingDocumentPersistenceError = class extends Schema.TaggedError()("WorkingDocuments.PersistenceError", {
2201
1404
  operation: Schema.NonEmptyString,
2202
1405
  cause: Schema.Defect()
2203
1406
  }) {};
2204
- var DocumentGenerationConflict = class extends Schema.TaggedError()("Documents.GenerationConflict", {
2205
- expectedGeneration: DocumentGeneration,
2206
- actualGeneration: DocumentGeneration
1407
+ var WorkingDocumentSuperseded = class extends Schema.TaggedError()("WorkingDocuments.Superseded", {
1408
+ documentId: DocumentId,
1409
+ expectedGeneration: WorkingDocumentGenerationId,
1410
+ actualGeneration: Schema.optionalKey(WorkingDocumentGenerationId)
1411
+ }) {};
1412
+ var WorkingDocumentConflict = class extends Schema.TaggedError()("WorkingDocuments.Conflict", {
1413
+ documentId: DocumentId,
1414
+ operation: Schema.NonEmptyString,
1415
+ message: Schema.NonEmptyString
2207
1416
  }) {};
2208
1417
 
2209
1418
  //#endregion
2210
- //#region ../server/src/internal/documents/document-history.ts
2211
- const CursorPayload$1 = Schema.Struct({
2212
- createdAt: Schema.DateTimeUtcFromString,
2213
- revisionId: RevisionId
2214
- });
2215
- const CursorJson$1 = Schema.fromJsonString(CursorPayload$1);
2216
- const cursors$1 = cursorCodec(CursorJson$1, (value) => DocumentHistoryCursor.make(value), () => new DocumentHistoryInvalidCursor());
2217
- function documentHistoryCapability(accessControl, repository) {
2218
- const requireHistory = (context, documentId) => accessControl.require(context, {
2219
- action: "document.history",
2220
- resource: {
2221
- documentId,
2222
- kind: "document"
2223
- }
2224
- });
2225
- const read = Effect.fn("Documents.history.read")(function* (context, documentId, revisionId) {
2226
- yield* requireHistory(context, documentId);
2227
- return yield* repository.revision(documentId, revisionId);
2228
- });
2229
- const list = Effect.fn("Documents.history.list")(function* (context, query) {
2230
- yield* requireHistory(context, query.documentId);
2231
- const cursor = query.cursor ? yield* Effect.fromResult(cursors$1.decode(query.cursor)) : void 0;
2232
- const page = yield* repository.revisions({
2233
- cursor,
2234
- documentId: query.documentId,
2235
- limit: query.limit ?? 25
2236
- });
2237
- const output = {
2238
- revisions: page.revisions.map(({ markdown: _markdown, ...revision }) => revision),
2239
- total: page.total
2240
- };
2241
- return page.next ? DocumentRevisionPage.make({
2242
- ...output,
2243
- nextCursor: cursors$1.encode(page.next)
2244
- }) : DocumentRevisionPage.make(output);
2245
- });
2246
- return {
2247
- compare: Effect.fn("Documents.history.compare")(function* (context, input) {
2248
- yield* requireHistory(context, input.documentId);
2249
- const from = yield* repository.revision(input.documentId, input.fromRevisionId);
2250
- const to = yield* repository.revision(input.documentId, input.toRevisionId);
2251
- const changes = yield* compareLines(from.markdown, to.markdown);
2252
- return DocumentRevisionComparison.make({
2253
- changes,
2254
- from,
2255
- to
2256
- });
2257
- }),
2258
- list,
2259
- read
2260
- };
2261
- }
2262
- const MAXIMUM_COMPARISON_LINES = 1e4;
2263
- const MAXIMUM_EDIT_LENGTH = 2e3;
2264
- const comparisonTooLarge = () => new DocumentHistoryComparisonTooLarge({
2265
- maximumEditLength: MAXIMUM_EDIT_LENGTH,
2266
- maximumLines: MAXIMUM_COMPARISON_LINES
2267
- });
2268
- const compareLines = Effect.fn("Documents.history.compareLines")(function* (beforeMarkdown, afterMarkdown) {
2269
- const before = beforeMarkdown.split("\n");
2270
- const after = afterMarkdown.split("\n");
2271
- if (before.length > MAXIMUM_COMPARISON_LINES || after.length > MAXIMUM_COMPARISON_LINES) return yield* comparisonTooLarge();
2272
- const diff = diffArrays(before, after, {
2273
- maxEditLength: MAXIMUM_EDIT_LENGTH,
2274
- oneChangePerToken: true,
2275
- timeout: 100
2276
- });
2277
- if (!diff) return yield* comparisonTooLarge();
2278
- const changes = [];
2279
- let beforeLine = 1;
2280
- let afterLine = 1;
2281
- for (const change of diff) for (const line of change.value) if (change.removed) {
2282
- changes.push({
2283
- beforeLine,
2284
- kind: "removed",
2285
- line
2286
- });
2287
- beforeLine += 1;
2288
- } else if (change.added) {
2289
- changes.push({
2290
- afterLine,
2291
- kind: "added",
2292
- line
2293
- });
2294
- afterLine += 1;
2295
- } else {
2296
- changes.push({
2297
- afterLine,
2298
- beforeLine,
2299
- kind: "equal",
2300
- line
2301
- });
2302
- beforeLine += 1;
2303
- afterLine += 1;
2304
- }
2305
- return changes;
1419
+ //#region ../server/src/internal/working-documents/working-document.ts
1420
+ const WorkingDocumentRef = Schema.Struct({
1421
+ documentId: DocumentId,
1422
+ generationId: WorkingDocumentGenerationId
2306
1423
  });
2307
-
2308
- //#endregion
2309
- //#region ../server/src/internal/documents/document-delivery.ts
2310
- function documentDelivery(configuration, catalog) {
2311
- const read = Effect.fn("Documents.delivery.read")(function* (lookup) {
2312
- const document = findDocument(yield* catalog, lookup);
2313
- return Option.isSome(document) ? document.value : yield* new PublishedDocumentNotFound({ lookup });
2314
- });
2315
- const query = Effect.fn("Documents.delivery.query")(function* (input) {
2316
- return queryDocuments((yield* catalog).documents, input, {
2317
- limit: input.limit,
2318
- offset: input.offset ?? 0
2319
- });
2320
- });
2321
- const navigation = Effect.fn("Documents.delivery.navigation")(function* (input) {
2322
- return createNavigation((yield* catalog).documents, input, configuration);
2323
- });
2324
- const surround = Effect.fn("Documents.delivery.surround")(function* (lookup, input) {
2325
- const current = yield* catalog;
2326
- const document = findDocument(current, lookup);
2327
- if (Option.isNone(document)) return yield* new PublishedDocumentNotFound({ lookup });
2328
- const surroundQuery = Object.assign({}, { collection: input.collection ?? document.value.collection }, input.locale === void 0 && document.value.locale === void 0 ? void 0 : { locale: input.locale ?? document.value.locale }, input.where === void 0 ? void 0 : { where: input.where }, input.sort === void 0 ? void 0 : { sort: input.sort });
2329
- const page = queryDocuments(current.documents, surroundQuery);
2330
- const index = page.items.findIndex((candidate) => candidate.id === document.value.id);
2331
- return DocumentSurround.make({
2332
- previous: index > 0 ? page.items[index - 1] ?? null : null,
2333
- next: index >= 0 ? page.items[index + 1] ?? null : null
2334
- });
2335
- });
2336
- return {
2337
- diagnostics: catalog.pipe(Effect.map((current) => current.issues)),
2338
- navigation,
2339
- query,
2340
- read,
2341
- surround
2342
- };
2343
- }
2344
- function findDocument(catalog, lookup) {
2345
- switch (lookup.kind) {
2346
- case "id": return Option.fromNullishOr(catalog.documents.find((document) => document.id === lookup.documentId));
2347
- case "path": return catalog.findByPath(lookup.path);
2348
- case "key": return catalog.findByKey(lookup.key, lookup.locale).pipe(Option.filter((document) => lookup.collection === void 0 || document.collection === lookup.collection));
2349
- }
2350
- }
2351
- function queryDocuments(documents, query, page) {
2352
- const filtered = documents.filter((document) => {
2353
- if (query.collection !== void 0 && document.collection !== query.collection) return false;
2354
- if (query.locale !== void 0 && document.locale !== query.locale) return false;
2355
- return Object.entries(query.where ?? {}).every(([field, expected]) => matchesValue(documentField(document, field), expected));
2356
- });
2357
- const sorted = query.sort && query.sort.length > 0 ? sortDocuments(filtered, query.sort) : filtered;
2358
- const items = page ? sorted.slice(page.offset, page.offset + page.limit) : sorted;
2359
- return PublishedDocumentPage.make({
2360
- items,
2361
- total: filtered.length
2362
- });
2363
- }
2364
- function createNavigation(documents, query, configuration) {
2365
- const locale = query.locale ?? configuration.locales?.defaultLocale;
2366
- const candidates = documents.filter((document) => (query.collection === void 0 || document.collection === query.collection) && (locale === void 0 || document.locale === locale) && document.data.navigation !== false);
2367
- const roots = [];
2368
- const itemsByKey = /* @__PURE__ */ new Map();
2369
- const orderByItem = /* @__PURE__ */ new Map();
2370
- for (const document of candidates) {
2371
- if (document.key === "/") {
2372
- const item = navigationItem(document);
2373
- roots.push(item);
2374
- orderByItem.set(item, Number.NEGATIVE_INFINITY);
2375
- continue;
2376
- }
2377
- const segments = document.key.slice(1).split("/");
2378
- let parent;
2379
- for (let index = 0; index < segments.length; index += 1) {
2380
- const key = PublicPath.make(`/${segments.slice(0, index + 1).join("/")}`);
2381
- let item = itemsByKey.get(key);
2382
- if (!item) {
2383
- item = {
2384
- children: [],
2385
- key,
2386
- title: titleFromSegment(segments[index] ?? "")
2387
- };
2388
- itemsByKey.set(key, item);
2389
- if (parent) parent.children.push(item);
2390
- else roots.push(item);
2391
- }
2392
- if (index === segments.length - 1) {
2393
- const resolved = navigationItem(document);
2394
- item.title = resolved.title;
2395
- item.path = resolved.path;
2396
- if (resolved.locale !== void 0) item.locale = resolved.locale;
2397
- else delete item.locale;
2398
- item.documentId = resolved.documentId;
2399
- orderByItem.set(item, navigationOrder(document));
2400
- }
2401
- parent = item;
2402
- }
2403
- }
2404
- sortNavigation(roots, orderByItem);
2405
- return roots.map((item) => DocumentNavigationItem.make(item));
2406
- }
2407
- function navigationItem(document) {
2408
- const navigation = document.data.navigation;
2409
- const configuredTitle = Schema.is(Schema.JsonObject)(navigation) ? navigation.title : void 0;
2410
- return Object.assign({
2411
- children: [],
2412
- documentId: document.id,
2413
- key: document.key,
2414
- path: document.path,
2415
- title: Schema.is(Schema.String)(configuredTitle) && configuredTitle.trim() ? configuredTitle.trim() : document.title
2416
- }, document.locale === void 0 ? void 0 : { locale: document.locale });
2417
- }
2418
- function navigationOrder(document) {
2419
- const navigation = document.data.navigation;
2420
- const order = (Schema.is(Schema.JsonObject)(navigation) ? navigation.order : void 0) ?? document.data.order;
2421
- return Schema.is(Schema.Finite)(order) ? order : Number.POSITIVE_INFINITY;
2422
- }
2423
- function sortNavigation(items, orderByItem) {
2424
- items.sort((left, right) => {
2425
- return (orderByItem.get(left) ?? Number.POSITIVE_INFINITY) - (orderByItem.get(right) ?? Number.POSITIVE_INFINITY) || left.title.localeCompare(right.title);
2426
- });
2427
- items.forEach((item) => sortNavigation(item.children, orderByItem));
2428
- }
2429
- function documentField(document, field) {
2430
- switch (field) {
2431
- case "collection": return document.collection;
2432
- case "description": return document.description;
2433
- case "id": return document.id;
2434
- case "key": return document.key;
2435
- case "locale": return document.locale;
2436
- case "path": return document.path;
2437
- case "source.path": return document.source.path;
2438
- case "title": return document.title;
2439
- default: return nestedValue(document.data, field.startsWith("data.") ? field.slice(5) : field);
2440
- }
2441
- }
2442
- function nestedValue(value, path) {
2443
- return path.split(".").reduce((current, segment) => {
2444
- if (!Schema.is(Schema.JsonObject)(current)) return void 0;
2445
- return current[segment];
2446
- }, value);
2447
- }
2448
- function matchesValue(actual, expected) {
2449
- const expectedValues = Array.isArray(expected) ? expected : [expected];
2450
- return (Array.isArray(actual) ? actual : [actual]).some((value) => Schema.is(DocumentQueryScalar)(value) && expectedValues.includes(value));
2451
- }
2452
- function sortDocuments(documents, sorts) {
2453
- return documents.toSorted((left, right) => {
2454
- for (const sort of sorts) {
2455
- const comparison = compareValues(documentField(left, sort.field), documentField(right, sort.field));
2456
- if (comparison !== 0) return sort.direction === "desc" ? -comparison : comparison;
2457
- }
2458
- return 0;
2459
- });
2460
- }
2461
- function compareValues(left, right) {
2462
- if (left === right) return 0;
2463
- if (left === void 0 || left === null) return 1;
2464
- if (right === void 0 || right === null) return -1;
2465
- if (Schema.is(Schema.Finite)(left) && Schema.is(Schema.Finite)(right)) return left - right;
2466
- const leftText = Schema.is(Schema.String)(left) ? left : JSON.stringify(left) ?? "";
2467
- const rightText = Schema.is(Schema.String)(right) ? right : JSON.stringify(right) ?? "";
2468
- return leftText.localeCompare(rightText);
2469
- }
2470
- function titleFromSegment(segment) {
2471
- return segment.replace(/[-_]+/gu, " ").replace(/^./u, (character) => character.toUpperCase());
2472
- }
2473
-
2474
- //#endregion
2475
- //#region ../server/src/internal/documents/documents.ts
2476
- const isDocumentGenerationConflict$2 = Schema.is(DocumentGenerationConflict);
2477
- const isDocumentMediaUnavailable$1 = Schema.is(DocumentMediaUnavailable);
2478
- const CONTENT_MUTATION_BUFFER_CAPACITY = 16;
2479
- var Documents = class extends Context.Service()("@internal/server/Documents") {};
2480
- var DocumentPreviews = class extends Context.Service()("@internal/server/DocumentPreviews") {};
2481
- var PublishedDocuments = class extends Context.Service()("@internal/server/PublishedDocuments") {};
2482
- var DocumentMaintenance = class extends Context.Service()("@internal/server/internal/DocumentMaintenance") {};
2483
- const makeDocuments = Effect.fn("Documents.make")(function* (configuration, options) {
2484
- const repository = yield* DocumentStore;
2485
- const accessControl = yield* AccessControl;
2486
- const crypto = yield* Crypto.Crypto;
2487
- const engine = yield* createDocumentEngine(configuration, options);
2488
- const state = yield* Ref.make(yield* loadSynchronizedState(repository, engine));
2489
- const semaphore = yield* Semaphore.make(1);
2490
- const mutationEvents = yield* Effect.acquireRelease(PubSub.sliding(CONTENT_MUTATION_BUFFER_CAPACITY), PubSub.shutdown);
2491
- const dependencies = {
2492
- engine,
2493
- configuration,
2494
- repository,
2495
- components: options.components
2496
- };
2497
- const refreshUnlocked = Effect.fn("Documents.refreshUnlocked")(function* (force = false) {
2498
- const current = yield* Ref.get(state);
2499
- const snapshot = force ? yield* loadRequired(repository) : yield* repository.load(current.snapshot.generation);
2500
- if (!snapshot || !generationIsAfter(snapshot.generation, current.snapshot.generation)) return current.snapshot.generation;
2501
- const refreshed = yield* compileDocumentState(engine, snapshot, current);
2502
- yield* Ref.set(state, refreshed);
2503
- return refreshed.snapshot.generation;
2504
- });
2505
- const executeUnlocked = Effect.fn("Documents.execute")(function* (command, activity) {
2506
- return yield* Effect.gen(function* () {
2507
- yield* refreshUnlocked();
2508
- const current = yield* Ref.get(state);
2509
- if (command.action === "document.move" || command.action === "document.remove" || command.action === "document.duplicate") {
2510
- const ids = command.action === "document.duplicate" && !command.input.includeChildren ? [command.input.documentId] : subtreeIds(current, command.input.documentId);
2511
- for (const documentId of ids) yield* accessControl.require({
2512
- actor: activity.actor,
2513
- requestId: activity.requestId
2514
- }, {
2515
- action: command.action,
2516
- resource: {
2517
- kind: "document",
2518
- documentId
2519
- }
2520
- });
2521
- if (command.action === "document.duplicate") {
2522
- const source = current.authoredById.get(command.input.documentId);
2523
- if (source) yield* accessControl.require({
2524
- actor: activity.actor,
2525
- requestId: activity.requestId
2526
- }, {
2527
- action: "document.create",
2528
- resource: {
2529
- kind: "collection",
2530
- collection: source.collection,
2531
- locale: source.locale
2532
- }
2533
- });
2534
- }
2535
- }
2536
- const prepared = yield* prepareDocumentMutation(dependencies, current, command);
2537
- if (command.action === "document.move" && prepared._tag === "Commit") {
2538
- for (const plan of prepared.additionalPlans ?? []) if (plan.action === "document.update") yield* accessControl.require({
2539
- actor: activity.actor,
2540
- requestId: activity.requestId
2541
- }, {
2542
- action: "document.update",
2543
- resource: {
2544
- kind: "document",
2545
- documentId: plan.documentId
2546
- }
2547
- });
2548
- }
2549
- if (command.action === "document.undelete" && prepared._tag === "Commit") for (const plan of [prepared.commitPlan, ...prepared.additionalPlans ?? []]) yield* accessControl.require({
2550
- actor: activity.actor,
2551
- requestId: activity.requestId
2552
- }, {
2553
- action: "document.undelete",
2554
- resource: {
2555
- kind: "document",
2556
- documentId: plan.documentId
2557
- }
2558
- });
2559
- if (prepared._tag === "Unchanged") return prepared.result;
2560
- const committed = yield* repository.commit({
2561
- activity,
2562
- plan: prepared.commitPlan,
2563
- additionalPlans: prepared.additionalPlans,
2564
- expectedGeneration: current.snapshot.generation,
2565
- mediaReferences: [prepared.commitPlan, ...prepared.additionalPlans ?? []].flatMap((plan) => projectMediaReferences(prepared.authored, prepared.published, plan.documentId))
2566
- });
2567
- const next = makeDocumentState(prepared.authored, prepared.published, committed.snapshot);
2568
- yield* Ref.set(state, next);
2569
- const entry = entryById(next, committed.mutation.documentId);
2570
- const result = DocumentMutationResult.make(Object.assign({}, committed.mutation, entry === void 0 ? void 0 : { entry }));
2571
- yield* PubSub.publish(mutationEvents, result);
2572
- return result;
2573
- }).pipe(Effect.tapError((error) => isDocumentGenerationConflict$2(error) ? refreshUnlocked(true) : Effect.void), Effect.retry({
2574
- schedule: Schedule.recurs(3),
2575
- while: isDocumentGenerationConflict$2
2576
- }));
2577
- });
2578
- const runMutation = Effect.fn("Documents.runMutation")(function* (command, activity, reason) {
2579
- const resolvedActivity = enrichActivity(activity, reason);
2580
- return yield* semaphore.withPermits(1)(executeUnlocked(command, resolvedActivity));
2581
- });
2582
- const authored = semaphore.withPermits(1)(Effect.gen(function* () {
2583
- yield* refreshUnlocked();
2584
- return (yield* Ref.get(state)).authored;
2585
- })).pipe(Effect.withSpan("Documents.authored"));
2586
- const browse = Effect.fn("Documents.browse")((query) => semaphore.withPermits(1)(Effect.gen(function* () {
2587
- yield* refreshUnlocked();
2588
- return authoringEntries(yield* Ref.get(state), query);
2589
- })));
2590
- const create = Effect.fn("Documents.create")(function* (input, activity) {
2591
- const plan = yield* Effect.fromResult(planDocumentCreation(configuration, input));
2592
- const source = yield* createInitialDocumentSource(plan);
2593
- const documentId = DocumentId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
2594
- return yield* runMutation({
2595
- action: "document.create",
2596
- documentId,
2597
- markdown: source.markdown,
2598
- sourcePath: plan.sourcePath
2599
- }, activity, input.reason);
2600
- });
2601
- const importSources = (input, activity) => importDocumentSources({
2602
- current: (sourcePath) => semaphore.withPermits(1)(Effect.gen(function* () {
2603
- yield* refreshUnlocked();
2604
- return (yield* Ref.get(state)).snapshot.active.find((candidate) => candidate.sourcePath === sourcePath);
2605
- })),
2606
- nextDocumentId: crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((value) => DocumentId.make(value))),
2607
- runMutation
2608
- }, input, activity);
2609
- const duplicate = Effect.fn("Documents.duplicate")((input, activity) => runMutation({
2610
- action: "document.duplicate",
2611
- input,
2612
- nextDocumentId: crypto.randomUUIDv4.pipe(Effect.orDie, Effect.map((value) => DocumentId.make(value)))
2613
- }, activity, input.reason));
2614
- const unpublish = Effect.fn("Documents.unpublish")((input, activity) => runMutation({
2615
- action: "document.unpublish",
2616
- input
2617
- }, activity, input.reason));
2618
- const move = Effect.fn("Documents.move")((input, activity) => runMutation({
2619
- action: "document.move",
2620
- input
2621
- }, activity, input.reason));
2622
- const subscribeMutations = Effect.gen(function* () {
2623
- const subscription = yield* PubSub.subscribe(mutationEvents);
2624
- const baseline = yield* semaphore.withPermits(1)(Ref.get(state).pipe(Effect.map((current) => current.snapshot.generation)));
2625
- return {
2626
- baseline,
2627
- mutations: Stream.fromSubscription(subscription).pipe(Stream.filter((mutation) => generationIsAfter(mutation.generation, baseline)))
2628
- };
2629
- }).pipe(Effect.withSpan("Documents.subscribeMutations"));
2630
- const publish = Effect.fn("Documents.publish")((input, activity) => runMutation({
2631
- action: "document.publish",
2632
- input
2633
- }, activity, input.reason));
2634
- const published = semaphore.withPermits(1)(Effect.gen(function* () {
2635
- yield* refreshUnlocked();
2636
- return (yield* Ref.get(state)).published;
2637
- })).pipe(Effect.withSpan("Documents.published"));
2638
- const read = Effect.fn("Documents.read")((documentId) => semaphore.withPermits(1)(Effect.gen(function* () {
2639
- yield* refreshUnlocked();
2640
- return (yield* Ref.get(state)).activeById.get(documentId) || (yield* new DocumentNotFound({ documentId }));
2641
- })));
2642
- const refresh = semaphore.withPermits(1)(refreshUnlocked()).pipe(Effect.withSpan("Documents.refresh"));
2643
- const remove = Effect.fn("Documents.remove")((input, activity) => runMutation({
2644
- action: "document.remove",
2645
- input
2646
- }, activity, input.reason));
2647
- const restore = Effect.fn("Documents.restore")((input, activity) => runMutation({
2648
- action: "document.restore",
2649
- input
2650
- }, activity, input.reason));
2651
- const trash = semaphore.withPermits(1)(Effect.gen(function* () {
2652
- yield* refreshUnlocked();
2653
- return deletedEntries(yield* Ref.get(state));
2654
- })).pipe(Effect.withSpan("Documents.trash"));
2655
- const undelete = Effect.fn("Documents.undelete")((input, activity) => runMutation({
2656
- action: "document.undelete",
2657
- input
2658
- }, activity, input.reason));
2659
- const update = Effect.fn("Documents.update")((input, activity) => runMutation({
2660
- action: "document.update",
2661
- input
2662
- }, activity, input.reason));
2663
- const validate = Effect.fn("Documents.validate")((input) => semaphore.withPermits(1)(Effect.gen(function* () {
2664
- yield* refreshUnlocked();
2665
- return yield* validateDocumentUpdate(engine, yield* Ref.get(state), input);
2666
- })));
2667
- const history = documentHistoryCapability(accessControl, repository);
2668
- const delivery = documentDelivery(configuration, published);
2669
- const document = (documentId) => ({
2670
- documentId,
2671
- kind: "document"
2672
- });
2673
- const requireThen = (context, action, resource, effect) => accessControl.require(context, {
2674
- action,
2675
- resource
2676
- }).pipe(Effect.andThen(effect));
2677
- const documents = Documents.of({
2678
- browse: (context, query) => requireThen(context, "document.browse", query.collection ? {
2679
- collection: query.collection,
2680
- kind: "collection",
2681
- locale: query.locale
2682
- } : { kind: "catalog" }, () => browse(query)),
2683
- create: (context, input) => requireThen(context, "document.create", {
2684
- collection: input.collection,
2685
- kind: "collection",
2686
- locale: input.locale
2687
- }, () => create(input, requestActivity(context, input.reason))),
2688
- historyCompare: history.compare,
2689
- historyList: history.list,
2690
- historyRead: history.read,
2691
- duplicate: (context, input) => requireThen(context, "document.duplicate", document(input.documentId), () => duplicate(input, requestActivity(context, input.reason))),
2692
- unpublish: (context, input) => requireThen(context, "document.unpublish", document(input.documentId), () => unpublish(input, requestActivity(context, input.reason))),
2693
- move: (context, input) => requireThen(context, "document.move", document(input.documentId), () => move(input, requestActivity(context, input.reason))),
2694
- publish: (context, input) => requireThen(context, "document.publish", document(input.documentId), () => publish(input, requestActivity(context, input.reason))),
2695
- read: (context, documentId) => requireThen(context, "document.read", document(documentId), () => read(documentId)),
2696
- remove: (context, input) => requireThen(context, "document.remove", document(input.documentId), () => remove(input, requestActivity(context, input.reason))),
2697
- restore: (context, input) => requireThen(context, "document.restore", document(input.documentId), () => restore(input, requestActivity(context, input.reason))),
2698
- trash: (context) => requireThen(context, "document.trash", { kind: "catalog" }, () => trash),
2699
- undelete: (context, input) => requireThen(context, "document.undelete", document(input.documentId), () => undelete(input, requestActivity(context, input.reason))),
2700
- update: (context, input) => requireThen(context, "document.update", document(input.documentId), () => update(input, requestActivity(context, input.reason))),
2701
- validate: (context, input) => requireThen(context, "document.validate", document(input.documentId), () => validate(input))
2702
- });
2703
- const draftDelivery = documentDelivery(configuration, authored);
2704
- const previews = DocumentPreviews.of({
2705
- read: (context, lookup) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.read(lookup)),
2706
- query: (context, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.query(query)),
2707
- navigation: (context, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.navigation(query)),
2708
- surround: (context, lookup, query) => requireThen(context, "document.browse", { kind: "catalog" }, () => draftDelivery.surround(lookup, query))
2709
- });
2710
- const publishedService = PublishedDocuments.of({
2711
- navigation: delivery.navigation,
2712
- query: delivery.query,
2713
- read: delivery.read,
2714
- surround: delivery.surround,
2715
- diagnostics: (context) => accessControl.require(context, {
2716
- action: "document.browse",
2717
- resource: { kind: "catalog" }
2718
- }).pipe(Effect.andThen(delivery.diagnostics))
2719
- });
2720
- return {
2721
- documents,
2722
- maintenance: DocumentMaintenance.of({
2723
- importSources,
2724
- mediaReferences: (documentId) => semaphore.withPermits(1)(Effect.gen(function* () {
2725
- yield* refreshUnlocked();
2726
- const current = yield* Ref.get(state);
2727
- return projectMediaReferences(current.authored, current.published, documentId);
2728
- })),
2729
- publish,
2730
- read,
2731
- refresh,
2732
- subscribeMutations,
2733
- validate
2734
- }),
2735
- previews,
2736
- publishedDocuments: publishedService
2737
- };
1424
+ var WorkingDocumentInvalid = class extends Schema.TaggedError()("WorkingDocuments.Invalid", {
1425
+ documentId: DocumentId,
1426
+ message: Schema.NonEmptyString,
1427
+ cause: Schema.Defect()
1428
+ }) {};
1429
+ var WorkingDocumentUnavailable = class extends Schema.TaggedError()("WorkingDocuments.Unavailable", {
1430
+ operation: Schema.NonEmptyString,
1431
+ cause: Schema.Defect()
1432
+ }) {};
1433
+ /** An authored or working baseline committed, but the publish workflow still needs recovery. */
1434
+ var WorkingDocumentCheckpointed = class extends Schema.TaggedError()("WorkingDocuments.Checkpointed", {
1435
+ checkpointVersion: DocumentVersion,
1436
+ documentId: DocumentId,
1437
+ result: Schema.optionalKey(DocumentMutationResult),
1438
+ stateVector: Schema.Uint8Array,
1439
+ cause: Schema.Defect()
1440
+ }) {};
1441
+ const WorkingDocumentChange = Schema.Struct({
1442
+ kind: Schema.Literals(["updated", "invalidated"]),
1443
+ ref: WorkingDocumentRef
2738
1444
  });
2739
- function documentsLayer(configuration, options) {
2740
- return Layer.effectContext(makeDocuments(configuration, options).pipe(Effect.map(({ documents, maintenance, previews, publishedDocuments }) => Context.empty().pipe(Context.add(Documents, documents), Context.add(DocumentMaintenance, maintenance), Context.add(DocumentPreviews, previews), Context.add(PublishedDocuments, publishedDocuments)))));
2741
- }
2742
- function loadRequired(repository) {
2743
- return repository.load().pipe(Effect.filterOrFail((snapshot) => snapshot !== void 0, () => new DocumentPersistenceError({
2744
- cause: /* @__PURE__ */ new Error("DocumentStore returned no initial snapshot."),
2745
- operation: "DocumentStore.load"
2746
- })));
2747
- }
2748
- function loadSynchronizedState(repository, engine) {
2749
- return Effect.gen(function* () {
2750
- const snapshot = yield* loadRequired(repository);
2751
- const state = yield* compileDocumentState(engine, snapshot);
2752
- yield* repository.synchronizeMediaReferences({
2753
- expectedGeneration: snapshot.generation,
2754
- references: projectMediaReferences(state.authored, state.published)
2755
- });
2756
- return state;
2757
- }).pipe(Effect.retry({
2758
- schedule: Schedule.recurs(3),
2759
- while: isDocumentGenerationConflict$2
2760
- }), Effect.mapError((error) => isDocumentGenerationConflict$2(error) || isDocumentMediaUnavailable$1(error) ? new DocumentPersistenceError({
2761
- cause: error,
2762
- operation: "Documents.synchronizeMediaReferences"
2763
- }) : error));
2764
- }
2765
- function generationIsAfter(candidate, current) {
2766
- return BigInt(candidate) > BigInt(current);
2767
- }
2768
- function enrichActivity(activity, reason) {
2769
- return ActivityContext.make(Object.assign({}, activity, reason === void 0 ? void 0 : { reason }));
2770
- }
2771
- function requestActivity(context, reason) {
2772
- return ActivityContext.make(Object.assign({
2773
- actor: context.actor,
2774
- requestId: context.requestId
2775
- }, reason === void 0 ? void 0 : { reason }));
2776
- }
1445
+ var WorkingDocuments = class extends Context.Service()("@internal/server/WorkingDocuments") {};
2777
1446
 
2778
1447
  //#endregion
2779
1448
  //#region ../document/src/internal/editor/identity-extension.ts
2780
- /** Ensures Yjs only observes authorable nodes with final, unique identities. */
2781
1449
  const EditorNodeIdentity = Extension.create({
2782
1450
  name: "cmsNodeIdentity",
2783
1451
  priority: 1e4,
@@ -3326,7 +1994,7 @@ function unwrapFirstParagraph(nodes) {
3326
1994
  }
3327
1995
  function restoreOpaqueNode(value, nodeId) {
3328
1996
  const decoded = Schema.decodeUnknownOption(MarkdownNode)(value);
3329
- if (decoded._tag === "None" || Predicate.isString(decoded.value)) return void 0;
1997
+ if (Option.isNone(decoded) || Predicate.isString(decoded.value)) return void 0;
3330
1998
  const node = structuredClone(decoded.value);
3331
1999
  if (nodeId === void 0) return node;
3332
2000
  if (node[0] === null) return [
@@ -3436,10 +2104,10 @@ const PROPERTIES_NAME = "properties";
3436
2104
  const PROPS_PREFIX = "props:";
3437
2105
  const CMS_PROPS_PREFIX = "cmsProps:";
3438
2106
  const editorSchema$1 = createEditorSchema();
3439
- const decodeEditorNodeId = Schema.decodeUnknownSync(EditorNodeId);
3440
- const decodeJson$1 = Schema.decodeUnknownSync(Schema.Json);
3441
- const decodeJsonObject$1 = Schema.decodeUnknownSync(Schema.JsonObject);
3442
- const decodePath = Schema.decodeUnknownSync(Schema.Array(Schema.String));
2107
+ const decodeEditorNodeId = flow(Schema.decodeUnknownResult(EditorNodeId), Result.getOrThrow);
2108
+ const decodeJson$1 = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
2109
+ const decodeJsonObject$1 = flow(Schema.decodeUnknownResult(Schema.JsonObject), Result.getOrThrow);
2110
+ const decodePath = flow(Schema.decodeUnknownResult(Schema.Array(Schema.String)), Result.getOrThrow);
3443
2111
  function seedCollaborationFields(document, content) {
3444
2112
  const state = extractGranularState(content);
3445
2113
  replaceMap(document.getMap(FRONTMATTER_NAME), flatten(state.frontmatter));
@@ -3589,9 +2257,9 @@ const METADATA_NAME = "cms";
3589
2257
  const META_KEY = "meta";
3590
2258
  const SOURCE_PATH_KEY = "sourcePath";
3591
2259
  const editorSchema = createEditorSchema();
3592
- const decodeJson = Schema.decodeUnknownSync(Schema.Json);
3593
- const decodeJsonObject = Schema.decodeUnknownSync(Schema.JsonObject);
3594
- const decodeSourcePath = Schema.decodeUnknownSync(SourcePath);
2260
+ const decodeJson = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
2261
+ const decodeJsonObject = flow(Schema.decodeUnknownResult(Schema.JsonObject), Result.getOrThrow);
2262
+ const decodeSourcePath = flow(Schema.decodeUnknownResult(SourcePath), Result.getOrThrow);
3595
2263
  function seedCollaborationDocument(sourcePath, seed, meta) {
3596
2264
  const document = prosemirrorJSONToYDoc(editorSchema, seed, BODY_NAME);
3597
2265
  try {
@@ -3688,7 +2356,6 @@ function sameBytes$1(left, right) {
3688
2356
 
3689
2357
  //#endregion
3690
2358
  //#region ../document/src/CollaborationDocument.ts
3691
- /** Creates the initial collaborative state from the canonical editor projection. */
3692
2359
  const createCollaborationDocument = Effect.fn("CollaborationDocument.create")(function* (input) {
3693
2360
  const seed = yield* openEditorDocument(input.body);
3694
2361
  return yield* Effect.try({
@@ -3696,7 +2363,6 @@ const createCollaborationDocument = Effect.fn("CollaborationDocument.create")(fu
3696
2363
  catch: (cause) => invalidCollaboration("Could not create the collaborative editor document.", cause)
3697
2364
  });
3698
2365
  });
3699
- /** Renders collaborative state through the canonical editor-to-Markdown encoder. */
3700
2366
  const renderCollaborationDocument = Effect.fn("CollaborationDocument.render")(function* (document) {
3701
2367
  const inspected = yield* inspectCollaborationDocument(document);
3702
2368
  return {
@@ -3715,31 +2381,26 @@ const captureCollaborationDocument = Effect.fn("CollaborationDocument.capture")(
3715
2381
  catch: (cause) => invalidCollaboration("Could not capture the collaborative editor document.", cause)
3716
2382
  });
3717
2383
  });
3718
- /** Restores a validated, caller-owned document from a full collaboration snapshot. */
3719
2384
  const restoreCollaborationDocument = Effect.fn("CollaborationDocument.restore")(function* (snapshot) {
3720
2385
  return yield* validateRestoredDocument(yield* Effect.try({
3721
2386
  try: () => restoreCollaborationSnapshot(snapshot),
3722
2387
  catch: (cause) => invalidCollaboration("Could not restore the collaborative editor document.", cause)
3723
2388
  }));
3724
2389
  });
3725
- /** Restores a validated document when only its authoritative full-state update is persisted. */
3726
2390
  const restoreCollaborationState = Effect.fn("CollaborationDocument.restoreState")(function* (state) {
3727
2391
  return yield* validateRestoredDocument(yield* Effect.try({
3728
2392
  try: () => restoreCollaborationUpdate(state),
3729
2393
  catch: (cause) => invalidCollaboration("Could not restore the collaborative editor document.", cause)
3730
2394
  }));
3731
2395
  });
3732
- /** Applies one encoded collaboration update while translating corrupt data to a typed failure. */
3733
2396
  const applyCollaborationUpdate = Effect.fn("CollaborationDocument.applyUpdate")((document, update) => Effect.try({
3734
2397
  try: () => applyCollaborationStateUpdate(document, update),
3735
2398
  catch: (cause) => invalidCollaboration("Could not apply the collaborative document update.", cause)
3736
2399
  }));
3737
- /** Captures the current state vector and an optional differential update. */
3738
2400
  const synchronizeCollaborationState = Effect.fn("CollaborationDocument.synchronizeState")((document, stateVector) => Effect.try({
3739
2401
  try: () => synchronizeCollaborationStateValue(document, stateVector),
3740
2402
  catch: (cause) => invalidCollaboration("Could not synchronize the collaborative document state.", cause)
3741
2403
  }));
3742
- /** Releases a collaboration document owned by a scoped workflow. */
3743
2404
  function destroyCollaborationDocument(document) {
3744
2405
  return Effect.sync(() => document.destroy());
3745
2406
  }
@@ -3840,6 +2501,25 @@ function sameJson(left, right) {
3840
2501
  return JSON.stringify(left) === JSON.stringify(right);
3841
2502
  }
3842
2503
 
2504
+ //#endregion
2505
+ //#region ../server/src/internal/working-documents/working-document-source.ts
2506
+ const validatedWorkingDocument = Effect.fn("WorkingDocuments.validateSource")(function* (source, validate) {
2507
+ const validation = yield* validate(UpdateDocumentInput.make({
2508
+ documentId: source.documentId,
2509
+ expectedVersion: source.version,
2510
+ markdown: source.markdown
2511
+ }));
2512
+ if (!validation.valid || !validation.document) return yield* new WorkingDocumentInvalid({
2513
+ cause: validation.issues,
2514
+ documentId: source.documentId,
2515
+ message: "Authored document cannot initialize a Working Document."
2516
+ });
2517
+ return validation.document;
2518
+ });
2519
+ const newWorkingSnapshot = Effect.fn("WorkingDocuments.newSnapshot")(function* (source, document, generationId) {
2520
+ return yield* createWorkingSnapshot(source, document, yield* generationId(source.documentId));
2521
+ });
2522
+
3843
2523
  //#endregion
3844
2524
  //#region ../server/src/internal/working-documents/working-document-reconciliation.ts
3845
2525
  const isDocumentNotFound$3 = Schema.is(DocumentNotFound);
@@ -3854,18 +2534,6 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
3854
2534
  })
3855
2535
  }).pipe(Effect.asVoid);
3856
2536
  const currentSource = (documentId) => options.documents.read(documentId).pipe(Effect.asSome, Effect.catchIf(isDocumentNotFound$3, () => Effect.succeedNone));
3857
- const validatedDocument = Effect.fn("WorkingDocuments.reconcile.validateSource")(function* (source) {
3858
- const validation = yield* options.documents.validate(UpdateDocumentInput.make({
3859
- documentId: source.documentId,
3860
- expectedVersion: source.version,
3861
- markdown: source.markdown
3862
- }));
3863
- if (!validation.valid || !validation.document) return yield* invalidSource(source.documentId, validation);
3864
- return validation.document;
3865
- });
3866
- const newSnapshot = Effect.fn("WorkingDocuments.reconcile.newSnapshot")(function* (source, document) {
3867
- return yield* createWorkingSnapshot(source, document, yield* options.generationId(source.documentId));
3868
- });
3869
2537
  const rotateDirty = Effect.fn("WorkingDocuments.reconcile.rotateDirty")(function* (snapshot) {
3870
2538
  const generationId = yield* options.generationId(snapshot.documentId);
3871
2539
  const seed = yield* useWorkingSnapshot(snapshot, (document) => captureCollaborationDocument(document).pipe(Effect.map((captured) => rotatedSeed(snapshot, generationId, captured))));
@@ -3901,10 +2569,10 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
3901
2569
  yield* publishChange(loaded.value);
3902
2570
  return;
3903
2571
  }
3904
- const document = yield* validatedDocument(source.value);
2572
+ const document = yield* validatedWorkingDocument(source.value, options.documents.validate);
3905
2573
  yield* options.store.replace({
3906
2574
  expectedGeneration: loaded.value.generationId,
3907
- snapshot: yield* newSnapshot(source.value, document)
2575
+ snapshot: yield* newWorkingSnapshot(source.value, document, options.generationId)
3908
2576
  });
3909
2577
  yield* publishChange(loaded.value);
3910
2578
  }));
@@ -3990,13 +2658,6 @@ function rotatedSeed(snapshot, generationId, captured) {
3990
2658
  state: Uint8Array.from(captured.state)
3991
2659
  };
3992
2660
  }
3993
- function invalidSource(documentId, validation) {
3994
- return Effect.fail(new WorkingDocumentInvalid({
3995
- cause: validation.issues,
3996
- documentId,
3997
- message: "Authored document cannot initialize a Working Document."
3998
- }));
3999
- }
4000
2661
  function generationAtOrBefore(candidate, current) {
4001
2662
  return BigInt(candidate) <= BigInt(current);
4002
2663
  }
@@ -4004,6 +2665,28 @@ function generationsAreAdjacent(current, candidate) {
4004
2665
  return BigInt(candidate) === BigInt(current) + 1n;
4005
2666
  }
4006
2667
 
2668
+ //#endregion
2669
+ //#region ../server/src/internal/working-documents/document-locks.ts
2670
+ /** Retain a lock only while an operation is running or waiting for that document. */
2671
+ function documentSynchronization() {
2672
+ const locks = /* @__PURE__ */ new Map();
2673
+ return (documentId, effect) => Effect.acquireUseRelease(Effect.sync(() => {
2674
+ let lock = locks.get(documentId);
2675
+ if (!lock) {
2676
+ lock = {
2677
+ semaphore: Semaphore.makeUnsafe(1),
2678
+ users: 0
2679
+ };
2680
+ locks.set(documentId, lock);
2681
+ }
2682
+ lock.users += 1;
2683
+ return lock;
2684
+ }), (lock) => lock.semaphore.withPermit(effect), (lock) => Effect.sync(() => {
2685
+ lock.users -= 1;
2686
+ if (lock.users === 0 && locks.get(documentId) === lock) locks.delete(documentId);
2687
+ }));
2688
+ }
2689
+
4007
2690
  //#endregion
4008
2691
  //#region ../server/src/internal/working-documents/working-documents.ts
4009
2692
  function workingDocumentsLayer(options = {}) {
@@ -4017,31 +2700,11 @@ const makeWorkingDocuments = Effect.fn("WorkingDocuments.make")(function* (optio
4017
2700
  const store = yield* WorkingDocumentStore;
4018
2701
  const changeSource = yield* WorkingDocumentChanges;
4019
2702
  const changes = yield* Effect.acquireRelease(PubSub.bounded(64), PubSub.shutdown);
4020
- const locks = /* @__PURE__ */ new Map();
2703
+ const synchronized = documentSynchronization();
4021
2704
  const compactAfterUpdates = validCompactionLimit(options.compactAfterUpdates);
4022
- const synchronized = (documentId, effect) => Effect.suspend(() => {
4023
- let semaphore = locks.get(documentId);
4024
- if (!semaphore) {
4025
- semaphore = Semaphore.makeUnsafe(1);
4026
- locks.set(documentId, semaphore);
4027
- }
4028
- return semaphore.withPermit(effect);
4029
- });
4030
2705
  const generationId = (documentId) => crypto.randomUUIDv4.pipe(Effect.map((uuid) => WorkingDocumentGenerationId.make(uuid)), Effect.mapError((cause) => invalid(documentId, "Could not create a working document generation.", cause)));
4031
2706
  const attemptId = (documentId) => crypto.randomUUIDv4.pipe(Effect.map((uuid) => WorkingDocumentPublishAttemptId.make(uuid)), Effect.mapError((cause) => invalid(documentId, "Could not create a publish attempt.", cause)));
4032
2707
  const markdownHash = (documentId, markdown) => crypto.digest("SHA-256", new TextEncoder().encode(markdown)).pipe(Effect.map(hex), Effect.mapError((cause) => invalid(documentId, "Could not fingerprint Working Document Markdown.", cause)));
4033
- const validatedDocument = Effect.fn("WorkingDocuments.validateSource")(function* (source) {
4034
- const validation = yield* maintenance.validate(UpdateDocumentInput.make({
4035
- documentId: source.documentId,
4036
- expectedVersion: source.version,
4037
- markdown: source.markdown
4038
- }));
4039
- if (!validation.valid || !validation.document) return yield* invalid(source.documentId, "Authored document cannot initialize a Working Document.", validation.issues);
4040
- return validation.document;
4041
- });
4042
- const newSnapshot = Effect.fn("WorkingDocuments.newSnapshot")(function* (source, document) {
4043
- return yield* createWorkingSnapshot(source, document, yield* generationId(source.documentId));
4044
- });
4045
2708
  const compactSnapshot = Effect.fn("WorkingDocuments.compactSnapshot")(function* (snapshot) {
4046
2709
  const lastUpdate = snapshot.updates.at(-1);
4047
2710
  if (!lastUpdate) return false;
@@ -4121,21 +2784,21 @@ const makeWorkingDocuments = Effect.fn("WorkingDocuments.make")(function* (optio
4121
2784
  const ensureOpenSnapshot = Effect.fn("WorkingDocuments.ensureOpenSnapshot")(function* (source) {
4122
2785
  const loaded = yield* store.load(source.documentId);
4123
2786
  if (Option.isNone(loaded)) {
4124
- const document = yield* validatedDocument(source);
4125
- const initialized = yield* store.initialize(yield* newSnapshot(source, document));
2787
+ const document = yield* validatedWorkingDocument(source, maintenance.validate);
2788
+ const initialized = yield* store.initialize(yield* newWorkingSnapshot(source, document, generationId));
4126
2789
  if (initialized.savedVersion === source.version) return initialized;
4127
2790
  if (yield* workingSnapshotIsDirty(initialized)) return initialized;
4128
2791
  return yield* store.replace({
4129
2792
  expectedGeneration: initialized.generationId,
4130
- snapshot: yield* newSnapshot(source, document)
2793
+ snapshot: yield* newWorkingSnapshot(source, document, generationId)
4131
2794
  });
4132
2795
  }
4133
2796
  if (loaded.value.savedVersion === source.version) return loaded.value;
4134
2797
  if (yield* workingSnapshotIsDirty(loaded.value)) return loaded.value;
4135
- const document = yield* validatedDocument(source);
2798
+ const document = yield* validatedWorkingDocument(source, maintenance.validate);
4136
2799
  return yield* store.replace({
4137
2800
  expectedGeneration: loaded.value.generationId,
4138
- snapshot: yield* newSnapshot(source, document)
2801
+ snapshot: yield* newWorkingSnapshot(source, document, generationId)
4139
2802
  });
4140
2803
  });
4141
2804
  const open = (context, documentId) => synchronized(documentId, Effect.gen(function* () {
@@ -4359,7 +3022,7 @@ function closeHocuspocus(hocuspocus, operations) {
4359
3022
  const store = hocuspocus.debouncer.executeNow(`onStoreDocument-${document.name}`);
4360
3023
  return store === void 0 ? [] : [store];
4361
3024
  });
4362
- yield* Effect.promise(() => Promise.allSettled(stores));
3025
+ const storeResults = yield* Effect.promise(() => Promise.allSettled(stores));
4363
3026
  yield* Effect.promise(operations.drain);
4364
3027
  const cleanupResults = yield* Effect.promise(() => Promise.allSettled(documents.map((document) => document.saveMutex.runExclusive(() => Promise.resolve()).then(() => hocuspocus.unloadDocument(document)))));
4365
3028
  const destroyResult = yield* Effect.result(Effect.tryPromise({
@@ -4367,6 +3030,8 @@ function closeHocuspocus(hocuspocus, operations) {
4367
3030
  catch: (cause) => gatewayError("close", cause)
4368
3031
  }).pipe(Effect.asVoid));
4369
3032
  const cleanupFailure = cleanupResults.find((result) => result.status === "rejected");
3033
+ const storeFailure = storeResults.find((result) => result.status === "rejected");
3034
+ if (storeFailure?.status === "rejected") return yield* gatewayError("close", storeFailure.reason);
4370
3035
  if (cleanupFailure?.status === "rejected") return yield* gatewayError("close", cleanupFailure.reason);
4371
3036
  if (Result.isFailure(destroyResult)) return yield* destroyResult.failure;
4372
3037
  });
@@ -4380,7 +3045,6 @@ function gatewayError(operation, cause) {
4380
3045
 
4381
3046
  //#endregion
4382
3047
  //#region ../platform-node/src/internal/realtime/keyed-operations.ts
4383
- /** Serializes each room independently and retains poisoned writes for shutdown reporting. */
4384
3048
  function createKeyedOperations(options) {
4385
3049
  const pending = /* @__PURE__ */ new Map();
4386
3050
  const failures = /* @__PURE__ */ new Map();
@@ -4474,7 +3138,6 @@ function stableStringHash(value) {
4474
3138
  //#region ../platform-node/src/internal/realtime/wake-dispatcher.ts
4475
3139
  const DEFAULT_CAPACITY = 1024;
4476
3140
  const DEFAULT_CONCURRENCY = 32;
4477
- /** Bounded wake scheduling: duplicate rooms coalesce and unrelated rooms run concurrently. */
4478
3141
  const runStudioRealtimeWakeDispatcher = Effect.fn("StudioRealtimeWakeDispatcher.run")(function* (wakes, handle, options = {}) {
4479
3142
  const queue = yield* Queue.dropping(options.capacity ?? DEFAULT_CAPACITY);
4480
3143
  const queued = /* @__PURE__ */ new Set();
@@ -4687,7 +3350,7 @@ const makeStudioRealtimeRoomHost = Effect.fn("StudioRealtimeRoomHost.make")(func
4687
3350
  },
4688
3351
  onStateless: (payload) => runTracked(Effect.gen(function* () {
4689
3352
  const decoded = yield* Effect.option(decodeClientMessage(payload.payload));
4690
- if (decoded._tag === "None" || decoded.value.type !== "checkpoint") return;
3353
+ if (Option.isNone(decoded) || decoded.value.type !== "checkpoint") return;
4691
3354
  const name = yield* decodeRoomName(payload.documentName).pipe(Effect.mapError((cause) => roomHostError("checkpoint", cause)));
4692
3355
  const stateVector = Y.encodeStateVector(payload.document);
4693
3356
  yield* Effect.tryPromise({
@@ -4820,7 +3483,9 @@ function decodeBytes(value, tokenInvalid) {
4820
3483
  try: () => {
4821
3484
  const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
4822
3485
  const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
4823
- return Uint8Array.from(binary, (character) => character.codePointAt(0) ?? 0);
3486
+ const bytes = Uint8Array.from(binary, (character) => character.codePointAt(0) ?? 0);
3487
+ if (encodeBytes(bytes) !== value) throw new Error("Noncanonical base64url encoding");
3488
+ return bytes;
4824
3489
  },
4825
3490
  catch: tokenInvalid
4826
3491
  });
@@ -4962,7 +3627,7 @@ const makeStudioRealtimeGateway = Effect.gen(function* () {
4962
3627
  onStateless: (payload) => {
4963
3628
  return runTracked(Effect.gen(function* () {
4964
3629
  const decoded = yield* Effect.option(decodeClientMessage(payload.payload));
4965
- if (decoded._tag === "None") return;
3630
+ if (Option.isNone(decoded)) return;
4966
3631
  const request = decoded.value;
4967
3632
  const context = authenticatedContext(payload.connection.context, payload.documentName);
4968
3633
  if (request.type === "checkpoint") return;
@@ -5172,11 +3837,10 @@ function httpProtocol(protocol) {
5172
3837
  * StudioRealtimeRoomHost synchronizes durable Yjs state across replicas. Presence
5173
3838
  * and stateless broadcasts remain process-local until an ephemeral bus is added.
5174
3839
  */
5175
- const studioRealtimeGatewayLayer = Layer.effect(StudioRealtimeGateway, Effect.acquireRelease(makeStudioRealtimeGateway, (gateway) => gateway.close.pipe(Effect.catchCause((cause) => Effect.logError("Studio realtime gateway shutdown failed.", cause)))));
3840
+ const studioRealtimeGatewayLayer = Layer.effect(StudioRealtimeGateway, Effect.acquireRelease(makeStudioRealtimeGateway, (gateway) => gateway.close.pipe(Effect.tapError((error) => Effect.logError("Studio realtime gateway shutdown failed.", error)), Effect.orDie)));
5176
3841
 
5177
3842
  //#endregion
5178
3843
  //#region ../platform-node/src/internal/realtime/websocket.ts
5179
- /** Hides Hocuspocus connection bookkeeping from framework WebSocket adapters. */
5180
3844
  function studioRealtimeWebSocketBridge(gateway) {
5181
3845
  const connections = /* @__PURE__ */ new WeakMap();
5182
3846
  const release = (socket) => {
@@ -5201,21 +3865,25 @@ function studioRealtimeWebSocketBridge(gateway) {
5201
3865
  //#endregion
5202
3866
  //#region ../platform-node/src/internal/installation/host.ts
5203
3867
  var Installation = class extends Context.Service()("@internal/platform-node/Installation") {};
5204
- function installationHttpLayer(bootstrap, application, cleanup) {
3868
+ function installationHttpLayer(bootstrap, cleanup) {
5205
3869
  return Layer.effect(Installation, Effect.gen(function* () {
5206
- const outerContext = yield* Effect.context();
5207
3870
  const cleanupContext = yield* Effect.context();
5208
- const memoMap = Context.getUnsafe(outerContext, Layer.CurrentMemoMap);
5209
- const webHandler = yield* Effect.acquireRelease(Effect.sync(() => HttpRouter.toWebHandler(application, { memoMap })), ({ dispose }) => Effect.promise(dispose));
5210
- return yield* makeInstallation(bootstrap, webHandler.handler, (context) => cleanup(context).pipe(Effect.provide(cleanupContext)));
3871
+ const routerConfig = yield* HttpRouter.RouterConfig;
3872
+ const router = yield* HttpRouter.HttpRouter;
3873
+ const handle = HttpEffect.toWebHandlerWith(yield* Effect.context())(router.asHttpEffect(), HttpMiddleware.logger);
3874
+ return yield* makeInstallation(bootstrap, handle, (context) => cleanup(context).pipe(Effect.provide(cleanupContext)), routerConfig);
5211
3875
  }));
5212
3876
  }
5213
- const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, handle, cleanup) {
3877
+ const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, handle, cleanup, routerConfig) {
5214
3878
  const authentication = yield* AuthenticationHandler;
5215
3879
  const crypto = yield* Crypto.Crypto;
5216
3880
  const realtime = yield* StudioRealtimeGateway;
5217
3881
  const roomHost = yield* StudioRealtimeRoomHost;
5218
- const health = (yield* Readiness).check.pipe(Effect.andThen(roomHost.check), Effect.match({
3882
+ const readiness = yield* Readiness;
3883
+ const protectedRoutes = FindMyWay.make(routerConfig);
3884
+ protectedRoutes.all(CMS_FORM_SUBMIT_PATH, "form");
3885
+ protectedRoutes.all(`${STUDIO_API_PATH}/preview/*`, "preview");
3886
+ const health = readiness.check.pipe(Effect.andThen(roomHost.check), Effect.match({
5219
3887
  onFailure: () => ({
5220
3888
  live: true,
5221
3889
  ready: false
@@ -5241,13 +3909,15 @@ const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, ha
5241
3909
  cleanupMedia,
5242
3910
  fetch: (request) => {
5243
3911
  const pathname = new URL(request.url).pathname;
3912
+ const protection = protectedRoutes.find(request.method, pathname)?.handler;
3913
+ if (protection === "form") return handleFormRequest(request, handle);
5244
3914
  if (pathname === "/api/cms/auth" || pathname.startsWith(`${"/api/cms/auth"}/`)) return authentication.handle(request);
5245
3915
  if (pathname === CMS_LIVENESS_PATH) return Promise.resolve(healthResponse({
5246
3916
  live: true,
5247
3917
  ready: true
5248
3918
  }, false));
5249
3919
  if (pathname === CMS_READINESS_PATH) return runPromise(health).then((status) => healthResponse(status, true));
5250
- if (pathname.startsWith(`${STUDIO_API_PATH}/preview/`)) return handle(request).then((response) => {
3920
+ if (protection === "preview") return handle(request).then((response) => {
5251
3921
  response.headers.set("cache-control", "private, no-store");
5252
3922
  return response;
5253
3923
  });
@@ -5295,21 +3965,6 @@ const startInstallation = Effect.fn("CmsInstallation.start")(function* (options)
5295
3965
  };
5296
3966
  });
5297
3967
 
5298
- //#endregion
5299
- //#region ../server/src/internal/media/media-object-store.ts
5300
- var MediaObjectStore = class extends Context.Service()("@internal/server/MediaObjectStore") {};
5301
-
5302
- //#endregion
5303
- //#region ../server/src/internal/media/media-failures.ts
5304
- var MediaPersistenceError = class extends Schema.TaggedError()("Media.PersistenceError", {
5305
- operation: Schema.NonEmptyString,
5306
- cause: Schema.Defect()
5307
- }) {};
5308
- var MediaStorageError = class extends Schema.TaggedError()("Media.StorageError", {
5309
- operation: Schema.NonEmptyString,
5310
- cause: Schema.Defect()
5311
- }) {};
5312
-
5313
3968
  //#endregion
5314
3969
  //#region ../server/src/internal/media/media-change.ts
5315
3970
  const MediaUploadIntent = Schema.Struct({
@@ -5472,7 +4127,7 @@ function makeMediaChange(input) {
5472
4127
  action: input.action,
5473
4128
  context: input.change.context,
5474
4129
  id: input.change.id,
5475
- intent: Schema.encodeSync(MediaChangeIntent)(input.intent),
4130
+ intent: Result.getOrThrow(Schema.encodeResult(MediaChangeIntent)(input.intent)),
5476
4131
  scope: "media",
5477
4132
  subject: input.subject,
5478
4133
  target: input.target
@@ -5481,55 +4136,43 @@ function makeMediaChange(input) {
5481
4136
 
5482
4137
  //#endregion
5483
4138
  //#region ../server/src/internal/media/media-reconciliation.ts
5484
- function recoverPendingMedia(store, objectStore, operations, recoveryAge, options = {}) {
4139
+ const recoveryBatchSize = 100;
4140
+ function recoverPendingMedia(store, objectStore, operations, recoveryAge, cursor = {}) {
5485
4141
  return Effect.gen(function* () {
5486
4142
  const before = DateTime.subtractDuration(yield* DateTime.now, recoveryAge);
5487
- const maxBatches = options.maxBatches ?? Number.POSITIVE_INFINITY;
5488
- const recoverBatch = Effect.gen(function* () {
5489
- const pending = yield* store.pending({
5490
- before,
5491
- limit: 100
5492
- });
5493
- for (const record of pending) {
5494
- if (record.status === "deleting") {
4143
+ const pending = yield* store.pending({
4144
+ after: cursor.media,
4145
+ before,
4146
+ limit: recoveryBatchSize
4147
+ });
4148
+ for (const record of pending) {
4149
+ if (record.status === "deleting") {
4150
+ yield* objectStore.remove(record.objectKey);
4151
+ yield* store.finishDelete(record.id);
4152
+ continue;
4153
+ }
4154
+ const stored = yield* objectStore.stat(record.objectKey);
4155
+ if (Option.isSome(stored) && stored.value.byteSize === record.byteSize && (stored.value.mimeType === void 0 || stored.value.mimeType === record.mimeType)) yield* store.activate(record.id);
4156
+ else {
4157
+ if (Option.isSome(stored)) {
5495
4158
  yield* objectStore.remove(record.objectKey);
5496
- yield* store.finishDelete(record.id);
5497
- continue;
5498
- }
5499
- const stored = yield* objectStore.stat(record.objectKey);
5500
- if (Option.isSome(stored) && stored.value.byteSize === record.byteSize && (stored.value.mimeType === void 0 || stored.value.mimeType === record.mimeType)) yield* store.activate(record.id);
5501
- else {
5502
- if (Option.isSome(stored)) {
5503
- yield* objectStore.remove(record.objectKey);
5504
- const remaining = yield* objectStore.stat(record.objectKey);
5505
- if (Option.isSome(remaining)) continue;
5506
- }
5507
- yield* store.abandonUpload(record.id);
4159
+ const remaining = yield* objectStore.stat(record.objectKey);
4160
+ if (Option.isSome(remaining)) continue;
5508
4161
  }
4162
+ yield* store.abandonUpload(record.id);
5509
4163
  }
5510
- return pending.length < 100;
5511
- });
5512
- let mediaBatch = 0;
5513
- let mediaComplete = false;
5514
- while (!mediaComplete && mediaBatch < maxBatches) {
5515
- mediaComplete = yield* recoverBatch;
5516
- mediaBatch += 1;
5517
4164
  }
5518
- const recoverReplacementBatch = Effect.gen(function* () {
5519
- const replacements = yield* store.pendingReplacements({
5520
- before,
5521
- limit: 100
5522
- });
5523
- yield* Effect.forEach(replacements, (replacement) => recoverReplacement(store, objectStore, replacement).pipe(Effect.catchTag("Media.Conflict", () => Effect.void)), { discard: true });
5524
- return replacements.length < 100;
4165
+ const replacements = yield* store.pendingReplacements({
4166
+ after: cursor.replacements,
4167
+ before,
4168
+ limit: recoveryBatchSize
5525
4169
  });
5526
- let replacementBatch = 0;
5527
- let replacementsComplete = false;
5528
- while (!replacementsComplete && replacementBatch < maxBatches) {
5529
- replacementsComplete = yield* recoverReplacementBatch;
5530
- replacementBatch += 1;
5531
- }
4170
+ yield* Effect.forEach(replacements, (replacement) => recoverReplacement(store, objectStore, replacement).pipe(Effect.catchTag("Media.Conflict", () => Effect.void)), { discard: true });
5532
4171
  yield* recoverPendingMediaCleanup(operations, objectStore, before);
4172
+ return {
4173
+ media: pending.length < recoveryBatchSize ? void 0 : pending.at(-1)?.id,
4174
+ replacements: replacements.length < recoveryBatchSize ? void 0 : replacements.at(-1)?.mediaId
4175
+ };
5533
4176
  });
5534
4177
  }
5535
4178
  function recoverPendingMediaCleanup(operations, objectStore, before) {
@@ -5717,10 +4360,14 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
5717
4360
  const store = yield* MediaStore;
5718
4361
  const baseUrl = options.publicBaseUrl.replace(/\/+$/gu, "");
5719
4362
  const pendingRecoveryAge = options.pendingRecoveryAge ?? "5 minutes";
5720
- yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge);
4363
+ const recoveryCursor = yield* Ref.make(yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge));
5721
4364
  const mutations = yield* Semaphore.make(1);
5722
4365
  const recoveryInterval = options.pendingRecoveryInterval ?? MEDIA_OPERATION_RECOVERY_INTERVAL;
5723
- if (recoveryInterval !== false) yield* mutations.withPermits(1)(recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge, { maxBatches: 1 })).pipe(Effect.catch((error) => Effect.logWarning("Pending media change recovery sweep failed.", { errorTag: error._tag }))).pipe(Effect.repeat(Schedule.spaced(recoveryInterval)), Effect.delay(recoveryInterval), Effect.forkScoped({ startImmediately: true }));
4366
+ if (recoveryInterval !== false) yield* mutations.withPermits(1)(Effect.gen(function* () {
4367
+ const cursor = yield* Ref.get(recoveryCursor);
4368
+ const next = yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge, cursor);
4369
+ yield* Ref.set(recoveryCursor, next);
4370
+ })).pipe(Effect.catch((error) => Effect.logWarning("Pending media change recovery sweep failed.", { errorTag: error._tag }))).pipe(Effect.repeat(Schedule.spaced(recoveryInterval)), Effect.delay(recoveryInterval), Effect.forkScoped({ startImmediately: true }));
5724
4371
  const toAsset = (record) => MediaAsset.make({
5725
4372
  byteSize: record.byteSize,
5726
4373
  createdAt: record.createdAt,
@@ -5789,7 +4436,7 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
5789
4436
  return yield* Effect.scoped(prepareUpload(input).pipe(Effect.flatMap((prepared) => mutations.withPermits(1)(Effect.gen(function* () {
5790
4437
  const id = MediaId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
5791
4438
  const operationId = ActivityId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
5792
- const objectKey = MediaObjectKey.make(`media/${id}.${imageExtension(prepared.mimeType)}`);
4439
+ const objectKey = MediaObjectKey.make(`${mediaObjectPrefix}${id}.${imageExtension(prepared.mimeType)}`);
5793
4440
  const now = yield* DateTime.now;
5794
4441
  const reserved = yield* store.reserve({
5795
4442
  byteSize: input.metadata.declaredByteSize,
@@ -5837,7 +4484,7 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
5837
4484
  name: input.metadata.name,
5838
4485
  objectKey: current.value.objectKey,
5839
4486
  staged: false,
5840
- stagingObjectKey: MediaObjectKey.make(`media/replacements/${operationId}.${imageExtension(prepared.mimeType)}`),
4487
+ stagingObjectKey: MediaObjectKey.make(`${mediaObjectPrefix}replacements/${operationId}.${imageExtension(prepared.mimeType)}`),
5841
4488
  updatedAt: timestamp
5842
4489
  };
5843
4490
  const pendingReplacement = yield* store.beginReplace(replacement, {
@@ -5960,11 +4607,42 @@ function cleanupBeforePromotionBestEffort(cleanup) {
5960
4607
  onSuccess: () => Effect.void
5961
4608
  }));
5962
4609
  }
5963
- function activityFailure(operation) {
5964
- return (cause) => new ActivityUnavailable({
5965
- cause,
5966
- operation
4610
+ function activityFailure(operation) {
4611
+ return (cause) => new ActivityUnavailable({
4612
+ cause,
4613
+ operation
4614
+ });
4615
+ }
4616
+
4617
+ //#endregion
4618
+ //#region ../server/src/ProductModules.ts
4619
+ function productModulesLayer(options) {
4620
+ const accessControl = defaultAccessControlLayer;
4621
+ const seo = seoLayer(options.bootstrap.seo ?? makeDefaultSeoOptions(options.baseUrl)).pipe(Layer.provide(accessControl));
4622
+ const authoredDocuments = documentsLayer(options.bootstrap.documents, {
4623
+ ...options.documentEngine,
4624
+ components: options.documentEngine.components ?? [],
4625
+ mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? "https://media.invalid"
4626
+ }).pipe(Layer.provide(accessControl));
4627
+ const documentAccess = Layer.merge(authoredDocuments, accessControl);
4628
+ const activity = activityLayer.pipe(Layer.provide(accessControl));
4629
+ const workingDocuments = workingDocumentsLayer(options.workingDocuments).pipe(Layer.provide(documentAccess));
4630
+ const seoDelivery = seoDeliveryLayer.pipe(Layer.provide(Layer.merge(documentAccess, seo)));
4631
+ return Layer.mergeAll(activity, documentAccess, workingDocuments, seo, seoDelivery, readinessLayer);
4632
+ }
4633
+ function productModulesWithMediaLayer(options) {
4634
+ const products = productModulesLayer({
4635
+ ...options,
4636
+ documentEngine: {
4637
+ ...options.documentEngine,
4638
+ mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? options.mediaPublicBaseUrl
4639
+ }
5967
4640
  });
4641
+ const media = mediaLibraryLayer({
4642
+ maxUploadBytes: options.bootstrap.media.maxUploadBytes,
4643
+ publicBaseUrl: options.mediaPublicBaseUrl
4644
+ }).pipe(Layer.provide(products));
4645
+ return Layer.merge(products, media);
5968
4646
  }
5969
4647
 
5970
4648
  //#endregion
@@ -5974,7 +4652,6 @@ var Initialization = class extends Context.Service()("@internal/server/Initializ
5974
4652
  function initializationLayer(options) {
5975
4653
  return Layer.effect(Initialization, Effect.gen(function* () {
5976
4654
  const journal = yield* ChangeJournal;
5977
- const documentOperations = yield* DocumentMaintenance;
5978
4655
  const crypto = yield* Crypto.Crypto;
5979
4656
  const identity = yield* InitialIdentity;
5980
4657
  const recovery = yield* InitialIdentityRecovery;
@@ -6013,15 +4690,6 @@ function initializationLayer(options) {
6013
4690
  intent: prepared.intent
6014
4691
  });
6015
4692
  if (!(yield* store.claim(durableClaim))) return yield* new InitializationAlreadyCompleted();
6016
- const imported = yield* documentOperations.importSources(DocumentImportInput.make({
6017
- mode: "preserve",
6018
- reason: "CMS initialization",
6019
- sources: options.initialSources ?? []
6020
- }), context).pipe(Effect.result);
6021
- if (Result.isFailure(imported)) {
6022
- yield* store.reset(changeId);
6023
- return yield* imported.failure;
6024
- }
6025
4693
  const begin = yield* journal.begin(initialAdministratorDraft({
6026
4694
  context,
6027
4695
  id: changeId
@@ -6067,20 +4735,21 @@ const reconcileStored = Effect.fn("Initialization.reconcileStored")(function* (s
6067
4735
  id: state.claim.changeId
6068
4736
  }, state.claim.intent);
6069
4737
  const begin = yield* journal.begin(draft).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
6070
- switch (begin._tag) {
6071
- case "Recorded":
6072
- yield* store.complete(state.claim.changeId);
6073
- return yield* store.read;
6074
- case "Cancelled":
6075
- yield* store.reset(state.claim.changeId);
6076
- return yield* store.read;
6077
- case "Unresolved": {
6078
- const resolution = yield* reconcileInitialAdministrator(journal, begin.change, recovery.observe).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
6079
- if (resolution.kind === "applied") yield* store.complete(state.claim.changeId);
6080
- if (resolution.kind === "cancelled") yield* store.reset(state.claim.changeId);
6081
- return yield* store.read;
6082
- }
4738
+ if (Predicate.isTagged("Recorded")(begin)) {
4739
+ yield* store.complete(state.claim.changeId);
4740
+ return yield* store.read;
4741
+ }
4742
+ if (Predicate.isTagged("Cancelled")(begin)) {
4743
+ yield* store.reset(state.claim.changeId);
4744
+ return yield* store.read;
6083
4745
  }
4746
+ if (Predicate.isTagged("Unresolved")(begin)) {
4747
+ const resolution = yield* reconcileInitialAdministrator(journal, begin.change, recovery.observe).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
4748
+ if (resolution.kind === "applied") yield* store.complete(state.claim.changeId);
4749
+ if (resolution.kind === "cancelled") yield* store.reset(state.claim.changeId);
4750
+ return yield* store.read;
4751
+ }
4752
+ return yield* store.read;
6084
4753
  });
6085
4754
  function completeBestEffort(store, changeId) {
6086
4755
  return store.complete(changeId).pipe(Effect.catch((error) => Effect.logWarning("Initialization completion remains recoverable.", {
@@ -6281,6 +4950,69 @@ function targetError(operation) {
6281
4950
  return (error) => Schema.is(UserAlreadyExists)(error) ? unavailable$2(operation)(error) : error;
6282
4951
  }
6283
4952
 
4953
+ //#endregion
4954
+ //#region ../server/src/Forms.ts
4955
+ /** Only the currently published definition can authorize a submission. Never trust client rules. */
4956
+ const submitForm = Effect.fn("Forms.submit")(function* (input, components, handlers) {
4957
+ const document = yield* (yield* PublishedDocuments).read({
4958
+ kind: "id",
4959
+ documentId: input.documentId
4960
+ }).pipe(Effect.orElseSucceed(() => void 0));
4961
+ if (!document) return FormResult.make({
4962
+ status: "unavailable",
4963
+ errors: {}
4964
+ });
4965
+ const form = documentForms(document, components).find((candidate) => JSON.stringify(candidate.nodePath) === JSON.stringify(input.nodePath));
4966
+ if (!form || form.revision !== input.revision) return FormResult.make({
4967
+ status: "stale",
4968
+ errors: {}
4969
+ });
4970
+ const handler = Object.hasOwn(handlers, form.name) ? handlers[form.name] : void 0;
4971
+ if (!handler) return FormResult.make({
4972
+ status: "unavailable",
4973
+ errors: {}
4974
+ });
4975
+ const fields = form.steps.flatMap((step) => step.fields);
4976
+ const errors = validateFormValues(fields, input.values);
4977
+ if (Object.keys(input.values).some((id) => !fields.some((field) => field.id === id))) errors._form = "The submission contains unknown answers.";
4978
+ if (Object.keys(errors).length) return FormResult.make({
4979
+ status: "invalid",
4980
+ errors
4981
+ });
4982
+ const values = Object.fromEntries(fields.map((field) => [field.id, normalizedAnswer(Object.hasOwn(input.values, field.id) ? input.values[field.id] : void 0)]));
4983
+ return yield* Effect.tryPromise(() => Promise.resolve(handler({
4984
+ form,
4985
+ values
4986
+ }))).pipe(Effect.as(FormResult.make({
4987
+ status: "accepted",
4988
+ errors: {}
4989
+ })), Effect.orElseSucceed(() => FormResult.make({
4990
+ status: "failed",
4991
+ errors: {}
4992
+ })));
4993
+ });
4994
+ function normalizedAnswer(value) {
4995
+ return value === void 0 || Predicate.isString(value) && !value.trim() ? null : value;
4996
+ }
4997
+
4998
+ //#endregion
4999
+ //#region ../platform-node/src/internal/http/forms.ts
5000
+ function formsApiLayer(components, handlers) {
5001
+ return HttpApiBuilder.group(StudioApi, "forms", (group) => Effect.gen(function* () {
5002
+ const documents = yield* PublishedDocuments;
5003
+ const limiter = yield* RateLimiter.RateLimiter;
5004
+ return group.handle("submit", ({ payload }) => limiter.consume({
5005
+ key: "cms-forms",
5006
+ limit: 60,
5007
+ window: "1 minute",
5008
+ onExceeded: "fail"
5009
+ }).pipe(Effect.andThen(submitForm(payload, components, handlers).pipe(Effect.provideService(PublishedDocuments, documents))), Effect.orElseSucceed(() => FormResult.make({
5010
+ status: "unavailable",
5011
+ errors: {}
5012
+ }))));
5013
+ })).pipe(Layer.provide(RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory))));
5014
+ }
5015
+
6284
5016
  //#endregion
6285
5017
  //#region ../platform-node/src/internal/http/errors.ts
6286
5018
  const studioSchemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(StudioSchemaError, (error) => Effect.fail(mapStudioSchemaError(error.kind)));
@@ -6345,6 +5077,7 @@ const isAccessDenied$1 = Schema.is(AccessDenied);
6345
5077
  const isDocumentNotFound$1 = Schema.is(DocumentNotFound);
6346
5078
  const isDocumentVersionConflict = Schema.is(DocumentVersionConflict);
6347
5079
  const isDocumentGenerationConflict = Schema.is(DocumentGenerationConflict);
5080
+ const isContentTransferConflict = Schema.is(ContentTransferConflict);
6348
5081
  const isPublicMutationError = Schema.is(Schema.Union([
6349
5082
  AccessDenied,
6350
5083
  DocumentMediaUnavailable,
@@ -6384,6 +5117,10 @@ const documentsApiLayer = HttpApiBuilder.group(StudioApi, "documents", (handlers
6384
5117
  ...payload
6385
5118
  }))).pipe(mapMutationError));
6386
5119
  }));
5120
+ const contentApiLayer = HttpApiBuilder.group(StudioApi, "content", (handlers) => Effect.gen(function* () {
5121
+ const documents = yield* Documents;
5122
+ return handlers.handle("snapshot", () => withStudioRequest((context) => documents.contentSnapshot(context)).pipe(preservePublicErrors(isAccessDenied$1))).handle("plan", ({ payload }) => withStudioRequest((context) => documents.contentPlan(context, payload)).pipe(preservePublicErrors((error) => isAccessDenied$1(error) || isContentTransferConflict(error)))).handle("apply", ({ payload }) => withStudioRequest((context) => documents.contentApply(context, payload)).pipe(preservePublicErrors((error) => isAccessDenied$1(error) || isContentTransferConflict(error) || Schema.is(DocumentValidationFailed)(error))));
5123
+ }));
6387
5124
  function mapMutationError(effect) {
6388
5125
  return effect.pipe(Effect.mapError((error) => {
6389
5126
  if (isPublicMutationError(error)) return error;
@@ -6462,7 +5199,7 @@ function decodeMediaUpload(payload, query, maxUploadBytes) {
6462
5199
  }
6463
5200
  function mapMultipartError(error) {
6464
5201
  if (isMediaInvalid(error)) return error;
6465
- return error.reason._tag === "InternalError" ? studioInternalError() : invalidMultipart();
5202
+ return Predicate.isTagged("InternalError")(error.reason) ? studioInternalError() : invalidMultipart();
6466
5203
  }
6467
5204
  function invalidMultipart() {
6468
5205
  return new MediaInvalid({ message: "Upload exactly one file whose body matches its declared size." });
@@ -6474,7 +5211,16 @@ const publicError = Schema.is(Schema.Union([AccessDenied, PublishedDocumentNotFo
6474
5211
  const denied = Schema.is(AccessDenied);
6475
5212
  const previewApiLayer = HttpApiBuilder.group(StudioApi, "previewDelivery", (handlers) => Effect.gen(function* () {
6476
5213
  const documents = yield* DocumentPreviews;
6477
- return handlers.handle("read", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.read(context, payload)), preservePublicErrors(publicError))).handle("query", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.query(context, payload)), preservePublicErrors(denied))).handle("navigation", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.navigation(context, payload)), preservePublicErrors(denied))).handle("surround", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.surround(context, payload.lookup, payload.query)), preservePublicErrors(publicError)));
5214
+ const accessControl = yield* AccessControl;
5215
+ const seo = yield* PreviewSeo;
5216
+ const seoDelivery = yield* SeoDelivery;
5217
+ return handlers.handle("read", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.read(context, payload).pipe(Effect.flatMap((document) => seoDelivery.previewDocument(document)))), preservePublicErrors(publicError))).handle("query", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.query(context, payload).pipe(Effect.flatMap((page) => Effect.forEach(page.items, (document) => seoDelivery.previewDocument(document)).pipe(Effect.map((items) => Object.assign({}, page, { items })))))), preservePublicErrors(denied))).handle("navigation", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.navigation(context, payload)), preservePublicErrors(denied))).handle("surround", ({ payload }) => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.surround(context, payload.lookup, payload.query).pipe(Effect.flatMap((surround) => Effect.all({
5218
+ previous: surround.previous ? seoDelivery.previewDocument(surround.previous) : Effect.succeed(null),
5219
+ next: surround.next ? seoDelivery.previewDocument(surround.next) : Effect.succeed(null)
5220
+ })))), preservePublicErrors(publicError))).handle("seo", () => CurrentStudioRequest.pipe(Effect.flatMap((context) => accessControl.require(context, {
5221
+ action: "document.browse",
5222
+ resource: { kind: "catalog" }
5223
+ }).pipe(Effect.andThen(seo.read))), preservePublicErrors(denied)));
6478
5224
  }));
6479
5225
 
6480
5226
  //#endregion
@@ -6482,7 +5228,15 @@ const previewApiLayer = HttpApiBuilder.group(StudioApi, "previewDelivery", (hand
6482
5228
  const isPublishedDocumentNotFound = Schema.is(PublishedDocumentNotFound);
6483
5229
  const deliveryApiLayer = Layer.merge(HttpApiBuilder.group(StudioApi, "delivery", (handlers) => Effect.gen(function* () {
6484
5230
  const documents = yield* PublishedDocuments;
6485
- return handlers.handle("read", ({ payload }) => documents.read(payload).pipe(preservePublicErrors(isPublishedDocumentNotFound))).handle("query", ({ payload }) => documents.query(payload).pipe(mapUnexpectedError)).handle("navigation", ({ payload }) => documents.navigation(payload).pipe(mapUnexpectedError)).handle("surround", ({ payload }) => documents.surround(payload.lookup, payload.query).pipe(preservePublicErrors(isPublishedDocumentNotFound)));
5231
+ const seo = yield* PublishedSeo;
5232
+ const seoDelivery = yield* SeoDelivery;
5233
+ return handlers.handle("read", ({ payload }) => documents.read(payload).pipe(Effect.flatMap(seoDelivery.publishedDocument), preservePublicErrors(isPublishedDocumentNotFound))).handle("query", ({ payload }) => documents.query(payload).pipe(Effect.flatMap((page) => Effect.forEach(page.items, seoDelivery.publishedDocument).pipe(Effect.map((items) => ({
5234
+ items,
5235
+ total: page.total
5236
+ })))), mapUnexpectedError)).handle("navigation", ({ payload }) => documents.navigation(payload).pipe(mapUnexpectedError)).handle("surround", ({ payload }) => documents.surround(payload.lookup, payload.query).pipe(Effect.flatMap((surround) => Effect.all({
5237
+ previous: surround.previous ? seoDelivery.publishedDocument(surround.previous) : Effect.succeed(null),
5238
+ next: surround.next ? seoDelivery.publishedDocument(surround.next) : Effect.succeed(null)
5239
+ })), preservePublicErrors(isPublishedDocumentNotFound))).handle("seo", () => seo.read.pipe(mapUnexpectedError)).handle("sitemap", () => seoDelivery.sitemap.pipe(mapUnexpectedError));
6486
5240
  })), HttpApiBuilder.group(StudioApi, "diagnostics", (handlers) => Effect.gen(function* () {
6487
5241
  const documents = yield* PublishedDocuments;
6488
5242
  return handlers.handle("documents", () => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.diagnostics(context)), preservePublicErrors(Schema.is(AccessDenied))));
@@ -6498,6 +5252,12 @@ const isHistoryPublicError = Schema.is(Schema.Union([
6498
5252
  DocumentNotFound
6499
5253
  ]));
6500
5254
  const isInitializationPublicError = Schema.is(Schema.Union([InitializationAlreadyCompleted, InitializationProofInvalid]));
5255
+ const isSeoPublicError = Schema.is(Schema.Union([
5256
+ AccessDenied,
5257
+ SeoConfirmationRequired,
5258
+ SeoMediaUnavailable,
5259
+ SeoVersionConflict
5260
+ ]));
6501
5261
  const administrationApiLayer = Layer.mergeAll(HttpApiBuilder.group(StudioApi, "initialization", (handlers) => Effect.gen(function* () {
6502
5262
  const initialization = yield* Initialization;
6503
5263
  return handlers.handle("status", () => initialization.status.pipe(mapUnexpectedError)).handle("claim", ({ payload }) => initialization.claim(payload).pipe(preservePublicErrors(isInitializationPublicError)));
@@ -6507,6 +5267,9 @@ const administrationApiLayer = Layer.mergeAll(HttpApiBuilder.group(StudioApi, "i
6507
5267
  })), HttpApiBuilder.group(StudioApi, "activity", (handlers) => Effect.gen(function* () {
6508
5268
  const activity = yield* Activity;
6509
5269
  return handlers.handle("list", ({ query }) => withStudioRequest((context) => activity.list(context, query)).pipe(preservePublicErrors(isActivityPublicError)));
5270
+ })), HttpApiBuilder.group(StudioApi, "seo", (handlers) => Effect.gen(function* () {
5271
+ const seo = yield* Seo;
5272
+ return handlers.handle("read", () => withStudioRequest((context) => seo.read(context)).pipe(preservePublicErrors(isSeoPublicError))).handle("save", ({ payload }) => withStudioRequest((context) => seo.save(context, payload)).pipe(preservePublicErrors(isSeoPublicError))).handle("publish", ({ payload }) => withStudioRequest((context) => seo.publish(context, payload)).pipe(preservePublicErrors(isSeoPublicError)));
6510
5273
  })));
6511
5274
 
6512
5275
  //#endregion
@@ -6532,43 +5295,14 @@ const userAdministrationHttpLayer = HttpApiBuilder.group(StudioApi, "users", (ha
6532
5295
 
6533
5296
  //#endregion
6534
5297
  //#region ../platform-node/src/internal/http/api.ts
6535
- function studioApiLayer(bootstrap, authoringRevision) {
5298
+ function studioApiLayer(bootstrap, authoringRevision, components = [], formHandlers = {}) {
6536
5299
  const configuration = HttpApiBuilder.group(StudioApi, "configuration", (handlers) => handlers.handle("bootstrap", () => Effect.succeed(bootstrap)));
6537
- return authenticatedRouterHandlers(Layer.mergeAll(configuration, documentsApiLayer, mediaApiLayer(bootstrap.media.enabled, bootstrap.media.maxUploadBytes), collaborationApiLayer(authoringRevision), deliveryApiLayer, previewApiLayer, administrationApiLayer, userAdministrationHttpLayer).pipe(Layer.provideMerge(studioSchemaErrorLayer)));
5300
+ return authenticatedRouterHandlers(Layer.mergeAll(configuration, formsApiLayer(components, formHandlers), documentsApiLayer, contentApiLayer, mediaApiLayer(bootstrap.media.enabled, bootstrap.media.maxUploadBytes), collaborationApiLayer(authoringRevision), deliveryApiLayer, previewApiLayer, administrationApiLayer, userAdministrationHttpLayer).pipe(Layer.provideMerge(studioSchemaErrorLayer)));
6538
5301
  }
6539
5302
  function authenticatedRouterHandlers(handlers) {
6540
5303
  return handlers;
6541
5304
  }
6542
5305
 
6543
- //#endregion
6544
- //#region ../platform-node/src/internal/installation/server-modules.ts
6545
- function productModulesLayer(options) {
6546
- const accessControl = defaultAccessControlLayer;
6547
- const authoredDocuments = documentsLayer(options.bootstrap.documents, {
6548
- ...options.documentEngine,
6549
- components: options.documentEngine.components ?? [],
6550
- mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? "https://media.invalid"
6551
- }).pipe(Layer.provide(accessControl));
6552
- const documentAccess = Layer.merge(authoredDocuments, accessControl);
6553
- const activity = activityLayer.pipe(Layer.provide(accessControl));
6554
- const workingDocuments = workingDocumentsLayer(options.workingDocuments).pipe(Layer.provide(documentAccess));
6555
- return Layer.mergeAll(activity, documentAccess, workingDocuments);
6556
- }
6557
- function productModulesWithMediaLayer(options) {
6558
- const products = productModulesLayer({
6559
- ...options,
6560
- documentEngine: {
6561
- ...options.documentEngine,
6562
- mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? options.mediaPublicBaseUrl
6563
- }
6564
- });
6565
- const media = mediaLibraryLayer({
6566
- maxUploadBytes: options.bootstrap.media.maxUploadBytes,
6567
- publicBaseUrl: options.mediaPublicBaseUrl
6568
- }).pipe(Layer.provide(products));
6569
- return Layer.merge(products, media);
6570
- }
6571
-
6572
5306
  //#endregion
6573
5307
  //#region ../platform-node/src/internal/installation/error.ts
6574
5308
  var CmsInstallationError = class extends Schema.TaggedError()("Cms.InstallationError", {
@@ -6585,6 +5319,7 @@ function makeInstallationLayer(validated) {
6585
5319
  const options = validated.options;
6586
5320
  const core = options.composition.core;
6587
5321
  const productOptions = {
5322
+ baseUrl: options.baseUrl,
6588
5323
  bootstrap: options.bootstrap,
6589
5324
  documentEngine: {
6590
5325
  documentModel: options.documentModel,
@@ -6598,18 +5333,15 @@ function makeInstallationLayer(validated) {
6598
5333
  }).pipe(Layer.provide(Layer.merge(core, media.objects)));
6599
5334
  const productModules = validated.media.enabled ? enabledProductModules(validated.media.configuration) : productModulesLayer(productOptions).pipe(Layer.provide(core));
6600
5335
  const authentication = studioAuthenticationLayer.pipe(Layer.provide(core));
6601
- const initialization = initializationLayer({
6602
- initialSources: options.initialSources,
6603
- initializationToken: options.initializationToken
6604
- }).pipe(Layer.provide(Layer.merge(productModules, core)));
5336
+ const initialization = initializationLayer({ initializationToken: options.initializationToken }).pipe(Layer.provide(core));
6605
5337
  const users = userAdministrationLayer.pipe(Layer.provide(core));
6606
5338
  const tickets = studioRealtimeTicketsLayer(options.studioSecret).pipe(Layer.provide(core));
6607
5339
  const roomHost = studioRealtimeRoomHostLayer().pipe(Layer.provide(productModules));
6608
5340
  const gateway = studioRealtimeGatewayLayer.pipe(Layer.provide(Layer.mergeAll(productModules, roomHost, tickets)));
6609
5341
  const httpDependencies = Layer.mergeAll(core, productModules, gateway, initialization, roomHost, users).pipe(Layer.provideMerge(authentication), Layer.provideMerge(tickets));
6610
- const handlers = studioApiLayer(options.bootstrap, options.authoring.revision).pipe(Layer.provide(httpDependencies));
6611
- const httpApi = HttpApiBuilder.layer(StudioApi).pipe(Layer.provide(handlers), Layer.provide(HttpServer.layerServices));
6612
- return validated.media.enabled ? installationHttpLayer(options.bootstrap, httpApi, (context) => Effect.flatMap(MediaLibrary, (library) => library.cleanup(context))).pipe(Layer.provide(httpDependencies)) : installationHttpLayer(options.bootstrap, httpApi, () => Effect.succeed(MediaCleanupReport.make({ removedObjectKeys: [] }))).pipe(Layer.provide(httpDependencies));
5342
+ const handlers = studioApiLayer(options.bootstrap, options.authoring.revision, options.authoring.components, options.formHandlers).pipe(Layer.provide(httpDependencies));
5343
+ const httpApi = HttpApiBuilder.layer(StudioApi).pipe(Layer.provide(handlers), Layer.provide(HttpServer.layerServices), Layer.provideMerge(HttpRouter.layer));
5344
+ return validated.media.enabled ? installationHttpLayer(options.bootstrap, (context) => Effect.flatMap(MediaLibrary, (library) => library.cleanup(context))).pipe(Layer.provide(Layer.merge(httpDependencies, httpApi))) : installationHttpLayer(options.bootstrap, () => Effect.succeed(MediaCleanupReport.make({ removedObjectKeys: [] }))).pipe(Layer.provide(Layer.merge(httpDependencies, httpApi)));
6613
5345
  }
6614
5346
  function validateInstallationOptions(options) {
6615
5347
  if (options.bootstrap.media.enabled) {
@@ -6647,7 +5379,6 @@ function mapStartupError(layer, operation) {
6647
5379
 
6648
5380
  //#endregion
6649
5381
  //#region ../platform-node/src/Installation.ts
6650
- /** Starts auth, persistence, Studio realtime, delivery, health, and maintenance as one deep module. */
6651
5382
  const installCms$1 = Effect.fn("CmsInstallation.install")(function* (options) {
6652
5383
  return yield* startInstallation({
6653
5384
  cleanup: options.bootstrap.media.enabled ? options.cleanup : void 0,
@@ -6656,272 +5387,17 @@ const installCms$1 = Effect.fn("CmsInstallation.install")(function* (options) {
6656
5387
  });
6657
5388
  function createCmsInstallationLayer(options) {
6658
5389
  return installationLayer({
5390
+ baseUrl: options.baseUrl,
5391
+ formHandlers: options.formHandlers,
6659
5392
  authoring: options.authoring,
6660
5393
  bootstrap: options.bootstrap,
6661
5394
  composition: options.composition,
6662
5395
  studioSecret: options.studioSecret,
6663
5396
  documentModel: options.documentModel,
6664
- initialSources: options.initialSources,
6665
5397
  initializationToken: options.initializationToken
6666
5398
  });
6667
5399
  }
6668
5400
 
6669
- //#endregion
6670
- //#region ../s3/src/internal/client.ts
6671
- var S3RequestError = class extends Schema.TaggedError()("S3.RequestError", {
6672
- cause: Schema.Defect(),
6673
- operation: Schema.NonEmptyString
6674
- }) {};
6675
- function openAwsS3MediaObjectClient(options) {
6676
- const credentials = options.credentials ? {
6677
- accessKeyId: options.credentials.accessKeyId,
6678
- secretAccessKey: Redacted.value(options.credentials.secretAccessKey),
6679
- sessionToken: options.credentials.sessionToken ? Redacted.value(options.credentials.sessionToken) : void 0
6680
- } : void 0;
6681
- return makeAwsS3MediaObjectClient(new S3Client({
6682
- credentials,
6683
- endpoint: options.endpoint,
6684
- forcePathStyle: options.forcePathStyle,
6685
- region: options.region
6686
- }));
6687
- }
6688
- /** Wraps the official client; exported only through the package-private path for transport-level tests. */
6689
- function makeAwsS3MediaObjectClient(client) {
6690
- const request = (operation, send) => Effect.tryPromise({
6691
- catch: (cause) => new S3RequestError({
6692
- cause,
6693
- operation
6694
- }),
6695
- try: send
6696
- });
6697
- return {
6698
- close: Effect.sync(() => client.destroy()),
6699
- copyObject: (input) => request("MediaObjectStore.S3.copyObject", (signal) => client.send(new CopyObjectCommand({
6700
- Bucket: input.bucket,
6701
- CacheControl: input.cacheControl,
6702
- ContentType: input.contentType,
6703
- CopySource: input.copySource,
6704
- Key: input.key,
6705
- MetadataDirective: "REPLACE"
6706
- }), { abortSignal: signal })).pipe(Effect.asVoid),
6707
- createBucket: (input) => request("MediaObjectStore.S3.createBucket", (signal) => client.send(new CreateBucketCommand({
6708
- Bucket: input.bucket,
6709
- CreateBucketConfiguration: input.locationConstraint ? { LocationConstraint: input.locationConstraint } : void 0
6710
- }), { abortSignal: signal })).pipe(Effect.asVoid),
6711
- deleteObject: (input) => request("MediaObjectStore.S3.deleteObject", (signal) => client.send(new DeleteObjectCommand({
6712
- Bucket: input.bucket,
6713
- Key: input.key
6714
- }), { abortSignal: signal })).pipe(Effect.asVoid),
6715
- headBucket: (input) => request("MediaObjectStore.S3.headBucket", (signal) => client.send(new HeadBucketCommand({ Bucket: input.bucket }), { abortSignal: signal })).pipe(Effect.asVoid),
6716
- headObject: (input) => request("MediaObjectStore.S3.headObject", (signal) => client.send(new HeadObjectCommand({
6717
- Bucket: input.bucket,
6718
- Key: input.key
6719
- }), { abortSignal: signal })).pipe(Effect.map((output) => ({
6720
- contentLength: output.ContentLength,
6721
- contentType: output.ContentType
6722
- }))),
6723
- listObjects: (input) => request("MediaObjectStore.S3.listObjects", (signal) => client.send(new ListObjectsV2Command({
6724
- Bucket: input.bucket,
6725
- ContinuationToken: input.continuationToken,
6726
- Prefix: input.prefix
6727
- }), { abortSignal: signal })).pipe(Effect.map((output) => ({
6728
- isTruncated: output.IsTruncated === true,
6729
- nextContinuationToken: output.NextContinuationToken,
6730
- objects: (output.Contents ?? []).map((object) => ({
6731
- key: object.Key,
6732
- size: object.Size
6733
- }))
6734
- }))),
6735
- putObject: (input) => request("MediaObjectStore.S3.putObject", (signal) => client.send(new PutObjectCommand({
6736
- Body: NodeStream.toReadableNever(Stream.fromAsyncIterable(input.body, (cause) => cause)),
6737
- Bucket: input.bucket,
6738
- CacheControl: input.cacheControl,
6739
- ContentLength: input.contentLength,
6740
- ContentType: input.contentType,
6741
- Key: input.key
6742
- }), { abortSignal: signal })).pipe(Effect.asVoid)
6743
- };
6744
- }
6745
-
6746
- //#endregion
6747
- //#region ../s3/src/internal/media-object-store.ts
6748
- const mediaPrefix = "media/";
6749
- const replaceableCacheControl = "public, max-age=0, must-revalidate";
6750
- const isWriteBodyError = Schema.is(Schema.Union([MediaInvalid, MediaStorageError]));
6751
- /** Owns and closes the official AWS client for the lifetime of the layer. */
6752
- function s3MediaObjectStoreLayer(options) {
6753
- return Layer.effect(MediaObjectStore, resolveOptions(options).pipe(Effect.flatMap((resolved) => Effect.acquireRelease(Effect.sync(() => openAwsS3MediaObjectClient(resolved)), (client) => client.close).pipe(Effect.tap((client) => provisionBucket(resolved, client)), Effect.map((client) => makeStore(resolved, client))))));
6754
- }
6755
- function makeStore(options, client) {
6756
- return {
6757
- list: listObjects(options.bucket, client),
6758
- promote: (input) => promoteObject(options.bucket, input, client),
6759
- put: (input) => putObject(options.bucket, input, client),
6760
- remove: (objectKey) => providerRequest("MediaObjectStore.S3.remove", client.deleteObject({
6761
- bucket: options.bucket,
6762
- key: objectKey
6763
- })),
6764
- stat: (objectKey) => statObject(options.bucket, objectKey, client)
6765
- };
6766
- }
6767
- function promoteObject(bucket, input, client) {
6768
- return statObject(bucket, input.sourceObjectKey, client).pipe(Effect.flatMap((source) => Option.isSome(source) && source.value.byteSize === input.byteSize && source.value.mimeType === input.mimeType ? Effect.void : Effect.fail(storageError("MediaObjectStore.S3.promote.source", "The staged object metadata does not match its intent."))), Effect.flatMap(() => providerRequest("MediaObjectStore.S3.promote.copy", client.copyObject({
6769
- bucket,
6770
- cacheControl: replaceableCacheControl,
6771
- contentType: input.mimeType,
6772
- copySource: copySource(bucket, input.sourceObjectKey),
6773
- key: input.destinationObjectKey
6774
- }))), Effect.flatMap(() => statObject(bucket, input.destinationObjectKey, client)), Effect.flatMap((destination) => Option.isSome(destination) && destination.value.byteSize === input.byteSize && destination.value.mimeType === input.mimeType ? Effect.succeed(destination.value) : Effect.fail(storageError("MediaObjectStore.S3.promote.verify", "The promoted object metadata does not match its intent."))));
6775
- }
6776
- function copySource(bucket, objectKey) {
6777
- return `/${encodeURIComponent(bucket)}/${objectKey.split("/").map(encodeURIComponent).join("/")}`;
6778
- }
6779
- function putObject(bucket, input, client) {
6780
- return Effect.gen(function* () {
6781
- const body = yield* validatedWriteBody(input);
6782
- const iterable = yield* Stream.toAsyncIterableEffect(body);
6783
- yield* client.putObject({
6784
- body: iterable,
6785
- bucket,
6786
- cacheControl: replaceableCacheControl,
6787
- contentLength: input.byteSize,
6788
- contentType: input.mimeType,
6789
- key: input.objectKey
6790
- }).pipe(Effect.catch((failure) => isWriteBodyError(failure.cause) ? Effect.fail(failure.cause) : Effect.fail(providerError("MediaObjectStore.S3.put", failure))));
6791
- return {
6792
- byteSize: input.byteSize,
6793
- mimeType: input.mimeType,
6794
- objectKey: input.objectKey
6795
- };
6796
- });
6797
- }
6798
- const validatedWriteBody = Effect.fn("MediaObjectStore.S3.validateWriteBody")(function* (input) {
6799
- const byteLength = yield* Ref.make(0);
6800
- return input.body.pipe(Stream.mapEffect((chunk) => Effect.gen(function* () {
6801
- if ((yield* Ref.updateAndGet(byteLength, (current) => current + chunk.byteLength)) > input.byteSize) return yield* storageError("MediaObjectStore.S3.put.body", "The object body exceeds its declared byte size.");
6802
- return chunk;
6803
- })), Stream.concat(Stream.fromEffect(Ref.get(byteLength).pipe(Effect.flatMap((actual) => actual === input.byteSize ? Effect.void : Effect.fail(storageError("MediaObjectStore.S3.put.body", "The object body does not match its declared byte size."))))).pipe(Stream.drain)));
6804
- });
6805
- function statObject(bucket, objectKey, client) {
6806
- return client.headObject({
6807
- bucket,
6808
- key: objectKey
6809
- }).pipe(Effect.matchEffect({
6810
- onFailure: (failure) => isMissingObject(failure.cause) ? Effect.succeed(Option.none()) : Effect.fail(providerError("MediaObjectStore.S3.stat", failure)),
6811
- onSuccess: (output) => decodeStoredObject("MediaObjectStore.S3.stat", objectKey, output.contentLength, output.contentType).pipe(Effect.asSome)
6812
- }));
6813
- }
6814
- function listObjects(bucket, client) {
6815
- return Stream.paginate({ seenTokens: /* @__PURE__ */ new Set() }, (state) => providerRequest("MediaObjectStore.S3.list", client.listObjects({
6816
- bucket,
6817
- continuationToken: state.continuationToken,
6818
- prefix: mediaPrefix
6819
- })).pipe(Effect.flatMap((page) => Effect.gen(function* () {
6820
- return [yield* Effect.forEach(page.objects, decodeListedObject), yield* nextListState(state, page)];
6821
- }))));
6822
- }
6823
- function decodeListedObject(object) {
6824
- if (object.key === void 0) return Effect.fail(storageError("MediaObjectStore.S3.list", "The provider returned object metadata without a key."));
6825
- return decodeStoredObject("MediaObjectStore.S3.list", object.key, object.size);
6826
- }
6827
- function nextListState(state, page) {
6828
- if (!page.isTruncated) return Effect.succeedNone;
6829
- const token = page.nextContinuationToken;
6830
- if (!token || token === state.continuationToken || state.seenTokens.has(token)) return Effect.fail(storageError("MediaObjectStore.S3.list", "The provider returned an invalid continuation token."));
6831
- return Effect.succeedSome({
6832
- continuationToken: token,
6833
- seenTokens: /* @__PURE__ */ new Set([...state.seenTokens, token])
6834
- });
6835
- }
6836
- function decodeStoredObject(operation, objectKey, byteSize, contentType) {
6837
- const decodedObjectKey = Schema.decodeOption(MediaObjectKey)(objectKey);
6838
- const decodedByteSize = Schema.decodeUnknownOption(MediaByteSize)(byteSize);
6839
- if (Option.isNone(decodedObjectKey) || Option.isNone(decodedByteSize)) return Effect.fail(storageError(operation, "The provider returned invalid object metadata."));
6840
- const decodedMimeType = Schema.decodeUnknownOption(ImageMimeType)(contentType);
6841
- const storedObject = {
6842
- byteSize: decodedByteSize.value,
6843
- objectKey: decodedObjectKey.value
6844
- };
6845
- return Effect.succeed(Option.isSome(decodedMimeType) ? {
6846
- ...storedObject,
6847
- mimeType: decodedMimeType.value
6848
- } : storedObject);
6849
- }
6850
- function providerRequest(operation, request) {
6851
- return request.pipe(Effect.mapError((failure) => providerError(operation, failure)));
6852
- }
6853
- function provisionBucket(options, client) {
6854
- if (!options.provisionBucket) return Effect.void;
6855
- return client.headBucket({ bucket: options.bucket }).pipe(Effect.catch((failure) => isMissingBucket(failure.cause) ? createMissingBucket(options, client) : Effect.fail(providerError("MediaObjectStore.S3.bucket.head", failure))));
6856
- }
6857
- function createMissingBucket(options, client) {
6858
- return client.createBucket({
6859
- bucket: options.bucket,
6860
- locationConstraint: options.endpoint || options.region === "us-east-1" ? void 0 : options.region
6861
- }).pipe(Effect.catch((failure) => isBucketAlreadyOwned(failure.cause) ? providerRequest("MediaObjectStore.S3.bucket.verify", client.headBucket({ bucket: options.bucket })) : Effect.fail(providerError("MediaObjectStore.S3.bucket.create", failure))));
6862
- }
6863
- const ProviderFailureDetails = Schema.Struct({
6864
- $metadata: Schema.optionalKey(Schema.Struct({
6865
- attempts: Schema.optionalKey(Schema.Finite),
6866
- httpStatusCode: Schema.optionalKey(Schema.Finite)
6867
- })),
6868
- name: Schema.optionalKey(Schema.String)
6869
- });
6870
- const decodeProviderFailure = Schema.decodeUnknownOption(ProviderFailureDetails);
6871
- function providerError(operation, failure) {
6872
- const details = decodeProviderFailure(failure.cause);
6873
- const diagnostics = [];
6874
- if (Option.isSome(details)) {
6875
- if (details.value.name) diagnostics.push(`name=${details.value.name}`);
6876
- if (details.value.$metadata?.httpStatusCode !== void 0) diagnostics.push(`status=${details.value.$metadata.httpStatusCode}`);
6877
- if (details.value.$metadata?.attempts !== void 0) diagnostics.push(`attempts=${details.value.$metadata.attempts}`);
6878
- }
6879
- return storageError(operation, `The S3 provider request failed${diagnostics.length > 0 ? ` (${diagnostics.join(", ")}).` : "."}`);
6880
- }
6881
- function isMissingObject(cause) {
6882
- const details = decodeProviderFailure(cause);
6883
- if (Option.isNone(details)) return false;
6884
- return details.value.name === "NoSuchKey" || details.value.name === "NotFound";
6885
- }
6886
- function isMissingBucket(cause) {
6887
- const details = decodeProviderFailure(cause);
6888
- if (Option.isNone(details)) return false;
6889
- return details.value.name === "NoSuchBucket" || details.value.$metadata?.httpStatusCode === 404;
6890
- }
6891
- function isBucketAlreadyOwned(cause) {
6892
- const details = decodeProviderFailure(cause);
6893
- return Option.isSome(details) && details.value.name === "BucketAlreadyOwnedByYou";
6894
- }
6895
- function resolveOptions(options) {
6896
- const bucket = options.bucket.trim();
6897
- const region = options.region.trim();
6898
- const endpoint = options.endpoint?.trim();
6899
- if (!bucket || !region || options.endpoint !== void 0 && !endpoint) return Effect.fail(storageError("MediaObjectStore.S3.configure", "The bucket, region, and configured endpoint must be non-empty."));
6900
- if (options.credentials) {
6901
- const secretAccessKey = Redacted.value(options.credentials.secretAccessKey);
6902
- const sessionToken = options.credentials.sessionToken && Redacted.value(options.credentials.sessionToken);
6903
- if (!options.credentials.accessKeyId.trim() || !secretAccessKey || sessionToken === "") return Effect.fail(storageError("MediaObjectStore.S3.configure", "The configured credentials must be non-empty."));
6904
- }
6905
- return Effect.succeed({
6906
- ...options,
6907
- bucket,
6908
- credentials: options.credentials ? {
6909
- ...options.credentials,
6910
- accessKeyId: options.credentials.accessKeyId.trim()
6911
- } : void 0,
6912
- endpoint,
6913
- forcePathStyle: options.forcePathStyle ?? false,
6914
- provisionBucket: options.provisionBucket ?? false,
6915
- region
6916
- });
6917
- }
6918
- function storageError(operation, message) {
6919
- return new MediaStorageError({
6920
- cause: new Error(message),
6921
- operation
6922
- });
6923
- }
6924
-
6925
5401
  //#endregion
6926
5402
  //#region ../sql-pg/src/internal/database/table-helpers.ts
6927
5403
  const transactionTimestamp = sql`transaction_timestamp()`;
@@ -6935,6 +5411,7 @@ const ACTIVITY_TARGET_KINDS = [
6935
5411
  "document",
6936
5412
  "media",
6937
5413
  "media-library",
5414
+ "seo",
6938
5415
  "user"
6939
5416
  ];
6940
5417
  const IDENTIFIED_ACTIVITY_TARGET_KINDS = [
@@ -6961,8 +5438,8 @@ const activityEntries = pgTable("activity_entries", {
6961
5438
  }, (table) => [
6962
5439
  check("activity_entries_action_check", isOneOf(table.action, ACTIVITY_ACTIONS)),
6963
5440
  check("activity_entries_target_kind_check", isOneOf(table.targetKind, ACTIVITY_TARGET_KINDS)),
6964
- check("activity_entries_target_check", sql`(${table.targetKind} = 'media-library' AND ${table.targetId} IS NULL)
6965
- OR (${table.targetKind} <> 'media-library' AND ${table.targetId} IS NOT NULL)`),
5441
+ check("activity_entries_target_check", sql`(${table.targetKind} in ('media-library', 'seo') AND ${table.targetId} IS NULL)
5442
+ OR (${table.targetKind} not in ('media-library', 'seo') AND ${table.targetId} IS NOT NULL)`),
6966
5443
  check("activity_entries_actor_roles_check", sql`jsonb_typeof(${table.actorRoles}) = 'array'`),
6967
5444
  check("activity_entries_details_check", sql`jsonb_typeof(${table.details}) = 'object'`),
6968
5445
  index("activity_entries_history").on(table.occurredAt.desc(), table.id.desc()),
@@ -7053,27 +5530,30 @@ function activityEntryValues(entry) {
7053
5530
  };
7054
5531
  }
7055
5532
  function activityEntriesEqual(left, right) {
7056
- return isDeepStrictEqual(Schema.encodeSync(ActivityEntry)(left), Schema.encodeSync(ActivityEntry)(right));
5533
+ return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(ActivityEntry)(left)), Result.getOrThrow(Schema.encodeResult(ActivityEntry)(right)));
7057
5534
  }
7058
5535
  function decodeActivityTarget(kind, id) {
7059
- const value = kind === "document" ? {
5536
+ if (kind === "document") return Schema.decodeUnknownEffect(ActivityTarget)({
7060
5537
  documentId: id,
7061
5538
  kind
7062
- } : kind === "media" ? {
5539
+ });
5540
+ if (kind === "media") return Schema.decodeUnknownEffect(ActivityTarget)({
7063
5541
  kind,
7064
5542
  mediaId: id
7065
- } : kind === "user" ? {
5543
+ });
5544
+ if (kind === "user") return Schema.decodeUnknownEffect(ActivityTarget)({
7066
5545
  kind,
7067
5546
  userId: id
7068
- } : { kind };
7069
- return Schema.decodeUnknownEffect(ActivityTarget)(value);
5547
+ });
5548
+ return Schema.decodeEffect(ActivityTarget)({ kind });
7070
5549
  }
7071
5550
  function activityTargetId(entry) {
7072
5551
  switch (entry.target.kind) {
7073
5552
  case "document": return entry.target.documentId;
7074
5553
  case "media": return entry.target.mediaId;
7075
5554
  case "user": return entry.target.userId;
7076
- case "media-library": return;
5555
+ case "media-library":
5556
+ case "seo": return;
7077
5557
  }
7078
5558
  }
7079
5559
  function activityPage(entries, limit, total) {
@@ -7372,7 +5852,8 @@ function targetColumns(target) {
7372
5852
  targetId: target.userId,
7373
5853
  targetKind: target.kind
7374
5854
  };
7375
- case "media-library": return { targetKind: target.kind };
5855
+ case "media-library":
5856
+ case "seo": return { targetKind: target.kind };
7376
5857
  }
7377
5858
  }
7378
5859
  function decodeOptionalTarget(kind, id) {
@@ -7380,7 +5861,7 @@ function decodeOptionalTarget(kind, id) {
7380
5861
  return decodeActivityTarget(kind, id).pipe(Effect.mapError(() => new ChangeMismatch({})));
7381
5862
  }
7382
5863
  function draftEqual(change, draft) {
7383
- return isDeepStrictEqual(Schema.encodeSync(ChangeDraft)(draftFromChange(change)), Schema.encodeSync(ChangeDraft)(draft));
5864
+ return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(ChangeDraft)(draftFromChange(change))), Result.getOrThrow(Schema.encodeResult(ChangeDraft)(draft)));
7384
5865
  }
7385
5866
  function draftFromChange(change) {
7386
5867
  const value = {
@@ -7397,7 +5878,7 @@ function draftFromChange(change) {
7397
5878
  });
7398
5879
  }
7399
5880
  function changeEqual(left, right) {
7400
- return isDeepStrictEqual(Schema.encodeSync(PendingChange)(left), Schema.encodeSync(PendingChange)(right));
5881
+ return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(PendingChange)(left)), Result.getOrThrow(Schema.encodeResult(PendingChange)(right)));
7401
5882
  }
7402
5883
  function boundedLimit(limit) {
7403
5884
  return Math.min(100, Math.max(0, Math.trunc(limit)));
@@ -7505,6 +5986,26 @@ const embeddedMigrations = [
7505
5986
  folderMillis: 1788866265e3,
7506
5987
  hash: "24c56a37796caf8d76c0a97128a382b31e71808aa5b0ca7e5bac96795f9e16c7",
7507
5988
  name: "20260908111745_classy_johnny_storm"
5989
+ },
5990
+ {
5991
+ sql: [
5992
+ "CREATE TABLE \"seo_media_references\" (\n \"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n \"scope\" text NOT NULL,\n \"media_id\" uuid NOT NULL,\n CONSTRAINT \"seo_media_references_scope_media_unique\" UNIQUE(\"scope\",\"media_id\"),\n CONSTRAINT \"seo_media_references_scope_check\" CHECK (\"scope\" in ('working', 'published'))\n);\n",
5993
+ "\nCREATE TABLE \"seo_revisions\" (\n \"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n \"settings\" jsonb NOT NULL,\n \"actor_id\" text NOT NULL,\n \"actor_email\" text NOT NULL,\n \"actor_name\" text NOT NULL,\n \"actor_roles\" jsonb NOT NULL,\n \"request_id\" uuid NOT NULL,\n \"reason\" text,\n \"created_at\" timestamp with time zone DEFAULT transaction_timestamp() NOT NULL,\n CONSTRAINT \"seo_revisions_actor_roles_check\" CHECK (jsonb_typeof(\"actor_roles\") = 'array'),\n CONSTRAINT \"seo_revisions_settings_check\" CHECK (jsonb_typeof(\"settings\") = 'object')\n);\n",
5994
+ "\nCREATE TABLE \"seo_state\" (\n \"singleton\" boolean PRIMARY KEY DEFAULT true,\n \"working_revision_id\" uuid NOT NULL,\n \"published_revision_id\" uuid NOT NULL,\n CONSTRAINT \"seo_state_singleton_check\" CHECK (\"singleton\")\n);\n",
5995
+ "\nCREATE INDEX \"seo_media_references_media\" ON \"seo_media_references\" (\"media_id\");",
5996
+ "\nCREATE INDEX \"seo_revisions_history\" ON \"seo_revisions\" (\"created_at\" DESC NULLS LAST,\"id\" DESC NULLS LAST);",
5997
+ "\nALTER TABLE \"seo_media_references\" ADD CONSTRAINT \"seo_media_references_media_id_media_assets_id_fkey\" FOREIGN KEY (\"media_id\") REFERENCES \"media_assets\"(\"id\") ON DELETE RESTRICT;",
5998
+ "\nALTER TABLE \"seo_state\" ADD CONSTRAINT \"seo_state_working_revision_fkey\" FOREIGN KEY (\"working_revision_id\") REFERENCES \"seo_revisions\"(\"id\") ON DELETE RESTRICT;",
5999
+ "\nALTER TABLE \"seo_state\" ADD CONSTRAINT \"seo_state_published_revision_fkey\" FOREIGN KEY (\"published_revision_id\") REFERENCES \"seo_revisions\"(\"id\") ON DELETE RESTRICT;",
6000
+ "\nALTER TABLE \"activity_entries\" DROP CONSTRAINT \"activity_entries_action_check\", ADD CONSTRAINT \"activity_entries_action_check\" CHECK (\"action\" in ('document.created', 'document.updated', 'document.moved', 'document.trashed', 'document.revision.restored', 'document.untrashed', 'document.published', 'document.unpublished', 'media.uploaded', 'media.replaced', 'media.removed', 'media.cleaned', 'seo.saved', 'seo.published', 'user.created', 'user.removed', 'user.role.changed', 'user.sessions.revoked', 'authentication.signed-in', 'authentication.signed-out', 'authentication.password.changed', 'authentication.password.reset'));",
6001
+ "\nALTER TABLE \"activity_entries\" DROP CONSTRAINT \"activity_entries_target_kind_check\", ADD CONSTRAINT \"activity_entries_target_kind_check\" CHECK (\"target_kind\" in ('document', 'media', 'media-library', 'seo', 'user'));",
6002
+ "\nALTER TABLE \"activity_entries\" DROP CONSTRAINT \"activity_entries_target_check\", ADD CONSTRAINT \"activity_entries_target_check\" CHECK ((\"target_kind\" in ('media-library', 'seo') AND \"target_id\" IS NULL)\n OR (\"target_kind\" not in ('media-library', 'seo') AND \"target_id\" IS NOT NULL));",
6003
+ "\nALTER TABLE \"pending_changes\" DROP CONSTRAINT \"pending_changes_action_check\", ADD CONSTRAINT \"pending_changes_action_check\" CHECK (\"action\" in ('document.created', 'document.updated', 'document.moved', 'document.trashed', 'document.revision.restored', 'document.untrashed', 'document.published', 'document.unpublished', 'media.uploaded', 'media.replaced', 'media.removed', 'media.cleaned', 'seo.saved', 'seo.published', 'user.created', 'user.removed', 'user.role.changed', 'user.sessions.revoked', 'authentication.signed-in', 'authentication.signed-out', 'authentication.password.changed', 'authentication.password.reset'));"
6004
+ ],
6005
+ bps: true,
6006
+ folderMillis: 1789233351e3,
6007
+ hash: "8e6a6abf5385ab3ecf8eb083c1923ac9c74d72aa71bd1d444b7402d3e7e77000",
6008
+ name: "20260912171551_charming_valkyrie"
7508
6009
  }
7509
6010
  ];
7510
6011
 
@@ -7603,12 +6104,16 @@ const initializationState = pgTable("initialization_state", {
7603
6104
 
7604
6105
  //#endregion
7605
6106
  //#region ../sql-pg/src/internal/database/migrations.ts
7606
- /** Applies generated CMS migrations and initializes required application state. */
6107
+ const MIGRATION_LOCK_KEY = "cms:database-migrations:v1";
7607
6108
  const migrateCmsDatabase = Effect.fn("CmsDatabase.migrate")(function* () {
6109
+ const postgres = yield* PgClient.PgClient;
7608
6110
  const database = yield* PgDrizzle.makeWithDefaults();
7609
- yield* migrate(embeddedMigrations, database._.session, "__drizzle_migrations");
7610
- yield* database.insert(cmsState).values({}).onConflictDoNothing({ target: cmsState.singleton });
7611
- yield* database.insert(initializationState).values({}).onConflictDoNothing({ target: initializationState.singleton });
6111
+ yield* postgres.withTransaction(Effect.gen(function* () {
6112
+ yield* postgres.unsafe("select pg_advisory_xact_lock(hashtextextended($1, 0))", [MIGRATION_LOCK_KEY]);
6113
+ yield* migrate(embeddedMigrations, database._.session, "__drizzle_migrations");
6114
+ yield* database.insert(cmsState).values({}).onConflictDoNothing({ target: cmsState.singleton });
6115
+ yield* database.insert(initializationState).values({}).onConflictDoNothing({ target: initializationState.singleton });
6116
+ }));
7612
6117
  });
7613
6118
  const cmsDatabaseMigrationLayer = Layer.effectDiscard(migrateCmsDatabase());
7614
6119
 
@@ -7625,13 +6130,15 @@ const applyDocumentCommitPlan = Effect.fn("DocumentStore.Drizzle.applyCommitPlan
7625
6130
  const revisionId = yield* appendRevision(transaction, plan, activity);
7626
6131
  yield* transaction.insert(documentHeads).values({
7627
6132
  authoredRevisionId: revisionId,
7628
- documentId: plan.documentId
6133
+ documentId: plan.documentId,
6134
+ publishedRevisionId: plan.publish ? revisionId : null
7629
6135
  });
7630
6136
  return;
7631
6137
  }
7632
6138
  case "document.update": {
7633
6139
  const revisionId = yield* appendRevision(transaction, plan, activity);
7634
6140
  yield* updateAuthoredHead(transaction, plan.documentId, revisionId);
6141
+ if (plan.publish) yield* transaction.update(documentHeads).set({ publishedRevisionId: revisionId }).where(eq(documentHeads.documentId, plan.documentId));
7635
6142
  yield* touchDocument(transaction, plan.documentId);
7636
6143
  return;
7637
6144
  }
@@ -7712,7 +6219,7 @@ function mapActivePathConflict(sourcePath) {
7712
6219
  const causeError = Cause$1.findErrorOption(error.cause);
7713
6220
  if (Option$1.isNone(causeError) || !SqlError.isSqlError(causeError.value)) return error;
7714
6221
  const reason = causeError.value.reason;
7715
- return reason._tag === "UniqueViolation" && reason.constraint === "documents_active_path" ? new DocumentPathConflict({ sourcePath }) : error;
6222
+ return Predicate.isTagged("UniqueViolation")(reason) && reason.constraint === "documents_active_path" ? new DocumentPathConflict({ sourcePath }) : error;
7716
6223
  }));
7717
6224
  }
7718
6225
  function updateAuthoredHead(transaction, documentId, revisionId) {
@@ -8026,7 +6533,7 @@ const isRepositoryDomainError = Schema.is(RepositoryDomainError);
8026
6533
  const isDocumentNotFound = Schema.is(DocumentNotFound);
8027
6534
  const isDocumentMediaUnavailable = Schema.is(DocumentMediaUnavailable);
8028
6535
  const isDocumentPersistenceError = Schema.is(DocumentPersistenceError);
8029
- const systemActivity = ActivityContext.make({
6536
+ const systemActivity$1 = ActivityContext.make({
8030
6537
  actor: {
8031
6538
  email: "system@cms.invalid",
8032
6539
  id: "system",
@@ -8049,15 +6556,15 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
8049
6556
  const state = yield* Schema.decodeUnknownEffect(CmsStateSelect)(stateRow);
8050
6557
  const plans = [input.plan, ...input.additionalPlans ?? []];
8051
6558
  const references = plans.flatMap((plan) => input.mediaReferences.filter((reference) => reference.documentId === plan.documentId));
8052
- if (references.length !== input.mediaReferences.length) return yield* persistenceFailure$2("DocumentStore.commit", /* @__PURE__ */ new Error("Unexpected media reference owner."));
6559
+ if (references.length !== input.mediaReferences.length) return yield* persistenceFailure$3("DocumentStore.commit", /* @__PURE__ */ new Error("Unexpected media reference owner."));
8053
6560
  yield* lockMediaReferenceKeys(transaction, references.map((reference) => reference.objectKey));
8054
- yield* rejectDeletingMedia(transaction, references);
8055
- const activity = input.activity ?? systemActivity;
6561
+ yield* rejectUnavailableMedia(transaction, references);
6562
+ const activity = input.activity ?? systemActivity$1;
8056
6563
  for (const plan of plans) {
8057
6564
  yield* applyDocumentCommitPlan(transaction, plan, activity);
8058
6565
  yield* replaceDocumentMediaReferences(transaction, plan.documentId, references.filter((reference) => reference.documentId === plan.documentId));
8059
6566
  }
8060
- const snapshot = yield* readSnapshot(transaction, state.generation);
6567
+ const snapshot = yield* readSnapshot$1(transaction, state.generation);
8061
6568
  for (const plan of plans) {
8062
6569
  const affected = yield* sourceForMutation(snapshot, plan.documentId, plan.action === "document.remove");
8063
6570
  yield* recordDocumentActivity(transaction, affected, plan, activity);
@@ -8079,15 +6586,15 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
8079
6586
  const load = Effect.fn("DocumentStore.Drizzle.load")((after) => database.transaction((transaction) => Effect.gen(function* () {
8080
6587
  const generation = yield* readGeneration(transaction);
8081
6588
  if (after === generation.toString()) return void 0;
8082
- return yield* readSnapshot(transaction, generation);
6589
+ return yield* readSnapshot$1(transaction, generation);
8083
6590
  }), {
8084
6591
  accessMode: "read only",
8085
6592
  isolationLevel: "repeatable read"
8086
- }).pipe(mapPersistenceFailure$2("DocumentStore.Drizzle.load")));
6593
+ }).pipe(mapPersistenceFailure$3("DocumentStore.Drizzle.load")));
8087
6594
  const synchronizeMediaReferences = Effect.fn("DocumentStore.Drizzle.synchronizeMediaReferences")((input) => database.transaction((transaction) => Effect.gen(function* () {
8088
6595
  const stateRows = yield* transaction.select().from(cmsState).where(eq(cmsState.singleton, true)).limit(2).for("update");
8089
6596
  const stateRow = stateRows[0];
8090
- if (stateRows.length !== 1 || !stateRow) return yield* persistenceFailure$2("DocumentStore.Drizzle.synchronizeMediaReferences", /* @__PURE__ */ new Error(`Expected exactly one document generation row; received ${stateRows.length}.`));
6597
+ if (stateRows.length !== 1 || !stateRow) return yield* persistenceFailure$3("DocumentStore.Drizzle.synchronizeMediaReferences", /* @__PURE__ */ new Error(`Expected exactly one document generation row; received ${stateRows.length}.`));
8091
6598
  const state = yield* Schema.decodeUnknownEffect(CmsStateSelect)(stateRow);
8092
6599
  if (state.generation.toString() !== input.expectedGeneration) return yield* new DocumentGenerationConflict({
8093
6600
  actualGeneration: DocumentGeneration.make(String(state.generation)),
@@ -8095,7 +6602,7 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
8095
6602
  });
8096
6603
  const references = uniqueMediaReferences(input.references);
8097
6604
  yield* lockMediaReferenceKeys(transaction, references.map((reference) => reference.objectKey));
8098
- yield* rejectDeletingMedia(transaction, references);
6605
+ yield* rejectUnavailableMedia(transaction, references);
8099
6606
  yield* transaction.delete(documentMediaReferences);
8100
6607
  if (references.length > 0) yield* transaction.insert(documentMediaReferences).values(references.map(referenceValues));
8101
6608
  })).pipe(mapSynchronizationFailure("DocumentStore.Drizzle.synchronizeMediaReferences")));
@@ -8111,12 +6618,12 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
8111
6618
  const cursor = query.cursor ? or(lt(documentRevisions.createdAt, DateTime.formatIso(query.cursor.createdAt)), and(eq(documentRevisions.createdAt, DateTime.formatIso(query.cursor.createdAt)), lt(documentRevisions.id, query.cursor.revisionId))) : void 0;
8112
6619
  const rows = yield* transaction.select().from(documentRevisions).where(and(eq(documentRevisions.documentId, query.documentId), cursor)).orderBy(desc(documentRevisions.createdAt), desc(documentRevisions.id)).limit(query.limit + 1);
8113
6620
  const total = (yield* transaction.select({ value: count() }).from(documentRevisions).where(eq(documentRevisions.documentId, query.documentId)))[0]?.value;
8114
- if (total === void 0) return yield* persistenceFailure$2("DocumentStore.Drizzle.revisions", /* @__PURE__ */ new Error("Documents persistence did not return a history total."));
6621
+ if (total === void 0) return yield* persistenceFailure$3("DocumentStore.Drizzle.revisions", /* @__PURE__ */ new Error("Documents persistence did not return a history total."));
8115
6622
  return revisionPage(yield* Effect.forEach(rows, decodeDocumentRevision), query.limit, total);
8116
6623
  }), {
8117
6624
  accessMode: "read only",
8118
6625
  isolationLevel: "repeatable read"
8119
- }).pipe(mapPersistenceFailure$2("DocumentStore.Drizzle.revisions")));
6626
+ }).pipe(mapPersistenceFailure$3("DocumentStore.Drizzle.revisions")));
8120
6627
  return DocumentStore.of({
8121
6628
  commit,
8122
6629
  load,
@@ -8157,8 +6664,13 @@ const recordDocumentActivity = Effect.fn("DocumentStore.Drizzle.recordActivity")
8157
6664
  };
8158
6665
  if (plan.action !== "document.publish" && plan.action !== "document.unpublish") values.id = source.revisionId;
8159
6666
  yield* database.insert(activityEntries).values(values);
6667
+ if ((plan.action === "document.create" || plan.action === "document.update") && plan.publish) yield* database.insert(activityEntries).values({
6668
+ ...values,
6669
+ action: "document.published",
6670
+ id: void 0
6671
+ });
8160
6672
  });
8161
- const readSnapshot = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (database, generation) {
6673
+ const readSnapshot$1 = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (database, generation) {
8162
6674
  const rows = yield* database.select({
8163
6675
  authored: authoredRevision,
8164
6676
  created: createdRevision,
@@ -8171,12 +6683,12 @@ const readSnapshot = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (
8171
6683
  const readGeneration = Effect.fn("DocumentStore.Drizzle.readGeneration")(function* (database) {
8172
6684
  const rows = yield* database.select().from(cmsState);
8173
6685
  const row = rows[0];
8174
- if (rows.length !== 1 || !row) return yield* persistenceFailure$2("DocumentStore.Drizzle.readGeneration", /* @__PURE__ */ new Error(`Expected exactly one document generation row; received ${rows.length}.`));
6686
+ if (rows.length !== 1 || !row) return yield* persistenceFailure$3("DocumentStore.Drizzle.readGeneration", /* @__PURE__ */ new Error(`Expected exactly one document generation row; received ${rows.length}.`));
8175
6687
  return (yield* Schema.decodeUnknownEffect(CmsStateSelect)(row)).generation;
8176
6688
  });
8177
6689
  const sourceForMutation = Effect.fn("DocumentStore.Drizzle.sourceForMutation")(function* (snapshot, documentId, deleted) {
8178
6690
  const source = (deleted ? snapshot.deleted : snapshot.active).find((candidate) => candidate.documentId === documentId);
8179
- if (!source) return yield* persistenceFailure$2("DocumentStore.Drizzle.sourceForMutation", /* @__PURE__ */ new Error(`Committed document "${documentId}" is missing from its resulting snapshot.`));
6691
+ if (!source) return yield* persistenceFailure$3("DocumentStore.Drizzle.sourceForMutation", /* @__PURE__ */ new Error(`Committed document "${documentId}" is missing from its resulting snapshot.`));
8180
6692
  return source;
8181
6693
  });
8182
6694
  function mapCommitFailure(operation) {
@@ -8188,14 +6700,27 @@ function mapSynchronizationFailure(operation) {
8188
6700
  return documentPersistenceError(operation, cause);
8189
6701
  }));
8190
6702
  }
8191
- const rejectDeletingMedia = Effect.fn("DocumentStore.Drizzle.rejectDeletingMedia")(function* (transaction, references) {
6703
+ const rejectUnavailableMedia = Effect.fn("DocumentStore.Drizzle.rejectUnavailableMedia")(function* (transaction, references) {
8192
6704
  const keys = [...new Set(references.map((reference) => reference.objectKey))];
6705
+ const mediaIds = [...new Set(references.flatMap((reference) => reference.mediaId ? [reference.mediaId] : []))];
8193
6706
  if (keys.length === 0) return;
8194
- const rows = yield* transaction.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).where(and(inArray(mediaAssets.objectKey, keys), eq(mediaAssets.status, "deleting")));
8195
- const unavailable = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(rows.map((row) => row.objectKey));
6707
+ if (mediaIds.length > 0) {
6708
+ const identityRows = yield* transaction.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).where(inArray(mediaAssets.id, mediaIds));
6709
+ yield* lockMediaReferenceKeys(transaction, yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(identityRows.map((row) => row.objectKey)));
6710
+ }
6711
+ const rows = yield* transaction.select({
6712
+ id: mediaAssets.id,
6713
+ objectKey: mediaAssets.objectKey,
6714
+ status: mediaAssets.status
6715
+ }).from(mediaAssets).where(mediaIds.length > 0 ? or(inArray(mediaAssets.objectKey, keys), inArray(mediaAssets.id, mediaIds)) : inArray(mediaAssets.objectKey, keys));
6716
+ const deletingKeys = new Set(yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(rows.filter((row) => row.status === "deleting").map((row) => row.objectKey)));
6717
+ const unavailable = references.filter((reference) => {
6718
+ if (!reference.mediaId) return deletingKeys.has(reference.objectKey);
6719
+ const asset = rows.find((row) => row.id === reference.mediaId);
6720
+ return !asset || asset.status !== "ready" || asset.objectKey !== reference.objectKey;
6721
+ });
8196
6722
  if (unavailable.length === 0) return;
8197
- const unavailableSet = new Set(unavailable);
8198
- const affected = references.filter((reference) => unavailableSet.has(reference.objectKey)).sort((left, right) => left.documentId === right.documentId ? left.objectKey.localeCompare(right.objectKey) : left.documentId.localeCompare(right.documentId));
6723
+ const affected = unavailable.sort((left, right) => left.documentId === right.documentId ? left.objectKey.localeCompare(right.objectKey) : left.documentId.localeCompare(right.documentId));
8199
6724
  const first = affected[0];
8200
6725
  if (!first) return;
8201
6726
  return yield* new DocumentMediaUnavailable({
@@ -8219,7 +6744,7 @@ function referenceValues(reference) {
8219
6744
  sourcePath: reference.sourcePath
8220
6745
  };
8221
6746
  }
8222
- function mapPersistenceFailure$2(operation) {
6747
+ function mapPersistenceFailure$3(operation) {
8223
6748
  return (effect) => effect.pipe(Effect.mapError((cause) => isDocumentPersistenceError(cause) ? cause : documentPersistenceError(operation, cause)));
8224
6749
  }
8225
6750
  function mapRevisionFailure(operation) {
@@ -8228,7 +6753,7 @@ function mapRevisionFailure(operation) {
8228
6753
  return documentPersistenceError(operation, cause);
8229
6754
  }));
8230
6755
  }
8231
- function persistenceFailure$2(operation, cause) {
6756
+ function persistenceFailure$3(operation, cause) {
8232
6757
  return Effect.fail(documentPersistenceError(operation, cause));
8233
6758
  }
8234
6759
 
@@ -8291,7 +6816,7 @@ const initializationStoreLayer = Layer.effect(InitializationStore, Effect.gen(fu
8291
6816
  const locks = yield* PostgresAdvisoryLock;
8292
6817
  const claim = Effect.fn("InitializationStore.Postgres.claim")((value) => database.update(initializationState).set({
8293
6818
  changeId: value.changeId,
8294
- claim: Schema.encodeSync(InitializationClaim)(value),
6819
+ claim: Result.getOrThrow(Schema.encodeResult(InitializationClaim)(value)),
8295
6820
  state: "claiming"
8296
6821
  }).where(and(eq(initializationState.singleton, true), eq(initializationState.state, "uninitialized"))).returning({ singleton: initializationState.singleton }).pipe(Effect.map((rows) => rows.length === 1), mapUnavailable("InitializationStore.Postgres.claim")));
8297
6822
  const complete = Effect.fn("InitializationStore.Postgres.complete")((changeId) => transition(database, changeId, "ready"));
@@ -8343,6 +6868,53 @@ function mapUnavailable(operation) {
8343
6868
  })));
8344
6869
  }
8345
6870
 
6871
+ //#endregion
6872
+ //#region ../sql-pg/src/internal/seo/tables.ts
6873
+ const seoRevisions = pgTable("seo_revisions", {
6874
+ id: uuid().defaultRandom().primaryKey(),
6875
+ settings: jsonb().$type().notNull(),
6876
+ actorId: text("actor_id").notNull(),
6877
+ actorEmail: text("actor_email").notNull(),
6878
+ actorName: text("actor_name").notNull(),
6879
+ actorRoles: jsonb("actor_roles").$type().notNull(),
6880
+ requestId: uuid("request_id").notNull(),
6881
+ reason: text(),
6882
+ createdAt: timestamp("created_at", {
6883
+ mode: "string",
6884
+ withTimezone: true
6885
+ }).default(transactionTimestamp).notNull()
6886
+ }, (table) => [
6887
+ check("seo_revisions_actor_roles_check", sql`jsonb_typeof(${table.actorRoles}) = 'array'`),
6888
+ check("seo_revisions_settings_check", sql`jsonb_typeof(${table.settings}) = 'object'`),
6889
+ index("seo_revisions_history").on(table.createdAt.desc(), table.id.desc())
6890
+ ]);
6891
+ const seoState = pgTable("seo_state", {
6892
+ singleton: boolean().default(true).primaryKey(),
6893
+ workingRevisionId: uuid("working_revision_id").notNull(),
6894
+ publishedRevisionId: uuid("published_revision_id").notNull()
6895
+ }, (table) => [
6896
+ check("seo_state_singleton_check", sql`${table.singleton}`),
6897
+ foreignKey({
6898
+ columns: [table.workingRevisionId],
6899
+ foreignColumns: [seoRevisions.id],
6900
+ name: "seo_state_working_revision_fkey"
6901
+ }).onDelete("restrict"),
6902
+ foreignKey({
6903
+ columns: [table.publishedRevisionId],
6904
+ foreignColumns: [seoRevisions.id],
6905
+ name: "seo_state_published_revision_fkey"
6906
+ }).onDelete("restrict")
6907
+ ]);
6908
+ const seoMediaReferences = pgTable("seo_media_references", {
6909
+ id: uuid().defaultRandom().primaryKey(),
6910
+ scope: text({ enum: ["working", "published"] }).notNull(),
6911
+ mediaId: uuid("media_id").notNull().references(() => mediaAssets.id, { onDelete: "restrict" })
6912
+ }, (table) => [
6913
+ check("seo_media_references_scope_check", sql`${table.scope} in ('working', 'published')`),
6914
+ unique("seo_media_references_scope_media_unique").on(table.scope, table.mediaId),
6915
+ index("seo_media_references_media").on(table.mediaId)
6916
+ ]);
6917
+
8346
6918
  //#endregion
8347
6919
  //#region ../sql-pg/src/internal/media/row-schema.ts
8348
6920
  const mediaRefinements = {
@@ -8410,14 +6982,14 @@ const isMediaReferenced = Schema.is(MediaReferenced);
8410
6982
  const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8411
6983
  const database = yield* PgDrizzle.makeWithDefaults();
8412
6984
  const reserve = Effect.fn("MediaStore.Drizzle.reserve")((record, operation) => database.transaction((transaction) => Effect.gen(function* () {
8413
- if (record.status !== "uploading") return yield* persistenceFailure$1("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`A media reservation must start as uploading, received "${record.status}".`));
6985
+ if (record.status !== "uploading") return yield* persistenceFailure$2("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`A media reservation must start as uploading, received "${record.status}".`));
8414
6986
  yield* beginChange(transaction, mediaUploadChange(record, operation)).pipe(Effect.flatMap(requireUnresolvedChange));
8415
6987
  const rows = yield* transaction.insert(mediaAssets).values(recordValues(record)).onConflictDoNothing().returning();
8416
6988
  if (rows[0]) return yield* decodeMediaRecord(rows[0]);
8417
6989
  const existing = yield* rowById(transaction, record.id);
8418
6990
  if (existing && existing.status !== "deleting" && isSameReservation(existing, record)) return existing;
8419
- return yield* persistenceFailure$1("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`Media reservation "${record.id}" conflicts with durable media state.`));
8420
- })).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.reserve")));
6991
+ return yield* persistenceFailure$2("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`Media reservation "${record.id}" conflicts with durable media state.`));
6992
+ })).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.reserve")));
8421
6993
  const activate = Effect.fn("MediaStore.Drizzle.activate")((id) => database.transaction((transaction) => Effect.gen(function* () {
8422
6994
  const existing = yield* lockedRowById(transaction, id);
8423
6995
  if (existing?.status === "ready") return existing;
@@ -8435,11 +7007,11 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8435
7007
  kind: "applied"
8436
7008
  });
8437
7009
  return activated;
8438
- })).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.activate")));
7010
+ })).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.activate")));
8439
7011
  const find = Effect.fn("MediaStore.Drizzle.find")((id) => Effect.gen(function* () {
8440
7012
  const rows = yield* database.select().from(mediaAssets).where(and(eq(mediaAssets.id, id), eq(mediaAssets.status, "ready"))).limit(1);
8441
7013
  return rows[0] ? Option.some(yield* decodeMediaRecord(rows[0])) : Option.none();
8442
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.find")));
7014
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.find")));
8443
7015
  const beginDelete = Effect.fn("MediaStore.Drizzle.beginDelete")((id, operation) => database.transaction((transaction) => Effect.gen(function* () {
8444
7016
  const existing = yield* lockedRowById(transaction, id);
8445
7017
  if (!existing) return yield* new MediaNotFound({ mediaId: id });
@@ -8452,7 +7024,13 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8452
7024
  yield* lockMediaReferenceKeys(transaction, [existing.objectKey]);
8453
7025
  const referenceRows = yield* transaction.select().from(documentMediaReferences).where(eq(documentMediaReferences.objectKey, existing.objectKey)).orderBy(asc(documentMediaReferences.documentId), asc(documentMediaReferences.sourcePath), asc(documentMediaReferences.nodePath));
8454
7026
  const references = yield* decodeMediaReferences(referenceRows);
8455
- if (references.length > 0) return yield* new MediaReferenced({
7027
+ const seoReferenceRows = yield* transaction.select({ scope: seoMediaReferences.scope }).from(seoMediaReferences).where(eq(seoMediaReferences.mediaId, id)).orderBy(asc(seoMediaReferences.scope));
7028
+ const seoScopes = yield* Schema.decodeEffect(Schema.Array(Schema.Literals(["working", "published"])))(seoReferenceRows.map((row) => row.scope)).pipe(Effect.mapError((cause) => mediaPersistenceError("MediaStore.Drizzle.beginDelete", cause)));
7029
+ if (references.length > 0 || seoScopes.length > 0) return yield* new MediaReferenced(seoScopes.length > 0 ? {
7030
+ mediaId: id,
7031
+ references,
7032
+ seoScopes
7033
+ } : {
8456
7034
  mediaId: id,
8457
7035
  references
8458
7036
  });
@@ -8473,7 +7051,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8473
7051
  const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
8474
7052
  if (existing && isSameReplacement(existing, replacement)) return existing;
8475
7053
  return yield* new MediaConflict({ mediaId: replacement.mediaId });
8476
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.beginReplace")));
7054
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.beginReplace")));
8477
7055
  const markReplaceStaged = Effect.fn("MediaStore.Drizzle.markReplaceStaged")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
8478
7056
  yield* lockedRowById(transaction, replacement.mediaId);
8479
7057
  const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
@@ -8481,14 +7059,14 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8481
7059
  if (existing.staged) return existing;
8482
7060
  const rows = yield* transaction.update(mediaReplacements).set({ staged: true }).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning();
8483
7061
  if (rows[0]) return yield* decodeMediaReplacement(rows[0]);
8484
- return yield* persistenceFailure$1("MediaStore.Drizzle.markReplaceStaged", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" lost its durable intent.`));
8485
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.markReplaceStaged")));
7062
+ return yield* persistenceFailure$2("MediaStore.Drizzle.markReplaceStaged", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" lost its durable intent.`));
7063
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.markReplaceStaged")));
8486
7064
  const finishReplace = Effect.fn("MediaStore.Drizzle.finishReplace")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
8487
7065
  const asset = yield* lockedRowById(transaction, replacement.mediaId);
8488
7066
  const pendingReplacement = yield* replacementByMediaId(transaction, replacement.mediaId);
8489
7067
  if (!pendingReplacement) {
8490
7068
  if (asset && isCompletedReplacement(asset, replacement)) return asset;
8491
- return yield* persistenceFailure$1("MediaStore.Drizzle.finishReplace", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" cannot be replayed from durable state.`));
7069
+ return yield* persistenceFailure$2("MediaStore.Drizzle.finishReplace", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" cannot be replayed from durable state.`));
8492
7070
  }
8493
7071
  if (!isSameReplacement(pendingReplacement, replacement)) return yield* new MediaConflict({ mediaId: replacement.mediaId });
8494
7072
  if (!asset || asset.status !== "ready" || asset.objectKey !== replacement.objectKey || asset.mimeType !== replacement.mimeType) return yield* new MediaConflict({ mediaId: replacement.mediaId });
@@ -8501,7 +7079,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8501
7079
  updatedAt: transactionTimestamp
8502
7080
  }).where(and(eq(mediaAssets.id, replacement.mediaId), eq(mediaAssets.status, "ready"))).returning())[0];
8503
7081
  if (!replaced) return yield* transitionFailure("MediaStore.Drizzle.finishReplace", replacement.mediaId, asset.status);
8504
- if (!(yield* transaction.delete(mediaReplacements).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning({ mediaId: mediaReplacements.mediaId }))[0]) return yield* persistenceFailure$1("MediaStore.Drizzle.finishReplace", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" lost its durable intent.`));
7082
+ if (!(yield* transaction.delete(mediaReplacements).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning({ mediaId: mediaReplacements.mediaId }))[0]) return yield* persistenceFailure$2("MediaStore.Drizzle.finishReplace", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" lost its durable intent.`));
8505
7083
  const decoded = yield* decodeMediaRecord(replaced);
8506
7084
  const intent = yield* decodeMediaChangeIntent(change);
8507
7085
  yield* resolveChange(transaction, change, {
@@ -8509,7 +7087,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8509
7087
  kind: "applied"
8510
7088
  });
8511
7089
  return decoded;
8512
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.finishReplace")));
7090
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.finishReplace")));
8513
7091
  const cancelReplace = Effect.fn("MediaStore.Drizzle.cancelReplace")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
8514
7092
  yield* lockedRowById(transaction, replacement.mediaId);
8515
7093
  const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
@@ -8518,7 +7096,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8518
7096
  const change = yield* requirePendingChange(transaction, mediaSubject(replacement.mediaId), "media.replaced");
8519
7097
  if (!(yield* transaction.delete(mediaReplacements).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.staged, replacement.staged), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning({ mediaId: mediaReplacements.mediaId }))[0]) return yield* new MediaConflict({ mediaId: replacement.mediaId });
8520
7098
  yield* resolveChange(transaction, change, { kind: "cancelled" });
8521
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.cancelReplace")));
7099
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.cancelReplace")));
8522
7100
  const finishDelete = Effect.fn("MediaStore.Drizzle.finishDelete")((id) => database.transaction((transaction) => Effect.gen(function* () {
8523
7101
  const existing = yield* lockedRowById(transaction, id);
8524
7102
  if (!existing) return;
@@ -8530,7 +7108,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8530
7108
  details: mediaChangeDetails(intent),
8531
7109
  kind: "applied"
8532
7110
  });
8533
- })).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.finishDelete")));
7111
+ })).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.finishDelete")));
8534
7112
  const abandonUpload = Effect.fn("MediaStore.Drizzle.abandonUpload")((id) => database.transaction((transaction) => Effect.gen(function* () {
8535
7113
  const existing = yield* lockedRowById(transaction, id);
8536
7114
  if (!existing) return;
@@ -8538,15 +7116,15 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8538
7116
  const change = yield* requirePendingChange(transaction, mediaSubject(id), "media.uploaded");
8539
7117
  if (!(yield* transaction.delete(mediaAssets).where(and(eq(mediaAssets.id, id), eq(mediaAssets.status, "uploading"))).returning({ id: mediaAssets.id }))[0]) return yield* transitionFailure("MediaStore.Drizzle.abandonUpload", id, existing.status);
8540
7118
  yield* resolveChange(transaction, change, { kind: "cancelled" });
8541
- })).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.abandonUpload")));
7119
+ })).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.abandonUpload")));
8542
7120
  const pending = Effect.fn("MediaStore.Drizzle.pending")((query) => Effect.gen(function* () {
8543
- const rows = yield* database.select().from(mediaAssets).where(and(ne(mediaAssets.status, "ready"), lte(mediaAssets.updatedAt, DateTime.formatIso(query.before)))).orderBy(asc(mediaAssets.updatedAt), asc(mediaAssets.id)).limit(query.limit);
7121
+ const rows = yield* database.select().from(mediaAssets).where(and(ne(mediaAssets.status, "ready"), lte(mediaAssets.updatedAt, DateTime.formatIso(query.before)), query.after === void 0 ? void 0 : gt(mediaAssets.id, query.after))).orderBy(asc(mediaAssets.id)).limit(query.limit);
8544
7122
  return yield* Effect.forEach(rows, decodeMediaRecord);
8545
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.pending")));
7123
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.pending")));
8546
7124
  const pendingReplacements = Effect.fn("MediaStore.Drizzle.pendingReplacements")((query) => Effect.gen(function* () {
8547
- const rows = yield* database.select().from(mediaReplacements).where(lte(mediaReplacements.updatedAt, DateTime.formatIso(query.before))).orderBy(asc(mediaReplacements.updatedAt), asc(mediaReplacements.mediaId)).limit(query.limit);
7125
+ const rows = yield* database.select().from(mediaReplacements).where(and(lte(mediaReplacements.updatedAt, DateTime.formatIso(query.before)), query.after === void 0 ? void 0 : gt(mediaReplacements.mediaId, query.after))).orderBy(asc(mediaReplacements.mediaId)).limit(query.limit);
8548
7126
  return yield* Effect.forEach(rows, decodeMediaReplacement);
8549
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.pendingReplacements")));
7127
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.pendingReplacements")));
8550
7128
  const inspectReplacement = Effect.fn("MediaStore.Drizzle.inspectReplacement")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
8551
7129
  const asset = yield* lockedRowById(transaction, replacement.mediaId);
8552
7130
  const pending = yield* replacementByMediaId(transaction, replacement.mediaId);
@@ -8555,17 +7133,17 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8555
7133
  return yield* new MediaConflict({ mediaId: replacement.mediaId });
8556
7134
  }
8557
7135
  return asset && isCompletedReplacement(asset, replacement) ? "completed" : "absent";
8558
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.inspectReplacement")));
7136
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.inspectReplacement")));
8559
7137
  const inventory = Effect.gen(function* () {
8560
7138
  const rows = yield* database.select().from(mediaAssets).where(eq(mediaAssets.status, "ready")).orderBy(asc(mediaAssets.objectKey));
8561
7139
  return yield* Effect.forEach(rows, decodeMediaRecord);
8562
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.inventory"), Effect.withSpan("MediaStore.Drizzle.inventory"));
7140
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.inventory"), Effect.withSpan("MediaStore.Drizzle.inventory"));
8563
7141
  const knownObjectKeys = Effect.gen(function* () {
8564
7142
  const assetRows = yield* database.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).orderBy(asc(mediaAssets.objectKey));
8565
7143
  const replacementRows = yield* database.select({ objectKey: mediaReplacements.stagingObjectKey }).from(mediaReplacements).orderBy(asc(mediaReplacements.stagingObjectKey));
8566
7144
  const objectKeys = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))([...assetRows, ...replacementRows].map((row) => row.objectKey));
8567
7145
  return new Set(objectKeys);
8568
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.knownObjectKeys"), Effect.withSpan("MediaStore.Drizzle.knownObjectKeys"));
7146
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.knownObjectKeys"), Effect.withSpan("MediaStore.Drizzle.knownObjectKeys"));
8569
7147
  const search = Effect.fn("MediaStore.Drizzle.search")((query) => database.transaction((transaction) => Effect.gen(function* () {
8570
7148
  const normalizedSearch = query.search?.trim();
8571
7149
  const pattern = normalizedSearch ? `%${escapeLike(normalizedSearch)}%` : void 0;
@@ -8574,14 +7152,14 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8574
7152
  const rows = yield* transaction.select().from(mediaAssets).where(and(filters, cursorFilter)).orderBy(desc(mediaAssets.createdAt), desc(mediaAssets.id)).limit(query.limit + 1);
8575
7153
  const folderRows = yield* transaction.selectDistinct({ folder: mediaAssets.folder }).from(mediaAssets).where(and(eq(mediaAssets.status, "ready"), ne(mediaAssets.folder, ""))).orderBy(asc(mediaAssets.folder));
8576
7154
  const total = (yield* transaction.select({ value: count() }).from(mediaAssets).where(filters))[0]?.value;
8577
- if (total === void 0) return yield* persistenceFailure$1("MediaStore.Drizzle.search", /* @__PURE__ */ new Error("Media persistence did not return a search total."));
7155
+ if (total === void 0) return yield* persistenceFailure$2("MediaStore.Drizzle.search", /* @__PURE__ */ new Error("Media persistence did not return a search total."));
8578
7156
  const decoded = yield* Effect.forEach(rows, decodeMediaRecord);
8579
7157
  const folders = yield* Schema.decodeEffect(Schema.Array(MediaFolder))(folderRows.map((row) => row.folder)).pipe(Effect.mapError((cause) => mediaPersistenceError("MediaStore.Drizzle.search", cause)));
8580
7158
  return mediaPage(decoded, query.limit, folders, total);
8581
7159
  }), {
8582
7160
  accessMode: "read only",
8583
7161
  isolationLevel: "repeatable read"
8584
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.search")));
7162
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.search")));
8585
7163
  return MediaStore.of({
8586
7164
  abandonUpload,
8587
7165
  activate,
@@ -8666,45 +7244,230 @@ const replacementByMediaId = Effect.fn("MediaStore.Drizzle.replacementByMediaId"
8666
7244
  return rows[0] ? yield* decodeMediaReplacement(rows[0]) : void 0;
8667
7245
  });
8668
7246
  function transitionFailure(operation, id, actualStatus) {
8669
- return persistenceFailure$1(operation, /* @__PURE__ */ new Error(actualStatus === void 0 ? `Media record "${id}" does not exist.` : `Media record "${id}" cannot transition from "${actualStatus}".`));
7247
+ return persistenceFailure$2(operation, /* @__PURE__ */ new Error(actualStatus === void 0 ? `Media record "${id}" does not exist.` : `Media record "${id}" cannot transition from "${actualStatus}".`));
8670
7248
  }
8671
- function mapPersistenceFailure$1(operation) {
7249
+ function mapPersistenceFailure$2(operation) {
8672
7250
  return (effect) => effect.pipe(Effect.mapError((cause) => isMediaPersistenceError(cause) ? cause : mediaPersistenceError(operation, cause)));
8673
7251
  }
8674
- function mapStoreFailure$1(operation) {
7252
+ function mapStoreFailure$2(operation) {
8675
7253
  return (effect) => effect.pipe(Effect.mapError((cause) => isMediaConflict(cause) || isMediaPersistenceError(cause) ? cause : mediaPersistenceError(operation, cause)));
8676
7254
  }
8677
7255
  function mapBeginDeleteFailure(operation) {
8678
7256
  return (effect) => effect.pipe(Effect.mapError((cause) => isMediaConflict(cause) || isMediaNotFound(cause) || isMediaPersistenceError(cause) || isMediaReferenced(cause) ? cause : mediaPersistenceError(operation, cause)));
8679
7257
  }
8680
- function persistenceFailure$1(operation, cause) {
7258
+ function persistenceFailure$2(operation, cause) {
8681
7259
  return Effect.fail(mediaPersistenceError(operation, cause));
8682
7260
  }
8683
7261
 
8684
7262
  //#endregion
8685
- //#region ../sql-pg/src/internal/readiness.ts
8686
- const postgresReadinessLayer = Layer.effect(Readiness, Effect.gen(function* () {
8687
- const postgres = yield* PgClient.PgClient;
8688
- const journal = yield* ChangeJournal;
8689
- const check = Effect.gen(function* () {
8690
- yield* postgres.unsafe("select 1");
8691
- const now = yield* DateTime.now;
8692
- const pending = yield* journal.pending({
8693
- before: DateTime.subtractDuration(now, "30 seconds"),
8694
- limit: 1
8695
- });
8696
- const oldest = pending.changes[0];
8697
- if (pending.total === 0 || oldest === void 0) return;
8698
- yield* Effect.logWarning("CMS readiness found a stale pending change.", {
8699
- action: oldest.action,
8700
- ageMillis: DateTime.toEpochMillis(now) - DateTime.toEpochMillis(oldest.updatedAt),
8701
- changeId: oldest.id,
8702
- phase: "pending"
7263
+ //#region ../sql-pg/src/internal/seo/seo-store.ts
7264
+ const SeoStoreDomainError = Schema.Union([
7265
+ SeoMediaUnavailable,
7266
+ SeoPersistenceError,
7267
+ SeoVersionConflict
7268
+ ]);
7269
+ const isSeoStoreDomainError = Schema.is(SeoStoreDomainError);
7270
+ const isSeoPersistenceError = Schema.is(SeoPersistenceError);
7271
+ const systemActivity = {
7272
+ actor: {
7273
+ email: "system@cms.invalid",
7274
+ id: "system",
7275
+ name: "CMS System",
7276
+ roles: ["system"]
7277
+ },
7278
+ requestId: RequestId.make("00000000-0000-4000-8000-000000000000")
7279
+ };
7280
+ const seoStoreLayer = Layer.effect(SeoStore, Effect.gen(function* () {
7281
+ const database = yield* PgDrizzle.makeWithDefaults();
7282
+ const initialize = Effect.fn("SeoStore.Drizzle.initialize")((defaults) => database.transaction((transaction) => Effect.gen(function* () {
7283
+ yield* transaction.execute(sql`SELECT pg_advisory_xact_lock(hashtext('cms:seo:singleton:v1'))`);
7284
+ const current = yield* readSnapshot(transaction);
7285
+ if (current) return current;
7286
+ yield* ensureMediaAvailable(transaction, defaults).pipe(Effect.mapError((cause) => new SeoPersistenceError({
7287
+ cause,
7288
+ operation: "SeoStore.Drizzle.initializeDefaults"
7289
+ })));
7290
+ const revision = yield* insertRevision(transaction, defaults, systemActivity);
7291
+ yield* transaction.insert(seoState).values({
7292
+ publishedRevisionId: revision.revisionId,
7293
+ workingRevisionId: revision.revisionId
8703
7294
  });
8704
- return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("A stale change remains pending.") });
8705
- }).pipe(Effect.mapError((cause) => Schema.is(ReadinessFailed)(cause) ? cause : new ReadinessFailed({ cause })), Effect.withSpan("Installation.readiness"));
8706
- return Readiness.of({ check });
7295
+ yield* replaceMediaReferences(transaction, "working", defaults);
7296
+ yield* replaceMediaReferences(transaction, "published", defaults);
7297
+ return {
7298
+ published: revision,
7299
+ working: revision
7300
+ };
7301
+ })).pipe(mapStoreFailure$1("SeoStore.Drizzle.initialize")));
7302
+ const load = database.transaction((transaction) => readSnapshot(transaction), {
7303
+ accessMode: "read only",
7304
+ isolationLevel: "repeatable read"
7305
+ }).pipe(mapPersistenceFailure$1("SeoStore.Drizzle.load"));
7306
+ const save = Effect.fn("SeoStore.Drizzle.save")((input) => database.transaction((transaction) => Effect.gen(function* () {
7307
+ const current = yield* lockedSnapshot(transaction);
7308
+ yield* requireVersion(current, input.expectedVersion);
7309
+ yield* ensureMediaAvailable(transaction, input.settings);
7310
+ const revision = yield* insertRevision(transaction, input.settings, input.activity);
7311
+ yield* transaction.update(seoState).set({ workingRevisionId: revision.revisionId }).where(eq(seoState.singleton, true));
7312
+ yield* replaceMediaReferences(transaction, "working", input.settings);
7313
+ yield* recordActivity(transaction, "seo.saved", input.activity, {
7314
+ previousVersion: current.working.version,
7315
+ version: revision.version
7316
+ }, ActivityId.make(revision.revisionId));
7317
+ return {
7318
+ ...current,
7319
+ working: revision
7320
+ };
7321
+ })).pipe(mapStoreFailure$1("SeoStore.Drizzle.save")));
7322
+ const publish = Effect.fn("SeoStore.Drizzle.publish")((input) => database.transaction((transaction) => Effect.gen(function* () {
7323
+ const current = yield* lockedSnapshot(transaction);
7324
+ yield* requireVersion(current, input.expectedVersion);
7325
+ yield* ensureMediaAvailable(transaction, current.working.settings);
7326
+ yield* transaction.update(seoState).set({ publishedRevisionId: current.working.revisionId }).where(eq(seoState.singleton, true));
7327
+ yield* replaceMediaReferences(transaction, "published", current.working.settings);
7328
+ yield* recordActivity(transaction, "seo.published", input.activity, { version: current.working.version });
7329
+ return {
7330
+ ...current,
7331
+ published: current.working
7332
+ };
7333
+ })).pipe(mapStoreFailure$1("SeoStore.Drizzle.publish")));
7334
+ return SeoStore.of({
7335
+ initialize,
7336
+ load,
7337
+ publish,
7338
+ save
7339
+ });
8707
7340
  }));
7341
+ const readSnapshot = Effect.fn("SeoStore.Drizzle.readSnapshot")(function* (database) {
7342
+ const state = (yield* database.select().from(seoState).limit(1))[0];
7343
+ if (!state) return void 0;
7344
+ const revisionRows = yield* database.select().from(seoRevisions).where(inArray(seoRevisions.id, [state.workingRevisionId, state.publishedRevisionId]));
7345
+ const revisions = yield* Effect.forEach(revisionRows, decodeRevision);
7346
+ const working = revisions.find((revision) => revision.revisionId === state.workingRevisionId);
7347
+ const published = revisions.find((revision) => revision.revisionId === state.publishedRevisionId);
7348
+ if (!working || !published) return yield* persistenceFailure$1("SeoStore.Drizzle.readSnapshot", /* @__PURE__ */ new Error("SEO state points to a missing revision."));
7349
+ return {
7350
+ published,
7351
+ working
7352
+ };
7353
+ });
7354
+ const lockedSnapshot = Effect.fn("SeoStore.Drizzle.lockSnapshot")(function* (transaction) {
7355
+ if (!(yield* transaction.select().from(seoState).where(eq(seoState.singleton, true)).for("update").limit(1))[0]) return yield* persistenceFailure$1("SeoStore.Drizzle.lockSnapshot", /* @__PURE__ */ new Error("SEO state is not initialized."));
7356
+ return (yield* readSnapshot(transaction)) ?? (yield* persistenceFailure$1("SeoStore.Drizzle.lockSnapshot", /* @__PURE__ */ new Error("SEO state disappeared.")));
7357
+ });
7358
+ const insertRevision = Effect.fn("SeoStore.Drizzle.insertRevision")(function* (transaction, settings, activity) {
7359
+ const row = (yield* transaction.insert(seoRevisions).values({
7360
+ actorEmail: activity.actor.email,
7361
+ actorId: activity.actor.id,
7362
+ actorName: activity.actor.name,
7363
+ actorRoles: activity.actor.roles,
7364
+ reason: activity.reason,
7365
+ requestId: activity.requestId,
7366
+ settings
7367
+ }).returning())[0];
7368
+ return row ? yield* decodeRevision(row) : yield* persistenceFailure$1("SeoStore.Drizzle.insertRevision", /* @__PURE__ */ new Error("No revision row returned."));
7369
+ });
7370
+ const decodeRevision = Effect.fn("SeoStore.Drizzle.decodeRevision")(function* (row) {
7371
+ return yield* Schema.decodeEffect(SeoRevision)({
7372
+ editedAt: row.createdAt,
7373
+ editedBy: {
7374
+ email: row.actorEmail,
7375
+ id: row.actorId,
7376
+ name: row.actorName,
7377
+ roles: row.actorRoles
7378
+ },
7379
+ revisionId: row.id,
7380
+ settings: row.settings,
7381
+ version: row.id
7382
+ }).pipe(Effect.mapError((cause) => new SeoPersistenceError({
7383
+ cause,
7384
+ operation: "SeoStore.Drizzle.decodeRevision"
7385
+ })));
7386
+ });
7387
+ function requireVersion(snapshot, expectedVersion) {
7388
+ return snapshot.working.version === expectedVersion ? Effect.void : Effect.fail(new SeoVersionConflict({
7389
+ actualVersion: snapshot.working.version,
7390
+ expectedVersion
7391
+ }));
7392
+ }
7393
+ const ensureMediaAvailable = Effect.fn("SeoStore.Drizzle.ensureMediaAvailable")(function* (transaction, settings) {
7394
+ const mediaIds = settingsMediaIds(settings);
7395
+ if (mediaIds.length === 0) return;
7396
+ const initialRows = yield* transaction.select({
7397
+ id: mediaAssets.id,
7398
+ objectKey: mediaAssets.objectKey
7399
+ }).from(mediaAssets).where(inArray(mediaAssets.id, mediaIds));
7400
+ const objectKeys = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(initialRows.map((row) => row.objectKey));
7401
+ yield* lockMediaReferenceKeys(transaction, objectKeys);
7402
+ const readyRows = yield* transaction.select({
7403
+ id: mediaAssets.id,
7404
+ objectKey: mediaAssets.objectKey
7405
+ }).from(mediaAssets).where(and(inArray(mediaAssets.id, mediaIds), eq(mediaAssets.status, "ready")));
7406
+ const unavailable = settingsMediaImages(settings).find((image) => {
7407
+ const asset = readyRows.find((row) => row.id === image.mediaId);
7408
+ return !asset || mediaObjectKey(image.url) !== asset.objectKey;
7409
+ });
7410
+ if (unavailable) return yield* new SeoMediaUnavailable({ mediaId: unavailable.mediaId });
7411
+ });
7412
+ const replaceMediaReferences = Effect.fn("SeoStore.Drizzle.replaceMediaReferences")(function* (transaction, scope, settings) {
7413
+ yield* transaction.delete(seoMediaReferences).where(eq(seoMediaReferences.scope, scope));
7414
+ const mediaIds = settingsMediaIds(settings);
7415
+ if (mediaIds.length > 0) yield* transaction.insert(seoMediaReferences).values(mediaIds.map((mediaId) => ({
7416
+ mediaId,
7417
+ scope
7418
+ })));
7419
+ });
7420
+ function settingsMediaIds(settings) {
7421
+ return [...new Set(settingsMediaImages(settings).map((image) => image.mediaId))];
7422
+ }
7423
+ function settingsMediaImages(settings) {
7424
+ const images = [settings.socialImage];
7425
+ const identity = settings.identity;
7426
+ if (identity?.type === "Person") images.push(identity.image);
7427
+ if (identity?.type === "Organization") images.push(identity.logo);
7428
+ if (identity?.type === "LocalBusiness") images.push(identity.logo, identity.image);
7429
+ return images.filter((image) => image !== void 0);
7430
+ }
7431
+ function mediaObjectKey(url) {
7432
+ try {
7433
+ const encodedKey = new URL(url).pathname.match(/(?:^|\/)(media\/[^/]+)$/u)?.[1];
7434
+ return encodedKey ? encodedKey.split("/").map(decodeURIComponent).join("/") : void 0;
7435
+ } catch {
7436
+ return;
7437
+ }
7438
+ }
7439
+ const recordActivity = Effect.fn("SeoStore.Drizzle.recordActivity")(function* (transaction, action, context, details, id) {
7440
+ yield* transaction.insert(activityEntries).values({
7441
+ action,
7442
+ actorEmail: context.actor.email,
7443
+ actorId: context.actor.id,
7444
+ actorName: context.actor.name,
7445
+ actorRoles: context.actor.roles,
7446
+ details,
7447
+ id,
7448
+ reason: context.reason,
7449
+ requestId: context.requestId,
7450
+ targetKind: "seo"
7451
+ });
7452
+ });
7453
+ function mapStoreFailure$1(operation) {
7454
+ return (effect) => effect.pipe(Effect.mapError((cause) => isSeoStoreDomainError(cause) ? cause : new SeoPersistenceError({
7455
+ cause,
7456
+ operation
7457
+ })));
7458
+ }
7459
+ function mapPersistenceFailure$1(operation) {
7460
+ return (effect) => effect.pipe(Effect.mapError((cause) => isSeoPersistenceError(cause) ? cause : new SeoPersistenceError({
7461
+ cause,
7462
+ operation
7463
+ })));
7464
+ }
7465
+ function persistenceFailure$1(operation, cause) {
7466
+ return Effect.fail(new SeoPersistenceError({
7467
+ cause,
7468
+ operation
7469
+ }));
7470
+ }
8708
7471
 
8709
7472
  //#endregion
8710
7473
  //#region ../sql-pg/src/internal/user-administration/user-command-fence.ts
@@ -8722,7 +7485,7 @@ const postgresUserCommandFenceLayer = Layer.effect(UserCommandFence, PostgresAdv
8722
7485
  const WORKING_DOCUMENT_CHANGES_CHANNEL = "cms_working_documents";
8723
7486
  const WorkingDocumentChangePayload = Schema.fromJsonString(WorkingDocumentChange);
8724
7487
  function workingDocumentChangePayload(change) {
8725
- return Schema.encodeSync(WorkingDocumentChangePayload)(change);
7488
+ return Result.getOrThrow(Schema.encodeResult(WorkingDocumentChangePayload)(change));
8726
7489
  }
8727
7490
  const decodeWorkingDocumentChangePayload = Schema.decodeUnknownEffect(WorkingDocumentChangePayload);
8728
7491
 
@@ -8757,12 +7520,13 @@ const makePostgresChanges = Effect.fn("WorkingDocumentChanges.makePostgres")(fun
8757
7520
  const postgres = Context.get(context, PgClient.PgClient);
8758
7521
  const heartbeats = /* @__PURE__ */ new Map();
8759
7522
  let heartbeat = 0;
8760
- const listener = yield* postgres.listen(WORKING_DOCUMENT_CHANGES_CHANNEL).pipe(Stream.runForEach((payload) => {
8761
- const waiting = heartbeats.get(payload);
7523
+ const notifications = yield* postgres.listen(WORKING_DOCUMENT_CHANGES_CHANNEL).pipe(Effect.mapError((cause) => changesError("listen", cause)));
7524
+ const listener = yield* Stream.fromQueue(notifications).pipe(Stream.runForEach((payload) => {
7525
+ const waiting = heartbeats.get(payload.payload);
8762
7526
  if (waiting !== void 0) return Deferred.succeed(waiting, void 0).pipe(Effect.asVoid);
8763
- if (payload.startsWith("__cms_probe__:")) return Effect.void;
8764
- return decodeWorkingDocumentChangePayload(payload).pipe(Effect.flatMap((change) => PubSub.publish(changes, change)), Effect.asVoid, Effect.catchCause((cause) => Effect.logWarning("Ignored an invalid Working Document change.", Cause.pretty(cause))));
8765
- }), Effect.mapError((cause) => changesError("listen", cause))).pipe(Effect.forkScoped({ startImmediately: true }));
7527
+ if (payload.payload.startsWith("__cms_probe__:")) return Effect.void;
7528
+ return decodeWorkingDocumentChangePayload(payload.payload).pipe(Effect.flatMap((change) => PubSub.publish(changes, change)), Effect.asVoid, Effect.catchCause((cause) => Effect.logWarning("Ignored an invalid Working Document change.", Cause.pretty(cause))));
7529
+ }), Effect.catchCause((cause) => Effect.fail(changesError("listen", cause)))).pipe(Effect.forkScoped({ startImmediately: true }));
8766
7530
  const pulse = Effect.gen(function* () {
8767
7531
  heartbeat += 1;
8768
7532
  const payload = `__cms_probe__:${session}:${heartbeat}`;
@@ -9320,16 +8084,13 @@ function postgresLayer(options) {
9320
8084
  });
9321
8085
  const migrations = cmsDatabaseMigrationLayer.pipe(Layer.provide(database));
9322
8086
  const locks = postgresAdvisoryLockLayer(options);
9323
- const stores = Layer.mergeAll(postgresActivityStoreLayer, changeJournalLayer, documentStoreLayer, mediaStoreLayer, initializationStoreLayer.pipe(Layer.provide(locks)), postgresUserCommandFenceLayer.pipe(Layer.provide(locks)), postgresWorkingDocumentChangesLayer(options), workingDocumentStoreLayer).pipe(Layer.provide(migrations), Layer.provide(database));
9324
- const readiness = postgresReadinessLayer.pipe(Layer.provide(stores), Layer.provide(database));
9325
- return Layer.merge(stores, readiness);
8087
+ return Layer.mergeAll(postgresActivityStoreLayer, changeJournalLayer, documentStoreLayer, mediaStoreLayer, seoStoreLayer, initializationStoreLayer.pipe(Layer.provide(locks)), postgresUserCommandFenceLayer.pipe(Layer.provide(locks)), postgresWorkingDocumentChangesLayer(options), workingDocumentStoreLayer).pipe(Layer.provide(migrations), Layer.provide(database));
9326
8088
  }
9327
8089
 
9328
8090
  //#endregion
9329
8091
  //#region src/runtime/installation.ts
9330
- /** Composes the private providers selected by the Nuxt product adapter. */
9331
8092
  const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options) {
9332
- const crypto = NodeCrypto$1.layer;
8093
+ const crypto = NodeCrypto.layer;
9333
8094
  const postgres = postgresLayer({ url: Redacted.make(options.databaseUrl) });
9334
8095
  const identity = Layer.unwrap(Effect.acquireRelease(Effect.sync(() => new Pool({ connectionString: options.databaseUrl })), (pool) => Effect.promise(() => pool.end()).pipe(Effect.asVoid)).pipe(Effect.map((pool) => betterAuthLayer({
9335
8096
  baseURL: options.baseUrl,
@@ -9339,6 +8100,7 @@ const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options)
9339
8100
  const core = Layer.mergeAll(crypto, postgres, identity);
9340
8101
  return yield* installCms$1({
9341
8102
  baseUrl: options.baseUrl,
8103
+ formHandlers: options.formHandlers,
9342
8104
  authoring: options.authoring,
9343
8105
  bootstrap: options.bootstrap,
9344
8106
  cleanup: options.cleanup.enabled ? {
@@ -9353,7 +8115,6 @@ const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options)
9353
8115
  } : void 0
9354
8116
  },
9355
8117
  documentModel: options.documentModel,
9356
- initialSources: options.initialSources,
9357
8118
  initializationToken: Redacted.make(options.initializationToken),
9358
8119
  studioSecret: Redacted.make(`cms-collaboration:${options.secret}`)
9359
8120
  });
@@ -9377,4 +8138,4 @@ function toS3MediaObjectStoreOptions(options) {
9377
8138
 
9378
8139
  //#endregion
9379
8140
  export { toS3MediaObjectStoreOptions as n, installCms as t };
9380
- //# sourceMappingURL=installation-DeuBiFRE.js.map
8141
+ //# sourceMappingURL=installation-CNfIriVm.js.map