@setzkasten-cms/astro-admin 1.4.0 → 1.4.1

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 (153) hide show
  1. package/dist/api-routes/_auth-guard.d.ts +47 -0
  2. package/dist/api-routes/_auth-guard.js +18 -0
  3. package/dist/api-routes/_commit-trailers.d.ts +8 -0
  4. package/dist/api-routes/_commit-trailers.js +8 -0
  5. package/dist/api-routes/_feature-gate.d.ts +23 -0
  6. package/dist/api-routes/_feature-gate.js +7 -0
  7. package/dist/api-routes/_github-cache.d.ts +4 -0
  8. package/dist/api-routes/_github-cache.js +8 -0
  9. package/dist/api-routes/_github-token.d.ts +27 -0
  10. package/dist/api-routes/_github-token.js +8 -0
  11. package/dist/api-routes/_license-tier.d.ts +22 -0
  12. package/dist/api-routes/_license-tier.js +6 -0
  13. package/dist/api-routes/_pages-meta-store.d.ts +32 -0
  14. package/dist/api-routes/_pages-meta-store.js +9 -0
  15. package/dist/api-routes/_role-resolver.d.ts +15 -0
  16. package/dist/api-routes/_role-resolver.js +13 -0
  17. package/dist/api-routes/_session-cookie.d.ts +18 -0
  18. package/dist/api-routes/_session-cookie.js +6 -0
  19. package/dist/api-routes/_storage-config.d.ts +60 -0
  20. package/dist/api-routes/_storage-config.js +10 -0
  21. package/dist/api-routes/_vercel-origin.d.ts +16 -0
  22. package/dist/api-routes/_vercel-origin.js +6 -0
  23. package/dist/api-routes/_webhook-dispatcher.d.ts +13 -0
  24. package/dist/api-routes/_webhook-dispatcher.js +97 -0
  25. package/dist/api-routes/_webhook-signing.d.ts +11 -0
  26. package/dist/api-routes/_webhook-signing.js +6 -0
  27. package/dist/api-routes/_webhook-status-store.d.ts +19 -0
  28. package/dist/api-routes/_webhook-status-store.js +10 -0
  29. package/dist/api-routes/_website-resolver.d.ts +49 -0
  30. package/dist/api-routes/_website-resolver.js +14 -0
  31. package/dist/api-routes/_websites-store.d.ts +30 -0
  32. package/dist/api-routes/_websites-store.js +11 -0
  33. package/dist/api-routes/asset-proxy.d.ts +12 -0
  34. package/dist/api-routes/asset-proxy.js +67 -0
  35. package/dist/api-routes/auth-callback.d.ts +9 -0
  36. package/dist/api-routes/auth-callback.js +68 -0
  37. package/dist/api-routes/auth-login.d.ts +11 -0
  38. package/dist/api-routes/auth-login.js +27 -0
  39. package/dist/api-routes/auth-logout.d.ts +10 -0
  40. package/dist/api-routes/auth-logout.js +13 -0
  41. package/dist/api-routes/auth-session.d.ts +9 -0
  42. package/dist/api-routes/auth-session.js +31 -0
  43. package/dist/api-routes/auth-setzkasten-login.d.ts +18 -0
  44. package/dist/api-routes/auth-setzkasten-login.js +74 -0
  45. package/dist/api-routes/catalog-add.d.ts +14 -0
  46. package/dist/api-routes/catalog-add.js +153 -0
  47. package/dist/api-routes/catalog-export.d.ts +13 -0
  48. package/dist/api-routes/catalog-export.js +71 -0
  49. package/dist/api-routes/catalog-helpers.d.ts +41 -0
  50. package/dist/api-routes/catalog-helpers.js +11 -0
  51. package/dist/api-routes/catalog-list.d.ts +11 -0
  52. package/dist/api-routes/catalog-list.js +12 -0
  53. package/dist/api-routes/config.d.ts +12 -0
  54. package/dist/api-routes/config.js +43 -0
  55. package/dist/api-routes/deploy-hook.d.ts +14 -0
  56. package/dist/api-routes/deploy-hook.js +52 -0
  57. package/dist/api-routes/editors.d.ts +29 -0
  58. package/dist/api-routes/editors.js +18 -0
  59. package/dist/api-routes/github-proxy.d.ts +12 -0
  60. package/dist/api-routes/github-proxy.js +82 -0
  61. package/dist/api-routes/global-config.d.ts +20 -0
  62. package/dist/api-routes/global-config.js +19 -0
  63. package/dist/api-routes/history-rollback.d.ts +22 -0
  64. package/dist/api-routes/history-rollback.js +111 -0
  65. package/dist/api-routes/history-version.d.ts +11 -0
  66. package/dist/api-routes/history-version.js +57 -0
  67. package/dist/api-routes/history.d.ts +13 -0
  68. package/dist/api-routes/history.js +85 -0
  69. package/dist/api-routes/icons-local.d.ts +28 -0
  70. package/dist/api-routes/icons-local.js +115 -0
  71. package/dist/api-routes/init-add-section.d.ts +23 -0
  72. package/dist/api-routes/init-add-section.js +396 -0
  73. package/dist/api-routes/init-apply.d.ts +11 -0
  74. package/dist/api-routes/init-apply.js +266 -0
  75. package/dist/api-routes/init-migrate.d.ts +16 -0
  76. package/dist/api-routes/init-migrate.js +205 -0
  77. package/dist/api-routes/init-scan-page.d.ts +39 -0
  78. package/dist/api-routes/init-scan-page.js +260 -0
  79. package/dist/api-routes/init-scan.d.ts +11 -0
  80. package/dist/api-routes/init-scan.js +128 -0
  81. package/dist/api-routes/migrate-to-multi.d.ts +26 -0
  82. package/dist/api-routes/migrate-to-multi.js +188 -0
  83. package/dist/api-routes/pages.d.ts +39 -0
  84. package/dist/api-routes/pages.js +88 -0
  85. package/dist/api-routes/section-add.d.ts +18 -0
  86. package/dist/api-routes/section-add.js +173 -0
  87. package/dist/api-routes/section-commit-pending.d.ts +18 -0
  88. package/dist/api-routes/section-commit-pending.js +207 -0
  89. package/dist/api-routes/section-delete.d.ts +15 -0
  90. package/dist/api-routes/section-delete.js +149 -0
  91. package/dist/api-routes/section-duplicate.d.ts +15 -0
  92. package/dist/api-routes/section-duplicate.js +143 -0
  93. package/dist/api-routes/section-management.d.ts +41 -0
  94. package/dist/api-routes/section-management.js +14 -0
  95. package/dist/api-routes/section-prepare-copy.d.ts +25 -0
  96. package/dist/api-routes/section-prepare-copy.js +69 -0
  97. package/dist/api-routes/section-prepare.d.ts +18 -0
  98. package/dist/api-routes/section-prepare.js +104 -0
  99. package/dist/api-routes/setup-github-app-bounce.d.ts +13 -0
  100. package/dist/api-routes/setup-github-app-bounce.js +45 -0
  101. package/dist/api-routes/setup-github-app-branches.d.ts +14 -0
  102. package/dist/api-routes/setup-github-app-branches.js +58 -0
  103. package/dist/api-routes/setup-github-app-callback.d.ts +15 -0
  104. package/dist/api-routes/setup-github-app-callback.js +45 -0
  105. package/dist/api-routes/setup-github-app-installed.d.ts +15 -0
  106. package/dist/api-routes/setup-github-app-installed.js +33 -0
  107. package/dist/api-routes/setup-github-app-repos.d.ts +17 -0
  108. package/dist/api-routes/setup-github-app-repos.js +41 -0
  109. package/dist/api-routes/setup-github-app.d.ts +15 -0
  110. package/dist/api-routes/setup-github-app.js +41 -0
  111. package/dist/api-routes/updater-check.d.ts +10 -0
  112. package/dist/api-routes/updater-check.js +37 -0
  113. package/dist/api-routes/updater-register.d.ts +14 -0
  114. package/dist/api-routes/updater-register.js +71 -0
  115. package/dist/api-routes/updater-transfer.d.ts +11 -0
  116. package/dist/api-routes/updater-transfer.js +37 -0
  117. package/dist/api-routes/updater-unbind.d.ts +17 -0
  118. package/dist/api-routes/updater-unbind.js +35 -0
  119. package/dist/api-routes/webhooks-status.d.ts +12 -0
  120. package/dist/api-routes/webhooks-status.js +22 -0
  121. package/dist/api-routes/webhooks-test.d.ts +13 -0
  122. package/dist/api-routes/webhooks-test.js +124 -0
  123. package/dist/api-routes/webhooks.d.ts +6 -0
  124. package/dist/api-routes/webhooks.js +148 -0
  125. package/dist/api-routes/websites-add.d.ts +15 -0
  126. package/dist/api-routes/websites-add.js +92 -0
  127. package/dist/api-routes/websites-list.d.ts +12 -0
  128. package/dist/api-routes/websites-list.js +35 -0
  129. package/dist/api-routes/websites-remove.d.ts +15 -0
  130. package/dist/api-routes/websites-remove.js +69 -0
  131. package/dist/chunk-35S35OIV.js +80 -0
  132. package/dist/chunk-45ARVNT3.js +25 -0
  133. package/dist/chunk-5PIMDP4N.js +25 -0
  134. package/dist/chunk-5ZFTG4BW.js +10 -0
  135. package/dist/chunk-6UIKVKED.js +51 -0
  136. package/dist/chunk-737TIZRU.js +9 -0
  137. package/dist/chunk-AM4DZXXM.js +120 -0
  138. package/dist/chunk-FXNOTESI.js +87 -0
  139. package/dist/chunk-GHNK2GFE.js +48 -0
  140. package/dist/chunk-GRG3LNKH.js +37 -0
  141. package/dist/chunk-INIWFKQ3.js +236 -0
  142. package/dist/chunk-JHY6XTLL.js +24 -0
  143. package/dist/chunk-K22A4ZBS.js +1574 -0
  144. package/dist/chunk-KH22FJO5.js +17 -0
  145. package/dist/chunk-NKDATSPA.js +43 -0
  146. package/dist/chunk-RHJONMLK.js +1267 -0
  147. package/dist/chunk-TJNJKPUL.js +11 -0
  148. package/dist/chunk-V6IMPVF3.js +120 -0
  149. package/dist/chunk-W3QHY5GW.js +19 -0
  150. package/dist/chunk-ZQDGGWJP.js +43 -0
  151. package/package.json +249 -53
  152. package/src/api-routes/__tests__/route-registry.test.ts +7 -1
  153. package/tsconfig.json +0 -9
@@ -0,0 +1,15 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * POST /api/setzkasten/sections/duplicate
5
+ *
6
+ * Duplicates a section within a page:
7
+ * - Copies the content JSON to a new key (_sections/{newKey}.json)
8
+ * - Inserts new entry after the original in the page config
9
+ * - New key is auto-generated: hero → hero--copy → hero--copy2 …
10
+ *
11
+ * Body: { pageKey, sectionKey, owner?, repo?, branch?, contentPath? }
12
+ */
13
+ declare const POST: APIRoute;
14
+
15
+ export { POST };
@@ -0,0 +1,143 @@
1
+ import {
2
+ duplicateInPageConfig,
3
+ generateDuplicateKey
4
+ } from "../chunk-GHNK2GFE.js";
5
+ import {
6
+ guardPageAccess,
7
+ parseSession
8
+ } from "../chunk-INIWFKQ3.js";
9
+ import {
10
+ resolveStorageConfigForRequest
11
+ } from "../chunk-6UIKVKED.js";
12
+ import "../chunk-5PIMDP4N.js";
13
+ import "../chunk-45ARVNT3.js";
14
+ import {
15
+ resolveGitHubTokenForRequest
16
+ } from "../chunk-NKDATSPA.js";
17
+ import "../chunk-TJNJKPUL.js";
18
+ import {
19
+ withTrailers
20
+ } from "../chunk-KH22FJO5.js";
21
+
22
+ // src/api-routes/section-duplicate.ts
23
+ var POST = async ({ request, cookies }) => {
24
+ const session = cookies.get("setzkasten_session")?.value;
25
+ if (!session) return new Response("Unauthorized", { status: 401 });
26
+ const tokenResult = await resolveGitHubTokenForRequest(request);
27
+ if (!tokenResult.ok) {
28
+ return new Response(tokenResult.error.message, { status: 500 });
29
+ }
30
+ const githubToken = tokenResult.value;
31
+ try {
32
+ const body = await request.json();
33
+ const storage = await resolveStorageConfigForRequest(request, body);
34
+ if (!storage) return Response.json({ error: "Could not resolve owner/repo" }, { status: 400 });
35
+ const { owner, repo, branch, projectPrefix } = storage;
36
+ const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
37
+ const fullConfig = globalThis.__SETZKASTEN_FULL_CONFIG__;
38
+ const contentPath = body.contentPath || serverConfig?.storage?.contentPath || "content";
39
+ const { pageKey, sectionKey } = body;
40
+ if (!pageKey || !sectionKey) {
41
+ return Response.json({ error: "pageKey and sectionKey are required" }, { status: 400 });
42
+ }
43
+ const denied = await guardPageAccess(parseSession(cookies.get("setzkasten_session")?.value), pageKey, fullConfig, request);
44
+ if (denied) return denied;
45
+ const headers = {
46
+ Authorization: `Bearer ${githubToken}`,
47
+ Accept: "application/vnd.github+json",
48
+ "X-GitHub-Api-Version": "2022-11-28",
49
+ "Content-Type": "application/json"
50
+ };
51
+ const configKey = "_" + pageKey.replace(/\//g, "_");
52
+ const pageConfigPath = `${contentPath}/pages/${configKey}.json`;
53
+ const pageConfigRaw = await fetchFileContent(owner, repo, branch, pageConfigPath, githubToken);
54
+ if (!pageConfigRaw) return Response.json({ error: "Page config not found" }, { status: 404 });
55
+ const pageConfig = JSON.parse(pageConfigRaw);
56
+ const existingKeys = (pageConfig.sections ?? []).map((s) => s.key);
57
+ const newKey = generateDuplicateKey(existingKeys, sectionKey);
58
+ const originalJsonPath = `${contentPath}/_sections/${sectionKey}.json`;
59
+ const originalContent = await fetchFileContent(owner, repo, branch, originalJsonPath, githubToken);
60
+ const updatedConfig = duplicateInPageConfig(pageConfig, sectionKey, newKey);
61
+ const filesToCommit = [
62
+ { path: pageConfigPath, content: JSON.stringify(updatedConfig, null, 2) }
63
+ ];
64
+ if (originalContent) {
65
+ const copyPath = `${contentPath}/_sections/${newKey}.json`;
66
+ filesToCommit.push({ path: copyPath, content: originalContent });
67
+ }
68
+ const commitResult = await batchCommit(
69
+ owner,
70
+ repo,
71
+ branch,
72
+ filesToCommit,
73
+ withTrailers(
74
+ `content: duplicate ${sectionKey} \u2192 ${newKey} on ${pageKey}`,
75
+ parseSession(cookies.get("setzkasten_session")?.value)?.user?.email
76
+ ),
77
+ headers
78
+ );
79
+ if (!commitResult.ok) return Response.json({ error: commitResult.error }, { status: 500 });
80
+ const { recordPageEdit } = await import("./_pages-meta-store.js");
81
+ await recordPageEdit(
82
+ { owner, repo, branch, contentPath, token: tokenResult.value },
83
+ pageKey
84
+ ).catch(() => {
85
+ });
86
+ return Response.json({ success: true, newKey, commitSha: commitResult.sha });
87
+ } catch (error) {
88
+ console.error("[setzkasten] section-duplicate error:", error);
89
+ return Response.json(
90
+ { error: error instanceof Error ? error.message : "Duplicate failed" },
91
+ { status: 500 }
92
+ );
93
+ }
94
+ };
95
+ async function fetchFileContent(owner, repo, branch, path, token) {
96
+ try {
97
+ const res = await fetch(
98
+ `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
99
+ { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
100
+ );
101
+ if (!res.ok) return null;
102
+ const data = await res.json();
103
+ return data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ async function batchCommit(owner, repo, branch, files, message, headers) {
109
+ try {
110
+ const refRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, { headers });
111
+ if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` };
112
+ const { object: { sha: headSha } } = await refRes.json();
113
+ const commitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`, { headers });
114
+ if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` };
115
+ const { tree: { sha: baseSha } } = await commitRes.json();
116
+ const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
117
+ method: "POST",
118
+ headers,
119
+ body: JSON.stringify({ base_tree: baseSha, tree: files.map((f) => ({ path: f.path, mode: "100644", type: "blob", content: f.content })) })
120
+ });
121
+ if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` };
122
+ const { sha: treeSha } = await treeRes.json();
123
+ const newCommitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits`, {
124
+ method: "POST",
125
+ headers,
126
+ body: JSON.stringify({ tree: treeSha, parents: [headSha], message })
127
+ });
128
+ if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` };
129
+ const { sha: newSha } = await newCommitRes.json();
130
+ const updateRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
131
+ method: "PATCH",
132
+ headers,
133
+ body: JSON.stringify({ sha: newSha })
134
+ });
135
+ if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` };
136
+ return { ok: true, sha: newSha };
137
+ } catch (error) {
138
+ return { ok: false, error: error instanceof Error ? error.message : "Commit failed" };
139
+ }
140
+ }
141
+ export {
142
+ POST
143
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pure helper functions for section delete and duplicate operations.
3
+ * No GitHub API — callers handle storage.
4
+ */
5
+ interface SectionEntry {
6
+ key: string;
7
+ type?: string;
8
+ enabled?: boolean;
9
+ order?: number;
10
+ [key: string]: unknown;
11
+ }
12
+ interface PageConfig {
13
+ sections: SectionEntry[];
14
+ [key: string]: unknown;
15
+ }
16
+ /**
17
+ * Removes a section from the page config and re-numbers order.
18
+ */
19
+ declare function removeFromPageConfig(config: PageConfig, sectionKey: string): PageConfig;
20
+ /**
21
+ * Generates a unique duplicate key for a section.
22
+ * 'hero' → 'hero--copy', then 'hero--copy2', 'hero--copy3', ...
23
+ */
24
+ declare function generateDuplicateKey(existingKeys: string[], originalKey: string): string;
25
+ /**
26
+ * Generates a unique key for a new section of a given type.
27
+ * 'hero' → 'hero' (if free), then 'hero--2', 'hero--3', ...
28
+ */
29
+ declare function generateAddKey(existingKeys: string[], type: string): string;
30
+ /**
31
+ * Appends a new section entry at the end of the page config.
32
+ * Sets `type` only when key differs from type (multi-instance case).
33
+ */
34
+ declare function addToPageConfig(config: PageConfig, key: string, type: string): PageConfig;
35
+ /**
36
+ * Inserts a duplicate entry immediately after the original in the page config.
37
+ * The copy is always enabled. Order is re-numbered.
38
+ */
39
+ declare function duplicateInPageConfig(config: PageConfig, originalKey: string, newKey: string): PageConfig;
40
+
41
+ export { addToPageConfig, duplicateInPageConfig, generateAddKey, generateDuplicateKey, removeFromPageConfig };
@@ -0,0 +1,14 @@
1
+ import {
2
+ addToPageConfig,
3
+ duplicateInPageConfig,
4
+ generateAddKey,
5
+ generateDuplicateKey,
6
+ removeFromPageConfig
7
+ } from "../chunk-GHNK2GFE.js";
8
+ export {
9
+ addToPageConfig,
10
+ duplicateInPageConfig,
11
+ generateAddKey,
12
+ generateDuplicateKey,
13
+ removeFromPageConfig
14
+ };
@@ -0,0 +1,25 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * POST /api/setzkasten/sections/prepare-copy
5
+ *
6
+ * Prepares a deferred duplicate of an existing section:
7
+ * - Reads the original section content from GitHub
8
+ * - Generates a unique copy key (hero → hero--copy → hero--copy2 …)
9
+ * - Returns { key, type, content, updatedPageConfig } WITHOUT committing
10
+ *
11
+ * The client uses this to update local state + preview draft immediately.
12
+ * Only committed to GitHub when the user presses "Live setzen".
13
+ *
14
+ * Note: this route intentionally does NOT call recordPageEdit. The
15
+ * page-recency spec lists it as a "mutating route", but in practice it
16
+ * only reads and returns — the real GitHub commit happens later in
17
+ * commit-pending, which records the edit. Bumping the timestamp here
18
+ * would mark a page as recently-modified even when the user opens the
19
+ * duplicate dialog and then cancels without committing.
20
+ *
21
+ * Body: { pageKey, sectionKey, owner?, repo?, branch?, contentPath? }
22
+ */
23
+ declare const POST: APIRoute;
24
+
25
+ export { POST };
@@ -0,0 +1,69 @@
1
+ import {
2
+ duplicateInPageConfig,
3
+ generateDuplicateKey
4
+ } from "../chunk-GHNK2GFE.js";
5
+ import {
6
+ resolveStorageConfigForRequest
7
+ } from "../chunk-6UIKVKED.js";
8
+ import {
9
+ resolveGitHubTokenForRequest
10
+ } from "../chunk-NKDATSPA.js";
11
+
12
+ // src/api-routes/section-prepare-copy.ts
13
+ var POST = async ({ request, cookies }) => {
14
+ const session = cookies.get("setzkasten_session")?.value;
15
+ if (!session) return new Response("Unauthorized", { status: 401 });
16
+ const tokenResult = await resolveGitHubTokenForRequest(request);
17
+ if (!tokenResult.ok) {
18
+ return new Response(tokenResult.error.message, { status: 500 });
19
+ }
20
+ const githubToken = tokenResult.value;
21
+ try {
22
+ const body = await request.json();
23
+ const storage = await resolveStorageConfigForRequest(request, body);
24
+ if (!storage) return Response.json({ error: "Could not resolve owner/repo" }, { status: 400 });
25
+ const { owner, repo, branch } = storage;
26
+ const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
27
+ const contentPath = body.contentPath || serverConfig?.storage?.contentPath || "content";
28
+ const { pageKey, sectionKey } = body;
29
+ if (!pageKey || !sectionKey) {
30
+ return Response.json({ error: "pageKey and sectionKey are required" }, { status: 400 });
31
+ }
32
+ const configKey = "_" + pageKey.replace(/\//g, "_");
33
+ const pageConfigPath = `${contentPath}/pages/${configKey}.json`;
34
+ const pageConfigRaw = await fetchFileContent(owner, repo, branch, pageConfigPath, githubToken);
35
+ if (!pageConfigRaw) return Response.json({ error: "Page config not found" }, { status: 404 });
36
+ const pageConfig = JSON.parse(pageConfigRaw);
37
+ const existingKeys = (pageConfig.sections ?? []).map((s) => s.key);
38
+ const newKey = generateDuplicateKey(existingKeys, sectionKey);
39
+ const originalJsonPath = `${contentPath}/_sections/${sectionKey}.json`;
40
+ const originalRaw = await fetchFileContent(owner, repo, branch, originalJsonPath, githubToken);
41
+ const content = originalRaw ? JSON.parse(originalRaw) : {};
42
+ const originalEntry = (pageConfig.sections ?? []).find((s) => s.key === sectionKey);
43
+ const type = originalEntry?.type ?? sectionKey;
44
+ const updatedPageConfig = duplicateInPageConfig(pageConfig, sectionKey, newKey);
45
+ return Response.json({ key: newKey, type, content, updatedPageConfig });
46
+ } catch (error) {
47
+ console.error("[setzkasten] section-prepare-copy error:", error);
48
+ return Response.json(
49
+ { error: error instanceof Error ? error.message : "Prepare copy failed" },
50
+ { status: 500 }
51
+ );
52
+ }
53
+ };
54
+ async function fetchFileContent(owner, repo, branch, path, token) {
55
+ try {
56
+ const res = await fetch(
57
+ `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
58
+ { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
59
+ );
60
+ if (!res.ok) return null;
61
+ const data = await res.json();
62
+ return data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ export {
68
+ POST
69
+ };
@@ -0,0 +1,18 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * POST /api/setzkasten/sections/prepare
5
+ *
6
+ * Prepares a new section for optimistic/deferred addition:
7
+ * - Generates a unique key for the given sectionType
8
+ * - Builds default content from schema (NO GitHub commit)
9
+ * - Returns { key, type, defaultContent, updatedPageConfig }
10
+ *
11
+ * The client uses this to update local state + preview draft immediately,
12
+ * and only commits to GitHub when the user explicitly saves ("Seite speichern").
13
+ *
14
+ * Body: { pageKey, sectionType, owner?, repo?, branch?, contentPath? }
15
+ */
16
+ declare const POST: APIRoute;
17
+
18
+ export { POST };
@@ -0,0 +1,104 @@
1
+ import {
2
+ generateAddKey
3
+ } from "../chunk-GHNK2GFE.js";
4
+ import {
5
+ guardPageAccess,
6
+ parseSession
7
+ } from "../chunk-INIWFKQ3.js";
8
+ import {
9
+ resolveStorageConfigForRequest
10
+ } from "../chunk-6UIKVKED.js";
11
+ import "../chunk-5PIMDP4N.js";
12
+ import "../chunk-45ARVNT3.js";
13
+ import {
14
+ resolveGitHubTokenForRequest
15
+ } from "../chunk-NKDATSPA.js";
16
+ import "../chunk-TJNJKPUL.js";
17
+ import "../chunk-KH22FJO5.js";
18
+
19
+ // src/api-routes/section-prepare.ts
20
+ var POST = async ({ request, cookies }) => {
21
+ const session = cookies.get("setzkasten_session")?.value;
22
+ if (!session) return new Response("Unauthorized", { status: 401 });
23
+ const tokenResult = await resolveGitHubTokenForRequest(request);
24
+ if (!tokenResult.ok) {
25
+ return new Response(tokenResult.error.message, { status: 500 });
26
+ }
27
+ const githubToken = tokenResult.value;
28
+ try {
29
+ const body = await request.json();
30
+ const storage = await resolveStorageConfigForRequest(request, body);
31
+ if (!storage) return Response.json({ error: "Could not resolve owner/repo" }, { status: 400 });
32
+ const { owner, repo, branch } = storage;
33
+ const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
34
+ const fullConfig = globalThis.__SETZKASTEN_FULL_CONFIG__;
35
+ const contentPath = body.contentPath || serverConfig?.storage?.contentPath || "content";
36
+ const { pageKey, sectionType } = body;
37
+ if (!pageKey || !sectionType) {
38
+ return Response.json({ error: "pageKey and sectionType are required" }, { status: 400 });
39
+ }
40
+ const denied = await guardPageAccess(parseSession(cookies.get("setzkasten_session")?.value), pageKey, fullConfig, request);
41
+ if (denied) return denied;
42
+ const configKey = "_" + pageKey.replace(/\//g, "_");
43
+ const pageConfigPath = `${contentPath}/pages/${configKey}.json`;
44
+ const pageConfigRaw = await fetchFileContent(owner, repo, branch, pageConfigPath, githubToken);
45
+ if (!pageConfigRaw) return Response.json({ error: "Page config not found" }, { status: 404 });
46
+ const pageConfig = JSON.parse(pageConfigRaw);
47
+ const existingKeys = (pageConfig.sections ?? []).map((s) => s.key);
48
+ const newKey = generateAddKey(existingKeys, sectionType);
49
+ const sectionDef = findSectionDef(fullConfig, sectionType);
50
+ const defaultContent = sectionDef ? buildDefaultContent(sectionDef.fields ?? {}) : {};
51
+ const newEntry = {
52
+ key: newKey,
53
+ enabled: true,
54
+ order: pageConfig.sections.length,
55
+ ...newKey !== sectionType ? { type: sectionType } : {}
56
+ };
57
+ const updatedPageConfig = {
58
+ ...pageConfig,
59
+ sections: [...pageConfig.sections ?? [], newEntry]
60
+ };
61
+ return Response.json({
62
+ key: newKey,
63
+ type: sectionType,
64
+ defaultContent,
65
+ updatedPageConfig
66
+ });
67
+ } catch (error) {
68
+ console.error("[setzkasten] section-prepare error:", error);
69
+ return Response.json(
70
+ { error: error instanceof Error ? error.message : "Prepare failed" },
71
+ { status: 500 }
72
+ );
73
+ }
74
+ };
75
+ function findSectionDef(fullConfig, sectionType) {
76
+ if (!fullConfig?.products) return null;
77
+ for (const product of Object.values(fullConfig.products)) {
78
+ if (product?.sections?.[sectionType]) return product.sections[sectionType];
79
+ }
80
+ return null;
81
+ }
82
+ function buildDefaultContent(fields) {
83
+ const result = {};
84
+ for (const [key, field] of Object.entries(fields)) {
85
+ if (field.defaultValue !== void 0) result[key] = field.defaultValue;
86
+ }
87
+ return result;
88
+ }
89
+ async function fetchFileContent(owner, repo, branch, path, token) {
90
+ try {
91
+ const res = await fetch(
92
+ `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
93
+ { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
94
+ );
95
+ if (!res.ok) return null;
96
+ const data = await res.json();
97
+ return data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ export {
103
+ POST
104
+ };
@@ -0,0 +1,13 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * Server-side manifest bounce for the GitHub App Manifest flow.
5
+ *
6
+ * GET /api/setzkasten/setup/github-app/bounce?name=my-app
7
+ *
8
+ * Generates the GitHub App manifest JSON using the server-known origin,
9
+ * then returns a minimal HTML page that auto-submits a form to GitHub.
10
+ */
11
+ declare const GET: APIRoute;
12
+
13
+ export { GET };
@@ -0,0 +1,45 @@
1
+ import {
2
+ getPublicOrigin
3
+ } from "../chunk-5ZFTG4BW.js";
4
+
5
+ // src/api-routes/setup-github-app-bounce.ts
6
+ var GET = async ({ url, request }) => {
7
+ const name = url.searchParams.get("name")?.trim() || "Setzkasten CMS";
8
+ const origin = getPublicOrigin(request);
9
+ const manifest = JSON.stringify({
10
+ name,
11
+ url: origin,
12
+ redirect_url: `${origin}/api/setzkasten/setup/github-app/callback`,
13
+ setup_url: `${origin}/api/setzkasten/setup/github-app/installed`,
14
+ setup_on_update: false,
15
+ public: false,
16
+ default_permissions: { contents: "write" }
17
+ });
18
+ const safeManifest = manifest.replace(/&/g, "&").replace(/"/g, """);
19
+ const html = `<!DOCTYPE html>
20
+ <html lang="de">
21
+ <head>
22
+ <meta charset="UTF-8">
23
+ <title>Weiterleitung zu GitHub\u2026</title>
24
+ <style>
25
+ body { font-family: sans-serif; display: flex; align-items: center;
26
+ justify-content: center; height: 100vh; margin: 0;
27
+ background: #0d1117; color: #e6edf3; }
28
+ p { opacity: .6; }
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <p>Weiterleitung zu GitHub\u2026</p>
33
+ <form id="f" method="POST" action="https://github.com/settings/apps/new">
34
+ <input type="hidden" name="manifest" value="${safeManifest}">
35
+ </form>
36
+ <script>document.getElementById('f').submit()</script>
37
+ </body>
38
+ </html>`;
39
+ return new Response(html, {
40
+ headers: { "Content-Type": "text/html; charset=utf-8" }
41
+ });
42
+ };
43
+ export {
44
+ GET
45
+ };
@@ -0,0 +1,14 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * GET /api/setzkasten/setup/github-app/branches?installation=<id>&repo=<owner/repo>
5
+ *
6
+ * Returns the list of branches for one repo, fetched via the installation
7
+ * token of the given installation. Used by the WebsitesView form so the
8
+ * Branch field becomes a dropdown after the user picks a repo.
9
+ *
10
+ * Admin-only — same reasoning as /setup/github-app/repos.
11
+ */
12
+ declare const GET: APIRoute;
13
+
14
+ export { GET };
@@ -0,0 +1,58 @@
1
+ import {
2
+ requireAdmin
3
+ } from "../chunk-INIWFKQ3.js";
4
+ import "../chunk-6UIKVKED.js";
5
+ import "../chunk-5PIMDP4N.js";
6
+ import "../chunk-45ARVNT3.js";
7
+ import "../chunk-NKDATSPA.js";
8
+ import "../chunk-TJNJKPUL.js";
9
+ import "../chunk-KH22FJO5.js";
10
+
11
+ // src/api-routes/setup-github-app-branches.ts
12
+ import { listRepoBranches } from "@setzkasten-cms/github-adapter";
13
+ var GET = async ({ cookies, url }) => {
14
+ const denied = requireAdmin(cookies.get("setzkasten_session")?.value);
15
+ if (denied) return denied;
16
+ const appId = process.env.GITHUB_APP_ID;
17
+ const privateKey = process.env.GITHUB_APP_PRIVATE_KEY;
18
+ if (!appId || !privateKey) {
19
+ return new Response(
20
+ JSON.stringify({
21
+ error: "GitHub App not configured. Set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY."
22
+ }),
23
+ { status: 400, headers: { "Content-Type": "application/json" } }
24
+ );
25
+ }
26
+ const installationId = url.searchParams.get("installation");
27
+ const repoFull = url.searchParams.get("repo");
28
+ if (!installationId || !repoFull) {
29
+ return new Response(
30
+ JSON.stringify({ error: "Both ?installation and ?repo (owner/name) are required." }),
31
+ { status: 400, headers: { "Content-Type": "application/json" } }
32
+ );
33
+ }
34
+ const slash = repoFull.indexOf("/");
35
+ if (slash <= 0 || slash === repoFull.length - 1) {
36
+ return new Response(
37
+ JSON.stringify({ error: '?repo must be in "owner/name" format.' }),
38
+ { status: 400, headers: { "Content-Type": "application/json" } }
39
+ );
40
+ }
41
+ const owner = repoFull.slice(0, slash);
42
+ const repo = repoFull.slice(slash + 1);
43
+ const result = await listRepoBranches({ appId, privateKey }, installationId, owner, repo);
44
+ if (!result.ok) {
45
+ const status = result.error.type === "auth" ? 401 : result.error.type === "not-found" ? 404 : 502;
46
+ return new Response(JSON.stringify({ error: result.error.message }), {
47
+ status,
48
+ headers: { "Content-Type": "application/json" }
49
+ });
50
+ }
51
+ return new Response(JSON.stringify({ branches: result.value }), {
52
+ status: 200,
53
+ headers: { "Content-Type": "application/json" }
54
+ });
55
+ };
56
+ export {
57
+ GET
58
+ };
@@ -0,0 +1,15 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * Receives the code from GitHub after a GitHub App Manifest creation.
5
+ * Exchanges it for the full app credentials (App ID, private key, slug).
6
+ *
7
+ * GET /api/setzkasten/setup/github-app/callback?code=xxx
8
+ *
9
+ * Note: uses mutable new Response() instead of Response.redirect() —
10
+ * Response.redirect() is immutable and prevents Astro from appending
11
+ * the Set-Cookie header (TypeError: immutable).
12
+ */
13
+ declare const GET: APIRoute;
14
+
15
+ export { GET };
@@ -0,0 +1,45 @@
1
+ import {
2
+ getPublicOrigin
3
+ } from "../chunk-5ZFTG4BW.js";
4
+
5
+ // src/api-routes/setup-github-app-callback.ts
6
+ var COOKIE_NAME = "sk_app_setup";
7
+ var COOKIE_MAX_AGE = 600;
8
+ var GET = async ({ url, request, cookies }) => {
9
+ const config = globalThis.__SETZKASTEN_CONFIG__;
10
+ const adminPath = config?.adminPath ?? "/admin";
11
+ const adminUrl = new URL(adminPath, getPublicOrigin(request));
12
+ const code = url.searchParams.get("code");
13
+ if (!code) {
14
+ adminUrl.searchParams.set("github-app-error", "missing_code");
15
+ return new Response(null, { status: 302, headers: { Location: adminUrl.toString() } });
16
+ }
17
+ let data = null;
18
+ try {
19
+ const response = await fetch(`https://api.github.com/app-manifests/${code}/conversions`, {
20
+ method: "POST",
21
+ headers: { Accept: "application/vnd.github.v3+json" },
22
+ signal: AbortSignal.timeout(8e3)
23
+ });
24
+ if (!response.ok) throw new Error(`GitHub returned ${response.status}`);
25
+ data = await response.json();
26
+ } catch {
27
+ adminUrl.searchParams.set("github-app-error", "exchange_failed");
28
+ return new Response(null, { status: 302, headers: { Location: adminUrl.toString() } });
29
+ }
30
+ cookies.set(
31
+ COOKIE_NAME,
32
+ JSON.stringify({
33
+ appId: String(data.id),
34
+ slug: data.slug,
35
+ privateKey: data.pem,
36
+ clientId: data.client_id,
37
+ clientSecret: data.client_secret
38
+ }),
39
+ { httpOnly: false, sameSite: "lax", maxAge: COOKIE_MAX_AGE, path: "/" }
40
+ );
41
+ return new Response(null, { status: 302, headers: { Location: adminUrl.toString() } });
42
+ };
43
+ export {
44
+ GET
45
+ };
@@ -0,0 +1,15 @@
1
+ import { APIRoute } from 'astro';
2
+
3
+ /**
4
+ * Receives the installation_id from GitHub after the user installs the App.
5
+ * Merges it into the existing setup cookie and redirects back to the admin.
6
+ *
7
+ * GET /api/setzkasten/setup/github-app/installed?installation_id=xxx
8
+ *
9
+ * Note: uses mutable new Response() instead of Response.redirect() —
10
+ * Response.redirect() is immutable and prevents Astro from appending
11
+ * the Set-Cookie header (TypeError: immutable).
12
+ */
13
+ declare const GET: APIRoute;
14
+
15
+ export { GET };