@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.
- package/dist/api-routes/_auth-guard.d.ts +27 -3
- package/dist/api-routes/_auth-guard.js +5 -2
- package/dist/api-routes/_dev-session-secret.d.ts +8 -0
- package/dist/api-routes/_dev-session-secret.js +8 -0
- package/dist/api-routes/_github-token.js +1 -1
- package/dist/api-routes/_role-resolver.js +6 -3
- package/dist/api-routes/_session-secret.d.ts +19 -0
- package/dist/api-routes/_session-secret.js +7 -0
- package/dist/api-routes/_session-signing.d.ts +45 -0
- package/dist/api-routes/_session-signing.js +8 -0
- package/dist/api-routes/_webhook-dispatcher.js +4 -4
- package/dist/api-routes/asset-proxy.js +1 -1
- package/dist/api-routes/auth-callback.js +12 -5
- package/dist/api-routes/auth-logout.d.ts +4 -4
- package/dist/api-routes/auth-logout.js +8 -2
- package/dist/api-routes/auth-session.d.ts +6 -0
- package/dist/api-routes/auth-session.js +19 -19
- package/dist/api-routes/auth-setzkasten-login.js +14 -7
- package/dist/api-routes/catalog-add.js +59 -17
- package/dist/api-routes/catalog-export.js +14 -4
- package/dist/api-routes/config.d.ts +10 -3
- package/dist/api-routes/config.js +26 -4
- package/dist/api-routes/deploy-hook.js +8 -8
- package/dist/api-routes/editors.d.ts +1 -1
- package/dist/api-routes/editors.js +5 -2
- package/dist/api-routes/github-proxy.js +30 -8
- package/dist/api-routes/global-config.js +6 -3
- package/dist/api-routes/history-rollback.js +31 -14
- package/dist/api-routes/history-version.js +8 -6
- package/dist/api-routes/history.js +5 -2
- package/dist/api-routes/icons-local.js +1 -1
- package/dist/api-routes/init-add-section.js +150 -48
- package/dist/api-routes/init-apply.js +56 -42
- package/dist/api-routes/init-migrate.js +43 -36
- package/dist/api-routes/init-scan-page.d.ts +1 -1
- package/dist/api-routes/init-scan-page.js +59 -13
- package/dist/api-routes/init-scan.js +22 -7
- package/dist/api-routes/migrate-to-multi.js +5 -2
- package/dist/api-routes/pages.js +15 -4
- package/dist/api-routes/section-add.js +68 -16
- package/dist/api-routes/section-commit-pending.js +70 -22
- package/dist/api-routes/section-delete.js +49 -14
- package/dist/api-routes/section-duplicate.js +65 -16
- package/dist/api-routes/section-prepare-copy.js +15 -2
- package/dist/api-routes/section-prepare.js +25 -4
- package/dist/api-routes/setup-github-app-bounce.js +15 -1
- package/dist/api-routes/setup-github-app-branches.js +9 -6
- package/dist/api-routes/setup-github-app-callback.js +24 -1
- package/dist/api-routes/setup-github-app-credentials.d.ts +27 -0
- package/dist/api-routes/setup-github-app-credentials.js +43 -0
- package/dist/api-routes/setup-github-app-installed.js +22 -1
- package/dist/api-routes/setup-github-app-repos.js +5 -2
- package/dist/api-routes/setup-github-app.d.ts +4 -0
- package/dist/api-routes/setup-github-app.js +19 -2
- package/dist/api-routes/updater-register.js +7 -1
- package/dist/api-routes/webhooks-status.js +5 -2
- package/dist/api-routes/webhooks-test.js +9 -8
- package/dist/api-routes/webhooks.js +12 -14
- package/dist/api-routes/websites-add.js +5 -2
- package/dist/api-routes/websites-remove.js +5 -2
- package/dist/{chunk-ZQDGGWJP.js → chunk-5KMGSFCZ.js} +2 -2
- package/dist/{chunk-RHJONMLK.js → chunk-CDXCYYQR.js} +222 -5
- package/dist/{chunk-NKDATSPA.js → chunk-DP6RTINQ.js} +1 -1
- package/dist/chunk-KENFINT4.js +76 -0
- package/dist/chunk-ONP6BRZO.js +47 -0
- package/dist/{chunk-INIWFKQ3.js → chunk-Q5HV47DW.js} +33 -19
- package/dist/chunk-QVCW6EF3.js +26 -0
- package/dist/{chunk-K22A4ZBS.js → chunk-UHI6323G.js} +293 -174
- package/dist/{chunk-AM4DZXXM.js → chunk-UJAFZEX2.js} +76 -9
- package/package.json +12 -6
- package/src/api-routes/__tests__/_session-signing.test.ts +114 -0
- package/src/api-routes/__tests__/_session-test-helper.ts +27 -0
- package/src/api-routes/__tests__/add-section-helpers.test.ts +91 -97
- package/src/api-routes/__tests__/auth-guard.test.ts +46 -7
- package/src/api-routes/__tests__/catalog-api.test.ts +14 -6
- package/src/api-routes/__tests__/commit-trailers.test.ts +5 -5
- package/src/api-routes/__tests__/deferred-operations.test.ts +9 -12
- package/src/api-routes/__tests__/deploy-hook.test.ts +3 -8
- package/src/api-routes/__tests__/feature-gate.test.ts +1 -1
- package/src/api-routes/__tests__/github-cache.test.ts +1 -1
- package/src/api-routes/__tests__/github-token.test.ts +1 -1
- package/src/api-routes/__tests__/global-config-theme.test.ts +4 -4
- package/src/api-routes/__tests__/history-rollback.test.ts +6 -3
- package/src/api-routes/__tests__/history.test.ts +9 -6
- package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +11 -3
- package/src/api-routes/__tests__/migrate-to-multi.test.ts +5 -1
- package/src/api-routes/__tests__/pages-meta-store.test.ts +10 -5
- package/src/api-routes/__tests__/pages.test.ts +7 -2
- package/src/api-routes/__tests__/patch-page-file.test.ts +71 -19
- package/src/api-routes/__tests__/route-registry.test.ts +11 -18
- package/src/api-routes/__tests__/scan-page-helpers.test.ts +13 -10
- package/src/api-routes/__tests__/section-management.test.ts +28 -28
- package/src/api-routes/__tests__/setup-github-app-callback.test.ts +58 -16
- package/src/api-routes/__tests__/setup-github-app-repos.test.ts +4 -5
- package/src/api-routes/__tests__/setup-github-app.test.ts +27 -7
- package/src/api-routes/__tests__/storage-config-for-request.test.ts +83 -0
- package/src/api-routes/__tests__/updater-register.test.ts +230 -0
- package/src/api-routes/__tests__/webhook-signing.test.ts +1 -1
- package/src/api-routes/__tests__/webhooks.test.ts +19 -7
- package/src/api-routes/__tests__/websites-add.test.ts +2 -1
- package/src/api-routes/__tests__/websites-remove.test.ts +2 -1
- package/src/api-routes/_auth-guard.ts +47 -15
- package/src/api-routes/_commit-trailers.ts +3 -2
- package/src/api-routes/_dev-session-secret.ts +79 -0
- package/src/api-routes/_github-token.ts +1 -1
- package/src/api-routes/_pages-meta-store.ts +2 -2
- package/src/api-routes/_role-resolver.ts +7 -5
- package/src/api-routes/_session-secret.ts +46 -0
- package/src/api-routes/_session-signing.ts +135 -0
- package/src/api-routes/_vercel-origin.ts +2 -6
- package/src/api-routes/_webhook-dispatcher.ts +12 -16
- package/src/api-routes/_website-resolver.ts +3 -10
- package/src/api-routes/auth-callback.ts +9 -5
- package/src/api-routes/auth-login.ts +5 -3
- package/src/api-routes/auth-logout.ts +18 -1
- package/src/api-routes/auth-session.ts +13 -21
- package/src/api-routes/auth-setzkasten-login.ts +12 -9
- package/src/api-routes/catalog-add.ts +89 -31
- package/src/api-routes/catalog-export.ts +30 -10
- package/src/api-routes/config.ts +39 -6
- package/src/api-routes/deploy-hook.ts +13 -11
- package/src/api-routes/editors.ts +33 -22
- package/src/api-routes/github-proxy.ts +25 -11
- package/src/api-routes/global-config.ts +103 -18
- package/src/api-routes/history-rollback.ts +41 -14
- package/src/api-routes/history-version.ts +5 -6
- package/src/api-routes/history.ts +3 -3
- package/src/api-routes/icons-local.ts +2 -2
- package/src/api-routes/init-add-section.ts +218 -88
- package/src/api-routes/init-apply.ts +71 -56
- package/src/api-routes/init-migrate.ts +54 -48
- package/src/api-routes/init-scan-page.ts +77 -30
- package/src/api-routes/init-scan.ts +19 -11
- package/src/api-routes/pages.ts +16 -11
- package/src/api-routes/section-add.ts +98 -27
- package/src/api-routes/section-commit-pending.ts +87 -34
- package/src/api-routes/section-delete.ts +76 -27
- package/src/api-routes/section-duplicate.ts +95 -28
- package/src/api-routes/section-management.ts +3 -7
- package/src/api-routes/section-prepare-copy.ts +29 -8
- package/src/api-routes/section-prepare.ts +38 -10
- package/src/api-routes/setup-github-app-bounce.ts +7 -1
- package/src/api-routes/setup-github-app-branches.ts +6 -7
- package/src/api-routes/setup-github-app-callback.ts +18 -1
- package/src/api-routes/setup-github-app-credentials.ts +55 -0
- package/src/api-routes/setup-github-app-installed.ts +12 -1
- package/src/api-routes/setup-github-app-repos.ts +2 -3
- package/src/api-routes/setup-github-app.ts +14 -5
- package/src/api-routes/updater-check.ts +6 -4
- package/src/api-routes/updater-register.ts +34 -20
- package/src/api-routes/updater-transfer.ts +8 -6
- package/src/api-routes/updater-unbind.ts +14 -10
- package/src/api-routes/webhooks-test.ts +9 -11
- package/src/api-routes/webhooks.ts +15 -19
- package/src/init/__tests__/page-level.test.ts +279 -105
- package/src/init/__tests__/page-list-coverage.test.ts +70 -70
- package/src/init/__tests__/patcher-child-component.test.ts +126 -0
- package/src/init/__tests__/patcher-edge-cases.test.ts +47 -23
- package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +16 -6
- package/src/init/__tests__/patcher-page-mode.test.ts +30 -20
- package/src/init/__tests__/section-pipeline.test.ts +102 -16
- package/src/init/astro-config-patcher.ts +4 -18
- package/src/init/astro-detector.ts +2 -7
- package/src/init/astro-section-analyzer-v2.ts +475 -193
- package/src/init/field-label-enricher.ts +6 -6
- 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
|
-
|
|
35
|
-
|
|
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(
|
|
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(
|
|
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':
|
|
149
|
-
|
|
150
|
-
case '
|
|
151
|
-
|
|
152
|
-
case '
|
|
153
|
-
|
|
154
|
-
case '
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 {
|
|
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
|
|
21
|
-
if (
|
|
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(
|
|
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
|
|
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 {
|
|
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 =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 =
|
|
24
|
-
|
|
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
|
|
39
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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 =
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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,
|
|
259
|
-
|
|
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 {
|
|
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
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
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
|
}
|