@warpgogol/werkstatt-shared 0.8.1 → 0.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpgogol/werkstatt-shared",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1343,6 +1343,15 @@
1343
1343
  "default": "./src/share/scripts/index.ts"
1344
1344
  }
1345
1345
  },
1346
+ "scripts": {
1347
+ "lint": "pnpm exec eslint \"src/**/*.ts\"",
1348
+ "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
1349
+ "build": "pnpm exec tsc -p tsconfig.json --noEmit",
1350
+ "build:check": "pnpm exec tsc -p tsconfig.json --noEmit",
1351
+ "prepublishOnly": "pnpm exec tsc -p tsconfig.json --noEmit",
1352
+ "test": "vitest run",
1353
+ "test:watch": "vitest"
1354
+ },
1346
1355
  "publishConfig": {
1347
1356
  "access": "public"
1348
1357
  },
@@ -1382,12 +1391,5 @@
1382
1391
  "typescript-eslint": "8.65.0",
1383
1392
  "vitest": "^4.1.10"
1384
1393
  },
1385
- "scripts": {
1386
- "lint": "pnpm exec eslint \"src/**/*.ts\"",
1387
- "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
1388
- "build": "pnpm exec tsc -p tsconfig.json --noEmit",
1389
- "build:check": "pnpm exec tsc -p tsconfig.json --noEmit",
1390
- "test": "vitest run",
1391
- "test:watch": "vitest"
1392
- }
1393
- }
1394
+ "packageManager": "pnpm@11.10.0"
1395
+ }
@@ -219,6 +219,8 @@ export interface ImageVariantEntry {
219
219
  * stale derived variants (same contract as video.variants.generate RFC-0210).
220
220
  */
221
221
  sourceHash?: string;
222
+ /** RFC-0928: WebP quality used by image.variants.generate (minimum 90). */
223
+ quality?: number;
222
224
  }
223
225
 
224
226
  /** Top-level manifest written by `image.variants.generate` and read by createBuildPortableProvider. */
@@ -4,7 +4,7 @@
4
4
  <keywords>middleware, access-protection, basic-auth, pin, dev, alt, RFC-0899</keywords>
5
5
  <responsibilities>
6
6
  <item>Check Host header against dev.* and alt.* patterns — pass through for main domain.</item>
7
- <item>Require Basic Auth (username: access, password: ACCESS_PIN env var) for dev/alt hosts.</item>
7
+ <item>Require Basic Auth (username: warp, password: ACCESS_PIN env var) for dev/alt hosts.</item>
8
8
  <item>Set X-Robots-Tag: noindex, nofollow, noai, noimageai on ALL dev/alt responses (including 401).</item>
9
9
  <item>Use constant-time string comparison for auth check to prevent timing attacks.</item>
10
10
  <item>Pass through when ACCESS_PIN is unset (allows new sites before protection is configured).</item>
@@ -45,6 +45,16 @@ function isDevOrAltHost(host: string): boolean {
45
45
  return host.startsWith("dev.") || host.startsWith("alt.");
46
46
  }
47
47
 
48
+ /**
49
+ * Paths exempt from access protection on dev/alt subdomains.
50
+ * Browsers fetch these without sending Basic Auth credentials, causing 401 console errors.
51
+ */
52
+ const PUBLIC_STATIC_EXEMPTIONS = ["/manifest.webmanifest"];
53
+
54
+ function isExemptPath(pathname: string): boolean {
55
+ return PUBLIC_STATIC_EXEMPTIONS.includes(pathname);
56
+ }
57
+
48
58
  /**
49
59
  * RFC-0899: Check access protection for a request. Called from the Worker entry point
50
60
  * (worker.ts) before passing to the Astro handler. This is necessary because Astro
@@ -66,6 +76,9 @@ export function checkAccessProtection(
66
76
  const host = request.headers.get("host") ?? "";
67
77
  if (!isDevOrAltHost(host)) return null;
68
78
 
79
+ const url = new URL(request.url);
80
+ if (isExemptPath(url.pathname)) return null;
81
+
69
82
  const pin = (env.ACCESS_PIN as string | undefined) ?? undefined;
70
83
 
71
84
  // No PIN set — allow access (caller should add X-Robots-Tag)
@@ -73,7 +86,7 @@ export function checkAccessProtection(
73
86
 
74
87
  // Check Basic Auth
75
88
  const auth = request.headers.get("authorization") ?? "";
76
- const expected = `Basic ${btoa(`access:${pin}`)}`;
89
+ const expected = `Basic ${btoa(`warp:${pin}`)}`;
77
90
 
78
91
  if (auth && constantTimeEqual(auth, expected)) {
79
92
  return null; // Authenticated — pass through
@@ -144,6 +157,13 @@ export const accessProtectionMiddleware = defineMiddleware(async (context: any,
144
157
  return next();
145
158
  }
146
159
 
160
+ const url = new URL(context.request.url);
161
+ if (isExemptPath(url.pathname)) {
162
+ const response = await next();
163
+ response.headers.set("X-Robots-Tag", NOINDEX_HEADER);
164
+ return response;
165
+ }
166
+
147
167
  const pin = await resolveAccessPin();
148
168
 
149
169
  // No PIN set — allow access but still set noindex headers
@@ -155,7 +175,7 @@ export const accessProtectionMiddleware = defineMiddleware(async (context: any,
155
175
 
156
176
  // Check Basic Auth BEFORE calling next()
157
177
  const auth = context.request.headers.get("authorization") ?? "";
158
- const expected = `Basic ${btoa(`access:${pin}`)}`;
178
+ const expected = `Basic ${btoa(`warp:${pin}`)}`;
159
179
 
160
180
  if (auth && constantTimeEqual(auth, expected)) {
161
181
  const response = await next();
@@ -25,7 +25,7 @@ describe("RFC-0899: access protection middleware", () => {
25
25
  ) => Promise<Response>;
26
26
  }
27
27
 
28
- function makeContext(host: string, authHeader?: string) {
28
+ function makeContext(host: string, authHeader?: string, pathname = "/") {
29
29
  const headers = new Map<string, string>();
30
30
  headers.set("host", host);
31
31
  if (authHeader) headers.set("authorization", authHeader);
@@ -34,6 +34,7 @@ describe("RFC-0899: access protection middleware", () => {
34
34
  headers: {
35
35
  get: (name: string) => headers.get(name.toLowerCase()) ?? null,
36
36
  },
37
+ url: `https://${host}${pathname}`,
37
38
  },
38
39
  };
39
40
  }
@@ -42,10 +43,11 @@ describe("RFC-0899: access protection middleware", () => {
42
43
  handler: (context: unknown, next: () => Promise<Response>) => Promise<Response>,
43
44
  host: string,
44
45
  authHeader?: string,
46
+ pathname = "/",
45
47
  ): Promise<Response & { _nextCalled: boolean }> {
46
48
  let nextCalled = false;
47
49
  const nextResponse = new Response("page content", { status: 200 });
48
- const result = await handler(makeContext(host, authHeader), async () => {
50
+ const result = await handler(makeContext(host, authHeader, pathname), async () => {
49
51
  nextCalled = true;
50
52
  return nextResponse;
51
53
  });
@@ -80,7 +82,7 @@ describe("RFC-0899: access protection middleware", () => {
80
82
 
81
83
  it("passes through dev.* with correct Basic Auth", async () => {
82
84
  mockEnv.ACCESS_PIN = "1234";
83
- const expected = `Basic ${btoa("access:1234")}`;
85
+ const expected = `Basic ${btoa("warp:1234")}`;
84
86
  const handler = await loadMiddleware();
85
87
  const res = await runMiddleware(handler, "dev.example.com", expected);
86
88
  expect(res._nextCalled).toBe(true);
@@ -90,7 +92,7 @@ describe("RFC-0899: access protection middleware", () => {
90
92
 
91
93
  it("returns 401 for dev.* with wrong PIN", async () => {
92
94
  mockEnv.ACCESS_PIN = "1234";
93
- const wrong = `Basic ${btoa("access:9999")}`;
95
+ const wrong = `Basic ${btoa("warp:9999")}`;
94
96
  const handler = await loadMiddleware();
95
97
  const res = await runMiddleware(handler, "dev.example.com", wrong);
96
98
  expect(res._nextCalled).toBe(false);
@@ -120,4 +122,21 @@ describe("RFC-0899: access protection middleware", () => {
120
122
  const res = await runMiddleware(handler, "example.com");
121
123
  expect(res.headers.get("X-Robots-Tag")).toBe(null);
122
124
  });
125
+
126
+ it("exempts /manifest.webmanifest from auth on dev.* when PIN is set", async () => {
127
+ mockEnv.ACCESS_PIN = "1234";
128
+ const handler = await loadMiddleware();
129
+ const res = await runMiddleware(handler, "dev.example.com", undefined, "/manifest.webmanifest");
130
+ expect(res._nextCalled).toBe(true);
131
+ expect(res.status).toBe(200);
132
+ expect(res.headers.get("X-Robots-Tag")).toBe("noindex, nofollow, noai, noimageai");
133
+ });
134
+
135
+ it("exempts /manifest.webmanifest from auth on alt.* when PIN is set", async () => {
136
+ mockEnv.ACCESS_PIN = "1234";
137
+ const handler = await loadMiddleware();
138
+ const res = await runMiddleware(handler, "alt.example.com", undefined, "/manifest.webmanifest");
139
+ expect(res._nextCalled).toBe(true);
140
+ expect(res.status).toBe(200);
141
+ });
123
142
  });
@@ -1,38 +0,0 @@
1
- import { test, expect } from "vitest";
2
- import { createDevPropsValidator } from "../dev-props-validator.ts";
3
-
4
- /*
5
- <MODULE_CONTRACT>
6
- <purpose>
7
- RFC-0262: end-to-end test of createDevPropsValidator against the real
8
- packages/werkstatt-site/src/domain/ui/sections/hero manifest (this test runs from within the
9
- actual monorepo checkout, so workspace-root discovery resolves for real).
10
- </purpose>
11
- </MODULE_CONTRACT>
12
- */
13
-
14
- test("createDevPropsValidator: passes valid Europa (hero-section) props", async () => {
15
- const validate = createDevPropsValidator();
16
- await validate("Europa", { header: { heading: "Hello" } }, "hero-block"); // cosmic-literals-ignore: fixture cosmicName exercising the real hero-section manifest
17
- });
18
-
19
- test("createDevPropsValidator: throws PAGE-PROPS-01 on an undeclared prop key", async () => {
20
- const validate = createDevPropsValidator();
21
- try {
22
- await validate(
23
- "Europa", // cosmic-literals-ignore: fixture cosmicName exercising the real hero-section manifest
24
- { header: { heading: "Hello" }, totallyUnknownField: true },
25
- "hero-block",
26
- ); // cosmic-literals-ignore: fixture cosmicName exercising the real hero-section manifest
27
- expect.fail("should have thrown");
28
- } catch (error) {
29
- expect(error).toBeInstanceOf(Error);
30
- expect((error as Error).message).toMatch(/PAGE-PROPS-01/);
31
- expect((error as Error).message).toMatch(/hero-block/);
32
- }
33
- });
34
-
35
- test("createDevPropsValidator: an unknown planetName resolves no schema and does not throw", async () => {
36
- const validate = createDevPropsValidator();
37
- await validate("NotARealPlanet", { anything: true }, null);
38
- });