@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.
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 +150 -48
  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-RHJONMLK.js → chunk-CDXCYYQR.js} +222 -5
  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-K22A4ZBS.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 +91 -97
  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 +218 -88
  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 +126 -0
  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 +102 -16
  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 +490 -56
@@ -1,17 +1,24 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
- import { parseSession, guardPageAccess } from '../_auth-guard'
3
1
  import type { AuthSession, SetzKastenConfig } from '@setzkasten-cms/core'
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { guardPageAccess, parseSession } from '../_auth-guard'
4
+ import { makeTestSessionCookie } from './_session-test-helper'
4
5
 
5
6
  // ---------------------------------------------------------------------------
6
7
  // Helpers
7
8
  // ---------------------------------------------------------------------------
8
9
 
9
10
  function adminSession(): AuthSession {
10
- return { user: { id: '1', email: 'admin@example.com', provider: 'github', role: 'admin' }, expiresAt: Date.now() + 86400_000 }
11
+ return {
12
+ user: { id: '1', email: 'admin@example.com', provider: 'github', role: 'admin' },
13
+ expiresAt: Date.now() + 86400_000,
14
+ }
11
15
  }
12
16
 
13
17
  function editorSession(email = 'editor@example.com'): AuthSession {
14
- return { user: { id: '2', email, provider: 'google', role: 'editor' }, expiresAt: Date.now() + 86400_000 }
18
+ return {
19
+ user: { id: '2', email, provider: 'google', role: 'editor' },
20
+ expiresAt: Date.now() + 86400_000,
21
+ }
15
22
  }
16
23
 
17
24
  const baseConfig: SetzKastenConfig = {
@@ -33,9 +40,37 @@ describe('parseSession', () => {
33
40
  expect(parseSession('not-json')).toBeNull()
34
41
  })
35
42
 
36
- it('parses a valid session cookie', () => {
43
+ it('rejects an unsigned plain-JSON cookie (legacy / forged format)', () => {
44
+ // Pre-C1 the cookie was JSON.stringify(session) — anyone could forge
45
+ // any role. parseSession must refuse the old format outright.
37
46
  const session = adminSession()
38
- expect(parseSession(JSON.stringify(session))).toEqual(session)
47
+ expect(parseSession(JSON.stringify(session))).toBeNull()
48
+ })
49
+
50
+ it('accepts a properly signed session cookie', () => {
51
+ const session = adminSession()
52
+ expect(parseSession(makeTestSessionCookie(session))).toEqual(session)
53
+ })
54
+
55
+ it('rejects an expired signed session', () => {
56
+ const expired: AuthSession = {
57
+ user: { id: '1', email: 'admin@example.com', provider: 'github', role: 'admin' },
58
+ expiresAt: Date.now() - 1,
59
+ }
60
+ expect(parseSession(makeTestSessionCookie(expired))).toBeNull()
61
+ })
62
+
63
+ it('rejects a tampered payload (role escalation forgery)', () => {
64
+ const editor: AuthSession = {
65
+ user: { id: '2', email: 'e@example.com', provider: 'google', role: 'editor' },
66
+ expiresAt: Date.now() + 60_000,
67
+ }
68
+ const cookie = makeTestSessionCookie(editor)
69
+ const [, sig] = cookie.split('.')
70
+ const forgedPayload = Buffer.from(
71
+ JSON.stringify({ ...editor, user: { ...editor.user, role: 'admin' } }),
72
+ ).toString('base64url')
73
+ expect(parseSession(`${forgedPayload}.${sig}`)).toBeNull()
39
74
  })
40
75
  })
41
76
 
@@ -103,7 +138,11 @@ describe('guardPageAccess – dynamic editors', () => {
103
138
  kind: 'present',
104
139
  editors: [{ email: 'editor@example.com' }],
105
140
  })
106
- const res = await guardPageAccess(editorSession('editor@example.com'), 'secret-page', baseConfig)
141
+ const res = await guardPageAccess(
142
+ editorSession('editor@example.com'),
143
+ 'secret-page',
144
+ baseConfig,
145
+ )
107
146
  expect(res).toBeNull()
108
147
  })
109
148
 
@@ -10,11 +10,11 @@
10
10
  * 3. buildCatalogAddCommit — builds file paths for a catalog add commit
11
11
  */
12
12
 
13
- import { describe, it, expect } from 'vitest'
13
+ import { describe, expect, it } from 'vitest'
14
14
  import {
15
+ buildCatalogAddCommit,
15
16
  buildCatalogResponse,
16
17
  validateCatalogAddBody,
17
- buildCatalogAddCommit,
18
18
  } from '../catalog-helpers'
19
19
 
20
20
  // ---------------------------------------------------------------------------
@@ -31,7 +31,7 @@ describe('buildCatalogResponse', () => {
31
31
  })
32
32
 
33
33
  it('includes hero, features, cta', () => {
34
- const names = buildCatalogResponse().map(t => t.name)
34
+ const names = buildCatalogResponse().map((t) => t.name)
35
35
  expect(names).toContain('hero')
36
36
  expect(names).toContain('features')
37
37
  expect(names).toContain('cta')
@@ -62,7 +62,9 @@ describe('validateCatalogAddBody', () => {
62
62
  })
63
63
 
64
64
  it('accepts body with sectionKey override', () => {
65
- expect(() => validateCatalogAddBody({ templateName: 'hero', pageKey: 'index', sectionKey: 'hero--top' })).not.toThrow()
65
+ expect(() =>
66
+ validateCatalogAddBody({ templateName: 'hero', pageKey: 'index', sectionKey: 'hero--top' }),
67
+ ).not.toThrow()
66
68
  })
67
69
 
68
70
  it('throws when templateName is missing', () => {
@@ -74,7 +76,9 @@ describe('validateCatalogAddBody', () => {
74
76
  })
75
77
 
76
78
  it('throws when templateName is not in registry', () => {
77
- expect(() => validateCatalogAddBody({ templateName: 'nonexistent-xyz', pageKey: 'index' })).toThrow()
79
+ expect(() =>
80
+ validateCatalogAddBody({ templateName: 'nonexistent-xyz', pageKey: 'index' }),
81
+ ).toThrow()
78
82
  })
79
83
  })
80
84
 
@@ -109,7 +113,11 @@ describe('buildCatalogAddCommit', () => {
109
113
  })
110
114
 
111
115
  it('pageConfigPath matches the pageKey', () => {
112
- const result = buildCatalogAddCommit({ ...opts, pageKey: 'about', pageConfigPath: 'content/pages/_about.json' })
116
+ const result = buildCatalogAddCommit({
117
+ ...opts,
118
+ pageKey: 'about',
119
+ pageConfigPath: 'content/pages/_about.json',
120
+ })
113
121
  expect(result.pageConfigPath).toBe('content/pages/_about.json')
114
122
  })
115
123
  })
@@ -3,8 +3,8 @@
3
3
  * @vitest-environment node
4
4
  */
5
5
 
6
- import { describe, it, expect } from 'vitest'
7
- import { withTrailers, SETZKASTEN_CO_AUTHOR } from '../_commit-trailers'
6
+ import { describe, expect, it } from 'vitest'
7
+ import { SETZKASTEN_CO_AUTHOR, withTrailers } from '../_commit-trailers'
8
8
 
9
9
  describe('SETZKASTEN_CO_AUTHOR', () => {
10
10
  it('is a valid Co-authored-by trailer', () => {
@@ -28,20 +28,20 @@ describe('withTrailers', () => {
28
28
 
29
29
  it('does not add editor trailer when editorEmail is not provided', () => {
30
30
  const result = withTrailers('chore: something')
31
- const lines = result.split('\n').filter(l => l.startsWith('Co-authored-by:'))
31
+ const lines = result.split('\n').filter((l) => l.startsWith('Co-authored-by:'))
32
32
  expect(lines).toHaveLength(1)
33
33
  expect(lines[0]).toBe(SETZKASTEN_CO_AUTHOR)
34
34
  })
35
35
 
36
36
  it('does not add editor trailer when editorEmail is null', () => {
37
37
  const result = withTrailers('chore: something', null)
38
- const lines = result.split('\n').filter(l => l.startsWith('Co-authored-by:'))
38
+ const lines = result.split('\n').filter((l) => l.startsWith('Co-authored-by:'))
39
39
  expect(lines).toHaveLength(1)
40
40
  })
41
41
 
42
42
  it('adds editor Co-authored-by when editorEmail is provided', () => {
43
43
  const result = withTrailers('content: update', 'jane.doe@example.com')
44
- const lines = result.split('\n').filter(l => l.startsWith('Co-authored-by:'))
44
+ const lines = result.split('\n').filter((l) => l.startsWith('Co-authored-by:'))
45
45
  expect(lines).toHaveLength(2)
46
46
  expect(lines[1]).toContain('jane.doe@example.com')
47
47
  })
@@ -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
  })