@valkyrianlabs/payload-markdown-docs 0.1.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +195 -0
  2. package/dist/admin/DocsSetManager.d.ts +2 -0
  3. package/dist/admin/DocsSetManager.js +298 -0
  4. package/dist/admin/DocsSetManager.js.map +1 -0
  5. package/dist/admin/docsSetManagerData.d.ts +25 -0
  6. package/dist/admin/docsSetManagerData.js +266 -0
  7. package/dist/admin/docsSetManagerData.js.map +1 -0
  8. package/dist/admin/docsSetManagerTypes.d.ts +103 -0
  9. package/dist/admin/docsSetManagerTypes.js +3 -0
  10. package/dist/admin/docsSetManagerTypes.js.map +1 -0
  11. package/dist/admin/index.d.ts +3 -0
  12. package/dist/admin/index.js +4 -0
  13. package/dist/admin/index.js.map +1 -0
  14. package/dist/cli/commands/install.d.ts +2 -0
  15. package/dist/cli/commands/install.js +211 -0
  16. package/dist/cli/commands/install.js.map +1 -0
  17. package/dist/cli/commands/keygen.d.ts +2 -0
  18. package/dist/cli/commands/keygen.js +89 -0
  19. package/dist/cli/commands/keygen.js.map +1 -0
  20. package/dist/cli/commands/manifest.d.ts +2 -0
  21. package/dist/cli/commands/manifest.js +50 -0
  22. package/dist/cli/commands/manifest.js.map +1 -0
  23. package/dist/cli/commands/plan.d.ts +2 -0
  24. package/dist/cli/commands/plan.js +110 -0
  25. package/dist/cli/commands/plan.js.map +1 -0
  26. package/dist/cli/commands/push.d.ts +3 -0
  27. package/dist/cli/commands/push.js +308 -0
  28. package/dist/cli/commands/push.js.map +1 -0
  29. package/dist/cli/commands/validate.d.ts +3 -0
  30. package/dist/cli/commands/validate.js +109 -0
  31. package/dist/cli/commands/validate.js.map +1 -0
  32. package/dist/cli/filesystem.d.ts +20 -0
  33. package/dist/cli/filesystem.js +96 -0
  34. package/dist/cli/filesystem.js.map +1 -0
  35. package/dist/cli/format.d.ts +35 -0
  36. package/dist/cli/format.js +76 -0
  37. package/dist/cli/format.js.map +1 -0
  38. package/dist/cli/http.d.ts +19 -0
  39. package/dist/cli/http.js +39 -0
  40. package/dist/cli/http.js.map +1 -0
  41. package/dist/cli/index.d.ts +3 -0
  42. package/dist/cli/index.js +214 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/cli/parseArgs.d.ts +5 -0
  45. package/dist/cli/parseArgs.js +219 -0
  46. package/dist/cli/parseArgs.js.map +1 -0
  47. package/dist/cli/types.d.ts +51 -0
  48. package/dist/cli/types.js +3 -0
  49. package/dist/cli/types.js.map +1 -0
  50. package/dist/collections/docs.d.ts +9 -0
  51. package/dist/collections/docs.js +168 -0
  52. package/dist/collections/docs.js.map +1 -0
  53. package/dist/collections/docsGroups.d.ts +5 -0
  54. package/dist/collections/docsGroups.js +57 -0
  55. package/dist/collections/docsGroups.js.map +1 -0
  56. package/dist/collections/docsSets.d.ts +8 -0
  57. package/dist/collections/docsSets.js +158 -0
  58. package/dist/collections/docsSets.js.map +1 -0
  59. package/dist/collections/index.d.ts +10 -0
  60. package/dist/collections/index.js +7 -0
  61. package/dist/collections/index.js.map +1 -0
  62. package/dist/collections/nonces.d.ts +6 -0
  63. package/dist/collections/nonces.js +57 -0
  64. package/dist/collections/nonces.js.map +1 -0
  65. package/dist/collections/syncRuns.d.ts +5 -0
  66. package/dist/collections/syncRuns.js +139 -0
  67. package/dist/collections/syncRuns.js.map +1 -0
  68. package/dist/constants.d.ts +21 -0
  69. package/dist/constants.js +23 -0
  70. package/dist/constants.js.map +1 -0
  71. package/dist/endpoints/index.d.ts +2 -0
  72. package/dist/endpoints/index.js +3 -0
  73. package/dist/endpoints/index.js.map +1 -0
  74. package/dist/endpoints/sync.d.ts +47 -0
  75. package/dist/endpoints/sync.js +616 -0
  76. package/dist/endpoints/sync.js.map +1 -0
  77. package/dist/index.d.ts +9 -0
  78. package/dist/index.js +7 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/next/PayloadMarkdownDocsPage.d.ts +7 -0
  81. package/dist/next/PayloadMarkdownDocsPage.js +142 -0
  82. package/dist/next/PayloadMarkdownDocsPage.js.map +1 -0
  83. package/dist/next/index.d.ts +9 -0
  84. package/dist/next/index.js +7 -0
  85. package/dist/next/index.js.map +1 -0
  86. package/dist/next/markdown.d.ts +14 -0
  87. package/dist/next/markdown.js +232 -0
  88. package/dist/next/markdown.js.map +1 -0
  89. package/dist/next/metadata.d.ts +3 -0
  90. package/dist/next/metadata.js +33 -0
  91. package/dist/next/metadata.js.map +1 -0
  92. package/dist/next/records.d.ts +14 -0
  93. package/dist/next/records.js +146 -0
  94. package/dist/next/records.js.map +1 -0
  95. package/dist/next/route.d.ts +6 -0
  96. package/dist/next/route.js +271 -0
  97. package/dist/next/route.js.map +1 -0
  98. package/dist/next/sidebar.d.ts +15 -0
  99. package/dist/next/sidebar.js +137 -0
  100. package/dist/next/sidebar.js.map +1 -0
  101. package/dist/next/types.d.ts +117 -0
  102. package/dist/next/types.js +3 -0
  103. package/dist/next/types.js.map +1 -0
  104. package/dist/payload/applyDocsSync.d.ts +54 -0
  105. package/dist/payload/applyDocsSync.js +176 -0
  106. package/dist/payload/applyDocsSync.js.map +1 -0
  107. package/dist/payload/docsConflicts.d.ts +12 -0
  108. package/dist/payload/docsConflicts.js +34 -0
  109. package/dist/payload/docsConflicts.js.map +1 -0
  110. package/dist/payload/docsData.d.ts +23 -0
  111. package/dist/payload/docsData.js +59 -0
  112. package/dist/payload/docsData.js.map +1 -0
  113. package/dist/payload/docsSets.d.ts +38 -0
  114. package/dist/payload/docsSets.js +57 -0
  115. package/dist/payload/docsSets.js.map +1 -0
  116. package/dist/payload/existingDocs.d.ts +43 -0
  117. package/dist/payload/existingDocs.js +97 -0
  118. package/dist/payload/existingDocs.js.map +1 -0
  119. package/dist/payload/index.d.ts +15 -0
  120. package/dist/payload/index.js +10 -0
  121. package/dist/payload/index.js.map +1 -0
  122. package/dist/payload/routeCollisions.d.ts +31 -0
  123. package/dist/payload/routeCollisions.js +104 -0
  124. package/dist/payload/routeCollisions.js.map +1 -0
  125. package/dist/payload/syncRuns.d.ts +60 -0
  126. package/dist/payload/syncRuns.js +53 -0
  127. package/dist/payload/syncRuns.js.map +1 -0
  128. package/dist/plugin.d.ts +3 -0
  129. package/dist/plugin.js +165 -0
  130. package/dist/plugin.js.map +1 -0
  131. package/dist/routing/index.d.ts +3 -0
  132. package/dist/routing/index.js +4 -0
  133. package/dist/routing/index.js.map +1 -0
  134. package/dist/routing/paths.d.ts +7 -0
  135. package/dist/routing/paths.js +23 -0
  136. package/dist/routing/paths.js.map +1 -0
  137. package/dist/routing/reservations.d.ts +37 -0
  138. package/dist/routing/reservations.js +79 -0
  139. package/dist/routing/reservations.js.map +1 -0
  140. package/dist/security/canonical.d.ts +12 -0
  141. package/dist/security/canonical.js +24 -0
  142. package/dist/security/canonical.js.map +1 -0
  143. package/dist/security/githubOidc.d.ts +45 -0
  144. package/dist/security/githubOidc.js +177 -0
  145. package/dist/security/githubOidc.js.map +1 -0
  146. package/dist/security/headers.d.ts +22 -0
  147. package/dist/security/headers.js +44 -0
  148. package/dist/security/headers.js.map +1 -0
  149. package/dist/security/index.d.ts +15 -0
  150. package/dist/security/index.js +9 -0
  151. package/dist/security/index.js.map +1 -0
  152. package/dist/security/jwks.d.ts +20 -0
  153. package/dist/security/jwks.js +40 -0
  154. package/dist/security/jwks.js.map +1 -0
  155. package/dist/security/jwt.d.ts +10 -0
  156. package/dist/security/jwt.js +42 -0
  157. package/dist/security/jwt.js.map +1 -0
  158. package/dist/security/nonce.d.ts +34 -0
  159. package/dist/security/nonce.js +43 -0
  160. package/dist/security/nonce.js.map +1 -0
  161. package/dist/security/sign.d.ts +13 -0
  162. package/dist/security/sign.js +39 -0
  163. package/dist/security/sign.js.map +1 -0
  164. package/dist/security/verify.d.ts +28 -0
  165. package/dist/security/verify.js +54 -0
  166. package/dist/security/verify.js.map +1 -0
  167. package/dist/skills/codex/SKILL.md +173 -0
  168. package/dist/skills/codex/examples/docs-page.md +42 -0
  169. package/dist/skills/codex/examples/github-actions.md +64 -0
  170. package/dist/skills/codex/reference/admin.md +28 -0
  171. package/dist/skills/codex/reference/frontmatter.md +39 -0
  172. package/dist/skills/codex/reference/payload-markdown-directives.md +77 -0
  173. package/dist/skills/codex/reference/routing.md +35 -0
  174. package/dist/skills/codex/reference/sync.md +35 -0
  175. package/dist/skills/codex/reference/troubleshooting.md +53 -0
  176. package/dist/skills/codex/reference/workflow.md +39 -0
  177. package/dist/sync/aiExportManifest.d.ts +58 -0
  178. package/dist/sync/aiExportManifest.js +430 -0
  179. package/dist/sync/aiExportManifest.js.map +1 -0
  180. package/dist/sync/frontmatter.d.ts +28 -0
  181. package/dist/sync/frontmatter.js +210 -0
  182. package/dist/sync/frontmatter.js.map +1 -0
  183. package/dist/sync/hash.d.ts +1 -0
  184. package/dist/sync/hash.js +8 -0
  185. package/dist/sync/hash.js.map +1 -0
  186. package/dist/sync/index.d.ts +12 -0
  187. package/dist/sync/index.js +9 -0
  188. package/dist/sync/index.js.map +1 -0
  189. package/dist/sync/manifest.d.ts +58 -0
  190. package/dist/sync/manifest.js +21 -0
  191. package/dist/sync/manifest.js.map +1 -0
  192. package/dist/sync/paths.d.ts +16 -0
  193. package/dist/sync/paths.js +116 -0
  194. package/dist/sync/paths.js.map +1 -0
  195. package/dist/sync/plan.d.ts +29 -0
  196. package/dist/sync/plan.js +72 -0
  197. package/dist/sync/plan.js.map +1 -0
  198. package/dist/sync/validate.d.ts +26 -0
  199. package/dist/sync/validate.js +308 -0
  200. package/dist/sync/validate.js.map +1 -0
  201. package/dist/types.d.ts +84 -0
  202. package/dist/types.js +3 -0
  203. package/dist/types.js.map +1 -0
  204. package/package.json +143 -0
@@ -0,0 +1,616 @@
1
+ import { DEFAULT_DOCS_ROUTE_BASE, DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_SKEW_SECONDS, DEFAULT_NONCE_TTL_SECONDS } from '../constants.js';
2
+ import { applyDocsSync, assertApplyDeleteBehaviorSupported, createSyncRunAudit, findConfiguredPagesRouteCollisions, findDocsSetBySourceId, findDocsSyncConflicts, findDuplicateDesiredRouteCollisions, findExistingDocsRouteCollisions, findExistingPayloadDocsRecords, getRecordId, toExistingDocsRecord, updateDocsSetAfterSync, updateSyncRunAudit } from '../payload/index.js';
3
+ import { assertNonceNotReplayed, buildCanonicalSigningString, extractSyncRequestHeaders, getCanonicalPathFromRequestUrl, storeAcceptedNonce, validateTimestampSkew, verifyBodySha256, verifyEd25519Signature, verifyGitHubOidcToken } from '../security/index.js';
4
+ import { planDocsSync, validateDocsManifest } from '../sync/index.js';
5
+ const jsonResponse = (body, status = 200)=>Response.json(body, {
6
+ status
7
+ });
8
+ const errorResponse = (code, message, status = 400, extras = {})=>jsonResponse({
9
+ ...extras,
10
+ error: {
11
+ code,
12
+ message
13
+ },
14
+ ok: false
15
+ }, status);
16
+ const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ const parseManifestBody = (rawBody)=>{
18
+ try {
19
+ const parsed = JSON.parse(rawBody);
20
+ return isRecord(parsed) ? parsed : undefined;
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ };
25
+ const findSourceConfig = (sourceId, sources)=>sources?.find((source)=>source.id === sourceId);
26
+ const getAllowedSourceIds = (sources)=>{
27
+ if (!sources || sources.length === 0) {
28
+ return undefined;
29
+ }
30
+ return sources.map((source)=>source.id);
31
+ };
32
+ const resolveSyncSource = async ({ manifest, options, payload })=>{
33
+ const sourceId = manifest.source?.id;
34
+ if (!sourceId) {
35
+ return {
36
+ source: {
37
+ allowedSourceIds: getAllowedSourceIds(options.sources),
38
+ routeBase: options.routeBase ?? DEFAULT_DOCS_ROUTE_BASE
39
+ }
40
+ };
41
+ }
42
+ const docsSet = options.docsSetsEnabled ? await findDocsSetBySourceId({
43
+ collectionSlug: options.docsSetsCollectionSlug,
44
+ payload,
45
+ sourceId
46
+ }) : undefined;
47
+ if (docsSet) {
48
+ if (docsSet.sourceRoot && manifest.source.root && docsSet.sourceRoot !== manifest.source.root) {
49
+ return {
50
+ response: errorResponse('source_not_allowed', `Manifest source.root "${manifest.source.root}" is not allowed for docs set source "${sourceId}".`, 400)
51
+ };
52
+ }
53
+ return {
54
+ source: {
55
+ allowedSourceIds: [
56
+ sourceId
57
+ ],
58
+ docsSet,
59
+ routeBase: docsSet.routeBase,
60
+ sourceRoot: docsSet.sourceRoot
61
+ }
62
+ };
63
+ }
64
+ const sourceConfig = findSourceConfig(sourceId, options.sources);
65
+ if (options.sources && options.sources.length > 0 && !sourceConfig) {
66
+ return {
67
+ response: errorResponse('source_not_allowed', `Manifest source.id "${sourceId}" is not configured for this endpoint.`, 400)
68
+ };
69
+ }
70
+ if (sourceConfig?.root && manifest.source.root && sourceConfig.root !== manifest.source.root) {
71
+ return {
72
+ response: errorResponse('source_not_allowed', `Manifest source.root "${manifest.source.root}" is not allowed for source "${sourceId}".`, 400)
73
+ };
74
+ }
75
+ return {
76
+ source: {
77
+ allowedSourceIds: getAllowedSourceIds(options.sources),
78
+ routeBase: sourceConfig?.routeBase ?? options.routeBase ?? DEFAULT_DOCS_ROUTE_BASE,
79
+ sourceRoot: sourceConfig?.root
80
+ }
81
+ };
82
+ };
83
+ const summarizePlan = (plan)=>({
84
+ archive: plan.archive.length,
85
+ create: plan.create.length,
86
+ delete: plan.delete.length,
87
+ draft: plan.draft.length,
88
+ unchanged: plan.unchanged.length,
89
+ update: plan.update.length,
90
+ warnings: plan.warnings.length
91
+ });
92
+ const serializeChange = (change)=>({
93
+ current: change.current ? {
94
+ archived: change.current.archived,
95
+ route: change.current.route,
96
+ sourceHash: change.current.sourceHash,
97
+ title: change.current.title
98
+ } : undefined,
99
+ desired: change.desired ? {
100
+ route: change.desired.route,
101
+ sha256: change.desired.sha256,
102
+ title: change.desired.title
103
+ } : undefined,
104
+ reason: change.reason,
105
+ sourcePath: change.sourcePath
106
+ });
107
+ const serializeChanges = (plan)=>({
108
+ archive: plan.archive.map(serializeChange),
109
+ create: plan.create.map(serializeChange),
110
+ delete: plan.delete.map(serializeChange),
111
+ draft: plan.draft.map(serializeChange),
112
+ unchanged: plan.unchanged.map(serializeChange),
113
+ update: plan.update.map(serializeChange)
114
+ });
115
+ const getTotalManifestBytes = (manifest)=>manifest.files.reduce((total, file)=>total + Buffer.byteLength(file.content, 'utf8'), 0);
116
+ const getPlannedConflictChanges = ({ existing, plan })=>{
117
+ const existingBySourcePath = new Map(existing.map((record)=>[
118
+ record.sourcePath,
119
+ record
120
+ ]));
121
+ const archivedUnchanged = plan.unchanged.filter((change)=>{
122
+ const current = existingBySourcePath.get(change.sourcePath);
123
+ return current?.archived === true;
124
+ });
125
+ return [
126
+ ...plan.update,
127
+ ...plan.archive,
128
+ ...plan.draft,
129
+ ...plan.delete,
130
+ ...archivedUnchanged
131
+ ];
132
+ };
133
+ const getDefaultPublishMode = (options)=>options.defaultPublishMode ?? (options.docsEnableDrafts ? 'draft' : 'preserve');
134
+ const getLifecyclePolicyError = ({ deleteBehavior, manifest, options, publishMode })=>{
135
+ if (manifest.publish && options.allowPublish !== true) {
136
+ return errorResponse('publish_disabled', 'Publishing is disabled by server configuration.', 403);
137
+ }
138
+ if ((manifest.publish || publishMode === 'published') && !options.docsEnableDrafts) {
139
+ return errorResponse('publish_not_available', 'Publishing requires a draft-enabled dedicated docs collection.', 400);
140
+ }
141
+ if (publishMode === 'published' && options.allowPublish !== true) {
142
+ return errorResponse('publish_disabled', 'Publishing is disabled by server configuration.', 403);
143
+ }
144
+ if (publishMode === 'draft' && !options.docsEnableDrafts) {
145
+ return errorResponse('draft_behavior_not_available', 'Draft mode requires a draft-enabled dedicated docs collection.', 400);
146
+ }
147
+ if (deleteBehavior === 'draft' && !options.docsEnableDrafts) {
148
+ return errorResponse('draft_behavior_not_available', 'Draft delete behavior requires a draft-enabled dedicated docs collection.', 400);
149
+ }
150
+ if (deleteBehavior === 'delete' && options.allowHardDelete !== true) {
151
+ return errorResponse('hard_delete_disabled', 'Hard delete is disabled by server configuration.', 403);
152
+ }
153
+ return undefined;
154
+ };
155
+ const getRouteCollisionIssues = async ({ docsSet, manifest, options, payload, routeBase })=>{
156
+ const desiredRoutes = manifest.files.map((file)=>file.route);
157
+ const duplicateDesiredRouteCollisions = findDuplicateDesiredRouteCollisions(desiredRoutes);
158
+ const existingDocsRouteCollisions = options.docsEnabled ? await findExistingDocsRouteCollisions({
159
+ collectionSlug: options.docsCollectionSlug,
160
+ docsSetId: docsSet?.id,
161
+ payload,
162
+ routes: desiredRoutes,
163
+ sourceId: manifest.source.id
164
+ }) : [];
165
+ const pageRouteCollisions = options.routing?.pages?.enabled === true ? await findConfiguredPagesRouteCollisions({
166
+ allowBridgePages: options.routing.pages.allowBridgePages,
167
+ bridgeField: options.routing.pages.bridgeField,
168
+ collectionSlug: options.routing.pages.collection,
169
+ docsSetRouteBase: routeBase,
170
+ payload,
171
+ routeField: options.routing.pages.routeField
172
+ }) : [];
173
+ return [
174
+ ...duplicateDesiredRouteCollisions,
175
+ ...existingDocsRouteCollisions,
176
+ ...pageRouteCollisions
177
+ ];
178
+ };
179
+ const getRequiredHeader = (headers, name)=>{
180
+ const value = headers.get(name);
181
+ return value && value.trim() !== '' ? value.trim() : undefined;
182
+ };
183
+ const getBearerToken = (headers)=>{
184
+ const authorization = getRequiredHeader(headers, 'authorization');
185
+ if (!authorization) {
186
+ return undefined;
187
+ }
188
+ const [scheme, token] = authorization.split(/\s+/, 2);
189
+ if (scheme?.toLowerCase() !== 'bearer' || !token) {
190
+ return '';
191
+ }
192
+ return token;
193
+ };
194
+ const assertReplayProtectionAvailable = (options)=>options.noncesEnabled ? undefined : errorResponse('replay_protection_unavailable', 'Sync endpoint requires nonce replay protection.', 500);
195
+ const authenticateEd25519Request = async ({ now, options, rawBody, req })=>{
196
+ if (!options.auth || options.auth.mode !== 'ed25519') {
197
+ return {
198
+ response: errorResponse('auth_disabled', 'Signed sync authentication is not configured for this endpoint.', 401)
199
+ };
200
+ }
201
+ const headersResult = extractSyncRequestHeaders(req.headers);
202
+ if (!headersResult.ok) {
203
+ return {
204
+ response: errorResponse('missing_header', `Missing required sync header: ${headersResult.header}.`, 401)
205
+ };
206
+ }
207
+ const keyConfig = options.auth.keys.find((key)=>key.id === headersResult.headers.keyId);
208
+ if (!keyConfig) {
209
+ return {
210
+ response: errorResponse('unknown_key', 'Unknown sync request key id.', 401)
211
+ };
212
+ }
213
+ const bodyHash = verifyBodySha256({
214
+ body: rawBody,
215
+ expectedHash: headersResult.headers.bodySha256
216
+ });
217
+ if (!bodyHash.ok) {
218
+ return {
219
+ response: errorResponse('body_hash_mismatch', 'Sync request body hash does not match the signed header.', 401)
220
+ };
221
+ }
222
+ const timestampValidation = validateTimestampSkew({
223
+ maxSkewSeconds: options.auth.maxSkewSeconds ?? options.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS,
224
+ now,
225
+ timestamp: headersResult.headers.timestamp
226
+ });
227
+ if (!timestampValidation.ok) {
228
+ return {
229
+ response: errorResponse('invalid_timestamp', timestampValidation.message, 401)
230
+ };
231
+ }
232
+ const replayUnavailable = assertReplayProtectionAvailable(options);
233
+ if (replayUnavailable) {
234
+ return {
235
+ response: replayUnavailable
236
+ };
237
+ }
238
+ const nonceAvailable = await assertNonceNotReplayed({
239
+ collectionSlug: options.noncesCollectionSlug,
240
+ keyId: headersResult.headers.keyId,
241
+ nonce: headersResult.headers.nonce,
242
+ now,
243
+ payload: req.payload
244
+ });
245
+ if (!nonceAvailable) {
246
+ return {
247
+ response: errorResponse('nonce_replay', 'Sync request nonce has already been used.', 409)
248
+ };
249
+ }
250
+ const canonicalPath = getCanonicalPathFromRequestUrl({
251
+ endpointPath: options.endpointPath,
252
+ url: req.url
253
+ });
254
+ const canonicalString = buildCanonicalSigningString({
255
+ bodySha256: bodyHash.computedHash,
256
+ method: 'POST',
257
+ nonce: headersResult.headers.nonce,
258
+ path: canonicalPath,
259
+ timestamp: headersResult.headers.timestamp
260
+ });
261
+ if (!verifyEd25519Signature({
262
+ canonicalString,
263
+ publicKey: keyConfig.publicKey,
264
+ signature: headersResult.headers.signature
265
+ })) {
266
+ return {
267
+ response: errorResponse('invalid_signature', 'Invalid sync request signature.', 401)
268
+ };
269
+ }
270
+ const nonceTtlSeconds = options.auth.nonceTtlSeconds ?? options.nonceTtlSeconds ?? DEFAULT_NONCE_TTL_SECONDS;
271
+ return {
272
+ identity: {
273
+ bodyHash: bodyHash.computedHash,
274
+ expiresAt: new Date(now.getTime() + nonceTtlSeconds * 1000),
275
+ keyId: headersResult.headers.keyId,
276
+ nonce: headersResult.headers.nonce
277
+ }
278
+ };
279
+ };
280
+ const authenticateGitHubOidcRequest = async ({ now, options, rawBody, req })=>{
281
+ if (!options.auth || options.auth.mode !== 'github-oidc') {
282
+ return {
283
+ response: errorResponse('auth_disabled', 'GitHub OIDC sync authentication is not configured for this endpoint.', 401)
284
+ };
285
+ }
286
+ const token = getBearerToken(req.headers);
287
+ if (token === undefined) {
288
+ return {
289
+ response: errorResponse('missing_header', 'Missing required sync header: Authorization.', 401)
290
+ };
291
+ }
292
+ if (token === '') {
293
+ return {
294
+ response: errorResponse('oidc_invalid_token', 'Authorization must be a Bearer GitHub OIDC token.', 401)
295
+ };
296
+ }
297
+ const expectedHash = getRequiredHeader(req.headers, 'x-vl-md-docs-body-sha256');
298
+ if (!expectedHash) {
299
+ return {
300
+ response: errorResponse('missing_header', 'Missing required sync header: X-VL-MD-DOCS-Body-SHA256.', 401)
301
+ };
302
+ }
303
+ const bodyHash = verifyBodySha256({
304
+ body: rawBody,
305
+ expectedHash
306
+ });
307
+ if (!bodyHash.ok) {
308
+ return {
309
+ response: errorResponse('body_hash_mismatch', 'Sync request body hash does not match the OIDC header.', 401)
310
+ };
311
+ }
312
+ const verified = await verifyGitHubOidcToken({
313
+ config: options.auth,
314
+ fetchJson: options.oidcFetchJson,
315
+ now,
316
+ token
317
+ });
318
+ if (!verified.ok) {
319
+ return {
320
+ response: errorResponse(verified.code, verified.message, verified.code === 'oidc_jwks_unavailable' ? 503 : 401)
321
+ };
322
+ }
323
+ const replayUnavailable = assertReplayProtectionAvailable(options);
324
+ if (replayUnavailable) {
325
+ return {
326
+ response: replayUnavailable
327
+ };
328
+ }
329
+ const nonceAvailable = await assertNonceNotReplayed({
330
+ collectionSlug: options.noncesCollectionSlug,
331
+ keyId: verified.token.keyId,
332
+ nonce: verified.token.claims.jti,
333
+ now,
334
+ payload: req.payload
335
+ });
336
+ if (!nonceAvailable) {
337
+ return {
338
+ response: errorResponse('oidc_replay', 'GitHub OIDC token jti has already been used.', 409)
339
+ };
340
+ }
341
+ return {
342
+ identity: {
343
+ actor: verified.token.claims.actor,
344
+ bodyHash: bodyHash.computedHash,
345
+ branch: verified.token.claims.ref,
346
+ commit: verified.token.claims.sha,
347
+ expiresAt: verified.token.expiresAt,
348
+ keyId: verified.token.keyId,
349
+ nonce: verified.token.claims.jti,
350
+ repository: verified.token.claims.repository
351
+ }
352
+ };
353
+ };
354
+ const authenticateSyncRequest = async ({ now, options, rawBody, req })=>{
355
+ if (!options.auth || options.auth.mode === 'disabled') {
356
+ return {
357
+ response: errorResponse('auth_disabled', 'Sync authentication is not configured for this endpoint.', 401)
358
+ };
359
+ }
360
+ if (options.auth.mode === 'github-oidc') {
361
+ return authenticateGitHubOidcRequest({
362
+ now,
363
+ options,
364
+ rawBody,
365
+ req
366
+ });
367
+ }
368
+ return authenticateEd25519Request({
369
+ now,
370
+ options,
371
+ rawBody,
372
+ req
373
+ });
374
+ };
375
+ const createSyncEndpointHandler = (options)=>async (req)=>{
376
+ const startedAt = options.getNow?.() ?? new Date();
377
+ if (req.method && req.method.toUpperCase() !== 'POST') {
378
+ return errorResponse('invalid_method', 'Sync endpoint only accepts POST.', 405);
379
+ }
380
+ if (typeof req.text !== 'function') {
381
+ return errorResponse('invalid_body', 'Sync endpoint requires access to the request body text.', 400);
382
+ }
383
+ const rawBody = await req.text();
384
+ const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
385
+ if (Buffer.byteLength(rawBody, 'utf8') > maxBodyBytes) {
386
+ return errorResponse('invalid_body', 'Sync request body is too large.', 413);
387
+ }
388
+ const authentication = await authenticateSyncRequest({
389
+ now: startedAt,
390
+ options,
391
+ rawBody,
392
+ req
393
+ });
394
+ if (authentication.response) {
395
+ return authentication.response;
396
+ }
397
+ const manifest = parseManifestBody(rawBody);
398
+ if (!manifest) {
399
+ return errorResponse('invalid_body', 'Sync request body must be a JSON manifest.', 400);
400
+ }
401
+ const sourceResolution = await resolveSyncSource({
402
+ manifest,
403
+ options,
404
+ payload: req.payload
405
+ });
406
+ if (sourceResolution.response) {
407
+ return sourceResolution.response;
408
+ }
409
+ const validation = validateDocsManifest(manifest, {
410
+ allowedSourceIds: sourceResolution.source.allowedSourceIds,
411
+ maxTotalBytes: maxBodyBytes,
412
+ routeBase: sourceResolution.source.routeBase
413
+ });
414
+ if (!validation.ok) {
415
+ return jsonResponse({
416
+ error: {
417
+ code: 'invalid_manifest',
418
+ message: 'Sync manifest is invalid.'
419
+ },
420
+ ok: false
421
+ }, 400);
422
+ }
423
+ const effectiveDeleteBehavior = options.deleteBehavior ?? 'archive';
424
+ const effectivePublishMode = validation.data.publish ? 'published' : getDefaultPublishMode(options);
425
+ const lifecyclePolicyError = getLifecyclePolicyError({
426
+ deleteBehavior: effectiveDeleteBehavior,
427
+ manifest: validation.data,
428
+ options,
429
+ publishMode: effectivePublishMode
430
+ });
431
+ if (lifecyclePolicyError) {
432
+ return lifecyclePolicyError;
433
+ }
434
+ const routeCollisions = await getRouteCollisionIssues({
435
+ docsSet: sourceResolution.source.docsSet,
436
+ manifest: validation.data,
437
+ options,
438
+ payload: req.payload,
439
+ routeBase: sourceResolution.source.routeBase
440
+ });
441
+ if (routeCollisions.length > 0) {
442
+ return errorResponse('route_collision', 'One or more docs routes collide with an existing route reservation.', 409, {
443
+ routeCollisions
444
+ });
445
+ }
446
+ const isSyncMode = validation.data.mode === 'sync';
447
+ if (isSyncMode && options.allowWrites !== true) {
448
+ return errorResponse('sync_writes_disabled', 'Sync writes are disabled by server configuration.', 403);
449
+ }
450
+ if (isSyncMode && options.requireDryRunBeforeApply === true) {
451
+ return errorResponse('dry_run_required_not_implemented', 'Required dry-run proof before apply is not implemented yet.', 400);
452
+ }
453
+ if (isSyncMode && !assertApplyDeleteBehaviorSupported(effectiveDeleteBehavior, {
454
+ allowHardDelete: options.allowHardDelete,
455
+ docsEnableDrafts: options.docsEnableDrafts
456
+ })) {
457
+ return errorResponse('delete_behavior_not_implemented', 'Configured delete behavior cannot be applied.', 400);
458
+ }
459
+ if (isSyncMode && !options.syncRunsEnabled) {
460
+ return errorResponse('audit_unavailable', 'Applied sync requires the sync-run audit collection.', 500);
461
+ }
462
+ const existingPayloadDocs = options.docsEnabled ? await findExistingPayloadDocsRecords({
463
+ collectionSlug: options.docsCollectionSlug,
464
+ docsSetId: sourceResolution.source.docsSet?.id,
465
+ markdownFieldName: options.markdownFieldName,
466
+ payload: req.payload,
467
+ sourceId: validation.data.source.id
468
+ }) : [];
469
+ const existingDocs = existingPayloadDocs.map(toExistingDocsRecord);
470
+ const plan = planDocsSync({
471
+ deleteBehavior: effectiveDeleteBehavior,
472
+ desired: validation.data,
473
+ existing: existingDocs
474
+ });
475
+ const summary = summarizePlan(plan);
476
+ const warnings = [
477
+ ...validation.warnings,
478
+ ...plan.warnings
479
+ ];
480
+ if (isSyncMode) {
481
+ const existingBySourcePath = new Map(existingPayloadDocs.map((record)=>[
482
+ record.sourcePath,
483
+ record
484
+ ]));
485
+ const conflicts = findDocsSyncConflicts({
486
+ existingBySourcePath,
487
+ plannedChanges: getPlannedConflictChanges({
488
+ existing: existingPayloadDocs,
489
+ plan
490
+ })
491
+ });
492
+ if (conflicts.length > 0) {
493
+ return errorResponse('manual_edit_conflict', 'One or more docs were modified outside the docs sync workflow.', 409, {
494
+ conflicts
495
+ });
496
+ }
497
+ }
498
+ await storeAcceptedNonce({
499
+ bodyHash: authentication.identity.bodyHash,
500
+ collectionSlug: options.noncesCollectionSlug,
501
+ expiresAt: authentication.identity.expiresAt,
502
+ keyId: authentication.identity.keyId,
503
+ nonce: authentication.identity.nonce,
504
+ payload: req.payload,
505
+ sourceId: validation.data.source.id,
506
+ usedAt: startedAt
507
+ });
508
+ let syncRunId;
509
+ if (options.syncRunsEnabled) {
510
+ const syncRun = await createSyncRunAudit({
511
+ actor: authentication.identity.actor,
512
+ bodyHash: authentication.identity.bodyHash,
513
+ branch: authentication.identity.branch ?? validation.data.source.branch,
514
+ collectionSlug: options.syncRunsCollectionSlug,
515
+ commit: authentication.identity.commit ?? validation.data.source.commit,
516
+ completedAt: isSyncMode ? startedAt : options.getNow?.() ?? new Date(),
517
+ deleteBehavior: effectiveDeleteBehavior,
518
+ effectivePublishMode,
519
+ errors: [],
520
+ fileCount: validation.data.files.length,
521
+ keyId: authentication.identity.keyId,
522
+ mode: isSyncMode ? 'sync' : 'dry-run',
523
+ payload: req.payload,
524
+ publishRequested: validation.data.publish,
525
+ repository: authentication.identity.repository ?? validation.data.source.repository,
526
+ sourceId: validation.data.source.id,
527
+ startedAt,
528
+ status: isSyncMode ? 'pending' : 'success',
529
+ summary,
530
+ totalBytes: getTotalManifestBytes(validation.data),
531
+ warnings
532
+ });
533
+ syncRunId = getRecordId(syncRun);
534
+ }
535
+ if (isSyncMode) {
536
+ if (!syncRunId) {
537
+ return errorResponse('audit_unavailable', 'Applied sync could not create a sync-run audit record.', 500);
538
+ }
539
+ try {
540
+ const applyResult = await applyDocsSync({
541
+ collectionSlug: options.docsCollectionSlug,
542
+ deleteBehavior: effectiveDeleteBehavior,
543
+ docsEnableDrafts: options.docsEnableDrafts,
544
+ docsSetId: sourceResolution.source.docsSet?.id,
545
+ existing: existingPayloadDocs,
546
+ manifest: validation.data,
547
+ markdownFieldName: options.markdownFieldName,
548
+ now: options.getNow?.() ?? new Date(),
549
+ payload: req.payload,
550
+ plan,
551
+ publishMode: effectivePublishMode,
552
+ syncRunId
553
+ });
554
+ if (!applyResult.ok) {
555
+ return errorResponse('manual_edit_conflict', 'One or more docs were modified outside the docs sync workflow.', 409, {
556
+ conflicts: applyResult.conflicts
557
+ });
558
+ }
559
+ await updateSyncRunAudit({
560
+ collectionSlug: options.syncRunsCollectionSlug,
561
+ completedAt: options.getNow?.() ?? new Date(),
562
+ payload: req.payload,
563
+ status: 'success',
564
+ summary,
565
+ syncRunId,
566
+ warnings
567
+ });
568
+ if (sourceResolution.source.docsSet) {
569
+ await updateDocsSetAfterSync({
570
+ aiExport: validation.data.aiExport,
571
+ collectionSlug: options.docsSetsCollectionSlug,
572
+ docsCount: validation.data.files.length,
573
+ docsSetId: sourceResolution.source.docsSet.id,
574
+ now: options.getNow?.() ?? new Date(),
575
+ payload: req.payload,
576
+ syncRunId
577
+ });
578
+ }
579
+ } catch (error) {
580
+ await updateSyncRunAudit({
581
+ collectionSlug: options.syncRunsCollectionSlug,
582
+ completedAt: options.getNow?.() ?? new Date(),
583
+ errors: [
584
+ {
585
+ code: 'invalid_manifest',
586
+ message: error instanceof Error ? error.message : 'Sync apply failed.'
587
+ }
588
+ ],
589
+ payload: req.payload,
590
+ status: 'failed',
591
+ summary,
592
+ syncRunId,
593
+ warnings
594
+ });
595
+ return errorResponse('sync_apply_failed', 'Sync apply failed.', 500);
596
+ }
597
+ }
598
+ return jsonResponse({
599
+ changes: serializeChanges(plan),
600
+ deleteBehavior: effectiveDeleteBehavior,
601
+ dryRun: !isSyncMode,
602
+ effectivePublishMode,
603
+ ok: true,
604
+ publishRequested: validation.data.publish,
605
+ summary,
606
+ syncRunId: syncRunId === undefined ? undefined : String(syncRunId),
607
+ warnings
608
+ });
609
+ };
610
+ export const createSyncEndpoint = (options)=>({
611
+ handler: createSyncEndpointHandler(options),
612
+ method: 'post',
613
+ path: options.endpointPath
614
+ });
615
+
616
+ //# sourceMappingURL=sync.js.map