ugly-app 0.1.1069 → 0.1.1071

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 (50) hide show
  1. package/dist/cli/publish/assetUpload.d.ts +26 -0
  2. package/dist/cli/publish/assetUpload.d.ts.map +1 -0
  3. package/dist/cli/publish/assetUpload.js +82 -0
  4. package/dist/cli/publish/assetUpload.js.map +1 -0
  5. package/dist/cli/publish/orchestrator.d.ts.map +1 -1
  6. package/dist/cli/publish/orchestrator.js +20 -25
  7. package/dist/cli/publish/orchestrator.js.map +1 -1
  8. package/dist/cli/publish/providers/cloudflare.d.ts +8 -8
  9. package/dist/cli/publish/providers/cloudflare.d.ts.map +1 -1
  10. package/dist/cli/publish/providers/cloudflare.js +90 -75
  11. package/dist/cli/publish/providers/cloudflare.js.map +1 -1
  12. package/dist/cli/publish/staticOps.d.ts +2 -0
  13. package/dist/cli/publish/staticOps.d.ts.map +1 -1
  14. package/dist/cli/publish/staticOps.js +16 -5
  15. package/dist/cli/publish/staticOps.js.map +1 -1
  16. package/dist/cli/publish/staticSync.d.ts +2 -0
  17. package/dist/cli/publish/staticSync.d.ts.map +1 -1
  18. package/dist/cli/publish/staticSync.js.map +1 -1
  19. package/dist/cli/publish/uploadAuth.d.ts +9 -0
  20. package/dist/cli/publish/uploadAuth.d.ts.map +1 -0
  21. package/dist/cli/publish/uploadAuth.js +60 -0
  22. package/dist/cli/publish/uploadAuth.js.map +1 -0
  23. package/dist/cli/version.d.ts +1 -1
  24. package/dist/cli/version.js +1 -1
  25. package/dist/server/Auth.d.ts.map +1 -1
  26. package/dist/server/Auth.js +6 -0
  27. package/dist/server/Auth.js.map +1 -1
  28. package/dist/server/adapter/workers/createWorkersApp.d.ts.map +1 -1
  29. package/dist/server/adapter/workers/createWorkersApp.js +11 -1
  30. package/dist/server/adapter/workers/createWorkersApp.js.map +1 -1
  31. package/dist/server/adapter/workers/edgeCache.d.ts +17 -0
  32. package/dist/server/adapter/workers/edgeCache.d.ts.map +1 -1
  33. package/dist/server/adapter/workers/edgeCache.js +38 -0
  34. package/dist/server/adapter/workers/edgeCache.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/cli/publish/assetUpload.test.ts +258 -0
  37. package/src/cli/publish/assetUpload.ts +107 -0
  38. package/src/cli/publish/assetUploadS3.test.ts +55 -0
  39. package/src/cli/publish/orchestrator.ts +32 -27
  40. package/src/cli/publish/providers/cloudflare.ts +139 -108
  41. package/src/cli/publish/staticOps.test.ts +28 -1
  42. package/src/cli/publish/staticOps.ts +39 -32
  43. package/src/cli/publish/staticSync.ts +2 -0
  44. package/src/cli/publish/uploadAuth.test.ts +76 -0
  45. package/src/cli/publish/uploadAuth.ts +69 -0
  46. package/src/cli/version.ts +1 -1
  47. package/src/server/Auth.ts +6 -0
  48. package/src/server/adapter/workers/createWorkersApp.ts +14 -1
  49. package/src/server/adapter/workers/edgeCache.test.ts +64 -0
  50. package/src/server/adapter/workers/edgeCache.ts +38 -0
@@ -11,6 +11,8 @@ import { getUglyBotUrl } from '../shared/uglyBotUrl.js';
11
11
 
12
12
  /** Default cookie max-age: 30 days (in seconds). */
13
13
  const COOKIE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
14
+ /** `/auth/token` is per-visitor (cookie-keyed): no shared or browser cache. */
15
+ const AUTH_TOKEN_CACHE_CONTROL = 'private, no-store';
14
16
 
15
17
  /**
16
18
  * Resolve the auth secret used for Mode B local JWTs. Same precedence as
@@ -307,6 +309,10 @@ export function createAuthRoutes(
307
309
 
308
310
  // GET /auth/token — exchange a valid auth_token cookie for a fresh JWT
309
311
  router.get('/auth/token', async (req: Request, res: Response) => {
312
+ // Per-visitor answer keyed only by the Cookie header, which shared caches
313
+ // ignore: a cached anonymous `{ token: null }` was replayed to signed-in
314
+ // visitors in production. Never let any cache store it.
315
+ res.setHeader('Cache-Control', AUTH_TOKEN_CACHE_CONTROL);
310
316
  const token = req.cookies?.auth_token as string | undefined;
311
317
  // `?probe=1` (the SSR boot's session check): "not signed in" is a normal
312
318
  // answer, not an error. Mirrors the Workers adapter.
@@ -97,6 +97,7 @@ import {
97
97
  import type { DbAdapter, PubSubMessage, ServerAdapter } from '../types.js';
98
98
  import type { QueueMessage, WorkersEnv } from './cf-types.js';
99
99
  import { CollectionDO } from './do-collection.js';
100
+ import { withDynamicCacheDefault } from './edgeCache.js';
100
101
  import { createWorkersAdapter } from './index.js';
101
102
  import { runWithPendingPublishes } from './pubsub.js';
102
103
  import { backgroundWait } from '../../backgroundWait.js';
@@ -2835,6 +2836,8 @@ export function createWorkersApp<R extends AppRegistryBase>(
2835
2836
  // The Node implementation registers these via createAuthRoutes() on
2836
2837
  // the express router; on Workers they live here.
2837
2838
  const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; // 30 days
2839
+ /** `/auth/token` is per-visitor (cookie-keyed): no shared or browser cache. */
2840
+ const AUTH_TOKEN_CACHE_CONTROL = 'private, no-store';
2838
2841
  function buildCookie(token: string, opts: { clear?: boolean } = {}): string {
2839
2842
  const parts = [`auth_token=${opts.clear ? '' : token}`];
2840
2843
  parts.push('Path=/');
@@ -2898,6 +2901,11 @@ export function createWorkersApp<R extends AppRegistryBase>(
2898
2901
  });
2899
2902
 
2900
2903
  hono.get('/auth/token', async (c) => {
2904
+ // The answer depends only on the Cookie header, which Cloudflare's Workers
2905
+ // cache does not key on: without this, one anonymous probe's
2906
+ // `{ token: null }` was cached and replayed to signed-in visitors, so SSR
2907
+ // pages booted logged out. Mirrors server/Auth.ts.
2908
+ c.header('Cache-Control', AUTH_TOKEN_CACHE_CONTROL);
2901
2909
  const token = getAuthCookie(c);
2902
2910
  // `?probe=1` (the SSR boot's session check) asks "is anyone signed in?",
2903
2911
  // for which "no" is a normal answer — a 401 there is a console error on
@@ -3273,7 +3281,12 @@ export function createWorkersApp<R extends AppRegistryBase>(
3273
3281
  // this request (the new-model cache lives on `ctx.cache`, not yet in the
3274
3282
  // published types — hence the structural `WorkerExecutionCtx`).
3275
3283
  ctx.executionCtx = ctxArg;
3276
- const res = await hono.fetch(request, env, ctxArg);
3284
+ // A router response with no Cache-Control is never shared-cacheable:
3285
+ // the edge cache ignores Cookie, so a per-visitor answer would leak.
3286
+ const res = withDynamicCacheDefault(
3287
+ request,
3288
+ await hono.fetch(request, env, ctxArg),
3289
+ );
3277
3290
  // Drain any console.error/warn buffered during this request into the
3278
3291
  // project's Postgres errorLog. The `setTimeout`-based flush scheduler
3279
3292
  // can't fire after the isolate freezes post-response, so we extend the
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ DYNAMIC_DEFAULT_CACHE_CONTROL,
4
+ withDynamicCacheDefault,
5
+ } from './edgeCache.js';
6
+
7
+ const get = new Request('https://app.test/x');
8
+
9
+ describe('withDynamicCacheDefault', () => {
10
+ it('marks a header-less GET response private, no-store', () => {
11
+ const res = withDynamicCacheDefault(get, Response.json({ token: null }));
12
+ expect(res.headers.get('cache-control')).toBe(
13
+ DYNAMIC_DEFAULT_CACHE_CONTROL,
14
+ );
15
+ });
16
+
17
+ it('covers HEAD too', () => {
18
+ const head = new Request('https://app.test/x', { method: 'HEAD' });
19
+ const res = withDynamicCacheDefault(head, new Response(null));
20
+ expect(res.headers.get('cache-control')).toBe('private, no-store');
21
+ });
22
+
23
+ it('keeps a route that chose its own Cache-Control', () => {
24
+ const res = withDynamicCacheDefault(
25
+ get,
26
+ new Response('ok', {
27
+ headers: { 'cache-control': 'public, s-maxage=600' },
28
+ }),
29
+ );
30
+ expect(res.headers.get('cache-control')).toBe('public, s-maxage=600');
31
+ });
32
+
33
+ it('leaves non-GET methods alone (never cached anyway)', () => {
34
+ const post = new Request('https://app.test/x', { method: 'POST' });
35
+ const res = withDynamicCacheDefault(post, new Response('ok'));
36
+ expect(res.headers.get('cache-control')).toBeNull();
37
+ });
38
+
39
+ it('copies a response whose headers are immutable', async () => {
40
+ const original = new Response('body', {
41
+ status: 202,
42
+ headers: { 'x-a': '1' },
43
+ });
44
+ // Simulate a passthrough response (fetch / DO stub) with frozen headers.
45
+ Object.defineProperty(original, 'headers', {
46
+ value: new Proxy(original.headers, {
47
+ get(target, prop) {
48
+ if (prop === 'set')
49
+ return () => {
50
+ throw new TypeError("Can't modify immutable headers.");
51
+ };
52
+ const v = Reflect.get(target, prop) as unknown;
53
+ return typeof v === 'function' ? v.bind(target) : v;
54
+ },
55
+ }),
56
+ });
57
+ const res = withDynamicCacheDefault(get, original);
58
+ expect(res).not.toBe(original);
59
+ expect(res.status).toBe(202);
60
+ expect(res.headers.get('x-a')).toBe('1');
61
+ expect(res.headers.get('cache-control')).toBe('private, no-store');
62
+ expect(await res.text()).toBe('body');
63
+ });
64
+ });
@@ -179,3 +179,41 @@ export function ssrCacheTags(routeName: string, extra: string[] = []): string {
179
179
  const tags = ['ssr', `ssr-${routeTag}`, ...extra.map(clean)].filter(Boolean);
180
180
  return [...new Set(tags)].join(',');
181
181
  }
182
+
183
+ /** Default `Cache-Control` for a dynamic Worker response that set none. */
184
+ export const DYNAMIC_DEFAULT_CACHE_CONTROL = 'private, no-store';
185
+
186
+ /**
187
+ * Give a GET/HEAD response from the Worker's router (framework + app routes)
188
+ * `Cache-Control: private, no-store` when it carries no `Cache-Control` of its
189
+ * own.
190
+ *
191
+ * With the declarative Workers cache on, Cloudflare stores a header-less 200
192
+ * under path+query and does NOT key on Cookie or Authorization. A route whose
193
+ * answer depends on the caller (the auth cookie, a session) therefore leaked one
194
+ * visitor's answer to everyone: an anonymous `/auth/token?probe=1`
195
+ * `{ token: null }` was replayed to signed-in visitors. A route that is meant to
196
+ * be shared-cached says so with its own `Cache-Control` (static assets and SSR
197
+ * documents are produced outside the router and set theirs), so only the
198
+ * header-less remainder is touched. WebSocket upgrades (101) pass through.
199
+ */
200
+ export function withDynamicCacheDefault(
201
+ request: Request,
202
+ res: Response,
203
+ ): Response {
204
+ const method = request.method.toUpperCase();
205
+ if (method !== 'GET' && method !== 'HEAD') return res;
206
+ if (res.status === 101) return res;
207
+ if ((res as Response & { webSocket?: unknown }).webSocket) return res;
208
+ if (res.headers.has('cache-control')) return res;
209
+ try {
210
+ res.headers.set('cache-control', DYNAMIC_DEFAULT_CACHE_CONTROL);
211
+ return res;
212
+ } catch {
213
+ // Immutable headers (a response passed through from fetch() or a Durable
214
+ // Object stub): copy it into a mutable one.
215
+ const copy = new Response(res.body, res);
216
+ copy.headers.set('cache-control', DYNAMIC_DEFAULT_CACHE_CONTROL);
217
+ return copy;
218
+ }
219
+ }