next-evolve 0.3.1 → 0.3.2
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,12 @@ 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;
|
|
15
22
|
export declare function handleAggregate(req: Request, deps: AggregateRouteDeps): Promise<Response>;
|
|
16
23
|
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,16 @@ 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
|
+
if (!delivery || !guard.check(delivery)) return false;
|
|
21
|
+
return guard.check(`msg:${t}:${createHash("sha256").update(body).digest("base64url")}`);
|
|
22
|
+
}
|
|
12
23
|
const json = (status, body, headers = {}) => Response.json(body, {
|
|
13
24
|
status,
|
|
14
25
|
headers
|
|
@@ -25,8 +36,7 @@ async function handleAggregate(req, deps) {
|
|
|
25
36
|
error: "invalid_signature",
|
|
26
37
|
reason: sig.reason
|
|
27
38
|
});
|
|
28
|
-
|
|
29
|
-
if (!(deps.replay ?? sharedReplayGuard()).check(delivery)) return json(409, { error: "replayed" });
|
|
39
|
+
if (!firstDelivery(deps.replay ?? sharedReplayGuard(), req.headers.get(DELIVERY_HEADER), sig.t, body)) return json(409, { error: "replayed" });
|
|
30
40
|
let parsed;
|
|
31
41
|
try {
|
|
32
42
|
parsed = JSON.parse(body);
|
|
@@ -65,4 +75,4 @@ async function handleAggregate(req, deps) {
|
|
|
65
75
|
}
|
|
66
76
|
});
|
|
67
77
|
}
|
|
68
|
-
export { handleAggregate, sharedReplayGuard };
|
|
78
|
+
export { firstDelivery, handleAggregate, sharedReplayGuard };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logRateLimited } from "../core/log.js";
|
|
2
|
-
import { sharedReplayGuard } from "./aggregate-route.js";
|
|
2
|
+
import { firstDelivery, 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,8 @@ async function handleSync(req, deps) {
|
|
|
31
31
|
error: "invalid_signature",
|
|
32
32
|
reason: sig.reason
|
|
33
33
|
});
|
|
34
|
-
|
|
34
|
+
const guard = deps.replay ?? sharedReplayGuard();
|
|
35
|
+
if (!firstDelivery(guard, req.headers.get(DELIVERY_HEADER), sig.t, body)) return json(409, { error: "replayed" });
|
|
35
36
|
let parsed;
|
|
36
37
|
try {
|
|
37
38
|
parsed = JSON.parse(body);
|