Skip to content
Source
Sign inSign up
Open History to see the last commit

auth.ts

143 lines5.0 KB

1import type {
2 AuthAccessSnapshot,
3 AuthAccessStreamEvent,
4 AuthAccessStreamSnapshotEvent,
5 AuthCreatePairingCredentialInput,
6 AuthRevokeClientSessionInput,
7 AuthRevokePairingLinkInput,
8} from "@t3tools/contracts";
9import { WS_METHODS } from "@t3tools/contracts";
10import * as Effect from "effect/Effect";
11import type { HttpClient } from "effect/http";
12import * as Stream from "effect/Stream";
13import { Atom } from "effect/reactivity";
14​
15import type { EnvironmentRegistry } from "../connection/registry.ts";
16import { subscribe } from "../rpc/client.ts";
17import { executeConnectedEnvironmentHttpRequest } from "./environmentHttpAuth.ts";
18import { createEnvironmentCommand, createEnvironmentSubscriptionAtomFamily } from "./runtime.ts";
19​
20const ACCESS_REQUEST_TIMEOUT_MS = 8_000;
21​
22export const EMPTY_AUTH_ACCESS_SNAPSHOT: AuthAccessSnapshot = {
23 pairingLinks: [],
24 clientSessions: [],
25};
26​
27function upsertByKey<A>(
28 values: ReadonlyArray<A>,
29 next: A,
30 key: (value: A) => string,
31): ReadonlyArray<A> {
32 const nextKey = key(next);
33 return [...values.filter((value) => key(value) !== nextKey), next];
35​
36export function applyAuthAccessStreamEvent(
37 current: AuthAccessSnapshot,
38 event: AuthAccessStreamEvent,
39): AuthAccessSnapshot {
40 switch (event.type) {
41 case "snapshot":
42 return event.payload;
43 case "pairingLinkUpserted":
44 return {
45 ...current,
46 pairingLinks: upsertByKey(current.pairingLinks, event.payload, (value) => value.id),
47 };
48 case "pairingLinkRemoved":
49 return {
50 ...current,
51 pairingLinks: current.pairingLinks.filter((value) => value.id !== event.payload.id),
52 };
53 case "clientUpserted":
54 return {
55 ...current,
56 clientSessions: upsertByKey(
57 current.clientSessions,
58 event.payload,
59 (value) => value.sessionId,
60 ),
61 };
62 case "clientRemoved":
63 return {
64 ...current,
65 clientSessions: current.clientSessions.filter(
66 (value) => value.sessionId !== event.payload.sessionId,
67 ),
68 };
69 }
71​
72function projectAuthAccessSnapshot(
73 current: AuthAccessSnapshot,
74 event: AuthAccessStreamEvent,
75): readonly [AuthAccessSnapshot, ReadonlyArray<AuthAccessStreamEvent>] {
76 const snapshot = applyAuthAccessStreamEvent(current, event);
77 const projected: AuthAccessStreamSnapshotEvent = {
78 version: 1,
79 revision: event.revision,
80 type: "snapshot",
81 payload: snapshot,
82 };
83 return [snapshot, [projected]];
85​
86/** Access management runs on the chosen environment with this client's own session there. */
87export function createAuthEnvironmentAtoms<R, E>(
88 runtime: Atom.AtomRuntime<EnvironmentRegistry | HttpClient.HttpClient | R, E>,
89) {
90 return {
91 createPairingCredential: createEnvironmentCommand(runtime, {
92 label: "environment-data:server:auth-create-pairing-credential",
93 execute: (input: AuthCreatePairingCredentialInput) =>
94 executeConnectedEnvironmentHttpRequest({
95 group: "auth",
96 method: "POST",
97 url: (urls) => urls.pairingCredential(),
98 timeoutMs: ACCESS_REQUEST_TIMEOUT_MS,
99 request: ({ client, headers }) => client.pairingCredential({ headers, payload: input }),
100 }),
101 }),
102 revokePairingLink: createEnvironmentCommand(runtime, {
103 label: "environment-data:server:auth-revoke-pairing-link",
104 execute: (input: AuthRevokePairingLinkInput) =>
105 executeConnectedEnvironmentHttpRequest({
106 group: "auth",
107 method: "POST",
108 url: (urls) => urls.revokePairingLink(),
109 timeoutMs: ACCESS_REQUEST_TIMEOUT_MS,
110 request: ({ client, headers }) => client.revokePairingLink({ headers, payload: input }),
111 }),
112 }),
113 revokeClient: createEnvironmentCommand(runtime, {
114 label: "environment-data:server:auth-revoke-client",
115 execute: (input: AuthRevokeClientSessionInput) =>
116 executeConnectedEnvironmentHttpRequest({
117 group: "auth",
118 method: "POST",
119 url: (urls) => urls.revokeClient(),
120 timeoutMs: ACCESS_REQUEST_TIMEOUT_MS,
121 request: ({ client, headers }) => client.revokeClient({ headers, payload: input }),
122 }),
123 }),
124 revokeOtherClients: createEnvironmentCommand(runtime, {
125 label: "environment-data:server:auth-revoke-other-clients",
126 execute: (_input: null) =>
127 executeConnectedEnvironmentHttpRequest({
128 group: "auth",
129 method: "POST",
130 url: (urls) => urls.revokeOtherClients(),
131 timeoutMs: ACCESS_REQUEST_TIMEOUT_MS,
132 request: ({ client, headers }) => client.revokeOtherClients({ headers }),
133 }).pipe(Effect.map((result) => result.revokedCount)),
134 }),
135 accessChanges: createEnvironmentSubscriptionAtomFamily(runtime, {
136 label: "environment-data:server:auth-access-changes",
137 subscribe: (_input: null) =>
138 subscribe(WS_METHODS.subscribeAuthAccess, {}).pipe(
139 Stream.mapAccum(() => EMPTY_AUTH_ACCESS_SNAPSHOT, projectAuthAccessSnapshot),
140 ),
141 }),
142 };