1import {
2 AuthAdministrativeScopes,
3 AuthSessionId,
4 AuthStandardClientScopes,
5} from "@t3tools/contracts";
6import * as Console from "effect/Console";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as References from "effect/References";
11import { FetchHttpClient } from "effect/http";
12import { Argument, Command, Flag, GlobalFlag } from "effect/cli";
13
14import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
15import * as HostedAuth from "../auth/HostedAuth.ts";
16import * as HostedAuthConfig from "../auth/HostedAuthConfig.ts";
17
18import {
19 formatIssuedPairingCredential,
20 formatIssuedSession,
21 formatPairingCredentialList,
22 formatSessionList,
23} from "../cliAuthFormat.ts";
24import * as ServerConfig from "../config.ts";
25import { authScopesFlag } from "./authScopes.ts";
26import {
27 authLocationFlags,
28 type CliAuthLocationFlags,
29 DurationFromString,
30 resolveCliAuthConfig,
31} from "./config.ts";
32
33const runWithEnvironmentAuth = <A, E>(
34 flags: CliAuthLocationFlags,
35 run: (environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"]) => Effect.Effect<A, E>,
36 options?: {
37 readonly quietLogs?: boolean;
38 },
39) =>
40 Effect.gen(function* () {
41 const logLevel = yield* GlobalFlag.LogLevel;
42 const config = yield* resolveCliAuthConfig(flags, logLevel);
43 const minimumLogLevel = options?.quietLogs ? "Error" : config.logLevel;
44 return yield* Effect.gen(function* () {
45 const environmentAuth = yield* EnvironmentAuth.EnvironmentAuth;
46 return yield* run(environmentAuth);
47 }).pipe(
48 Effect.provide(
49 Layer.mergeAll(EnvironmentAuth.layerRuntime).pipe(
50 Layer.provide(ServerConfig.layer(config)),
51 Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
52 ),
53 ),
54 );
55 });
56
57const ttlFlag = Flag.String("ttl").pipe(
58 Flag.withSchema(DurationFromString),
59 Flag.withDescription("TTL, for example `5m`, `1h`, `30d`, or `15 minutes`."),
60 Flag.optional,
61);
62
63const jsonFlag = Flag.Boolean("json").pipe(
64 Flag.withDescription("Emit JSON instead of human-readable output."),
65 Flag.withDefault(false),
66);
67
68const labelFlag = Flag.String("label").pipe(
69 Flag.withDescription("Optional human-readable label."),
70 Flag.optional,
71);
72
73const subjectFlag = Flag.String("subject").pipe(
74 Flag.withDescription("Optional session subject."),
75 Flag.optional,
76);
77
78const baseUrlFlag = Flag.String("base-url").pipe(
79 Flag.withDescription("Optional public base URL used to print a ready `/pair#token=...` link."),
80 Flag.optional,
81);
82
83const tokenOnlyFlag = Flag.Boolean("token-only").pipe(
84 Flag.withDescription("Print only the issued bearer token."),
85 Flag.withDefault(false),
86);
87
88const pairingCreateCommand = Command.make("create", {
89 ...authLocationFlags,
90 scopes: authScopesFlag(AuthStandardClientScopes),
91 ttl: ttlFlag,
92 label: labelFlag,
93 baseUrl: baseUrlFlag,
94 json: jsonFlag,
95}).pipe(
96 Command.withDescription("Issue a new client pairing token."),
97 Command.withHandler((flags) =>
98 runWithEnvironmentAuth(
99 flags,
100 (environmentAuth) =>
101 Effect.gen(function* () {
102 const issued = yield* environmentAuth.createPairingLink({
103 scopes: flags.scopes,
104 subject: "one-time-token",
105 ...(Option.isSome(flags.ttl) ? { ttl: flags.ttl.value } : {}),
106 ...(Option.isSome(flags.label) ? { label: flags.label.value } : {}),
107 });
108 const output = formatIssuedPairingCredential(issued, {
109 json: flags.json,
110 ...(Option.isSome(flags.baseUrl) ? { baseUrl: flags.baseUrl.value } : {}),
111 });
112 yield* Console.log(output);
113 }),
114 {
115 quietLogs: flags.json,
116 },
117 ),
118 ),
119);
120
121const pairingListCommand = Command.make("list", {
122 ...authLocationFlags,
123 json: jsonFlag,
124}).pipe(
125 Command.withDescription("List active client pairing tokens without revealing their secrets."),
126 Command.withHandler((flags) =>
127 runWithEnvironmentAuth(
128 flags,
129 (environmentAuth) =>
130 Effect.gen(function* () {
131 const pairingLinks = yield* environmentAuth.listPairingLinks({
132 excludeSubjects: [EnvironmentAuth.INTERNAL_ADMINISTRATIVE_BOOTSTRAP_SUBJECT],
133 });
134 yield* Console.log(formatPairingCredentialList(pairingLinks, { json: flags.json }));
135 }),
136 {
137 quietLogs: flags.json,
138 },
139 ),
140 ),
141);
142
143const pairingRevokeCommand = Command.make("revoke", {
144 ...authLocationFlags,
145 id: Argument.String("id").pipe(Argument.withDescription("Pairing credential id to revoke.")),
146}).pipe(
147 Command.withDescription("Revoke an active client pairing token."),
148 Command.withHandler((flags) =>
149 runWithEnvironmentAuth(flags, (environmentAuth) =>
150 Effect.gen(function* () {
151 const revoked = yield* environmentAuth.revokePairingLink(flags.id);
152 yield* Console.log(
153 revoked
154 ? `Revoked pairing credential ${flags.id}.\n`
155 : `No active pairing credential found for ${flags.id}.\n`,
156 );
157 }),
158 ),
159 ),
160);
161
162const pairingCommand = Command.make("pairing").pipe(
163 Command.withDescription("Manage one-time client pairing tokens."),
164 Command.withSubcommands([pairingCreateCommand, pairingListCommand, pairingRevokeCommand]),
165);
166
167const sessionIssueCommand = Command.make("issue", {
168 ...authLocationFlags,
169 scopes: authScopesFlag(AuthAdministrativeScopes),
170 ttl: ttlFlag,
171 label: labelFlag,
172 subject: subjectFlag,
173 tokenOnly: tokenOnlyFlag,
174 json: jsonFlag,
175}).pipe(
176 Command.withDescription("Issue a scoped bearer access token for headless or remote clients."),
177 Command.withHandler((flags) =>
178 runWithEnvironmentAuth(
179 flags,
180 (environmentAuth) =>
181 Effect.gen(function* () {
182 const issued = yield* environmentAuth.issueSession({
183 scopes: flags.scopes,
184 ...(Option.isSome(flags.ttl) ? { ttl: flags.ttl.value } : {}),
185 ...(Option.isSome(flags.label) ? { label: flags.label.value } : {}),
186 ...(Option.isSome(flags.subject) ? { subject: flags.subject.value } : {}),
187 });
188 yield* Console.log(
189 formatIssuedSession(issued, {
190 json: flags.json,
191 tokenOnly: flags.tokenOnly,
192 }),
193 );
194 }),
195 {
196 quietLogs: flags.json || flags.tokenOnly,
197 },
198 ),
199 ),
200);
201
202const sessionListCommand = Command.make("list", {
203 ...authLocationFlags,
204 json: jsonFlag,
205}).pipe(
206 Command.withDescription("List active sessions without revealing bearer tokens."),
207 Command.withHandler((flags) =>
208 runWithEnvironmentAuth(
209 flags,
210 (environmentAuth) =>
211 Effect.gen(function* () {
212 const sessions = yield* environmentAuth.listSessions();
213 yield* Console.log(formatSessionList(sessions, { json: flags.json }));
214 }),
215 {
216 quietLogs: flags.json,
217 },
218 ),
219 ),
220);
221
222const sessionRevokeCommand = Command.make("revoke", {
223 ...authLocationFlags,
224 sessionId: Argument.String("session-id").pipe(
225 Argument.withDescription("Session id to revoke."),
226 Argument.withSchema(AuthSessionId),
227 ),
228}).pipe(
229 Command.withDescription("Revoke an active session."),
230 Command.withHandler((flags) =>
231 runWithEnvironmentAuth(flags, (environmentAuth) =>
232 Effect.gen(function* () {
233 const revoked = yield* environmentAuth.revokeSession(flags.sessionId);
234 yield* Console.log(
235 revoked
236 ? `Revoked session ${flags.sessionId}.\n`
237 : `No active session found for ${flags.sessionId}.\n`,
238 );
239 }),
240 ),
241 ),
242);
243
244const sessionCommand = Command.make("session").pipe(
245 Command.withDescription("Manage bearer sessions."),
246 Command.withSubcommands([sessionIssueCommand, sessionListCommand, sessionRevokeCommand]),
247);
248
249const devicesCommand = Command.make("devices").pipe(
250 Command.withSubcommands([
251 Command.make("revoke-all", authLocationFlags).pipe(
252 Command.withDescription("Revoke all SSO devices and sessions using local server access."),
253 Command.withHandler((flags) =>
254 Effect.gen(function* () {
255 const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
256 const count = yield* Effect.gen(function* () {
257 return yield* (yield* HostedAuth.HostedAuth).emergencyRevokeAll;
258 }).pipe(
259 Effect.provide(
260 HostedAuth.layer.pipe(
261 Layer.provide(EnvironmentAuth.layerRuntime),
262 Layer.provide(ServerConfig.layer(config)),
263 Layer.provide(Layer.succeed(HostedAuthConfig.HostedAuthConfig, Option.none())),
264 Layer.provide(FetchHttpClient.layer),
265 ),
266 ),
267 );
268 yield* Console.log(
269 `Revoked ${count} SSO devices. Existing pairing credentials remain valid.`,
270 );
271 }),
272 ),
273 ),
274 ]),
275);
276
277export const authCommand = Command.make("auth").pipe(
278 Command.withDescription("Manage the local auth control plane for headless deployments."),
279 Command.withSubcommands([pairingCommand, sessionCommand, devicesCommand]),
280);