@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
package/src/api-routes/pages.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
|
+
import { parseSession } from './_auth-guard'
|
|
3
|
+
import { cachedFetch } from './_github-cache'
|
|
2
4
|
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
3
|
-
import {
|
|
5
|
+
import { type PagesMetaTarget, readPagesMeta } from './_pages-meta-store'
|
|
4
6
|
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
5
|
-
import { cachedFetch } from './_github-cache'
|
|
6
7
|
|
|
7
8
|
interface PageInfo {
|
|
8
9
|
path: string
|
|
@@ -29,7 +30,9 @@ declare const __SETZKASTEN_PAGES__: PageInfo[] | undefined
|
|
|
29
30
|
*/
|
|
30
31
|
export function resolvePages(): PageInfo[] {
|
|
31
32
|
const buildPages = typeof __SETZKASTEN_PAGES__ !== 'undefined' ? __SETZKASTEN_PAGES__ : null
|
|
32
|
-
return
|
|
33
|
+
return (
|
|
34
|
+
buildPages ?? ((globalThis as Record<string, unknown>).__SETZKASTEN_PAGES__ as PageInfo[]) ?? []
|
|
35
|
+
)
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
@@ -47,11 +50,16 @@ export function resolvePages(): PageInfo[] {
|
|
|
47
50
|
* would see the admin's own page list — typically a single "index"
|
|
48
51
|
* stub — instead of its real pages.
|
|
49
52
|
*/
|
|
50
|
-
export const GET: APIRoute = async ({ request }) => {
|
|
53
|
+
export const GET: APIRoute = async ({ request, cookies }) => {
|
|
54
|
+
// Authenticated users only — pre-fix this was an unauthenticated
|
|
55
|
+
// reconnaissance endpoint that also minted an installation token to
|
|
56
|
+
// crawl `src/pages/` of any website the App could reach.
|
|
57
|
+
if (!parseSession(cookies.get('setzkasten_session')?.value)) {
|
|
58
|
+
return new Response('Unauthorized', { status: 401 })
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
const isMulti = request.headers.get('x-sk-website') !== null
|
|
52
|
-
const pages = isMulti
|
|
53
|
-
? await fetchPagesFromGitHub(request).catch(() => [])
|
|
54
|
-
: resolvePages()
|
|
62
|
+
const pages = isMulti ? await fetchPagesFromGitHub(request).catch(() => []) : resolvePages()
|
|
55
63
|
|
|
56
64
|
const enriched = await enrichWithLastModified(pages, request).catch(() => pages)
|
|
57
65
|
|
|
@@ -116,10 +124,7 @@ async function fetchPagesFromGitHub(request: Request): Promise<PageInfo[]> {
|
|
|
116
124
|
})
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
async function enrichWithLastModified(
|
|
120
|
-
pages: PageInfo[],
|
|
121
|
-
request: Request,
|
|
122
|
-
): Promise<PageInfo[]> {
|
|
127
|
+
async function enrichWithLastModified(pages: PageInfo[], request: Request): Promise<PageInfo[]> {
|
|
123
128
|
if (pages.length === 0) return pages
|
|
124
129
|
|
|
125
130
|
const storage = await resolveStorageConfigForRequest(request)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { isSafeKey } from '@setzkasten-cms/core'
|
|
1
2
|
import type { APIRoute } from 'astro'
|
|
2
|
-
import {
|
|
3
|
-
import { generateAddKey, addToPageConfig } 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 { addToPageConfig, generateAddKey } from './section-management'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* POST /api/setzkasten/sections/add
|
|
@@ -29,7 +30,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
29
30
|
const githubToken = tokenResult.value
|
|
30
31
|
|
|
31
32
|
try {
|
|
32
|
-
const body = await request.json() as {
|
|
33
|
+
const body = (await request.json()) as {
|
|
33
34
|
pageKey: string
|
|
34
35
|
sectionType: string
|
|
35
36
|
sectionKey?: string
|
|
@@ -52,8 +53,25 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
52
53
|
if (!pageKey || !sectionType) {
|
|
53
54
|
return Response.json({ error: 'pageKey and sectionType are required' }, { status: 400 })
|
|
54
55
|
}
|
|
56
|
+
if (!isSafeKey(pageKey)) {
|
|
57
|
+
return Response.json({ error: 'invalid pageKey' }, { status: 400 })
|
|
58
|
+
}
|
|
59
|
+
if (!isSafeKey(sectionType)) {
|
|
60
|
+
return Response.json({ error: 'invalid sectionType' }, { status: 400 })
|
|
61
|
+
}
|
|
62
|
+
if (body.sectionKey !== undefined && !isSafeKey(body.sectionKey)) {
|
|
63
|
+
return Response.json({ error: 'invalid sectionKey' }, { status: 400 })
|
|
64
|
+
}
|
|
65
|
+
if (sourcePage !== undefined && !isSafeKey(sourcePage)) {
|
|
66
|
+
return Response.json({ error: 'invalid sourcePage' }, { status: 400 })
|
|
67
|
+
}
|
|
55
68
|
|
|
56
|
-
const denied = await guardPageAccess(
|
|
69
|
+
const denied = await guardPageAccess(
|
|
70
|
+
parseSession(cookies.get('setzkasten_session')?.value),
|
|
71
|
+
pageKey,
|
|
72
|
+
fullConfig,
|
|
73
|
+
request,
|
|
74
|
+
)
|
|
57
75
|
if (denied) return denied
|
|
58
76
|
|
|
59
77
|
const headers = {
|
|
@@ -76,7 +94,10 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
76
94
|
// 2. Determine key
|
|
77
95
|
const newKey = body.sectionKey ?? generateAddKey(existingKeys, sectionType)
|
|
78
96
|
if (existingKeys.includes(newKey)) {
|
|
79
|
-
return Response.json(
|
|
97
|
+
return Response.json(
|
|
98
|
+
{ error: `Key "${newKey}" already exists on this page` },
|
|
99
|
+
{ status: 409 },
|
|
100
|
+
)
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
// 3. Determine initial content: sourcePage seed → schema defaults → empty
|
|
@@ -88,7 +109,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
88
109
|
const sourceJsonPath = `${contentPath}/_sections/${sourceKey}.json`
|
|
89
110
|
const sourceContent = await fetchFileContent(owner, repo, branch, sourceJsonPath, githubToken)
|
|
90
111
|
if (sourceContent) {
|
|
91
|
-
try {
|
|
112
|
+
try {
|
|
113
|
+
defaultContent = JSON.parse(sourceContent)
|
|
114
|
+
} catch {
|
|
115
|
+
/* fallback to schema */
|
|
116
|
+
}
|
|
92
117
|
}
|
|
93
118
|
}
|
|
94
119
|
if (Object.keys(defaultContent).length === 0) {
|
|
@@ -101,7 +126,9 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
101
126
|
const sectionJsonPath = `${contentPath}/_sections/${newKey}.json`
|
|
102
127
|
|
|
103
128
|
const commitResult = await batchCommit(
|
|
104
|
-
owner,
|
|
129
|
+
owner,
|
|
130
|
+
repo,
|
|
131
|
+
branch,
|
|
105
132
|
[
|
|
106
133
|
{ path: pageConfigPath, content: JSON.stringify(updatedConfig, null, 2) },
|
|
107
134
|
{ path: sectionJsonPath, content: JSON.stringify(defaultContent, null, 2) },
|
|
@@ -154,50 +181,94 @@ function buildDefaultContent(fields: Record<string, any>): Record<string, unknow
|
|
|
154
181
|
return result
|
|
155
182
|
}
|
|
156
183
|
|
|
157
|
-
async function fetchFileContent(
|
|
184
|
+
async function fetchFileContent(
|
|
185
|
+
owner: string,
|
|
186
|
+
repo: string,
|
|
187
|
+
branch: string,
|
|
188
|
+
path: string,
|
|
189
|
+
token: string,
|
|
190
|
+
): Promise<string | null> {
|
|
158
191
|
try {
|
|
159
192
|
const res = await fetch(
|
|
160
193
|
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
161
|
-
{
|
|
194
|
+
{
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${token}`,
|
|
197
|
+
Accept: 'application/vnd.github+json',
|
|
198
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
162
201
|
)
|
|
163
202
|
if (!res.ok) return null
|
|
164
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
165
|
-
return data.encoding === 'base64'
|
|
166
|
-
|
|
203
|
+
const data = (await res.json()) as { content: string; encoding: string }
|
|
204
|
+
return data.encoding === 'base64'
|
|
205
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
206
|
+
: data.content
|
|
207
|
+
} catch {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
167
210
|
}
|
|
168
211
|
|
|
169
212
|
async function batchCommit(
|
|
170
|
-
owner: string,
|
|
213
|
+
owner: string,
|
|
214
|
+
repo: string,
|
|
215
|
+
branch: string,
|
|
171
216
|
files: Array<{ path: string; content: string }>,
|
|
172
217
|
message: string,
|
|
173
218
|
headers: Record<string, string>,
|
|
174
219
|
): Promise<{ ok: true; sha: string } | { ok: false; error: string }> {
|
|
175
220
|
try {
|
|
176
|
-
const refRes = await fetch(
|
|
221
|
+
const refRes = await fetch(
|
|
222
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
223
|
+
{ headers },
|
|
224
|
+
)
|
|
177
225
|
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
|
|
178
|
-
const {
|
|
226
|
+
const {
|
|
227
|
+
object: { sha: headSha },
|
|
228
|
+
} = (await refRes.json()) as { object: { sha: string } }
|
|
179
229
|
|
|
180
|
-
const commitRes = await fetch(
|
|
230
|
+
const commitRes = await fetch(
|
|
231
|
+
`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`,
|
|
232
|
+
{ headers },
|
|
233
|
+
)
|
|
181
234
|
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
|
|
182
|
-
const {
|
|
235
|
+
const {
|
|
236
|
+
tree: { sha: baseSha },
|
|
237
|
+
} = (await commitRes.json()) as { tree: { sha: string } }
|
|
183
238
|
|
|
184
239
|
const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
|
|
185
|
-
method: 'POST',
|
|
186
|
-
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers,
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
base_tree: baseSha,
|
|
244
|
+
tree: files.map((f) => ({
|
|
245
|
+
path: f.path,
|
|
246
|
+
mode: '100644',
|
|
247
|
+
type: 'blob',
|
|
248
|
+
content: f.content,
|
|
249
|
+
})),
|
|
250
|
+
}),
|
|
187
251
|
})
|
|
188
252
|
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
|
|
189
|
-
const { sha: treeSha } = await treeRes.json() as { sha: string }
|
|
253
|
+
const { sha: treeSha } = (await treeRes.json()) as { sha: string }
|
|
190
254
|
|
|
191
255
|
const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
|
|
192
|
-
method: 'POST',
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers,
|
|
193
258
|
body: JSON.stringify({ tree: treeSha, parents: [headSha], message }),
|
|
194
259
|
})
|
|
195
|
-
if (!newCommitRes.ok)
|
|
196
|
-
|
|
260
|
+
if (!newCommitRes.ok)
|
|
261
|
+
return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
|
|
262
|
+
const { sha: newSha } = (await newCommitRes.json()) as { sha: string }
|
|
197
263
|
|
|
198
|
-
const updateRes = await fetch(
|
|
199
|
-
|
|
200
|
-
|
|
264
|
+
const updateRes = await fetch(
|
|
265
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
266
|
+
{
|
|
267
|
+
method: 'PATCH',
|
|
268
|
+
headers,
|
|
269
|
+
body: JSON.stringify({ sha: newSha }),
|
|
270
|
+
},
|
|
271
|
+
)
|
|
201
272
|
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` }
|
|
202
273
|
|
|
203
274
|
return { ok: true, sha: newSha }
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type { APIRoute } from 'astro'
|
|
2
1
|
import { writeFile } from 'node:fs/promises'
|
|
3
2
|
import { join } from 'node:path'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { isSafeKey, setPageLastModified } from '@setzkasten-cms/core'
|
|
4
|
+
import type { APIRoute } from 'astro'
|
|
5
|
+
import { convertToSetHtml } from '../init/template-patcher-v2'
|
|
6
|
+
import { guardPageAccess, parseSession } from './_auth-guard'
|
|
6
7
|
import { withTrailers } from './_commit-trailers'
|
|
7
8
|
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
8
|
-
import { convertToSetHtml } from '../init/template-patcher-v2'
|
|
9
9
|
import { readPagesMeta } from './_pages-meta-store'
|
|
10
|
-
import {
|
|
10
|
+
import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* POST /api/setzkasten/sections/commit-pending
|
|
@@ -33,7 +33,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
33
33
|
const githubToken = tokenResult.value
|
|
34
34
|
|
|
35
35
|
try {
|
|
36
|
-
const body = await request.json() as {
|
|
36
|
+
const body = (await request.json()) as {
|
|
37
37
|
pageKey: string
|
|
38
38
|
pageConfig: Record<string, unknown>
|
|
39
39
|
sections: Array<{ key: string; content: Record<string, unknown> }>
|
|
@@ -54,10 +54,33 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
54
54
|
const { pageKey, pageConfig, sections, edits = [] } = body
|
|
55
55
|
|
|
56
56
|
if (!pageKey || !pageConfig || !Array.isArray(sections)) {
|
|
57
|
-
return Response.json(
|
|
57
|
+
return Response.json(
|
|
58
|
+
{ error: 'pageKey, pageConfig, and sections are required' },
|
|
59
|
+
{ status: 400 },
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
if (!isSafeKey(pageKey)) {
|
|
63
|
+
return Response.json({ error: 'invalid pageKey' }, { status: 400 })
|
|
64
|
+
}
|
|
65
|
+
// Every section/edit key composes a file path. Reject anything that
|
|
66
|
+
// could escape the _sections/ folder before we hit the GitHub API.
|
|
67
|
+
for (const s of sections) {
|
|
68
|
+
if (!isSafeKey(s?.key)) {
|
|
69
|
+
return Response.json({ error: `invalid section key: ${s?.key}` }, { status: 400 })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const e of edits) {
|
|
73
|
+
if (!isSafeKey(e?.key)) {
|
|
74
|
+
return Response.json({ error: `invalid edit key: ${e?.key}` }, { status: 400 })
|
|
75
|
+
}
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
const denied = await guardPageAccess(
|
|
78
|
+
const denied = await guardPageAccess(
|
|
79
|
+
parseSession(cookies.get('setzkasten_session')?.value),
|
|
80
|
+
pageKey,
|
|
81
|
+
fullConfig,
|
|
82
|
+
request,
|
|
83
|
+
)
|
|
61
84
|
if (denied) return denied
|
|
62
85
|
|
|
63
86
|
const headers = {
|
|
@@ -73,11 +96,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
73
96
|
|
|
74
97
|
const files: Array<{ path: string; content: string }> = [
|
|
75
98
|
{ path: pageConfigPath, content: JSON.stringify(pageConfig, null, 2) },
|
|
76
|
-
...sections.map(s => ({
|
|
99
|
+
...sections.map((s) => ({
|
|
77
100
|
path: `${contentPath}/_sections/${s.key}.json`,
|
|
78
101
|
content: JSON.stringify(s.content, null, 2),
|
|
79
102
|
})),
|
|
80
|
-
...edits.map(s => ({
|
|
103
|
+
...edits.map((s) => ({
|
|
81
104
|
path: `${contentPath}/_sections/${s.key}.json`,
|
|
82
105
|
content: JSON.stringify(s.content, null, 2),
|
|
83
106
|
})),
|
|
@@ -90,15 +113,15 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
90
113
|
// run convertToSetHtml (idempotent — no-op if already converted), and
|
|
91
114
|
// include the patched template in the same batch commit.
|
|
92
115
|
const sectionsWithHtml = [...sections, ...edits]
|
|
93
|
-
.filter(s => containsHtmlValue(s.content))
|
|
94
|
-
.map(s => s.key)
|
|
116
|
+
.filter((s) => containsHtmlValue(s.content))
|
|
117
|
+
.map((s) => s.key)
|
|
95
118
|
const projectPrefix = (storage as { projectPrefix?: string }).projectPrefix
|
|
96
119
|
for (const sectionKey of sectionsWithHtml) {
|
|
97
120
|
const componentPath = prefixPath(
|
|
98
121
|
`src/components/sections/${pascalCase(sectionKey)}Section.astro`,
|
|
99
122
|
projectPrefix ?? '',
|
|
100
123
|
)
|
|
101
|
-
if (files.some(f => f.path === componentPath)) continue
|
|
124
|
+
if (files.some((f) => f.path === componentPath)) continue
|
|
102
125
|
const original = await fetchFileContent(owner, repo, branch, componentPath, githubToken)
|
|
103
126
|
if (!original) continue
|
|
104
127
|
const patched = convertToSetHtml(original)
|
|
@@ -124,11 +147,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
124
147
|
|
|
125
148
|
const parts: string[] = []
|
|
126
149
|
if (sections.length > 0) {
|
|
127
|
-
const keys = sections.map(s => s.key).join(', ')
|
|
150
|
+
const keys = sections.map((s) => s.key).join(', ')
|
|
128
151
|
parts.push(`add ${sections.length} section${sections.length > 1 ? 's' : ''} (${keys})`)
|
|
129
152
|
}
|
|
130
153
|
if (edits.length > 0) {
|
|
131
|
-
const keys = edits.map(s => s.key).join(', ')
|
|
154
|
+
const keys = edits.map((s) => s.key).join(', ')
|
|
132
155
|
parts.push(`update ${edits.length} section${edits.length > 1 ? 's' : ''} (${keys})`)
|
|
133
156
|
}
|
|
134
157
|
const editorEmail = parseSession(cookies.get('setzkasten_session')?.value)?.user?.email
|
|
@@ -146,9 +169,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
146
169
|
const repoRoot: string | undefined = serverConfig?.repoRoot
|
|
147
170
|
if (repoRoot) {
|
|
148
171
|
await Promise.all(
|
|
149
|
-
files.map(
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
files.map((f) =>
|
|
173
|
+
writeFile(join(repoRoot, f.path), f.content, 'utf-8').catch(() => {
|
|
174
|
+
// Non-fatal: local sync is best-effort (e.g. read-only FS in some CI envs)
|
|
175
|
+
}),
|
|
176
|
+
),
|
|
152
177
|
)
|
|
153
178
|
}
|
|
154
179
|
|
|
@@ -196,7 +221,7 @@ function pascalCase(input: string): string {
|
|
|
196
221
|
return input
|
|
197
222
|
.split(/[-_\s]+/)
|
|
198
223
|
.filter(Boolean)
|
|
199
|
-
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
224
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
200
225
|
.join('')
|
|
201
226
|
}
|
|
202
227
|
|
|
@@ -219,7 +244,7 @@ async function fetchFileContent(
|
|
|
219
244
|
},
|
|
220
245
|
)
|
|
221
246
|
if (!res.ok) return null
|
|
222
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
247
|
+
const data = (await res.json()) as { content: string; encoding: string }
|
|
223
248
|
return data.encoding === 'base64'
|
|
224
249
|
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
225
250
|
: data.content
|
|
@@ -229,37 +254,65 @@ async function fetchFileContent(
|
|
|
229
254
|
}
|
|
230
255
|
|
|
231
256
|
async function batchCommit(
|
|
232
|
-
owner: string,
|
|
257
|
+
owner: string,
|
|
258
|
+
repo: string,
|
|
259
|
+
branch: string,
|
|
233
260
|
files: Array<{ path: string; content: string }>,
|
|
234
261
|
message: string,
|
|
235
262
|
headers: Record<string, string>,
|
|
236
263
|
): Promise<{ ok: true; sha: string } | { ok: false; error: string }> {
|
|
237
264
|
try {
|
|
238
|
-
const refRes = await fetch(
|
|
265
|
+
const refRes = await fetch(
|
|
266
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
267
|
+
{ headers },
|
|
268
|
+
)
|
|
239
269
|
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
|
|
240
|
-
const {
|
|
270
|
+
const {
|
|
271
|
+
object: { sha: headSha },
|
|
272
|
+
} = (await refRes.json()) as { object: { sha: string } }
|
|
241
273
|
|
|
242
|
-
const commitRes = await fetch(
|
|
274
|
+
const commitRes = await fetch(
|
|
275
|
+
`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`,
|
|
276
|
+
{ headers },
|
|
277
|
+
)
|
|
243
278
|
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
|
|
244
|
-
const {
|
|
279
|
+
const {
|
|
280
|
+
tree: { sha: baseSha },
|
|
281
|
+
} = (await commitRes.json()) as { tree: { sha: string } }
|
|
245
282
|
|
|
246
283
|
const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
|
|
247
|
-
method: 'POST',
|
|
248
|
-
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers,
|
|
286
|
+
body: JSON.stringify({
|
|
287
|
+
base_tree: baseSha,
|
|
288
|
+
tree: files.map((f) => ({
|
|
289
|
+
path: f.path,
|
|
290
|
+
mode: '100644',
|
|
291
|
+
type: 'blob',
|
|
292
|
+
content: f.content,
|
|
293
|
+
})),
|
|
294
|
+
}),
|
|
249
295
|
})
|
|
250
296
|
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
|
|
251
|
-
const { sha: treeSha } = await treeRes.json() as { sha: string }
|
|
297
|
+
const { sha: treeSha } = (await treeRes.json()) as { sha: string }
|
|
252
298
|
|
|
253
299
|
const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
|
|
254
|
-
method: 'POST',
|
|
300
|
+
method: 'POST',
|
|
301
|
+
headers,
|
|
255
302
|
body: JSON.stringify({ tree: treeSha, parents: [headSha], message }),
|
|
256
303
|
})
|
|
257
|
-
if (!newCommitRes.ok)
|
|
258
|
-
|
|
304
|
+
if (!newCommitRes.ok)
|
|
305
|
+
return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
|
|
306
|
+
const { sha: newSha } = (await newCommitRes.json()) as { sha: string }
|
|
259
307
|
|
|
260
|
-
const updateRes = await fetch(
|
|
261
|
-
|
|
262
|
-
|
|
308
|
+
const updateRes = await fetch(
|
|
309
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
310
|
+
{
|
|
311
|
+
method: 'PATCH',
|
|
312
|
+
headers,
|
|
313
|
+
body: JSON.stringify({ sha: newSha }),
|
|
314
|
+
},
|
|
315
|
+
)
|
|
263
316
|
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` }
|
|
264
317
|
|
|
265
318
|
return { ok: true, sha: newSha }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { isSafeKey } from '@setzkasten-cms/core'
|
|
1
2
|
import type { APIRoute } from 'astro'
|
|
2
|
-
import {
|
|
3
|
-
import { removeFromPageConfig } 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 { removeFromPageConfig } from './section-management'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* DELETE /api/setzkasten/sections
|
|
@@ -26,7 +27,7 @@ export const DELETE: 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 DELETE: 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 = {
|
|
@@ -72,7 +84,9 @@ export const DELETE: APIRoute = async ({ request, cookies }) => {
|
|
|
72
84
|
const sectionJsonPath = `${contentPath}/_sections/${sectionKey}.json`
|
|
73
85
|
|
|
74
86
|
const commitResult = await batchCommitWithDeletions(
|
|
75
|
-
owner,
|
|
87
|
+
owner,
|
|
88
|
+
repo,
|
|
89
|
+
branch,
|
|
76
90
|
[{ path: pageConfigPath, content: JSON.stringify(updatedConfig, null, 2) }],
|
|
77
91
|
[sectionJsonPath],
|
|
78
92
|
withTrailers(
|
|
@@ -114,57 +128,92 @@ export const DELETE: APIRoute = async ({ request, cookies }) => {
|
|
|
114
128
|
}
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
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> {
|
|
118
138
|
try {
|
|
119
139
|
const res = await fetch(
|
|
120
140
|
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
121
|
-
{
|
|
141
|
+
{
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: `Bearer ${token}`,
|
|
144
|
+
Accept: 'application/vnd.github+json',
|
|
145
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
122
148
|
)
|
|
123
149
|
if (!res.ok) return null
|
|
124
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
125
|
-
return data.encoding === 'base64'
|
|
126
|
-
|
|
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
|
+
}
|
|
127
157
|
}
|
|
128
158
|
|
|
129
159
|
async function batchCommitWithDeletions(
|
|
130
|
-
owner: string,
|
|
160
|
+
owner: string,
|
|
161
|
+
repo: string,
|
|
162
|
+
branch: string,
|
|
131
163
|
upserts: Array<{ path: string; content: string }>,
|
|
132
164
|
deletions: string[],
|
|
133
165
|
message: string,
|
|
134
166
|
headers: Record<string, string>,
|
|
135
167
|
): Promise<{ ok: true; sha: string } | { ok: false; error: string }> {
|
|
136
168
|
try {
|
|
137
|
-
const refRes = await fetch(
|
|
169
|
+
const refRes = await fetch(
|
|
170
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
171
|
+
{ headers },
|
|
172
|
+
)
|
|
138
173
|
if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` }
|
|
139
|
-
const {
|
|
174
|
+
const {
|
|
175
|
+
object: { sha: headSha },
|
|
176
|
+
} = (await refRes.json()) as { object: { sha: string } }
|
|
140
177
|
|
|
141
|
-
const commitRes = await fetch(
|
|
178
|
+
const commitRes = await fetch(
|
|
179
|
+
`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`,
|
|
180
|
+
{ headers },
|
|
181
|
+
)
|
|
142
182
|
if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` }
|
|
143
|
-
const {
|
|
183
|
+
const {
|
|
184
|
+
tree: { sha: baseSha },
|
|
185
|
+
} = (await commitRes.json()) as { tree: { sha: string } }
|
|
144
186
|
|
|
145
187
|
const tree = [
|
|
146
|
-
...upserts.map(f => ({ path: f.path, mode: '100644', type: 'blob', content: f.content })),
|
|
147
|
-
...deletions.map(path => ({ path, mode: '100644', type: 'blob', sha: null })),
|
|
188
|
+
...upserts.map((f) => ({ path: f.path, mode: '100644', type: 'blob', content: f.content })),
|
|
189
|
+
...deletions.map((path) => ({ path, mode: '100644', type: 'blob', sha: null })),
|
|
148
190
|
]
|
|
149
191
|
|
|
150
192
|
const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
|
|
151
|
-
method: 'POST',
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers,
|
|
152
195
|
body: JSON.stringify({ base_tree: baseSha, tree }),
|
|
153
196
|
})
|
|
154
197
|
if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` }
|
|
155
|
-
const { sha: treeSha } = await treeRes.json() as { sha: string }
|
|
198
|
+
const { sha: treeSha } = (await treeRes.json()) as { sha: string }
|
|
156
199
|
|
|
157
200
|
const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
|
|
158
|
-
method: 'POST',
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers,
|
|
159
203
|
body: JSON.stringify({ tree: treeSha, parents: [headSha], message }),
|
|
160
204
|
})
|
|
161
|
-
if (!newCommitRes.ok)
|
|
162
|
-
|
|
205
|
+
if (!newCommitRes.ok)
|
|
206
|
+
return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` }
|
|
207
|
+
const { sha: newSha } = (await newCommitRes.json()) as { sha: string }
|
|
163
208
|
|
|
164
|
-
const updateRes = await fetch(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
209
|
+
const updateRes = await fetch(
|
|
210
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
|
211
|
+
{
|
|
212
|
+
method: 'PATCH',
|
|
213
|
+
headers,
|
|
214
|
+
body: JSON.stringify({ sha: newSha }),
|
|
215
|
+
},
|
|
216
|
+
)
|
|
168
217
|
if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` }
|
|
169
218
|
|
|
170
219
|
return { ok: true, sha: newSha }
|