@setzkasten-cms/astro-admin 1.4.6 → 1.5.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 (166) hide show
  1. package/dist/api-routes/_auth-guard.d.ts +27 -3
  2. package/dist/api-routes/_auth-guard.js +5 -2
  3. package/dist/api-routes/_dev-session-secret.d.ts +8 -0
  4. package/dist/api-routes/_dev-session-secret.js +8 -0
  5. package/dist/api-routes/_github-token.js +1 -1
  6. package/dist/api-routes/_role-resolver.js +6 -3
  7. package/dist/api-routes/_session-secret.d.ts +19 -0
  8. package/dist/api-routes/_session-secret.js +7 -0
  9. package/dist/api-routes/_session-signing.d.ts +45 -0
  10. package/dist/api-routes/_session-signing.js +8 -0
  11. package/dist/api-routes/_webhook-dispatcher.js +4 -4
  12. package/dist/api-routes/asset-proxy.js +1 -1
  13. package/dist/api-routes/auth-callback.js +12 -5
  14. package/dist/api-routes/auth-logout.d.ts +4 -4
  15. package/dist/api-routes/auth-logout.js +8 -2
  16. package/dist/api-routes/auth-session.d.ts +6 -0
  17. package/dist/api-routes/auth-session.js +19 -19
  18. package/dist/api-routes/auth-setzkasten-login.js +14 -7
  19. package/dist/api-routes/catalog-add.js +59 -17
  20. package/dist/api-routes/catalog-export.js +14 -4
  21. package/dist/api-routes/config.d.ts +10 -3
  22. package/dist/api-routes/config.js +26 -4
  23. package/dist/api-routes/deploy-hook.js +8 -8
  24. package/dist/api-routes/editors.d.ts +1 -1
  25. package/dist/api-routes/editors.js +5 -2
  26. package/dist/api-routes/github-proxy.js +30 -8
  27. package/dist/api-routes/global-config.js +6 -3
  28. package/dist/api-routes/history-rollback.js +31 -14
  29. package/dist/api-routes/history-version.js +8 -6
  30. package/dist/api-routes/history.js +5 -2
  31. package/dist/api-routes/icons-local.js +1 -1
  32. package/dist/api-routes/init-add-section.js +113 -47
  33. package/dist/api-routes/init-apply.js +56 -42
  34. package/dist/api-routes/init-migrate.js +43 -36
  35. package/dist/api-routes/init-scan-page.d.ts +1 -1
  36. package/dist/api-routes/init-scan-page.js +59 -13
  37. package/dist/api-routes/init-scan.js +22 -7
  38. package/dist/api-routes/migrate-to-multi.js +5 -2
  39. package/dist/api-routes/pages.js +15 -4
  40. package/dist/api-routes/section-add.js +68 -16
  41. package/dist/api-routes/section-commit-pending.js +70 -22
  42. package/dist/api-routes/section-delete.js +49 -14
  43. package/dist/api-routes/section-duplicate.js +65 -16
  44. package/dist/api-routes/section-prepare-copy.js +15 -2
  45. package/dist/api-routes/section-prepare.js +25 -4
  46. package/dist/api-routes/setup-github-app-bounce.js +15 -1
  47. package/dist/api-routes/setup-github-app-branches.js +9 -6
  48. package/dist/api-routes/setup-github-app-callback.js +24 -1
  49. package/dist/api-routes/setup-github-app-credentials.d.ts +27 -0
  50. package/dist/api-routes/setup-github-app-credentials.js +43 -0
  51. package/dist/api-routes/setup-github-app-installed.js +22 -1
  52. package/dist/api-routes/setup-github-app-repos.js +5 -2
  53. package/dist/api-routes/setup-github-app.d.ts +4 -0
  54. package/dist/api-routes/setup-github-app.js +19 -2
  55. package/dist/api-routes/updater-register.js +7 -1
  56. package/dist/api-routes/webhooks-status.js +5 -2
  57. package/dist/api-routes/webhooks-test.js +9 -8
  58. package/dist/api-routes/webhooks.js +12 -14
  59. package/dist/api-routes/websites-add.js +5 -2
  60. package/dist/api-routes/websites-remove.js +5 -2
  61. package/dist/{chunk-ZQDGGWJP.js → chunk-5KMGSFCZ.js} +2 -2
  62. package/dist/{chunk-Q3N336KR.js → chunk-CDXCYYQR.js} +29 -24
  63. package/dist/{chunk-NKDATSPA.js → chunk-DP6RTINQ.js} +1 -1
  64. package/dist/chunk-KENFINT4.js +76 -0
  65. package/dist/chunk-ONP6BRZO.js +47 -0
  66. package/dist/{chunk-INIWFKQ3.js → chunk-Q5HV47DW.js} +33 -19
  67. package/dist/chunk-QVCW6EF3.js +26 -0
  68. package/dist/{chunk-TD76R3A6.js → chunk-UHI6323G.js} +293 -174
  69. package/dist/{chunk-AM4DZXXM.js → chunk-UJAFZEX2.js} +76 -9
  70. package/package.json +12 -6
  71. package/src/api-routes/__tests__/_session-signing.test.ts +114 -0
  72. package/src/api-routes/__tests__/_session-test-helper.ts +27 -0
  73. package/src/api-routes/__tests__/add-section-helpers.test.ts +59 -25
  74. package/src/api-routes/__tests__/auth-guard.test.ts +46 -7
  75. package/src/api-routes/__tests__/catalog-api.test.ts +14 -6
  76. package/src/api-routes/__tests__/commit-trailers.test.ts +5 -5
  77. package/src/api-routes/__tests__/deferred-operations.test.ts +9 -12
  78. package/src/api-routes/__tests__/deploy-hook.test.ts +3 -8
  79. package/src/api-routes/__tests__/feature-gate.test.ts +1 -1
  80. package/src/api-routes/__tests__/github-cache.test.ts +1 -1
  81. package/src/api-routes/__tests__/github-token.test.ts +1 -1
  82. package/src/api-routes/__tests__/global-config-theme.test.ts +4 -4
  83. package/src/api-routes/__tests__/history-rollback.test.ts +6 -3
  84. package/src/api-routes/__tests__/history.test.ts +9 -6
  85. package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +11 -3
  86. package/src/api-routes/__tests__/migrate-to-multi.test.ts +5 -1
  87. package/src/api-routes/__tests__/pages-meta-store.test.ts +10 -5
  88. package/src/api-routes/__tests__/pages.test.ts +7 -2
  89. package/src/api-routes/__tests__/patch-page-file.test.ts +71 -19
  90. package/src/api-routes/__tests__/route-registry.test.ts +11 -18
  91. package/src/api-routes/__tests__/scan-page-helpers.test.ts +13 -10
  92. package/src/api-routes/__tests__/section-management.test.ts +28 -28
  93. package/src/api-routes/__tests__/setup-github-app-callback.test.ts +58 -16
  94. package/src/api-routes/__tests__/setup-github-app-repos.test.ts +4 -5
  95. package/src/api-routes/__tests__/setup-github-app.test.ts +27 -7
  96. package/src/api-routes/__tests__/storage-config-for-request.test.ts +83 -0
  97. package/src/api-routes/__tests__/updater-register.test.ts +230 -0
  98. package/src/api-routes/__tests__/webhook-signing.test.ts +1 -1
  99. package/src/api-routes/__tests__/webhooks.test.ts +19 -7
  100. package/src/api-routes/__tests__/websites-add.test.ts +2 -1
  101. package/src/api-routes/__tests__/websites-remove.test.ts +2 -1
  102. package/src/api-routes/_auth-guard.ts +47 -15
  103. package/src/api-routes/_commit-trailers.ts +3 -2
  104. package/src/api-routes/_dev-session-secret.ts +79 -0
  105. package/src/api-routes/_github-token.ts +1 -1
  106. package/src/api-routes/_pages-meta-store.ts +2 -2
  107. package/src/api-routes/_role-resolver.ts +7 -5
  108. package/src/api-routes/_session-secret.ts +46 -0
  109. package/src/api-routes/_session-signing.ts +135 -0
  110. package/src/api-routes/_vercel-origin.ts +2 -6
  111. package/src/api-routes/_webhook-dispatcher.ts +12 -16
  112. package/src/api-routes/_website-resolver.ts +3 -10
  113. package/src/api-routes/auth-callback.ts +9 -5
  114. package/src/api-routes/auth-login.ts +5 -3
  115. package/src/api-routes/auth-logout.ts +18 -1
  116. package/src/api-routes/auth-session.ts +13 -21
  117. package/src/api-routes/auth-setzkasten-login.ts +12 -9
  118. package/src/api-routes/catalog-add.ts +89 -31
  119. package/src/api-routes/catalog-export.ts +30 -10
  120. package/src/api-routes/config.ts +39 -6
  121. package/src/api-routes/deploy-hook.ts +13 -11
  122. package/src/api-routes/editors.ts +33 -22
  123. package/src/api-routes/github-proxy.ts +25 -11
  124. package/src/api-routes/global-config.ts +103 -18
  125. package/src/api-routes/history-rollback.ts +41 -14
  126. package/src/api-routes/history-version.ts +5 -6
  127. package/src/api-routes/history.ts +3 -3
  128. package/src/api-routes/icons-local.ts +2 -2
  129. package/src/api-routes/init-add-section.ts +174 -79
  130. package/src/api-routes/init-apply.ts +71 -56
  131. package/src/api-routes/init-migrate.ts +54 -48
  132. package/src/api-routes/init-scan-page.ts +77 -30
  133. package/src/api-routes/init-scan.ts +19 -11
  134. package/src/api-routes/pages.ts +16 -11
  135. package/src/api-routes/section-add.ts +98 -27
  136. package/src/api-routes/section-commit-pending.ts +87 -34
  137. package/src/api-routes/section-delete.ts +76 -27
  138. package/src/api-routes/section-duplicate.ts +95 -28
  139. package/src/api-routes/section-management.ts +3 -7
  140. package/src/api-routes/section-prepare-copy.ts +29 -8
  141. package/src/api-routes/section-prepare.ts +38 -10
  142. package/src/api-routes/setup-github-app-bounce.ts +7 -1
  143. package/src/api-routes/setup-github-app-branches.ts +6 -7
  144. package/src/api-routes/setup-github-app-callback.ts +18 -1
  145. package/src/api-routes/setup-github-app-credentials.ts +55 -0
  146. package/src/api-routes/setup-github-app-installed.ts +12 -1
  147. package/src/api-routes/setup-github-app-repos.ts +2 -3
  148. package/src/api-routes/setup-github-app.ts +14 -5
  149. package/src/api-routes/updater-check.ts +6 -4
  150. package/src/api-routes/updater-register.ts +34 -20
  151. package/src/api-routes/updater-transfer.ts +8 -6
  152. package/src/api-routes/updater-unbind.ts +14 -10
  153. package/src/api-routes/webhooks-test.ts +9 -11
  154. package/src/api-routes/webhooks.ts +15 -19
  155. package/src/init/__tests__/page-level.test.ts +279 -105
  156. package/src/init/__tests__/page-list-coverage.test.ts +70 -70
  157. package/src/init/__tests__/patcher-child-component.test.ts +12 -3
  158. package/src/init/__tests__/patcher-edge-cases.test.ts +47 -23
  159. package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +16 -6
  160. package/src/init/__tests__/patcher-page-mode.test.ts +30 -20
  161. package/src/init/__tests__/section-pipeline.test.ts +53 -19
  162. package/src/init/astro-config-patcher.ts +4 -18
  163. package/src/init/astro-detector.ts +2 -7
  164. package/src/init/astro-section-analyzer-v2.ts +475 -193
  165. package/src/init/field-label-enricher.ts +6 -6
  166. package/src/init/template-patcher-v2.ts +218 -97
@@ -1,9 +1,9 @@
1
+ import { type CommitInfo, parseCoAuthorTrailers } from '@setzkasten-cms/core'
1
2
  import type { APIRoute } from 'astro'
2
- import { resolveStorageConfigForRequest } from './_storage-config'
3
- import { resolveGitHubTokenForRequest } from './_github-token'
4
3
  import { requireAdmin } from './_auth-guard'
5
- import { parseCoAuthorTrailers, type CommitInfo } from '@setzkasten-cms/core'
6
4
  import { cachedFetch } from './_github-cache'
5
+ import { resolveGitHubTokenForRequest } from './_github-token'
6
+ import { resolveStorageConfigForRequest } from './_storage-config'
7
7
 
8
8
  /**
9
9
  * GET /api/setzkasten/history?path=<contentPath>&before=<sha>
@@ -1,11 +1,11 @@
1
- import type { APIRoute } from 'astro'
2
1
  import {
3
2
  LOCAL_ICONS_DISCOVERY_PATHS,
4
3
  resolveLocalIconsPaths,
5
4
  sanitizeSvg,
6
5
  } from '@setzkasten-cms/core'
7
- import { resolveStorageConfigForRequest, prefixPath } from './_storage-config'
6
+ import type { APIRoute } from 'astro'
8
7
  import { resolveGitHubTokenForRequest } from './_github-token'
8
+ import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
9
9
 
10
10
  /**
11
11
  * GET /api/setzkasten/icons/local
@@ -1,11 +1,18 @@
1
- import type { APIRoute } from 'astro'
1
+ import { isSafeKey } from '@setzkasten-cms/core'
2
2
  import type { InferredSection } from '@setzkasten-cms/core/init'
3
3
  import { addSectionToConfig } from '@setzkasten-cms/core/init'
4
- import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
5
- import { patchTemplateForFields, stripTemplateFallbacks, detectChildImports, patchChildComponentForFieldPrefix } from '../init/template-patcher-v2'
4
+ import type { APIRoute } from 'astro'
6
5
  import type { RepeatedGroup } from '../init/analyzer-types'
6
+ import {
7
+ detectChildImports,
8
+ patchChildComponentForFieldPrefix,
9
+ patchTemplateForFields,
10
+ stripTemplateFallbacks,
11
+ } from '../init/template-patcher-v2'
12
+ import { requireAdmin } from './_auth-guard'
7
13
  import { withTrailers } from './_commit-trailers'
8
14
  import { resolveGitHubTokenForRequest } from './_github-token'
15
+ import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
9
16
 
10
17
  /**
11
18
  * POST /api/setzkasten/init/add-section
@@ -17,10 +24,8 @@ import { resolveGitHubTokenForRequest } from './_github-token'
17
24
  * Body: { owner, repo, branch?, projectRoot, section, pageKey, contentPath? }
18
25
  */
19
26
  export const POST: APIRoute = async ({ request, cookies }) => {
20
- const session = cookies.get('setzkasten_session')?.value
21
- if (!session) {
22
- return new Response('Unauthorized', { status: 401 })
23
- }
27
+ const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
28
+ if (denied) return denied
24
29
 
25
30
  const tokenResult = await resolveGitHubTokenForRequest(request)
26
31
  if (!tokenResult.ok) {
@@ -29,7 +34,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
29
34
  const githubToken = tokenResult.value
30
35
 
31
36
  try {
32
- const body = await request.json() as {
37
+ const body = (await request.json()) as {
33
38
  owner?: string
34
39
  repo?: string
35
40
  branch?: string
@@ -42,7 +47,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
42
47
 
43
48
  const storage = await resolveStorageConfigForRequest(request, body)
44
49
  if (!storage) {
45
- return Response.json({ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.' }, { status: 400 })
50
+ return Response.json(
51
+ {
52
+ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.',
53
+ },
54
+ { status: 400 },
55
+ )
46
56
  }
47
57
  const { owner, repo, branch, projectPrefix } = storage
48
58
  const serverConfig = (globalThis as any).__SETZKASTEN_CONFIG__
@@ -52,6 +62,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
52
62
  if (!section || !pageKey) {
53
63
  return Response.json({ error: 'section and pageKey are required' }, { status: 400 })
54
64
  }
65
+ if (!isSafeKey(pageKey)) {
66
+ return Response.json({ error: 'invalid pageKey' }, { status: 400 })
67
+ }
68
+ if (!isSafeKey(section.key)) {
69
+ return Response.json({ error: 'invalid section key' }, { status: 400 })
70
+ }
55
71
 
56
72
  const headers = {
57
73
  Authorization: `Bearer ${githubToken}`,
@@ -67,7 +83,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
67
83
  const existingConfig = await fetchFileContent(owner, repo, branch, configPath, githubToken)
68
84
 
69
85
  if (existingConfig) {
70
- const updatedConfig = addSectionToConfig(existingConfig, section.key, section, section.allFields)
86
+ const updatedConfig = addSectionToConfig(
87
+ existingConfig,
88
+ section.key,
89
+ section,
90
+ section.allFields,
91
+ )
71
92
  if (updatedConfig) {
72
93
  filesToCommit.push({ path: configPath, content: updatedConfig })
73
94
  }
@@ -75,13 +96,21 @@ export const POST: APIRoute = async ({ request, cookies }) => {
75
96
 
76
97
  // 2. Generate content JSON — merge new fields into existing data (if any)
77
98
  const sectionJsonPath = `${contentPath}/_sections/${section.key}.json`
78
- const existingSectionJson = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken)
99
+ const existingSectionJson = await fetchFileContent(
100
+ owner,
101
+ repo,
102
+ branch,
103
+ sectionJsonPath,
104
+ githubToken,
105
+ )
79
106
 
80
107
  let sectionData: Record<string, unknown> = {}
81
108
  if (existingSectionJson) {
82
109
  try {
83
110
  sectionData = JSON.parse(existingSectionJson)
84
- } catch { /* start fresh if invalid */ }
111
+ } catch {
112
+ /* start fresh if invalid */
113
+ }
85
114
  }
86
115
 
87
116
  // Only add values for fields that don't already exist
@@ -94,7 +123,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
94
123
  }
95
124
 
96
125
  // Reorder JSON keys to match template order (allFields if available, else fields)
97
- const orderedKeys = (section.allFields ?? section.fields).map(f => f.key)
126
+ const orderedKeys = (section.allFields ?? section.fields).map((f) => f.key)
98
127
  const orderedData: Record<string, unknown> = {}
99
128
  for (const key of orderedKeys) {
100
129
  if (key in sectionData) {
@@ -116,7 +145,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
116
145
  // 3. Update page config (add section to the end)
117
146
  const configKey = '_' + pageKey.replace(/\//g, '_')
118
147
  const pageConfigPath = `${contentPath}/pages/${configKey}.json`
119
- const existingPageConfig = await fetchFileContent(owner, repo, branch, pageConfigPath, githubToken)
148
+ const existingPageConfig = await fetchFileContent(
149
+ owner,
150
+ repo,
151
+ branch,
152
+ pageConfigPath,
153
+ githubToken,
154
+ )
120
155
 
121
156
  let pageConfig: { sections: Array<{ key: string; enabled: boolean }> }
122
157
  if (existingPageConfig) {
@@ -154,9 +189,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
154
189
  if (pageSource) resolvedPagePath = indexPath
155
190
  }
156
191
  if (pageSource) {
157
- const repeatedGroups: RepeatedGroup[] = (section as any)._analyzerResult?.repeatedGroups ?? []
192
+ const repeatedGroups: RepeatedGroup[] =
193
+ (section as any)._analyzerResult?.repeatedGroups ?? []
158
194
  let patchedSource = await patchTemplateForFields(
159
- pageSource, section.key, section.allFields ?? section.fields, repeatedGroups,
195
+ pageSource,
196
+ section.key,
197
+ section.allFields ?? section.fields,
198
+ repeatedGroups,
160
199
  { mode: 'page' },
161
200
  )
162
201
 
@@ -175,14 +214,19 @@ export const POST: APIRoute = async ({ request, cookies }) => {
175
214
  // analyzer's text-only defaultValue.
176
215
  const existingIsPlain = typeof existing === 'string' && !/<[a-z]/.test(existing)
177
216
  const newIsHtml = typeof value === 'string' && /<[a-z]/.test(value as string)
178
- if (!(key in sectionData) || existing === '' || existing === null || (existingIsPlain && newIsHtml)) {
217
+ if (
218
+ !(key in sectionData) ||
219
+ existing === '' ||
220
+ existing === null ||
221
+ (existingIsPlain && newIsHtml)
222
+ ) {
179
223
  sectionData[key] = value
180
224
  jsonUpdated = true
181
225
  }
182
226
  }
183
227
  // Update the content JSON entry if we added values
184
228
  if (jsonUpdated) {
185
- const jsonIdx = filesToCommit.findIndex(f => f.path === sectionJsonPath)
229
+ const jsonIdx = filesToCommit.findIndex((f) => f.path === sectionJsonPath)
186
230
  if (jsonIdx !== -1) {
187
231
  filesToCommit[jsonIdx]!.content = JSON.stringify(sectionData, null, 2)
188
232
  }
@@ -209,10 +253,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
209
253
  // Update content JSON with any _classN fields from patcher
210
254
  const fields = section.allFields ?? section.fields
211
255
  for (const g of repeatedGroups) {
212
- const topField = fields.find(f => f.key === g.fieldKey)
256
+ const topField = fields.find((f) => f.key === g.fieldKey)
213
257
  if (!topField || !Array.isArray(topField.defaultValue)) continue
214
- sectionData[g.fieldKey] = (topField.defaultValue as unknown[]).filter(item => item != null)
215
- const jsonIdx = filesToCommit.findIndex(f => f.path === sectionJsonPath)
258
+ sectionData[g.fieldKey] = (topField.defaultValue as unknown[]).filter(
259
+ (item) => item != null,
260
+ )
261
+ const jsonIdx = filesToCommit.findIndex((f) => f.path === sectionJsonPath)
216
262
  if (jsonIdx !== -1) {
217
263
  filesToCommit[jsonIdx]!.content = JSON.stringify(sectionData, null, 2)
218
264
  }
@@ -220,10 +266,22 @@ export const POST: APIRoute = async ({ request, cookies }) => {
220
266
  }
221
267
  // No patchPageFile / SECTION_COMPONENTS for page-level sections
222
268
  } else if (section.componentPath) {
223
- const componentSource = await fetchFileContent(owner, repo, branch, section.componentPath, githubToken)
269
+ const componentSource = await fetchFileContent(
270
+ owner,
271
+ repo,
272
+ branch,
273
+ section.componentPath,
274
+ githubToken,
275
+ )
224
276
  if (componentSource) {
225
- const repeatedGroups: RepeatedGroup[] = (section as any)._analyzerResult?.repeatedGroups ?? []
226
- const patchedSource = await patchTemplateForFields(componentSource, section.key, section.allFields ?? section.fields, repeatedGroups)
277
+ const repeatedGroups: RepeatedGroup[] =
278
+ (section as any)._analyzerResult?.repeatedGroups ?? []
279
+ const patchedSource = await patchTemplateForFields(
280
+ componentSource,
281
+ section.key,
282
+ section.allFields ?? section.fields,
283
+ repeatedGroups,
284
+ )
227
285
  if (patchedSource !== componentSource) {
228
286
  filesToCommit.push({ path: section.componentPath, content: patchedSource })
229
287
  }
@@ -233,15 +291,17 @@ export const POST: APIRoute = async ({ request, cookies }) => {
233
291
  // Re-read the now-mutated fields and update the content JSON entry.
234
292
  const fields = section.allFields ?? section.fields
235
293
  for (const g of repeatedGroups) {
236
- const topField = fields.find(f => f.key === g.fieldKey)
294
+ const topField = fields.find((f) => f.key === g.fieldKey)
237
295
  if (!topField || !Array.isArray(topField.defaultValue)) continue
238
- const items = (topField.defaultValue as Array<Record<string, unknown>>).filter(item => item != null)
296
+ const items = (topField.defaultValue as Array<Record<string, unknown>>).filter(
297
+ (item) => item != null,
298
+ )
239
299
 
240
300
  // Update sectionData with the enriched items array
241
301
  sectionData[g.fieldKey] = items
242
302
 
243
303
  // Rebuild and replace the content JSON in filesToCommit
244
- const jsonIdx = filesToCommit.findIndex(f => f.path === sectionJsonPath)
304
+ const jsonIdx = filesToCommit.findIndex((f) => f.path === sectionJsonPath)
245
305
  if (jsonIdx !== -1) {
246
306
  filesToCommit[jsonIdx]!.content = JSON.stringify(sectionData, null, 2)
247
307
  }
@@ -257,7 +317,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
257
317
  const sectionDir = section.componentPath.replace(/\/[^/]+$/, '')
258
318
  const resolvedChildPath = resolveRelativePath(sectionDir, child.importPath)
259
319
  if (!resolvedChildPath) continue
260
- const childSource = await fetchFileContent(owner, repo, branch, resolvedChildPath, githubToken)
320
+ const childSource = await fetchFileContent(
321
+ owner,
322
+ repo,
323
+ branch,
324
+ resolvedChildPath,
325
+ githubToken,
326
+ )
261
327
  if (!childSource) continue
262
328
  const patchedChild = patchChildComponentForFieldPrefix(childSource, child.innerFields)
263
329
  if (patchedChild !== childSource) {
@@ -271,7 +337,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
271
337
  const fullPagePath = prefixPath(body.pagePath, projectPrefix)
272
338
  const pageSource = await fetchFileContent(owner, repo, branch, fullPagePath, githubToken)
273
339
  if (pageSource) {
274
- const patched = patchPageFile(pageSource, section.key, section.componentName, section.componentPath, body.pagePath)
340
+ const patched = patchPageFile(
341
+ pageSource,
342
+ section.key,
343
+ section.componentName,
344
+ section.componentPath,
345
+ body.pagePath,
346
+ )
275
347
  if (patched && patched !== pageSource) {
276
348
  filesToCommit.push({ path: fullPagePath, content: patched })
277
349
  }
@@ -282,7 +354,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
282
354
  const previewPath = prefixPath(`${pageDir}/sk-preview/[...page].astro`, projectPrefix)
283
355
  const previewSource = await fetchFileContent(owner, repo, branch, previewPath, githubToken)
284
356
  if (previewSource) {
285
- const patchedPreview = patchPageFile(previewSource, section.key, section.componentName, section.componentPath, `${pageDir}/sk-preview/[...page].astro`)
357
+ const patchedPreview = patchPageFile(
358
+ previewSource,
359
+ section.key,
360
+ section.componentName,
361
+ section.componentPath,
362
+ `${pageDir}/sk-preview/[...page].astro`,
363
+ )
286
364
  if (patchedPreview && patchedPreview !== previewSource) {
287
365
  filesToCommit.push({ path: previewPath, content: patchedPreview })
288
366
  }
@@ -300,9 +378,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
300
378
  repo,
301
379
  branch,
302
380
  filesToCommit,
303
- withTrailers(existingSectionJson
304
- ? `content: update ${section.key} section — add new fields`
305
- : `content: add ${section.key} section to Setzkasten`),
381
+ withTrailers(
382
+ existingSectionJson
383
+ ? `content: update ${section.key} section add new fields`
384
+ : `content: add ${section.key} section to Setzkasten`,
385
+ ),
306
386
  headers,
307
387
  )
308
388
 
@@ -332,15 +412,24 @@ export const POST: APIRoute = async ({ request, cookies }) => {
332
412
 
333
413
  function getDefaultValue(fieldType: string): unknown {
334
414
  switch (fieldType) {
335
- case 'text': return ''
336
- case 'number': return 0
337
- case 'boolean': return false
338
- case 'image': return { path: '', alt: '' }
339
- case 'array': return []
340
- case 'color': return '#000000'
341
- case 'date': return ''
342
- case 'icon': return ''
343
- default: return ''
415
+ case 'text':
416
+ return ''
417
+ case 'number':
418
+ return 0
419
+ case 'boolean':
420
+ return false
421
+ case 'image':
422
+ return { path: '', alt: '' }
423
+ case 'array':
424
+ return []
425
+ case 'color':
426
+ return '#000000'
427
+ case 'date':
428
+ return ''
429
+ case 'icon':
430
+ return ''
431
+ default:
432
+ return ''
344
433
  }
345
434
  }
346
435
 
@@ -396,7 +485,11 @@ export function patchPageFile(
396
485
  // Find the last entry and add after it
397
486
  const lastEntryMatch = registryContent.match(/.*\w+Section,?\s*$/m)
398
487
  if (lastEntryMatch && lastEntryMatch.index !== undefined) {
399
- const insertPos = registryMatch.index! + registryMatch[0].indexOf(registryContent) + lastEntryMatch.index + lastEntryMatch[0].length
488
+ const insertPos =
489
+ registryMatch.index! +
490
+ registryMatch[0].indexOf(registryContent) +
491
+ lastEntryMatch.index +
492
+ lastEntryMatch[0].length
400
493
  const newEntry = `\n '${sectionKey}': ${componentName},`
401
494
  patched = patched.slice(0, insertPos) + newEntry + patched.slice(insertPos)
402
495
  }
@@ -419,7 +512,11 @@ export function calculateRelativePath(fromDir: string, toPath: string): string {
419
512
 
420
513
  // Find common prefix length
421
514
  let common = 0
422
- while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
515
+ while (
516
+ common < fromParts.length &&
517
+ common < toParts.length &&
518
+ fromParts[common] === toParts[common]
519
+ ) {
423
520
  common++
424
521
  }
425
522
 
@@ -440,7 +537,10 @@ function resolveRelativePath(baseDir: string, relativePath: string): string | nu
440
537
  const parts = [...baseDir.split('/').filter(Boolean)]
441
538
  for (const segment of relativePath.split('/')) {
442
539
  if (segment === '.') continue
443
- if (segment === '..') { parts.pop(); continue }
540
+ if (segment === '..') {
541
+ parts.pop()
542
+ continue
543
+ }
444
544
  parts.push(segment)
445
545
  }
446
546
  const resolved = parts.join('/')
@@ -468,7 +568,7 @@ async function fetchFileContent(
468
568
  },
469
569
  )
470
570
  if (!response.ok) return null
471
- const data = await response.json() as { content: string; encoding: string }
571
+ const data = (await response.json()) as { content: string; encoding: string }
472
572
  return data.encoding === 'base64'
473
573
  ? Buffer.from(data.content, 'base64').toString('utf-8')
474
574
  : data.content
@@ -492,7 +592,7 @@ async function batchCommit(
492
592
  { headers },
493
593
  )
494
594
  if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
495
- const refData = await refRes.json() as { object: { sha: string } }
595
+ const refData = (await refRes.json()) as { object: { sha: string } }
496
596
  const headSha = refData.object.sha
497
597
 
498
598
  // 2. Get base tree
@@ -501,43 +601,38 @@ async function batchCommit(
501
601
  { headers },
502
602
  )
503
603
  if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
504
- const commitData = await commitRes.json() as { tree: { sha: string } }
604
+ const commitData = (await commitRes.json()) as { tree: { sha: string } }
505
605
 
506
606
  // 3. Create new tree
507
- const treeRes = await fetch(
508
- `https://api.github.com/repos/${owner}/${repo}/git/trees`,
509
- {
510
- method: 'POST',
511
- headers,
512
- body: JSON.stringify({
513
- base_tree: commitData.tree.sha,
514
- tree: files.map((f) => ({
515
- path: f.path,
516
- mode: '100644',
517
- type: 'blob',
518
- content: f.content,
519
- })),
520
- }),
521
- },
522
- )
607
+ const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
608
+ method: 'POST',
609
+ headers,
610
+ body: JSON.stringify({
611
+ base_tree: commitData.tree.sha,
612
+ tree: files.map((f) => ({
613
+ path: f.path,
614
+ mode: '100644',
615
+ type: 'blob',
616
+ content: f.content,
617
+ })),
618
+ }),
619
+ })
523
620
  if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
524
- const treeData = await treeRes.json() as { sha: string }
621
+ const treeData = (await treeRes.json()) as { sha: string }
525
622
 
526
623
  // 4. Create commit
527
- const newCommitRes = await fetch(
528
- `https://api.github.com/repos/${owner}/${repo}/git/commits`,
529
- {
530
- method: 'POST',
531
- headers,
532
- body: JSON.stringify({
533
- tree: treeData.sha,
534
- parents: [headSha],
535
- message,
536
- }),
537
- },
538
- )
539
- if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
540
- const newCommitData = await newCommitRes.json() as { sha: string }
624
+ const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
625
+ method: 'POST',
626
+ headers,
627
+ body: JSON.stringify({
628
+ tree: treeData.sha,
629
+ parents: [headSha],
630
+ message,
631
+ }),
632
+ })
633
+ if (!newCommitRes.ok)
634
+ return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
635
+ const newCommitData = (await newCommitRes.json()) as { sha: string }
541
636
 
542
637
  // 5. Update ref
543
638
  const updateRes = await fetch(
@@ -1,7 +1,12 @@
1
+ import {
2
+ type ConfigGeneratorInput,
3
+ type InferredSection,
4
+ generateConfigFile,
5
+ } from '@setzkasten-cms/core/init'
1
6
  import type { APIRoute } from 'astro'
2
- import { generateConfigFile, type InferredSection, type ConfigGeneratorInput } from '@setzkasten-cms/core/init'
3
7
  import { patchAstroConfig } from '../init/astro-config-patcher'
4
8
  import { patchTemplateForFields } from '../init/template-patcher-v2'
9
+ import { requireAdmin } from './_auth-guard'
5
10
  import { withTrailers } from './_commit-trailers'
6
11
  import { resolveGitHubTokenForRequest } from './_github-token'
7
12
 
@@ -31,11 +36,8 @@ interface FileToCommit {
31
36
  * Body: ApplyRequest
32
37
  */
33
38
  export const POST: APIRoute = async ({ request, cookies }) => {
34
- // Verify session
35
- const session = cookies.get('setzkasten_session')?.value
36
- if (!session) {
37
- return new Response('Unauthorized', { status: 401 })
38
- }
39
+ const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
40
+ if (denied) return denied
39
41
 
40
42
  const tokenResult = await resolveGitHubTokenForRequest(request)
41
43
  if (!tokenResult.ok) {
@@ -44,7 +46,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
44
46
  const githubToken = tokenResult.value
45
47
 
46
48
  try {
47
- const body = await request.json() as ApplyRequest
49
+ const body = (await request.json()) as ApplyRequest
48
50
  const { owner, repo, branch = 'main', projectRoot, astroConfigPath, sections, pages } = body
49
51
  const contentPath = body.contentPath ?? 'content'
50
52
 
@@ -62,7 +64,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
62
64
 
63
65
  // 2. Patch astro.config if needed
64
66
  if (astroConfigPath) {
65
- const astroConfigSource = await fetchFileContent(owner, repo, branch, astroConfigPath, githubToken)
67
+ const astroConfigSource = await fetchFileContent(
68
+ owner,
69
+ repo,
70
+ branch,
71
+ astroConfigPath,
72
+ githubToken,
73
+ )
66
74
  if (astroConfigSource) {
67
75
  const patched = patchAstroConfig(astroConfigSource)
68
76
  if (patched) {
@@ -87,7 +95,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
87
95
  // 4. Patch component templates — add data-sk-field + CMS variables
88
96
  for (const section of sections) {
89
97
  if (!section.componentPath) continue
90
- const componentSource = await fetchFileContent(owner, repo, branch, section.componentPath, githubToken)
98
+ const componentSource = await fetchFileContent(
99
+ owner,
100
+ repo,
101
+ branch,
102
+ section.componentPath,
103
+ githubToken,
104
+ )
91
105
  if (!componentSource) continue
92
106
  const patched = await patchTemplateForFields(componentSource, section.key, section.fields)
93
107
  if (patched !== componentSource) {
@@ -123,10 +137,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
123
137
  )
124
138
 
125
139
  if (!commitResult.ok) {
126
- return Response.json(
127
- { error: commitResult.error },
128
- { status: 500 },
129
- )
140
+ return Response.json({ error: commitResult.error }, { status: 500 })
130
141
  }
131
142
 
132
143
  return Response.json({
@@ -145,15 +156,24 @@ export const POST: APIRoute = async ({ request, cookies }) => {
145
156
 
146
157
  function getDefaultValue(fieldType: string): unknown {
147
158
  switch (fieldType) {
148
- case 'text': return ''
149
- case 'number': return 0
150
- case 'boolean': return false
151
- case 'image': return { path: '', alt: '' }
152
- case 'array': return []
153
- case 'color': return '#000000'
154
- case 'date': return ''
155
- case 'icon': return ''
156
- default: return ''
159
+ case 'text':
160
+ return ''
161
+ case 'number':
162
+ return 0
163
+ case 'boolean':
164
+ return false
165
+ case 'image':
166
+ return { path: '', alt: '' }
167
+ case 'array':
168
+ return []
169
+ case 'color':
170
+ return '#000000'
171
+ case 'date':
172
+ return ''
173
+ case 'icon':
174
+ return ''
175
+ default:
176
+ return ''
157
177
  }
158
178
  }
159
179
 
@@ -176,7 +196,7 @@ async function fetchFileContent(
176
196
  },
177
197
  )
178
198
  if (!response.ok) return null
179
- const data = await response.json() as { content: string; encoding: string }
199
+ const data = (await response.json()) as { content: string; encoding: string }
180
200
  return data.encoding === 'base64'
181
201
  ? Buffer.from(data.content, 'base64').toString('utf-8')
182
202
  : data.content
@@ -207,7 +227,7 @@ async function batchCommit(
207
227
  { headers },
208
228
  )
209
229
  if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
210
- const refData = await refRes.json() as { object: { sha: string } }
230
+ const refData = (await refRes.json()) as { object: { sha: string } }
211
231
  const headSha = refData.object.sha
212
232
 
213
233
  // 2. Get base tree
@@ -216,44 +236,39 @@ async function batchCommit(
216
236
  { headers },
217
237
  )
218
238
  if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
219
- const commitData = await commitRes.json() as { tree: { sha: string } }
239
+ const commitData = (await commitRes.json()) as { tree: { sha: string } }
220
240
  const baseTreeSha = commitData.tree.sha
221
241
 
222
242
  // 3. Create new tree
223
- const treeRes = await fetch(
224
- `https://api.github.com/repos/${owner}/${repo}/git/trees`,
225
- {
226
- method: 'POST',
227
- headers,
228
- body: JSON.stringify({
229
- base_tree: baseTreeSha,
230
- tree: files.map((f) => ({
231
- path: f.path,
232
- mode: '100644',
233
- type: 'blob',
234
- content: f.content,
235
- })),
236
- }),
237
- },
238
- )
243
+ const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
244
+ method: 'POST',
245
+ headers,
246
+ body: JSON.stringify({
247
+ base_tree: baseTreeSha,
248
+ tree: files.map((f) => ({
249
+ path: f.path,
250
+ mode: '100644',
251
+ type: 'blob',
252
+ content: f.content,
253
+ })),
254
+ }),
255
+ })
239
256
  if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
240
- const treeData = await treeRes.json() as { sha: string }
257
+ const treeData = (await treeRes.json()) as { sha: string }
241
258
 
242
259
  // 4. Create commit
243
- const newCommitRes = await fetch(
244
- `https://api.github.com/repos/${owner}/${repo}/git/commits`,
245
- {
246
- method: 'POST',
247
- headers,
248
- body: JSON.stringify({
249
- tree: treeData.sha,
250
- parents: [headSha],
251
- message,
252
- }),
253
- },
254
- )
255
- if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
256
- const newCommitData = await newCommitRes.json() as { sha: string }
260
+ const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
261
+ method: 'POST',
262
+ headers,
263
+ body: JSON.stringify({
264
+ tree: treeData.sha,
265
+ parents: [headSha],
266
+ message,
267
+ }),
268
+ })
269
+ if (!newCommitRes.ok)
270
+ return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
271
+ const newCommitData = (await newCommitRes.json()) as { sha: string }
257
272
 
258
273
  // 5. Update ref
259
274
  const updateRes = await fetch(