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.
- package/dist/cli/publish/assetUpload.d.ts +26 -0
- package/dist/cli/publish/assetUpload.d.ts.map +1 -0
- package/dist/cli/publish/assetUpload.js +82 -0
- package/dist/cli/publish/assetUpload.js.map +1 -0
- package/dist/cli/publish/orchestrator.d.ts.map +1 -1
- package/dist/cli/publish/orchestrator.js +20 -25
- package/dist/cli/publish/orchestrator.js.map +1 -1
- package/dist/cli/publish/providers/cloudflare.d.ts +8 -8
- package/dist/cli/publish/providers/cloudflare.d.ts.map +1 -1
- package/dist/cli/publish/providers/cloudflare.js +90 -75
- package/dist/cli/publish/providers/cloudflare.js.map +1 -1
- package/dist/cli/publish/staticOps.d.ts +2 -0
- package/dist/cli/publish/staticOps.d.ts.map +1 -1
- package/dist/cli/publish/staticOps.js +16 -5
- package/dist/cli/publish/staticOps.js.map +1 -1
- package/dist/cli/publish/staticSync.d.ts +2 -0
- package/dist/cli/publish/staticSync.d.ts.map +1 -1
- package/dist/cli/publish/staticSync.js.map +1 -1
- package/dist/cli/publish/uploadAuth.d.ts +9 -0
- package/dist/cli/publish/uploadAuth.d.ts.map +1 -0
- package/dist/cli/publish/uploadAuth.js +60 -0
- package/dist/cli/publish/uploadAuth.js.map +1 -0
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/server/Auth.d.ts.map +1 -1
- package/dist/server/Auth.js +6 -0
- package/dist/server/Auth.js.map +1 -1
- package/dist/server/adapter/workers/createWorkersApp.d.ts.map +1 -1
- package/dist/server/adapter/workers/createWorkersApp.js +11 -1
- package/dist/server/adapter/workers/createWorkersApp.js.map +1 -1
- package/dist/server/adapter/workers/edgeCache.d.ts +17 -0
- package/dist/server/adapter/workers/edgeCache.d.ts.map +1 -1
- package/dist/server/adapter/workers/edgeCache.js +38 -0
- package/dist/server/adapter/workers/edgeCache.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/publish/assetUpload.test.ts +258 -0
- package/src/cli/publish/assetUpload.ts +107 -0
- package/src/cli/publish/assetUploadS3.test.ts +55 -0
- package/src/cli/publish/orchestrator.ts +32 -27
- package/src/cli/publish/providers/cloudflare.ts +139 -108
- package/src/cli/publish/staticOps.test.ts +28 -1
- package/src/cli/publish/staticOps.ts +39 -32
- package/src/cli/publish/staticSync.ts +2 -0
- package/src/cli/publish/uploadAuth.test.ts +76 -0
- package/src/cli/publish/uploadAuth.ts +69 -0
- package/src/cli/version.ts +1 -1
- package/src/server/Auth.ts +6 -0
- package/src/server/adapter/workers/createWorkersApp.ts +14 -1
- package/src/server/adapter/workers/edgeCache.test.ts +64 -0
- package/src/server/adapter/workers/edgeCache.ts +38 -0
package/src/server/Auth.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|