@setzkasten-cms/astro-admin 1.4.2 → 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 +150 -48
  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-RHJONMLK.js → chunk-CDXCYYQR.js} +222 -5
  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-K22A4ZBS.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 +91 -97
  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 +218 -88
  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 +126 -0
  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 +102 -16
  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 +490 -56
@@ -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(
@@ -1,9 +1,10 @@
1
- import type { APIRoute } from 'astro'
2
1
  import type { SetzKastenConfig } from '@setzkasten-cms/core'
3
- import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
2
+ import type { APIRoute } from 'astro'
4
3
  import { patchTemplateForFields } from '../init/template-patcher-v2'
4
+ import { requireAdmin } from './_auth-guard'
5
5
  import { withTrailers } from './_commit-trailers'
6
6
  import { resolveGitHubTokenForRequest } from './_github-token'
7
+ import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
7
8
 
8
9
  /**
9
10
  * POST /api/setzkasten/init/migrate
@@ -17,10 +18,8 @@ import { resolveGitHubTokenForRequest } from './_github-token'
17
18
  * Returns: { commitSha, patchedSource, originalSource }
18
19
  */
19
20
  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
- }
21
+ const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
22
+ if (denied) return denied
24
23
 
25
24
  const tokenResult = await resolveGitHubTokenForRequest(request)
26
25
  if (!tokenResult.ok) {
@@ -29,7 +28,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
29
28
  const githubToken = tokenResult.value
30
29
 
31
30
  try {
32
- const body = await request.json() as {
31
+ const body = (await request.json()) as {
33
32
  owner?: string
34
33
  repo?: string
35
34
  branch?: string
@@ -39,7 +38,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
39
38
 
40
39
  const storage = await resolveStorageConfigForRequest(request, body)
41
40
  if (!storage) {
42
- return Response.json({ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.' }, { status: 400 })
41
+ return Response.json(
42
+ {
43
+ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.',
44
+ },
45
+ { status: 400 },
46
+ )
43
47
  }
44
48
  const { owner, repo, branch, projectPrefix } = storage
45
49
  const { sectionKey } = body
@@ -73,14 +77,20 @@ export const POST: APIRoute = async ({ request, cookies }) => {
73
77
  const sectionJsonPath = `${contentPath}/_sections/${sectionKey}.json`
74
78
  const sectionJson = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken)
75
79
 
76
- const fullConfig = (globalThis as any).__SETZKASTEN_FULL_CONFIG__ as SetzKastenConfig | undefined
80
+ const fullConfig = (globalThis as any).__SETZKASTEN_FULL_CONFIG__ as
81
+ | SetzKastenConfig
82
+ | undefined
77
83
  const fieldDefs = getFieldDefs(fullConfig, sectionKey)
78
84
 
79
85
  // Build fields: combine config field types with content JSON values
80
86
  const fields: Array<{ key: string; type: string; defaultValue?: unknown }> = []
81
87
  let contentData: Record<string, unknown> = {}
82
88
  if (sectionJson) {
83
- try { contentData = JSON.parse(sectionJson) } catch { /* ignore */ }
89
+ try {
90
+ contentData = JSON.parse(sectionJson)
91
+ } catch {
92
+ /* ignore */
93
+ }
84
94
  }
85
95
 
86
96
  // Add fields from config definitions
@@ -155,10 +165,11 @@ function getFieldDefs(
155
165
  }
156
166
 
157
167
  function deriveComponentPath(sectionKey: string): string {
158
- const componentName = sectionKey
159
- .split('-')
160
- .map(s => s.charAt(0).toUpperCase() + s.slice(1))
161
- .join('') + 'Section'
168
+ const componentName =
169
+ sectionKey
170
+ .split('-')
171
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
172
+ .join('') + 'Section'
162
173
  return `src/components/sections/${componentName}.astro`
163
174
  }
164
175
 
@@ -181,7 +192,7 @@ async function fetchFileContent(
181
192
  },
182
193
  )
183
194
  if (!response.ok) return null
184
- const data = await response.json() as { content: string; encoding: string }
195
+ const data = (await response.json()) as { content: string; encoding: string }
185
196
  return data.encoding === 'base64'
186
197
  ? Buffer.from(data.content, 'base64').toString('utf-8')
187
198
  : data.content
@@ -204,7 +215,7 @@ async function batchCommit(
204
215
  { headers },
205
216
  )
206
217
  if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
207
- const refData = await refRes.json() as { object: { sha: string } }
218
+ const refData = (await refRes.json()) as { object: { sha: string } }
208
219
  const headSha = refData.object.sha
209
220
 
210
221
  const commitRes = await fetch(
@@ -212,41 +223,36 @@ async function batchCommit(
212
223
  { headers },
213
224
  )
214
225
  if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
215
- const commitData = await commitRes.json() as { tree: { sha: string } }
226
+ const commitData = (await commitRes.json()) as { tree: { sha: string } }
216
227
 
217
- const treeRes = await fetch(
218
- `https://api.github.com/repos/${owner}/${repo}/git/trees`,
219
- {
220
- method: 'POST',
221
- headers,
222
- body: JSON.stringify({
223
- base_tree: commitData.tree.sha,
224
- tree: files.map((f) => ({
225
- path: f.path,
226
- mode: '100644',
227
- type: 'blob',
228
- content: f.content,
229
- })),
230
- }),
231
- },
232
- )
228
+ const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
229
+ method: 'POST',
230
+ headers,
231
+ body: JSON.stringify({
232
+ base_tree: commitData.tree.sha,
233
+ tree: files.map((f) => ({
234
+ path: f.path,
235
+ mode: '100644',
236
+ type: 'blob',
237
+ content: f.content,
238
+ })),
239
+ }),
240
+ })
233
241
  if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
234
- const treeData = await treeRes.json() as { sha: string }
242
+ const treeData = (await treeRes.json()) as { sha: string }
235
243
 
236
- const newCommitRes = await fetch(
237
- `https://api.github.com/repos/${owner}/${repo}/git/commits`,
238
- {
239
- method: 'POST',
240
- headers,
241
- body: JSON.stringify({
242
- tree: treeData.sha,
243
- parents: [headSha],
244
- message,
245
- }),
246
- },
247
- )
248
- if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
249
- const newCommitData = await newCommitRes.json() as { sha: string }
244
+ const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
245
+ method: 'POST',
246
+ headers,
247
+ body: JSON.stringify({
248
+ tree: treeData.sha,
249
+ parents: [headSha],
250
+ message,
251
+ }),
252
+ })
253
+ if (!newCommitRes.ok)
254
+ return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
255
+ const newCommitData = (await newCommitRes.json()) as { sha: string }
250
256
 
251
257
  const updateRes = await fetch(
252
258
  `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
@@ -1,10 +1,11 @@
1
- import type { APIRoute } from 'astro'
2
1
  import type { SetzKastenConfig } from '@setzkasten-cms/core'
3
- import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
4
- import { extractSectionImports, extractLayoutImport } from '../init/astro-detector'
5
- import { analyzeAstroSection } from '../init/astro-section-analyzer-v2'
6
2
  import type { RepoFile } from '@setzkasten-cms/core/init'
3
+ import type { APIRoute } from 'astro'
4
+ import { extractLayoutImport, extractSectionImports } from '../init/astro-detector'
5
+ import { analyzeAstroSection } from '../init/astro-section-analyzer-v2'
6
+ import { requireAdmin } from './_auth-guard'
7
7
  import { resolveGitHubTokenForRequest } from './_github-token'
8
+ import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
8
9
 
9
10
  // Build-time constant injected by the Vite define plugin — always available in
10
11
  // compiled API routes (unlike page-ssr injectScript which only runs for SSR pages).
@@ -20,8 +21,11 @@ declare const __SETZKASTEN_FULL_CONFIG__: SetzKastenConfig | null | undefined
20
21
  * (including _layout_header / _layout_footer) for re-adoption.
21
22
  */
22
23
  export function resolveFullConfig(): SetzKastenConfig | undefined {
23
- const buildConfig = typeof __SETZKASTEN_FULL_CONFIG__ !== 'undefined' ? __SETZKASTEN_FULL_CONFIG__ : null
24
- return (buildConfig ?? (globalThis as any).__SETZKASTEN_FULL_CONFIG__) as SetzKastenConfig | undefined
24
+ const buildConfig =
25
+ typeof __SETZKASTEN_FULL_CONFIG__ !== 'undefined' ? __SETZKASTEN_FULL_CONFIG__ : null
26
+ return (buildConfig ?? (globalThis as any).__SETZKASTEN_FULL_CONFIG__) as
27
+ | SetzKastenConfig
28
+ | undefined
25
29
  }
26
30
 
27
31
  /**
@@ -35,10 +39,8 @@ export function resolveFullConfig(): SetzKastenConfig | undefined {
35
39
  * Returns: { unmanagedSections: InferredSection[], managedUpdates: ManagedUpdate[] }
36
40
  */
37
41
  export const POST: APIRoute = async ({ request, cookies }) => {
38
- const session = cookies.get('setzkasten_session')?.value
39
- if (!session) {
40
- return new Response('Unauthorized', { status: 401 })
41
- }
42
+ const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
43
+ if (denied) return denied
42
44
 
43
45
  const tokenResult = await resolveGitHubTokenForRequest(request)
44
46
  if (!tokenResult.ok) {
@@ -47,7 +49,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
47
49
  const githubToken = tokenResult.value
48
50
 
49
51
  try {
50
- const body = await request.json() as {
52
+ const body = (await request.json()) as {
51
53
  owner?: string
52
54
  repo?: string
53
55
  branch?: string
@@ -57,7 +59,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
57
59
 
58
60
  const storage = await resolveStorageConfigForRequest(request, body)
59
61
  if (!storage) {
60
- return Response.json({ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.' }, { status: 400 })
62
+ return Response.json(
63
+ {
64
+ error: 'Could not resolve owner/repo. Set SETZKASTEN_OWNER and SETZKASTEN_REPO env vars.',
65
+ },
66
+ { status: 400 },
67
+ )
61
68
  }
62
69
  const { owner, repo, branch, projectPrefix } = storage
63
70
  const { pagePath } = body
@@ -89,7 +96,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
89
96
  )
90
97
  }
91
98
 
92
- const treeData = await treeResponse.json() as {
99
+ const treeData = (await treeResponse.json()) as {
93
100
  tree: Array<{ path: string; type: string }>
94
101
  }
95
102
  const files: RepoFile[] = treeData.tree.map((item) => ({
@@ -108,7 +115,10 @@ export const POST: APIRoute = async ({ request, cookies }) => {
108
115
  pageSource = await fetchFileContent(owner, repo, branch, fullPagePath, githubToken)
109
116
  }
110
117
  if (!pageSource) {
111
- return Response.json({ error: `Could not read page source: ${fullPagePath}` }, { status: 404 })
118
+ return Response.json(
119
+ { error: `Could not read page source: ${fullPagePath}` },
120
+ { status: 404 },
121
+ )
112
122
  }
113
123
 
114
124
  // Extract section imports
@@ -131,7 +141,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
131
141
 
132
142
  if (!managedSections.has(imp.sectionKey)) {
133
143
  // Unmanaged section — full analysis
134
- const sectionSource = await fetchFileContent(owner, repo, branch, imp.resolvedPath, githubToken)
144
+ const sectionSource = await fetchFileContent(
145
+ owner,
146
+ repo,
147
+ branch,
148
+ imp.resolvedPath,
149
+ githubToken,
150
+ )
135
151
  if (!sectionSource) continue
136
152
 
137
153
  const section = await analyzeAstroSection(
@@ -144,7 +160,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
144
160
  } else {
145
161
  // Managed section — check for missing fields
146
162
  const existingFieldKeys = managedSections.get(imp.sectionKey)!
147
- const sectionSource = await fetchFileContent(owner, repo, branch, imp.resolvedPath, githubToken)
163
+ const sectionSource = await fetchFileContent(
164
+ owner,
165
+ repo,
166
+ branch,
167
+ imp.resolvedPath,
168
+ githubToken,
169
+ )
148
170
  if (!sectionSource) continue
149
171
 
150
172
  const inferred = await analyzeAstroSection(
@@ -157,7 +179,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
157
179
  // Load existing content to filter out fields whose value already exists
158
180
  // (e.g. template has ctaText="GitHub öffnen" but schema has buttonText with same value)
159
181
  const sectionJsonPath = `${contentPath}/_sections/${imp.sectionKey}.json`
160
- const sectionJson = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken)
182
+ const sectionJson = await fetchFileContent(
183
+ owner,
184
+ repo,
185
+ branch,
186
+ sectionJsonPath,
187
+ githubToken,
188
+ )
161
189
  const existingValues = new Set<string>()
162
190
  if (sectionJson) {
163
191
  try {
@@ -165,11 +193,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
165
193
  for (const val of Object.values(data)) {
166
194
  if (typeof val === 'string' && val.length >= 2) existingValues.add(val)
167
195
  }
168
- } catch { /* ignore parse errors */ }
196
+ } catch {
197
+ /* ignore parse errors */
198
+ }
169
199
  }
170
200
 
171
201
  // Find fields that aren't in the schema AND whose value isn't already stored
172
- const missingFields = inferred.fields.filter(f => {
202
+ const missingFields = inferred.fields.filter((f) => {
173
203
  if (existingFieldKeys.has(f.key)) return false
174
204
  if (typeof f.defaultValue === 'string' && existingValues.has(f.defaultValue)) return false
175
205
  return true
@@ -189,10 +219,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
189
219
  // Always analyze page-level inline content (in addition to imported sections).
190
220
  // A page can have imported sections AND inline content (headings, text, arrays).
191
221
  {
192
- const pageKeyNorm = pagePath
193
- .replace(/^src\/pages\//, '')
194
- .replace(/\/(index)?\.astro$/, '')
195
- .replace(/\.astro$/, '') || 'index'
222
+ const pageKeyNorm =
223
+ pagePath
224
+ .replace(/^src\/pages\//, '')
225
+ .replace(/\/(index)?\.astro$/, '')
226
+ .replace(/\.astro$/, '') || 'index'
196
227
  const sectionKey = '_page_' + pageKeyNorm.replace(/\//g, '_')
197
228
 
198
229
  const pageSection = await analyzeAstroSection(
@@ -213,7 +244,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
213
244
  // Already in config — check for new unbound fields
214
245
  const existingFieldKeys = managedSections.get(sectionKey)!
215
246
  const sectionJsonPath = `${contentPath}/_sections/${sectionKey}.json`
216
- const sectionJson = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken)
247
+ const sectionJson = await fetchFileContent(
248
+ owner,
249
+ repo,
250
+ branch,
251
+ sectionJsonPath,
252
+ githubToken,
253
+ )
217
254
  const existingValues = new Set<string>()
218
255
  if (sectionJson) {
219
256
  try {
@@ -221,9 +258,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
221
258
  for (const val of Object.values(data)) {
222
259
  if (typeof val === 'string' && val.length >= 2) existingValues.add(val)
223
260
  }
224
- } catch { /* ignore */ }
261
+ } catch {
262
+ /* ignore */
263
+ }
225
264
  }
226
- const missingFields = pageSection.fields.filter(f => {
265
+ const missingFields = pageSection.fields.filter((f) => {
227
266
  if (existingFieldKeys.has(f.key)) return false
228
267
  if (typeof f.defaultValue === 'string' && existingValues.has(f.defaultValue)) return false
229
268
  return true
@@ -246,7 +285,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
246
285
  {
247
286
  const layoutImport = extractLayoutImport(pageSource, fullPagePath, files, projectPrefix)
248
287
  if (layoutImport?.resolvedPath) {
249
- const layoutSource = await fetchFileContent(owner, repo, branch, layoutImport.resolvedPath, githubToken)
288
+ const layoutSource = await fetchFileContent(
289
+ owner,
290
+ repo,
291
+ branch,
292
+ layoutImport.resolvedPath,
293
+ githubToken,
294
+ )
250
295
  if (layoutSource) {
251
296
  for (const region of extractLayoutRegions(layoutSource)) {
252
297
  const sectionKey = `_layout_${region.name}`
@@ -255,8 +300,10 @@ export const POST: APIRoute = async ({ request, cookies }) => {
255
300
  // Wrap the region in a minimal Astro file for the analyzer
256
301
  const regionSource = `---\n---\n\n${region.html}`
257
302
  const section = await analyzeAstroSection(
258
- regionSource, sectionKey,
259
- region.name, layoutImport.resolvedPath,
303
+ regionSource,
304
+ sectionKey,
305
+ region.name,
306
+ layoutImport.resolvedPath,
260
307
  { mode: 'page' },
261
308
  )
262
309
  ;(section as any).isPageLevel = true
@@ -346,7 +393,7 @@ async function fetchFileContent(
346
393
  token,
347
394
  )
348
395
  if (!response.ok) return null
349
- const data = await response.json() as { content: string; encoding: string }
396
+ const data = (await response.json()) as { content: string; encoding: string }
350
397
  return data.encoding === 'base64'
351
398
  ? Buffer.from(data.content, 'base64').toString('utf-8')
352
399
  : data.content
@@ -1,7 +1,8 @@
1
+ import { type RepoFile, analyzeProject } from '@setzkasten-cms/core/init'
1
2
  import type { APIRoute } from 'astro'
2
- import { analyzeProject, type RepoFile } from '@setzkasten-cms/core/init'
3
- import { findAstroPages, extractSectionImports } from '../init/astro-detector'
3
+ import { extractSectionImports, findAstroPages } from '../init/astro-detector'
4
4
  import { analyzeAstroSection } from '../init/astro-section-analyzer-v2'
5
+ import { requireAdmin } from './_auth-guard'
5
6
  import { resolveGitHubTokenForRequest } from './_github-token'
6
7
 
7
8
  /**
@@ -11,11 +12,12 @@ import { resolveGitHubTokenForRequest } from './_github-token'
11
12
  * Body: { owner: string, repo: string, branch?: string }
12
13
  */
13
14
  export const POST: APIRoute = async ({ request, cookies }) => {
14
- // Verify session
15
- const session = cookies.get('setzkasten_session')?.value
16
- if (!session) {
17
- return new Response('Unauthorized', { status: 401 })
18
- }
15
+ // Init wizard is admin-only — editors don't run it, and the route mints
16
+ // App-installation tokens against arbitrary repos from the body. Cookie-
17
+ // presence check used to be the only gate, which let any editor (or any
18
+ // forger pre-C1) probe / scan installations.
19
+ const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
20
+ if (denied) return denied
19
21
 
20
22
  const tokenResult = await resolveGitHubTokenForRequest(request)
21
23
  if (!tokenResult.ok) {
@@ -24,7 +26,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
24
26
  const githubToken = tokenResult.value
25
27
 
26
28
  try {
27
- const body = await request.json() as { owner: string; repo: string; branch?: string }
29
+ const body = (await request.json()) as { owner: string; repo: string; branch?: string }
28
30
  const { owner, repo, branch = 'main' } = body
29
31
 
30
32
  if (!owner || !repo) {
@@ -44,7 +46,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
44
46
  )
45
47
  }
46
48
 
47
- const treeData = await treeResponse.json() as {
49
+ const treeData = (await treeResponse.json()) as {
48
50
  tree: Array<{ path: string; type: string; sha: string }>
49
51
  }
50
52
 
@@ -88,7 +90,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
88
90
  if (allSections.has(imp.sectionKey)) continue
89
91
  if (!imp.resolvedPath) continue
90
92
 
91
- const sectionSource = await fetchFileContent(owner, repo, branch, imp.resolvedPath, githubToken)
93
+ const sectionSource = await fetchFileContent(
94
+ owner,
95
+ repo,
96
+ branch,
97
+ imp.resolvedPath,
98
+ githubToken,
99
+ )
92
100
  if (!sectionSource) continue
93
101
 
94
102
  const section = await analyzeAstroSection(
@@ -153,7 +161,7 @@ async function fetchFileContent(
153
161
  )
154
162
  if (!response.ok) return null
155
163
 
156
- const data = await response.json() as { content: string; encoding: string }
164
+ const data = (await response.json()) as { content: string; encoding: string }
157
165
  if (data.encoding === 'base64') {
158
166
  return Buffer.from(data.content, 'base64').toString('utf-8')
159
167
  }