next-evolve 0.3.1 → 0.3.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.
@@ -12,5 +12,14 @@ export type AggregateRouteDeps = {
12
12
  };
13
13
  /** Delivery ids seen in the last 10 minutes, shared by every signed cloud → app route. */
14
14
  export declare function sharedReplayGuard(): ReplayGuard;
15
+ /**
16
+ * True when a verified call is new: its delivery id and its signed message
17
+ * (`t` + body) are both unseen. The delivery id isn't covered by the
18
+ * signature, so checking it alone would let a captured call be replayed under
19
+ * a fresh id within the signature window (T12.5).
20
+ */
21
+ export declare function firstDelivery(guard: ReplayGuard, delivery: string | null, t: number, body: string): boolean;
22
+ /** True the first time this signed message (`t` + body) is seen in the replay window. */
23
+ export declare function firstMessage(guard: ReplayGuard, t: number, body: string): boolean;
15
24
  export declare function handleAggregate(req: Request, deps: AggregateRouteDeps): Promise<Response>;
16
25
  export {};
@@ -1,5 +1,6 @@
1
1
  import { runAggregate } from "../aggregate/run.js";
2
2
  import { DELIVERY_HEADER, SIGNATURE_HEADER, aggregateRequestSchema, createReplayGuard, isSupportedProtocol, parseSecretKey, signBody, unsupportedProtocolBody, verifyBody } from "@next-evolve/protocol";
3
+ import { createHash } from "node:crypto";
3
4
  // src/server/aggregate-route.ts
4
5
  const MAX_BODY_BYTES = 262144;
5
6
  const KEY = Symbol.for("next-evolve.replay");
@@ -9,6 +10,19 @@ function sharedReplayGuard() {
9
10
  g[KEY] ??= createReplayGuard();
10
11
  return g[KEY];
11
12
  }
13
+ /**
14
+ * True when a verified call is new: its delivery id and its signed message
15
+ * (`t` + body) are both unseen. The delivery id isn't covered by the
16
+ * signature, so checking it alone would let a captured call be replayed under
17
+ * a fresh id within the signature window (T12.5).
18
+ */
19
+ function firstDelivery(guard, delivery, t, body) {
20
+ return !!delivery && guard.check(delivery) && firstMessage(guard, t, body);
21
+ }
22
+ /** True the first time this signed message (`t` + body) is seen in the replay window. */
23
+ function firstMessage(guard, t, body) {
24
+ return guard.check(`msg:${t}:${createHash("sha256").update(body).digest("base64url")}`);
25
+ }
12
26
  const json = (status, body, headers = {}) => Response.json(body, {
13
27
  status,
14
28
  headers
@@ -25,8 +39,7 @@ async function handleAggregate(req, deps) {
25
39
  error: "invalid_signature",
26
40
  reason: sig.reason
27
41
  });
28
- const delivery = req.headers.get(DELIVERY_HEADER) ?? "";
29
- if (!(deps.replay ?? sharedReplayGuard()).check(delivery)) return json(409, { error: "replayed" });
42
+ if (!firstDelivery(deps.replay ?? sharedReplayGuard(), req.headers.get(DELIVERY_HEADER), sig.t, body)) return json(409, { error: "replayed" });
30
43
  let parsed;
31
44
  try {
32
45
  parsed = JSON.parse(body);
@@ -65,4 +78,4 @@ async function handleAggregate(req, deps) {
65
78
  }
66
79
  });
67
80
  }
68
- export { handleAggregate, sharedReplayGuard };
81
+ export { firstDelivery, firstMessage, handleAggregate, sharedReplayGuard };
@@ -1,5 +1,5 @@
1
1
  import { logRateLimited } from "../core/log.js";
2
- import { sharedReplayGuard } from "./aggregate-route.js";
2
+ import { firstMessage, sharedReplayGuard } from "./aggregate-route.js";
3
3
  import { DELIVERY_HEADER, SIGNATURE_HEADER, SUPPORTED_PROTOCOLS, isSupportedProtocol, parseSecretKey, syncWebhookSchema, unsupportedProtocolBody, verifyBody } from "@next-evolve/protocol";
4
4
  // src/server/sync-route.ts
5
5
  /** Cache tag on the render-side bundle fetch (spec §7.7). */
@@ -31,7 +31,9 @@ async function handleSync(req, deps) {
31
31
  error: "invalid_signature",
32
32
  reason: sig.reason
33
33
  });
34
- if (!(deps.replay ?? sharedReplayGuard()).check(req.headers.get(DELIVERY_HEADER) ?? "")) return json(409, { error: "replayed" });
34
+ const guard = deps.replay ?? sharedReplayGuard();
35
+ const delivery = req.headers.get(DELIVERY_HEADER);
36
+ if (!delivery || !guard.check(delivery)) return json(409, { error: "replayed" });
35
37
  let parsed;
36
38
  try {
37
39
  parsed = JSON.parse(body);
@@ -48,6 +50,7 @@ async function handleSync(req, deps) {
48
50
  protocol: { supported: SUPPORTED_PROTOCOLS }
49
51
  });
50
52
  if (r.data.site !== site) return json(403, { error: "site_mismatch" });
53
+ if (!firstMessage(guard, sig.t, body)) return json(409, { error: "replayed" });
51
54
  if (!(!current || r.data.ver > current.ver || r.data.ver === current.ver && r.data.iat > current.iat)) return json(202, {
52
55
  accepted: false,
53
56
  reason: "current"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-evolve",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Self-improving Next.js sites: server-side variants with no flicker, measured in your own Postgres.",
5
5
  "license": "MIT",
6
6
  "type": "module",