Signed requests: strictly increasing timestamps per key
CI / Node 22 (push) Successful in 13s
CI / Node 24 (push) Successful in 13s
CI / Node 26 (push) Successful in 14s
CI / Node 24 (pull_request) Successful in 12s
CI / Chromium (pull_request) Successful in 48s
CI / Publish to staging registry (pull_request) Skipped
CI / Node 22 (pull_request) Successful in 13s
CI / Node 26 (pull_request) Successful in 12s
CI / Chromium (push) Successful in 47s
CI / Publish to staging registry (push) Successful in 10s
CI / Node 22 (push) Successful in 13s
CI / Node 24 (push) Successful in 13s
CI / Node 26 (push) Successful in 14s
CI / Node 24 (pull_request) Successful in 12s
CI / Chromium (pull_request) Successful in 48s
CI / Publish to staging registry (pull_request) Skipped
CI / Node 22 (pull_request) Successful in 13s
CI / Node 26 (pull_request) Successful in 12s
CI / Chromium (push) Successful in 47s
CI / Publish to staging registry (push) Successful in 10s
Ed25519 signatures are deterministic, so two identical requests from one key in the same millisecond (e.g. two devices subscribing to the same path, or parallel identical reads) had the same signature, and the server rejected the second as a replay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
This commit is contained in:
@@ -54,9 +54,19 @@ export class RequestAuth {
|
||||
return new TextEncoder().encode(`${timestamp}:${path}:${body}`)
|
||||
}
|
||||
|
||||
/** Auth headers for a request to `path` with `body`. */
|
||||
/** Last timestamp used per public key in this process (see `headers`). */
|
||||
private static readonly lastTimestamps = new Map<string, number>()
|
||||
|
||||
/**
|
||||
* Auth headers for a request to `path` with `body`. Timestamps per public key strictly increase
|
||||
* within a process: Ed25519 signatures are deterministic, so two identical requests in the same
|
||||
* millisecond would otherwise carry the same signature, and the second would be rejected as a
|
||||
* replay.
|
||||
*/
|
||||
static async headers(signer: RequestSigner, path: string, body: string, now: number = Date.now()): Promise<Record<string, string>> {
|
||||
const timestamp = String(now)
|
||||
const time = Math.max(now, (RequestAuth.lastTimestamps.get(signer.publicKey) ?? 0) + 1)
|
||||
RequestAuth.lastTimestamps.set(signer.publicKey, time)
|
||||
const timestamp = String(time)
|
||||
const signature = await signer.sign(RequestAuth.payload(timestamp, path, body))
|
||||
return {
|
||||
[AUTH_HEADERS.publicKey]: signer.publicKey,
|
||||
|
||||
Reference in New Issue
Block a user