1import {
2 clientRpcRequiredScopes,
3 sessionGrantsScope,
4 authScopeRequiredResponse,
5 EnvironmentAuthorizationError,
6 type EnvironmentId,
7 type AuthSessionState,
8 type AuthEnvironmentScope,
9} from "@t3tools/contracts";
10import * as Effect from "effect/Effect";
11import * as Option from "effect/Option";
12import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
13import type { EnvironmentRegistry } from "../connection/registry.ts";
14import { createEnvironmentSessionAtoms } from "./session.ts";
15
16function makeCommandPermissions<R, E>(
17 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,
18 method: string,
19) {
20 const sessions = createEnvironmentSessionAtoms(runtime);
21 const requiredScopes = (input?: unknown) => clientRpcRequiredScopes(method, input);
22 const atomsByEnvironment = Atom.family((environmentId: EnvironmentId | null) =>
23 Atom.family((scopeKey: string) =>
24 Atom.make((get) => {
25 if (environmentId === null) return false;
26 const scopes = scopeKey.split(",").filter(Boolean) as AuthEnvironmentScope[];
27 if (scopes.length === 0) return true;
28 const result = get(sessions.sessionStateAtom(environmentId));
29 const session = AsyncResult.getOrElse(result, () => null);
30 return (
31 result._tag !== "Failure" &&
32 session !== null &&
33 scopes.every((scope) => sessionGrantsScope(session, scope))
34 );
35 }),
36 ),
37 );
38 const permissionAtom = (environmentId: EnvironmentId | null, input?: unknown) =>
39 atomsByEnvironment(environmentId)(requiredScopes(input).join(","));
40 const authorize = (
41 registry: AtomRegistry.AtomRegistry,
42 environmentId: EnvironmentId,
43 input?: unknown,
44 ) =>
45 Effect.suspend(() => {
46 const scopes = requiredScopes(input);
47 if (scopes.length === 0) return Effect.void;
48 const atom = sessions.sessionStateAtom(environmentId);
49 return Effect.scoped(
50 Effect.gen(function* () {
51 yield* AtomRegistry.mount(registry, atom);
52 const session = yield* AtomRegistry.getResult(registry, atom, {
53 suspendOnWaiting: false,
54 }).pipe(
55 Effect.timeoutOption(6_000),
56 Effect.orElseSucceed(() => Option.none<AuthSessionState>()),
57 );
58 const missing = scopes.find(
59 (scope) => Option.isNone(session) || !sessionGrantsScope(session.value, scope),
60 );
61 if (missing !== undefined)
62 return yield* new EnvironmentAuthorizationError({
63 ...authScopeRequiredResponse(missing),
64 message: `This connection requires ${missing}.`,
65 });
66 }),
67 );
68 });
69 return { requiredScopes, permissionAtom, authorize };
70}
71
72const permissionsByRuntime = new WeakMap<
73 object,
74 Map<string, ReturnType<typeof makeCommandPermissions>>
75>();
76
77export function createCommandPermissions<R, E>(
78 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,
79 method: string,
80) {
81 let permissions = permissionsByRuntime.get(runtime);
82 if (permissions === undefined) {
83 permissions = new Map();
84 permissionsByRuntime.set(runtime, permissions);
85 }
86 const existing = permissions.get(method);
87 if (existing !== undefined) return existing;
88 const created = makeCommandPermissions(runtime, method);
89 permissions.set(method, created);
90 return created;
91}