@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,5 +1,14 @@
1
1
  import { AuthSession, SetzKastenConfig } from '@setzkasten-cms/core';
2
2
 
3
+ /**
4
+ * Returns the verified session iff the cookie's HMAC signature matches
5
+ * the server secret AND `expiresAt` is in the future. Pre-C1 this used
6
+ * to do `JSON.parse` on whatever the client sent — any unauthenticated
7
+ * attacker could forge an admin session in 30 seconds.
8
+ *
9
+ * Single source of truth: every guard (`requireAdmin`, `guardPageAccess`)
10
+ * runs through this. No code path reads the cookie directly anymore.
11
+ */
3
12
  declare function parseSession(raw: string | undefined): AuthSession | null;
4
13
  /**
5
14
  * Returns 401 if the request has no valid session, 403 if the user is not
@@ -38,9 +47,24 @@ declare function guardPageAccess(session: AuthSession | null, pageKey: string, f
38
47
  * X-SK-Website header resolves to. Admins always pass; single-mode
39
48
  * skips this check entirely (no website context).
40
49
  *
41
- * Returns 403 on deny, null on allow. Resolver failures are
42
- * considered "no per-website restriction" the global guard above
43
- * already covers fail-closed for editors-file errors.
50
+ * Default-deny semantics (post-C5): in multi-mode, an editor with no
51
+ * explicit `allowedEmails` entry on the target website is denied.
52
+ *
53
+ * Pre-fix: an editor on website A could send `X-SK-Website: B` and
54
+ * inherit access transitively from the global editors file, because
55
+ * "no allowedEmails on B" was treated as "no restriction". That made
56
+ * the global file the only effective gate — an editor for project A
57
+ * could mutate project B by spoofing one HTTP header.
58
+ *
59
+ * Now: empty / missing `allowedEmails` means "no editors allowed on
60
+ * this website" — only admins pass. Set `allowedEmails: [editor@…]`
61
+ * explicitly to grant access. Existing single-website-per-editor
62
+ * setups need to add the email to the website entry; the migration
63
+ * note in `docs/migration-single-to-multi.md` documents this.
64
+ *
65
+ * Returns 403 on deny, null on allow. Resolver failures (no website
66
+ * context) are still treated as "no check applies" so single-mode
67
+ * routes work unchanged.
44
68
  */
45
69
  declare function guardWebsiteAccess(session: AuthSession, request: Request): Promise<Response | null>;
46
70
 
@@ -3,11 +3,14 @@ import {
3
3
  guardWebsiteAccess,
4
4
  parseSession,
5
5
  requireAdmin
6
- } from "../chunk-INIWFKQ3.js";
6
+ } from "../chunk-Q5HV47DW.js";
7
+ import "../chunk-QVCW6EF3.js";
8
+ import "../chunk-KENFINT4.js";
7
9
  import "../chunk-6UIKVKED.js";
10
+ import "../chunk-ONP6BRZO.js";
8
11
  import "../chunk-5PIMDP4N.js";
9
12
  import "../chunk-45ARVNT3.js";
10
- import "../chunk-NKDATSPA.js";
13
+ import "../chunk-DP6RTINQ.js";
11
14
  import "../chunk-TJNJKPUL.js";
12
15
  import "../chunk-KH22FJO5.js";
13
16
  export {
@@ -0,0 +1,8 @@
1
+ declare function ensureDevSessionSecret(): string;
2
+ /**
3
+ * Test-only escape hatch: lets a test helper set the secret without
4
+ * touching the filesystem. Production code never calls this.
5
+ */
6
+ declare function __setDevSessionSecretForTests(secret: string | null): void;
7
+
8
+ export { __setDevSessionSecretForTests, ensureDevSessionSecret };
@@ -0,0 +1,8 @@
1
+ import {
2
+ __setDevSessionSecretForTests,
3
+ ensureDevSessionSecret
4
+ } from "../chunk-ONP6BRZO.js";
5
+ export {
6
+ __setDevSessionSecretForTests,
7
+ ensureDevSessionSecret
8
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  resolveConfigRepoToken,
3
3
  resolveGitHubTokenForRequest
4
- } from "../chunk-NKDATSPA.js";
4
+ } from "../chunk-DP6RTINQ.js";
5
5
  export {
6
6
  resolveConfigRepoToken,
7
7
  resolveGitHubTokenForRequest
@@ -1,11 +1,14 @@
1
1
  import {
2
2
  makeRoleResolver
3
- } from "../chunk-ZQDGGWJP.js";
4
- import "../chunk-INIWFKQ3.js";
3
+ } from "../chunk-5KMGSFCZ.js";
4
+ import "../chunk-Q5HV47DW.js";
5
+ import "../chunk-QVCW6EF3.js";
6
+ import "../chunk-KENFINT4.js";
5
7
  import "../chunk-6UIKVKED.js";
8
+ import "../chunk-ONP6BRZO.js";
6
9
  import "../chunk-5PIMDP4N.js";
7
10
  import "../chunk-45ARVNT3.js";
8
- import "../chunk-NKDATSPA.js";
11
+ import "../chunk-DP6RTINQ.js";
9
12
  import "../chunk-TJNJKPUL.js";
10
13
  import "../chunk-KH22FJO5.js";
11
14
  export {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolves the HMAC secret used to sign session cookies.
3
+ *
4
+ * Priority:
5
+ * 1. `SETZKASTEN_SESSION_SECRET` env var (production requirement)
6
+ * 2. `__SETZKASTEN_BUILD_CONFIG__.sessionSecret` (build-time injection)
7
+ * 3. **Dev-only:** persistent per-machine random secret in
8
+ * `.setzkasten/dev-secret` (gitignored). Generated on first run,
9
+ * reused thereafter. Never the same string across machines, never
10
+ * checked into the repo.
11
+ *
12
+ * Pre-C1 there was no signing at all. Pre-this-fix the dev fallback was
13
+ * a hardcoded string in source — anyone reading the code could mint
14
+ * admin cookies against any deployment that accidentally ran without
15
+ * NODE_ENV=production. The fallback is now random per-machine.
16
+ */
17
+ declare function resolveSessionSecret(): string;
18
+
19
+ export { resolveSessionSecret };
@@ -0,0 +1,7 @@
1
+ import {
2
+ resolveSessionSecret
3
+ } from "../chunk-QVCW6EF3.js";
4
+ import "../chunk-ONP6BRZO.js";
5
+ export {
6
+ resolveSessionSecret
7
+ };
@@ -0,0 +1,45 @@
1
+ import { AuthSession } from '@setzkasten-cms/core';
2
+
3
+ /**
4
+ * Signed session cookies.
5
+ *
6
+ * Pre-C1 the session cookie was `JSON.stringify(session)` — readable and
7
+ * forgeable. An unauthenticated attacker could craft any role for any
8
+ * email and walk in. This module replaces that with HMAC-SHA256 over a
9
+ * canonical JSON payload, and refuses cookies whose signature doesn't
10
+ * match a known secret.
11
+ *
12
+ * Wire format:
13
+ *
14
+ * <base64url(JSON.stringify(payload))>.<base64url(HMAC-SHA256(payload))>
15
+ *
16
+ * Two segments separated by a literal `.`. Both segments are base64url
17
+ * (no padding). Anything that doesn't decode to that shape is rejected.
18
+ *
19
+ * The secret is supplied per-call so this module stays pure and keeps
20
+ * the secret-resolution policy (env var, cookie domain, fail-closed in
21
+ * prod) at the integration layer.
22
+ */
23
+
24
+ /**
25
+ * Produces the cookie value for a verified session. Throws on missing
26
+ * secret — the caller (always server-side) must have one resolved.
27
+ */
28
+ declare function signSession(session: AuthSession, secret: string): string;
29
+ type VerifyResult = {
30
+ readonly ok: true;
31
+ readonly value: AuthSession;
32
+ } | {
33
+ readonly ok: false;
34
+ readonly reason: VerifyFailureReason;
35
+ };
36
+ type VerifyFailureReason = 'malformed' | 'bad-signature' | 'invalid-payload' | 'expired' | 'missing-expiry';
37
+ /**
38
+ * Returns the parsed session iff the signature matches AND the payload
39
+ * has a numeric `expiresAt` in the future. Rejects everything else.
40
+ *
41
+ * @param now Override timestamp for tests. Defaults to `Date.now()`.
42
+ */
43
+ declare function verifySessionCookie(raw: string, secret: string, now?: number): VerifyResult;
44
+
45
+ export { type VerifyFailureReason, type VerifyResult, signSession, verifySessionCookie };
@@ -0,0 +1,8 @@
1
+ import {
2
+ signSession,
3
+ verifySessionCookie
4
+ } from "../chunk-KENFINT4.js";
5
+ export {
6
+ signSession,
7
+ verifySessionCookie
8
+ };
@@ -1,9 +1,9 @@
1
- import {
2
- signPayload
3
- } from "../chunk-737TIZRU.js";
4
1
  import {
5
2
  recordWebhookFire
6
3
  } from "../chunk-W3QHY5GW.js";
4
+ import {
5
+ signPayload
6
+ } from "../chunk-737TIZRU.js";
7
7
  import {
8
8
  resolveStorageConfigForRequest
9
9
  } from "../chunk-6UIKVKED.js";
@@ -12,7 +12,7 @@ import {
12
12
  } from "../chunk-45ARVNT3.js";
13
13
  import {
14
14
  resolveGitHubTokenForRequest
15
- } from "../chunk-NKDATSPA.js";
15
+ } from "../chunk-DP6RTINQ.js";
16
16
 
17
17
  // src/api-routes/_webhook-dispatcher.ts
18
18
  import {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveGitHubTokenForRequest
3
- } from "../chunk-NKDATSPA.js";
3
+ } from "../chunk-DP6RTINQ.js";
4
4
 
5
5
  // src/api-routes/asset-proxy.ts
6
6
  var GET = async ({ params, request, cookies }) => {
@@ -1,14 +1,21 @@
1
+ import {
2
+ makeRoleResolver
3
+ } from "../chunk-5KMGSFCZ.js";
1
4
  import {
2
5
  sessionCookieOptions
3
6
  } from "../chunk-JHY6XTLL.js";
7
+ import "../chunk-Q5HV47DW.js";
4
8
  import {
5
- makeRoleResolver
6
- } from "../chunk-ZQDGGWJP.js";
7
- import "../chunk-INIWFKQ3.js";
9
+ resolveSessionSecret
10
+ } from "../chunk-QVCW6EF3.js";
11
+ import {
12
+ signSession
13
+ } from "../chunk-KENFINT4.js";
8
14
  import "../chunk-6UIKVKED.js";
15
+ import "../chunk-ONP6BRZO.js";
9
16
  import "../chunk-5PIMDP4N.js";
10
17
  import "../chunk-45ARVNT3.js";
11
- import "../chunk-NKDATSPA.js";
18
+ import "../chunk-DP6RTINQ.js";
12
19
  import "../chunk-TJNJKPUL.js";
13
20
  import "../chunk-KH22FJO5.js";
14
21
 
@@ -55,7 +62,7 @@ var GET = async ({ request, url, cookies, redirect }) => {
55
62
  const session = sessionResult.value;
56
63
  cookies.set(
57
64
  "setzkasten_session",
58
- JSON.stringify({ user: session.user, expiresAt: session.expiresAt }),
65
+ signSession({ user: session.user, expiresAt: session.expiresAt }, resolveSessionSecret()),
59
66
  sessionCookieOptions(import.meta.env.PROD)
60
67
  );
61
68
  return redirect(adminPath);
@@ -1,10 +1,10 @@
1
1
  import { APIRoute } from 'astro';
2
2
 
3
+ declare const POST: APIRoute;
3
4
  /**
4
- * Logout clears the session cookie and redirects to home.
5
- * Mirrors the same `domain` attribute used when the cookie was set so the
6
- * browser actually deletes it on subdomains in standalone-admin setups.
5
+ * Backwards-compat: a few existing UI links still hit GET. We keep the
6
+ * handler but log a deprecation warning so the SPA can be migrated.
7
7
  */
8
8
  declare const GET: APIRoute;
9
9
 
10
- export { GET };
10
+ export { GET, POST };
@@ -3,11 +3,17 @@ import {
3
3
  } from "../chunk-JHY6XTLL.js";
4
4
 
5
5
  // src/api-routes/auth-logout.ts
6
- var GET = async ({ cookies, redirect }) => {
6
+ async function handleLogout({ cookies, redirect }) {
7
7
  const opts = sessionCookieOptions(false);
8
8
  cookies.delete("setzkasten_session", { path: "/", domain: opts.domain });
9
9
  return redirect("/");
10
+ }
11
+ var POST = handleLogout;
12
+ var GET = async (ctx) => {
13
+ console.warn("[setzkasten] GET /api/setzkasten/logout is deprecated \u2014 use POST");
14
+ return handleLogout(ctx);
10
15
  };
11
16
  export {
12
- GET
17
+ GET,
18
+ POST
13
19
  };
@@ -3,6 +3,12 @@ import { APIRoute } from 'astro';
3
3
  /**
4
4
  * Session check – returns current user info or 401.
5
5
  * Used by the admin SPA to check if the user is logged in.
6
+ *
7
+ * Goes through `parseSession` so signature verification + expiry are
8
+ * enforced in exactly one place. Pre-C1 this route did its own
9
+ * `JSON.parse` — the only one that ever checked `expiresAt`, while
10
+ * `_auth-guard.parseSession` ignored it. Now both run through the
11
+ * same verifier.
6
12
  */
7
13
  declare const GET: APIRoute;
8
14
 
@@ -1,30 +1,30 @@
1
+ import {
2
+ parseSession
3
+ } from "../chunk-Q5HV47DW.js";
4
+ import "../chunk-QVCW6EF3.js";
5
+ import "../chunk-KENFINT4.js";
6
+ import "../chunk-6UIKVKED.js";
7
+ import "../chunk-ONP6BRZO.js";
8
+ import "../chunk-5PIMDP4N.js";
9
+ import "../chunk-45ARVNT3.js";
10
+ import "../chunk-DP6RTINQ.js";
11
+ import "../chunk-TJNJKPUL.js";
12
+ import "../chunk-KH22FJO5.js";
13
+
1
14
  // src/api-routes/auth-session.ts
2
15
  var GET = async ({ cookies }) => {
3
- const session = cookies.get("setzkasten_session")?.value;
16
+ const raw = cookies.get("setzkasten_session")?.value;
17
+ const session = parseSession(raw);
4
18
  if (!session) {
19
+ if (raw) cookies.delete("setzkasten_session", { path: "/" });
5
20
  return new Response(JSON.stringify({ authenticated: false }), {
6
21
  status: 401,
7
22
  headers: { "Content-Type": "application/json" }
8
23
  });
9
24
  }
10
- try {
11
- const parsed = JSON.parse(session);
12
- if (parsed.expiresAt < Date.now()) {
13
- cookies.delete("setzkasten_session", { path: "/" });
14
- return new Response(JSON.stringify({ authenticated: false, reason: "expired" }), {
15
- status: 401,
16
- headers: { "Content-Type": "application/json" }
17
- });
18
- }
19
- return new Response(JSON.stringify({ authenticated: true, user: parsed.user }), {
20
- headers: { "Content-Type": "application/json" }
21
- });
22
- } catch {
23
- return new Response(JSON.stringify({ authenticated: false }), {
24
- status: 401,
25
- headers: { "Content-Type": "application/json" }
26
- });
27
- }
25
+ return new Response(JSON.stringify({ authenticated: true, user: session.user }), {
26
+ headers: { "Content-Type": "application/json" }
27
+ });
28
28
  };
29
29
  export {
30
30
  GET
@@ -1,25 +1,32 @@
1
1
  import {
2
2
  readGlobalConfig
3
- } from "../chunk-AM4DZXXM.js";
3
+ } from "../chunk-UJAFZEX2.js";
4
+ import {
5
+ makeRoleResolver
6
+ } from "../chunk-5KMGSFCZ.js";
4
7
  import {
5
8
  sessionCookieOptions
6
9
  } from "../chunk-JHY6XTLL.js";
7
- import {
8
- makeRoleResolver
9
- } from "../chunk-ZQDGGWJP.js";
10
10
  import {
11
11
  readEditorsFile
12
- } from "../chunk-INIWFKQ3.js";
12
+ } from "../chunk-Q5HV47DW.js";
13
+ import {
14
+ resolveSessionSecret
15
+ } from "../chunk-QVCW6EF3.js";
16
+ import {
17
+ signSession
18
+ } from "../chunk-KENFINT4.js";
13
19
  import {
14
20
  resolveStorageConfig
15
21
  } from "../chunk-6UIKVKED.js";
22
+ import "../chunk-ONP6BRZO.js";
16
23
  import {
17
24
  gateFeature
18
25
  } from "../chunk-5PIMDP4N.js";
19
26
  import "../chunk-45ARVNT3.js";
20
27
  import {
21
28
  resolveConfigRepoToken
22
- } from "../chunk-NKDATSPA.js";
29
+ } from "../chunk-DP6RTINQ.js";
23
30
  import "../chunk-TJNJKPUL.js";
24
31
  import "../chunk-KH22FJO5.js";
25
32
 
@@ -64,7 +71,7 @@ var POST = async ({ request, cookies }) => {
64
71
  const session = result.value;
65
72
  cookies.set(
66
73
  "setzkasten_session",
67
- JSON.stringify({ user: session.user, expiresAt: session.expiresAt }),
74
+ signSession({ user: session.user, expiresAt: session.expiresAt }, resolveSessionSecret()),
68
75
  sessionCookieOptions(import.meta.env.PROD)
69
76
  );
70
77
  return Response.json({ ok: true });
@@ -9,16 +9,19 @@ import {
9
9
  import {
10
10
  guardPageAccess,
11
11
  parseSession
12
- } from "../chunk-INIWFKQ3.js";
12
+ } from "../chunk-Q5HV47DW.js";
13
+ import "../chunk-QVCW6EF3.js";
14
+ import "../chunk-KENFINT4.js";
13
15
  import {
14
16
  prefixPath,
15
17
  resolveStorageConfigForRequest
16
18
  } from "../chunk-6UIKVKED.js";
19
+ import "../chunk-ONP6BRZO.js";
17
20
  import "../chunk-5PIMDP4N.js";
18
21
  import "../chunk-45ARVNT3.js";
19
22
  import {
20
23
  resolveGitHubTokenForRequest
21
- } from "../chunk-NKDATSPA.js";
24
+ } from "../chunk-DP6RTINQ.js";
22
25
  import "../chunk-TJNJKPUL.js";
23
26
  import {
24
27
  withTrailers
@@ -40,7 +43,10 @@ var POST = async ({ request, cookies }) => {
40
43
  try {
41
44
  validated = validateCatalogAddBody(body);
42
45
  } catch (e) {
43
- return Response.json({ error: e instanceof Error ? e.message : "Invalid request" }, { status: 400 });
46
+ return Response.json(
47
+ { error: e instanceof Error ? e.message : "Invalid request" },
48
+ { status: 400 }
49
+ );
44
50
  }
45
51
  const { templateName, pageKey } = validated;
46
52
  const storage = await resolveStorageConfigForRequest(request, body);
@@ -49,7 +55,12 @@ var POST = async ({ request, cookies }) => {
49
55
  const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
50
56
  const fullConfig = globalThis.__SETZKASTEN_FULL_CONFIG__;
51
57
  const contentPath = body.contentPath || serverConfig?.storage?.contentPath || "content";
52
- const denied = await guardPageAccess(parseSession(cookies.get("setzkasten_session")?.value), pageKey, fullConfig, request);
58
+ const denied = await guardPageAccess(
59
+ parseSession(cookies.get("setzkasten_session")?.value),
60
+ pageKey,
61
+ fullConfig,
62
+ request
63
+ );
53
64
  if (denied) return denied;
54
65
  const headers = {
55
66
  Authorization: `Bearer ${githubToken}`,
@@ -66,7 +77,10 @@ var POST = async ({ request, cookies }) => {
66
77
  const existingKeys = (pageConfig.sections ?? []).map((s) => s.key);
67
78
  const sectionKey = validated.sectionKey ?? generateAddKey(existingKeys, templateName);
68
79
  if (existingKeys.includes(sectionKey)) {
69
- return Response.json({ error: `Key "${sectionKey}" already exists on this page` }, { status: 409 });
80
+ return Response.json(
81
+ { error: `Key "${sectionKey}" already exists on this page` },
82
+ { status: 409 }
83
+ );
70
84
  }
71
85
  const template = registry.get(templateName);
72
86
  const { sectionJsonPath } = buildCatalogAddCommit({
@@ -106,7 +120,13 @@ async function fetchFileContent(owner, repo, branch, path, token) {
106
120
  try {
107
121
  const res = await fetch(
108
122
  `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
109
- { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
123
+ {
124
+ headers: {
125
+ Authorization: `Bearer ${token}`,
126
+ Accept: "application/vnd.github+json",
127
+ "X-GitHub-Api-Version": "2022-11-28"
128
+ }
129
+ }
110
130
  );
111
131
  if (!res.ok) return null;
112
132
  const data = await res.json();
@@ -117,16 +137,34 @@ async function fetchFileContent(owner, repo, branch, path, token) {
117
137
  }
118
138
  async function batchCommit(owner, repo, branch, files, message, headers) {
119
139
  try {
120
- const refRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, { headers });
140
+ const refRes = await fetch(
141
+ `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
142
+ { headers }
143
+ );
121
144
  if (!refRes.ok) return { ok: false, error: `Failed to get HEAD: ${refRes.status}` };
122
- const { object: { sha: headSha } } = await refRes.json();
123
- const commitRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`, { headers });
145
+ const {
146
+ object: { sha: headSha }
147
+ } = await refRes.json();
148
+ const commitRes = await fetch(
149
+ `https://api.github.com/repos/${owner}/${repo}/git/commits/${headSha}`,
150
+ { headers }
151
+ );
124
152
  if (!commitRes.ok) return { ok: false, error: `Failed to get commit: ${commitRes.status}` };
125
- const { tree: { sha: baseSha } } = await commitRes.json();
153
+ const {
154
+ tree: { sha: baseSha }
155
+ } = await commitRes.json();
126
156
  const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees`, {
127
157
  method: "POST",
128
158
  headers,
129
- body: JSON.stringify({ base_tree: baseSha, tree: files.map((f) => ({ path: f.path, mode: "100644", type: "blob", content: f.content })) })
159
+ body: JSON.stringify({
160
+ base_tree: baseSha,
161
+ tree: files.map((f) => ({
162
+ path: f.path,
163
+ mode: "100644",
164
+ type: "blob",
165
+ content: f.content
166
+ }))
167
+ })
130
168
  });
131
169
  if (!treeRes.ok) return { ok: false, error: `Failed to create tree: ${treeRes.status}` };
132
170
  const { sha: treeSha } = await treeRes.json();
@@ -135,13 +173,17 @@ async function batchCommit(owner, repo, branch, files, message, headers) {
135
173
  headers,
136
174
  body: JSON.stringify({ tree: treeSha, parents: [headSha], message })
137
175
  });
138
- if (!newCommitRes.ok) return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` };
176
+ if (!newCommitRes.ok)
177
+ return { ok: false, error: `Failed to create commit: ${newCommitRes.status}` };
139
178
  const { sha: newSha } = await newCommitRes.json();
140
- const updateRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
141
- method: "PATCH",
142
- headers,
143
- body: JSON.stringify({ sha: newSha })
144
- });
179
+ const updateRes = await fetch(
180
+ `https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
181
+ {
182
+ method: "PATCH",
183
+ headers,
184
+ body: JSON.stringify({ sha: newSha })
185
+ }
186
+ );
145
187
  if (!updateRes.ok) return { ok: false, error: `Failed to update ref: ${updateRes.status}` };
146
188
  return { ok: true, sha: newSha };
147
189
  } catch (error) {
@@ -4,7 +4,7 @@ import {
4
4
  } from "../chunk-6UIKVKED.js";
5
5
  import {
6
6
  resolveGitHubTokenForRequest
7
- } from "../chunk-NKDATSPA.js";
7
+ } from "../chunk-DP6RTINQ.js";
8
8
 
9
9
  // src/api-routes/catalog-export.ts
10
10
  import { exportTemplate } from "@setzkasten-cms/catalog";
@@ -30,11 +30,15 @@ var POST = async ({ request, cookies }) => {
30
30
  const { sectionKey } = body;
31
31
  const sectionJsonPath = prefixPath(`${contentPath}/_sections/${sectionKey}.json`, projectPrefix);
32
32
  const contentRaw = await fetchFileContent(owner, repo, branch, sectionJsonPath, githubToken);
33
- if (!contentRaw) return Response.json({ error: `Section content not found: ${sectionKey}` }, { status: 404 });
33
+ if (!contentRaw)
34
+ return Response.json({ error: `Section content not found: ${sectionKey}` }, { status: 404 });
34
35
  const content = JSON.parse(contentRaw);
35
36
  const sectionDef = findSectionDef(fullConfig, sectionKey);
36
37
  if (!sectionDef) {
37
- return Response.json({ error: `Section definition not found for key: ${sectionKey}` }, { status: 404 });
38
+ return Response.json(
39
+ { error: `Section definition not found for key: ${sectionKey}` },
40
+ { status: 404 }
41
+ );
38
42
  }
39
43
  const templateJson = exportTemplate(sectionKey, sectionDef, content);
40
44
  return Response.json({ success: true, template: templateJson });
@@ -57,7 +61,13 @@ async function fetchFileContent(owner, repo, branch, path, token) {
57
61
  try {
58
62
  const res = await fetch(
59
63
  `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`,
60
- { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" } }
64
+ {
65
+ headers: {
66
+ Authorization: `Bearer ${token}`,
67
+ Accept: "application/vnd.github+json",
68
+ "X-GitHub-Api-Version": "2022-11-28"
69
+ }
70
+ }
61
71
  );
62
72
  if (!res.ok) return null;
63
73
  const data = await res.json();
@@ -3,9 +3,16 @@ import { APIRoute } from 'astro';
3
3
  /**
4
4
  * GET /api/setzkasten/config
5
5
  *
6
- * Returns the full SetzKastenConfig as JSON. Fields from GlobalConfig
7
- * (stored in _global_config.json) are merged over the static config so
8
- * admins can change theme and Firebase settings without a code deployment.
6
+ * Returns the SetzKastenConfig as JSON. The login screen (pre-session)
7
+ * needs to know which auth providers are enabled and Firebase project
8
+ * id, so a *minimal public subset* is exposed unauthenticated. The full
9
+ * config (storage params, products, page schema, etc.) is only served
10
+ * to authenticated users — pre-fix anyone could harvest the deployment
11
+ * blueprint.
12
+ *
13
+ * Fields from GlobalConfig (`_global_config.json`) are merged over the
14
+ * static config so admins can change theme and Firebase settings
15
+ * without a code deployment.
9
16
  */
10
17
  declare const GET: APIRoute;
11
18
 
@@ -1,21 +1,43 @@
1
1
  import {
2
2
  readGlobalConfig
3
- } from "../chunk-AM4DZXXM.js";
4
- import "../chunk-INIWFKQ3.js";
3
+ } from "../chunk-UJAFZEX2.js";
4
+ import {
5
+ parseSession
6
+ } from "../chunk-Q5HV47DW.js";
7
+ import "../chunk-QVCW6EF3.js";
8
+ import "../chunk-KENFINT4.js";
5
9
  import "../chunk-6UIKVKED.js";
10
+ import "../chunk-ONP6BRZO.js";
6
11
  import "../chunk-5PIMDP4N.js";
7
12
  import "../chunk-45ARVNT3.js";
8
- import "../chunk-NKDATSPA.js";
13
+ import "../chunk-DP6RTINQ.js";
9
14
  import "../chunk-TJNJKPUL.js";
10
15
  import "../chunk-KH22FJO5.js";
11
16
 
12
17
  // src/api-routes/config.ts
13
- var GET = async () => {
18
+ var GET = async ({ cookies }) => {
14
19
  const config = globalThis.__SETZKASTEN_FULL_CONFIG__ ?? {};
15
20
  const ssrConfig = globalThis.__SETZKASTEN_CONFIG__;
16
21
  const globalCfg = await readGlobalConfig().catch(() => null);
17
22
  const staticTheme = config.theme ?? {};
18
23
  const globalTheme = globalCfg?.theme ?? {};
24
+ const session = parseSession(cookies?.get("setzkasten_session")?.value);
25
+ if (!session) {
26
+ return new Response(
27
+ JSON.stringify({
28
+ auth: config.auth ?? { providers: ["github"] },
29
+ theme: { ...staticTheme, ...globalTheme },
30
+ adminPath: ssrConfig?.adminPath ?? "/admin",
31
+ _firebaseConfig: globalCfg?.firebaseConfig ?? null,
32
+ _hasGitHub: ssrConfig?.hasGitHub ?? false,
33
+ _hasGoogle: ssrConfig?.hasGoogle ?? false
34
+ }),
35
+ {
36
+ status: 200,
37
+ headers: { "Content-Type": "application/json" }
38
+ }
39
+ );
40
+ }
19
41
  const result = {
20
42
  // Default fallback when no config is injected at build time. Real
21
43
  // values are spread from `config` below.