@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,120 @@
1
+ import {
2
+ parseSession
3
+ } from "./chunk-INIWFKQ3.js";
4
+ import {
5
+ resolveStorageConfig
6
+ } from "./chunk-6UIKVKED.js";
7
+ import {
8
+ cachedFetch,
9
+ invalidateCache
10
+ } from "./chunk-45ARVNT3.js";
11
+ import {
12
+ resolveConfigRepoToken
13
+ } from "./chunk-NKDATSPA.js";
14
+ import {
15
+ withTrailers
16
+ } from "./chunk-KH22FJO5.js";
17
+
18
+ // src/api-routes/global-config.ts
19
+ var GLOBAL_CONFIG_FILE = (contentPath) => `${contentPath}/_global_config.json`;
20
+ var GET = async ({ cookies }) => {
21
+ const session = parseSession(cookies.get("setzkasten_session")?.value);
22
+ if (!session) return new Response("Unauthorized", { status: 401 });
23
+ const cfg = await readGlobalConfig();
24
+ return Response.json(cfg ?? {});
25
+ };
26
+ var PUT = async ({ request, cookies }) => {
27
+ const session = parseSession(cookies.get("setzkasten_session")?.value);
28
+ if (!session) return new Response("Unauthorized", { status: 401 });
29
+ if (session.user.role !== "admin") return new Response("Forbidden", { status: 403 });
30
+ let patch;
31
+ try {
32
+ patch = await request.json();
33
+ } catch {
34
+ return Response.json({ error: "Invalid request body" }, { status: 400 });
35
+ }
36
+ const current = await readGlobalConfig() ?? {};
37
+ const next = { ...current };
38
+ for (const [k, v] of Object.entries(patch)) {
39
+ if (v === null) delete next[k];
40
+ else next[k] = v;
41
+ }
42
+ await writeGlobalConfig(next);
43
+ return Response.json({ ok: true });
44
+ };
45
+ async function getStorageParams() {
46
+ const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
47
+ const storage = resolveStorageConfig();
48
+ if (!storage) return null;
49
+ const tokenResult = await resolveConfigRepoToken();
50
+ if (!tokenResult.ok) return null;
51
+ return {
52
+ owner: storage.owner,
53
+ repo: storage.repo,
54
+ branch: storage.branch,
55
+ contentPath: serverConfig?.storage?.contentPath ?? "content",
56
+ token: tokenResult.value
57
+ };
58
+ }
59
+ async function readGlobalConfig() {
60
+ const params = await getStorageParams();
61
+ if (!params) return null;
62
+ const { owner, repo, branch, contentPath, token } = params;
63
+ const key = `global-config:${owner}/${repo}:${branch}`;
64
+ return cachedFetch(key, 5 * 6e4, async () => {
65
+ const res = await fetch(
66
+ `https://api.github.com/repos/${owner}/${repo}/contents/${GLOBAL_CONFIG_FILE(contentPath)}?ref=${branch}`,
67
+ { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
68
+ );
69
+ if (!res.ok) return null;
70
+ const data = await res.json();
71
+ const raw = data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
72
+ return JSON.parse(raw);
73
+ });
74
+ }
75
+ async function writeGlobalConfig(config) {
76
+ const params = await getStorageParams();
77
+ if (!params) throw new Error("Storage not configured");
78
+ const { owner, repo, branch, contentPath, token } = params;
79
+ invalidateCache(`global-config:${owner}/${repo}:${branch}`);
80
+ const filePath = GLOBAL_CONFIG_FILE(contentPath);
81
+ const headers = {
82
+ Authorization: `Bearer ${token}`,
83
+ Accept: "application/vnd.github+json",
84
+ "X-GitHub-Api-Version": "2022-11-28",
85
+ "Content-Type": "application/json"
86
+ };
87
+ let sha;
88
+ try {
89
+ const existing = await fetch(
90
+ `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`,
91
+ { headers }
92
+ );
93
+ if (existing.ok) {
94
+ const data = await existing.json();
95
+ sha = data.sha;
96
+ }
97
+ } catch {
98
+ }
99
+ const body = {
100
+ message: withTrailers("chore(config): update global config"),
101
+ content: Buffer.from(JSON.stringify(config, null, 2)).toString("base64"),
102
+ branch
103
+ };
104
+ if (sha) body.sha = sha;
105
+ const res = await fetch(
106
+ `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`,
107
+ { method: "PUT", headers, body: JSON.stringify(body) }
108
+ );
109
+ if (!res.ok) {
110
+ const text = await res.text();
111
+ throw new Error(`GitHub write failed: ${text}`);
112
+ }
113
+ }
114
+
115
+ export {
116
+ GET,
117
+ PUT,
118
+ readGlobalConfig,
119
+ writeGlobalConfig
120
+ };
@@ -0,0 +1,87 @@
1
+ import {
2
+ withTrailers
3
+ } from "./chunk-KH22FJO5.js";
4
+
5
+ // src/api-routes/_pages-meta-store.ts
6
+ import {
7
+ emptyPagesMeta,
8
+ err,
9
+ networkError,
10
+ ok,
11
+ parsePagesMeta,
12
+ setPageLastModified
13
+ } from "@setzkasten-cms/core";
14
+ var RELATIVE_PATH = "_pages-meta.json";
15
+ var MAX_RETRIES = 3;
16
+ function metaFilePath(contentPath) {
17
+ return `${contentPath}/${RELATIVE_PATH}`;
18
+ }
19
+ function ghHeaders(token) {
20
+ return {
21
+ Authorization: `Bearer ${token}`,
22
+ Accept: "application/vnd.github+json",
23
+ "X-GitHub-Api-Version": "2022-11-28",
24
+ "Content-Type": "application/json"
25
+ };
26
+ }
27
+ function ghContentsUrl(target) {
28
+ return `https://api.github.com/repos/${target.owner}/${target.repo}/contents/${metaFilePath(target.contentPath)}`;
29
+ }
30
+ async function readPagesMeta(target) {
31
+ try {
32
+ const res = await fetch(`${ghContentsUrl(target)}?ref=${target.branch}`, {
33
+ headers: ghHeaders(target.token)
34
+ });
35
+ if (res.status === 404) {
36
+ return ok({ meta: emptyPagesMeta(), sha: null });
37
+ }
38
+ if (!res.ok) {
39
+ return err(networkError(`GitHub returned ${res.status} reading ${RELATIVE_PATH}`));
40
+ }
41
+ const data = await res.json();
42
+ const decoded = Buffer.from(data.content, "base64").toString("utf-8");
43
+ const parsed = parsePagesMeta(decoded);
44
+ if (!parsed.ok) return parsed;
45
+ return ok({ meta: parsed.value, sha: data.sha });
46
+ } catch (cause) {
47
+ const message = cause instanceof Error ? cause.message : "Unknown error";
48
+ return err(networkError(`Failed to read ${RELATIVE_PATH}: ${message}`, cause));
49
+ }
50
+ }
51
+ async function writePagesMeta(target, next, previousSha) {
52
+ const body = {
53
+ message: withTrailers("chore(meta): update _pages-meta.json"),
54
+ content: Buffer.from(JSON.stringify(next, null, 2)).toString("base64"),
55
+ branch: target.branch
56
+ };
57
+ if (previousSha) body.sha = previousSha;
58
+ return fetch(ghContentsUrl(target), {
59
+ method: "PUT",
60
+ headers: ghHeaders(target.token),
61
+ body: JSON.stringify(body)
62
+ });
63
+ }
64
+ async function recordPageEdit(target, pageKey, timestamp = Date.now()) {
65
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
66
+ const current = await readPagesMeta(target);
67
+ if (!current.ok) return current;
68
+ const next = setPageLastModified(current.value.meta, pageKey, timestamp);
69
+ let response;
70
+ try {
71
+ response = await writePagesMeta(target, next, current.value.sha);
72
+ } catch (cause) {
73
+ const message = cause instanceof Error ? cause.message : "Unknown error";
74
+ return err(networkError(`Failed to write ${RELATIVE_PATH}: ${message}`, cause));
75
+ }
76
+ if (response.ok) return ok(void 0);
77
+ if (response.status === 409 && attempt < MAX_RETRIES - 1) continue;
78
+ const text = await response.text().catch(() => "");
79
+ return err(networkError(`GitHub PUT failed: ${response.status} ${text}`));
80
+ }
81
+ return err(networkError(`recordPageEdit: exhausted ${MAX_RETRIES} retries`));
82
+ }
83
+
84
+ export {
85
+ readPagesMeta,
86
+ recordPageEdit
87
+ };
@@ -0,0 +1,48 @@
1
+ // src/api-routes/section-management.ts
2
+ function removeFromPageConfig(config, sectionKey) {
3
+ const sections = config.sections.filter((s) => s.key !== sectionKey).map((s, i) => ({ ...s, order: i }));
4
+ return { ...config, sections };
5
+ }
6
+ function generateDuplicateKey(existingKeys, originalKey) {
7
+ const base = `${originalKey}--copy`;
8
+ if (!existingKeys.includes(base)) return base;
9
+ let n = 2;
10
+ while (existingKeys.includes(`${base}${n}`)) n++;
11
+ return `${base}${n}`;
12
+ }
13
+ function generateAddKey(existingKeys, type) {
14
+ if (!existingKeys.includes(type)) return type;
15
+ let n = 2;
16
+ while (existingKeys.includes(`${type}--${n}`)) n++;
17
+ return `${type}--${n}`;
18
+ }
19
+ function addToPageConfig(config, key, type) {
20
+ const entry = {
21
+ key,
22
+ enabled: true,
23
+ order: config.sections.length,
24
+ ...key !== type ? { type } : {}
25
+ };
26
+ return { ...config, sections: [...config.sections, entry] };
27
+ }
28
+ function duplicateInPageConfig(config, originalKey, newKey) {
29
+ const original = config.sections.find((s) => s.key === originalKey);
30
+ if (!original) return config;
31
+ const resolvedType = original.type ?? originalKey;
32
+ const copy = { ...original, key: newKey, enabled: true, type: resolvedType };
33
+ const insertAfter = config.sections.indexOf(original);
34
+ const sections = [
35
+ ...config.sections.slice(0, insertAfter + 1),
36
+ copy,
37
+ ...config.sections.slice(insertAfter + 1)
38
+ ].map((s, i) => ({ ...s, order: i }));
39
+ return { ...config, sections };
40
+ }
41
+
42
+ export {
43
+ removeFromPageConfig,
44
+ generateDuplicateKey,
45
+ generateAddKey,
46
+ addToPageConfig,
47
+ duplicateInPageConfig
48
+ };
@@ -0,0 +1,37 @@
1
+ import {
2
+ prefixPath
3
+ } from "./chunk-6UIKVKED.js";
4
+
5
+ // src/api-routes/catalog-helpers.ts
6
+ import { registry } from "@setzkasten-cms/catalog";
7
+ function buildCatalogResponse() {
8
+ return registry.list();
9
+ }
10
+ function validateCatalogAddBody(body) {
11
+ if (!body.templateName || typeof body.templateName !== "string") {
12
+ throw new Error("templateName is required");
13
+ }
14
+ if (!body.pageKey || typeof body.pageKey !== "string") {
15
+ throw new Error("pageKey is required");
16
+ }
17
+ registry.get(body.templateName);
18
+ return {
19
+ templateName: body.templateName,
20
+ pageKey: body.pageKey,
21
+ sectionKey: typeof body.sectionKey === "string" ? body.sectionKey : void 0
22
+ };
23
+ }
24
+ function buildCatalogAddCommit(opts) {
25
+ const sectionJsonPath = prefixPath(
26
+ `${opts.contentPath}/_sections/${opts.sectionKey}.json`,
27
+ opts.projectPrefix
28
+ );
29
+ const pageConfigPath = prefixPath(opts.pageConfigPath, opts.projectPrefix);
30
+ return { sectionJsonPath, pageConfigPath };
31
+ }
32
+
33
+ export {
34
+ buildCatalogResponse,
35
+ validateCatalogAddBody,
36
+ buildCatalogAddCommit
37
+ };
@@ -0,0 +1,236 @@
1
+ import {
2
+ resolveStorageConfig
3
+ } from "./chunk-6UIKVKED.js";
4
+ import {
5
+ gateFeature
6
+ } from "./chunk-5PIMDP4N.js";
7
+ import {
8
+ cachedFetch,
9
+ invalidateCache
10
+ } from "./chunk-45ARVNT3.js";
11
+ import {
12
+ resolveConfigRepoToken
13
+ } from "./chunk-NKDATSPA.js";
14
+ import {
15
+ withTrailers
16
+ } from "./chunk-KH22FJO5.js";
17
+
18
+ // src/api-routes/_auth-guard.ts
19
+ import { canEditPage } from "@setzkasten-cms/auth";
20
+ function parseSession(raw) {
21
+ if (!raw) return null;
22
+ try {
23
+ return JSON.parse(raw);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function requireAdmin(rawSession) {
29
+ const session = parseSession(rawSession);
30
+ if (!session) {
31
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
32
+ status: 401,
33
+ headers: { "Content-Type": "application/json" }
34
+ });
35
+ }
36
+ if (session.user.role !== "admin") {
37
+ return new Response(JSON.stringify({ error: "Forbidden: admin role required" }), {
38
+ status: 403,
39
+ headers: { "Content-Type": "application/json" }
40
+ });
41
+ }
42
+ return null;
43
+ }
44
+ async function guardPageAccess(session, pageKey, fullConfig, request) {
45
+ if (!session) return new Response("Unauthorized", { status: 401 });
46
+ const editorsLookup = await resolveDynamicEditors();
47
+ if (!editorsLookup.ok) {
48
+ return new Response(
49
+ `Forbidden: editor permissions unavailable (${editorsLookup.error})`,
50
+ { status: 503 }
51
+ );
52
+ }
53
+ if (!canEditPage(session, pageKey, editorsLookup.editors)) {
54
+ return new Response("Forbidden: you do not have access to this page", { status: 403 });
55
+ }
56
+ if (request) {
57
+ const denied = await guardWebsiteAccess(session, request);
58
+ if (denied) return denied;
59
+ }
60
+ return null;
61
+ }
62
+ async function guardWebsiteAccess(session, request) {
63
+ if (session.user.role === "admin") return null;
64
+ const { resolveCurrentWebsite } = await import("./api-routes/_website-resolver.js");
65
+ const website = await resolveCurrentWebsite(request);
66
+ if (!website.ok) return null;
67
+ const allowed = website.value.allowedEmails;
68
+ if (!allowed || allowed.length === 0) return null;
69
+ if (!allowed.includes(session.user.email)) {
70
+ return new Response(
71
+ `Forbidden: not allowed on website "${website.value.id}"`,
72
+ { status: 403 }
73
+ );
74
+ }
75
+ return null;
76
+ }
77
+ async function resolveDynamicEditors() {
78
+ const storage = resolveStorageConfig();
79
+ if (!storage) return { ok: true, editors: void 0 };
80
+ const tokenResult = await resolveConfigRepoToken();
81
+ if (!tokenResult.ok) {
82
+ return { ok: false, error: `token: ${tokenResult.error.message}` };
83
+ }
84
+ const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
85
+ const contentPath = serverConfig?.storage?.contentPath ?? "content";
86
+ const status = await readEditorsFileStatus(
87
+ storage.owner,
88
+ storage.repo,
89
+ storage.branch,
90
+ contentPath,
91
+ tokenResult.value
92
+ );
93
+ if (status.kind === "absent") return { ok: true, editors: void 0 };
94
+ if (status.kind === "present") return { ok: true, editors: status.editors };
95
+ return { ok: false, error: status.message };
96
+ }
97
+
98
+ // src/api-routes/editors.ts
99
+ import { validateEditorsUpdate } from "@setzkasten-cms/core";
100
+ var EDITORS_FILE = (contentPath) => `${contentPath}/_editors.json`;
101
+ function configRepoStorage() {
102
+ const storage = resolveStorageConfig();
103
+ if (!storage) return null;
104
+ return { owner: storage.owner, repo: storage.repo, branch: storage.branch };
105
+ }
106
+ var GET = async ({ cookies }) => {
107
+ const session = parseSession(cookies.get("setzkasten_session")?.value);
108
+ if (!session) return new Response("Unauthorized", { status: 401 });
109
+ const tokenResult = await resolveConfigRepoToken();
110
+ if (!tokenResult.ok) return new Response("GitHub token not configured", { status: 500 });
111
+ const storage = configRepoStorage();
112
+ if (!storage) return Response.json({ error: "Could not resolve storage config" }, { status: 400 });
113
+ const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
114
+ const contentPath = serverConfig?.storage?.contentPath ?? "content";
115
+ const { owner, repo, branch } = storage;
116
+ const raw = await readEditorsFile(owner, repo, branch, contentPath, tokenResult.value);
117
+ return Response.json(raw ?? []);
118
+ };
119
+ var PUT = async ({ request, cookies }) => {
120
+ const session = parseSession(cookies.get("setzkasten_session")?.value);
121
+ if (!session) return new Response("Unauthorized", { status: 401 });
122
+ if (session.user.role !== "admin") return new Response("Forbidden", { status: 403 });
123
+ const gate = gateFeature("editors");
124
+ if (gate) return gate;
125
+ const tokenResult = await resolveConfigRepoToken();
126
+ if (!tokenResult.ok) return new Response("GitHub token not configured", { status: 500 });
127
+ const storage = configRepoStorage();
128
+ if (!storage) return Response.json({ error: "Could not resolve storage config" }, { status: 400 });
129
+ const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
130
+ const contentPath = serverConfig?.storage?.contentPath ?? "content";
131
+ const { owner, repo, branch } = storage;
132
+ let editors;
133
+ try {
134
+ editors = await request.json();
135
+ if (!Array.isArray(editors)) throw new Error("Expected array");
136
+ } catch {
137
+ return Response.json({ error: "Invalid request body" }, { status: 400 });
138
+ }
139
+ const validation = validateEditorsUpdate(editors, session.user.email);
140
+ if (!validation.ok) {
141
+ return Response.json(
142
+ { error: validation.message, code: validation.code },
143
+ { status: 400 }
144
+ );
145
+ }
146
+ const filePath = EDITORS_FILE(contentPath);
147
+ const fileContent = JSON.stringify(editors, null, 2);
148
+ const headers = {
149
+ Authorization: `Bearer ${tokenResult.value}`,
150
+ Accept: "application/vnd.github+json",
151
+ "X-GitHub-Api-Version": "2022-11-28",
152
+ "Content-Type": "application/json"
153
+ };
154
+ const existing = await fetchFileSha(owner, repo, branch, filePath, headers);
155
+ const body = {
156
+ message: withTrailers("chore(editors): update content editor permissions"),
157
+ content: Buffer.from(fileContent).toString("base64"),
158
+ branch
159
+ };
160
+ if (existing) body.sha = existing;
161
+ const res = await fetch(
162
+ `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`,
163
+ { method: "PUT", headers, body: JSON.stringify(body) }
164
+ );
165
+ if (!res.ok) {
166
+ const text = await res.text();
167
+ return Response.json({ error: `GitHub write failed: ${text}` }, { status: 502 });
168
+ }
169
+ invalidateCache(`editors:${owner}/${repo}:${branch}`);
170
+ return Response.json({ ok: true });
171
+ };
172
+ async function fetchFileSha(owner, repo, branch, path, headers) {
173
+ try {
174
+ const res = await fetch(
175
+ `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
176
+ { headers }
177
+ );
178
+ if (!res.ok) return null;
179
+ const data = await res.json();
180
+ return data.sha ?? null;
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+ async function readEditorsFile(owner, repo, branch, contentPath, token) {
186
+ const key = `editors:${owner}/${repo}:${branch}`;
187
+ return cachedFetch(key, 2 * 6e4, async () => {
188
+ const res = await fetch(
189
+ `https://api.github.com/repos/${owner}/${repo}/contents/${EDITORS_FILE(contentPath)}?ref=${branch}`,
190
+ { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
191
+ );
192
+ if (!res.ok) return null;
193
+ const data = await res.json();
194
+ const raw = data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
195
+ return JSON.parse(raw);
196
+ });
197
+ }
198
+ async function readEditorsFileStatus(owner, repo, branch, contentPath, token) {
199
+ let res;
200
+ try {
201
+ res = await fetch(
202
+ `https://api.github.com/repos/${owner}/${repo}/contents/${EDITORS_FILE(contentPath)}?ref=${branch}`,
203
+ {
204
+ headers: {
205
+ Authorization: `Bearer ${token}`,
206
+ Accept: "application/vnd.github+json",
207
+ "X-GitHub-Api-Version": "2022-11-28"
208
+ }
209
+ }
210
+ );
211
+ } catch (err) {
212
+ return { kind: "error", message: err instanceof Error ? err.message : "network error" };
213
+ }
214
+ if (res.status === 404) return { kind: "absent" };
215
+ if (!res.ok) {
216
+ return { kind: "error", message: `GitHub returned ${res.status}` };
217
+ }
218
+ try {
219
+ const data = await res.json();
220
+ const raw = data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
221
+ return { kind: "present", editors: JSON.parse(raw) };
222
+ } catch (err) {
223
+ return { kind: "error", message: err instanceof Error ? err.message : "parse error" };
224
+ }
225
+ }
226
+
227
+ export {
228
+ GET,
229
+ PUT,
230
+ readEditorsFile,
231
+ readEditorsFileStatus,
232
+ parseSession,
233
+ requireAdmin,
234
+ guardPageAccess,
235
+ guardWebsiteAccess
236
+ };
@@ -0,0 +1,24 @@
1
+ // src/api-routes/_session-cookie.ts
2
+ var SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
3
+ function sessionCookieOptions(secure) {
4
+ return {
5
+ httpOnly: true,
6
+ secure,
7
+ sameSite: "lax",
8
+ path: "/",
9
+ maxAge: SESSION_MAX_AGE_SECONDS,
10
+ ...resolveCookieDomain() ? { domain: resolveCookieDomain() } : {}
11
+ };
12
+ }
13
+ function resolveCookieDomain() {
14
+ const fullConfig = globalThis.__SETZKASTEN_FULL_CONFIG__;
15
+ const fromConfig = fullConfig?.auth?.cookieDomain;
16
+ if (typeof fromConfig === "string" && fromConfig) return fromConfig;
17
+ const fromEnv = process.env.SETZKASTEN_COOKIE_DOMAIN;
18
+ if (typeof fromEnv === "string" && fromEnv) return fromEnv;
19
+ return void 0;
20
+ }
21
+
22
+ export {
23
+ sessionCookieOptions
24
+ };