@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,7 +1,7 @@
|
|
|
1
|
-
import type { APIRoute } from 'astro'
|
|
2
1
|
import { exportTemplate } from '@setzkasten-cms/catalog'
|
|
3
|
-
import {
|
|
2
|
+
import type { APIRoute } from 'astro'
|
|
4
3
|
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
4
|
+
import { prefixPath, resolveStorageConfigForRequest } from './_storage-config'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* POST /api/setzkasten/catalog/export
|
|
@@ -22,7 +22,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
22
22
|
const githubToken = tokenResult.value
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
|
-
const body = await request.json() as {
|
|
25
|
+
const body = (await request.json()) as {
|
|
26
26
|
sectionKey: string
|
|
27
27
|
owner?: string
|
|
28
28
|
repo?: string
|
|
@@ -46,14 +46,18 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
46
46
|
// 1. Read section content JSON
|
|
47
47
|
const sectionJsonPath = prefixPath(`${contentPath}/_sections/${sectionKey}.json`, projectPrefix)
|
|
48
48
|
const contentRaw = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken)
|
|
49
|
-
if (!contentRaw)
|
|
49
|
+
if (!contentRaw)
|
|
50
|
+
return Response.json({ error: `Section content not found: ${sectionKey}` }, { status: 404 })
|
|
50
51
|
|
|
51
52
|
const content = JSON.parse(contentRaw) as Record<string, unknown>
|
|
52
53
|
|
|
53
54
|
// 2. Find section definition from full config
|
|
54
55
|
const sectionDef = findSectionDef(fullConfig, sectionKey)
|
|
55
56
|
if (!sectionDef) {
|
|
56
|
-
return Response.json(
|
|
57
|
+
return Response.json(
|
|
58
|
+
{ error: `Section definition not found for key: ${sectionKey}` },
|
|
59
|
+
{ status: 404 },
|
|
60
|
+
)
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
// 3. Export to template format
|
|
@@ -77,14 +81,30 @@ function findSectionDef(fullConfig: any, sectionKey: string): any {
|
|
|
77
81
|
return null
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
async function fetchFileContent(
|
|
84
|
+
async function fetchFileContent(
|
|
85
|
+
owner: string,
|
|
86
|
+
repo: string,
|
|
87
|
+
branch: string,
|
|
88
|
+
path: string,
|
|
89
|
+
token: string,
|
|
90
|
+
): Promise<string | null> {
|
|
81
91
|
try {
|
|
82
92
|
const res = await fetch(
|
|
83
93
|
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
|
|
84
|
-
{
|
|
94
|
+
{
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `Bearer ${token}`,
|
|
97
|
+
Accept: 'application/vnd.github+json',
|
|
98
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
85
101
|
)
|
|
86
102
|
if (!res.ok) return null
|
|
87
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
88
|
-
return data.encoding === 'base64'
|
|
89
|
-
|
|
103
|
+
const data = (await res.json()) as { content: string; encoding: string }
|
|
104
|
+
return data.encoding === 'base64'
|
|
105
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
106
|
+
: data.content
|
|
107
|
+
} catch {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
90
110
|
}
|
package/src/api-routes/config.ts
CHANGED
|
@@ -1,22 +1,55 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
|
+
import { parseSession } from './_auth-guard'
|
|
2
3
|
import { readGlobalConfig } from './global-config'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* GET /api/setzkasten/config
|
|
6
7
|
*
|
|
7
|
-
* Returns the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Returns the SetzKastenConfig as JSON. The login screen (pre-session)
|
|
9
|
+
* needs to know which auth providers are enabled and Firebase project
|
|
10
|
+
* id, so a *minimal public subset* is exposed unauthenticated. The full
|
|
11
|
+
* config (storage params, products, page schema, etc.) is only served
|
|
12
|
+
* to authenticated users — pre-fix anyone could harvest the deployment
|
|
13
|
+
* blueprint.
|
|
14
|
+
*
|
|
15
|
+
* Fields from GlobalConfig (`_global_config.json`) are merged over the
|
|
16
|
+
* static config so admins can change theme and Firebase settings
|
|
17
|
+
* without a code deployment.
|
|
10
18
|
*/
|
|
11
|
-
export const GET: APIRoute = async () => {
|
|
19
|
+
export const GET: APIRoute = async ({ cookies }) => {
|
|
12
20
|
const config = (globalThis as Record<string, unknown>).__SETZKASTEN_FULL_CONFIG__ ?? {}
|
|
13
|
-
const ssrConfig = (globalThis as Record<string, unknown>).__SETZKASTEN_CONFIG__ as
|
|
21
|
+
const ssrConfig = (globalThis as Record<string, unknown>).__SETZKASTEN_CONFIG__ as
|
|
22
|
+
| Record<string, unknown>
|
|
23
|
+
| undefined
|
|
14
24
|
|
|
15
25
|
const globalCfg = await readGlobalConfig().catch(() => null)
|
|
16
26
|
|
|
17
|
-
const staticTheme = (config as
|
|
27
|
+
const staticTheme = (config as Record<string, unknown>).theme ?? {}
|
|
18
28
|
const globalTheme = globalCfg?.theme ?? {}
|
|
19
29
|
|
|
30
|
+
const session = parseSession(cookies?.get('setzkasten_session')?.value)
|
|
31
|
+
|
|
32
|
+
// Public subset for the login screen. Provider list is intentionally
|
|
33
|
+
// visible — the SPA needs to render the right buttons. Firebase config
|
|
34
|
+
// is needed for `setzkasten-login` (Google IdP) before the user has a
|
|
35
|
+
// session cookie. Theme is cosmetic. Everything else is gated.
|
|
36
|
+
if (!session) {
|
|
37
|
+
return new Response(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
auth: (config as Record<string, unknown>).auth ?? { providers: ['github'] },
|
|
40
|
+
theme: { ...staticTheme, ...globalTheme },
|
|
41
|
+
adminPath: ssrConfig?.adminPath ?? '/admin',
|
|
42
|
+
_firebaseConfig: globalCfg?.firebaseConfig ?? null,
|
|
43
|
+
_hasGitHub: ssrConfig?.hasGitHub ?? false,
|
|
44
|
+
_hasGoogle: ssrConfig?.hasGoogle ?? false,
|
|
45
|
+
}),
|
|
46
|
+
{
|
|
47
|
+
status: 200,
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
20
53
|
const result = {
|
|
21
54
|
// Default fallback when no config is injected at build time. Real
|
|
22
55
|
// values are spread from `config` below.
|
|
@@ -15,9 +15,11 @@ export const POST: APIRoute = async ({ cookies }) => {
|
|
|
15
15
|
return new Response('Unauthorized', { status: 401 })
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const config = (globalThis as any).__SETZKASTEN_CONFIG__ as
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const config = (globalThis as any).__SETZKASTEN_CONFIG__ as
|
|
19
|
+
| {
|
|
20
|
+
deployHook?: { url: string; secret?: string }
|
|
21
|
+
}
|
|
22
|
+
| undefined
|
|
21
23
|
|
|
22
24
|
if (!config?.deployHook?.url) {
|
|
23
25
|
return new Response(JSON.stringify({ skipped: true, reason: 'Kein deployHook konfiguriert' }), {
|
|
@@ -49,10 +51,10 @@ export const POST: APIRoute = async ({ cookies }) => {
|
|
|
49
51
|
|
|
50
52
|
if (!response.ok) {
|
|
51
53
|
console.warn(`[setzkasten] Deploy hook antwortete mit ${response.status}: ${url}`)
|
|
52
|
-
return new Response(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
54
|
+
return new Response(JSON.stringify({ ok: false, status: response.status }), {
|
|
55
|
+
status: 200,
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
})
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
return new Response(JSON.stringify({ ok: true }), {
|
|
@@ -61,9 +63,9 @@ export const POST: APIRoute = async ({ cookies }) => {
|
|
|
61
63
|
})
|
|
62
64
|
} catch (error) {
|
|
63
65
|
console.error('[setzkasten] Deploy hook fehlgeschlagen:', error)
|
|
64
|
-
return new Response(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
66
|
+
return new Response(JSON.stringify({ ok: false, error: String(error) }), {
|
|
67
|
+
status: 200,
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
})
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { APIRoute } from 'astro'
|
|
2
|
-
import { resolveStorageConfig } from './_storage-config'
|
|
3
|
-
import { parseSession } from './_auth-guard'
|
|
4
|
-
import { resolveConfigRepoToken } from './_github-token'
|
|
5
1
|
import type { ContentEditorConfig } from '@setzkasten-cms/core'
|
|
6
2
|
import { validateEditorsUpdate } from '@setzkasten-cms/core'
|
|
7
|
-
import {
|
|
3
|
+
import type { APIRoute } from 'astro'
|
|
4
|
+
import { parseSession } from './_auth-guard'
|
|
8
5
|
import { withTrailers } from './_commit-trailers'
|
|
9
6
|
import { gateFeature } from './_feature-gate'
|
|
7
|
+
import { cachedFetch, invalidateCache } from './_github-cache'
|
|
8
|
+
import { resolveConfigRepoToken } from './_github-token'
|
|
9
|
+
import { resolveStorageConfig } from './_storage-config'
|
|
10
10
|
|
|
11
11
|
const EDITORS_FILE = (contentPath: string) => `${contentPath}/_editors.json`
|
|
12
12
|
|
|
@@ -36,8 +36,9 @@ export const GET: APIRoute = async ({ cookies }) => {
|
|
|
36
36
|
const storage = configRepoStorage()
|
|
37
37
|
if (!storage) return Response.json({ error: 'Could not resolve storage config' }, { status: 400 })
|
|
38
38
|
|
|
39
|
-
const serverConfig = (
|
|
40
|
-
|
|
39
|
+
const serverConfig = (
|
|
40
|
+
globalThis as { __SETZKASTEN_CONFIG__?: { storage?: { contentPath?: string } } }
|
|
41
|
+
).__SETZKASTEN_CONFIG__
|
|
41
42
|
const contentPath = serverConfig?.storage?.contentPath ?? 'content'
|
|
42
43
|
const { owner, repo, branch } = storage
|
|
43
44
|
|
|
@@ -67,8 +68,9 @@ export const PUT: APIRoute = async ({ request, cookies }) => {
|
|
|
67
68
|
const storage = configRepoStorage()
|
|
68
69
|
if (!storage) return Response.json({ error: 'Could not resolve storage config' }, { status: 400 })
|
|
69
70
|
|
|
70
|
-
const serverConfig = (
|
|
71
|
-
|
|
71
|
+
const serverConfig = (
|
|
72
|
+
globalThis as { __SETZKASTEN_CONFIG__?: { storage?: { contentPath?: string } } }
|
|
73
|
+
).__SETZKASTEN_CONFIG__
|
|
72
74
|
const contentPath = serverConfig?.storage?.contentPath ?? 'content'
|
|
73
75
|
const { owner, repo, branch } = storage
|
|
74
76
|
|
|
@@ -82,10 +84,7 @@ export const PUT: APIRoute = async ({ request, cookies }) => {
|
|
|
82
84
|
|
|
83
85
|
const validation = validateEditorsUpdate(editors, session.user.email)
|
|
84
86
|
if (!validation.ok) {
|
|
85
|
-
return Response.json(
|
|
86
|
-
{ error: validation.message, code: validation.code },
|
|
87
|
-
{ status: 400 },
|
|
88
|
-
)
|
|
87
|
+
return Response.json({ error: validation.message, code: validation.code }, { status: 400 })
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
const filePath = EDITORS_FILE(contentPath)
|
|
@@ -107,10 +106,11 @@ export const PUT: APIRoute = async ({ request, cookies }) => {
|
|
|
107
106
|
}
|
|
108
107
|
if (existing) body.sha = existing
|
|
109
108
|
|
|
110
|
-
const res = await fetch(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`, {
|
|
110
|
+
method: 'PUT',
|
|
111
|
+
headers,
|
|
112
|
+
body: JSON.stringify(body),
|
|
113
|
+
})
|
|
114
114
|
|
|
115
115
|
if (!res.ok) {
|
|
116
116
|
const text = await res.text()
|
|
@@ -138,9 +138,11 @@ async function fetchFileSha(
|
|
|
138
138
|
{ headers },
|
|
139
139
|
)
|
|
140
140
|
if (!res.ok) return null
|
|
141
|
-
const data = await res.json() as { sha: string }
|
|
141
|
+
const data = (await res.json()) as { sha: string }
|
|
142
142
|
return data.sha ?? null
|
|
143
|
-
} catch {
|
|
143
|
+
} catch {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
export async function readEditorsFile(
|
|
@@ -154,11 +156,20 @@ export async function readEditorsFile(
|
|
|
154
156
|
return cachedFetch(key, 2 * 60_000, async () => {
|
|
155
157
|
const res = await fetch(
|
|
156
158
|
`https://api.github.com/repos/${owner}/${repo}/contents/${EDITORS_FILE(contentPath)}?ref=${branch}`,
|
|
157
|
-
{
|
|
159
|
+
{
|
|
160
|
+
headers: {
|
|
161
|
+
Authorization: `Bearer ${token}`,
|
|
162
|
+
Accept: 'application/vnd.github+json',
|
|
163
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
158
166
|
)
|
|
159
167
|
if (!res.ok) return null
|
|
160
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
161
|
-
const raw =
|
|
168
|
+
const data = (await res.json()) as { content: string; encoding: string }
|
|
169
|
+
const raw =
|
|
170
|
+
data.encoding === 'base64'
|
|
171
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
172
|
+
: data.content
|
|
162
173
|
return JSON.parse(raw) as ContentEditorConfig[]
|
|
163
174
|
})
|
|
164
175
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { APIRoute } from 'astro'
|
|
2
1
|
import { writeFile } from 'node:fs/promises'
|
|
3
2
|
import { join } from 'node:path'
|
|
3
|
+
import type { APIRoute } from 'astro'
|
|
4
|
+
import { parseSession } from './_auth-guard'
|
|
4
5
|
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
6
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Server-side proxy for GitHub API calls.
|
|
@@ -12,7 +14,7 @@ import { resolveGitHubTokenForRequest } from './_github-token'
|
|
|
12
14
|
*/
|
|
13
15
|
export const ALL: APIRoute = async ({ params, request, cookies }) => {
|
|
14
16
|
// Verify session
|
|
15
|
-
const session = cookies.get('setzkasten_session')?.value
|
|
17
|
+
const session = parseSession(cookies.get('setzkasten_session')?.value)
|
|
16
18
|
if (!session) {
|
|
17
19
|
return new Response('Unauthorized', { status: 401 })
|
|
18
20
|
}
|
|
@@ -22,6 +24,22 @@ export const ALL: APIRoute = async ({ params, request, cookies }) => {
|
|
|
22
24
|
return new Response('Missing path', { status: 400 })
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
// Restrict the proxy to the resolved website's repo. Pre-fix any
|
|
28
|
+
// editor (or anyone post-C1 forgery) could call
|
|
29
|
+
// `repos/{any-app-installed-repo}/contents/...` and write through the
|
|
30
|
+
// App's contents:write token. Now the prefix must match exactly.
|
|
31
|
+
const storage = await resolveStorageConfigForRequest(request)
|
|
32
|
+
if (!storage) {
|
|
33
|
+
return new Response('Could not resolve owner/repo', { status: 400 })
|
|
34
|
+
}
|
|
35
|
+
const allowedPrefix = `repos/${storage.owner}/${storage.repo}/`
|
|
36
|
+
const allowedExact = `repos/${storage.owner}/${storage.repo}`
|
|
37
|
+
if (githubPath !== allowedExact && !githubPath.startsWith(allowedPrefix)) {
|
|
38
|
+
return new Response(`Forbidden: proxy is scoped to ${storage.owner}/${storage.repo}`, {
|
|
39
|
+
status: 403,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
const tokenResult = await resolveGitHubTokenForRequest(request)
|
|
26
44
|
if (!tokenResult.ok) {
|
|
27
45
|
return new Response(tokenResult.error.message, { status: 500 })
|
|
@@ -45,9 +63,7 @@ export const ALL: APIRoute = async ({ params, request, cookies }) => {
|
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
const body =
|
|
48
|
-
request.method !== 'GET' && request.method !== 'HEAD'
|
|
49
|
-
? await request.text()
|
|
50
|
-
: undefined
|
|
66
|
+
request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined
|
|
51
67
|
|
|
52
68
|
const response = await fetch(githubUrl, {
|
|
53
69
|
method: request.method,
|
|
@@ -59,11 +75,7 @@ export const ALL: APIRoute = async ({ params, request, cookies }) => {
|
|
|
59
75
|
const responseHeaders = new Headers()
|
|
60
76
|
responseHeaders.set('Content-Type', response.headers.get('content-type') ?? 'application/json')
|
|
61
77
|
|
|
62
|
-
const rateLimitHeaders = [
|
|
63
|
-
'x-ratelimit-limit',
|
|
64
|
-
'x-ratelimit-remaining',
|
|
65
|
-
'x-ratelimit-reset',
|
|
66
|
-
]
|
|
78
|
+
const rateLimitHeaders = ['x-ratelimit-limit', 'x-ratelimit-remaining', 'x-ratelimit-reset']
|
|
67
79
|
for (const header of rateLimitHeaders) {
|
|
68
80
|
const value = response.headers.get(header)
|
|
69
81
|
if (value) responseHeaders.set(header, value)
|
|
@@ -90,7 +102,9 @@ export const ALL: APIRoute = async ({ params, request, cookies }) => {
|
|
|
90
102
|
const parsed = JSON.parse(body) as { content?: string }
|
|
91
103
|
if (parsed.content) {
|
|
92
104
|
// GitHub API sends base64 with possible line breaks
|
|
93
|
-
const decoded = Buffer.from(parsed.content.replace(/\s/g, ''), 'base64').toString(
|
|
105
|
+
const decoded = Buffer.from(parsed.content.replace(/\s/g, ''), 'base64').toString(
|
|
106
|
+
'utf-8',
|
|
107
|
+
)
|
|
94
108
|
await writeFile(join(repoRoot, filePath), decoded, 'utf-8').catch(() => {})
|
|
95
109
|
}
|
|
96
110
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
2
|
import { parseSession } from './_auth-guard'
|
|
3
|
-
import { resolveStorageConfig } from './_storage-config'
|
|
4
|
-
import { resolveConfigRepoToken } from './_github-token'
|
|
5
|
-
import { cachedFetch, invalidateCache } from './_github-cache'
|
|
6
3
|
import { withTrailers } from './_commit-trailers'
|
|
4
|
+
import { cachedFetch, invalidateCache } from './_github-cache'
|
|
5
|
+
import { resolveConfigRepoToken } from './_github-token'
|
|
6
|
+
import { resolveStorageConfig } from './_storage-config'
|
|
7
7
|
|
|
8
8
|
const GLOBAL_CONFIG_FILE = (contentPath: string) => `${contentPath}/_global_config.json`
|
|
9
9
|
|
|
@@ -41,13 +41,19 @@ export const PUT: APIRoute = async ({ request, cookies }) => {
|
|
|
41
41
|
if (!session) return new Response('Unauthorized', { status: 401 })
|
|
42
42
|
if (session.user.role !== 'admin') return new Response('Forbidden', { status: 403 })
|
|
43
43
|
|
|
44
|
-
let
|
|
44
|
+
let raw: unknown
|
|
45
45
|
try {
|
|
46
|
-
|
|
46
|
+
raw = await request.json()
|
|
47
47
|
} catch {
|
|
48
48
|
return Response.json({ error: 'Invalid request body' }, { status: 400 })
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
const validated = validateGlobalConfigPatch(raw)
|
|
52
|
+
if (!validated.ok) {
|
|
53
|
+
return Response.json({ error: validated.error }, { status: 400 })
|
|
54
|
+
}
|
|
55
|
+
const patch = validated.value
|
|
56
|
+
|
|
51
57
|
const current = (await readGlobalConfig()) ?? {}
|
|
52
58
|
const next: GlobalConfig = { ...current }
|
|
53
59
|
for (const [k, v] of Object.entries(patch)) {
|
|
@@ -58,6 +64,74 @@ export const PUT: APIRoute = async ({ request, cookies }) => {
|
|
|
58
64
|
return Response.json({ ok: true })
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Schema-validates the PUT payload before merging into `_global_config.json`.
|
|
69
|
+
* Pre-fix the route accepted arbitrary JSON via `as Partial<GlobalConfig>`;
|
|
70
|
+
* a malformed payload (wrong types, non-object firebaseConfig) could
|
|
71
|
+
* break login on next boot. Every accepted field is now type-checked
|
|
72
|
+
* inline.
|
|
73
|
+
*/
|
|
74
|
+
function validateGlobalConfigPatch(
|
|
75
|
+
input: unknown,
|
|
76
|
+
): { ok: true; value: Partial<GlobalConfig> } | { ok: false; error: string } {
|
|
77
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
78
|
+
return { ok: false, error: 'Body must be a JSON object' }
|
|
79
|
+
}
|
|
80
|
+
const o = input as Record<string, unknown>
|
|
81
|
+
const out: Partial<GlobalConfig> = {}
|
|
82
|
+
|
|
83
|
+
if ('firebaseConfig' in o) {
|
|
84
|
+
if (o.firebaseConfig === null) {
|
|
85
|
+
;(out as Record<string, unknown>).firebaseConfig = null
|
|
86
|
+
} else {
|
|
87
|
+
if (!o.firebaseConfig || typeof o.firebaseConfig !== 'object') {
|
|
88
|
+
return { ok: false, error: 'firebaseConfig must be an object or null' }
|
|
89
|
+
}
|
|
90
|
+
const fc = o.firebaseConfig as Record<string, unknown>
|
|
91
|
+
for (const k of ['apiKey', 'authDomain', 'projectId'] as const) {
|
|
92
|
+
if (typeof fc[k] !== 'string' || !fc[k]) {
|
|
93
|
+
return { ok: false, error: `firebaseConfig.${k} must be a non-empty string` }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
out.firebaseConfig = {
|
|
97
|
+
apiKey: fc.apiKey as string,
|
|
98
|
+
authDomain: fc.authDomain as string,
|
|
99
|
+
projectId: fc.projectId as string,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ('theme' in o) {
|
|
105
|
+
if (o.theme === null) {
|
|
106
|
+
;(out as Record<string, unknown>).theme = null
|
|
107
|
+
} else {
|
|
108
|
+
if (!o.theme || typeof o.theme !== 'object') {
|
|
109
|
+
return { ok: false, error: 'theme must be an object or null' }
|
|
110
|
+
}
|
|
111
|
+
const t = o.theme as Record<string, unknown>
|
|
112
|
+
const theme: NonNullable<GlobalConfig['theme']> = {}
|
|
113
|
+
for (const k of ['primaryColor', 'brandName', 'logo'] as const) {
|
|
114
|
+
if (k in t) {
|
|
115
|
+
if (typeof t[k] !== 'string') {
|
|
116
|
+
return { ok: false, error: `theme.${k} must be a string` }
|
|
117
|
+
}
|
|
118
|
+
theme[k] = t[k] as string
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
out.theme = theme
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Reject unknown top-level keys so a forged-admin attacker can't
|
|
126
|
+
// smuggle fields the schema will later trust.
|
|
127
|
+
for (const k of Object.keys(o)) {
|
|
128
|
+
if (k !== 'firebaseConfig' && k !== 'theme') {
|
|
129
|
+
return { ok: false, error: `unknown top-level field: ${k}` }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, value: out }
|
|
133
|
+
}
|
|
134
|
+
|
|
61
135
|
// ---------------------------------------------------------------------------
|
|
62
136
|
// Helpers
|
|
63
137
|
//
|
|
@@ -71,8 +145,9 @@ export const PUT: APIRoute = async ({ request, cookies }) => {
|
|
|
71
145
|
// ---------------------------------------------------------------------------
|
|
72
146
|
|
|
73
147
|
async function getStorageParams() {
|
|
74
|
-
const serverConfig = (
|
|
75
|
-
|
|
148
|
+
const serverConfig = (
|
|
149
|
+
globalThis as { __SETZKASTEN_CONFIG__?: { storage?: { contentPath?: string } } }
|
|
150
|
+
).__SETZKASTEN_CONFIG__
|
|
76
151
|
const storage = resolveStorageConfig()
|
|
77
152
|
if (!storage) return null
|
|
78
153
|
const tokenResult = await resolveConfigRepoToken()
|
|
@@ -94,13 +169,20 @@ export async function readGlobalConfig(): Promise<GlobalConfig | null> {
|
|
|
94
169
|
return cachedFetch(key, 5 * 60_000, async () => {
|
|
95
170
|
const res = await fetch(
|
|
96
171
|
`https://api.github.com/repos/${owner}/${repo}/contents/${GLOBAL_CONFIG_FILE(contentPath)}?ref=${branch}`,
|
|
97
|
-
{
|
|
172
|
+
{
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${token}`,
|
|
175
|
+
Accept: 'application/vnd.github+json',
|
|
176
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
98
179
|
)
|
|
99
180
|
if (!res.ok) return null
|
|
100
|
-
const data = await res.json() as { content: string; encoding: string }
|
|
101
|
-
const raw =
|
|
102
|
-
|
|
103
|
-
|
|
181
|
+
const data = (await res.json()) as { content: string; encoding: string }
|
|
182
|
+
const raw =
|
|
183
|
+
data.encoding === 'base64'
|
|
184
|
+
? Buffer.from(data.content, 'base64').toString('utf-8')
|
|
185
|
+
: data.content
|
|
104
186
|
return JSON.parse(raw) as GlobalConfig
|
|
105
187
|
})
|
|
106
188
|
}
|
|
@@ -126,10 +208,12 @@ export async function writeGlobalConfig(config: GlobalConfig): Promise<void> {
|
|
|
126
208
|
{ headers },
|
|
127
209
|
)
|
|
128
210
|
if (existing.ok) {
|
|
129
|
-
const data = await existing.json() as { sha: string }
|
|
211
|
+
const data = (await existing.json()) as { sha: string }
|
|
130
212
|
sha = data.sha
|
|
131
213
|
}
|
|
132
|
-
} catch {
|
|
214
|
+
} catch {
|
|
215
|
+
/* file doesn't exist yet */
|
|
216
|
+
}
|
|
133
217
|
|
|
134
218
|
const body: Record<string, unknown> = {
|
|
135
219
|
message: withTrailers('chore(config): update global config'),
|
|
@@ -138,10 +222,11 @@ export async function writeGlobalConfig(config: GlobalConfig): Promise<void> {
|
|
|
138
222
|
}
|
|
139
223
|
if (sha) body.sha = sha
|
|
140
224
|
|
|
141
|
-
const res = await fetch(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
225
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`, {
|
|
226
|
+
method: 'PUT',
|
|
227
|
+
headers,
|
|
228
|
+
body: JSON.stringify(body),
|
|
229
|
+
})
|
|
145
230
|
if (!res.ok) {
|
|
146
231
|
const text = await res.text()
|
|
147
232
|
throw new Error(`GitHub write failed: ${text}`)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
|
-
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
3
|
-
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
4
2
|
import { parseSession, requireAdmin } from './_auth-guard'
|
|
5
3
|
import { withTrailers } from './_commit-trailers'
|
|
6
4
|
import { invalidateCache } from './_github-cache'
|
|
5
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
6
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
7
7
|
|
|
8
8
|
interface RollbackBody {
|
|
9
9
|
path?: string
|
|
@@ -57,6 +57,21 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
57
57
|
}
|
|
58
58
|
const { owner, repo, branch } = storage
|
|
59
59
|
|
|
60
|
+
// Restrict rollback to the content tree. Pre-fix, an admin (or anyone
|
|
61
|
+
// who could forge an admin cookie pre-C1) could roll back any path —
|
|
62
|
+
// including `.github/workflows/deploy.yml`, `setzkasten.config.ts`,
|
|
63
|
+
// or arbitrary source — to any historical SHA.
|
|
64
|
+
const serverConfig = (globalThis as Record<string, unknown>).__SETZKASTEN_CONFIG__ as
|
|
65
|
+
| { storage?: { contentPath?: string } }
|
|
66
|
+
| undefined
|
|
67
|
+
const contentPath = serverConfig?.storage?.contentPath ?? 'content'
|
|
68
|
+
if (!isPathInsideContent(path, contentPath)) {
|
|
69
|
+
return Response.json(
|
|
70
|
+
{ error: `Rollback restricted to the content folder (${contentPath}/...)` },
|
|
71
|
+
{ status: 400 },
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
const headers = {
|
|
61
76
|
Authorization: `Bearer ${tokenResult.value}`,
|
|
62
77
|
Accept: 'application/vnd.github+json',
|
|
@@ -76,10 +91,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
76
91
|
)
|
|
77
92
|
}
|
|
78
93
|
if (!versionRes.ok) {
|
|
79
|
-
return Response.json(
|
|
80
|
-
{ error: `Failed to read version: ${versionRes.status}` },
|
|
81
|
-
{ status: 502 },
|
|
82
|
-
)
|
|
94
|
+
return Response.json({ error: `Failed to read version: ${versionRes.status}` }, { status: 502 })
|
|
83
95
|
}
|
|
84
96
|
const versionData = (await versionRes.json()) as {
|
|
85
97
|
content: string
|
|
@@ -114,10 +126,7 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
114
126
|
// 3. Write new commit with the historical content
|
|
115
127
|
const shortSha = sha.slice(0, 7)
|
|
116
128
|
const fileName = path.split('/').pop() ?? path
|
|
117
|
-
const message = withTrailers(
|
|
118
|
-
`revert(${fileName}): rollback to ${shortSha}`,
|
|
119
|
-
session.user.email,
|
|
120
|
-
)
|
|
129
|
+
const message = withTrailers(`revert(${fileName}): rollback to ${shortSha}`, session.user.email)
|
|
121
130
|
|
|
122
131
|
const putBody: Record<string, unknown> = {
|
|
123
132
|
message,
|
|
@@ -126,10 +135,11 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
126
135
|
}
|
|
127
136
|
if (currentSha) putBody.sha = currentSha
|
|
128
137
|
|
|
129
|
-
const putRes = await fetch(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
const putRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`, {
|
|
139
|
+
method: 'PUT',
|
|
140
|
+
headers,
|
|
141
|
+
body: JSON.stringify(putBody),
|
|
142
|
+
})
|
|
133
143
|
|
|
134
144
|
if (!putRes.ok) {
|
|
135
145
|
const text = await putRes.text()
|
|
@@ -142,3 +152,20 @@ export const POST: APIRoute = async ({ request, cookies }) => {
|
|
|
142
152
|
const putData = (await putRes.json()) as { commit: { sha: string } }
|
|
143
153
|
return Response.json({ ok: true, commitSha: putData.commit.sha })
|
|
144
154
|
}
|
|
155
|
+
|
|
156
|
+
/** True when `target` is a sub-path of `base` after path normalisation.
|
|
157
|
+
* Forbids `..` segments, leading/embedded NUL, absolute paths, and
|
|
158
|
+
* Windows-style backslashes (the API accepts forward slashes only). */
|
|
159
|
+
function isPathInsideContent(target: string, base: string): boolean {
|
|
160
|
+
if (typeof target !== 'string' || target.length === 0) return false
|
|
161
|
+
if (target.includes('\0') || target.includes('\\')) return false
|
|
162
|
+
if (target.startsWith('/')) return false
|
|
163
|
+
|
|
164
|
+
const segments = target.split('/')
|
|
165
|
+
for (const seg of segments) {
|
|
166
|
+
if (seg === '' || seg === '.' || seg === '..') return false
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const normalizedBase = base.replace(/^\/+|\/+$/g, '')
|
|
170
|
+
return target === normalizedBase || target.startsWith(`${normalizedBase}/`)
|
|
171
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro'
|
|
2
|
-
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
3
|
-
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
4
2
|
import { requireAdmin } from './_auth-guard'
|
|
5
3
|
import { cachedFetch } from './_github-cache'
|
|
4
|
+
import { resolveGitHubTokenForRequest } from './_github-token'
|
|
5
|
+
import { resolveStorageConfigForRequest } from './_storage-config'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* GET /api/setzkasten/history/version?path=<file>&sha=<commit-sha>
|
|
@@ -31,9 +31,7 @@ export const GET: APIRoute = async ({ request, url, cookies }) => {
|
|
|
31
31
|
|
|
32
32
|
const cacheKey = `history-version:${owner}/${repo}:${path}:${sha}`
|
|
33
33
|
const result = await cachedFetch(cacheKey, 5 * 60_000, async () => {
|
|
34
|
-
const u = new URL(
|
|
35
|
-
`https://api.github.com/repos/${owner}/${repo}/contents/${path}`,
|
|
36
|
-
)
|
|
34
|
+
const u = new URL(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`)
|
|
37
35
|
u.searchParams.set('ref', sha)
|
|
38
36
|
const res = await fetch(u, {
|
|
39
37
|
headers: {
|
|
@@ -42,7 +40,8 @@ export const GET: APIRoute = async ({ request, url, cookies }) => {
|
|
|
42
40
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
43
41
|
},
|
|
44
42
|
})
|
|
45
|
-
if (res.status === 404)
|
|
43
|
+
if (res.status === 404)
|
|
44
|
+
return { ok: false as const, status: 404, error: 'File not found at given sha' }
|
|
46
45
|
if (!res.ok) return { ok: false as const, status: 502, error: `GitHub returned ${res.status}` }
|
|
47
46
|
const data = (await res.json()) as { content: string; encoding: string; sha: string }
|
|
48
47
|
const raw =
|