1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
3
4import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
5import {
6 CLOUD_ENDPOINT_CONFIRMED_ORIGIN,
7 CLOUD_ENDPOINT_RUNTIME_CONFIG,
8 CLOUD_LINK_REVOKED,
9 CLOUD_LINKED_USER_ID,
10 CLOUD_MINT_PUBLIC_KEY,
11 PUBLISH_AGENT_ACTIVITY_SECRET,
12 RELAY_ENVIRONMENT_CREDENTIAL_SECRET,
13 RELAY_ISSUER_SECRET,
14 RELAY_URL_SECRET,
15} from "./config.ts";
16
17export const CLOUD_CLI_DESIRED_LINK_SECRET = "cloud-cli-desired-link";
18
19// "managed" provisions a Cloudflare tunnel (default, legacy value "true").
20// "publish_only" links the environment to the relay purely to publish agent
21// activity — no tunnel, no relay-advertised endpoint — so activity can flow to
22// mobile clients even when they reach the environment out of band (Tailscale,
23// direct pairing) without T3 Connect.
24export type CliDesiredLinkMode = "managed" | "publish_only";
25
26const MANAGED_BYTES = new TextEncoder().encode("managed");
27const PUBLISH_ONLY_BYTES = new TextEncoder().encode("publish_only");
28
29export const readCliDesiredCloudLink = Effect.gen(function* () {
30 const secrets = yield* ServerSecretStore.ServerSecretStore;
31 return Option.isSome(yield* secrets.get(CLOUD_CLI_DESIRED_LINK_SECRET));
32});
33
34export const readCliDesiredLinkMode = Effect.gen(function* () {
35 const secrets = yield* ServerSecretStore.ServerSecretStore;
36 const value = yield* secrets.get(CLOUD_CLI_DESIRED_LINK_SECRET);
37 if (Option.isNone(value)) {
38 return "managed" as CliDesiredLinkMode;
39 }
40 // Legacy links stored the literal "true" and are always managed.
41 return new TextDecoder().decode(value.value) === "publish_only"
42 ? ("publish_only" as CliDesiredLinkMode)
43 : ("managed" as CliDesiredLinkMode);
44});
45
46export const setCliDesiredCloudLink = Effect.fn("cloud.cli_state.set_desired")(function* (
47 desired: boolean,
48 mode: CliDesiredLinkMode = "managed",
49) {
50 const secrets = yield* ServerSecretStore.ServerSecretStore;
51 if (desired) {
52 yield* secrets.set(
53 CLOUD_CLI_DESIRED_LINK_SECRET,
54 mode === "publish_only" ? PUBLISH_ONLY_BYTES : MANAGED_BYTES,
55 );
56 } else {
57 yield* secrets.remove(CLOUD_CLI_DESIRED_LINK_SECRET);
58 }
59});
60
61/**
62 * Drops a link the relay rejected, keeping the sign-in and preferences, so the
63 * next start links this environment again. `t3 connect link` calls it: the
64 * server never relinks a revoked link on its own. Returns whether it dropped one.
65 */
66export const dropRevokedCloudLink = Effect.gen(function* () {
67 const secrets = yield* ServerSecretStore.ServerSecretStore;
68 if (Option.isNone(yield* secrets.get(CLOUD_LINK_REVOKED))) {
69 return false;
70 }
71 yield* Effect.all(
72 [
73 secrets.remove(CLOUD_LINKED_USER_ID),
74 secrets.remove(RELAY_ENVIRONMENT_CREDENTIAL_SECRET),
75 secrets.remove(CLOUD_MINT_PUBLIC_KEY),
76 secrets.remove(CLOUD_ENDPOINT_RUNTIME_CONFIG),
77 secrets.remove(CLOUD_ENDPOINT_CONFIRMED_ORIGIN),
78 ],
79 { concurrency: "unbounded" },
80 );
81 // Last, so a failed removal above leaves the mark for the next attempt.
82 yield* secrets.remove(CLOUD_LINK_REVOKED);
83 return true;
84});
85
86export const clearPersistedCloudLink = Effect.gen(function* () {
87 const secrets = yield* ServerSecretStore.ServerSecretStore;
88 yield* Effect.all(
89 [
90 secrets.remove(CLOUD_CLI_DESIRED_LINK_SECRET),
91 secrets.remove(CLOUD_LINKED_USER_ID),
92 secrets.remove(RELAY_URL_SECRET),
93 secrets.remove(RELAY_ISSUER_SECRET),
94 secrets.remove(RELAY_ENVIRONMENT_CREDENTIAL_SECRET),
95 secrets.remove(CLOUD_MINT_PUBLIC_KEY),
96 secrets.remove(CLOUD_ENDPOINT_RUNTIME_CONFIG),
97 secrets.remove(CLOUD_ENDPOINT_CONFIRMED_ORIGIN),
98 secrets.remove(PUBLISH_AGENT_ACTIVITY_SECRET),
99 secrets.remove(CLOUD_LINK_REVOKED),
100 ],
101 { concurrency: "unbounded" },
102 );
103});