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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +192 -9
  2. package/package.json +30 -15
  3. package/src/Configuration-Br7tE9Qt.d.ts +68 -0
  4. package/src/{Configuration-DyKo0sLS.js → Configuration-CA_jvQKG.js} +26 -147
  5. package/src/Configuration-CA_jvQKG.js.map +1 -0
  6. package/src/Configuration.d.ts +2 -44
  7. package/src/Configuration.js +0 -1
  8. package/src/Configuration.js.map +1 -1
  9. package/src/{DocumentEngine-DYObwMDY.js → DocumentEngine-CgiBQU4g.js} +75 -27
  10. package/src/DocumentEngine-CgiBQU4g.js.map +1 -0
  11. package/src/DocumentModel-BB9igGC7.d.ts +34 -0
  12. package/src/DocumentModel.d.ts +2 -34
  13. package/src/DocumentModel.js +13 -3
  14. package/src/DocumentModel.js.map +1 -1
  15. package/src/Forms-BsJMev5_.d.ts +74 -0
  16. package/src/Forms-CfcI3tPU.js +79 -0
  17. package/src/Forms-CfcI3tPU.js.map +1 -0
  18. package/src/Forms-DxbkU3_5.js +367 -0
  19. package/src/Forms-DxbkU3_5.js.map +1 -0
  20. package/src/Forms.d.ts +35 -0
  21. package/src/Forms.js +13 -0
  22. package/src/Forms.js.map +1 -0
  23. package/src/{HttpPaths-BbS21Jy8.js → HttpPaths-c9nLZ9H2.js} +6 -2
  24. package/src/HttpPaths-c9nLZ9H2.js.map +1 -0
  25. package/src/I18n-DZK4CkJF.d.ts +21 -0
  26. package/src/I18n.d.ts +2 -0
  27. package/src/I18n.js +1 -0
  28. package/src/Module.js +57 -11
  29. package/src/Module.js.map +1 -1
  30. package/src/S3MediaObjects-JKMAWyVB.js +2664 -0
  31. package/src/S3MediaObjects-JKMAWyVB.js.map +1 -0
  32. package/src/Seed.d.ts +6 -0
  33. package/src/Seed.js +34 -0
  34. package/src/Seed.js.map +1 -0
  35. package/src/Seo-402YumpT.d.ts +198 -0
  36. package/src/{connection-CBTY-TzB.js → connection-B-iz0Y9Y.js} +5 -7
  37. package/src/connection-B-iz0Y9Y.js.map +1 -0
  38. package/src/{installation-DeuBiFRE.js → installation-DgF2M1gL.js} +1005 -2242
  39. package/src/installation-DgF2M1gL.js.map +1 -0
  40. package/src/internal/authoring-catalog.js +10 -4
  41. package/src/internal/authoring-catalog.js.map +1 -1
  42. package/src/internal/authoring-compiler.js +0 -1
  43. package/src/internal/authoring-compiler.js.map +1 -1
  44. package/src/internal/authoring-metadata.js +4 -4
  45. package/src/internal/authoring-metadata.js.map +1 -1
  46. package/src/internal/cms-config.js +0 -1
  47. package/src/internal/cms-config.js.map +1 -1
  48. package/src/internal/configuration.js +23 -10
  49. package/src/internal/configuration.js.map +1 -1
  50. package/src/internal/document-model.js +65 -20
  51. package/src/internal/document-model.js.map +1 -1
  52. package/src/internal/form-capabilities.js +12 -0
  53. package/src/internal/form-capabilities.js.map +1 -0
  54. package/src/internal/i18n.js +47 -0
  55. package/src/internal/i18n.js.map +1 -0
  56. package/src/internal/studio-app.js +2 -2
  57. package/src/internal/studio-app.js.map +1 -1
  58. package/src/{location-DtYv2aes.js → location-CFKTeK1K.js} +2 -3
  59. package/src/location-CFKTeK1K.js.map +1 -0
  60. package/src/{metadata-DE1Mc2td.js → metadata-BEZPWh-3.js} +3 -72
  61. package/src/metadata-BEZPWh-3.js.map +1 -0
  62. package/src/{models-C3witQBK.js → models-BWm1pTAf.js} +3 -2
  63. package/src/models-BWm1pTAf.js.map +1 -0
  64. package/src/{models-BkMgeaOL.js → models-DwYioyNV.js} +203 -32
  65. package/src/models-DwYioyNV.js.map +1 -0
  66. package/src/runtime/blocks.js +22 -9
  67. package/src/runtime/blocks.js.map +1 -1
  68. package/src/runtime/collaboration.js.map +1 -1
  69. package/src/runtime/delivery/DocumentRenderer.js +7 -4
  70. package/src/runtime/delivery/DocumentRenderer.js.map +1 -1
  71. package/src/runtime/delivery/blocks.js +3 -1
  72. package/src/runtime/delivery/blocks.js.map +1 -1
  73. package/src/runtime/delivery/context.js +1 -1
  74. package/src/runtime/delivery/document-page.js +12 -12
  75. package/src/runtime/delivery/document-page.js.map +1 -1
  76. package/src/runtime/delivery/document.js +4 -2
  77. package/src/runtime/delivery/document.js.map +1 -1
  78. package/src/runtime/delivery/error.js.map +1 -1
  79. package/src/runtime/delivery/plugin.js +22 -2
  80. package/src/runtime/delivery/plugin.js.map +1 -1
  81. package/src/runtime/delivery/types.d.ts +5 -0
  82. package/src/runtime/delivery/useCmsDocumentSeo.d.ts +56 -0
  83. package/src/runtime/delivery/useCmsDocumentSeo.js +99 -0
  84. package/src/runtime/delivery/useCmsDocumentSeo.js.map +1 -0
  85. package/src/runtime/delivery/useDocument.js +2 -2
  86. package/src/runtime/delivery/useDocument.js.map +1 -1
  87. package/src/runtime/delivery/useDocumentQuery.js +2 -2
  88. package/src/runtime/delivery/useDocumentQuery.js.map +1 -1
  89. package/src/runtime/forms/document.js +38 -0
  90. package/src/runtime/forms/document.js.map +1 -0
  91. package/src/runtime/forms/useCmsForm.js +149 -0
  92. package/src/runtime/forms/useCmsForm.js.map +1 -0
  93. package/src/runtime/i18n/translator.js +22 -0
  94. package/src/runtime/i18n/translator.js.map +1 -0
  95. package/src/runtime/i18n/useCmsI18n.d.ts +9 -0
  96. package/src/runtime/i18n/useCmsI18n.js +29 -0
  97. package/src/runtime/i18n/useCmsI18n.js.map +1 -0
  98. package/src/runtime/installation.js +1 -1
  99. package/src/runtime/plugin.js +1 -1
  100. package/src/runtime/plugin.js.map +1 -1
  101. package/src/runtime/preview/comark-document.js +0 -1
  102. package/src/runtime/preview/comark-document.js.map +1 -1
  103. package/src/runtime/preview/connection.js +1 -1
  104. package/src/runtime/preview/controller.js +10 -3
  105. package/src/runtime/preview/controller.js.map +1 -1
  106. package/src/runtime/preview/overlay.js.map +1 -1
  107. package/src/runtime/preview/plugin.client.js +59 -48
  108. package/src/runtime/preview/plugin.client.js.map +1 -1
  109. package/src/runtime/preview/protocol.js.map +1 -1
  110. package/src/runtime/preview/store.js +1 -1
  111. package/src/runtime/preview/targets.js +0 -1
  112. package/src/runtime/preview/targets.js.map +1 -1
  113. package/src/runtime/sitemap-entries.js +17 -0
  114. package/src/runtime/sitemap-entries.js.map +1 -0
  115. package/src/runtime/sitemap.js +24 -0
  116. package/src/runtime/sitemap.js.map +1 -0
  117. package/src/runtime/studio-app.js.map +1 -1
  118. package/src/runtime/studio-entry.js +2 -2
  119. package/src/runtime/studio-entry.js.map +1 -1
  120. package/src/runtime/studio-navigation.js +1 -1
  121. package/src/scalars-B2lgGUnj.js +26 -0
  122. package/src/scalars-B2lgGUnj.js.map +1 -0
  123. package/src/{store-CfFFx9Ua.js → store-DjJs3I7k.js} +12 -7
  124. package/src/store-DjJs3I7k.js.map +1 -0
  125. package/studio/{CreateDocumentDialog--L9lTfx6.js → CreateDocumentDialog-CTeIFlPb.js} +44 -44
  126. package/studio/{collaboration-DUuu678R.js → collaboration-DcK2F6v9.js} +7 -6
  127. package/studio/{documentPlanning-BXX04Woe.js → documentPlanning-Bfv-d7OU.js} +7310 -6479
  128. package/studio/{editor-PexnXkJY.js → editor-Bzlh3zXt.js} +7965 -7955
  129. package/studio/host.css +1 -1
  130. package/studio/host.js +10416 -7981
  131. package/src/Configuration-DyKo0sLS.js.map +0 -1
  132. package/src/DocumentEngine-DYObwMDY.js.map +0 -1
  133. package/src/HttpPaths-BbS21Jy8.js.map +0 -1
  134. package/src/StudioHttp-jR2-bc01.js +0 -718
  135. package/src/StudioHttp-jR2-bc01.js.map +0 -1
  136. package/src/connection-CBTY-TzB.js.map +0 -1
  137. package/src/installation-DeuBiFRE.js.map +0 -1
  138. package/src/location-DtYv2aes.js.map +0 -1
  139. package/src/metadata-DE1Mc2td.js.map +0 -1
  140. package/src/models-BkMgeaOL.js.map +0 -1
  141. package/src/models-C3witQBK.js.map +0 -1
  142. package/src/store-CfFFx9Ua.js.map +0 -1
@@ -1,31 +1,30 @@
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-DxbkU3_5.js";
4
+ import { A as RemoveDocumentInput, C as MarkdownNode, Ct as MediaTooLarge, G as SeoRevision, H as SeoConfirmationRequired, M as UndeleteDocumentInput, N as UnpublishDocumentInput, P as UpdateDocumentInput, Q as AuthenticationRequired, S as DuplicateDocumentInput, St as MediaStatus, T as PublishDocumentInput, U as SeoMediaUnavailable, Y as SeoVersionConflict, Z as AccessDenied, _ as DocumentSource, _t as MediaObjectKey, a as DOCUMENT_REVISION_ACTIONS, at as ImageMimeType, bt as MediaReference, c as DocumentImportInput, ct as MediaCleanupReport, dt as MediaFolder, f as DocumentMutationResult, ft as MediaId, g as DocumentRevisionAction, gt as MediaNotFound, ht as MediaName, it as StudioRequestContext, j as RestoreDocumentInput, kt as RequestId, lt as MediaConflict, mt as MediaInvalid, n as CmsMarkdownDocument, nt as StudioActor, o as DeletedDocument, ot as MediaAsset, pt as MediaIntegrityReport, rt as StudioEntryDenied, st as MediaByteSize, tt as Role, ut as MediaCursor, vt as MediaPage, w as MoveDocumentInput, wt as MediaUpload, xt as MediaReferenced } from "./models-DwYioyNV.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-JKMAWyVB.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-CA_jvQKG.js";
9
+ 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
10
  import { Extension, Node, getSchema, mergeAttributes } from "@tiptap/core";
9
11
  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";
12
+ import { FindMyWay, HttpEffect, HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerResponse, Multipart } from "effect/unstable/http";
12
13
  import { HttpApiBuilder, HttpApiMiddleware } from "effect/unstable/httpapi";
13
- import * as NodeCrypto$1 from "@effect/platform-node-shared/NodeCrypto";
14
+ import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto";
15
+ import { createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
14
16
  import { betterAuth } from "better-auth";
15
17
  import { createAccessControl } from "better-auth/plugins/access";
16
18
  import { admin } from "better-auth/plugins/admin";
17
19
  import { defaultStatements } from "better-auth/plugins/admin/access";
18
- import { APIError, createAuthMiddleware, getSessionFromCtx, isAPIError } from "better-auth/api";
19
20
  import { Hocuspocus, IncomingMessage, MessageType, shouldSkipStoreHooks } from "@hocuspocus/server";
20
- import { renderMarkdown } from "comark/render";
21
- import { diffArrays } from "diff";
22
21
  import StarterKit from "@tiptap/starter-kit";
23
22
  import * as Y from "yjs";
24
23
  import { Doc, applyUpdate, encodeStateAsUpdate, encodeStateVector } from "yjs";
25
- import { CopyObjectCommand, CreateBucketCommand, DeleteObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
24
+ import { RateLimiter } from "effect/unstable/persistence";
26
25
  import { PgClient } from "@effect/sql-pg";
27
26
  import { isDeepStrictEqual } from "node:util";
28
- import { and, asc, count, desc, eq, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
27
+ import { and, asc, count, desc, eq, gt, inArray, lt, lte, ne, or, sql } from "drizzle-orm";
29
28
  import * as PgDrizzle from "drizzle-orm/effect-postgres";
30
29
  import { createInsertSchema, createSelectSchema } from "drizzle-orm/effect-schema";
31
30
  import { alias, bigint, boolean, bytea, check, foreignKey, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique, uniqueIndex, uuid } from "drizzle-orm/pg-core";
@@ -58,50 +57,6 @@ function activityEntryFromDraft(id, draft, occurredAt) {
58
57
  });
59
58
  }
60
59
 
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
60
  //#endregion
106
61
  //#region ../server/src/internal/activity/activity.ts
107
62
  var ActivityUnavailable = class extends Schema.TaggedError()("Activity.Unavailable", {
@@ -109,11 +64,11 @@ var ActivityUnavailable = class extends Schema.TaggedError()("Activity.Unavailab
109
64
  cause: Schema.Defect()
110
65
  }) {};
111
66
  var Activity = class extends Context.Service()("@internal/server/Activity") {};
112
- const CursorPayload$2 = Schema.Struct({
67
+ const CursorPayload$1 = Schema.Struct({
113
68
  id: ActivityId,
114
69
  occurredAt: Schema.DateTimeUtcFromString
115
70
  });
116
- const cursors$2 = cursorCodec(Schema.fromJsonString(CursorPayload$2), (value) => ActivityCursor.make(value), () => new ActivityCursorInvalid());
71
+ const cursors$1 = cursorCodec(Schema.fromJsonString(CursorPayload$1), (value) => ActivityCursor.make(value), () => new ActivityCursorInvalid());
117
72
  const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
118
73
  const accessControl = yield* AccessControl;
119
74
  const storage = yield* ActivityStore;
@@ -122,7 +77,7 @@ const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
122
77
  action: "activity.read",
123
78
  resource: { kind: "activity" }
124
79
  });
125
- const cursor = query.cursor ? yield* Effect.fromResult(cursors$2.decode(query.cursor)) : void 0;
80
+ const cursor = query.cursor ? yield* Effect.fromResult(cursors$1.decode(query.cursor)) : void 0;
126
81
  const page = yield* storage.list({
127
82
  action: query.action,
128
83
  actorId: query.actorId,
@@ -138,7 +93,7 @@ const activityLayer = Layer.effect(Activity, Effect.gen(function* () {
138
93
  };
139
94
  return page.next ? ActivityPage.make({
140
95
  ...output,
141
- nextCursor: cursors$2.encode(page.next)
96
+ nextCursor: cursors$1.encode(page.next)
142
97
  }) : ActivityPage.make(output);
143
98
  });
144
99
  return Activity.of({ list });
@@ -254,7 +209,6 @@ const ROUND = [
254
209
  3204031479,
255
210
  3329325298
256
211
  ];
257
- /** Portable synchronous SHA-256 for deterministic journal fingerprints and secret redaction. */
258
212
  function sha256Hex(value) {
259
213
  const input = new TextEncoder().encode(value);
260
214
  const bitLength = BigInt(input.length) * 8n;
@@ -334,14 +288,14 @@ const PendingChange = ChangeDraft.pipe(Schema.fieldsAssign({
334
288
  updatedAt: Schema.DateTimeUtcFromString
335
289
  }));
336
290
  function changeFingerprint(change) {
337
- const encoded = Schema.encodeSync(ChangeDraft)(change);
291
+ const encoded = Result.getOrThrow(Schema.encodeResult(ChangeDraft)(change));
338
292
  return sha256Hex(JSON.stringify(canonicalJson(encoded)));
339
293
  }
340
- const isJsonObject$5 = Schema.is(Schema.JsonObject);
341
- const decodeJson$2 = Schema.decodeUnknownSync(Schema.Json);
294
+ const isJsonObject$4 = Schema.is(Schema.JsonObject);
295
+ const decodeJson$2 = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
342
296
  function canonicalJson(value) {
343
297
  if (Array.isArray(value)) return value.map(canonicalJson);
344
- if (!isJsonObject$5(value)) return value;
298
+ if (!isJsonObject$4(value)) return value;
345
299
  return Object.fromEntries(Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, canonicalJson(decodeJson$2(value[key]))]));
346
300
  }
347
301
 
@@ -349,17 +303,17 @@ function canonicalJson(value) {
349
303
  //#region ../server/src/internal/change-journal/change-journal.ts
350
304
  const ChangeBegin = Data.taggedEnum();
351
305
  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,
306
+ return ChangeBegin.$match(result, {
307
+ Unresolved: ({ change }) => Effect.succeed(change),
308
+ Recorded: ({ entry }) => Effect.fail(new ChangeAlreadyResolved({
309
+ changeId: entry.id,
356
310
  outcome: "applied"
357
- }));
358
- case "Cancelled": return Effect.fail(new ChangeAlreadyResolved({
359
- changeId: result.changeId,
311
+ })),
312
+ Cancelled: ({ changeId }) => Effect.fail(new ChangeAlreadyResolved({
313
+ changeId,
360
314
  outcome: "cancelled"
361
- }));
362
- }
315
+ }))
316
+ });
363
317
  }
364
318
  var ChangeJournal = class extends Context.Service()("@internal/server/ChangeJournal") {};
365
319
  const findPendingChange = Effect.fn("ChangeJournal.findPending")(function* (journal, subject) {
@@ -424,7 +378,7 @@ function initialAdministratorDraft(identity, intent) {
424
378
  action: "user.created",
425
379
  context: identity.context,
426
380
  id: identity.id,
427
- intent: Schema.encodeSync(InitialAdministratorIntent)(intent),
381
+ intent: Result.getOrThrow(Schema.encodeResult(InitialAdministratorIntent)(intent)),
428
382
  scope: "initialization",
429
383
  subject: userEmailSubject(intent.normalizedEmail)
430
384
  });
@@ -566,7 +520,7 @@ function userChangeDraft(identity, intent) {
566
520
  action: userChangeAction(intent),
567
521
  context: identity.context,
568
522
  id: identity.id,
569
- intent: Schema.encodeSync(UserChangeIntent)(intent),
523
+ intent: Result.getOrThrow(Schema.encodeResult(UserChangeIntent)(intent)),
570
524
  scope: "users",
571
525
  subject: userEmailSubject(intent.kind === "create" ? intent.normalizedEmail : intent.email)
572
526
  };
@@ -732,12 +686,22 @@ function appliedResolution(change, intent, userId) {
732
686
  //#region ../auth/src/internal/user.ts
733
687
  const cmsUserCreationChangeField = "cmsCreationChangeId";
734
688
  const cmsUserSessionsRevokedChangeField = "cmsSessionsRevokedChangeId";
689
+ const BetterAuthUser = Schema.Struct({
690
+ banned: Schema.optionalKey(Schema.NullOr(Schema.Boolean)),
691
+ createdAt: Schema.Date,
692
+ email: Schema.String,
693
+ emailVerified: Schema.Boolean,
694
+ id: Schema.String,
695
+ name: Schema.String,
696
+ role: Schema.optionalKey(Schema.NullOr(Schema.String)),
697
+ updatedAt: Schema.Date
698
+ });
735
699
  const BetterAuthUserRecoveryEvidence = Schema.Struct({
736
700
  [cmsUserCreationChangeField]: Schema.optionalKey(Schema.UndefinedOr(Schema.NullOr(ActivityId))),
737
701
  [cmsUserSessionsRevokedChangeField]: Schema.optionalKey(Schema.UndefinedOr(Schema.NullOr(ActivityId)))
738
702
  });
739
- function decodeBetterAuthUser(user) {
740
- return Schema.decodeUnknownEffect(User)({
703
+ function decodeBetterAuthUser(value) {
704
+ return Schema.decodeEffect(BetterAuthUser)(value).pipe(Effect.flatMap((user) => Schema.decodeUnknownEffect(User)({
741
705
  banned: user.banned === true,
742
706
  createdAt: user.createdAt.toISOString(),
743
707
  email: user.email,
@@ -746,7 +710,7 @@ function decodeBetterAuthUser(user) {
746
710
  name: user.name,
747
711
  role: user.role,
748
712
  updatedAt: user.updatedAt.toISOString()
749
- });
713
+ })));
750
714
  }
751
715
  function decodeBetterAuthActor(user) {
752
716
  return Schema.decodeEffect(StudioActor)(betterAuthActorInput(user));
@@ -783,12 +747,12 @@ function betterAuthActivityWriter(storage, crypto) {
783
747
  return yield* storage.append(activityEntryFromDraft(id, draft, yield* DateTime.now));
784
748
  });
785
749
  }
786
- function betterAuthActivityOptions(writeActivity, crypto) {
750
+ function betterAuthActivityOptions(writeActivity, crypto, runPromise) {
787
751
  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))));
752
+ 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
753
  const before = createAuthMiddleware((context) => {
790
754
  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(() => {
755
+ return runPromise(Effect.tryPromise(() => getSessionFromCtx(context, { disableRefresh: true })).pipe(Effect.orElseSucceed(() => null), Effect.tap((session) => Effect.sync(() => {
792
756
  const actor = session?.user ? decodeActor(session.user) : void 0;
793
757
  if (actor) capturedActors.set(context.context, actor);
794
758
  })), Effect.asVoid));
@@ -832,7 +796,149 @@ function activityDraft(action, actor, requestId) {
832
796
  }
833
797
  function decodeActor(user) {
834
798
  const option = decodeBetterAuthActorOption(user);
835
- return option._tag === "Some" ? option.value : void 0;
799
+ return Option.getOrUndefined(option);
800
+ }
801
+
802
+ //#endregion
803
+ //#region ../auth/src/internal/better-auth.ts
804
+ const cmsBetterAuthAccess = createAccessControl(defaultStatements);
805
+ const cmsBetterAuthRoles = {
806
+ administrator: cmsBetterAuthAccess.newRole({
807
+ session: ["list", "revoke"],
808
+ user: [
809
+ "create",
810
+ "list",
811
+ "get",
812
+ "set-role",
813
+ "delete",
814
+ "update"
815
+ ]
816
+ }),
817
+ editor: cmsBetterAuthAccess.newRole({
818
+ session: [],
819
+ user: []
820
+ }),
821
+ viewer: cmsBetterAuthAccess.newRole({
822
+ session: [],
823
+ user: []
824
+ })
825
+ };
826
+ const timestampFields = {
827
+ createdAt: "created_at",
828
+ updatedAt: "updated_at"
829
+ };
830
+ function createCmsBetterAuthOptions(options, activity) {
831
+ return {
832
+ advanced: { database: {
833
+ generateId: "uuid",
834
+ joins: true
835
+ } },
836
+ appName: "CMS Studio",
837
+ basePath: options.basePath ?? "/api/cms/auth",
838
+ baseURL: options.baseURL,
839
+ database: options.database,
840
+ emailAndPassword: {
841
+ disableSignUp: true,
842
+ enabled: true,
843
+ ...activity?.emailAndPassword
844
+ },
845
+ hooks: activity?.hooks,
846
+ user: {
847
+ additionalFields: {
848
+ [cmsUserCreationChangeField]: hiddenChangeField("cms_creation_change_id"),
849
+ [cmsUserSessionsRevokedChangeField]: hiddenChangeField("cms_sessions_revoked_change_id")
850
+ },
851
+ modelName: "auth_users",
852
+ fields: {
853
+ ...timestampFields,
854
+ emailVerified: "email_verified"
855
+ }
856
+ },
857
+ session: {
858
+ cookieCache: {
859
+ enabled: false,
860
+ strategy: "jwt"
861
+ },
862
+ modelName: "auth_sessions",
863
+ fields: {
864
+ ...timestampFields,
865
+ expiresAt: "expires_at",
866
+ ipAddress: "ip_address",
867
+ userAgent: "user_agent",
868
+ userId: "user_id"
869
+ }
870
+ },
871
+ account: {
872
+ modelName: "auth_accounts",
873
+ fields: {
874
+ accessToken: "access_token",
875
+ accessTokenExpiresAt: "access_token_expires_at",
876
+ accountId: "account_id",
877
+ ...timestampFields,
878
+ idToken: "id_token",
879
+ providerId: "provider_id",
880
+ refreshToken: "refresh_token",
881
+ refreshTokenExpiresAt: "refresh_token_expires_at",
882
+ userId: "user_id"
883
+ }
884
+ },
885
+ verification: {
886
+ modelName: "auth_verifications",
887
+ fields: {
888
+ ...timestampFields,
889
+ expiresAt: "expires_at"
890
+ }
891
+ },
892
+ rateLimit: {
893
+ enabled: true,
894
+ storage: "database",
895
+ modelName: "auth_rate_limits",
896
+ fields: { lastRequest: "last_request" }
897
+ },
898
+ plugins: [admin({
899
+ ac: cmsBetterAuthAccess,
900
+ adminRoles: ["administrator"],
901
+ defaultRole: "editor",
902
+ roles: cmsBetterAuthRoles,
903
+ schema: {
904
+ session: { fields: { impersonatedBy: "impersonated_by" } },
905
+ user: { fields: {
906
+ banExpires: "ban_expires",
907
+ banReason: "ban_reason",
908
+ banned: "banned",
909
+ role: "role"
910
+ } }
911
+ }
912
+ })],
913
+ secret: options.secret
914
+ };
915
+ }
916
+ function createCmsBetterAuth(options, activity) {
917
+ return betterAuth(createCmsBetterAuthOptions(options, activity));
918
+ }
919
+ function hiddenChangeField(fieldName) {
920
+ return {
921
+ fieldName,
922
+ input: false,
923
+ required: false,
924
+ returned: false,
925
+ type: "string"
926
+ };
927
+ }
928
+
929
+ //#endregion
930
+ //#region ../auth/src/internal/handler.ts
931
+ function rejectExternalAdminRoutes(handler, basePath) {
932
+ const mountedAt = canonicalBasePath(basePath);
933
+ return (request) => {
934
+ const path = new URL(request.url).pathname;
935
+ const relative = mountedAt === "/" ? path : path === mountedAt ? "/" : path.startsWith(`${mountedAt}/`) ? path.slice(mountedAt.length) : path;
936
+ if (relative === "/admin" || relative.startsWith("/admin/")) return Promise.resolve(new Response(null, { status: 404 }));
937
+ return handler(request);
938
+ };
939
+ }
940
+ function canonicalBasePath(basePath) {
941
+ return (basePath.startsWith("/") ? basePath : `/${basePath}`).replace(/\/+$/u, "") || "/";
836
942
  }
837
943
 
838
944
  //#endregion
@@ -915,9 +1021,24 @@ function matches(code, ...candidates) {
915
1021
  return code !== void 0 && candidates.some((candidate) => candidate.code === code);
916
1022
  }
917
1023
  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;
1024
+ if (!(cause instanceof Error) || cause.name !== "APIError") return void 0;
1025
+ const apiError = Schema.decodeUnknownOption(Schema.Struct({
1026
+ body: Schema.Struct({ code: Schema.String }),
1027
+ statusCode: Schema.Int
1028
+ }))(cause);
1029
+ return Option.map(apiError, ({ body }) => body.code).pipe(Option.getOrUndefined);
1030
+ }
1031
+
1032
+ //#endregion
1033
+ //#region ../auth/src/internal/provider.ts
1034
+ function providerCall(operation, request, onError) {
1035
+ return Effect.tryPromise({
1036
+ catch: (cause) => onError(operation, cause),
1037
+ try: request
1038
+ }).pipe(Effect.withSpan(operation));
1039
+ }
1040
+ function providerAdapter(auth, operation, onError) {
1041
+ return providerCall(operation, () => auth.$context, onError).pipe(Effect.map((context) => context.internalAdapter));
921
1042
  }
922
1043
 
923
1044
  //#endregion
@@ -926,13 +1047,10 @@ function betterAuthIdentity(auth) {
926
1047
  const authorityHeaders = /* @__PURE__ */ new WeakMap();
927
1048
  const authenticate = Effect.fn("BetterAuth.Authentication.authenticate")(function* (headers) {
928
1049
  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
- });
1050
+ const session = yield* providerCall("BetterAuth.getSession", () => auth.api.getSession({
1051
+ headers: providerHeaders,
1052
+ query: { disableCookieCache: true }
1053
+ }), () => studioInternalError());
936
1054
  if (!session?.user) return null;
937
1055
  const actor = yield* decodeBetterAuthActor(session.user).pipe(Effect.mapError(() => studioInternalError()));
938
1056
  const authority = issueIdentityAuthority();
@@ -1026,16 +1144,13 @@ function betterAuthIdentity(auth) {
1026
1144
  const apply = (changeId) => applyOperation(auth, "BetterAuth.revokeUserSessions", command, () => auth.api.revokeUserSessions({
1027
1145
  body: { userId: command.userId },
1028
1146
  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));
1147
+ })).pipe(Effect.flatMap(() => providerCall("BetterAuth.adminUpdateUser.sessionRevocationReceipt", () => auth.api.adminUpdateUser({
1148
+ body: {
1149
+ data: { [cmsUserSessionsRevokedChangeField]: changeId },
1150
+ userId: command.userId
1151
+ },
1152
+ headers
1153
+ }), (operation, cause) => IdentityApplyFailure.Unknown({ error: unavailable$4(operation, cause) }))), Effect.as(user));
1039
1154
  return IdentityPreparation.Prepared({
1040
1155
  apply,
1041
1156
  intent,
@@ -1053,22 +1168,13 @@ function betterAuthIdentity(auth) {
1053
1168
  };
1054
1169
  }
1055
1170
  function prepareOperation(auth, operation, request) {
1056
- return Effect.tryPromise({
1057
- catch: (cause) => prepareError(auth.$ERROR_CODES, operation, cause),
1058
- try: request
1059
- });
1171
+ return providerCall(operation, request, (name, cause) => prepareError(auth.$ERROR_CODES, name, cause));
1060
1172
  }
1061
1173
  function prepareUserOperation(auth, operation, userId, request) {
1062
- return Effect.tryPromise({
1063
- catch: (cause) => prepareError(auth.$ERROR_CODES, operation, cause, userId),
1064
- try: request
1065
- });
1174
+ return providerCall(operation, request, (name, cause) => prepareError(auth.$ERROR_CODES, name, cause, userId));
1066
1175
  }
1067
1176
  function applyOperation(auth, operation, command, request) {
1068
- return Effect.tryPromise({
1069
- catch: (cause) => applyError(auth.$ERROR_CODES, operation, command, cause),
1070
- try: request
1071
- });
1177
+ return providerCall(operation, request, (name, cause) => applyError(auth.$ERROR_CODES, name, command, cause));
1072
1178
  }
1073
1179
  function requireAuthority(authorityHeaders, authority) {
1074
1180
  const headers = authorityHeaders.get(authority);
@@ -1106,13 +1212,10 @@ function betterAuthInitialIdentity(auth) {
1106
1212
  });
1107
1213
  const intent = initialAdministratorIntent(input);
1108
1214
  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) }))))),
1215
+ apply: providerCall("BetterAuth.initialization.createUser", () => auth.api.createUser({ body: {
1216
+ data: { emailVerified: true },
1217
+ ...createInput
1218
+ } }), (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
1219
  intent
1117
1220
  };
1118
1221
  } });
@@ -1121,22 +1224,13 @@ function betterAuthInitialIdentity(auth) {
1121
1224
  //#endregion
1122
1225
  //#region ../auth/src/internal/recovery.ts
1123
1226
  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));
1227
+ const userAdapter = providerAdapter(auth, "BetterAuth.recovery.context", unavailable$4);
1228
+ const initialAdapter = providerAdapter(auth, "BetterAuth.initialization.recovery.context", initializationUnavailable$1);
1132
1229
  const users = IdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeUserChange")(function* (intent) {
1133
1230
  const adapter = yield* userAdapter;
1134
1231
  switch (intent.kind) {
1135
1232
  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
- });
1233
+ const result = yield* providerCall("BetterAuth.recovery.findUserByEmail", () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true }), unavailable$4);
1140
1234
  if (!result) return {
1141
1235
  creationChangeId: null,
1142
1236
  credentialReady: false,
@@ -1178,10 +1272,7 @@ function betterAuthRecovery(auth) {
1178
1272
  return {
1179
1273
  initial: InitialIdentityRecovery.of({ observe: Effect.fn("BetterAuth.Recovery.observeInitialAdministrator")(function* (intent) {
1180
1274
  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
- });
1275
+ const result = yield* providerCall("BetterAuth.recovery.findInitialAdministrator", () => adapter.findUserByEmail(intent.normalizedEmail, { includeAccounts: true }), initializationUnavailable$1);
1185
1276
  if (!result) return {
1186
1277
  credentialReady: false,
1187
1278
  user: null
@@ -1195,1589 +1286,165 @@ function betterAuthRecovery(auth) {
1195
1286
  };
1196
1287
  }
1197
1288
  function hasCredential(accounts) {
1198
- return accounts.some((account) => account.providerId === "credential" && (account.password?.length ?? 0) > 0);
1199
- }
1200
- function decodeUser(user, operation) {
1201
- return decodeBetterAuthUser(user).pipe(Effect.mapError((cause) => unavailable$4(operation, cause)));
1202
- }
1203
- function findUser(adapter, userId) {
1204
- return findRawUser(adapter, userId).pipe(Effect.flatMap((user) => user ? decodeUser(user, "BetterAuth.recovery.findUserById") : Effect.succeed(null)));
1205
- }
1206
- function findRawUser(adapter, userId) {
1207
- return Effect.tryPromise({
1208
- catch: (cause) => unavailable$4("BetterAuth.recovery.findUserById", cause),
1209
- try: () => adapter.findUserById(userId)
1210
- });
1211
- }
1212
- 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))));
1217
- }
1218
-
1219
- //#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
- }
1336
- function betterAuthLayer(options) {
1337
- return Layer.effectContext(Effect.gen(function* () {
1338
- const crypto = yield* Crypto.Crypto;
1339
- const storage = yield* ActivityStore;
1340
- const auth = createCmsBetterAuth(options, betterAuthActivityOptions(betterAuthActivityWriter(storage, crypto), crypto));
1341
- const identity = betterAuthIdentity(auth);
1342
- const recovery = betterAuthRecovery(auth);
1343
- const initialization = betterAuthInitialIdentity(auth);
1344
- 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 }));
1289
+ return accounts.some((account) => account.providerId === "credential" && (account.password?.length ?? 0) > 0);
2067
1290
  }
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
2073
- }));
1291
+ function decodeUser(user, operation) {
1292
+ return decodeBetterAuthUser(user).pipe(Effect.mapError((cause) => unavailable$4(operation, cause)));
2074
1293
  }
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;
1294
+ function findUser(adapter, userId) {
1295
+ return findRawUser(adapter, userId).pipe(Effect.flatMap((user) => user ? decodeUser(user, "BetterAuth.recovery.findUserById") : Effect.succeed(null)));
2077
1296
  }
2078
- function replaceSource(sources, source) {
2079
- return [...sources.filter((candidate) => candidate.documentId !== source.documentId), source];
1297
+ function findRawUser(adapter, userId) {
1298
+ return providerCall("BetterAuth.recovery.findUserById", () => adapter.findUserById(userId), unavailable$4);
2080
1299
  }
2081
- function withoutDocument(sources, documentId) {
2082
- return sources.filter((source) => source.documentId !== documentId);
1300
+ function findSessionFingerprints(adapter, userId) {
1301
+ return providerCall("BetterAuth.recovery.listSessions", () => adapter.listSessions(userId), unavailable$4).pipe(Effect.map((sessions) => sessions.map((session) => sessionFingerprint(session.id))));
2083
1302
  }
2084
1303
 
2085
1304
  //#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}`;
1305
+ //#region ../auth/src/BetterAuth.ts
1306
+ function betterAuthLayer(options) {
1307
+ if (options.basePath !== void 0 && options.basePath !== "/api/cms/auth") throw new Error(`Authentication basePath must be ${CMS_AUTHENTICATION_PATH}.`);
1308
+ return Layer.effectContext(Effect.gen(function* () {
1309
+ const crypto = yield* Crypto.Crypto;
1310
+ const storage = yield* ActivityStore;
1311
+ const runPromise = Effect.runPromiseWith(yield* Effect.context());
1312
+ const auth = createCmsBetterAuth(options, betterAuthActivityOptions(betterAuthActivityWriter(storage, crypto), crypto, runPromise));
1313
+ const identity = betterAuthIdentity(auth);
1314
+ const recovery = betterAuthRecovery(auth);
1315
+ const initialization = betterAuthInitialIdentity(auth);
1316
+ const basePath = options.basePath ?? "/api/cms/auth";
1317
+ const handler = AuthenticationHandler.of({ handle: rejectExternalAdminRoutes(auth.handler, basePath) });
1318
+ 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));
1319
+ }));
2107
1320
  }
2108
1321
 
2109
1322
  //#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
1323
+ //#region ../platform-node/src/internal/http/form-ingress.ts
1324
+ function handleFormRequest(request, handle) {
1325
+ return Effect.runPromise(Effect.gen(function* () {
1326
+ const ingress = yield* Effect.gen(function* () {
1327
+ const origin = request.headers.get("origin");
1328
+ if (origin && origin !== new URL(request.url).origin) return Response.json({ error: "Origin rejected." }, { status: 403 });
1329
+ if (request.method !== "POST") return new Response(null, { status: 405 });
1330
+ if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response(null, { status: 415 });
1331
+ const reader = request.body?.getReader();
1332
+ if (!reader) return new Response(null, { status: 400 });
1333
+ const chunks = [];
1334
+ let size = 0;
1335
+ try {
1336
+ while (true) {
1337
+ const chunk = yield* Effect.tryPromise(() => reader.read());
1338
+ if (chunk.done) break;
1339
+ size += chunk.value.byteLength;
1340
+ if (size > 131072) {
1341
+ yield* Effect.tryPromise(() => reader.cancel()).pipe(Effect.ignore);
1342
+ return new Response(null, { status: 413 });
2153
1343
  }
2154
- }, activity, input.reason);
2155
- report.updated += 1;
2156
- if (plan.publish) {
2157
- yield* publish(dependencies, updated, input, activity);
2158
- report.published += 1;
1344
+ chunks.push(chunk.value);
2159
1345
  }
2160
- break;
1346
+ } finally {
1347
+ reader.releaseLock();
2161
1348
  }
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);
1349
+ const body = new Uint8Array(size);
1350
+ let offset = 0;
1351
+ for (const chunk of chunks) {
1352
+ body.set(chunk, offset);
1353
+ offset += chunk.byteLength;
1354
+ }
1355
+ return body;
1356
+ }).pipe(Effect.orElseSucceed(() => new Response(null, { status: 400 })));
1357
+ if (ingress instanceof Response) return ingress;
1358
+ return yield* Effect.promise(() => handle(new Request(request.url, {
1359
+ method: "POST",
1360
+ headers: request.headers,
1361
+ body: ingress,
1362
+ signal: request.signal
1363
+ })));
1364
+ }).pipe(Effect.map((response) => {
1365
+ response.headers.set("cache-control", "no-store");
1366
+ return response;
1367
+ })));
2192
1368
  }
2193
1369
 
2194
1370
  //#endregion
2195
- //#region ../server/src/internal/documents/document-store.ts
2196
- var DocumentStore = class extends Context.Service()("@internal/server/DocumentStore") {};
1371
+ //#region ../server/src/Readiness.ts
1372
+ var ReadinessFailed = class extends Schema.TaggedError()("Cms.ReadinessFailed", { cause: Schema.Defect() }) {};
1373
+ var Readiness = class extends Context.Service()("@internal/server/Readiness") {};
1374
+ /** A stale unresolved external change makes the installation unavailable until reconciliation resolves it. */
1375
+ const readinessLayer = Layer.effect(Readiness, Effect.gen(function* () {
1376
+ const journal = yield* ChangeJournal;
1377
+ const check = Effect.gen(function* () {
1378
+ const now = yield* DateTime.now;
1379
+ const pending = yield* journal.pending({
1380
+ before: DateTime.subtractDuration(now, "30 seconds"),
1381
+ limit: 1
1382
+ }).pipe(Effect.mapError((cause) => new ReadinessFailed({ cause })));
1383
+ const oldest = pending.changes[0];
1384
+ if (pending.total === 0) return;
1385
+ if (oldest === void 0) return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("The pending change journal returned no change.") });
1386
+ yield* Effect.logWarning("CMS readiness found a stale pending change.", {
1387
+ action: oldest.action,
1388
+ ageMillis: DateTime.toEpochMillis(now) - DateTime.toEpochMillis(oldest.updatedAt),
1389
+ changeId: oldest.id,
1390
+ phase: "pending"
1391
+ });
1392
+ return yield* new ReadinessFailed({ cause: /* @__PURE__ */ new Error("A stale change remains pending.") });
1393
+ }).pipe(Effect.withSpan("Installation.readiness"));
1394
+ return Readiness.of({ check });
1395
+ }));
2197
1396
 
2198
1397
  //#endregion
2199
- //#region ../server/src/internal/documents/document-failures.ts
2200
- var DocumentPersistenceError = class extends Schema.TaggedError()("Documents.PersistenceError", {
1398
+ //#region ../server/src/internal/working-documents/working-document-model.ts
1399
+ const WorkingDocumentGenerationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("WorkingDocumentGenerationId"));
1400
+ const WorkingDocumentUpdateSequence = Schema.Int.check(Schema.isGreaterThan(0)).pipe(Schema.brand("WorkingDocumentUpdateSequence"));
1401
+ const WorkingDocumentHighWatermark = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.brand("WorkingDocumentHighWatermark"));
1402
+ var WorkingDocumentPersistenceError = class extends Schema.TaggedError()("WorkingDocuments.PersistenceError", {
2201
1403
  operation: Schema.NonEmptyString,
2202
1404
  cause: Schema.Defect()
2203
1405
  }) {};
2204
- var DocumentGenerationConflict = class extends Schema.TaggedError()("Documents.GenerationConflict", {
2205
- expectedGeneration: DocumentGeneration,
2206
- actualGeneration: DocumentGeneration
1406
+ var WorkingDocumentSuperseded = class extends Schema.TaggedError()("WorkingDocuments.Superseded", {
1407
+ documentId: DocumentId,
1408
+ expectedGeneration: WorkingDocumentGenerationId,
1409
+ actualGeneration: Schema.optionalKey(WorkingDocumentGenerationId)
1410
+ }) {};
1411
+ var WorkingDocumentConflict = class extends Schema.TaggedError()("WorkingDocuments.Conflict", {
1412
+ documentId: DocumentId,
1413
+ operation: Schema.NonEmptyString,
1414
+ message: Schema.NonEmptyString
2207
1415
  }) {};
2208
1416
 
2209
1417
  //#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;
1418
+ //#region ../server/src/internal/working-documents/working-document.ts
1419
+ const WorkingDocumentRef = Schema.Struct({
1420
+ documentId: DocumentId,
1421
+ generationId: WorkingDocumentGenerationId
2306
1422
  });
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
- };
1423
+ var WorkingDocumentInvalid = class extends Schema.TaggedError()("WorkingDocuments.Invalid", {
1424
+ documentId: DocumentId,
1425
+ message: Schema.NonEmptyString,
1426
+ cause: Schema.Defect()
1427
+ }) {};
1428
+ var WorkingDocumentUnavailable = class extends Schema.TaggedError()("WorkingDocuments.Unavailable", {
1429
+ operation: Schema.NonEmptyString,
1430
+ cause: Schema.Defect()
1431
+ }) {};
1432
+ /** An authored or working baseline committed, but the publish workflow still needs recovery. */
1433
+ var WorkingDocumentCheckpointed = class extends Schema.TaggedError()("WorkingDocuments.Checkpointed", {
1434
+ checkpointVersion: DocumentVersion,
1435
+ documentId: DocumentId,
1436
+ result: Schema.optionalKey(DocumentMutationResult),
1437
+ stateVector: Schema.Uint8Array,
1438
+ cause: Schema.Defect()
1439
+ }) {};
1440
+ const WorkingDocumentChange = Schema.Struct({
1441
+ kind: Schema.Literals(["updated", "invalidated"]),
1442
+ ref: WorkingDocumentRef
2738
1443
  });
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
- }
1444
+ var WorkingDocuments = class extends Context.Service()("@internal/server/WorkingDocuments") {};
2777
1445
 
2778
1446
  //#endregion
2779
1447
  //#region ../document/src/internal/editor/identity-extension.ts
2780
- /** Ensures Yjs only observes authorable nodes with final, unique identities. */
2781
1448
  const EditorNodeIdentity = Extension.create({
2782
1449
  name: "cmsNodeIdentity",
2783
1450
  priority: 1e4,
@@ -3326,7 +1993,7 @@ function unwrapFirstParagraph(nodes) {
3326
1993
  }
3327
1994
  function restoreOpaqueNode(value, nodeId) {
3328
1995
  const decoded = Schema.decodeUnknownOption(MarkdownNode)(value);
3329
- if (decoded._tag === "None" || Predicate.isString(decoded.value)) return void 0;
1996
+ if (Option.isNone(decoded) || Predicate.isString(decoded.value)) return void 0;
3330
1997
  const node = structuredClone(decoded.value);
3331
1998
  if (nodeId === void 0) return node;
3332
1999
  if (node[0] === null) return [
@@ -3436,10 +2103,10 @@ const PROPERTIES_NAME = "properties";
3436
2103
  const PROPS_PREFIX = "props:";
3437
2104
  const CMS_PROPS_PREFIX = "cmsProps:";
3438
2105
  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));
2106
+ const decodeEditorNodeId = flow(Schema.decodeUnknownResult(EditorNodeId), Result.getOrThrow);
2107
+ const decodeJson$1 = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
2108
+ const decodeJsonObject$1 = flow(Schema.decodeUnknownResult(Schema.JsonObject), Result.getOrThrow);
2109
+ const decodePath = flow(Schema.decodeUnknownResult(Schema.Array(Schema.String)), Result.getOrThrow);
3443
2110
  function seedCollaborationFields(document, content) {
3444
2111
  const state = extractGranularState(content);
3445
2112
  replaceMap(document.getMap(FRONTMATTER_NAME), flatten(state.frontmatter));
@@ -3589,9 +2256,9 @@ const METADATA_NAME = "cms";
3589
2256
  const META_KEY = "meta";
3590
2257
  const SOURCE_PATH_KEY = "sourcePath";
3591
2258
  const editorSchema = createEditorSchema();
3592
- const decodeJson = Schema.decodeUnknownSync(Schema.Json);
3593
- const decodeJsonObject = Schema.decodeUnknownSync(Schema.JsonObject);
3594
- const decodeSourcePath = Schema.decodeUnknownSync(SourcePath);
2259
+ const decodeJson = flow(Schema.decodeUnknownResult(Schema.Json), Result.getOrThrow);
2260
+ const decodeJsonObject = flow(Schema.decodeUnknownResult(Schema.JsonObject), Result.getOrThrow);
2261
+ const decodeSourcePath = flow(Schema.decodeUnknownResult(SourcePath), Result.getOrThrow);
3595
2262
  function seedCollaborationDocument(sourcePath, seed, meta) {
3596
2263
  const document = prosemirrorJSONToYDoc(editorSchema, seed, BODY_NAME);
3597
2264
  try {
@@ -3688,7 +2355,6 @@ function sameBytes$1(left, right) {
3688
2355
 
3689
2356
  //#endregion
3690
2357
  //#region ../document/src/CollaborationDocument.ts
3691
- /** Creates the initial collaborative state from the canonical editor projection. */
3692
2358
  const createCollaborationDocument = Effect.fn("CollaborationDocument.create")(function* (input) {
3693
2359
  const seed = yield* openEditorDocument(input.body);
3694
2360
  return yield* Effect.try({
@@ -3696,7 +2362,6 @@ const createCollaborationDocument = Effect.fn("CollaborationDocument.create")(fu
3696
2362
  catch: (cause) => invalidCollaboration("Could not create the collaborative editor document.", cause)
3697
2363
  });
3698
2364
  });
3699
- /** Renders collaborative state through the canonical editor-to-Markdown encoder. */
3700
2365
  const renderCollaborationDocument = Effect.fn("CollaborationDocument.render")(function* (document) {
3701
2366
  const inspected = yield* inspectCollaborationDocument(document);
3702
2367
  return {
@@ -3715,31 +2380,26 @@ const captureCollaborationDocument = Effect.fn("CollaborationDocument.capture")(
3715
2380
  catch: (cause) => invalidCollaboration("Could not capture the collaborative editor document.", cause)
3716
2381
  });
3717
2382
  });
3718
- /** Restores a validated, caller-owned document from a full collaboration snapshot. */
3719
2383
  const restoreCollaborationDocument = Effect.fn("CollaborationDocument.restore")(function* (snapshot) {
3720
2384
  return yield* validateRestoredDocument(yield* Effect.try({
3721
2385
  try: () => restoreCollaborationSnapshot(snapshot),
3722
2386
  catch: (cause) => invalidCollaboration("Could not restore the collaborative editor document.", cause)
3723
2387
  }));
3724
2388
  });
3725
- /** Restores a validated document when only its authoritative full-state update is persisted. */
3726
2389
  const restoreCollaborationState = Effect.fn("CollaborationDocument.restoreState")(function* (state) {
3727
2390
  return yield* validateRestoredDocument(yield* Effect.try({
3728
2391
  try: () => restoreCollaborationUpdate(state),
3729
2392
  catch: (cause) => invalidCollaboration("Could not restore the collaborative editor document.", cause)
3730
2393
  }));
3731
2394
  });
3732
- /** Applies one encoded collaboration update while translating corrupt data to a typed failure. */
3733
2395
  const applyCollaborationUpdate = Effect.fn("CollaborationDocument.applyUpdate")((document, update) => Effect.try({
3734
2396
  try: () => applyCollaborationStateUpdate(document, update),
3735
2397
  catch: (cause) => invalidCollaboration("Could not apply the collaborative document update.", cause)
3736
2398
  }));
3737
- /** Captures the current state vector and an optional differential update. */
3738
2399
  const synchronizeCollaborationState = Effect.fn("CollaborationDocument.synchronizeState")((document, stateVector) => Effect.try({
3739
2400
  try: () => synchronizeCollaborationStateValue(document, stateVector),
3740
2401
  catch: (cause) => invalidCollaboration("Could not synchronize the collaborative document state.", cause)
3741
2402
  }));
3742
- /** Releases a collaboration document owned by a scoped workflow. */
3743
2403
  function destroyCollaborationDocument(document) {
3744
2404
  return Effect.sync(() => document.destroy());
3745
2405
  }
@@ -3840,6 +2500,25 @@ function sameJson(left, right) {
3840
2500
  return JSON.stringify(left) === JSON.stringify(right);
3841
2501
  }
3842
2502
 
2503
+ //#endregion
2504
+ //#region ../server/src/internal/working-documents/working-document-source.ts
2505
+ const validatedWorkingDocument = Effect.fn("WorkingDocuments.validateSource")(function* (source, validate) {
2506
+ const validation = yield* validate(UpdateDocumentInput.make({
2507
+ documentId: source.documentId,
2508
+ expectedVersion: source.version,
2509
+ markdown: source.markdown
2510
+ }));
2511
+ if (!validation.valid || !validation.document) return yield* new WorkingDocumentInvalid({
2512
+ cause: validation.issues,
2513
+ documentId: source.documentId,
2514
+ message: "Authored document cannot initialize a Working Document."
2515
+ });
2516
+ return validation.document;
2517
+ });
2518
+ const newWorkingSnapshot = Effect.fn("WorkingDocuments.newSnapshot")(function* (source, document, generationId) {
2519
+ return yield* createWorkingSnapshot(source, document, yield* generationId(source.documentId));
2520
+ });
2521
+
3843
2522
  //#endregion
3844
2523
  //#region ../server/src/internal/working-documents/working-document-reconciliation.ts
3845
2524
  const isDocumentNotFound$3 = Schema.is(DocumentNotFound);
@@ -3854,18 +2533,6 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
3854
2533
  })
3855
2534
  }).pipe(Effect.asVoid);
3856
2535
  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
2536
  const rotateDirty = Effect.fn("WorkingDocuments.reconcile.rotateDirty")(function* (snapshot) {
3870
2537
  const generationId = yield* options.generationId(snapshot.documentId);
3871
2538
  const seed = yield* useWorkingSnapshot(snapshot, (document) => captureCollaborationDocument(document).pipe(Effect.map((captured) => rotatedSeed(snapshot, generationId, captured))));
@@ -3901,10 +2568,10 @@ const startWorkingDocumentReconciliation = Effect.fn("WorkingDocuments.startReco
3901
2568
  yield* publishChange(loaded.value);
3902
2569
  return;
3903
2570
  }
3904
- const document = yield* validatedDocument(source.value);
2571
+ const document = yield* validatedWorkingDocument(source.value, options.documents.validate);
3905
2572
  yield* options.store.replace({
3906
2573
  expectedGeneration: loaded.value.generationId,
3907
- snapshot: yield* newSnapshot(source.value, document)
2574
+ snapshot: yield* newWorkingSnapshot(source.value, document, options.generationId)
3908
2575
  });
3909
2576
  yield* publishChange(loaded.value);
3910
2577
  }));
@@ -3990,13 +2657,6 @@ function rotatedSeed(snapshot, generationId, captured) {
3990
2657
  state: Uint8Array.from(captured.state)
3991
2658
  };
3992
2659
  }
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
2660
  function generationAtOrBefore(candidate, current) {
4001
2661
  return BigInt(candidate) <= BigInt(current);
4002
2662
  }
@@ -4004,6 +2664,28 @@ function generationsAreAdjacent(current, candidate) {
4004
2664
  return BigInt(candidate) === BigInt(current) + 1n;
4005
2665
  }
4006
2666
 
2667
+ //#endregion
2668
+ //#region ../server/src/internal/working-documents/document-locks.ts
2669
+ /** Retain a lock only while an operation is running or waiting for that document. */
2670
+ function documentSynchronization() {
2671
+ const locks = /* @__PURE__ */ new Map();
2672
+ return (documentId, effect) => Effect.acquireUseRelease(Effect.sync(() => {
2673
+ let lock = locks.get(documentId);
2674
+ if (!lock) {
2675
+ lock = {
2676
+ semaphore: Semaphore.makeUnsafe(1),
2677
+ users: 0
2678
+ };
2679
+ locks.set(documentId, lock);
2680
+ }
2681
+ lock.users += 1;
2682
+ return lock;
2683
+ }), (lock) => lock.semaphore.withPermit(effect), (lock) => Effect.sync(() => {
2684
+ lock.users -= 1;
2685
+ if (lock.users === 0 && locks.get(documentId) === lock) locks.delete(documentId);
2686
+ }));
2687
+ }
2688
+
4007
2689
  //#endregion
4008
2690
  //#region ../server/src/internal/working-documents/working-documents.ts
4009
2691
  function workingDocumentsLayer(options = {}) {
@@ -4017,31 +2699,11 @@ const makeWorkingDocuments = Effect.fn("WorkingDocuments.make")(function* (optio
4017
2699
  const store = yield* WorkingDocumentStore;
4018
2700
  const changeSource = yield* WorkingDocumentChanges;
4019
2701
  const changes = yield* Effect.acquireRelease(PubSub.bounded(64), PubSub.shutdown);
4020
- const locks = /* @__PURE__ */ new Map();
2702
+ const synchronized = documentSynchronization();
4021
2703
  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
2704
  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
2705
  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
2706
  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
2707
  const compactSnapshot = Effect.fn("WorkingDocuments.compactSnapshot")(function* (snapshot) {
4046
2708
  const lastUpdate = snapshot.updates.at(-1);
4047
2709
  if (!lastUpdate) return false;
@@ -4121,21 +2783,21 @@ const makeWorkingDocuments = Effect.fn("WorkingDocuments.make")(function* (optio
4121
2783
  const ensureOpenSnapshot = Effect.fn("WorkingDocuments.ensureOpenSnapshot")(function* (source) {
4122
2784
  const loaded = yield* store.load(source.documentId);
4123
2785
  if (Option.isNone(loaded)) {
4124
- const document = yield* validatedDocument(source);
4125
- const initialized = yield* store.initialize(yield* newSnapshot(source, document));
2786
+ const document = yield* validatedWorkingDocument(source, maintenance.validate);
2787
+ const initialized = yield* store.initialize(yield* newWorkingSnapshot(source, document, generationId));
4126
2788
  if (initialized.savedVersion === source.version) return initialized;
4127
2789
  if (yield* workingSnapshotIsDirty(initialized)) return initialized;
4128
2790
  return yield* store.replace({
4129
2791
  expectedGeneration: initialized.generationId,
4130
- snapshot: yield* newSnapshot(source, document)
2792
+ snapshot: yield* newWorkingSnapshot(source, document, generationId)
4131
2793
  });
4132
2794
  }
4133
2795
  if (loaded.value.savedVersion === source.version) return loaded.value;
4134
2796
  if (yield* workingSnapshotIsDirty(loaded.value)) return loaded.value;
4135
- const document = yield* validatedDocument(source);
2797
+ const document = yield* validatedWorkingDocument(source, maintenance.validate);
4136
2798
  return yield* store.replace({
4137
2799
  expectedGeneration: loaded.value.generationId,
4138
- snapshot: yield* newSnapshot(source, document)
2800
+ snapshot: yield* newWorkingSnapshot(source, document, generationId)
4139
2801
  });
4140
2802
  });
4141
2803
  const open = (context, documentId) => synchronized(documentId, Effect.gen(function* () {
@@ -4359,7 +3021,7 @@ function closeHocuspocus(hocuspocus, operations) {
4359
3021
  const store = hocuspocus.debouncer.executeNow(`onStoreDocument-${document.name}`);
4360
3022
  return store === void 0 ? [] : [store];
4361
3023
  });
4362
- yield* Effect.promise(() => Promise.allSettled(stores));
3024
+ const storeResults = yield* Effect.promise(() => Promise.allSettled(stores));
4363
3025
  yield* Effect.promise(operations.drain);
4364
3026
  const cleanupResults = yield* Effect.promise(() => Promise.allSettled(documents.map((document) => document.saveMutex.runExclusive(() => Promise.resolve()).then(() => hocuspocus.unloadDocument(document)))));
4365
3027
  const destroyResult = yield* Effect.result(Effect.tryPromise({
@@ -4367,6 +3029,8 @@ function closeHocuspocus(hocuspocus, operations) {
4367
3029
  catch: (cause) => gatewayError("close", cause)
4368
3030
  }).pipe(Effect.asVoid));
4369
3031
  const cleanupFailure = cleanupResults.find((result) => result.status === "rejected");
3032
+ const storeFailure = storeResults.find((result) => result.status === "rejected");
3033
+ if (storeFailure?.status === "rejected") return yield* gatewayError("close", storeFailure.reason);
4370
3034
  if (cleanupFailure?.status === "rejected") return yield* gatewayError("close", cleanupFailure.reason);
4371
3035
  if (Result.isFailure(destroyResult)) return yield* destroyResult.failure;
4372
3036
  });
@@ -4380,7 +3044,6 @@ function gatewayError(operation, cause) {
4380
3044
 
4381
3045
  //#endregion
4382
3046
  //#region ../platform-node/src/internal/realtime/keyed-operations.ts
4383
- /** Serializes each room independently and retains poisoned writes for shutdown reporting. */
4384
3047
  function createKeyedOperations(options) {
4385
3048
  const pending = /* @__PURE__ */ new Map();
4386
3049
  const failures = /* @__PURE__ */ new Map();
@@ -4474,7 +3137,6 @@ function stableStringHash(value) {
4474
3137
  //#region ../platform-node/src/internal/realtime/wake-dispatcher.ts
4475
3138
  const DEFAULT_CAPACITY = 1024;
4476
3139
  const DEFAULT_CONCURRENCY = 32;
4477
- /** Bounded wake scheduling: duplicate rooms coalesce and unrelated rooms run concurrently. */
4478
3140
  const runStudioRealtimeWakeDispatcher = Effect.fn("StudioRealtimeWakeDispatcher.run")(function* (wakes, handle, options = {}) {
4479
3141
  const queue = yield* Queue.dropping(options.capacity ?? DEFAULT_CAPACITY);
4480
3142
  const queued = /* @__PURE__ */ new Set();
@@ -4687,7 +3349,7 @@ const makeStudioRealtimeRoomHost = Effect.fn("StudioRealtimeRoomHost.make")(func
4687
3349
  },
4688
3350
  onStateless: (payload) => runTracked(Effect.gen(function* () {
4689
3351
  const decoded = yield* Effect.option(decodeClientMessage(payload.payload));
4690
- if (decoded._tag === "None" || decoded.value.type !== "checkpoint") return;
3352
+ if (Option.isNone(decoded) || decoded.value.type !== "checkpoint") return;
4691
3353
  const name = yield* decodeRoomName(payload.documentName).pipe(Effect.mapError((cause) => roomHostError("checkpoint", cause)));
4692
3354
  const stateVector = Y.encodeStateVector(payload.document);
4693
3355
  yield* Effect.tryPromise({
@@ -4820,7 +3482,9 @@ function decodeBytes(value, tokenInvalid) {
4820
3482
  try: () => {
4821
3483
  const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
4822
3484
  const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
4823
- return Uint8Array.from(binary, (character) => character.codePointAt(0) ?? 0);
3485
+ const bytes = Uint8Array.from(binary, (character) => character.codePointAt(0) ?? 0);
3486
+ if (encodeBytes(bytes) !== value) throw new Error("Noncanonical base64url encoding");
3487
+ return bytes;
4824
3488
  },
4825
3489
  catch: tokenInvalid
4826
3490
  });
@@ -4962,7 +3626,7 @@ const makeStudioRealtimeGateway = Effect.gen(function* () {
4962
3626
  onStateless: (payload) => {
4963
3627
  return runTracked(Effect.gen(function* () {
4964
3628
  const decoded = yield* Effect.option(decodeClientMessage(payload.payload));
4965
- if (decoded._tag === "None") return;
3629
+ if (Option.isNone(decoded)) return;
4966
3630
  const request = decoded.value;
4967
3631
  const context = authenticatedContext(payload.connection.context, payload.documentName);
4968
3632
  if (request.type === "checkpoint") return;
@@ -5172,11 +3836,10 @@ function httpProtocol(protocol) {
5172
3836
  * StudioRealtimeRoomHost synchronizes durable Yjs state across replicas. Presence
5173
3837
  * and stateless broadcasts remain process-local until an ephemeral bus is added.
5174
3838
  */
5175
- const studioRealtimeGatewayLayer = Layer.effect(StudioRealtimeGateway, Effect.acquireRelease(makeStudioRealtimeGateway, (gateway) => gateway.close.pipe(Effect.catchCause((cause) => Effect.logError("Studio realtime gateway shutdown failed.", cause)))));
3839
+ 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
3840
 
5177
3841
  //#endregion
5178
3842
  //#region ../platform-node/src/internal/realtime/websocket.ts
5179
- /** Hides Hocuspocus connection bookkeeping from framework WebSocket adapters. */
5180
3843
  function studioRealtimeWebSocketBridge(gateway) {
5181
3844
  const connections = /* @__PURE__ */ new WeakMap();
5182
3845
  const release = (socket) => {
@@ -5201,21 +3864,25 @@ function studioRealtimeWebSocketBridge(gateway) {
5201
3864
  //#endregion
5202
3865
  //#region ../platform-node/src/internal/installation/host.ts
5203
3866
  var Installation = class extends Context.Service()("@internal/platform-node/Installation") {};
5204
- function installationHttpLayer(bootstrap, application, cleanup) {
3867
+ function installationHttpLayer(bootstrap, cleanup) {
5205
3868
  return Layer.effect(Installation, Effect.gen(function* () {
5206
- const outerContext = yield* Effect.context();
5207
3869
  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)));
3870
+ const routerConfig = yield* HttpRouter.RouterConfig;
3871
+ const router = yield* HttpRouter.HttpRouter;
3872
+ const handle = HttpEffect.toWebHandlerWith(yield* Effect.context())(router.asHttpEffect(), HttpMiddleware.logger);
3873
+ return yield* makeInstallation(bootstrap, handle, (context) => cleanup(context).pipe(Effect.provide(cleanupContext)), routerConfig);
5211
3874
  }));
5212
3875
  }
5213
- const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, handle, cleanup) {
3876
+ const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, handle, cleanup, routerConfig) {
5214
3877
  const authentication = yield* AuthenticationHandler;
5215
3878
  const crypto = yield* Crypto.Crypto;
5216
3879
  const realtime = yield* StudioRealtimeGateway;
5217
3880
  const roomHost = yield* StudioRealtimeRoomHost;
5218
- const health = (yield* Readiness).check.pipe(Effect.andThen(roomHost.check), Effect.match({
3881
+ const readiness = yield* Readiness;
3882
+ const protectedRoutes = FindMyWay.make(routerConfig);
3883
+ protectedRoutes.all(CMS_FORM_SUBMIT_PATH, "form");
3884
+ protectedRoutes.all(`${STUDIO_API_PATH}/preview/*`, "preview");
3885
+ const health = readiness.check.pipe(Effect.andThen(roomHost.check), Effect.match({
5219
3886
  onFailure: () => ({
5220
3887
  live: true,
5221
3888
  ready: false
@@ -5241,13 +3908,15 @@ const makeInstallation = Effect.fn("Installation.make")(function* (bootstrap, ha
5241
3908
  cleanupMedia,
5242
3909
  fetch: (request) => {
5243
3910
  const pathname = new URL(request.url).pathname;
3911
+ const protection = protectedRoutes.find(request.method, pathname)?.handler;
3912
+ if (protection === "form") return handleFormRequest(request, handle);
5244
3913
  if (pathname === "/api/cms/auth" || pathname.startsWith(`${"/api/cms/auth"}/`)) return authentication.handle(request);
5245
3914
  if (pathname === CMS_LIVENESS_PATH) return Promise.resolve(healthResponse({
5246
3915
  live: true,
5247
3916
  ready: true
5248
3917
  }, false));
5249
3918
  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) => {
3919
+ if (protection === "preview") return handle(request).then((response) => {
5251
3920
  response.headers.set("cache-control", "private, no-store");
5252
3921
  return response;
5253
3922
  });
@@ -5295,21 +3964,6 @@ const startInstallation = Effect.fn("CmsInstallation.start")(function* (options)
5295
3964
  };
5296
3965
  });
5297
3966
 
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
3967
  //#endregion
5314
3968
  //#region ../server/src/internal/media/media-change.ts
5315
3969
  const MediaUploadIntent = Schema.Struct({
@@ -5472,7 +4126,7 @@ function makeMediaChange(input) {
5472
4126
  action: input.action,
5473
4127
  context: input.change.context,
5474
4128
  id: input.change.id,
5475
- intent: Schema.encodeSync(MediaChangeIntent)(input.intent),
4129
+ intent: Result.getOrThrow(Schema.encodeResult(MediaChangeIntent)(input.intent)),
5476
4130
  scope: "media",
5477
4131
  subject: input.subject,
5478
4132
  target: input.target
@@ -5481,55 +4135,43 @@ function makeMediaChange(input) {
5481
4135
 
5482
4136
  //#endregion
5483
4137
  //#region ../server/src/internal/media/media-reconciliation.ts
5484
- function recoverPendingMedia(store, objectStore, operations, recoveryAge, options = {}) {
4138
+ const recoveryBatchSize = 100;
4139
+ function recoverPendingMedia(store, objectStore, operations, recoveryAge, cursor = {}) {
5485
4140
  return Effect.gen(function* () {
5486
4141
  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") {
4142
+ const pending = yield* store.pending({
4143
+ after: cursor.media,
4144
+ before,
4145
+ limit: recoveryBatchSize
4146
+ });
4147
+ for (const record of pending) {
4148
+ if (record.status === "deleting") {
4149
+ yield* objectStore.remove(record.objectKey);
4150
+ yield* store.finishDelete(record.id);
4151
+ continue;
4152
+ }
4153
+ const stored = yield* objectStore.stat(record.objectKey);
4154
+ if (Option.isSome(stored) && stored.value.byteSize === record.byteSize && (stored.value.mimeType === void 0 || stored.value.mimeType === record.mimeType)) yield* store.activate(record.id);
4155
+ else {
4156
+ if (Option.isSome(stored)) {
5495
4157
  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);
4158
+ const remaining = yield* objectStore.stat(record.objectKey);
4159
+ if (Option.isSome(remaining)) continue;
5508
4160
  }
4161
+ yield* store.abandonUpload(record.id);
5509
4162
  }
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
- }
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;
5525
- });
5526
- let replacementBatch = 0;
5527
- let replacementsComplete = false;
5528
- while (!replacementsComplete && replacementBatch < maxBatches) {
5529
- replacementsComplete = yield* recoverReplacementBatch;
5530
- replacementBatch += 1;
5531
4163
  }
4164
+ const replacements = yield* store.pendingReplacements({
4165
+ after: cursor.replacements,
4166
+ before,
4167
+ limit: recoveryBatchSize
4168
+ });
4169
+ yield* Effect.forEach(replacements, (replacement) => recoverReplacement(store, objectStore, replacement).pipe(Effect.catchTag("Media.Conflict", () => Effect.void)), { discard: true });
5532
4170
  yield* recoverPendingMediaCleanup(operations, objectStore, before);
4171
+ return {
4172
+ media: pending.length < recoveryBatchSize ? void 0 : pending.at(-1)?.id,
4173
+ replacements: replacements.length < recoveryBatchSize ? void 0 : replacements.at(-1)?.mediaId
4174
+ };
5533
4175
  });
5534
4176
  }
5535
4177
  function recoverPendingMediaCleanup(operations, objectStore, before) {
@@ -5717,10 +4359,14 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
5717
4359
  const store = yield* MediaStore;
5718
4360
  const baseUrl = options.publicBaseUrl.replace(/\/+$/gu, "");
5719
4361
  const pendingRecoveryAge = options.pendingRecoveryAge ?? "5 minutes";
5720
- yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge);
4362
+ const recoveryCursor = yield* Ref.make(yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge));
5721
4363
  const mutations = yield* Semaphore.make(1);
5722
4364
  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 }));
4365
+ if (recoveryInterval !== false) yield* mutations.withPermits(1)(Effect.gen(function* () {
4366
+ const cursor = yield* Ref.get(recoveryCursor);
4367
+ const next = yield* recoverPendingMedia(store, objectStore, operations, pendingRecoveryAge, cursor);
4368
+ yield* Ref.set(recoveryCursor, next);
4369
+ })).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
4370
  const toAsset = (record) => MediaAsset.make({
5725
4371
  byteSize: record.byteSize,
5726
4372
  createdAt: record.createdAt,
@@ -5789,7 +4435,7 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
5789
4435
  return yield* Effect.scoped(prepareUpload(input).pipe(Effect.flatMap((prepared) => mutations.withPermits(1)(Effect.gen(function* () {
5790
4436
  const id = MediaId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
5791
4437
  const operationId = ActivityId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
5792
- const objectKey = MediaObjectKey.make(`media/${id}.${imageExtension(prepared.mimeType)}`);
4438
+ const objectKey = MediaObjectKey.make(`${mediaObjectPrefix}${id}.${imageExtension(prepared.mimeType)}`);
5793
4439
  const now = yield* DateTime.now;
5794
4440
  const reserved = yield* store.reserve({
5795
4441
  byteSize: input.metadata.declaredByteSize,
@@ -5837,7 +4483,7 @@ const makeMediaLibrary = Effect.fn("MediaLibrary.make")(function* (options) {
5837
4483
  name: input.metadata.name,
5838
4484
  objectKey: current.value.objectKey,
5839
4485
  staged: false,
5840
- stagingObjectKey: MediaObjectKey.make(`media/replacements/${operationId}.${imageExtension(prepared.mimeType)}`),
4486
+ stagingObjectKey: MediaObjectKey.make(`${mediaObjectPrefix}replacements/${operationId}.${imageExtension(prepared.mimeType)}`),
5841
4487
  updatedAt: timestamp
5842
4488
  };
5843
4489
  const pendingReplacement = yield* store.beginReplace(replacement, {
@@ -5967,6 +4613,37 @@ function activityFailure(operation) {
5967
4613
  });
5968
4614
  }
5969
4615
 
4616
+ //#endregion
4617
+ //#region ../server/src/ProductModules.ts
4618
+ function productModulesLayer(options) {
4619
+ const accessControl = defaultAccessControlLayer;
4620
+ const seo = seoLayer(options.bootstrap.seo ?? makeDefaultSeoOptions(options.baseUrl)).pipe(Layer.provide(accessControl));
4621
+ const authoredDocuments = documentsLayer(options.bootstrap.documents, {
4622
+ ...options.documentEngine,
4623
+ components: options.documentEngine.components ?? [],
4624
+ mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? "https://media.invalid"
4625
+ }).pipe(Layer.provide(accessControl));
4626
+ const documentAccess = Layer.merge(authoredDocuments, accessControl);
4627
+ const activity = activityLayer.pipe(Layer.provide(accessControl));
4628
+ const workingDocuments = workingDocumentsLayer(options.workingDocuments).pipe(Layer.provide(documentAccess));
4629
+ const seoDelivery = seoDeliveryLayer.pipe(Layer.provide(Layer.merge(documentAccess, seo)));
4630
+ return Layer.mergeAll(activity, documentAccess, workingDocuments, seo, seoDelivery, readinessLayer);
4631
+ }
4632
+ function productModulesWithMediaLayer(options) {
4633
+ const products = productModulesLayer({
4634
+ ...options,
4635
+ documentEngine: {
4636
+ ...options.documentEngine,
4637
+ mediaBaseUrl: options.documentEngine.mediaBaseUrl ?? options.mediaPublicBaseUrl
4638
+ }
4639
+ });
4640
+ const media = mediaLibraryLayer({
4641
+ maxUploadBytes: options.bootstrap.media.maxUploadBytes,
4642
+ publicBaseUrl: options.mediaPublicBaseUrl
4643
+ }).pipe(Layer.provide(products));
4644
+ return Layer.merge(products, media);
4645
+ }
4646
+
5970
4647
  //#endregion
5971
4648
  //#region ../server/src/internal/initialization/initialization.ts
5972
4649
  const InitializationUnavailableError = InitializationUnavailable;
@@ -6067,20 +4744,21 @@ const reconcileStored = Effect.fn("Initialization.reconcileStored")(function* (s
6067
4744
  id: state.claim.changeId
6068
4745
  }, state.claim.intent);
6069
4746
  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
- }
4747
+ if (Predicate.isTagged("Recorded")(begin)) {
4748
+ yield* store.complete(state.claim.changeId);
4749
+ return yield* store.read;
6083
4750
  }
4751
+ if (Predicate.isTagged("Cancelled")(begin)) {
4752
+ yield* store.reset(state.claim.changeId);
4753
+ return yield* store.read;
4754
+ }
4755
+ if (Predicate.isTagged("Unresolved")(begin)) {
4756
+ const resolution = yield* reconcileInitialAdministrator(journal, begin.change, recovery.observe).pipe(Effect.mapError((cause) => unavailable$3("Initialization.recover", cause)));
4757
+ if (resolution.kind === "applied") yield* store.complete(state.claim.changeId);
4758
+ if (resolution.kind === "cancelled") yield* store.reset(state.claim.changeId);
4759
+ return yield* store.read;
4760
+ }
4761
+ return yield* store.read;
6084
4762
  });
6085
4763
  function completeBestEffort(store, changeId) {
6086
4764
  return store.complete(changeId).pipe(Effect.catch((error) => Effect.logWarning("Initialization completion remains recoverable.", {
@@ -6281,6 +4959,69 @@ function targetError(operation) {
6281
4959
  return (error) => Schema.is(UserAlreadyExists)(error) ? unavailable$2(operation)(error) : error;
6282
4960
  }
6283
4961
 
4962
+ //#endregion
4963
+ //#region ../server/src/Forms.ts
4964
+ /** Only the currently published definition can authorize a submission. Never trust client rules. */
4965
+ const submitForm = Effect.fn("Forms.submit")(function* (input, components, handlers) {
4966
+ const document = yield* (yield* PublishedDocuments).read({
4967
+ kind: "id",
4968
+ documentId: input.documentId
4969
+ }).pipe(Effect.orElseSucceed(() => void 0));
4970
+ if (!document) return FormResult.make({
4971
+ status: "unavailable",
4972
+ errors: {}
4973
+ });
4974
+ const form = documentForms(document, components).find((candidate) => JSON.stringify(candidate.nodePath) === JSON.stringify(input.nodePath));
4975
+ if (!form || form.revision !== input.revision) return FormResult.make({
4976
+ status: "stale",
4977
+ errors: {}
4978
+ });
4979
+ const handler = Object.hasOwn(handlers, form.name) ? handlers[form.name] : void 0;
4980
+ if (!handler) return FormResult.make({
4981
+ status: "unavailable",
4982
+ errors: {}
4983
+ });
4984
+ const fields = form.steps.flatMap((step) => step.fields);
4985
+ const errors = validateFormValues(fields, input.values);
4986
+ if (Object.keys(input.values).some((id) => !fields.some((field) => field.id === id))) errors._form = "The submission contains unknown answers.";
4987
+ if (Object.keys(errors).length) return FormResult.make({
4988
+ status: "invalid",
4989
+ errors
4990
+ });
4991
+ const values = Object.fromEntries(fields.map((field) => [field.id, normalizedAnswer(Object.hasOwn(input.values, field.id) ? input.values[field.id] : void 0)]));
4992
+ return yield* Effect.tryPromise(() => Promise.resolve(handler({
4993
+ form,
4994
+ values
4995
+ }))).pipe(Effect.as(FormResult.make({
4996
+ status: "accepted",
4997
+ errors: {}
4998
+ })), Effect.orElseSucceed(() => FormResult.make({
4999
+ status: "failed",
5000
+ errors: {}
5001
+ })));
5002
+ });
5003
+ function normalizedAnswer(value) {
5004
+ return value === void 0 || Predicate.isString(value) && !value.trim() ? null : value;
5005
+ }
5006
+
5007
+ //#endregion
5008
+ //#region ../platform-node/src/internal/http/forms.ts
5009
+ function formsApiLayer(components, handlers) {
5010
+ return HttpApiBuilder.group(StudioApi, "forms", (group) => Effect.gen(function* () {
5011
+ const documents = yield* PublishedDocuments;
5012
+ const limiter = yield* RateLimiter.RateLimiter;
5013
+ return group.handle("submit", ({ payload }) => limiter.consume({
5014
+ key: "cms-forms",
5015
+ limit: 60,
5016
+ window: "1 minute",
5017
+ onExceeded: "fail"
5018
+ }).pipe(Effect.andThen(submitForm(payload, components, handlers).pipe(Effect.provideService(PublishedDocuments, documents))), Effect.orElseSucceed(() => FormResult.make({
5019
+ status: "unavailable",
5020
+ errors: {}
5021
+ }))));
5022
+ })).pipe(Layer.provide(RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory))));
5023
+ }
5024
+
6284
5025
  //#endregion
6285
5026
  //#region ../platform-node/src/internal/http/errors.ts
6286
5027
  const studioSchemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(StudioSchemaError, (error) => Effect.fail(mapStudioSchemaError(error.kind)));
@@ -6462,7 +5203,7 @@ function decodeMediaUpload(payload, query, maxUploadBytes) {
6462
5203
  }
6463
5204
  function mapMultipartError(error) {
6464
5205
  if (isMediaInvalid(error)) return error;
6465
- return error.reason._tag === "InternalError" ? studioInternalError() : invalidMultipart();
5206
+ return Predicate.isTagged("InternalError")(error.reason) ? studioInternalError() : invalidMultipart();
6466
5207
  }
6467
5208
  function invalidMultipart() {
6468
5209
  return new MediaInvalid({ message: "Upload exactly one file whose body matches its declared size." });
@@ -6474,7 +5215,16 @@ const publicError = Schema.is(Schema.Union([AccessDenied, PublishedDocumentNotFo
6474
5215
  const denied = Schema.is(AccessDenied);
6475
5216
  const previewApiLayer = HttpApiBuilder.group(StudioApi, "previewDelivery", (handlers) => Effect.gen(function* () {
6476
5217
  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)));
5218
+ const accessControl = yield* AccessControl;
5219
+ const seo = yield* PreviewSeo;
5220
+ const seoDelivery = yield* SeoDelivery;
5221
+ 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({
5222
+ previous: surround.previous ? seoDelivery.previewDocument(surround.previous) : Effect.succeed(null),
5223
+ next: surround.next ? seoDelivery.previewDocument(surround.next) : Effect.succeed(null)
5224
+ })))), preservePublicErrors(publicError))).handle("seo", () => CurrentStudioRequest.pipe(Effect.flatMap((context) => accessControl.require(context, {
5225
+ action: "document.browse",
5226
+ resource: { kind: "catalog" }
5227
+ }).pipe(Effect.andThen(seo.read))), preservePublicErrors(denied)));
6478
5228
  }));
6479
5229
 
6480
5230
  //#endregion
@@ -6482,7 +5232,15 @@ const previewApiLayer = HttpApiBuilder.group(StudioApi, "previewDelivery", (hand
6482
5232
  const isPublishedDocumentNotFound = Schema.is(PublishedDocumentNotFound);
6483
5233
  const deliveryApiLayer = Layer.merge(HttpApiBuilder.group(StudioApi, "delivery", (handlers) => Effect.gen(function* () {
6484
5234
  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)));
5235
+ const seo = yield* PublishedSeo;
5236
+ const seoDelivery = yield* SeoDelivery;
5237
+ 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) => ({
5238
+ items,
5239
+ total: page.total
5240
+ })))), mapUnexpectedError)).handle("navigation", ({ payload }) => documents.navigation(payload).pipe(mapUnexpectedError)).handle("surround", ({ payload }) => documents.surround(payload.lookup, payload.query).pipe(Effect.flatMap((surround) => Effect.all({
5241
+ previous: surround.previous ? seoDelivery.publishedDocument(surround.previous) : Effect.succeed(null),
5242
+ next: surround.next ? seoDelivery.publishedDocument(surround.next) : Effect.succeed(null)
5243
+ })), preservePublicErrors(isPublishedDocumentNotFound))).handle("seo", () => seo.read.pipe(mapUnexpectedError)).handle("sitemap", () => seoDelivery.sitemap.pipe(mapUnexpectedError));
6486
5244
  })), HttpApiBuilder.group(StudioApi, "diagnostics", (handlers) => Effect.gen(function* () {
6487
5245
  const documents = yield* PublishedDocuments;
6488
5246
  return handlers.handle("documents", () => CurrentStudioRequest.pipe(Effect.flatMap((context) => documents.diagnostics(context)), preservePublicErrors(Schema.is(AccessDenied))));
@@ -6498,6 +5256,12 @@ const isHistoryPublicError = Schema.is(Schema.Union([
6498
5256
  DocumentNotFound
6499
5257
  ]));
6500
5258
  const isInitializationPublicError = Schema.is(Schema.Union([InitializationAlreadyCompleted, InitializationProofInvalid]));
5259
+ const isSeoPublicError = Schema.is(Schema.Union([
5260
+ AccessDenied,
5261
+ SeoConfirmationRequired,
5262
+ SeoMediaUnavailable,
5263
+ SeoVersionConflict
5264
+ ]));
6501
5265
  const administrationApiLayer = Layer.mergeAll(HttpApiBuilder.group(StudioApi, "initialization", (handlers) => Effect.gen(function* () {
6502
5266
  const initialization = yield* Initialization;
6503
5267
  return handlers.handle("status", () => initialization.status.pipe(mapUnexpectedError)).handle("claim", ({ payload }) => initialization.claim(payload).pipe(preservePublicErrors(isInitializationPublicError)));
@@ -6507,6 +5271,9 @@ const administrationApiLayer = Layer.mergeAll(HttpApiBuilder.group(StudioApi, "i
6507
5271
  })), HttpApiBuilder.group(StudioApi, "activity", (handlers) => Effect.gen(function* () {
6508
5272
  const activity = yield* Activity;
6509
5273
  return handlers.handle("list", ({ query }) => withStudioRequest((context) => activity.list(context, query)).pipe(preservePublicErrors(isActivityPublicError)));
5274
+ })), HttpApiBuilder.group(StudioApi, "seo", (handlers) => Effect.gen(function* () {
5275
+ const seo = yield* Seo;
5276
+ 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
5277
  })));
6511
5278
 
6512
5279
  //#endregion
@@ -6532,43 +5299,14 @@ const userAdministrationHttpLayer = HttpApiBuilder.group(StudioApi, "users", (ha
6532
5299
 
6533
5300
  //#endregion
6534
5301
  //#region ../platform-node/src/internal/http/api.ts
6535
- function studioApiLayer(bootstrap, authoringRevision) {
5302
+ function studioApiLayer(bootstrap, authoringRevision, components = [], formHandlers = {}) {
6536
5303
  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)));
5304
+ return authenticatedRouterHandlers(Layer.mergeAll(configuration, formsApiLayer(components, formHandlers), documentsApiLayer, mediaApiLayer(bootstrap.media.enabled, bootstrap.media.maxUploadBytes), collaborationApiLayer(authoringRevision), deliveryApiLayer, previewApiLayer, administrationApiLayer, userAdministrationHttpLayer).pipe(Layer.provideMerge(studioSchemaErrorLayer)));
6538
5305
  }
6539
5306
  function authenticatedRouterHandlers(handlers) {
6540
5307
  return handlers;
6541
5308
  }
6542
5309
 
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
5310
  //#endregion
6573
5311
  //#region ../platform-node/src/internal/installation/error.ts
6574
5312
  var CmsInstallationError = class extends Schema.TaggedError()("Cms.InstallationError", {
@@ -6585,6 +5323,7 @@ function makeInstallationLayer(validated) {
6585
5323
  const options = validated.options;
6586
5324
  const core = options.composition.core;
6587
5325
  const productOptions = {
5326
+ baseUrl: options.baseUrl,
6588
5327
  bootstrap: options.bootstrap,
6589
5328
  documentEngine: {
6590
5329
  documentModel: options.documentModel,
@@ -6607,9 +5346,9 @@ function makeInstallationLayer(validated) {
6607
5346
  const roomHost = studioRealtimeRoomHostLayer().pipe(Layer.provide(productModules));
6608
5347
  const gateway = studioRealtimeGatewayLayer.pipe(Layer.provide(Layer.mergeAll(productModules, roomHost, tickets)));
6609
5348
  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));
5349
+ const handlers = studioApiLayer(options.bootstrap, options.authoring.revision, options.authoring.components, options.formHandlers).pipe(Layer.provide(httpDependencies));
5350
+ const httpApi = HttpApiBuilder.layer(StudioApi).pipe(Layer.provide(handlers), Layer.provide(HttpServer.layerServices), Layer.provideMerge(HttpRouter.layer));
5351
+ 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
5352
  }
6614
5353
  function validateInstallationOptions(options) {
6615
5354
  if (options.bootstrap.media.enabled) {
@@ -6647,7 +5386,6 @@ function mapStartupError(layer, operation) {
6647
5386
 
6648
5387
  //#endregion
6649
5388
  //#region ../platform-node/src/Installation.ts
6650
- /** Starts auth, persistence, Studio realtime, delivery, health, and maintenance as one deep module. */
6651
5389
  const installCms$1 = Effect.fn("CmsInstallation.install")(function* (options) {
6652
5390
  return yield* startInstallation({
6653
5391
  cleanup: options.bootstrap.media.enabled ? options.cleanup : void 0,
@@ -6656,6 +5394,8 @@ const installCms$1 = Effect.fn("CmsInstallation.install")(function* (options) {
6656
5394
  });
6657
5395
  function createCmsInstallationLayer(options) {
6658
5396
  return installationLayer({
5397
+ baseUrl: options.baseUrl,
5398
+ formHandlers: options.formHandlers,
6659
5399
  authoring: options.authoring,
6660
5400
  bootstrap: options.bootstrap,
6661
5401
  composition: options.composition,
@@ -6666,262 +5406,6 @@ function createCmsInstallationLayer(options) {
6666
5406
  });
6667
5407
  }
6668
5408
 
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
5409
  //#endregion
6926
5410
  //#region ../sql-pg/src/internal/database/table-helpers.ts
6927
5411
  const transactionTimestamp = sql`transaction_timestamp()`;
@@ -6935,6 +5419,7 @@ const ACTIVITY_TARGET_KINDS = [
6935
5419
  "document",
6936
5420
  "media",
6937
5421
  "media-library",
5422
+ "seo",
6938
5423
  "user"
6939
5424
  ];
6940
5425
  const IDENTIFIED_ACTIVITY_TARGET_KINDS = [
@@ -6961,8 +5446,8 @@ const activityEntries = pgTable("activity_entries", {
6961
5446
  }, (table) => [
6962
5447
  check("activity_entries_action_check", isOneOf(table.action, ACTIVITY_ACTIONS)),
6963
5448
  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)`),
5449
+ check("activity_entries_target_check", sql`(${table.targetKind} in ('media-library', 'seo') AND ${table.targetId} IS NULL)
5450
+ OR (${table.targetKind} not in ('media-library', 'seo') AND ${table.targetId} IS NOT NULL)`),
6966
5451
  check("activity_entries_actor_roles_check", sql`jsonb_typeof(${table.actorRoles}) = 'array'`),
6967
5452
  check("activity_entries_details_check", sql`jsonb_typeof(${table.details}) = 'object'`),
6968
5453
  index("activity_entries_history").on(table.occurredAt.desc(), table.id.desc()),
@@ -7053,27 +5538,30 @@ function activityEntryValues(entry) {
7053
5538
  };
7054
5539
  }
7055
5540
  function activityEntriesEqual(left, right) {
7056
- return isDeepStrictEqual(Schema.encodeSync(ActivityEntry)(left), Schema.encodeSync(ActivityEntry)(right));
5541
+ return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(ActivityEntry)(left)), Result.getOrThrow(Schema.encodeResult(ActivityEntry)(right)));
7057
5542
  }
7058
5543
  function decodeActivityTarget(kind, id) {
7059
- const value = kind === "document" ? {
5544
+ if (kind === "document") return Schema.decodeUnknownEffect(ActivityTarget)({
7060
5545
  documentId: id,
7061
5546
  kind
7062
- } : kind === "media" ? {
5547
+ });
5548
+ if (kind === "media") return Schema.decodeUnknownEffect(ActivityTarget)({
7063
5549
  kind,
7064
5550
  mediaId: id
7065
- } : kind === "user" ? {
5551
+ });
5552
+ if (kind === "user") return Schema.decodeUnknownEffect(ActivityTarget)({
7066
5553
  kind,
7067
5554
  userId: id
7068
- } : { kind };
7069
- return Schema.decodeUnknownEffect(ActivityTarget)(value);
5555
+ });
5556
+ return Schema.decodeEffect(ActivityTarget)({ kind });
7070
5557
  }
7071
5558
  function activityTargetId(entry) {
7072
5559
  switch (entry.target.kind) {
7073
5560
  case "document": return entry.target.documentId;
7074
5561
  case "media": return entry.target.mediaId;
7075
5562
  case "user": return entry.target.userId;
7076
- case "media-library": return;
5563
+ case "media-library":
5564
+ case "seo": return;
7077
5565
  }
7078
5566
  }
7079
5567
  function activityPage(entries, limit, total) {
@@ -7372,7 +5860,8 @@ function targetColumns(target) {
7372
5860
  targetId: target.userId,
7373
5861
  targetKind: target.kind
7374
5862
  };
7375
- case "media-library": return { targetKind: target.kind };
5863
+ case "media-library":
5864
+ case "seo": return { targetKind: target.kind };
7376
5865
  }
7377
5866
  }
7378
5867
  function decodeOptionalTarget(kind, id) {
@@ -7380,7 +5869,7 @@ function decodeOptionalTarget(kind, id) {
7380
5869
  return decodeActivityTarget(kind, id).pipe(Effect.mapError(() => new ChangeMismatch({})));
7381
5870
  }
7382
5871
  function draftEqual(change, draft) {
7383
- return isDeepStrictEqual(Schema.encodeSync(ChangeDraft)(draftFromChange(change)), Schema.encodeSync(ChangeDraft)(draft));
5872
+ return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(ChangeDraft)(draftFromChange(change))), Result.getOrThrow(Schema.encodeResult(ChangeDraft)(draft)));
7384
5873
  }
7385
5874
  function draftFromChange(change) {
7386
5875
  const value = {
@@ -7397,7 +5886,7 @@ function draftFromChange(change) {
7397
5886
  });
7398
5887
  }
7399
5888
  function changeEqual(left, right) {
7400
- return isDeepStrictEqual(Schema.encodeSync(PendingChange)(left), Schema.encodeSync(PendingChange)(right));
5889
+ return isDeepStrictEqual(Result.getOrThrow(Schema.encodeResult(PendingChange)(left)), Result.getOrThrow(Schema.encodeResult(PendingChange)(right)));
7401
5890
  }
7402
5891
  function boundedLimit(limit) {
7403
5892
  return Math.min(100, Math.max(0, Math.trunc(limit)));
@@ -7505,6 +5994,26 @@ const embeddedMigrations = [
7505
5994
  folderMillis: 1788866265e3,
7506
5995
  hash: "24c56a37796caf8d76c0a97128a382b31e71808aa5b0ca7e5bac96795f9e16c7",
7507
5996
  name: "20260908111745_classy_johnny_storm"
5997
+ },
5998
+ {
5999
+ sql: [
6000
+ "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",
6001
+ "\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",
6002
+ "\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",
6003
+ "\nCREATE INDEX \"seo_media_references_media\" ON \"seo_media_references\" (\"media_id\");",
6004
+ "\nCREATE INDEX \"seo_revisions_history\" ON \"seo_revisions\" (\"created_at\" DESC NULLS LAST,\"id\" DESC NULLS LAST);",
6005
+ "\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;",
6006
+ "\nALTER TABLE \"seo_state\" ADD CONSTRAINT \"seo_state_working_revision_fkey\" FOREIGN KEY (\"working_revision_id\") REFERENCES \"seo_revisions\"(\"id\") ON DELETE RESTRICT;",
6007
+ "\nALTER TABLE \"seo_state\" ADD CONSTRAINT \"seo_state_published_revision_fkey\" FOREIGN KEY (\"published_revision_id\") REFERENCES \"seo_revisions\"(\"id\") ON DELETE RESTRICT;",
6008
+ "\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'));",
6009
+ "\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'));",
6010
+ "\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));",
6011
+ "\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'));"
6012
+ ],
6013
+ bps: true,
6014
+ folderMillis: 1789233351e3,
6015
+ hash: "8e6a6abf5385ab3ecf8eb083c1923ac9c74d72aa71bd1d444b7402d3e7e77000",
6016
+ name: "20260912171551_charming_valkyrie"
7508
6017
  }
7509
6018
  ];
7510
6019
 
@@ -7603,12 +6112,16 @@ const initializationState = pgTable("initialization_state", {
7603
6112
 
7604
6113
  //#endregion
7605
6114
  //#region ../sql-pg/src/internal/database/migrations.ts
7606
- /** Applies generated CMS migrations and initializes required application state. */
6115
+ const MIGRATION_LOCK_KEY = "cms:database-migrations:v1";
7607
6116
  const migrateCmsDatabase = Effect.fn("CmsDatabase.migrate")(function* () {
6117
+ const postgres = yield* PgClient.PgClient;
7608
6118
  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 });
6119
+ yield* postgres.withTransaction(Effect.gen(function* () {
6120
+ yield* postgres.unsafe("select pg_advisory_xact_lock(hashtextextended($1, 0))", [MIGRATION_LOCK_KEY]);
6121
+ yield* migrate(embeddedMigrations, database._.session, "__drizzle_migrations");
6122
+ yield* database.insert(cmsState).values({}).onConflictDoNothing({ target: cmsState.singleton });
6123
+ yield* database.insert(initializationState).values({}).onConflictDoNothing({ target: initializationState.singleton });
6124
+ }));
7612
6125
  });
7613
6126
  const cmsDatabaseMigrationLayer = Layer.effectDiscard(migrateCmsDatabase());
7614
6127
 
@@ -7712,7 +6225,7 @@ function mapActivePathConflict(sourcePath) {
7712
6225
  const causeError = Cause$1.findErrorOption(error.cause);
7713
6226
  if (Option$1.isNone(causeError) || !SqlError.isSqlError(causeError.value)) return error;
7714
6227
  const reason = causeError.value.reason;
7715
- return reason._tag === "UniqueViolation" && reason.constraint === "documents_active_path" ? new DocumentPathConflict({ sourcePath }) : error;
6228
+ return Predicate.isTagged("UniqueViolation")(reason) && reason.constraint === "documents_active_path" ? new DocumentPathConflict({ sourcePath }) : error;
7716
6229
  }));
7717
6230
  }
7718
6231
  function updateAuthoredHead(transaction, documentId, revisionId) {
@@ -8026,7 +6539,7 @@ const isRepositoryDomainError = Schema.is(RepositoryDomainError);
8026
6539
  const isDocumentNotFound = Schema.is(DocumentNotFound);
8027
6540
  const isDocumentMediaUnavailable = Schema.is(DocumentMediaUnavailable);
8028
6541
  const isDocumentPersistenceError = Schema.is(DocumentPersistenceError);
8029
- const systemActivity = ActivityContext.make({
6542
+ const systemActivity$1 = ActivityContext.make({
8030
6543
  actor: {
8031
6544
  email: "system@cms.invalid",
8032
6545
  id: "system",
@@ -8049,15 +6562,15 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
8049
6562
  const state = yield* Schema.decodeUnknownEffect(CmsStateSelect)(stateRow);
8050
6563
  const plans = [input.plan, ...input.additionalPlans ?? []];
8051
6564
  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."));
6565
+ if (references.length !== input.mediaReferences.length) return yield* persistenceFailure$3("DocumentStore.commit", /* @__PURE__ */ new Error("Unexpected media reference owner."));
8053
6566
  yield* lockMediaReferenceKeys(transaction, references.map((reference) => reference.objectKey));
8054
- yield* rejectDeletingMedia(transaction, references);
8055
- const activity = input.activity ?? systemActivity;
6567
+ yield* rejectUnavailableMedia(transaction, references);
6568
+ const activity = input.activity ?? systemActivity$1;
8056
6569
  for (const plan of plans) {
8057
6570
  yield* applyDocumentCommitPlan(transaction, plan, activity);
8058
6571
  yield* replaceDocumentMediaReferences(transaction, plan.documentId, references.filter((reference) => reference.documentId === plan.documentId));
8059
6572
  }
8060
- const snapshot = yield* readSnapshot(transaction, state.generation);
6573
+ const snapshot = yield* readSnapshot$1(transaction, state.generation);
8061
6574
  for (const plan of plans) {
8062
6575
  const affected = yield* sourceForMutation(snapshot, plan.documentId, plan.action === "document.remove");
8063
6576
  yield* recordDocumentActivity(transaction, affected, plan, activity);
@@ -8079,15 +6592,15 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
8079
6592
  const load = Effect.fn("DocumentStore.Drizzle.load")((after) => database.transaction((transaction) => Effect.gen(function* () {
8080
6593
  const generation = yield* readGeneration(transaction);
8081
6594
  if (after === generation.toString()) return void 0;
8082
- return yield* readSnapshot(transaction, generation);
6595
+ return yield* readSnapshot$1(transaction, generation);
8083
6596
  }), {
8084
6597
  accessMode: "read only",
8085
6598
  isolationLevel: "repeatable read"
8086
- }).pipe(mapPersistenceFailure$2("DocumentStore.Drizzle.load")));
6599
+ }).pipe(mapPersistenceFailure$3("DocumentStore.Drizzle.load")));
8087
6600
  const synchronizeMediaReferences = Effect.fn("DocumentStore.Drizzle.synchronizeMediaReferences")((input) => database.transaction((transaction) => Effect.gen(function* () {
8088
6601
  const stateRows = yield* transaction.select().from(cmsState).where(eq(cmsState.singleton, true)).limit(2).for("update");
8089
6602
  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}.`));
6603
+ 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
6604
  const state = yield* Schema.decodeUnknownEffect(CmsStateSelect)(stateRow);
8092
6605
  if (state.generation.toString() !== input.expectedGeneration) return yield* new DocumentGenerationConflict({
8093
6606
  actualGeneration: DocumentGeneration.make(String(state.generation)),
@@ -8095,7 +6608,7 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
8095
6608
  });
8096
6609
  const references = uniqueMediaReferences(input.references);
8097
6610
  yield* lockMediaReferenceKeys(transaction, references.map((reference) => reference.objectKey));
8098
- yield* rejectDeletingMedia(transaction, references);
6611
+ yield* rejectUnavailableMedia(transaction, references);
8099
6612
  yield* transaction.delete(documentMediaReferences);
8100
6613
  if (references.length > 0) yield* transaction.insert(documentMediaReferences).values(references.map(referenceValues));
8101
6614
  })).pipe(mapSynchronizationFailure("DocumentStore.Drizzle.synchronizeMediaReferences")));
@@ -8111,12 +6624,12 @@ const documentStoreLayer = Layer.effect(DocumentStore, Effect.gen(function* () {
8111
6624
  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
6625
  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
6626
  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."));
6627
+ if (total === void 0) return yield* persistenceFailure$3("DocumentStore.Drizzle.revisions", /* @__PURE__ */ new Error("Documents persistence did not return a history total."));
8115
6628
  return revisionPage(yield* Effect.forEach(rows, decodeDocumentRevision), query.limit, total);
8116
6629
  }), {
8117
6630
  accessMode: "read only",
8118
6631
  isolationLevel: "repeatable read"
8119
- }).pipe(mapPersistenceFailure$2("DocumentStore.Drizzle.revisions")));
6632
+ }).pipe(mapPersistenceFailure$3("DocumentStore.Drizzle.revisions")));
8120
6633
  return DocumentStore.of({
8121
6634
  commit,
8122
6635
  load,
@@ -8158,7 +6671,7 @@ const recordDocumentActivity = Effect.fn("DocumentStore.Drizzle.recordActivity")
8158
6671
  if (plan.action !== "document.publish" && plan.action !== "document.unpublish") values.id = source.revisionId;
8159
6672
  yield* database.insert(activityEntries).values(values);
8160
6673
  });
8161
- const readSnapshot = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (database, generation) {
6674
+ const readSnapshot$1 = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (database, generation) {
8162
6675
  const rows = yield* database.select({
8163
6676
  authored: authoredRevision,
8164
6677
  created: createdRevision,
@@ -8171,12 +6684,12 @@ const readSnapshot = Effect.fn("DocumentStore.Drizzle.readSnapshot")(function* (
8171
6684
  const readGeneration = Effect.fn("DocumentStore.Drizzle.readGeneration")(function* (database) {
8172
6685
  const rows = yield* database.select().from(cmsState);
8173
6686
  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}.`));
6687
+ 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
6688
  return (yield* Schema.decodeUnknownEffect(CmsStateSelect)(row)).generation;
8176
6689
  });
8177
6690
  const sourceForMutation = Effect.fn("DocumentStore.Drizzle.sourceForMutation")(function* (snapshot, documentId, deleted) {
8178
6691
  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.`));
6692
+ if (!source) return yield* persistenceFailure$3("DocumentStore.Drizzle.sourceForMutation", /* @__PURE__ */ new Error(`Committed document "${documentId}" is missing from its resulting snapshot.`));
8180
6693
  return source;
8181
6694
  });
8182
6695
  function mapCommitFailure(operation) {
@@ -8188,14 +6701,27 @@ function mapSynchronizationFailure(operation) {
8188
6701
  return documentPersistenceError(operation, cause);
8189
6702
  }));
8190
6703
  }
8191
- const rejectDeletingMedia = Effect.fn("DocumentStore.Drizzle.rejectDeletingMedia")(function* (transaction, references) {
6704
+ const rejectUnavailableMedia = Effect.fn("DocumentStore.Drizzle.rejectUnavailableMedia")(function* (transaction, references) {
8192
6705
  const keys = [...new Set(references.map((reference) => reference.objectKey))];
6706
+ const mediaIds = [...new Set(references.flatMap((reference) => reference.mediaId ? [reference.mediaId] : []))];
8193
6707
  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));
6708
+ if (mediaIds.length > 0) {
6709
+ const identityRows = yield* transaction.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).where(inArray(mediaAssets.id, mediaIds));
6710
+ yield* lockMediaReferenceKeys(transaction, yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(identityRows.map((row) => row.objectKey)));
6711
+ }
6712
+ const rows = yield* transaction.select({
6713
+ id: mediaAssets.id,
6714
+ objectKey: mediaAssets.objectKey,
6715
+ status: mediaAssets.status
6716
+ }).from(mediaAssets).where(mediaIds.length > 0 ? or(inArray(mediaAssets.objectKey, keys), inArray(mediaAssets.id, mediaIds)) : inArray(mediaAssets.objectKey, keys));
6717
+ const deletingKeys = new Set(yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(rows.filter((row) => row.status === "deleting").map((row) => row.objectKey)));
6718
+ const unavailable = references.filter((reference) => {
6719
+ if (!reference.mediaId) return deletingKeys.has(reference.objectKey);
6720
+ const asset = rows.find((row) => row.id === reference.mediaId);
6721
+ return !asset || asset.status !== "ready" || asset.objectKey !== reference.objectKey;
6722
+ });
8196
6723
  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));
6724
+ const affected = unavailable.sort((left, right) => left.documentId === right.documentId ? left.objectKey.localeCompare(right.objectKey) : left.documentId.localeCompare(right.documentId));
8199
6725
  const first = affected[0];
8200
6726
  if (!first) return;
8201
6727
  return yield* new DocumentMediaUnavailable({
@@ -8219,7 +6745,7 @@ function referenceValues(reference) {
8219
6745
  sourcePath: reference.sourcePath
8220
6746
  };
8221
6747
  }
8222
- function mapPersistenceFailure$2(operation) {
6748
+ function mapPersistenceFailure$3(operation) {
8223
6749
  return (effect) => effect.pipe(Effect.mapError((cause) => isDocumentPersistenceError(cause) ? cause : documentPersistenceError(operation, cause)));
8224
6750
  }
8225
6751
  function mapRevisionFailure(operation) {
@@ -8228,7 +6754,7 @@ function mapRevisionFailure(operation) {
8228
6754
  return documentPersistenceError(operation, cause);
8229
6755
  }));
8230
6756
  }
8231
- function persistenceFailure$2(operation, cause) {
6757
+ function persistenceFailure$3(operation, cause) {
8232
6758
  return Effect.fail(documentPersistenceError(operation, cause));
8233
6759
  }
8234
6760
 
@@ -8291,7 +6817,7 @@ const initializationStoreLayer = Layer.effect(InitializationStore, Effect.gen(fu
8291
6817
  const locks = yield* PostgresAdvisoryLock;
8292
6818
  const claim = Effect.fn("InitializationStore.Postgres.claim")((value) => database.update(initializationState).set({
8293
6819
  changeId: value.changeId,
8294
- claim: Schema.encodeSync(InitializationClaim)(value),
6820
+ claim: Result.getOrThrow(Schema.encodeResult(InitializationClaim)(value)),
8295
6821
  state: "claiming"
8296
6822
  }).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
6823
  const complete = Effect.fn("InitializationStore.Postgres.complete")((changeId) => transition(database, changeId, "ready"));
@@ -8343,6 +6869,53 @@ function mapUnavailable(operation) {
8343
6869
  })));
8344
6870
  }
8345
6871
 
6872
+ //#endregion
6873
+ //#region ../sql-pg/src/internal/seo/tables.ts
6874
+ const seoRevisions = pgTable("seo_revisions", {
6875
+ id: uuid().defaultRandom().primaryKey(),
6876
+ settings: jsonb().$type().notNull(),
6877
+ actorId: text("actor_id").notNull(),
6878
+ actorEmail: text("actor_email").notNull(),
6879
+ actorName: text("actor_name").notNull(),
6880
+ actorRoles: jsonb("actor_roles").$type().notNull(),
6881
+ requestId: uuid("request_id").notNull(),
6882
+ reason: text(),
6883
+ createdAt: timestamp("created_at", {
6884
+ mode: "string",
6885
+ withTimezone: true
6886
+ }).default(transactionTimestamp).notNull()
6887
+ }, (table) => [
6888
+ check("seo_revisions_actor_roles_check", sql`jsonb_typeof(${table.actorRoles}) = 'array'`),
6889
+ check("seo_revisions_settings_check", sql`jsonb_typeof(${table.settings}) = 'object'`),
6890
+ index("seo_revisions_history").on(table.createdAt.desc(), table.id.desc())
6891
+ ]);
6892
+ const seoState = pgTable("seo_state", {
6893
+ singleton: boolean().default(true).primaryKey(),
6894
+ workingRevisionId: uuid("working_revision_id").notNull(),
6895
+ publishedRevisionId: uuid("published_revision_id").notNull()
6896
+ }, (table) => [
6897
+ check("seo_state_singleton_check", sql`${table.singleton}`),
6898
+ foreignKey({
6899
+ columns: [table.workingRevisionId],
6900
+ foreignColumns: [seoRevisions.id],
6901
+ name: "seo_state_working_revision_fkey"
6902
+ }).onDelete("restrict"),
6903
+ foreignKey({
6904
+ columns: [table.publishedRevisionId],
6905
+ foreignColumns: [seoRevisions.id],
6906
+ name: "seo_state_published_revision_fkey"
6907
+ }).onDelete("restrict")
6908
+ ]);
6909
+ const seoMediaReferences = pgTable("seo_media_references", {
6910
+ id: uuid().defaultRandom().primaryKey(),
6911
+ scope: text({ enum: ["working", "published"] }).notNull(),
6912
+ mediaId: uuid("media_id").notNull().references(() => mediaAssets.id, { onDelete: "restrict" })
6913
+ }, (table) => [
6914
+ check("seo_media_references_scope_check", sql`${table.scope} in ('working', 'published')`),
6915
+ unique("seo_media_references_scope_media_unique").on(table.scope, table.mediaId),
6916
+ index("seo_media_references_media").on(table.mediaId)
6917
+ ]);
6918
+
8346
6919
  //#endregion
8347
6920
  //#region ../sql-pg/src/internal/media/row-schema.ts
8348
6921
  const mediaRefinements = {
@@ -8410,14 +6983,14 @@ const isMediaReferenced = Schema.is(MediaReferenced);
8410
6983
  const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8411
6984
  const database = yield* PgDrizzle.makeWithDefaults();
8412
6985
  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}".`));
6986
+ 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
6987
  yield* beginChange(transaction, mediaUploadChange(record, operation)).pipe(Effect.flatMap(requireUnresolvedChange));
8415
6988
  const rows = yield* transaction.insert(mediaAssets).values(recordValues(record)).onConflictDoNothing().returning();
8416
6989
  if (rows[0]) return yield* decodeMediaRecord(rows[0]);
8417
6990
  const existing = yield* rowById(transaction, record.id);
8418
6991
  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")));
6992
+ return yield* persistenceFailure$2("MediaStore.Drizzle.reserve", /* @__PURE__ */ new Error(`Media reservation "${record.id}" conflicts with durable media state.`));
6993
+ })).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.reserve")));
8421
6994
  const activate = Effect.fn("MediaStore.Drizzle.activate")((id) => database.transaction((transaction) => Effect.gen(function* () {
8422
6995
  const existing = yield* lockedRowById(transaction, id);
8423
6996
  if (existing?.status === "ready") return existing;
@@ -8435,11 +7008,11 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8435
7008
  kind: "applied"
8436
7009
  });
8437
7010
  return activated;
8438
- })).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.activate")));
7011
+ })).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.activate")));
8439
7012
  const find = Effect.fn("MediaStore.Drizzle.find")((id) => Effect.gen(function* () {
8440
7013
  const rows = yield* database.select().from(mediaAssets).where(and(eq(mediaAssets.id, id), eq(mediaAssets.status, "ready"))).limit(1);
8441
7014
  return rows[0] ? Option.some(yield* decodeMediaRecord(rows[0])) : Option.none();
8442
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.find")));
7015
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.find")));
8443
7016
  const beginDelete = Effect.fn("MediaStore.Drizzle.beginDelete")((id, operation) => database.transaction((transaction) => Effect.gen(function* () {
8444
7017
  const existing = yield* lockedRowById(transaction, id);
8445
7018
  if (!existing) return yield* new MediaNotFound({ mediaId: id });
@@ -8452,7 +7025,13 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8452
7025
  yield* lockMediaReferenceKeys(transaction, [existing.objectKey]);
8453
7026
  const referenceRows = yield* transaction.select().from(documentMediaReferences).where(eq(documentMediaReferences.objectKey, existing.objectKey)).orderBy(asc(documentMediaReferences.documentId), asc(documentMediaReferences.sourcePath), asc(documentMediaReferences.nodePath));
8454
7027
  const references = yield* decodeMediaReferences(referenceRows);
8455
- if (references.length > 0) return yield* new MediaReferenced({
7028
+ const seoReferenceRows = yield* transaction.select({ scope: seoMediaReferences.scope }).from(seoMediaReferences).where(eq(seoMediaReferences.mediaId, id)).orderBy(asc(seoMediaReferences.scope));
7029
+ 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)));
7030
+ if (references.length > 0 || seoScopes.length > 0) return yield* new MediaReferenced(seoScopes.length > 0 ? {
7031
+ mediaId: id,
7032
+ references,
7033
+ seoScopes
7034
+ } : {
8456
7035
  mediaId: id,
8457
7036
  references
8458
7037
  });
@@ -8473,7 +7052,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8473
7052
  const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
8474
7053
  if (existing && isSameReplacement(existing, replacement)) return existing;
8475
7054
  return yield* new MediaConflict({ mediaId: replacement.mediaId });
8476
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.beginReplace")));
7055
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.beginReplace")));
8477
7056
  const markReplaceStaged = Effect.fn("MediaStore.Drizzle.markReplaceStaged")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
8478
7057
  yield* lockedRowById(transaction, replacement.mediaId);
8479
7058
  const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
@@ -8481,14 +7060,14 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8481
7060
  if (existing.staged) return existing;
8482
7061
  const rows = yield* transaction.update(mediaReplacements).set({ staged: true }).where(and(eq(mediaReplacements.mediaId, replacement.mediaId), eq(mediaReplacements.stagingObjectKey, replacement.stagingObjectKey))).returning();
8483
7062
  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")));
7063
+ return yield* persistenceFailure$2("MediaStore.Drizzle.markReplaceStaged", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" lost its durable intent.`));
7064
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.markReplaceStaged")));
8486
7065
  const finishReplace = Effect.fn("MediaStore.Drizzle.finishReplace")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
8487
7066
  const asset = yield* lockedRowById(transaction, replacement.mediaId);
8488
7067
  const pendingReplacement = yield* replacementByMediaId(transaction, replacement.mediaId);
8489
7068
  if (!pendingReplacement) {
8490
7069
  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.`));
7070
+ return yield* persistenceFailure$2("MediaStore.Drizzle.finishReplace", /* @__PURE__ */ new Error(`Media replacement "${replacement.mediaId}" cannot be replayed from durable state.`));
8492
7071
  }
8493
7072
  if (!isSameReplacement(pendingReplacement, replacement)) return yield* new MediaConflict({ mediaId: replacement.mediaId });
8494
7073
  if (!asset || asset.status !== "ready" || asset.objectKey !== replacement.objectKey || asset.mimeType !== replacement.mimeType) return yield* new MediaConflict({ mediaId: replacement.mediaId });
@@ -8501,7 +7080,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8501
7080
  updatedAt: transactionTimestamp
8502
7081
  }).where(and(eq(mediaAssets.id, replacement.mediaId), eq(mediaAssets.status, "ready"))).returning())[0];
8503
7082
  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.`));
7083
+ 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
7084
  const decoded = yield* decodeMediaRecord(replaced);
8506
7085
  const intent = yield* decodeMediaChangeIntent(change);
8507
7086
  yield* resolveChange(transaction, change, {
@@ -8509,7 +7088,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8509
7088
  kind: "applied"
8510
7089
  });
8511
7090
  return decoded;
8512
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.finishReplace")));
7091
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.finishReplace")));
8513
7092
  const cancelReplace = Effect.fn("MediaStore.Drizzle.cancelReplace")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
8514
7093
  yield* lockedRowById(transaction, replacement.mediaId);
8515
7094
  const existing = yield* replacementByMediaId(transaction, replacement.mediaId);
@@ -8518,7 +7097,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8518
7097
  const change = yield* requirePendingChange(transaction, mediaSubject(replacement.mediaId), "media.replaced");
8519
7098
  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
7099
  yield* resolveChange(transaction, change, { kind: "cancelled" });
8521
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.cancelReplace")));
7100
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.cancelReplace")));
8522
7101
  const finishDelete = Effect.fn("MediaStore.Drizzle.finishDelete")((id) => database.transaction((transaction) => Effect.gen(function* () {
8523
7102
  const existing = yield* lockedRowById(transaction, id);
8524
7103
  if (!existing) return;
@@ -8530,7 +7109,7 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8530
7109
  details: mediaChangeDetails(intent),
8531
7110
  kind: "applied"
8532
7111
  });
8533
- })).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.finishDelete")));
7112
+ })).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.finishDelete")));
8534
7113
  const abandonUpload = Effect.fn("MediaStore.Drizzle.abandonUpload")((id) => database.transaction((transaction) => Effect.gen(function* () {
8535
7114
  const existing = yield* lockedRowById(transaction, id);
8536
7115
  if (!existing) return;
@@ -8538,15 +7117,15 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8538
7117
  const change = yield* requirePendingChange(transaction, mediaSubject(id), "media.uploaded");
8539
7118
  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
7119
  yield* resolveChange(transaction, change, { kind: "cancelled" });
8541
- })).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.abandonUpload")));
7120
+ })).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.abandonUpload")));
8542
7121
  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);
7122
+ 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
7123
  return yield* Effect.forEach(rows, decodeMediaRecord);
8545
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.pending")));
7124
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.pending")));
8546
7125
  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);
7126
+ 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
7127
  return yield* Effect.forEach(rows, decodeMediaReplacement);
8549
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.pendingReplacements")));
7128
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.pendingReplacements")));
8550
7129
  const inspectReplacement = Effect.fn("MediaStore.Drizzle.inspectReplacement")((replacement) => database.transaction((transaction) => Effect.gen(function* () {
8551
7130
  const asset = yield* lockedRowById(transaction, replacement.mediaId);
8552
7131
  const pending = yield* replacementByMediaId(transaction, replacement.mediaId);
@@ -8555,17 +7134,17 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8555
7134
  return yield* new MediaConflict({ mediaId: replacement.mediaId });
8556
7135
  }
8557
7136
  return asset && isCompletedReplacement(asset, replacement) ? "completed" : "absent";
8558
- })).pipe(mapStoreFailure$1("MediaStore.Drizzle.inspectReplacement")));
7137
+ })).pipe(mapStoreFailure$2("MediaStore.Drizzle.inspectReplacement")));
8559
7138
  const inventory = Effect.gen(function* () {
8560
7139
  const rows = yield* database.select().from(mediaAssets).where(eq(mediaAssets.status, "ready")).orderBy(asc(mediaAssets.objectKey));
8561
7140
  return yield* Effect.forEach(rows, decodeMediaRecord);
8562
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.inventory"), Effect.withSpan("MediaStore.Drizzle.inventory"));
7141
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.inventory"), Effect.withSpan("MediaStore.Drizzle.inventory"));
8563
7142
  const knownObjectKeys = Effect.gen(function* () {
8564
7143
  const assetRows = yield* database.select({ objectKey: mediaAssets.objectKey }).from(mediaAssets).orderBy(asc(mediaAssets.objectKey));
8565
7144
  const replacementRows = yield* database.select({ objectKey: mediaReplacements.stagingObjectKey }).from(mediaReplacements).orderBy(asc(mediaReplacements.stagingObjectKey));
8566
7145
  const objectKeys = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))([...assetRows, ...replacementRows].map((row) => row.objectKey));
8567
7146
  return new Set(objectKeys);
8568
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.knownObjectKeys"), Effect.withSpan("MediaStore.Drizzle.knownObjectKeys"));
7147
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.knownObjectKeys"), Effect.withSpan("MediaStore.Drizzle.knownObjectKeys"));
8569
7148
  const search = Effect.fn("MediaStore.Drizzle.search")((query) => database.transaction((transaction) => Effect.gen(function* () {
8570
7149
  const normalizedSearch = query.search?.trim();
8571
7150
  const pattern = normalizedSearch ? `%${escapeLike(normalizedSearch)}%` : void 0;
@@ -8574,14 +7153,14 @@ const mediaStoreLayer = Layer.effect(MediaStore, Effect.gen(function* () {
8574
7153
  const rows = yield* transaction.select().from(mediaAssets).where(and(filters, cursorFilter)).orderBy(desc(mediaAssets.createdAt), desc(mediaAssets.id)).limit(query.limit + 1);
8575
7154
  const folderRows = yield* transaction.selectDistinct({ folder: mediaAssets.folder }).from(mediaAssets).where(and(eq(mediaAssets.status, "ready"), ne(mediaAssets.folder, ""))).orderBy(asc(mediaAssets.folder));
8576
7155
  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."));
7156
+ if (total === void 0) return yield* persistenceFailure$2("MediaStore.Drizzle.search", /* @__PURE__ */ new Error("Media persistence did not return a search total."));
8578
7157
  const decoded = yield* Effect.forEach(rows, decodeMediaRecord);
8579
7158
  const folders = yield* Schema.decodeEffect(Schema.Array(MediaFolder))(folderRows.map((row) => row.folder)).pipe(Effect.mapError((cause) => mediaPersistenceError("MediaStore.Drizzle.search", cause)));
8580
7159
  return mediaPage(decoded, query.limit, folders, total);
8581
7160
  }), {
8582
7161
  accessMode: "read only",
8583
7162
  isolationLevel: "repeatable read"
8584
- }).pipe(mapPersistenceFailure$1("MediaStore.Drizzle.search")));
7163
+ }).pipe(mapPersistenceFailure$2("MediaStore.Drizzle.search")));
8585
7164
  return MediaStore.of({
8586
7165
  abandonUpload,
8587
7166
  activate,
@@ -8666,45 +7245,230 @@ const replacementByMediaId = Effect.fn("MediaStore.Drizzle.replacementByMediaId"
8666
7245
  return rows[0] ? yield* decodeMediaReplacement(rows[0]) : void 0;
8667
7246
  });
8668
7247
  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}".`));
7248
+ return persistenceFailure$2(operation, /* @__PURE__ */ new Error(actualStatus === void 0 ? `Media record "${id}" does not exist.` : `Media record "${id}" cannot transition from "${actualStatus}".`));
8670
7249
  }
8671
- function mapPersistenceFailure$1(operation) {
7250
+ function mapPersistenceFailure$2(operation) {
8672
7251
  return (effect) => effect.pipe(Effect.mapError((cause) => isMediaPersistenceError(cause) ? cause : mediaPersistenceError(operation, cause)));
8673
7252
  }
8674
- function mapStoreFailure$1(operation) {
7253
+ function mapStoreFailure$2(operation) {
8675
7254
  return (effect) => effect.pipe(Effect.mapError((cause) => isMediaConflict(cause) || isMediaPersistenceError(cause) ? cause : mediaPersistenceError(operation, cause)));
8676
7255
  }
8677
7256
  function mapBeginDeleteFailure(operation) {
8678
7257
  return (effect) => effect.pipe(Effect.mapError((cause) => isMediaConflict(cause) || isMediaNotFound(cause) || isMediaPersistenceError(cause) || isMediaReferenced(cause) ? cause : mediaPersistenceError(operation, cause)));
8679
7258
  }
8680
- function persistenceFailure$1(operation, cause) {
7259
+ function persistenceFailure$2(operation, cause) {
8681
7260
  return Effect.fail(mediaPersistenceError(operation, cause));
8682
7261
  }
8683
7262
 
8684
7263
  //#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"
7264
+ //#region ../sql-pg/src/internal/seo/seo-store.ts
7265
+ const SeoStoreDomainError = Schema.Union([
7266
+ SeoMediaUnavailable,
7267
+ SeoPersistenceError,
7268
+ SeoVersionConflict
7269
+ ]);
7270
+ const isSeoStoreDomainError = Schema.is(SeoStoreDomainError);
7271
+ const isSeoPersistenceError = Schema.is(SeoPersistenceError);
7272
+ const systemActivity = {
7273
+ actor: {
7274
+ email: "system@cms.invalid",
7275
+ id: "system",
7276
+ name: "CMS System",
7277
+ roles: ["system"]
7278
+ },
7279
+ requestId: RequestId.make("00000000-0000-4000-8000-000000000000")
7280
+ };
7281
+ const seoStoreLayer = Layer.effect(SeoStore, Effect.gen(function* () {
7282
+ const database = yield* PgDrizzle.makeWithDefaults();
7283
+ const initialize = Effect.fn("SeoStore.Drizzle.initialize")((defaults) => database.transaction((transaction) => Effect.gen(function* () {
7284
+ yield* transaction.execute(sql`SELECT pg_advisory_xact_lock(hashtext('cms:seo:singleton:v1'))`);
7285
+ const current = yield* readSnapshot(transaction);
7286
+ if (current) return current;
7287
+ yield* ensureMediaAvailable(transaction, defaults).pipe(Effect.mapError((cause) => new SeoPersistenceError({
7288
+ cause,
7289
+ operation: "SeoStore.Drizzle.initializeDefaults"
7290
+ })));
7291
+ const revision = yield* insertRevision(transaction, defaults, systemActivity);
7292
+ yield* transaction.insert(seoState).values({
7293
+ publishedRevisionId: revision.revisionId,
7294
+ workingRevisionId: revision.revisionId
8703
7295
  });
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 });
7296
+ yield* replaceMediaReferences(transaction, "working", defaults);
7297
+ yield* replaceMediaReferences(transaction, "published", defaults);
7298
+ return {
7299
+ published: revision,
7300
+ working: revision
7301
+ };
7302
+ })).pipe(mapStoreFailure$1("SeoStore.Drizzle.initialize")));
7303
+ const load = database.transaction((transaction) => readSnapshot(transaction), {
7304
+ accessMode: "read only",
7305
+ isolationLevel: "repeatable read"
7306
+ }).pipe(mapPersistenceFailure$1("SeoStore.Drizzle.load"));
7307
+ const save = Effect.fn("SeoStore.Drizzle.save")((input) => database.transaction((transaction) => Effect.gen(function* () {
7308
+ const current = yield* lockedSnapshot(transaction);
7309
+ yield* requireVersion(current, input.expectedVersion);
7310
+ yield* ensureMediaAvailable(transaction, input.settings);
7311
+ const revision = yield* insertRevision(transaction, input.settings, input.activity);
7312
+ yield* transaction.update(seoState).set({ workingRevisionId: revision.revisionId }).where(eq(seoState.singleton, true));
7313
+ yield* replaceMediaReferences(transaction, "working", input.settings);
7314
+ yield* recordActivity(transaction, "seo.saved", input.activity, {
7315
+ previousVersion: current.working.version,
7316
+ version: revision.version
7317
+ }, ActivityId.make(revision.revisionId));
7318
+ return {
7319
+ ...current,
7320
+ working: revision
7321
+ };
7322
+ })).pipe(mapStoreFailure$1("SeoStore.Drizzle.save")));
7323
+ const publish = Effect.fn("SeoStore.Drizzle.publish")((input) => database.transaction((transaction) => Effect.gen(function* () {
7324
+ const current = yield* lockedSnapshot(transaction);
7325
+ yield* requireVersion(current, input.expectedVersion);
7326
+ yield* ensureMediaAvailable(transaction, current.working.settings);
7327
+ yield* transaction.update(seoState).set({ publishedRevisionId: current.working.revisionId }).where(eq(seoState.singleton, true));
7328
+ yield* replaceMediaReferences(transaction, "published", current.working.settings);
7329
+ yield* recordActivity(transaction, "seo.published", input.activity, { version: current.working.version });
7330
+ return {
7331
+ ...current,
7332
+ published: current.working
7333
+ };
7334
+ })).pipe(mapStoreFailure$1("SeoStore.Drizzle.publish")));
7335
+ return SeoStore.of({
7336
+ initialize,
7337
+ load,
7338
+ publish,
7339
+ save
7340
+ });
8707
7341
  }));
7342
+ const readSnapshot = Effect.fn("SeoStore.Drizzle.readSnapshot")(function* (database) {
7343
+ const state = (yield* database.select().from(seoState).limit(1))[0];
7344
+ if (!state) return void 0;
7345
+ const revisionRows = yield* database.select().from(seoRevisions).where(inArray(seoRevisions.id, [state.workingRevisionId, state.publishedRevisionId]));
7346
+ const revisions = yield* Effect.forEach(revisionRows, decodeRevision);
7347
+ const working = revisions.find((revision) => revision.revisionId === state.workingRevisionId);
7348
+ const published = revisions.find((revision) => revision.revisionId === state.publishedRevisionId);
7349
+ if (!working || !published) return yield* persistenceFailure$1("SeoStore.Drizzle.readSnapshot", /* @__PURE__ */ new Error("SEO state points to a missing revision."));
7350
+ return {
7351
+ published,
7352
+ working
7353
+ };
7354
+ });
7355
+ const lockedSnapshot = Effect.fn("SeoStore.Drizzle.lockSnapshot")(function* (transaction) {
7356
+ 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."));
7357
+ return (yield* readSnapshot(transaction)) ?? (yield* persistenceFailure$1("SeoStore.Drizzle.lockSnapshot", /* @__PURE__ */ new Error("SEO state disappeared.")));
7358
+ });
7359
+ const insertRevision = Effect.fn("SeoStore.Drizzle.insertRevision")(function* (transaction, settings, activity) {
7360
+ const row = (yield* transaction.insert(seoRevisions).values({
7361
+ actorEmail: activity.actor.email,
7362
+ actorId: activity.actor.id,
7363
+ actorName: activity.actor.name,
7364
+ actorRoles: activity.actor.roles,
7365
+ reason: activity.reason,
7366
+ requestId: activity.requestId,
7367
+ settings
7368
+ }).returning())[0];
7369
+ return row ? yield* decodeRevision(row) : yield* persistenceFailure$1("SeoStore.Drizzle.insertRevision", /* @__PURE__ */ new Error("No revision row returned."));
7370
+ });
7371
+ const decodeRevision = Effect.fn("SeoStore.Drizzle.decodeRevision")(function* (row) {
7372
+ return yield* Schema.decodeEffect(SeoRevision)({
7373
+ editedAt: row.createdAt,
7374
+ editedBy: {
7375
+ email: row.actorEmail,
7376
+ id: row.actorId,
7377
+ name: row.actorName,
7378
+ roles: row.actorRoles
7379
+ },
7380
+ revisionId: row.id,
7381
+ settings: row.settings,
7382
+ version: row.id
7383
+ }).pipe(Effect.mapError((cause) => new SeoPersistenceError({
7384
+ cause,
7385
+ operation: "SeoStore.Drizzle.decodeRevision"
7386
+ })));
7387
+ });
7388
+ function requireVersion(snapshot, expectedVersion) {
7389
+ return snapshot.working.version === expectedVersion ? Effect.void : Effect.fail(new SeoVersionConflict({
7390
+ actualVersion: snapshot.working.version,
7391
+ expectedVersion
7392
+ }));
7393
+ }
7394
+ const ensureMediaAvailable = Effect.fn("SeoStore.Drizzle.ensureMediaAvailable")(function* (transaction, settings) {
7395
+ const mediaIds = settingsMediaIds(settings);
7396
+ if (mediaIds.length === 0) return;
7397
+ const initialRows = yield* transaction.select({
7398
+ id: mediaAssets.id,
7399
+ objectKey: mediaAssets.objectKey
7400
+ }).from(mediaAssets).where(inArray(mediaAssets.id, mediaIds));
7401
+ const objectKeys = yield* Schema.decodeEffect(Schema.Array(MediaObjectKey))(initialRows.map((row) => row.objectKey));
7402
+ yield* lockMediaReferenceKeys(transaction, objectKeys);
7403
+ const readyRows = yield* transaction.select({
7404
+ id: mediaAssets.id,
7405
+ objectKey: mediaAssets.objectKey
7406
+ }).from(mediaAssets).where(and(inArray(mediaAssets.id, mediaIds), eq(mediaAssets.status, "ready")));
7407
+ const unavailable = settingsMediaImages(settings).find((image) => {
7408
+ const asset = readyRows.find((row) => row.id === image.mediaId);
7409
+ return !asset || mediaObjectKey(image.url) !== asset.objectKey;
7410
+ });
7411
+ if (unavailable) return yield* new SeoMediaUnavailable({ mediaId: unavailable.mediaId });
7412
+ });
7413
+ const replaceMediaReferences = Effect.fn("SeoStore.Drizzle.replaceMediaReferences")(function* (transaction, scope, settings) {
7414
+ yield* transaction.delete(seoMediaReferences).where(eq(seoMediaReferences.scope, scope));
7415
+ const mediaIds = settingsMediaIds(settings);
7416
+ if (mediaIds.length > 0) yield* transaction.insert(seoMediaReferences).values(mediaIds.map((mediaId) => ({
7417
+ mediaId,
7418
+ scope
7419
+ })));
7420
+ });
7421
+ function settingsMediaIds(settings) {
7422
+ return [...new Set(settingsMediaImages(settings).map((image) => image.mediaId))];
7423
+ }
7424
+ function settingsMediaImages(settings) {
7425
+ const images = [settings.socialImage];
7426
+ const identity = settings.identity;
7427
+ if (identity?.type === "Person") images.push(identity.image);
7428
+ if (identity?.type === "Organization") images.push(identity.logo);
7429
+ if (identity?.type === "LocalBusiness") images.push(identity.logo, identity.image);
7430
+ return images.filter((image) => image !== void 0);
7431
+ }
7432
+ function mediaObjectKey(url) {
7433
+ try {
7434
+ const encodedKey = new URL(url).pathname.match(/(?:^|\/)(media\/[^/]+)$/u)?.[1];
7435
+ return encodedKey ? encodedKey.split("/").map(decodeURIComponent).join("/") : void 0;
7436
+ } catch {
7437
+ return;
7438
+ }
7439
+ }
7440
+ const recordActivity = Effect.fn("SeoStore.Drizzle.recordActivity")(function* (transaction, action, context, details, id) {
7441
+ yield* transaction.insert(activityEntries).values({
7442
+ action,
7443
+ actorEmail: context.actor.email,
7444
+ actorId: context.actor.id,
7445
+ actorName: context.actor.name,
7446
+ actorRoles: context.actor.roles,
7447
+ details,
7448
+ id,
7449
+ reason: context.reason,
7450
+ requestId: context.requestId,
7451
+ targetKind: "seo"
7452
+ });
7453
+ });
7454
+ function mapStoreFailure$1(operation) {
7455
+ return (effect) => effect.pipe(Effect.mapError((cause) => isSeoStoreDomainError(cause) ? cause : new SeoPersistenceError({
7456
+ cause,
7457
+ operation
7458
+ })));
7459
+ }
7460
+ function mapPersistenceFailure$1(operation) {
7461
+ return (effect) => effect.pipe(Effect.mapError((cause) => isSeoPersistenceError(cause) ? cause : new SeoPersistenceError({
7462
+ cause,
7463
+ operation
7464
+ })));
7465
+ }
7466
+ function persistenceFailure$1(operation, cause) {
7467
+ return Effect.fail(new SeoPersistenceError({
7468
+ cause,
7469
+ operation
7470
+ }));
7471
+ }
8708
7472
 
8709
7473
  //#endregion
8710
7474
  //#region ../sql-pg/src/internal/user-administration/user-command-fence.ts
@@ -8722,7 +7486,7 @@ const postgresUserCommandFenceLayer = Layer.effect(UserCommandFence, PostgresAdv
8722
7486
  const WORKING_DOCUMENT_CHANGES_CHANNEL = "cms_working_documents";
8723
7487
  const WorkingDocumentChangePayload = Schema.fromJsonString(WorkingDocumentChange);
8724
7488
  function workingDocumentChangePayload(change) {
8725
- return Schema.encodeSync(WorkingDocumentChangePayload)(change);
7489
+ return Result.getOrThrow(Schema.encodeResult(WorkingDocumentChangePayload)(change));
8726
7490
  }
8727
7491
  const decodeWorkingDocumentChangePayload = Schema.decodeUnknownEffect(WorkingDocumentChangePayload);
8728
7492
 
@@ -8757,12 +7521,13 @@ const makePostgresChanges = Effect.fn("WorkingDocumentChanges.makePostgres")(fun
8757
7521
  const postgres = Context.get(context, PgClient.PgClient);
8758
7522
  const heartbeats = /* @__PURE__ */ new Map();
8759
7523
  let heartbeat = 0;
8760
- const listener = yield* postgres.listen(WORKING_DOCUMENT_CHANGES_CHANNEL).pipe(Stream.runForEach((payload) => {
8761
- const waiting = heartbeats.get(payload);
7524
+ const notifications = yield* postgres.listen(WORKING_DOCUMENT_CHANGES_CHANNEL).pipe(Effect.mapError((cause) => changesError("listen", cause)));
7525
+ const listener = yield* Stream.fromQueue(notifications).pipe(Stream.runForEach((payload) => {
7526
+ const waiting = heartbeats.get(payload.payload);
8762
7527
  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 }));
7528
+ if (payload.payload.startsWith("__cms_probe__:")) return Effect.void;
7529
+ 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))));
7530
+ }), Effect.catchCause((cause) => Effect.fail(changesError("listen", cause)))).pipe(Effect.forkScoped({ startImmediately: true }));
8766
7531
  const pulse = Effect.gen(function* () {
8767
7532
  heartbeat += 1;
8768
7533
  const payload = `__cms_probe__:${session}:${heartbeat}`;
@@ -9320,16 +8085,13 @@ function postgresLayer(options) {
9320
8085
  });
9321
8086
  const migrations = cmsDatabaseMigrationLayer.pipe(Layer.provide(database));
9322
8087
  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);
8088
+ 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
8089
  }
9327
8090
 
9328
8091
  //#endregion
9329
8092
  //#region src/runtime/installation.ts
9330
- /** Composes the private providers selected by the Nuxt product adapter. */
9331
8093
  const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options) {
9332
- const crypto = NodeCrypto$1.layer;
8094
+ const crypto = NodeCrypto.layer;
9333
8095
  const postgres = postgresLayer({ url: Redacted.make(options.databaseUrl) });
9334
8096
  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
8097
  baseURL: options.baseUrl,
@@ -9339,6 +8101,7 @@ const installCms = Effect.fn("NuxtCmsInstallation.install")(function* (options)
9339
8101
  const core = Layer.mergeAll(crypto, postgres, identity);
9340
8102
  return yield* installCms$1({
9341
8103
  baseUrl: options.baseUrl,
8104
+ formHandlers: options.formHandlers,
9342
8105
  authoring: options.authoring,
9343
8106
  bootstrap: options.bootstrap,
9344
8107
  cleanup: options.cleanup.enabled ? {
@@ -9377,4 +8140,4 @@ function toS3MediaObjectStoreOptions(options) {
9377
8140
 
9378
8141
  //#endregion
9379
8142
  export { toS3MediaObjectStoreOptions as n, installCms as t };
9380
- //# sourceMappingURL=installation-DeuBiFRE.js.map
8143
+ //# sourceMappingURL=installation-DgF2M1gL.js.map