authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-05 20:56:10-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 20:56:10-07:00
log99db70cf1a1f24deca80e40a94a6cdbbaf511da6
tree26629e23bfd4ad25cff9dd93000c1113afed0b61
parent68e50db20b3784bad69dd754165020a08f2cd42a
signature Signed by PGP key B5690EEEBB952194

refactor(server): replay guards stay in CloudLink (#16349)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

5 files changed, 77 insertions(+), 100 deletions(-)

apps/server/src/auth/replayMarkers.ts+1-1
......@@ -7,7 +7,7 @@ import * as Option from "effect/Option";
77import * as Path from "effect/Path";
88import * as Schedule from "effect/Schedule";
99
10import { CLOUD_REPLAY_MARKER_PREFIXES } from "../cloud/linkChecks.ts";
10import { CLOUD_REPLAY_MARKER_PREFIXES } from "../cloud/CloudLink.ts";
1111import * as ServerConfig from "../config.ts";
1212import { forkParked } from "../serverActivation.ts";
1313import { DPOP_REPLAY_MARKER_PREFIX } from "./dpop.ts";
apps/server/src/cloud/CloudLink.lifecycle.test.ts+42
......@@ -8,6 +8,7 @@ import * as FileSystem from "effect/FileSystem";
88import * as Layer from "effect/Layer";
99import * as Option from "effect/Option";
1010import * as Path from "effect/Path";
11import * as PlatformError from "effect/PlatformError";
1112import * as Schema from "effect/Schema";
1213import * as TestClock from "effect/testing/TestClock";
1314import * as Stream from "effect/Stream";
......@@ -40,6 +41,7 @@ import {
4041 RELAY_URL_SECRET,
4142} from "./config.ts";
4243import * as CloudLink from "./CloudLink.ts";
44import { consumeCloudReplayGuards } from "./CloudLink.ts";
4345import { pendingServiceUpdateExists } from "./updateHandoff.ts";
4446import {
4547 managedTunnelStartupAction,
......@@ -47,6 +49,17 @@ import {
4749} from "./managedTunnelStartup.ts";
4850import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
4951
52const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") =>
53 new ServerSecretStore.SecretStorePersistError({
54 resource: "cloud replay guard",
55 cause: PlatformError.systemError({
56 _tag: tag,
57 module: "FileSystem",
58 method: "open",
59 pathOrDescriptor: "cloud-replay-guard.bin",
60 }),
61 });
62
5063const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");
5164// Linking wakes the awareness relay; these tests do not run it.
5265const idleAwarenessRelay = AgentAwarenessRelay.AgentAwarenessRelay.of({
......@@ -102,6 +115,35 @@ it("preserves messages surfaced by cloud 500 responses", () => {
102115 ]);
103116});
104117
118describe("consumeCloudReplayGuards", () => {
119 it.effect("reports already-created guards as replay conflicts", () =>
120 Effect.gen(function* () {
121 const consumed = yield* consumeCloudReplayGuards({
122 secrets: makeSecretStore(() => Effect.fail(storeFailure("AlreadyExists"))),
123 names: ["cloud-jti", "cloud-nonce"],
124 value: new Uint8Array(),
125 });
126
127 expect(consumed).toBe(false);
128 }),
129 );
130
131 it.effect("preserves replay-store availability failures", () =>
132 Effect.gen(function* () {
133 const failure = storeFailure("PermissionDenied");
134 const error = yield* Effect.flip(
135 consumeCloudReplayGuards({
136 secrets: makeSecretStore(() => Effect.fail(failure)),
137 names: ["cloud-jti", "cloud-nonce"],
138 value: new Uint8Array(),
139 }),
140 );
141
142 expect(error).toBe(failure);
143 }),
144 );
145});
146
105147describe("reconcileDesiredCloudLink", () => {
106148 it.effect("requires stored CLI authorization without exposing an HTTP endpoint", () =>
107149 Effect.gen(function* () {
apps/server/src/cloud/CloudLink.ts+32-5
......@@ -107,11 +107,6 @@ import {
107107 shouldRetryRelayRequest,
108108} from "./relayResponse.ts";
109109import {
110 CLOUD_HEALTH_JTI_PREFIX,
111 CLOUD_HEALTH_NONCE_PREFIX,
112 CLOUD_MINT_JTI_PREFIX,
113 CLOUD_MINT_NONCE_PREFIX,
114 consumeCloudReplayGuards,
115110 hasBoundedCloudProofLifetime,
116111 hasExactScope,
117112 hasForwardedAuthorityHeaders,
......@@ -124,6 +119,17 @@ import {
124119} from "./linkChecks.ts";
125120import { desktopUpdateRestartPending, pendingUpdateHandoffExists } from "./updateHandoff.ts";
126121
122const CLOUD_MINT_NONCE_PREFIX = "cloud-mint-nonce-";
123const CLOUD_MINT_JTI_PREFIX = "cloud-mint-jti-";
124const CLOUD_HEALTH_NONCE_PREFIX = "cloud-health-nonce-";
125const CLOUD_HEALTH_JTI_PREFIX = "cloud-health-jti-";
126/** Secret store name prefixes of cloud replay markers. The server prunes expired ones. */
127export const CLOUD_REPLAY_MARKER_PREFIXES = [
128 CLOUD_MINT_NONCE_PREFIX,
129 CLOUD_MINT_JTI_PREFIX,
130 CLOUD_HEALTH_NONCE_PREFIX,
131 CLOUD_HEALTH_JTI_PREFIX,
132] as const;
127133const MANAGED_ENDPOINT_PROVISION_REQUEST_TIMEOUT = Duration.minutes(2);
128134const RELAY_CONFIG_FIELD_MESSAGES = {
129135 relayUrl: "Relay URL must be a secure absolute HTTPS URL.",
......@@ -325,6 +331,27 @@ function stringToBytes(value: string): Uint8Array {
325331 return new TextEncoder().encode(value);
326332}
327333
334/** Records each one-shot marker; false when any already existed, so the request is a replay. */
335export function consumeCloudReplayGuards(input: {
336 readonly secrets: ServerSecretStore.ServerSecretStore["Service"];
337 readonly names: ReadonlyArray<string>;
338 readonly value: Uint8Array;
339}) {
340 return Effect.forEach(
341 input.names,
342 (name) =>
343 input.secrets.create(name, input.value).pipe(
344 Effect.as(true),
345 Effect.catchIf(ServerSecretStore.isSecretStoreError, (error) =>
346 ServerSecretStore.isSecretAlreadyExistsError(error)
347 ? Effect.succeed(false)
348 : Effect.fail(error),
349 ),
350 ),
351 { concurrency: input.names.length },
352 ).pipe(Effect.map((created) => created.every(Boolean)));
353}
354
328355function validateCloudMintPublicKey(
329356 publicKey: string,
330357): Effect.Effect<void, CloudLinkRelayConfigInvalidError> {
apps/server/src/cloud/linkChecks.test.ts-58
......@@ -1,70 +1,12 @@
11import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as PlatformError from "effect/PlatformError";
42import type { RelayLinkProofRequest } from "@t3tools/contracts/relay";
53
6import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
74import {
8 consumeCloudReplayGuards,
95 isSupportedLinkProviderKind,
106 linkProofScopes,
117 parseManagedEndpointLocalOrigin,
128} from "./linkChecks.ts";
139
14const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") =>
15 new ServerSecretStore.SecretStorePersistError({
16 resource: "cloud replay guard",
17 cause: PlatformError.systemError({
18 _tag: tag,
19 module: "FileSystem",
20 method: "open",
21 pathOrDescriptor: "cloud-replay-guard.bin",
22 }),
23 });
24
25const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");
26
27function makeSecretStore(
28 create: ServerSecretStore.ServerSecretStore["Service"]["create"],
29): ServerSecretStore.ServerSecretStore["Service"] {
30 return {
31 get: unusedSecretStoreOperation,
32 set: unusedSecretStoreOperation,
33 create,
34 getOrCreateRandom: unusedSecretStoreOperation,
35 remove: unusedSecretStoreOperation,
36 };
37}
38
39describe("consumeCloudReplayGuards", () => {
40 it.effect("reports already-created guards as replay conflicts", () =>
41 Effect.gen(function* () {
42 const consumed = yield* consumeCloudReplayGuards({
43 secrets: makeSecretStore(() => Effect.fail(storeFailure("AlreadyExists"))),
44 names: ["cloud-jti", "cloud-nonce"],
45 value: new Uint8Array(),
46 });
47
48 expect(consumed).toBe(false);
49 }),
50 );
51
52 it.effect("preserves replay-store availability failures", () =>
53 Effect.gen(function* () {
54 const failure = storeFailure("PermissionDenied");
55 const error = yield* Effect.flip(
56 consumeCloudReplayGuards({
57 secrets: makeSecretStore(() => Effect.fail(failure)),
58 names: ["cloud-jti", "cloud-nonce"],
59 value: new Uint8Array(),
60 }),
61 );
62
63 expect(error).toBe(failure);
64 }),
65 );
66});
67
6810describe("parseManagedEndpointLocalOrigin", () => {
6911 it.each([
7012 {
apps/server/src/cloud/linkChecks.ts+2-36
......@@ -1,7 +1,7 @@
11/**
22 * Pure checks behind T3 Connect link proofs and relay requests: which local
3 * origin a link may point at, which scopes a proof claims and accepts, and the
4 * one-shot replay guards for signed relay requests.
3 * origin a link may point at, and which scopes and lifetimes a proof claims and
4 * accepts.
55 */
66import type {
77 RelayEnvironmentLinkProofPayload,
......@@ -9,46 +9,12 @@ import type {
99 RelayManagedEndpointOrigin,
1010 RelayManagedEndpointRuntimeConfig,
1111} from "@t3tools/contracts/relay";
12import * as Effect from "effect/Effect";
1312import type { HttpServerRequest } from "effect/http";
1413
15import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
16
17export const CLOUD_MINT_NONCE_PREFIX = "cloud-mint-nonce-";
18export const CLOUD_MINT_JTI_PREFIX = "cloud-mint-jti-";
19export const CLOUD_HEALTH_NONCE_PREFIX = "cloud-health-nonce-";
20export const CLOUD_HEALTH_JTI_PREFIX = "cloud-health-jti-";
21/** Secret store name prefixes of cloud replay markers. The server prunes expired ones. */
22export const CLOUD_REPLAY_MARKER_PREFIXES = [
23 CLOUD_MINT_NONCE_PREFIX,
24 CLOUD_MINT_JTI_PREFIX,
25 CLOUD_HEALTH_NONCE_PREFIX,
26 CLOUD_HEALTH_JTI_PREFIX,
27] as const;
2814const CLOUD_PROOF_MAX_LIFETIME_SECONDS = 5 * 60;
2915const CLOUD_PROOF_CLOCK_SKEW_SECONDS = 60;
3016const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
3117
32export function consumeCloudReplayGuards(input: {
33 readonly secrets: ServerSecretStore.ServerSecretStore["Service"];
34 readonly names: ReadonlyArray<string>;
35 readonly value: Uint8Array;
36}) {
37 return Effect.forEach(
38 input.names,
39 (name) =>
40 input.secrets.create(name, input.value).pipe(
41 Effect.as(true),
42 Effect.catchIf(ServerSecretStore.isSecretStoreError, (error) =>
43 ServerSecretStore.isSecretAlreadyExistsError(error)
44 ? Effect.succeed(false)
45 : Effect.fail(error),
46 ),
47 ),
48 { concurrency: input.names.length },
49 ).pipe(Effect.map((created) => created.every(Boolean)));
50}
51
5218function normalizeHostname(hostname: string): string {
5319 return hostname
5420 .trim()