@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.
- 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 +113 -47
- 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-Q3N336KR.js → chunk-CDXCYYQR.js} +29 -24
- 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-TD76R3A6.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 +59 -25
- 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 +174 -79
- 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 +12 -3
- 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 +53 -19
- 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 +218 -97
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { isSafeKey } from '@setzkasten-cms/core'
|
|
1
2
|
import type { APIRoute } from 'astro'
|
|
2
|
-
import {
|
|
3
|
-
import { generateDuplicateKey, duplicateInPageConfig } from './section-management'
|
|
4
|
-
import { parseSession, guardPageAccess } from './_auth-guard'
|
|
3
|
+
import { guardPageAccess, parseSession } from './_auth-guard'
|
|
5
4
|
import { withTrailers } from './_commit-trailers'
|
|
6
5
|
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
6
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
7
|
+
import { duplicateInPageConfig, generateDuplicateKey } from './section-management'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* POST /api/setzkasten/sections/duplicate
|
|
@@ -26,7 +27,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
26
27
|
const githubToken = tokenResult.value
|
|
27
28
|
|
|
28
29
|
try {
|
|
29
|
-
const body = await request.json() as {
|
|
30
|
+
const body = (await request.json()) as {
|
|
30
31
|
pageKey: string
|
|
31
32
|
sectionKey: string
|
|
32
33
|
owner?: string
|
|
@@ -47,8 +48,19 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
47
48
|
if (!pageKey || !sectionKey) {
|
|
48
49
|
return Response.json({ error: 'pageKey and sectionKey are required' }, { status: 400 })
|
|
49
50
|
}
|
|
51
|
+
if (!isSafeKey(pageKey)) {
|
|
52
|
+
return Response.json({ error: 'invalid pageKey' }, { status: 400 })
|
|
53
|
+
}
|
|
54
|
+
if (!isSafeKey(sectionKey)) {
|
|
55
|
+
return Response.json({ error: 'invalid sectionKey' }, { status: 400 })
|
|
56
|
+
}
|
|
50
57
|
|
|
51
|
-
const denied = await guardPageAccess(
|
|
58
|
+
const denied = await guardPageAccess(
|
|
59
|
+
parseSession(cookies.get('setzkasten_session')?.value),
|
|
60
|
+
pageKey,
|
|
61
|
+
fullConfig,
|
|
62
|
+
request,
|
|
63
|
+
)
|
|
52
64
|
if (denied) return denied
|
|
53
65
|
|
|
54
66
|
const headers = {
|
|
@@ -73,7 +85,13 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
73
85
|
|
|
74
86
|
// 3. Read original section content
|
|
75
87
|
const originalJsonPath = `${contentPath}/_sections/${sectionKey}.json`
|
|
76
|
-
const originalContent = await fetchFileContent(
|
|
88
|
+
const originalContent = await fetchFileContent(
|
|
89
|
+
owner,
|
|
90
|
+
repo,
|
|
91
|
+
branch,
|
|
92
|
+
originalJsonPath,
|
|
93
|
+
githubToken,
|
|
94
|
+
)
|
|
77
95
|
|
|
78
96
|
// 4. Build commit
|
|
79
97
|
const updatedConfig = duplicateInPageConfig(pageConfig, sectionKey, newKey)
|
|
@@ -86,12 +104,17 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
86
104
|
filesToCommit.push({ path: copyPath, content: originalContent })
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
const commitResult = await batchCommit(
|
|
107
|
+
const commitResult = await batchCommit(
|
|
108
|
+
owner,
|
|
109
|
+
repo,
|
|
110
|
+
branch,
|
|
111
|
+
filesToCommit,
|
|
90
112
|
withTrailers(
|
|
91
113
|
`content: duplicate ${sectionKey} → ${newKey} on ${pageKey}`,
|
|
92
114
|
parseSession(cookies.get('setzkasten_session')?.value)?.user?.email,
|
|
93
115
|
),
|
|
94
|
-
headers
|
|
116
|
+
headers,
|
|
117
|
+
)
|
|
95
118
|
|
|
96
119
|
if (!commitResult.ok) return Response.json({ error: commitResult.error }, { status: 500 })
|
|
97
120
|
|
|
@@ -111,50 +134,94 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
111
134
|
}
|
|
112
135
|
}
|
|
113
136
|
|
|
114
|
-
async function fetchFileContent(
|
|
137
|
+
async function fetchFileContent(
|
|
138
|
+
owner: string,
|
|
139
|
+
repo: string,
|
|
140
|
+
branch: string,
|
|
141
|
+
path: string,
|
|
142
|
+
token: string,
|
|
143
|
+
): Promise<string | null> {
|
|
115
144
|
try {
|
|
116
145
|
const res = await fetch(
|
|
117
146
|
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
118
|
-
{
|
|
147
|
+
{
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${token}`,
|
|
150
|
+
Accept: 'application/vnd.github+json',
|
|
151
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
119
154
|
)
|
|
120
155
|
if (!res.ok) return null
|
|
121
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
122
|
-
return data.encoding === 'base64'
|
|
123
|
-
|
|
156
|
+
const data = (await res.json()) as { content: string; encoding: string }
|
|
157
|
+
return data.encoding === 'base64'
|
|
158
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
159
|
+
: data.content
|
|
160
|
+
} catch {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
124
163
|
}
|
|
125
164
|
|
|
126
165
|
async function batchCommit(
|
|
127
|
-
owner: string,
|
|
166
|
+
owner: string,
|
|
167
|
+
repo: string,
|
|
168
|
+
branch: string,
|
|
128
169
|
files: Array<{ path: string; content: string }>,
|
|
129
170
|
message: string,
|
|
130
171
|
headers: Record<string, string>,
|
|
131
172
|
): Promise<{ ok: true; sha: string } | { ok: false; error: string }> {
|
|
132
173
|
try {
|
|
133
|
-
const refRes = await fetch(
|
|
174
|
+
const refRes = await fetch(
|
|
175
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
176
|
+
{ headers },
|
|
177
|
+
)
|
|
134
178
|
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
|
|
135
|
-
const {
|
|
179
|
+
const {
|
|
180
|
+
object: { sha: headSha },
|
|
181
|
+
} = (await refRes.json()) as { object: { sha: string } }
|
|
136
182
|
|
|
137
|
-
const commitRes = await fetch(
|
|
183
|
+
const commitRes = await fetch(
|
|
184
|
+
`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`,
|
|
185
|
+
{ headers },
|
|
186
|
+
)
|
|
138
187
|
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
|
|
139
|
-
const {
|
|
188
|
+
const {
|
|
189
|
+
tree: { sha: baseSha },
|
|
190
|
+
} = (await commitRes.json()) as { tree: { sha: string } }
|
|
140
191
|
|
|
141
192
|
const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
|
|
142
|
-
method: 'POST',
|
|
143
|
-
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers,
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
base_tree: baseSha,
|
|
197
|
+
tree: files.map((f) => ({
|
|
198
|
+
path: f.path,
|
|
199
|
+
mode: '100644',
|
|
200
|
+
type: 'blob',
|
|
201
|
+
content: f.content,
|
|
202
|
+
})),
|
|
203
|
+
}),
|
|
144
204
|
})
|
|
145
205
|
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
|
|
146
|
-
const { sha: treeSha } = await treeRes.json() as { sha: string }
|
|
206
|
+
const { sha: treeSha } = (await treeRes.json()) as { sha: string }
|
|
147
207
|
|
|
148
208
|
const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
|
|
149
|
-
method: 'POST',
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers,
|
|
150
211
|
body: JSON.stringify({ tree: treeSha, parents: [headSha], message }),
|
|
151
212
|
})
|
|
152
|
-
if (!newCommitRes.ok)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
213
|
+
if (!newCommitRes.ok)
|
|
214
|
+
return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
|
|
215
|
+
const { sha: newSha } = (await newCommitRes.json()) as { sha: string }
|
|
216
|
+
|
|
217
|
+
const updateRes = await fetch(
|
|
218
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
219
|
+
{
|
|
220
|
+
method: 'PATCH',
|
|
221
|
+
headers,
|
|
222
|
+
body: JSON.stringify({ sha: newSha }),
|
|
223
|
+
},
|
|
224
|
+
)
|
|
158
225
|
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` }
|
|
159
226
|
|
|
160
227
|
return { ok: true, sha: newSha }
|
|
@@ -21,7 +21,7 @@ interface PageConfig {
|
|
|
21
21
|
*/
|
|
22
22
|
export function removeFromPageConfig(config: PageConfig, sectionKey: string): PageConfig {
|
|
23
23
|
const sections = config.sections
|
|
24
|
-
.filter(s => s.key !== sectionKey)
|
|
24
|
+
.filter((s) => s.key !== sectionKey)
|
|
25
25
|
.map((s, i) => ({ ...s, order: i }))
|
|
26
26
|
return { ...config, sections }
|
|
27
27
|
}
|
|
@@ -53,11 +53,7 @@ export function generateAddKey(existingKeys: string[], type: string): string {
|
|
|
53
53
|
* Appends a new section entry at the end of the page config.
|
|
54
54
|
* Sets `type` only when key differs from type (multi-instance case).
|
|
55
55
|
*/
|
|
56
|
-
export function addToPageConfig(
|
|
57
|
-
config: PageConfig,
|
|
58
|
-
key: string,
|
|
59
|
-
type: string,
|
|
60
|
-
): PageConfig {
|
|
56
|
+
export function addToPageConfig(config: PageConfig, key: string, type: string): PageConfig {
|
|
61
57
|
const entry: SectionEntry = {
|
|
62
58
|
key,
|
|
63
59
|
enabled: true,
|
|
@@ -76,7 +72,7 @@ export function duplicateInPageConfig(
|
|
|
76
72
|
originalKey: string,
|
|
77
73
|
newKey: string,
|
|
78
74
|
): PageConfig {
|
|
79
|
-
const original = config.sections.find(s => s.key === originalKey)
|
|
75
|
+
const original = config.sections.find((s) => s.key === originalKey)
|
|
80
76
|
if (!original) return config
|
|
81
77
|
|
|
82
78
|
// Always set type explicitly: copy key differs from type (e.g. 'testPricing--copy'),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { isSafeKey } from '@setzkasten-cms/core'
|
|
1
2
|
import type { APIRoute } from 'astro'
|
|
2
|
-
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
3
|
-
import { generateDuplicateKey, duplicateInPageConfig } from './section-management'
|
|
4
3
|
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
4
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
5
|
+
import { duplicateInPageConfig, generateDuplicateKey } from './section-management'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* POST /api/setzkasten/sections/prepare-copy
|
|
@@ -34,7 +35,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
34
35
|
const githubToken = tokenResult.value
|
|
35
36
|
|
|
36
37
|
try {
|
|
37
|
-
const body = await request.json() as {
|
|
38
|
+
const body = (await request.json()) as {
|
|
38
39
|
pageKey: string
|
|
39
40
|
sectionKey: string
|
|
40
41
|
owner?: string
|
|
@@ -54,6 +55,12 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
54
55
|
if (!pageKey || !sectionKey) {
|
|
55
56
|
return Response.json({ error: 'pageKey and sectionKey are required' }, { status: 400 })
|
|
56
57
|
}
|
|
58
|
+
if (!isSafeKey(pageKey)) {
|
|
59
|
+
return Response.json({ error: 'invalid pageKey' }, { status: 400 })
|
|
60
|
+
}
|
|
61
|
+
if (!isSafeKey(sectionKey)) {
|
|
62
|
+
return Response.json({ error: 'invalid sectionKey' }, { status: 400 })
|
|
63
|
+
}
|
|
57
64
|
|
|
58
65
|
// 1. Read current page config
|
|
59
66
|
const configKey = '_' + pageKey.replace(/\//g, '_')
|
|
@@ -90,15 +97,29 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
async function fetchFileContent(
|
|
93
|
-
owner: string,
|
|
100
|
+
owner: string,
|
|
101
|
+
repo: string,
|
|
102
|
+
branch: string,
|
|
103
|
+
path: string,
|
|
104
|
+
token: string,
|
|
94
105
|
): Promise<string | null> {
|
|
95
106
|
try {
|
|
96
107
|
const res = await fetch(
|
|
97
108
|
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
98
|
-
{
|
|
109
|
+
{
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${token}`,
|
|
112
|
+
Accept: 'application/vnd.github+json',
|
|
113
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
99
116
|
)
|
|
100
117
|
if (!res.ok) return null
|
|
101
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
102
|
-
return data.encoding === 'base64'
|
|
103
|
-
|
|
118
|
+
const data = (await res.json()) as { content: string; encoding: string }
|
|
119
|
+
return data.encoding === 'base64'
|
|
120
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
121
|
+
: data.content
|
|
122
|
+
} catch {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
104
125
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { isSafeKey } from '@setzkasten-cms/core'
|
|
1
2
|
import type { APIRoute } from 'astro'
|
|
2
|
-
import {
|
|
3
|
-
import { generateAddKey } from './section-management'
|
|
4
|
-
import { parseSession, guardPageAccess } from './_auth-guard'
|
|
3
|
+
import { guardPageAccess, parseSession } from './_auth-guard'
|
|
5
4
|
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
5
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
6
|
+
import { generateAddKey } from './section-management'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* POST /api/setzkasten/sections/prepare
|
|
@@ -28,7 +29,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
28
29
|
const githubToken = tokenResult.value
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
|
-
const body = await request.json() as {
|
|
32
|
+
const body = (await request.json()) as {
|
|
32
33
|
pageKey: string
|
|
33
34
|
sectionType: string
|
|
34
35
|
owner?: string
|
|
@@ -49,8 +50,19 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
49
50
|
if (!pageKey || !sectionType) {
|
|
50
51
|
return Response.json({ error: 'pageKey and sectionType are required' }, { status: 400 })
|
|
51
52
|
}
|
|
53
|
+
if (!isSafeKey(pageKey)) {
|
|
54
|
+
return Response.json({ error: 'invalid pageKey' }, { status: 400 })
|
|
55
|
+
}
|
|
56
|
+
if (!isSafeKey(sectionType)) {
|
|
57
|
+
return Response.json({ error: 'invalid sectionType' }, { status: 400 })
|
|
58
|
+
}
|
|
52
59
|
|
|
53
|
-
const denied = await guardPageAccess(
|
|
60
|
+
const denied = await guardPageAccess(
|
|
61
|
+
parseSession(cookies.get('setzkasten_session')?.value),
|
|
62
|
+
pageKey,
|
|
63
|
+
fullConfig,
|
|
64
|
+
request,
|
|
65
|
+
)
|
|
54
66
|
if (denied) return denied
|
|
55
67
|
|
|
56
68
|
// 1. Read current page config from GitHub to determine existing keys
|
|
@@ -116,14 +128,30 @@ function buildDefaultContent(fields: Record<string, any>): Record<string, unknow
|
|
|
116
128
|
return result
|
|
117
129
|
}
|
|
118
130
|
|
|
119
|
-
async function fetchFileContent(
|
|
131
|
+
async function fetchFileContent(
|
|
132
|
+
owner: string,
|
|
133
|
+
repo: string,
|
|
134
|
+
branch: string,
|
|
135
|
+
path: string,
|
|
136
|
+
token: string,
|
|
137
|
+
): Promise<string | null> {
|
|
120
138
|
try {
|
|
121
139
|
const res = await fetch(
|
|
122
140
|
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
123
|
-
{
|
|
141
|
+
{
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: `Bearer ${token}`,
|
|
144
|
+
Accept: 'application/vnd.github+json',
|
|
145
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
124
148
|
)
|
|
125
149
|
if (!res.ok) return null
|
|
126
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
127
|
-
return data.encoding === 'base64'
|
|
128
|
-
|
|
150
|
+
const data = (await res.json()) as { content: string; encoding: string }
|
|
151
|
+
return data.encoding === 'base64'
|
|
152
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
153
|
+
: data.content
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
129
157
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
|
+
import { requireAdmin } from './_auth-guard'
|
|
2
3
|
import { getPublicOrigin } from './_vercel-origin.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -9,7 +10,12 @@ import { getPublicOrigin } from './_vercel-origin.js'
|
|
|
9
10
|
* Generates the GitHub App manifest JSON using the server-known origin,
|
|
10
11
|
* then returns a minimal HTML page that auto-submits a form to GitHub.
|
|
11
12
|
*/
|
|
12
|
-
export const GET: APIRoute = async ({ url, request }) => {
|
|
13
|
+
export const GET: APIRoute = async ({ url, request, cookies }) => {
|
|
14
|
+
// Admin-only — pre-fix any unauthenticated visitor could trigger the
|
|
15
|
+
// manifest redirect and probe the deployment's origin / setup state.
|
|
16
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
17
|
+
if (denied) return denied
|
|
18
|
+
|
|
13
19
|
const name = url.searchParams.get('name')?.trim() || 'Setzkasten CMS'
|
|
14
20
|
const origin = getPublicOrigin(request)
|
|
15
21
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { APIRoute } from 'astro'
|
|
2
1
|
import { listRepoBranches } from '@setzkasten-cms/github-adapter'
|
|
2
|
+
import type { APIRoute } from 'astro'
|
|
3
3
|
import { requireAdmin } from './_auth-guard'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -20,8 +20,7 @@ export const GET: APIRoute = async ({ cookies, url }) => {
|
|
|
20
20
|
if (!appId || !privateKey) {
|
|
21
21
|
return new Response(
|
|
22
22
|
JSON.stringify({
|
|
23
|
-
error:
|
|
24
|
-
'GitHub App not configured. Set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY.',
|
|
23
|
+
error: 'GitHub App not configured. Set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY.',
|
|
25
24
|
}),
|
|
26
25
|
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
27
26
|
)
|
|
@@ -38,10 +37,10 @@ export const GET: APIRoute = async ({ cookies, url }) => {
|
|
|
38
37
|
|
|
39
38
|
const slash = repoFull.indexOf('/')
|
|
40
39
|
if (slash <= 0 || slash === repoFull.length - 1) {
|
|
41
|
-
return new Response(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
40
|
+
return new Response(JSON.stringify({ error: '?repo must be in "owner/name" format.' }), {
|
|
41
|
+
status: 400,
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
})
|
|
45
44
|
}
|
|
46
45
|
const owner = repoFull.slice(0, slash)
|
|
47
46
|
const repo = repoFull.slice(slash + 1)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
|
+
import { requireAdmin } from './_auth-guard'
|
|
2
3
|
import { getPublicOrigin } from './_vercel-origin.js'
|
|
3
4
|
|
|
4
5
|
const COOKIE_NAME = 'sk_app_setup'
|
|
@@ -15,6 +16,13 @@ const COOKIE_MAX_AGE = 600 // 10 minutes
|
|
|
15
16
|
* the Set-Cookie header (TypeError: immutable).
|
|
16
17
|
*/
|
|
17
18
|
export const GET: APIRoute = async ({ url, request, cookies }) => {
|
|
19
|
+
// GitHub redirects the admin's browser back here after the Manifest
|
|
20
|
+
// flow — the original session cookie travels with the top-level
|
|
21
|
+
// navigation. Without this gate, anyone could replay an older `?code`
|
|
22
|
+
// and force a re-write of the setup cookie. Admin-only.
|
|
23
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
24
|
+
if (denied) return denied
|
|
25
|
+
|
|
18
26
|
const config = (globalThis as Record<string, unknown>).__SETZKASTEN_CONFIG__ as
|
|
19
27
|
| { adminPath?: string }
|
|
20
28
|
| undefined
|
|
@@ -64,7 +72,16 @@ export const GET: APIRoute = async ({ url, request, cookies }) => {
|
|
|
64
72
|
clientId: data!.client_id,
|
|
65
73
|
clientSecret: data!.client_secret,
|
|
66
74
|
}),
|
|
67
|
-
|
|
75
|
+
// httpOnly:true — pre-fix this was readable to any JS on origin (XSS,
|
|
76
|
+
// extensions). The SPA now reads via /api/setzkasten/setup/github-app/
|
|
77
|
+
// credentials (server reads the cookie, returns values).
|
|
78
|
+
{
|
|
79
|
+
httpOnly: true,
|
|
80
|
+
secure: import.meta.env.PROD,
|
|
81
|
+
sameSite: 'lax',
|
|
82
|
+
maxAge: COOKIE_MAX_AGE,
|
|
83
|
+
path: '/',
|
|
84
|
+
},
|
|
68
85
|
)
|
|
69
86
|
|
|
70
87
|
return new Response(null, { status: 302, headers: { Location: adminUrl.toString() } })
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro'
|
|
2
|
+
import { requireAdmin } from './_auth-guard'
|
|
3
|
+
|
|
4
|
+
const COOKIE_NAME = 'sk_app_setup'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/setzkasten/setup/github-app/credentials
|
|
8
|
+
*
|
|
9
|
+
* Returns the freshly-minted GitHub App credentials so the admin SPA can
|
|
10
|
+
* display them (env-var copy step of the wizard). Pre-C6 the SPA read
|
|
11
|
+
* these directly from `document.cookie`, which required the cookie to
|
|
12
|
+
* be `httpOnly: false` — exposing the App private key to any JS on the
|
|
13
|
+
* origin (XSS, extensions, embedded widgets). Now the cookie is
|
|
14
|
+
* httpOnly and only this admin-gated endpoint can read it.
|
|
15
|
+
*
|
|
16
|
+
* The cookie itself stays for the wizard's full lifetime (10 min); this
|
|
17
|
+
* endpoint just makes the contents available without exposing them via
|
|
18
|
+
* the document.
|
|
19
|
+
*/
|
|
20
|
+
export const GET: APIRoute = async ({ cookies }) => {
|
|
21
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
22
|
+
if (denied) return denied
|
|
23
|
+
|
|
24
|
+
const raw = cookies.get(COOKIE_NAME)?.value
|
|
25
|
+
if (!raw) {
|
|
26
|
+
return Response.json({ available: false }, { status: 404 })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let parsed: unknown
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(raw)
|
|
32
|
+
} catch {
|
|
33
|
+
return Response.json({ available: false, error: 'malformed' }, { status: 500 })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
37
|
+
return Response.json({ available: false }, { status: 404 })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Response.json({ available: true, credentials: parsed })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* DELETE /api/setzkasten/setup/github-app/credentials
|
|
45
|
+
*
|
|
46
|
+
* Clears the setup cookie once the admin has copied the env vars and
|
|
47
|
+
* confirmed the deploy. Removes the credentials from the wire as soon
|
|
48
|
+
* as they're no longer needed.
|
|
49
|
+
*/
|
|
50
|
+
export const DELETE: APIRoute = async ({ cookies }) => {
|
|
51
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
52
|
+
if (denied) return denied
|
|
53
|
+
cookies.delete(COOKIE_NAME, { path: '/' })
|
|
54
|
+
return Response.json({ ok: true })
|
|
55
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
|
+
import { requireAdmin } from './_auth-guard'
|
|
2
3
|
import { getPublicOrigin } from './_vercel-origin.js'
|
|
3
4
|
|
|
4
5
|
const COOKIE_NAME = 'sk_app_setup'
|
|
@@ -15,6 +16,9 @@ const COOKIE_MAX_AGE = 600
|
|
|
15
16
|
* the Set-Cookie header (TypeError: immutable).
|
|
16
17
|
*/
|
|
17
18
|
export const GET: APIRoute = async ({ url, request, cookies }) => {
|
|
19
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
20
|
+
if (denied) return denied
|
|
21
|
+
|
|
18
22
|
const config = (globalThis as Record<string, unknown>).__SETZKASTEN_CONFIG__ as
|
|
19
23
|
| { adminPath?: string }
|
|
20
24
|
| undefined
|
|
@@ -33,7 +37,14 @@ export const GET: APIRoute = async ({ url, request, cookies }) => {
|
|
|
33
37
|
cookies.set(
|
|
34
38
|
COOKIE_NAME,
|
|
35
39
|
JSON.stringify({ ...data, installationId }),
|
|
36
|
-
|
|
40
|
+
// C6: same httpOnly hardening as the callback route.
|
|
41
|
+
{
|
|
42
|
+
httpOnly: true,
|
|
43
|
+
secure: import.meta.env.PROD,
|
|
44
|
+
sameSite: 'lax',
|
|
45
|
+
maxAge: COOKIE_MAX_AGE,
|
|
46
|
+
path: '/',
|
|
47
|
+
},
|
|
37
48
|
)
|
|
38
49
|
} catch {
|
|
39
50
|
adminUrl.searchParams.set('github-app-error', 'invalid_session')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { APIRoute } from 'astro'
|
|
2
1
|
import { listAccessibleRepos } from '@setzkasten-cms/github-adapter'
|
|
2
|
+
import type { APIRoute } from 'astro'
|
|
3
3
|
import { requireAdmin } from './_auth-guard'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -23,8 +23,7 @@ export const GET: APIRoute = async ({ cookies }) => {
|
|
|
23
23
|
if (!appId || !privateKey) {
|
|
24
24
|
return new Response(
|
|
25
25
|
JSON.stringify({
|
|
26
|
-
error:
|
|
27
|
-
'GitHub App not configured. Set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY.',
|
|
26
|
+
error: 'GitHub App not configured. Set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY.',
|
|
28
27
|
}),
|
|
29
28
|
{ status: 400, headers: { 'Content-Type': 'application/json' } },
|
|
30
29
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { APIRoute } from 'astro'
|
|
2
1
|
import { GitHubAppClient } from '@setzkasten-cms/github-adapter'
|
|
2
|
+
import type { APIRoute } from 'astro'
|
|
3
|
+
import { requireAdmin } from './_auth-guard'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Setup-Wizard: GitHub App Integration
|
|
@@ -9,9 +10,16 @@ import { GitHubAppClient } from '@setzkasten-cms/github-adapter'
|
|
|
9
10
|
*
|
|
10
11
|
* Credentials werden NICHT persistiert – der Nutzer setzt die env vars manuell.
|
|
11
12
|
* Der POST-Endpunkt validiert die Verbindung durch einen echten Token-Request.
|
|
13
|
+
*
|
|
14
|
+
* Both methods are admin-only. Pre-fix the GET let any unauthenticated
|
|
15
|
+
* visitor probe whether a deployment was configured (reconnaissance for
|
|
16
|
+
* targeting half-set-up instances).
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
|
-
export const GET: APIRoute = async () => {
|
|
19
|
+
export const GET: APIRoute = async ({ cookies }) => {
|
|
20
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
21
|
+
if (denied) return denied
|
|
22
|
+
|
|
15
23
|
const appId = process.env.GITHUB_APP_ID
|
|
16
24
|
const privateKey = process.env.GITHUB_APP_PRIVATE_KEY
|
|
17
25
|
const installationId = process.env.GITHUB_APP_INSTALLATION_ID
|
|
@@ -21,7 +29,9 @@ export const GET: APIRoute = async () => {
|
|
|
21
29
|
return Response.json({ configured, ...(configured ? { appId } : {}) })
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
export const POST: APIRoute = async ({ request }) => {
|
|
32
|
+
export const POST: APIRoute = async ({ request, cookies }) => {
|
|
33
|
+
const denied = requireAdmin(cookies.get('setzkasten_session')?.value)
|
|
34
|
+
if (denied) return denied
|
|
25
35
|
let body: unknown
|
|
26
36
|
try {
|
|
27
37
|
body = await request.json()
|
|
@@ -29,8 +39,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
29
39
|
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
const { appId, privateKey, installationId } =
|
|
33
|
-
(body as Record<string, unknown>) ?? {}
|
|
42
|
+
const { appId, privateKey, installationId } = (body as Record<string, unknown>) ?? {}
|
|
34
43
|
|
|
35
44
|
if (!appId || !privateKey || !installationId) {
|
|
36
45
|
return Response.json(
|
|
@@ -11,10 +11,12 @@ export const GET: APIRoute = async ({ cookies, url }) => {
|
|
|
11
11
|
return new Response('Unauthorized', { status: 401 })
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const config = (globalThis as any).__SETZKASTEN_CONFIG__ as
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const config = (globalThis as any).__SETZKASTEN_CONFIG__ as
|
|
15
|
+
| {
|
|
16
|
+
updaterUrl?: string
|
|
17
|
+
version?: string
|
|
18
|
+
}
|
|
19
|
+
| undefined
|
|
18
20
|
|
|
19
21
|
const updaterUrl = config?.updaterUrl
|
|
20
22
|
if (!updaterUrl) {
|