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";...@@ -7,7 +7,7 @@ import * as Option from "effect/Option";
7import * as Path from "effect/Path";7import * as Path from "effect/Path";
8import * as Schedule from "effect/Schedule";8import * as Schedule from "effect/Schedule";
99
10import { CLOUD_REPLAY_MARKER_PREFIXES } from "../cloud/linkChecks.ts";10import { CLOUD_REPLAY_MARKER_PREFIXES } from "../cloud/CloudLink.ts";
11import * as ServerConfig from "../config.ts";11import * as ServerConfig from "../config.ts";
12import { forkParked } from "../serverActivation.ts";12import { forkParked } from "../serverActivation.ts";
13import { DPOP_REPLAY_MARKER_PREFIX } from "./dpop.ts";13import { 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";...@@ -8,6 +8,7 @@ import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";9import * as Option from "effect/Option";
10import * as Path from "effect/Path";10import * as Path from "effect/Path";
11import * as PlatformError from "effect/PlatformError";
11import * as Schema from "effect/Schema";12import * as Schema from "effect/Schema";
12import * as TestClock from "effect/testing/TestClock";13import * as TestClock from "effect/testing/TestClock";
13import * as Stream from "effect/Stream";14import * as Stream from "effect/Stream";
...@@ -40,6 +41,7 @@ import {...@@ -40,6 +41,7 @@ import {
40 RELAY_URL_SECRET,41 RELAY_URL_SECRET,
41} from "./config.ts";42} from "./config.ts";
42import * as CloudLink from "./CloudLink.ts";43import * as CloudLink from "./CloudLink.ts";
44import { consumeCloudReplayGuards } from "./CloudLink.ts";
43import { pendingServiceUpdateExists } from "./updateHandoff.ts";45import { pendingServiceUpdateExists } from "./updateHandoff.ts";
44import {46import {
45 managedTunnelStartupAction,47 managedTunnelStartupAction,
...@@ -47,6 +49,17 @@ import {...@@ -47,6 +49,17 @@ import {
47} from "./managedTunnelStartup.ts";49} from "./managedTunnelStartup.ts";
48import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";50import * 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
50const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");63const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");
51// Linking wakes the awareness relay; these tests do not run it.64// Linking wakes the awareness relay; these tests do not run it.
52const idleAwarenessRelay = AgentAwarenessRelay.AgentAwarenessRelay.of({65const idleAwarenessRelay = AgentAwarenessRelay.AgentAwarenessRelay.of({
...@@ -102,6 +115,35 @@ it("preserves messages surfaced by cloud 500 responses", () => {...@@ -102,6 +115,35 @@ it("preserves messages surfaced by cloud 500 responses", () => {
102 ]);115 ]);
103});116});
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
105describe("reconcileDesiredCloudLink", () => {147describe("reconcileDesiredCloudLink", () => {
106 it.effect("requires stored CLI authorization without exposing an HTTP endpoint", () =>148 it.effect("requires stored CLI authorization without exposing an HTTP endpoint", () =>
107 Effect.gen(function* () {149 Effect.gen(function* () {
apps/server/src/cloud/CloudLink.ts+32-5
...@@ -107,11 +107,6 @@ import {...@@ -107,11 +107,6 @@ import {
107 shouldRetryRelayRequest,107 shouldRetryRelayRequest,
108} from "./relayResponse.ts";108} from "./relayResponse.ts";
109import {109import {
110 CLOUD_HEALTH_JTI_PREFIX,
111 CLOUD_HEALTH_NONCE_PREFIX,
112 CLOUD_MINT_JTI_PREFIX,
113 CLOUD_MINT_NONCE_PREFIX,
114 consumeCloudReplayGuards,
115 hasBoundedCloudProofLifetime,110 hasBoundedCloudProofLifetime,
116 hasExactScope,111 hasExactScope,
117 hasForwardedAuthorityHeaders,112 hasForwardedAuthorityHeaders,
...@@ -124,6 +119,17 @@ import {...@@ -124,6 +119,17 @@ import {
124} from "./linkChecks.ts";119} from "./linkChecks.ts";
125import { desktopUpdateRestartPending, pendingUpdateHandoffExists } from "./updateHandoff.ts";120import { 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;
127const MANAGED_ENDPOINT_PROVISION_REQUEST_TIMEOUT = Duration.minutes(2);133const MANAGED_ENDPOINT_PROVISION_REQUEST_TIMEOUT = Duration.minutes(2);
128const RELAY_CONFIG_FIELD_MESSAGES = {134const RELAY_CONFIG_FIELD_MESSAGES = {
129 relayUrl: "Relay URL must be a secure absolute HTTPS URL.",135 relayUrl: "Relay URL must be a secure absolute HTTPS URL.",
...@@ -325,6 +331,27 @@ function stringToBytes(value: string): Uint8Array {...@@ -325,6 +331,27 @@ function stringToBytes(value: string): Uint8Array {
325 return new TextEncoder().encode(value);331 return new TextEncoder().encode(value);
326}332}
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
328function validateCloudMintPublicKey(355function validateCloudMintPublicKey(
329 publicKey: string,356 publicKey: string,
330): Effect.Effect<void, CloudLinkRelayConfigInvalidError> {357): Effect.Effect<void, CloudLinkRelayConfigInvalidError> {
apps/server/src/cloud/linkChecks.test.ts-58
...@@ -1,70 +1,12 @@...@@ -1,70 +1,12 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as PlatformError from "effect/PlatformError";
4import type { RelayLinkProofRequest } from "@t3tools/contracts/relay";2import type { RelayLinkProofRequest } from "@t3tools/contracts/relay";
53
6import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
7import {4import {
8 consumeCloudReplayGuards,
9 isSupportedLinkProviderKind,5 isSupportedLinkProviderKind,
10 linkProofScopes,6 linkProofScopes,
11 parseManagedEndpointLocalOrigin,7 parseManagedEndpointLocalOrigin,
12} from "./linkChecks.ts";8} 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
68describe("parseManagedEndpointLocalOrigin", () => {10describe("parseManagedEndpointLocalOrigin", () => {
69 it.each([11 it.each([
70 {12 {
apps/server/src/cloud/linkChecks.ts+2-36
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1/**1/**
2 * Pure checks behind T3 Connect link proofs and relay requests: which local2 * 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 the3 * origin a link may point at, and which scopes and lifetimes a proof claims and
4 * one-shot replay guards for signed relay requests.4 * accepts.
5 */5 */
6import type {6import type {
7 RelayEnvironmentLinkProofPayload,7 RelayEnvironmentLinkProofPayload,
...@@ -9,46 +9,12 @@ import type {...@@ -9,46 +9,12 @@ import type {
9 RelayManagedEndpointOrigin,9 RelayManagedEndpointOrigin,
10 RelayManagedEndpointRuntimeConfig,10 RelayManagedEndpointRuntimeConfig,
11} from "@t3tools/contracts/relay";11} from "@t3tools/contracts/relay";
12import * as Effect from "effect/Effect";
13import type { HttpServerRequest } from "effect/http";12import 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;
28const CLOUD_PROOF_MAX_LIFETIME_SECONDS = 5 * 60;14const CLOUD_PROOF_MAX_LIFETIME_SECONDS = 5 * 60;
29const CLOUD_PROOF_CLOCK_SKEW_SECONDS = 60;15const CLOUD_PROOF_CLOCK_SKEW_SECONDS = 60;
30const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);16const 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
52function normalizeHostname(hostname: string): string {18function normalizeHostname(hostname: string): string {
53 return hostname19 return hostname
54 .trim()20 .trim()