@tideworthy/tidysites-platform 0.2.0 → 0.3.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.
- package/CHANGELOG.md +8 -0
- package/README.md +4 -0
- package/package.json +2 -1
- package/src/index.ts +7 -2
- package/src/lib/frame.ts +26 -0
- package/src/lib/runtime.ts +8 -1
- package/src/middleware.ts +20 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- `tidyPlatform()` registers a middleware: with a `TIDY_FRAME_ANCESTORS` variable on the site (space-separated https origins), responses carry `Content-Security-Policy: frame-ancestors 'self' <origins>` in place of EmDash's `X-Frame-Options: SAMEORIGIN`, so the platform can embed the staging preview. Inert without the variable. New export `@tideworthy/tidysites-platform/middleware`.
|
|
6
|
+
|
|
7
|
+
## 0.2.1
|
|
8
|
+
|
|
9
|
+
- Fix: `POST /_tidy/fill` failed every upsert with "internal error" because the content-only seed document carried a numeric version; the seed format's version is the string "1". The document is now validated before it is applied, so a shape problem answers 400 with the validator's paths. 0.2.0's fill route is unusable for writes; removals worked.
|
|
10
|
+
|
|
3
11
|
## 0.2.0
|
|
4
12
|
|
|
5
13
|
- New route `POST /_tidy/fill`: real content over the template's sample content. Settings, entries upserted by slug with field-level merge over the existing entry, `$ref` and `$media` resolved as in a seed, and a `remove` list for sample entries the fill did not replace. Client gains `fill()`.
|
package/README.md
CHANGED
|
@@ -39,6 +39,10 @@ The worker entry stays EmDash's own:
|
|
|
39
39
|
export { default, PluginBridge } from "@emdash-cms/cloudflare/worker";
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
## Framing the staging preview
|
|
43
|
+
|
|
44
|
+
EmDash answers every page with `X-Frame-Options: SAMEORIGIN` unless a Content-Security-Policy is present. `tidyPlatform()` registers a middleware that, when the site carries a `TIDY_FRAME_ANCESTORS` variable (space-separated https origins), replaces that header with `Content-Security-Policy: frame-ancestors 'self' <origins>` so the platform's website page can embed the site. Without the variable nothing changes.
|
|
45
|
+
|
|
42
46
|
## Bindings the routes read
|
|
43
47
|
|
|
44
48
|
| Binding | Type | Purpose |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tideworthy/tidysites-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The Tidysites platform integration for EmDash sites: bootstrap, sessions, maintenance, health, and publish hooks under /_tidy/*.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./src/index.ts",
|
|
20
20
|
"./plugin": "./src/plugin.ts",
|
|
21
|
+
"./middleware": "./src/middleware.ts",
|
|
21
22
|
"./client": "./src/client.ts",
|
|
22
23
|
"./routes/bootstrap": "./src/routes/bootstrap.ts",
|
|
23
24
|
"./routes/session": "./src/routes/session.ts",
|
package/src/index.ts
CHANGED
|
@@ -31,13 +31,18 @@ export const PLATFORM_ROUTES = [
|
|
|
31
31
|
{ pattern: "/_tidy/maintenance", entrypoint: `${PACKAGE}/routes/maintenance` }
|
|
32
32
|
] as const;
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Astro integration: registers every platform route as a server route and the
|
|
36
|
+
* framing middleware (a `frame-ancestors` policy from TIDY_FRAME_ANCESTORS so
|
|
37
|
+
* the platform can embed the staging preview; inert without the variable).
|
|
38
|
+
*/
|
|
35
39
|
export function tidyPlatform(): AstroIntegration {
|
|
36
40
|
return {
|
|
37
41
|
name: PACKAGE,
|
|
38
42
|
hooks: {
|
|
39
|
-
"astro:config:setup": ({ injectRoute }) => {
|
|
43
|
+
"astro:config:setup": ({ injectRoute, addMiddleware }) => {
|
|
40
44
|
for (const route of PLATFORM_ROUTES) injectRoute({ pattern: route.pattern, entrypoint: route.entrypoint, prerender: false });
|
|
45
|
+
addMiddleware({ entrypoint: `${PACKAGE}/middleware`, order: "pre" });
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
};
|
package/src/lib/frame.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framing policy. EmDash's middleware sets `X-Frame-Options: SAMEORIGIN` on
|
|
3
|
+
* every response that has no Content-Security-Policy, which keeps the
|
|
4
|
+
* platform's website page from embedding the staging preview. When the site
|
|
5
|
+
* carries a TIDY_FRAME_ANCESTORS variable (space-separated origins), responses
|
|
6
|
+
* get a `frame-ancestors` policy naming them instead. Nothing changes without
|
|
7
|
+
* the variable.
|
|
8
|
+
*/
|
|
9
|
+
const ORIGIN = /^https:\/\/[a-z0-9.-]+(?::\d+)?$/i;
|
|
10
|
+
|
|
11
|
+
/** The origins allowed to frame the site, from the variable's value; invalid entries are dropped. */
|
|
12
|
+
export function frameAncestors(value: unknown): string[] {
|
|
13
|
+
if (typeof value !== "string") return [];
|
|
14
|
+
return [...new Set(value.split(/[\s,]+/).map((s) => s.trim()).filter((s) => ORIGIN.test(s)))];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Apply the policy to a response's headers. Returns true when something changed. */
|
|
18
|
+
export function applyFramePolicy(headers: Headers, ancestors: string[]): boolean {
|
|
19
|
+
if (ancestors.length === 0) return false;
|
|
20
|
+
const existing = headers.get("Content-Security-Policy");
|
|
21
|
+
if (existing && /frame-ancestors/i.test(existing)) return false;
|
|
22
|
+
const directive = `frame-ancestors 'self' ${ancestors.join(" ")}`;
|
|
23
|
+
headers.set("Content-Security-Policy", existing ? `${existing.replace(/;?\s*$/, "")}; ${directive}` : directive);
|
|
24
|
+
headers.delete("X-Frame-Options");
|
|
25
|
+
return true;
|
|
26
|
+
}
|
package/src/lib/runtime.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { getDb } from "emdash/runtime";
|
|
|
11
11
|
import { loadSeed } from "emdash/seed";
|
|
12
12
|
import { VALID_SCOPES, generatePrefixedToken } from "@emdash-cms/auth";
|
|
13
13
|
import type { Deps } from "./handlers";
|
|
14
|
+
import { HttpError } from "./http";
|
|
14
15
|
import { createEmdashStore } from "./store";
|
|
15
16
|
|
|
16
17
|
export { workerEnv } from "./env";
|
|
@@ -63,7 +64,13 @@ export async function buildDeps(): Promise<Deps> {
|
|
|
63
64
|
merged[collection].push({ ...e, data: existing ? { ...current, ...e.data } : e.data });
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
// The seed format's version is the string "1"; applySeed validates the
|
|
68
|
+
// document first and a shape problem comes back as a 400 with the
|
|
69
|
+
// validator's paths instead of a bare 500.
|
|
70
|
+
const doc = { version: "1", content: merged };
|
|
71
|
+
const v = validateSeed(doc);
|
|
72
|
+
if (!v.valid) throw new HttpError(400, "invalid content", v.errors);
|
|
73
|
+
const r = await applySeed(db, doc as unknown as Parameters<typeof applySeed>[1], { includeContent: true, onConflict: "update", ...storageOpt });
|
|
67
74
|
return { created: r.content?.created ?? 0, updated: r.content?.updated ?? 0, media: r.media?.created ?? 0 };
|
|
68
75
|
}
|
|
69
76
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Astro middleware registered by tidyPlatform(): the framing policy from TIDY_FRAME_ANCESTORS. */
|
|
2
|
+
import type { MiddlewareHandler } from "astro";
|
|
3
|
+
import { workerEnv } from "./lib/env";
|
|
4
|
+
import { applyFramePolicy, frameAncestors } from "./lib/frame";
|
|
5
|
+
|
|
6
|
+
export const onRequest: MiddlewareHandler = async (_context, next) => {
|
|
7
|
+
const response = await next();
|
|
8
|
+
const env = await workerEnv();
|
|
9
|
+
const ancestors = frameAncestors(env?.TIDY_FRAME_ANCESTORS);
|
|
10
|
+
if (ancestors.length === 0) return response;
|
|
11
|
+
// Response headers can be immutable (static assets, redirects); rebuild when so.
|
|
12
|
+
try {
|
|
13
|
+
applyFramePolicy(response.headers, ancestors);
|
|
14
|
+
return response;
|
|
15
|
+
} catch {
|
|
16
|
+
const copy = new Response(response.body, response);
|
|
17
|
+
applyFramePolicy(copy.headers, ancestors);
|
|
18
|
+
return copy;
|
|
19
|
+
}
|
|
20
|
+
};
|
package/src/version.ts
CHANGED