@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.
- package/dist/api-routes/_auth-guard.d.ts +47 -0
- package/dist/api-routes/_auth-guard.js +18 -0
- package/dist/api-routes/_commit-trailers.d.ts +8 -0
- package/dist/api-routes/_commit-trailers.js +8 -0
- package/dist/api-routes/_feature-gate.d.ts +23 -0
- package/dist/api-routes/_feature-gate.js +7 -0
- package/dist/api-routes/_github-cache.d.ts +4 -0
- package/dist/api-routes/_github-cache.js +8 -0
- package/dist/api-routes/_github-token.d.ts +27 -0
- package/dist/api-routes/_github-token.js +8 -0
- package/dist/api-routes/_license-tier.d.ts +22 -0
- package/dist/api-routes/_license-tier.js +6 -0
- package/dist/api-routes/_pages-meta-store.d.ts +32 -0
- package/dist/api-routes/_pages-meta-store.js +9 -0
- package/dist/api-routes/_role-resolver.d.ts +15 -0
- package/dist/api-routes/_role-resolver.js +13 -0
- package/dist/api-routes/_session-cookie.d.ts +18 -0
- package/dist/api-routes/_session-cookie.js +6 -0
- package/dist/api-routes/_storage-config.d.ts +60 -0
- package/dist/api-routes/_storage-config.js +10 -0
- package/dist/api-routes/_vercel-origin.d.ts +16 -0
- package/dist/api-routes/_vercel-origin.js +6 -0
- package/dist/api-routes/_webhook-dispatcher.d.ts +13 -0
- package/dist/api-routes/_webhook-dispatcher.js +97 -0
- package/dist/api-routes/_webhook-signing.d.ts +11 -0
- package/dist/api-routes/_webhook-signing.js +6 -0
- package/dist/api-routes/_webhook-status-store.d.ts +19 -0
- package/dist/api-routes/_webhook-status-store.js +10 -0
- package/dist/api-routes/_website-resolver.d.ts +49 -0
- package/dist/api-routes/_website-resolver.js +14 -0
- package/dist/api-routes/_websites-store.d.ts +30 -0
- package/dist/api-routes/_websites-store.js +11 -0
- package/dist/api-routes/asset-proxy.d.ts +12 -0
- package/dist/api-routes/asset-proxy.js +67 -0
- package/dist/api-routes/auth-callback.d.ts +9 -0
- package/dist/api-routes/auth-callback.js +68 -0
- package/dist/api-routes/auth-login.d.ts +11 -0
- package/dist/api-routes/auth-login.js +27 -0
- package/dist/api-routes/auth-logout.d.ts +10 -0
- package/dist/api-routes/auth-logout.js +13 -0
- package/dist/api-routes/auth-session.d.ts +9 -0
- package/dist/api-routes/auth-session.js +31 -0
- package/dist/api-routes/auth-setzkasten-login.d.ts +18 -0
- package/dist/api-routes/auth-setzkasten-login.js +74 -0
- package/dist/api-routes/catalog-add.d.ts +14 -0
- package/dist/api-routes/catalog-add.js +153 -0
- package/dist/api-routes/catalog-export.d.ts +13 -0
- package/dist/api-routes/catalog-export.js +71 -0
- package/dist/api-routes/catalog-helpers.d.ts +41 -0
- package/dist/api-routes/catalog-helpers.js +11 -0
- package/dist/api-routes/catalog-list.d.ts +11 -0
- package/dist/api-routes/catalog-list.js +12 -0
- package/dist/api-routes/config.d.ts +12 -0
- package/dist/api-routes/config.js +43 -0
- package/dist/api-routes/deploy-hook.d.ts +14 -0
- package/dist/api-routes/deploy-hook.js +52 -0
- package/dist/api-routes/editors.d.ts +29 -0
- package/dist/api-routes/editors.js +18 -0
- package/dist/api-routes/github-proxy.d.ts +12 -0
- package/dist/api-routes/github-proxy.js +82 -0
- package/dist/api-routes/global-config.d.ts +20 -0
- package/dist/api-routes/global-config.js +19 -0
- package/dist/api-routes/history-rollback.d.ts +22 -0
- package/dist/api-routes/history-rollback.js +111 -0
- package/dist/api-routes/history-version.d.ts +11 -0
- package/dist/api-routes/history-version.js +57 -0
- package/dist/api-routes/history.d.ts +13 -0
- package/dist/api-routes/history.js +85 -0
- package/dist/api-routes/icons-local.d.ts +28 -0
- package/dist/api-routes/icons-local.js +115 -0
- package/dist/api-routes/init-add-section.d.ts +23 -0
- package/dist/api-routes/init-add-section.js +396 -0
- package/dist/api-routes/init-apply.d.ts +11 -0
- package/dist/api-routes/init-apply.js +266 -0
- package/dist/api-routes/init-migrate.d.ts +16 -0
- package/dist/api-routes/init-migrate.js +205 -0
- package/dist/api-routes/init-scan-page.d.ts +39 -0
- package/dist/api-routes/init-scan-page.js +260 -0
- package/dist/api-routes/init-scan.d.ts +11 -0
- package/dist/api-routes/init-scan.js +128 -0
- package/dist/api-routes/migrate-to-multi.d.ts +26 -0
- package/dist/api-routes/migrate-to-multi.js +188 -0
- package/dist/api-routes/pages.d.ts +39 -0
- package/dist/api-routes/pages.js +88 -0
- package/dist/api-routes/section-add.d.ts +18 -0
- package/dist/api-routes/section-add.js +173 -0
- package/dist/api-routes/section-commit-pending.d.ts +18 -0
- package/dist/api-routes/section-commit-pending.js +207 -0
- package/dist/api-routes/section-delete.d.ts +15 -0
- package/dist/api-routes/section-delete.js +149 -0
- package/dist/api-routes/section-duplicate.d.ts +15 -0
- package/dist/api-routes/section-duplicate.js +143 -0
- package/dist/api-routes/section-management.d.ts +41 -0
- package/dist/api-routes/section-management.js +14 -0
- package/dist/api-routes/section-prepare-copy.d.ts +25 -0
- package/dist/api-routes/section-prepare-copy.js +69 -0
- package/dist/api-routes/section-prepare.d.ts +18 -0
- package/dist/api-routes/section-prepare.js +104 -0
- package/dist/api-routes/setup-github-app-bounce.d.ts +13 -0
- package/dist/api-routes/setup-github-app-bounce.js +45 -0
- package/dist/api-routes/setup-github-app-branches.d.ts +14 -0
- package/dist/api-routes/setup-github-app-branches.js +58 -0
- package/dist/api-routes/setup-github-app-callback.d.ts +15 -0
- package/dist/api-routes/setup-github-app-callback.js +45 -0
- package/dist/api-routes/setup-github-app-installed.d.ts +15 -0
- package/dist/api-routes/setup-github-app-installed.js +33 -0
- package/dist/api-routes/setup-github-app-repos.d.ts +17 -0
- package/dist/api-routes/setup-github-app-repos.js +41 -0
- package/dist/api-routes/setup-github-app.d.ts +15 -0
- package/dist/api-routes/setup-github-app.js +41 -0
- package/dist/api-routes/updater-check.d.ts +10 -0
- package/dist/api-routes/updater-check.js +37 -0
- package/dist/api-routes/updater-register.d.ts +14 -0
- package/dist/api-routes/updater-register.js +71 -0
- package/dist/api-routes/updater-transfer.d.ts +11 -0
- package/dist/api-routes/updater-transfer.js +37 -0
- package/dist/api-routes/updater-unbind.d.ts +17 -0
- package/dist/api-routes/updater-unbind.js +35 -0
- package/dist/api-routes/webhooks-status.d.ts +12 -0
- package/dist/api-routes/webhooks-status.js +22 -0
- package/dist/api-routes/webhooks-test.d.ts +13 -0
- package/dist/api-routes/webhooks-test.js +124 -0
- package/dist/api-routes/webhooks.d.ts +6 -0
- package/dist/api-routes/webhooks.js +148 -0
- package/dist/api-routes/websites-add.d.ts +15 -0
- package/dist/api-routes/websites-add.js +92 -0
- package/dist/api-routes/websites-list.d.ts +12 -0
- package/dist/api-routes/websites-list.js +35 -0
- package/dist/api-routes/websites-remove.d.ts +15 -0
- package/dist/api-routes/websites-remove.js +69 -0
- package/dist/chunk-35S35OIV.js +80 -0
- package/dist/chunk-45ARVNT3.js +25 -0
- package/dist/chunk-5PIMDP4N.js +25 -0
- package/dist/chunk-5ZFTG4BW.js +10 -0
- package/dist/chunk-6UIKVKED.js +51 -0
- package/dist/chunk-737TIZRU.js +9 -0
- package/dist/chunk-AM4DZXXM.js +120 -0
- package/dist/chunk-FXNOTESI.js +87 -0
- package/dist/chunk-GHNK2GFE.js +48 -0
- package/dist/chunk-GRG3LNKH.js +37 -0
- package/dist/chunk-INIWFKQ3.js +236 -0
- package/dist/chunk-JHY6XTLL.js +24 -0
- package/dist/chunk-K22A4ZBS.js +1574 -0
- package/dist/chunk-KH22FJO5.js +17 -0
- package/dist/chunk-NKDATSPA.js +43 -0
- package/dist/chunk-RHJONMLK.js +1267 -0
- package/dist/chunk-TJNJKPUL.js +11 -0
- package/dist/chunk-V6IMPVF3.js +120 -0
- package/dist/chunk-W3QHY5GW.js +19 -0
- package/dist/chunk-ZQDGGWJP.js +43 -0
- package/package.json +249 -53
- package/src/api-routes/__tests__/route-registry.test.ts +7 -1
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { AuthSession, SetzKastenConfig } from '@setzkasten-cms/core';
|
|
2
|
+
|
|
3
|
+
declare function parseSession(raw: string | undefined): AuthSession | null;
|
|
4
|
+
/**
|
|
5
|
+
* Returns 401 if the request has no valid session, 403 if the user is not
|
|
6
|
+
* an admin, or null (= allowed) otherwise. Used by every admin-only API
|
|
7
|
+
* endpoint so the role check is mechanically applied — never just
|
|
8
|
+
* `if (!session) return 401`, which lets editors hit admin-only routes.
|
|
9
|
+
*/
|
|
10
|
+
declare function requireAdmin(rawSession: string | undefined): Response | null;
|
|
11
|
+
/**
|
|
12
|
+
* Returns a 403/503 Response if the session user may NOT edit the given page,
|
|
13
|
+
* or null (= allowed) otherwise.
|
|
14
|
+
*
|
|
15
|
+
* Authorization is two-layered:
|
|
16
|
+
*
|
|
17
|
+
* 1. Global editors (_editors.json in the config-repo) decide which pages
|
|
18
|
+
* an editor account can touch at all. Admins always pass.
|
|
19
|
+
*
|
|
20
|
+
* 2. Per-website allowedEmails (in WebsiteEntry, multi-mode only) decide
|
|
21
|
+
* which editors may operate on the website the request is targeting.
|
|
22
|
+
* Without this layer, an editor could swap the X-SK-Website header
|
|
23
|
+
* to any registered website id and inherit edit access transitively
|
|
24
|
+
* from layer 1. Single-mode requests skip this layer because the
|
|
25
|
+
* resolver returns no Result.ok for them.
|
|
26
|
+
*
|
|
27
|
+
* Fail-modes for layer 1:
|
|
28
|
+
* - File absent (genuine 404) → undefined → canEditPage allows
|
|
29
|
+
* (semantic: "no restrictions configured")
|
|
30
|
+
* - File present and parsed → list → canEditPage applies it
|
|
31
|
+
* - Fetch failed / parse failed → 503, deny access (was a silent grant
|
|
32
|
+
* before review finding #1).
|
|
33
|
+
*/
|
|
34
|
+
declare function guardPageAccess(session: AuthSession | null, pageKey: string, fullConfig: SetzKastenConfig | undefined, request?: Request): Promise<Response | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Per-website authorization. Editors must be listed on
|
|
37
|
+
* `WebsiteEntry.allowedEmails` to operate on the website that the
|
|
38
|
+
* X-SK-Website header resolves to. Admins always pass; single-mode
|
|
39
|
+
* skips this check entirely (no website context).
|
|
40
|
+
*
|
|
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.
|
|
44
|
+
*/
|
|
45
|
+
declare function guardWebsiteAccess(session: AuthSession, request: Request): Promise<Response | null>;
|
|
46
|
+
|
|
47
|
+
export { guardPageAccess, guardWebsiteAccess, parseSession, requireAdmin };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
guardPageAccess,
|
|
3
|
+
guardWebsiteAccess,
|
|
4
|
+
parseSession,
|
|
5
|
+
requireAdmin
|
|
6
|
+
} from "../chunk-INIWFKQ3.js";
|
|
7
|
+
import "../chunk-6UIKVKED.js";
|
|
8
|
+
import "../chunk-5PIMDP4N.js";
|
|
9
|
+
import "../chunk-45ARVNT3.js";
|
|
10
|
+
import "../chunk-NKDATSPA.js";
|
|
11
|
+
import "../chunk-TJNJKPUL.js";
|
|
12
|
+
import "../chunk-KH22FJO5.js";
|
|
13
|
+
export {
|
|
14
|
+
guardPageAccess,
|
|
15
|
+
guardWebsiteAccess,
|
|
16
|
+
parseSession,
|
|
17
|
+
requireAdmin
|
|
18
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare const SETZKASTEN_CO_AUTHOR = "Co-authored-by: Setzkasten <setzkasten@setzkasten.dev>";
|
|
2
|
+
/**
|
|
3
|
+
* Appends git commit trailers to a commit message.
|
|
4
|
+
* Always includes the Setzkasten co-author; optionally includes the editor.
|
|
5
|
+
*/
|
|
6
|
+
declare function withTrailers(message: string, editorEmail?: string | null): string;
|
|
7
|
+
|
|
8
|
+
export { SETZKASTEN_CO_AUTHOR, withTrailers };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft-fail feature-gate guard for API routes. Returns `null` when the
|
|
3
|
+
* feature is available at the current license tier; otherwise returns a
|
|
4
|
+
* 403 Response with a machine-readable JSON body.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
*
|
|
8
|
+
* const gate = gateFeature('editors')
|
|
9
|
+
* if (gate) return gate
|
|
10
|
+
* // ... normal route logic
|
|
11
|
+
*
|
|
12
|
+
* The body shape:
|
|
13
|
+
*
|
|
14
|
+
* { error: string, code: 'feature-locked', feature: string, requiredTier: string }
|
|
15
|
+
*
|
|
16
|
+
* Routes use 403 (not 402) because UI clients robustly handle 403 Forbidden
|
|
17
|
+
* but often surface 402 Payment Required as a generic error. The `code`
|
|
18
|
+
* field lets the UI hook (`useFeatureGate`) distinguish locked features
|
|
19
|
+
* from real authorization failures.
|
|
20
|
+
*/
|
|
21
|
+
declare function gateFeature(feature: string): Response | null;
|
|
22
|
+
|
|
23
|
+
export { gateFeature };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Result } from '@setzkasten-cms/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token for the **config repo** (in multi mode) or the **website repo**
|
|
5
|
+
* (in single mode). Reads the App credentials straight from ENV — no
|
|
6
|
+
* X-SK-Website routing involved. Used by config-management endpoints
|
|
7
|
+
* (`/websites/add`, `/websites/remove`) and any read of `_global_config.json`
|
|
8
|
+
* / `_editors.json` that lives next to the registry, not next to the
|
|
9
|
+
* editable content.
|
|
10
|
+
*
|
|
11
|
+
* In single mode this is the only relevant token; in multi mode it is
|
|
12
|
+
* the token for the config repo, while per-website operations use
|
|
13
|
+
* {@link resolveGitHubTokenForRequest}.
|
|
14
|
+
*/
|
|
15
|
+
declare function resolveConfigRepoToken(): Promise<Result<string>>;
|
|
16
|
+
/**
|
|
17
|
+
* Token for the website resolved from the request's `X-SK-Website` header.
|
|
18
|
+
* In multi mode this picks the installation id of the active website;
|
|
19
|
+
* in single mode the resolver synthesises one website from build-time
|
|
20
|
+
* storage and returns the same token as {@link resolveConfigRepoToken}.
|
|
21
|
+
*
|
|
22
|
+
* The PEM private key always comes from `GITHUB_APP_PRIVATE_KEY` — one
|
|
23
|
+
* App, many installations.
|
|
24
|
+
*/
|
|
25
|
+
declare function resolveGitHubTokenForRequest(request: Request): Promise<Result<string>>;
|
|
26
|
+
|
|
27
|
+
export { resolveConfigRepoToken, resolveGitHubTokenForRequest };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FeatureTier } from '@setzkasten-cms/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-side license-tier lookup based on the configured license key.
|
|
5
|
+
*
|
|
6
|
+
* The prefix is the canonical signal:
|
|
7
|
+
* SK-PRO-... → 'pro'
|
|
8
|
+
* SK-ENT-... → 'enterprise'
|
|
9
|
+
* anything else (or unset) → 'free'
|
|
10
|
+
*
|
|
11
|
+
* The updater backend does the real validation (revocation, expiry); this
|
|
12
|
+
* function is enough to enforce honest-user limits at the API boundary.
|
|
13
|
+
* A bad actor faking a prefix will get 200 here but be flagged on the next
|
|
14
|
+
* dashboard register call — and the limit gate is a deterrent, not a
|
|
15
|
+
* payment system.
|
|
16
|
+
*
|
|
17
|
+
* Reads `SETZKASTEN_LICENSE_KEY` from the env first; future versions may
|
|
18
|
+
* add a config-repo `_global_config.json` fallback.
|
|
19
|
+
*/
|
|
20
|
+
declare function resolveLicenseTier(): FeatureTier;
|
|
21
|
+
|
|
22
|
+
export { resolveLicenseTier };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Result, PagesMeta } from '@setzkasten-cms/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-side read/write helpers for `_pages-meta.json`.
|
|
5
|
+
*
|
|
6
|
+
* - `readPagesMeta` returns the parsed registry plus its current SHA, or
|
|
7
|
+
* an empty registry with `sha: null` when the file does not exist.
|
|
8
|
+
* - `recordPageEdit` updates a single page's `lastModified` timestamp and
|
|
9
|
+
* commits back, retrying once on 409 (concurrent edit). Failures
|
|
10
|
+
* propagate as network errors so callers can decide whether to log
|
|
11
|
+
* silently or surface them.
|
|
12
|
+
*/
|
|
13
|
+
interface PagesMetaTarget {
|
|
14
|
+
readonly owner: string;
|
|
15
|
+
readonly repo: string;
|
|
16
|
+
readonly branch: string;
|
|
17
|
+
readonly contentPath: string;
|
|
18
|
+
readonly token: string;
|
|
19
|
+
}
|
|
20
|
+
interface PagesMetaSnapshot {
|
|
21
|
+
readonly meta: PagesMeta;
|
|
22
|
+
readonly sha: string | null;
|
|
23
|
+
}
|
|
24
|
+
declare function readPagesMeta(target: PagesMetaTarget): Promise<Result<PagesMetaSnapshot>>;
|
|
25
|
+
/**
|
|
26
|
+
* Records that `pageKey` was just edited. Reads the meta, sets the
|
|
27
|
+
* timestamp, writes back. On a 409 conflict (someone else committed
|
|
28
|
+
* meanwhile) the function re-reads and retries up to MAX_RETRIES times.
|
|
29
|
+
*/
|
|
30
|
+
declare function recordPageEdit(target: PagesMetaTarget, pageKey: string, timestamp?: number): Promise<Result<void>>;
|
|
31
|
+
|
|
32
|
+
export { type PagesMetaTarget, readPagesMeta, recordPageEdit };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AuthProviderKind, UserRole } from '@setzkasten-cms/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds the role-resolver callback that auth-adapters call per OAuth
|
|
5
|
+
* callback. Reads the live `_editors.json` (case-insensitive lookup) and
|
|
6
|
+
* combines it with the configured `allowedEmails` env list. Callers pass
|
|
7
|
+
* the resolver into `createGitHubAuth` / `createGoogleAuth` /
|
|
8
|
+
* `verifyFirebaseJwt` so role assignment happens once, in one place.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` when the user is not allowed at all, otherwise the
|
|
11
|
+
* effective role.
|
|
12
|
+
*/
|
|
13
|
+
declare function makeRoleResolver(provider: AuthProviderKind, allowedEmails: readonly string[] | undefined): (email: string) => Promise<UserRole | null>;
|
|
14
|
+
|
|
15
|
+
export { makeRoleResolver };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
makeRoleResolver
|
|
3
|
+
} from "../chunk-ZQDGGWJP.js";
|
|
4
|
+
import "../chunk-INIWFKQ3.js";
|
|
5
|
+
import "../chunk-6UIKVKED.js";
|
|
6
|
+
import "../chunk-5PIMDP4N.js";
|
|
7
|
+
import "../chunk-45ARVNT3.js";
|
|
8
|
+
import "../chunk-NKDATSPA.js";
|
|
9
|
+
import "../chunk-TJNJKPUL.js";
|
|
10
|
+
import "../chunk-KH22FJO5.js";
|
|
11
|
+
export {
|
|
12
|
+
makeRoleResolver
|
|
13
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised builder for the session-cookie attributes. Two routes set the
|
|
3
|
+
* cookie today (auth-callback for GitHub OAuth, auth-setzkasten-login for
|
|
4
|
+
* Firebase) — both must share the exact same domain/secure/path so the
|
|
5
|
+
* cookie can be read across the admin and (in standalone setups) the
|
|
6
|
+
* managed website on a sibling subdomain.
|
|
7
|
+
*/
|
|
8
|
+
interface SessionCookieOptions {
|
|
9
|
+
readonly httpOnly: true;
|
|
10
|
+
readonly secure: boolean;
|
|
11
|
+
readonly sameSite: 'lax';
|
|
12
|
+
readonly path: '/';
|
|
13
|
+
readonly maxAge: number;
|
|
14
|
+
readonly domain?: string;
|
|
15
|
+
}
|
|
16
|
+
declare function sessionCookieOptions(secure: boolean): SessionCookieOptions;
|
|
17
|
+
|
|
18
|
+
export { type SessionCookieOptions, sessionCookieOptions };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage resolution helpers for admin API routes.
|
|
3
|
+
*
|
|
4
|
+
* Two entry points:
|
|
5
|
+
*
|
|
6
|
+
* - {@link resolveStorageConfigForRequest} (default for content routes) —
|
|
7
|
+
* resolves the **active website** for a request. In single mode this
|
|
8
|
+
* is always the integration's website; in multi mode it is the website
|
|
9
|
+
* selected by the X-SK-Website header.
|
|
10
|
+
*
|
|
11
|
+
* - {@link resolveStorageConfig} (build-time only, used by auth-guard
|
|
12
|
+
* for the editors file) — returns the integration's build-time storage
|
|
13
|
+
* target. In single mode this is the website repo; in multi mode it
|
|
14
|
+
* is the config repo (where `_editors.json` lives next to
|
|
15
|
+
* `websites.json`).
|
|
16
|
+
*
|
|
17
|
+
* The lookup chain inside `resolveStorageConfig`:
|
|
18
|
+
* 1. Request body override (explicit `{ owner, repo, branch }` payload)
|
|
19
|
+
* 2. `__SETZKASTEN_STORAGE__` Vite define (embedded at build time)
|
|
20
|
+
* 3. `globalThis.__SETZKASTEN_CONFIG__` (injectScript, SSR-only fallback)
|
|
21
|
+
*/
|
|
22
|
+
interface StorageConfig {
|
|
23
|
+
owner: string;
|
|
24
|
+
repo: string;
|
|
25
|
+
branch: string;
|
|
26
|
+
/** Monorepo prefix to prepend to file paths, e.g. 'apps/website' */
|
|
27
|
+
projectPrefix: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns the integration's build-time storage target. Used by the
|
|
31
|
+
* auth-guard to read the editors file (which lives next to the build-
|
|
32
|
+
* time storage in both single and multi mode) and by tests that want
|
|
33
|
+
* to bypass the per-request resolver. Most content routes should use
|
|
34
|
+
* {@link resolveStorageConfigForRequest} instead.
|
|
35
|
+
*/
|
|
36
|
+
declare function resolveStorageConfig(body?: {
|
|
37
|
+
owner?: string;
|
|
38
|
+
repo?: string;
|
|
39
|
+
branch?: string;
|
|
40
|
+
}): StorageConfig | null;
|
|
41
|
+
/**
|
|
42
|
+
* Prefix a file path with the monorepo project prefix.
|
|
43
|
+
* e.g. prefixPath('src/pages/index.astro', 'apps/website') → 'apps/website/src/pages/index.astro'
|
|
44
|
+
*/
|
|
45
|
+
declare function prefixPath(filePath: string, projectPrefix: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Standalone-aware variant: resolves storage from the active website
|
|
48
|
+
* (X-SK-Website header → WebsitesRegistry) before falling back to the
|
|
49
|
+
* single-repo build-time configuration.
|
|
50
|
+
*
|
|
51
|
+
* Body overrides still win over both — useful for routes that take an
|
|
52
|
+
* explicit `{ owner, repo, branch }` payload.
|
|
53
|
+
*/
|
|
54
|
+
declare function resolveStorageConfigForRequest(request: Request, body?: {
|
|
55
|
+
owner?: string;
|
|
56
|
+
repo?: string;
|
|
57
|
+
branch?: string;
|
|
58
|
+
}): Promise<StorageConfig | null>;
|
|
59
|
+
|
|
60
|
+
export { type StorageConfig, prefixPath, resolveStorageConfig, resolveStorageConfigForRequest };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the real public origin on Vercel (and any reverse-proxy setup).
|
|
3
|
+
*
|
|
4
|
+
* Problem: Inside a Vercel serverless function, both `request.url` and
|
|
5
|
+
* Astro's `url` context resolve to the internal function host
|
|
6
|
+
* (e.g. "https://localhost"), not the public hostname.
|
|
7
|
+
*
|
|
8
|
+
* Solution: Read the x-forwarded-host and x-forwarded-proto headers that
|
|
9
|
+
* Vercel's edge layer sets on every inbound request.
|
|
10
|
+
*
|
|
11
|
+
* Falls back gracefully to the `host` header and `https` for local dev
|
|
12
|
+
* or non-Vercel environments.
|
|
13
|
+
*/
|
|
14
|
+
declare function getPublicOrigin(request: Request): string;
|
|
15
|
+
|
|
16
|
+
export { getPublicOrigin };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WebhookEvent, WebhookPayload } from '@setzkasten-cms/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fire all enabled webhooks subscribed to `event`. Best-effort —
|
|
5
|
+
* each request runs with a 5s timeout; failures are recorded in the
|
|
6
|
+
* in-memory status store but do not throw or block the caller.
|
|
7
|
+
*
|
|
8
|
+
* Caller-side: invoke as `void fireWebhooks(...)` to make the
|
|
9
|
+
* fire-and-forget intent explicit.
|
|
10
|
+
*/
|
|
11
|
+
declare function fireWebhooks(event: WebhookEvent, payload: Omit<WebhookPayload, 'event' | 'timestamp'>, request: Request): Promise<void>;
|
|
12
|
+
|
|
13
|
+
export { fireWebhooks };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
signPayload
|
|
3
|
+
} from "../chunk-737TIZRU.js";
|
|
4
|
+
import {
|
|
5
|
+
recordWebhookFire
|
|
6
|
+
} from "../chunk-W3QHY5GW.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveStorageConfigForRequest
|
|
9
|
+
} from "../chunk-6UIKVKED.js";
|
|
10
|
+
import {
|
|
11
|
+
cachedFetch
|
|
12
|
+
} from "../chunk-45ARVNT3.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveGitHubTokenForRequest
|
|
15
|
+
} from "../chunk-NKDATSPA.js";
|
|
16
|
+
|
|
17
|
+
// src/api-routes/_webhook-dispatcher.ts
|
|
18
|
+
import {
|
|
19
|
+
parseWebhooksFile,
|
|
20
|
+
selectWebhooksForEvent
|
|
21
|
+
} from "@setzkasten-cms/core";
|
|
22
|
+
var WEBHOOKS_FILE = (contentPath) => `${contentPath}/_webhooks.json`;
|
|
23
|
+
var DISPATCH_TIMEOUT_MS = 5e3;
|
|
24
|
+
async function fireWebhooks(event, payload, request) {
|
|
25
|
+
try {
|
|
26
|
+
const webhooks = await loadWebhooksForRequest(request);
|
|
27
|
+
if (!webhooks || webhooks.length === 0) return;
|
|
28
|
+
const targets = selectWebhooksForEvent(webhooks, event);
|
|
29
|
+
if (targets.length === 0) return;
|
|
30
|
+
const fullPayload = {
|
|
31
|
+
event,
|
|
32
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
33
|
+
...payload
|
|
34
|
+
};
|
|
35
|
+
const body = JSON.stringify(fullPayload);
|
|
36
|
+
await Promise.all(targets.map((w) => fireOne(w, event, body)));
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error("[setzkasten] webhook dispatch failed:", err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function fireOne(webhook, event, body) {
|
|
42
|
+
const headers = {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"X-Setzkasten-Event": event,
|
|
45
|
+
"X-Setzkasten-Delivery": crypto.randomUUID()
|
|
46
|
+
};
|
|
47
|
+
if (webhook.secret) {
|
|
48
|
+
headers["X-Setzkasten-Signature"] = `sha256=${signPayload(body, webhook.secret)}`;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(webhook.url, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers,
|
|
54
|
+
body,
|
|
55
|
+
signal: AbortSignal.timeout(DISPATCH_TIMEOUT_MS)
|
|
56
|
+
});
|
|
57
|
+
recordWebhookFire(webhook.id, res.status);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
recordWebhookFire(webhook.id, "error");
|
|
60
|
+
console.warn(`[setzkasten] webhook "${webhook.id}" failed:`, err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function loadWebhooksForRequest(request) {
|
|
64
|
+
const tokenResult = await resolveGitHubTokenForRequest(request);
|
|
65
|
+
if (!tokenResult.ok) return null;
|
|
66
|
+
const storage = await resolveStorageConfigForRequest(request);
|
|
67
|
+
if (!storage) return null;
|
|
68
|
+
const serverConfig = globalThis.__SETZKASTEN_CONFIG__;
|
|
69
|
+
const contentPath = serverConfig?.storage?.contentPath ?? "content";
|
|
70
|
+
const { owner, repo, branch } = storage;
|
|
71
|
+
const cacheKey = `webhooks:${owner}/${repo}:${branch}`;
|
|
72
|
+
return cachedFetch(cacheKey, 6e4, async () => {
|
|
73
|
+
const res = await fetch(
|
|
74
|
+
`https://api.github.com/repos/${owner}/${repo}/contents/${WEBHOOKS_FILE(contentPath)}?ref=${branch}`,
|
|
75
|
+
{
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${tokenResult.value}`,
|
|
78
|
+
Accept: "application/vnd.github+json",
|
|
79
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
if (res.status === 404) return [];
|
|
84
|
+
if (!res.ok) return null;
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
const raw = data.encoding === "base64" ? Buffer.from(data.content, "base64").toString("utf-8") : data.content;
|
|
87
|
+
const parsed = parseWebhooksFile(raw);
|
|
88
|
+
if (!parsed.ok) {
|
|
89
|
+
console.warn("[setzkasten] _webhooks.json parse error:", parsed.error.message);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return parsed.value.webhooks;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
fireWebhooks
|
|
97
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMAC-SHA256 signature of the webhook payload body, hex-encoded.
|
|
3
|
+
* Receivers verify with the same secret and the **raw** request body —
|
|
4
|
+
* pattern is identical to GitHub webhooks.
|
|
5
|
+
*
|
|
6
|
+
* Lives in astro-admin (not core) because it imports node:crypto.
|
|
7
|
+
* core's "zero external deps" rule keeps it edge/browser-runnable.
|
|
8
|
+
*/
|
|
9
|
+
declare function signPayload(body: string, secret: string): string;
|
|
10
|
+
|
|
11
|
+
export { signPayload };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory webhook status — last fired timestamp + last HTTP status per
|
|
3
|
+
* webhook id. Survives only the server process; cold-start losses are
|
|
4
|
+
* acceptable because this is a status display, not the source of truth.
|
|
5
|
+
*
|
|
6
|
+
* Persisting to `_webhooks.json` would create a commit-storm
|
|
7
|
+
* (every save → webhook fire → webhook commit → trigger save → …). The
|
|
8
|
+
* UI just refetches `/webhooks/status` after a test-fire or save.
|
|
9
|
+
*/
|
|
10
|
+
interface WebhookStatusEntry {
|
|
11
|
+
readonly lastFiredAt: string;
|
|
12
|
+
readonly lastStatus: number | 'error';
|
|
13
|
+
}
|
|
14
|
+
declare function recordWebhookFire(id: string, status: number | 'error'): void;
|
|
15
|
+
declare function getWebhookStatus(): Record<string, WebhookStatusEntry>;
|
|
16
|
+
/** Test-only — clears the in-memory map. */
|
|
17
|
+
declare function _resetWebhookStatusForTests(): void;
|
|
18
|
+
|
|
19
|
+
export { type WebhookStatusEntry, _resetWebhookStatusForTests, getWebhookStatus, recordWebhookFire };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { WebsitesRegistryProvider, WebsiteEntry, Result } from '@setzkasten-cms/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-request resolver for the active website.
|
|
5
|
+
*
|
|
6
|
+
* Standalone mode: looks up the entry matching the X-SK-Website request
|
|
7
|
+
* header in the websites registry (config-repo).
|
|
8
|
+
*
|
|
9
|
+
* Single-repo mode (backward compat): returns a synthesized WebsiteEntry
|
|
10
|
+
* built from the integration's build-time storage + GitHub-App ENV vars.
|
|
11
|
+
* The header is ignored.
|
|
12
|
+
*/
|
|
13
|
+
interface MultiState {
|
|
14
|
+
readonly mode: 'multi';
|
|
15
|
+
readonly registry: WebsitesRegistryProvider;
|
|
16
|
+
}
|
|
17
|
+
interface SingleRepoState {
|
|
18
|
+
readonly mode: 'single';
|
|
19
|
+
readonly synthesized: WebsiteEntry;
|
|
20
|
+
}
|
|
21
|
+
type ResolverState = MultiState | SingleRepoState | null;
|
|
22
|
+
/** Test/admin hook: install or clear the resolver state. */
|
|
23
|
+
declare function __resetWebsiteResolverForTests(next: ResolverState): void;
|
|
24
|
+
/**
|
|
25
|
+
* Configure the resolver state explicitly. Currently only the test suite
|
|
26
|
+
* uses this — production wiring runs lazily through
|
|
27
|
+
* {@link bootstrapResolverFromGlobals} on the first request, since the
|
|
28
|
+
* Astro integration doesn't have an obvious "initialize once" hook
|
|
29
|
+
* (`astro:server:start` runs in dev only). The export stays available so
|
|
30
|
+
* a future integration-startup wire-up has a typed entry point.
|
|
31
|
+
*/
|
|
32
|
+
declare function configureWebsiteResolver(next: ResolverState): void;
|
|
33
|
+
/**
|
|
34
|
+
* Lazy bootstrap from build-time globals. Reads `__SETZKASTEN_FULL_CONFIG__`
|
|
35
|
+
* and `__SETZKASTEN_STORAGE__` (set by the Astro integration via Vite
|
|
36
|
+
* define — literals only, NOT globalThis) to derive single-repo or
|
|
37
|
+
* standalone state. Idempotent — does nothing if the resolver is already
|
|
38
|
+
* configured.
|
|
39
|
+
*/
|
|
40
|
+
declare function bootstrapResolverFromGlobals(): void;
|
|
41
|
+
declare function resolveCurrentWebsite(request: Request): Promise<Result<WebsiteEntry>>;
|
|
42
|
+
/**
|
|
43
|
+
* Lists all websites known to the resolver.
|
|
44
|
+
* Standalone mode: defers to the registry. Single-repo mode: returns the
|
|
45
|
+
* synthesized entry as a one-element list.
|
|
46
|
+
*/
|
|
47
|
+
declare function listAllWebsites(): Promise<Result<readonly WebsiteEntry[]>>;
|
|
48
|
+
|
|
49
|
+
export { __resetWebsiteResolverForTests, bootstrapResolverFromGlobals, configureWebsiteResolver, listAllWebsites, resolveCurrentWebsite };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__resetWebsiteResolverForTests,
|
|
3
|
+
bootstrapResolverFromGlobals,
|
|
4
|
+
configureWebsiteResolver,
|
|
5
|
+
listAllWebsites,
|
|
6
|
+
resolveCurrentWebsite
|
|
7
|
+
} from "../chunk-V6IMPVF3.js";
|
|
8
|
+
export {
|
|
9
|
+
__resetWebsiteResolverForTests,
|
|
10
|
+
bootstrapResolverFromGlobals,
|
|
11
|
+
configureWebsiteResolver,
|
|
12
|
+
listAllWebsites,
|
|
13
|
+
resolveCurrentWebsite
|
|
14
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Result, WebsitesRegistry } from '@setzkasten-cms/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read/write the standalone admin's `websites.json` from the config repo.
|
|
5
|
+
* Pure HTTP — no caching here; the GitHub-side cache lives in
|
|
6
|
+
* GitHubWebsitesRegistry, which API routes invalidate explicitly after a
|
|
7
|
+
* successful write.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface ConfigRepoTarget {
|
|
11
|
+
readonly owner: string;
|
|
12
|
+
readonly repo: string;
|
|
13
|
+
readonly branch: string;
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly token: string;
|
|
16
|
+
}
|
|
17
|
+
interface ReadResult {
|
|
18
|
+
readonly registry: WebsitesRegistry;
|
|
19
|
+
readonly sha: string | null;
|
|
20
|
+
}
|
|
21
|
+
declare function readWebsitesRegistryFromGitHub(target: ConfigRepoTarget): Promise<Result<ReadResult>>;
|
|
22
|
+
declare function writeWebsitesRegistryToGitHub(target: ConfigRepoTarget, registry: WebsitesRegistry, previousSha: string | null, commitMessage: string): Promise<Result<void>>;
|
|
23
|
+
/**
|
|
24
|
+
* Reads the standalone-admin storage config out of the build-time
|
|
25
|
+
* `__SETZKASTEN_FULL_CONFIG__` global. Returns null when the deployment
|
|
26
|
+
* is not in standalone mode (single-repo setups have no websites.json).
|
|
27
|
+
*/
|
|
28
|
+
declare function resolveConfigRepoTargetFromGlobals(token: string): ConfigRepoTarget | null;
|
|
29
|
+
|
|
30
|
+
export { readWebsitesRegistryFromGitHub, resolveConfigRepoTargetFromGlobals, writeWebsitesRegistryToGitHub };
|