@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.
Files changed (166) hide show
  1. package/dist/api-routes/_auth-guard.d.ts +27 -3
  2. package/dist/api-routes/_auth-guard.js +5 -2
  3. package/dist/api-routes/_dev-session-secret.d.ts +8 -0
  4. package/dist/api-routes/_dev-session-secret.js +8 -0
  5. package/dist/api-routes/_github-token.js +1 -1
  6. package/dist/api-routes/_role-resolver.js +6 -3
  7. package/dist/api-routes/_session-secret.d.ts +19 -0
  8. package/dist/api-routes/_session-secret.js +7 -0
  9. package/dist/api-routes/_session-signing.d.ts +45 -0
  10. package/dist/api-routes/_session-signing.js +8 -0
  11. package/dist/api-routes/_webhook-dispatcher.js +4 -4
  12. package/dist/api-routes/asset-proxy.js +1 -1
  13. package/dist/api-routes/auth-callback.js +12 -5
  14. package/dist/api-routes/auth-logout.d.ts +4 -4
  15. package/dist/api-routes/auth-logout.js +8 -2
  16. package/dist/api-routes/auth-session.d.ts +6 -0
  17. package/dist/api-routes/auth-session.js +19 -19
  18. package/dist/api-routes/auth-setzkasten-login.js +14 -7
  19. package/dist/api-routes/catalog-add.js +59 -17
  20. package/dist/api-routes/catalog-export.js +14 -4
  21. package/dist/api-routes/config.d.ts +10 -3
  22. package/dist/api-routes/config.js +26 -4
  23. package/dist/api-routes/deploy-hook.js +8 -8
  24. package/dist/api-routes/editors.d.ts +1 -1
  25. package/dist/api-routes/editors.js +5 -2
  26. package/dist/api-routes/github-proxy.js +30 -8
  27. package/dist/api-routes/global-config.js +6 -3
  28. package/dist/api-routes/history-rollback.js +31 -14
  29. package/dist/api-routes/history-version.js +8 -6
  30. package/dist/api-routes/history.js +5 -2
  31. package/dist/api-routes/icons-local.js +1 -1
  32. package/dist/api-routes/init-add-section.js +113 -47
  33. package/dist/api-routes/init-apply.js +56 -42
  34. package/dist/api-routes/init-migrate.js +43 -36
  35. package/dist/api-routes/init-scan-page.d.ts +1 -1
  36. package/dist/api-routes/init-scan-page.js +59 -13
  37. package/dist/api-routes/init-scan.js +22 -7
  38. package/dist/api-routes/migrate-to-multi.js +5 -2
  39. package/dist/api-routes/pages.js +15 -4
  40. package/dist/api-routes/section-add.js +68 -16
  41. package/dist/api-routes/section-commit-pending.js +70 -22
  42. package/dist/api-routes/section-delete.js +49 -14
  43. package/dist/api-routes/section-duplicate.js +65 -16
  44. package/dist/api-routes/section-prepare-copy.js +15 -2
  45. package/dist/api-routes/section-prepare.js +25 -4
  46. package/dist/api-routes/setup-github-app-bounce.js +15 -1
  47. package/dist/api-routes/setup-github-app-branches.js +9 -6
  48. package/dist/api-routes/setup-github-app-callback.js +24 -1
  49. package/dist/api-routes/setup-github-app-credentials.d.ts +27 -0
  50. package/dist/api-routes/setup-github-app-credentials.js +43 -0
  51. package/dist/api-routes/setup-github-app-installed.js +22 -1
  52. package/dist/api-routes/setup-github-app-repos.js +5 -2
  53. package/dist/api-routes/setup-github-app.d.ts +4 -0
  54. package/dist/api-routes/setup-github-app.js +19 -2
  55. package/dist/api-routes/updater-register.js +7 -1
  56. package/dist/api-routes/webhooks-status.js +5 -2
  57. package/dist/api-routes/webhooks-test.js +9 -8
  58. package/dist/api-routes/webhooks.js +12 -14
  59. package/dist/api-routes/websites-add.js +5 -2
  60. package/dist/api-routes/websites-remove.js +5 -2
  61. package/dist/{chunk-ZQDGGWJP.js → chunk-5KMGSFCZ.js} +2 -2
  62. package/dist/{chunk-Q3N336KR.js → chunk-CDXCYYQR.js} +29 -24
  63. package/dist/{chunk-NKDATSPA.js → chunk-DP6RTINQ.js} +1 -1
  64. package/dist/chunk-KENFINT4.js +76 -0
  65. package/dist/chunk-ONP6BRZO.js +47 -0
  66. package/dist/{chunk-INIWFKQ3.js → chunk-Q5HV47DW.js} +33 -19
  67. package/dist/chunk-QVCW6EF3.js +26 -0
  68. package/dist/{chunk-TD76R3A6.js → chunk-UHI6323G.js} +293 -174
  69. package/dist/{chunk-AM4DZXXM.js → chunk-UJAFZEX2.js} +76 -9
  70. package/package.json +12 -6
  71. package/src/api-routes/__tests__/_session-signing.test.ts +114 -0
  72. package/src/api-routes/__tests__/_session-test-helper.ts +27 -0
  73. package/src/api-routes/__tests__/add-section-helpers.test.ts +59 -25
  74. package/src/api-routes/__tests__/auth-guard.test.ts +46 -7
  75. package/src/api-routes/__tests__/catalog-api.test.ts +14 -6
  76. package/src/api-routes/__tests__/commit-trailers.test.ts +5 -5
  77. package/src/api-routes/__tests__/deferred-operations.test.ts +9 -12
  78. package/src/api-routes/__tests__/deploy-hook.test.ts +3 -8
  79. package/src/api-routes/__tests__/feature-gate.test.ts +1 -1
  80. package/src/api-routes/__tests__/github-cache.test.ts +1 -1
  81. package/src/api-routes/__tests__/github-token.test.ts +1 -1
  82. package/src/api-routes/__tests__/global-config-theme.test.ts +4 -4
  83. package/src/api-routes/__tests__/history-rollback.test.ts +6 -3
  84. package/src/api-routes/__tests__/history.test.ts +9 -6
  85. package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +11 -3
  86. package/src/api-routes/__tests__/migrate-to-multi.test.ts +5 -1
  87. package/src/api-routes/__tests__/pages-meta-store.test.ts +10 -5
  88. package/src/api-routes/__tests__/pages.test.ts +7 -2
  89. package/src/api-routes/__tests__/patch-page-file.test.ts +71 -19
  90. package/src/api-routes/__tests__/route-registry.test.ts +11 -18
  91. package/src/api-routes/__tests__/scan-page-helpers.test.ts +13 -10
  92. package/src/api-routes/__tests__/section-management.test.ts +28 -28
  93. package/src/api-routes/__tests__/setup-github-app-callback.test.ts +58 -16
  94. package/src/api-routes/__tests__/setup-github-app-repos.test.ts +4 -5
  95. package/src/api-routes/__tests__/setup-github-app.test.ts +27 -7
  96. package/src/api-routes/__tests__/storage-config-for-request.test.ts +83 -0
  97. package/src/api-routes/__tests__/updater-register.test.ts +230 -0
  98. package/src/api-routes/__tests__/webhook-signing.test.ts +1 -1
  99. package/src/api-routes/__tests__/webhooks.test.ts +19 -7
  100. package/src/api-routes/__tests__/websites-add.test.ts +2 -1
  101. package/src/api-routes/__tests__/websites-remove.test.ts +2 -1
  102. package/src/api-routes/_auth-guard.ts +47 -15
  103. package/src/api-routes/_commit-trailers.ts +3 -2
  104. package/src/api-routes/_dev-session-secret.ts +79 -0
  105. package/src/api-routes/_github-token.ts +1 -1
  106. package/src/api-routes/_pages-meta-store.ts +2 -2
  107. package/src/api-routes/_role-resolver.ts +7 -5
  108. package/src/api-routes/_session-secret.ts +46 -0
  109. package/src/api-routes/_session-signing.ts +135 -0
  110. package/src/api-routes/_vercel-origin.ts +2 -6
  111. package/src/api-routes/_webhook-dispatcher.ts +12 -16
  112. package/src/api-routes/_website-resolver.ts +3 -10
  113. package/src/api-routes/auth-callback.ts +9 -5
  114. package/src/api-routes/auth-login.ts +5 -3
  115. package/src/api-routes/auth-logout.ts +18 -1
  116. package/src/api-routes/auth-session.ts +13 -21
  117. package/src/api-routes/auth-setzkasten-login.ts +12 -9
  118. package/src/api-routes/catalog-add.ts +89 -31
  119. package/src/api-routes/catalog-export.ts +30 -10
  120. package/src/api-routes/config.ts +39 -6
  121. package/src/api-routes/deploy-hook.ts +13 -11
  122. package/src/api-routes/editors.ts +33 -22
  123. package/src/api-routes/github-proxy.ts +25 -11
  124. package/src/api-routes/global-config.ts +103 -18
  125. package/src/api-routes/history-rollback.ts +41 -14
  126. package/src/api-routes/history-version.ts +5 -6
  127. package/src/api-routes/history.ts +3 -3
  128. package/src/api-routes/icons-local.ts +2 -2
  129. package/src/api-routes/init-add-section.ts +174 -79
  130. package/src/api-routes/init-apply.ts +71 -56
  131. package/src/api-routes/init-migrate.ts +54 -48
  132. package/src/api-routes/init-scan-page.ts +77 -30
  133. package/src/api-routes/init-scan.ts +19 -11
  134. package/src/api-routes/pages.ts +16 -11
  135. package/src/api-routes/section-add.ts +98 -27
  136. package/src/api-routes/section-commit-pending.ts +87 -34
  137. package/src/api-routes/section-delete.ts +76 -27
  138. package/src/api-routes/section-duplicate.ts +95 -28
  139. package/src/api-routes/section-management.ts +3 -7
  140. package/src/api-routes/section-prepare-copy.ts +29 -8
  141. package/src/api-routes/section-prepare.ts +38 -10
  142. package/src/api-routes/setup-github-app-bounce.ts +7 -1
  143. package/src/api-routes/setup-github-app-branches.ts +6 -7
  144. package/src/api-routes/setup-github-app-callback.ts +18 -1
  145. package/src/api-routes/setup-github-app-credentials.ts +55 -0
  146. package/src/api-routes/setup-github-app-installed.ts +12 -1
  147. package/src/api-routes/setup-github-app-repos.ts +2 -3
  148. package/src/api-routes/setup-github-app.ts +14 -5
  149. package/src/api-routes/updater-check.ts +6 -4
  150. package/src/api-routes/updater-register.ts +34 -20
  151. package/src/api-routes/updater-transfer.ts +8 -6
  152. package/src/api-routes/updater-unbind.ts +14 -10
  153. package/src/api-routes/webhooks-test.ts +9 -11
  154. package/src/api-routes/webhooks.ts +15 -19
  155. package/src/init/__tests__/page-level.test.ts +279 -105
  156. package/src/init/__tests__/page-list-coverage.test.ts +70 -70
  157. package/src/init/__tests__/patcher-child-component.test.ts +12 -3
  158. package/src/init/__tests__/patcher-edge-cases.test.ts +47 -23
  159. package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +16 -6
  160. package/src/init/__tests__/patcher-page-mode.test.ts +30 -20
  161. package/src/init/__tests__/section-pipeline.test.ts +53 -19
  162. package/src/init/astro-config-patcher.ts +4 -18
  163. package/src/init/astro-detector.ts +2 -7
  164. package/src/init/astro-section-analyzer-v2.ts +475 -193
  165. package/src/init/field-label-enricher.ts +6 -6
  166. package/src/init/template-patcher-v2.ts +218 -97
@@ -8,8 +8,8 @@
8
8
  * - Commits triggered on every duplicate/delete instead of batched
9
9
  */
10
10
 
11
- import { describe, it, expect } from 'vitest'
12
- import { generateDuplicateKey, duplicateInPageConfig } from '../section-management'
11
+ import { describe, expect, it } from 'vitest'
12
+ import { duplicateInPageConfig, generateDuplicateKey } from '../section-management'
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
15
  // 1. Duplicate key generation (reused by prepare-copy)
@@ -25,8 +25,9 @@ describe('generateDuplicateKey (used by prepare-copy)', () => {
25
25
  })
26
26
 
27
27
  it('increments suffix until unique', () => {
28
- expect(generateDuplicateKey(['hero', 'hero--copy', 'hero--copy2', 'hero--copy3'], 'hero'))
29
- .toBe('hero--copy4')
28
+ expect(generateDuplicateKey(['hero', 'hero--copy', 'hero--copy2', 'hero--copy3'], 'hero')).toBe(
29
+ 'hero--copy4',
30
+ )
30
31
  })
31
32
 
32
33
  it('generates copy of a copy', () => {
@@ -82,9 +83,7 @@ describe('duplicateInPageConfig (used by prepare-copy)', () => {
82
83
 
83
84
  it('preserves type field from original entry when present', () => {
84
85
  const configWithType = {
85
- sections: [
86
- { key: 'hero--about', type: 'hero', enabled: true, order: 0 },
87
- ],
86
+ sections: [{ key: 'hero--about', type: 'hero', enabled: true, order: 0 }],
88
87
  }
89
88
  const result = duplicateInPageConfig(configWithType, 'hero--about', 'hero--about--copy')
90
89
  const copy = result.sections.find((s: any) => s.key === 'hero--about--copy')
@@ -98,9 +97,7 @@ describe('duplicateInPageConfig (used by prepare-copy)', () => {
98
97
  // getSectionDef looks up 'testPricing--copy' in the catalog → not found → empty editor.
99
98
  // resolveSectionType returns 'testPricing--copy' → no component → not in preview.
100
99
  const configNoType = {
101
- sections: [
102
- { key: 'testPricing', enabled: true, order: 0 },
103
- ],
100
+ sections: [{ key: 'testPricing', enabled: true, order: 0 }],
104
101
  }
105
102
  const result = duplicateInPageConfig(configNoType, 'testPricing', 'testPricing--copy')
106
103
  const copy = result.sections.find((s: any) => s.key === 'testPricing--copy')
@@ -169,7 +166,7 @@ describe('commit-pending: commit message', () => {
169
166
  function buildCommitMessage(pageKey: string, sections: Array<{ key: string }>): string {
170
167
  const parts: string[] = []
171
168
  if (sections.length > 0) {
172
- const keys = sections.map(s => s.key).join(', ')
169
+ const keys = sections.map((s) => s.key).join(', ')
173
170
  parts.push(`add ${sections.length} section${sections.length > 1 ? 's' : ''} (${keys})`)
174
171
  }
175
172
  return `content: ${parts.length > 0 ? parts.join(', ') : 'update page config'} on ${pageKey}`
@@ -210,7 +207,7 @@ describe('prepare-copy: type resolution', () => {
210
207
  sections: Array<{ key: string; type?: string }>,
211
208
  sectionKey: string,
212
209
  ): string {
213
- const entry = sections.find(s => s.key === sectionKey)
210
+ const entry = sections.find((s) => s.key === sectionKey)
214
211
  return entry?.type ?? sectionKey
215
212
  }
216
213
 
@@ -10,7 +10,7 @@
10
10
  * 6. Secret header is sent when configured
11
11
  */
12
12
 
13
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
13
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
14
14
 
15
15
  // We import the POST handler directly; Astro's APIRoute type is just a function.
16
16
  // We construct a minimal mock context manually.
@@ -40,9 +40,7 @@ describe('deploy-hook POST handler', () => {
40
40
  return {
41
41
  cookies: {
42
42
  get: (name: string) =>
43
- name === 'setzkasten_session' && sessionValue
44
- ? { value: sessionValue }
45
- : undefined,
43
+ name === 'setzkasten_session' && sessionValue ? { value: sessionValue } : undefined,
46
44
  },
47
45
  }
48
46
  }
@@ -64,10 +62,7 @@ describe('deploy-hook POST handler', () => {
64
62
  ;(globalThis as any).__SETZKASTEN_CONFIG__ = {
65
63
  deployHook: { url: 'https://example.com/hook' },
66
64
  }
67
- vi.stubGlobal(
68
- 'fetch',
69
- vi.fn().mockResolvedValue({ ok: true, status: 200 } as Response),
70
- )
65
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200 } as Response))
71
66
 
72
67
  const res = await POST(makeCtx('tok'))
73
68
  expect(res.status).toBe(200)
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
2
  import { gateFeature } from '../_feature-gate'
3
3
 
4
4
  const ORIGINAL_KEY = process.env.SETZKASTEN_LICENSE_KEY
@@ -3,7 +3,7 @@
3
3
  * @vitest-environment node
4
4
  */
5
5
 
6
- import { describe, it, expect, vi, beforeEach } from 'vitest'
6
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
7
7
  import { cachedFetch, invalidateCache } from '../_github-cache'
8
8
 
9
9
  beforeEach(() => {
@@ -6,8 +6,8 @@
6
6
  * 2. Nicht alle Vars gesetzt → auth error Result
7
7
  */
8
8
 
9
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
10
9
  import { generateKeyPairSync } from 'node:crypto'
10
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
11
11
 
12
12
  const { privateKey: TEST_KEY } = generateKeyPairSync('rsa', { modulusLength: 2048 })
13
13
  const TEST_PRIVATE_KEY_PEM = TEST_KEY.export({ type: 'pkcs8', format: 'pem' }) as string
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  vi.mock('../_storage-config', () => ({
4
4
  resolveStorageConfig: vi.fn(() => ({ owner: 'o', repo: 'r', branch: 'main' })),
@@ -8,7 +8,7 @@ vi.stubGlobal('__SETZKASTEN_CONFIG__', { storage: { contentPath: 'content' } })
8
8
 
9
9
  describe('GlobalConfig – theme field', () => {
10
10
  it('GlobalConfig type accepts a theme object', async () => {
11
- const { } = await import('../global-config')
11
+ const {} = await import('../global-config')
12
12
  // Type-level check: if this compiles the type is correct
13
13
  const cfg = {
14
14
  theme: { primaryColor: '#ff0000', brandName: 'Acme', logo: '/logo.png' },
@@ -43,7 +43,7 @@ describe('config.ts – theme merge', () => {
43
43
 
44
44
  const { GET } = await import('../config')
45
45
  const res = await GET({} as any)
46
- const body = await res.json() as any
46
+ const body = (await res.json()) as any
47
47
 
48
48
  expect(body.theme.primaryColor).toBe('#abc123')
49
49
  expect(body.theme.brandName).toBe('Dynamic Brand')
@@ -63,7 +63,7 @@ describe('config.ts – theme merge', () => {
63
63
 
64
64
  const { GET } = await import('../config')
65
65
  const res = await GET({} as any)
66
- const body = await res.json() as any
66
+ const body = (await res.json()) as any
67
67
 
68
68
  expect(body.theme.primaryColor).toBe('#ffffff')
69
69
  expect(body.theme.brandName).toBe('Static Brand')
@@ -4,16 +4,17 @@
4
4
 
5
5
  import { generateKeyPairSync } from 'node:crypto'
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+ import { makeTestSessionCookie } from './_session-test-helper'
7
8
 
8
9
  const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
9
10
  const PEM = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
10
11
 
11
- const ADMIN_SESSION = JSON.stringify({
12
+ const ADMIN_SESSION = makeTestSessionCookie({
12
13
  user: { id: 'u1', email: 'admin@example.com', role: 'admin', provider: 'github' },
13
14
  expiresAt: Date.now() + 60 * 60 * 1000,
14
15
  })
15
16
 
16
- const EDITOR_SESSION = JSON.stringify({
17
+ const EDITOR_SESSION = makeTestSessionCookie({
17
18
  user: { id: 'u2', email: 'editor@example.com', role: 'editor', provider: 'google' },
18
19
  expiresAt: Date.now() + 60 * 60 * 1000,
19
20
  })
@@ -88,7 +89,9 @@ describe('POST /api/setzkasten/history/rollback', () => {
88
89
 
89
90
  it('returns 400 when path or sha missing', async () => {
90
91
  const { POST } = await import('../history-rollback')
91
- const res = await (POST as (ctx: unknown) => Promise<Response>)(makeCtx({ path: ROLLBACK_PATH }))
92
+ const res = await (POST as (ctx: unknown) => Promise<Response>)(
93
+ makeCtx({ path: ROLLBACK_PATH }),
94
+ )
92
95
  expect(res.status).toBe(400)
93
96
  })
94
97
 
@@ -8,17 +8,22 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
8
8
  const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
9
9
  const PEM = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
10
10
 
11
- const ADMIN_SESSION = JSON.stringify({
11
+ import { makeTestSessionCookie } from './_session-test-helper'
12
+
13
+ const ADMIN_SESSION = makeTestSessionCookie({
12
14
  user: { id: 'u1', email: 'admin@example.com', role: 'admin', provider: 'github' },
13
15
  expiresAt: Date.now() + 60 * 60 * 1000,
14
16
  })
15
17
 
16
- const EDITOR_SESSION = JSON.stringify({
18
+ const EDITOR_SESSION = makeTestSessionCookie({
17
19
  user: { id: 'u2', email: 'editor@example.com', role: 'editor', provider: 'google' },
18
20
  expiresAt: Date.now() + 60 * 60 * 1000,
19
21
  })
20
22
 
21
- function makeCtx(searchParams: Record<string, string>, sessionValue: string | null = ADMIN_SESSION) {
23
+ function makeCtx(
24
+ searchParams: Record<string, string>,
25
+ sessionValue: string | null = ADMIN_SESSION,
26
+ ) {
22
27
  const url = new URL('https://cms.example.com/api/setzkasten/history')
23
28
  for (const [k, v] of Object.entries(searchParams)) url.searchParams.set(k, v)
24
29
  const request = new Request(url, { method: 'GET' })
@@ -136,9 +141,7 @@ describe('GET /api/setzkasten/history', () => {
136
141
  authorEmail: 'maria@example.com',
137
142
  message: 'Update hero',
138
143
  })
139
- expect(body.commits[0].coAuthors).toEqual([
140
- { name: 'Editor', email: 'editor@example.com' },
141
- ])
144
+ expect(body.commits[0].coAuthors).toEqual([{ name: 'Editor', email: 'editor@example.com' }])
142
145
  })
143
146
 
144
147
  it('returns empty list when GitHub returns 404 for the path', async () => {
@@ -14,7 +14,7 @@
14
14
  * @vitest-environment node
15
15
  */
16
16
 
17
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
17
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
18
18
  import { resolveFullConfig } from '../init-scan-page'
19
19
 
20
20
  const SAMPLE_CONFIG = {
@@ -23,8 +23,16 @@ const SAMPLE_CONFIG = {
23
23
  website: {
24
24
  label: 'Website',
25
25
  sections: {
26
- _layout_header: { label: 'header', icon: 'panel-top', fields: { items: { type: 'array' } } },
27
- _layout_footer: { label: 'footer', icon: 'file-text', fields: { description: { type: 'text' } } },
26
+ _layout_header: {
27
+ label: 'header',
28
+ icon: 'panel-top',
29
+ fields: { items: { type: 'array' } },
30
+ },
31
+ _layout_footer: {
32
+ label: 'footer',
33
+ icon: 'file-text',
34
+ fields: { description: { type: 'text' } },
35
+ },
28
36
  },
29
37
  },
30
38
  },
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { generateKeyPairSync } from 'node:crypto'
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+ import { makeTestSessionCookie } from './_session-test-helper'
7
8
 
8
9
  const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
9
10
  const PEM = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
@@ -15,7 +16,10 @@ function makeCtx(body: unknown, sessionValue = 'valid', role: 'admin' | 'editor'
15
16
  headers: { 'content-type': 'application/json' },
16
17
  })
17
18
  const sessionPayload = sessionValue
18
- ? JSON.stringify({ user: { email: 'a@b.com', role }, expiresAt: Date.now() + 60_000 })
19
+ ? makeTestSessionCookie({
20
+ user: { id: 'test-user', email: 'a@b.com', role, provider: 'github' },
21
+ expiresAt: Date.now() + 60_000,
22
+ })
19
23
  : ''
20
24
  return {
21
25
  request,
@@ -21,7 +21,9 @@ afterEach(() => {
21
21
  vi.unstubAllEnvs()
22
22
  })
23
23
 
24
- function fetchSequence(steps: Array<(url: string, init?: RequestInit) => Response | Promise<Response>>) {
24
+ function fetchSequence(
25
+ steps: Array<(url: string, init?: RequestInit) => Response | Promise<Response>>,
26
+ ) {
25
27
  let i = 0
26
28
  const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
27
29
  const handler = steps[Math.min(i++, steps.length - 1)]
@@ -51,7 +53,10 @@ describe('readPagesMeta', () => {
51
53
  fetchSequence([
52
54
  () =>
53
55
  new Response(
54
- JSON.stringify({ content: Buffer.from(JSON.stringify(meta)).toString('base64'), sha: 'abc' }),
56
+ JSON.stringify({
57
+ content: Buffer.from(JSON.stringify(meta)).toString('base64'),
58
+ sha: 'abc',
59
+ }),
55
60
  { status: 200, headers: { 'content-type': 'application/json' } },
56
61
  ),
57
62
  ])
@@ -156,9 +161,9 @@ describe('recordPageEdit', () => {
156
161
  () =>
157
162
  new Response(
158
163
  JSON.stringify({
159
- content: Buffer.from(JSON.stringify({ version: 1, pages: { x: { lastModified: 2 } } })).toString(
160
- 'base64',
161
- ),
164
+ content: Buffer.from(
165
+ JSON.stringify({ version: 1, pages: { x: { lastModified: 2 } } }),
166
+ ).toString('base64'),
162
167
  sha: 'second',
163
168
  }),
164
169
  { status: 200, headers: { 'content-type': 'application/json' } },
@@ -13,7 +13,7 @@
13
13
  * @vitest-environment node
14
14
  */
15
15
 
16
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
17
17
  import { resolvePages } from '../pages'
18
18
 
19
19
  interface PageInfo {
@@ -63,7 +63,12 @@ describe('resolvePages', () => {
63
63
  it('handles multiple pages including nested paths', () => {
64
64
  const pages: PageInfo[] = [
65
65
  { path: '/', pageKey: 'index', label: 'Startseite', hasConfig: true },
66
- { path: '/docs/architecture', pageKey: 'docs/architecture', label: 'Architecture', hasConfig: false },
66
+ {
67
+ path: '/docs/architecture',
68
+ pageKey: 'docs/architecture',
69
+ label: 'Architecture',
70
+ hasConfig: false,
71
+ },
67
72
  ]
68
73
  ;(globalThis as Record<string, unknown>).__SETZKASTEN_PAGES__ = pages
69
74
  expect(resolvePages()).toHaveLength(2)
@@ -9,8 +9,8 @@
9
9
  * Bugs here mean sections either don't appear in the page or appear twice.
10
10
  */
11
11
 
12
- import { describe, it, expect } from 'vitest'
13
- import { patchPageFile, calculateRelativePath } from '../init-add-section'
12
+ import { describe, expect, it } from 'vitest'
13
+ import { calculateRelativePath, patchPageFile } from '../init-add-section'
14
14
 
15
15
  // ---------------------------------------------------------------------------
16
16
  // Fixtures
@@ -64,29 +64,59 @@ describe('patchPageFile — new section injection', () => {
64
64
  const componentPath = 'src/components/sections/FeaturesSection.astro'
65
65
 
66
66
  it('should return a non-null result', () => {
67
- const result = patchPageFile(PAGE_WITH_SECTIONS, 'features', 'FeaturesSection', componentPath, pagePath)
67
+ const result = patchPageFile(
68
+ PAGE_WITH_SECTIONS,
69
+ 'features',
70
+ 'FeaturesSection',
71
+ componentPath,
72
+ pagePath,
73
+ )
68
74
  expect(result).not.toBeNull()
69
75
  })
70
76
 
71
77
  it('should add the import statement', () => {
72
- const result = patchPageFile(PAGE_WITH_SECTIONS, 'features', 'FeaturesSection', componentPath, pagePath)!
73
- expect(result).toContain("import FeaturesSection from")
78
+ const result = patchPageFile(
79
+ PAGE_WITH_SECTIONS,
80
+ 'features',
81
+ 'FeaturesSection',
82
+ componentPath,
83
+ pagePath,
84
+ )!
85
+ expect(result).toContain('import FeaturesSection from')
74
86
  })
75
87
 
76
88
  it('should include the correct relative path', () => {
77
- const result = patchPageFile(PAGE_WITH_SECTIONS, 'features', 'FeaturesSection', componentPath, pagePath)!
89
+ const result = patchPageFile(
90
+ PAGE_WITH_SECTIONS,
91
+ 'features',
92
+ 'FeaturesSection',
93
+ componentPath,
94
+ pagePath,
95
+ )!
78
96
  // From src/pages to src/components/sections → one level up → ../components/sections/...
79
97
  expect(result).toContain('../components/sections/FeaturesSection.astro')
80
98
  })
81
99
 
82
100
  it('should add the section key to SECTION_COMPONENTS', () => {
83
- const result = patchPageFile(PAGE_WITH_SECTIONS, 'features', 'FeaturesSection', componentPath, pagePath)!
101
+ const result = patchPageFile(
102
+ PAGE_WITH_SECTIONS,
103
+ 'features',
104
+ 'FeaturesSection',
105
+ componentPath,
106
+ pagePath,
107
+ )!
84
108
  expect(result).toContain('features')
85
109
  expect(result).toContain('FeaturesSection')
86
110
  })
87
111
 
88
112
  it('should preserve existing sections in the registry', () => {
89
- const result = patchPageFile(PAGE_WITH_SECTIONS, 'features', 'FeaturesSection', componentPath, pagePath)!
113
+ const result = patchPageFile(
114
+ PAGE_WITH_SECTIONS,
115
+ 'features',
116
+ 'FeaturesSection',
117
+ componentPath,
118
+ pagePath,
119
+ )!
90
120
  expect(result).toContain('HeroSection')
91
121
  expect(result).toContain("normalize('hero')")
92
122
  })
@@ -122,13 +152,25 @@ describe('patchPageFile — hardcoded tag removal', () => {
122
152
  it('should remove the hardcoded <HeroSection /> when adding to registry', () => {
123
153
  // PAGE_WITH_HARDCODED_COMPONENT has HeroSection imported but NOT in registry
124
154
  // → patchPageFile adds it to registry AND removes the hardcoded tag
125
- const result = patchPageFile(PAGE_WITH_HARDCODED_COMPONENT, 'hero', 'HeroSection', componentPath, pagePath)
155
+ const result = patchPageFile(
156
+ PAGE_WITH_HARDCODED_COMPONENT,
157
+ 'hero',
158
+ 'HeroSection',
159
+ componentPath,
160
+ pagePath,
161
+ )
126
162
  expect(result).not.toBeNull()
127
163
  expect(result).not.toMatch(/<HeroSection\s*\/>/)
128
164
  })
129
165
 
130
166
  it('should add the section to SECTION_COMPONENTS when removing hardcoded tag', () => {
131
- const result = patchPageFile(PAGE_WITH_HARDCODED_COMPONENT, 'hero', 'HeroSection', componentPath, pagePath)!
167
+ const result = patchPageFile(
168
+ PAGE_WITH_HARDCODED_COMPONENT,
169
+ 'hero',
170
+ 'HeroSection',
171
+ componentPath,
172
+ pagePath,
173
+ )!
132
174
  expect(result).toContain('hero')
133
175
  expect(result).toContain('HeroSection')
134
176
  })
@@ -157,7 +199,13 @@ const SECTION_COMPONENTS = {
157
199
  const pagePath = 'src/pages/docs/architecture.astro'
158
200
  const componentPath = 'src/components/sections/FeaturesSection.astro'
159
201
 
160
- const result = patchPageFile(PAGE_NESTED, 'features', 'FeaturesSection', componentPath, pagePath)
202
+ const result = patchPageFile(
203
+ PAGE_NESTED,
204
+ 'features',
205
+ 'FeaturesSection',
206
+ componentPath,
207
+ pagePath,
208
+ )
161
209
  if (result) {
162
210
  // From src/pages/docs to src/components/sections = ../../components/sections
163
211
  expect(result).toContain('../../components/sections/FeaturesSection.astro')
@@ -175,19 +223,23 @@ describe('calculateRelativePath', () => {
175
223
  })
176
224
 
177
225
  it('one level up', () => {
178
- expect(calculateRelativePath('src/pages', 'src/components/Hero.astro'))
179
- .toBe('../components/Hero.astro')
226
+ expect(calculateRelativePath('src/pages', 'src/components/Hero.astro')).toBe(
227
+ '../components/Hero.astro',
228
+ )
180
229
  })
181
230
 
182
231
  it('two levels up', () => {
183
- expect(calculateRelativePath('src/pages/docs', 'src/components/sections/Hero.astro'))
184
- .toBe('../../components/sections/Hero.astro')
232
+ expect(calculateRelativePath('src/pages/docs', 'src/components/sections/Hero.astro')).toBe(
233
+ '../../components/sections/Hero.astro',
234
+ )
185
235
  })
186
236
 
187
237
  it('deeper nesting in monorepo', () => {
188
- expect(calculateRelativePath(
189
- 'apps/website/src/pages',
190
- 'apps/website/src/components/sections/FooterSection.astro',
191
- )).toBe('../components/sections/FooterSection.astro')
238
+ expect(
239
+ calculateRelativePath(
240
+ 'apps/website/src/pages',
241
+ 'apps/website/src/components/sections/FooterSection.astro',
242
+ ),
243
+ ).toBe('../components/sections/FooterSection.astro')
192
244
  })
193
245
  })
@@ -15,14 +15,14 @@
15
15
  * ein Fehler, der erst beim Produktions-Build auffällt (ENOENT).
16
16
  */
17
17
 
18
- import { describe, it, expect } from 'vitest'
19
- import { readdirSync, readFileSync } from 'node:fs'
20
- import { resolve, dirname } from 'node:path'
18
+ import { readFileSync, readdirSync } from 'node:fs'
19
+ import { dirname, resolve } from 'node:path'
21
20
  import { fileURLToPath } from 'node:url'
21
+ import { describe, expect, it } from 'vitest'
22
22
 
23
23
  const __dirname = dirname(fileURLToPath(import.meta.url))
24
24
  // Test sits at src/api-routes/__tests__, so go up three levels
25
- const ADMIN_ROOT = resolve(__dirname, '../../../') // packages/astro-admin
25
+ const ADMIN_ROOT = resolve(__dirname, '../../../') // packages/astro-admin
26
26
  const ASTRO_ROOT = resolve(__dirname, '../../../../astro') // packages/astro
27
27
 
28
28
  // ---------------------------------------------------------------------------
@@ -30,17 +30,14 @@ const ASTRO_ROOT = resolve(__dirname, '../../../../astro') // packages/astro
30
30
  // ---------------------------------------------------------------------------
31
31
 
32
32
  const routeFiles = readdirSync(resolve(ADMIN_ROOT, 'src/api-routes')).filter(
33
- f => f.endsWith('.ts') && !f.endsWith('.test.ts'),
33
+ (f) => f.endsWith('.ts') && !f.endsWith('.test.ts'),
34
34
  )
35
35
 
36
- const packageJson = JSON.parse(
37
- readFileSync(resolve(ADMIN_ROOT, 'package.json'), 'utf-8'),
38
- ) as { exports: Record<string, string> }
36
+ const packageJson = JSON.parse(readFileSync(resolve(ADMIN_ROOT, 'package.json'), 'utf-8')) as {
37
+ exports: Record<string, string>
38
+ }
39
39
 
40
- const integrationSrc = readFileSync(
41
- resolve(ASTRO_ROOT, 'src/integration.ts'),
42
- 'utf-8',
43
- )
40
+ const integrationSrc = readFileSync(resolve(ASTRO_ROOT, 'src/integration.ts'), 'utf-8')
44
41
 
45
42
  // ---------------------------------------------------------------------------
46
43
  // Helpers
@@ -49,11 +46,7 @@ const integrationSrc = readFileSync(
49
46
  /** Files that are internal — no export or injectRoute needed */
50
47
  function isInternal(filename: string): boolean {
51
48
  const base = filename.replace(/\.ts$/, '')
52
- return (
53
- base.startsWith('_') ||
54
- base.endsWith('-helpers') ||
55
- base.endsWith('-management')
56
- )
49
+ return base.startsWith('_') || base.endsWith('-helpers') || base.endsWith('-management')
57
50
  }
58
51
 
59
52
  /** Export key as used in package.json, e.g. "catalog-list" → "./catalog" special-cased */
@@ -74,7 +67,7 @@ function entrypoint(filename: string): string {
74
67
  // Tests
75
68
  // ---------------------------------------------------------------------------
76
69
 
77
- const publicRoutes = routeFiles.filter(f => !isInternal(f))
70
+ const publicRoutes = routeFiles.filter((f) => !isInternal(f))
78
71
 
79
72
  describe('Route-Registry-Konsistenz', () => {
80
73
  describe('package.json exports', () => {
@@ -14,7 +14,7 @@
14
14
  * surface global nav/footer as editable sections in the admin.
15
15
  */
16
16
 
17
- import { describe, it, expect } from 'vitest'
17
+ import { describe, expect, it } from 'vitest'
18
18
  import { extractLayoutRegions } from '../init-scan-page'
19
19
 
20
20
  // ---------------------------------------------------------------------------
@@ -24,10 +24,11 @@ import { extractLayoutRegions } from '../init-scan-page'
24
24
  // ---------------------------------------------------------------------------
25
25
 
26
26
  function pagePathToSectionKey(pagePath: string): string {
27
- const pageKeyNorm = pagePath
28
- .replace(/^src\/pages\//, '')
29
- .replace(/\/(index)?\.astro$/, '')
30
- .replace(/\.astro$/, '') || 'index'
27
+ const pageKeyNorm =
28
+ pagePath
29
+ .replace(/^src\/pages\//, '')
30
+ .replace(/\/(index)?\.astro$/, '')
31
+ .replace(/\.astro$/, '') || 'index'
31
32
  return '_page_' + pageKeyNorm.replace(/\//g, '_')
32
33
  }
33
34
 
@@ -41,7 +42,9 @@ describe('pagePathToSectionKey — correct key derivation', () => {
41
42
  })
42
43
 
43
44
  it('nested page', () => {
44
- expect(pagePathToSectionKey('src/pages/docs/architecture.astro')).toBe('_page_docs_architecture')
45
+ expect(pagePathToSectionKey('src/pages/docs/architecture.astro')).toBe(
46
+ '_page_docs_architecture',
47
+ )
45
48
  })
46
49
 
47
50
  it('uses underscore not double-dash for nested', () => {
@@ -95,24 +98,24 @@ import { getSection } from 'setzkasten:content'
95
98
 
96
99
  it('should extract header region', () => {
97
100
  const regions = extractLayoutRegions(layoutWithBoth)
98
- expect(regions.some(r => r.name === 'header')).toBe(true)
101
+ expect(regions.some((r) => r.name === 'header')).toBe(true)
99
102
  })
100
103
 
101
104
  it('should extract footer region', () => {
102
105
  const regions = extractLayoutRegions(layoutWithBoth)
103
- expect(regions.some(r => r.name === 'footer')).toBe(true)
106
+ expect(regions.some((r) => r.name === 'footer')).toBe(true)
104
107
  })
105
108
 
106
109
  it('header region HTML should contain the nav content', () => {
107
110
  const regions = extractLayoutRegions(layoutWithBoth)
108
- const header = regions.find(r => r.name === 'header')!
111
+ const header = regions.find((r) => r.name === 'header')!
109
112
  expect(header.html).toContain('<nav>')
110
113
  expect(header.html).toContain('Home')
111
114
  })
112
115
 
113
116
  it('footer region HTML should contain the footer content', () => {
114
117
  const regions = extractLayoutRegions(layoutWithBoth)
115
- const footer = regions.find(r => r.name === 'footer')!
118
+ const footer = regions.find((r) => r.name === 'footer')!
116
119
  expect(footer.html).toContain('Lilapixel')
117
120
  })
118
121