@superblocksteam/sdk 1.4.2 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ import {
2
+ ApiToSign,
3
+ ApiToVerify,
4
+ AppToSign,
5
+ AppToVerify,
6
+ MethodHandlers,
7
+ RemoteCommitDto,
8
+ Signature,
9
+ } from "../types";
10
+ import { signResource, verifyResources } from "./signing";
11
+
12
+ type MethodSchema<Params, Response> = (params: Params) => Promise<Response>;
13
+
14
+ export interface ClientMethods {
15
+ v1: {
16
+ signing: {
17
+ signApplication: MethodSchema<
18
+ { branchName: string; toSign: AppToSign },
19
+ { signature: Signature }
20
+ >;
21
+ signApis: MethodSchema<
22
+ { branchName: string; toSign: ApiToSign[] },
23
+ { signatures: Signature[] }
24
+ >;
25
+ verifyApplication: MethodSchema<
26
+ { branchName: string; toVerify: AppToVerify },
27
+ { ok: boolean }
28
+ >;
29
+ verifyApi: MethodSchema<
30
+ { branchName: string; toVerify: ApiToVerify[] },
31
+ { ok: boolean }
32
+ >;
33
+ };
34
+ };
35
+ }
36
+
37
+ type ServerMethodSchema<Params, Response> = MethodSchema<
38
+ Params,
39
+ ResponseDto<Response>
40
+ >;
41
+
42
+ // This file contains the definition of the protocol used for communication between the SB server and clients
43
+
44
+ type ResponseDto<T> = {
45
+ responseMeta: ResponseMeta;
46
+ data: T;
47
+ };
48
+
49
+ export type ResponseMeta = {
50
+ status: number;
51
+ success: boolean;
52
+ error?: APIResponseError;
53
+ };
54
+
55
+ type APIResponseError = {
56
+ code: number;
57
+ message: string;
58
+ };
59
+
60
+ export interface ServerMethods {
61
+ v1: {
62
+ echo: MethodSchema<{ message: string }, { message: string }>;
63
+ public: {
64
+ application: {
65
+ component: {
66
+ register: ServerMethodSchema<
67
+ {
68
+ applicationId: string;
69
+ branchName: string;
70
+ cliVersion: string;
71
+ componentEvent: string;
72
+ components: Record<string, unknown>;
73
+ },
74
+ { success: boolean }
75
+ >;
76
+ update: ServerMethodSchema<
77
+ {
78
+ applicationId: string;
79
+ branchName?: string;
80
+ srcFiles: string[];
81
+ buildFiles: string[];
82
+ registeredComponents: Record<string, unknown>;
83
+ cliVersion: string | undefined;
84
+ componentBaseUrl: string;
85
+ signingRequired: boolean;
86
+ },
87
+ { success: boolean }
88
+ >;
89
+ };
90
+ pushCommit: ServerMethodSchema<
91
+ {
92
+ applicationId: string;
93
+ branchName: string;
94
+ commitId: string;
95
+ commitMessage: string;
96
+ application: Record<string, unknown>;
97
+ page: Record<string, unknown>;
98
+ apis: Record<string, unknown>[];
99
+ },
100
+ RemoteCommitDto
101
+ >;
102
+ };
103
+ api: {
104
+ pushCommit: ServerMethodSchema<
105
+ {
106
+ apiId: string;
107
+ branchName: string;
108
+ commitId: string;
109
+ commitMessage: string;
110
+ apiPb: Record<string, unknown>;
111
+ },
112
+ RemoteCommitDto
113
+ >;
114
+ };
115
+ };
116
+ };
117
+ }
118
+
119
+ export function createRequestHandlers({
120
+ agentUrl,
121
+ token,
122
+ }: {
123
+ token: string;
124
+ agentUrl?: string;
125
+ }) {
126
+ const requestHandlers: MethodHandlers<ClientMethods, ServerMethods, unknown> =
127
+ {
128
+ v1: {
129
+ signing: {
130
+ signApplication: [
131
+ async ({
132
+ branchName,
133
+ toSign,
134
+ }: {
135
+ branchName: string;
136
+ toSign: AppToSign;
137
+ }) => {
138
+ if (!agentUrl) {
139
+ throw new Error(
140
+ "Agent url not specified. This shouldn't happen."
141
+ );
142
+ }
143
+ const signature = await signResource({
144
+ agentUrl,
145
+ token: token,
146
+ branchName,
147
+ resource: {
148
+ literal: {
149
+ data: toSign.rootHash,
150
+ },
151
+ },
152
+ });
153
+ return { signature: signature };
154
+ },
155
+ ],
156
+ signApis: [
157
+ async ({
158
+ branchName,
159
+ toSign,
160
+ }: {
161
+ branchName: string;
162
+ toSign: ApiToSign[];
163
+ }) => {
164
+ const signatures: Signature[] = [];
165
+ for (const { apiPb } of toSign) {
166
+ if (!agentUrl) {
167
+ throw new Error(
168
+ "Agent url not specified. This shouldn't happen."
169
+ );
170
+ }
171
+ const signature = await signResource({
172
+ agentUrl,
173
+ token: token,
174
+ branchName,
175
+ resource: { api: apiPb },
176
+ });
177
+ signatures.push(signature);
178
+ }
179
+ return { signatures };
180
+ },
181
+ ],
182
+ verifyApplication: [
183
+ async ({
184
+ branchName,
185
+ toVerify,
186
+ }: {
187
+ branchName: string;
188
+ toVerify: AppToVerify;
189
+ }) => {
190
+ try {
191
+ if (!agentUrl) {
192
+ throw new Error(
193
+ "Agent url not specified. This shouldn't happen."
194
+ );
195
+ }
196
+ await verifyResources({
197
+ agentUrl,
198
+ token,
199
+ branchName,
200
+ resources: [
201
+ {
202
+ literal: {
203
+ data: toVerify.rootHash,
204
+ signature: toVerify.signature,
205
+ },
206
+ },
207
+ ],
208
+ });
209
+ return { ok: true };
210
+ } catch {
211
+ return { ok: false };
212
+ }
213
+ },
214
+ ],
215
+
216
+ verifyApi: [
217
+ async ({
218
+ branchName,
219
+ toVerify,
220
+ }: {
221
+ branchName: string;
222
+ toVerify: ApiToVerify[];
223
+ }) => {
224
+ try {
225
+ if (!agentUrl) {
226
+ throw new Error(
227
+ "Agent url not specified. This shouldn't happen."
228
+ );
229
+ }
230
+ await verifyResources({
231
+ agentUrl,
232
+ token,
233
+ branchName,
234
+ resources: toVerify.map(({ apiPb }) => ({
235
+ api: apiPb,
236
+ })),
237
+ });
238
+ return { ok: true };
239
+ } catch {
240
+ return { ok: false };
241
+ }
242
+ },
243
+ ],
244
+ },
245
+ },
246
+ };
247
+ return requestHandlers;
248
+ }
@@ -0,0 +1,164 @@
1
+ import WebSocket from "isomorphic-ws";
2
+ import {
3
+ ClientMethods,
4
+ createRequestHandlers,
5
+ ServerMethods,
6
+ } from "./handlers";
7
+ import { createISocketClient, ISocket } from "./socket";
8
+
9
+ export type StdISocketRPCClient = ISocketClient<ServerMethods>;
10
+
11
+ export async function connectToISocketRPCServer({
12
+ superblocksBaseUrl,
13
+ agentUrl,
14
+ token,
15
+ }: {
16
+ superblocksBaseUrl: string;
17
+ token: string;
18
+ agentUrl?: string;
19
+ }): Promise<StdISocketRPCClient> {
20
+ const requestHandlers = createRequestHandlers({
21
+ agentUrl,
22
+ token,
23
+ });
24
+ const authorization = `Bearer ${token}`;
25
+ const wsUrl = new URL("api/v1/rpc-ws", superblocksBaseUrl);
26
+ if (wsUrl.protocol === "http:") {
27
+ wsUrl.protocol = "ws:";
28
+ } else if (wsUrl.protocol === "https:") {
29
+ wsUrl.protocol = "wss:";
30
+ }
31
+
32
+ if (wsUrl.host === "localhost:3000") {
33
+ wsUrl.host = "127.0.0.1:8080";
34
+ } else if (wsUrl.hostname === "localhost") {
35
+ wsUrl.hostname = "127.0.0.1";
36
+ }
37
+ return await connectISocket<ServerMethods, ClientMethods, unknown>(
38
+ wsUrl.href,
39
+ authorization,
40
+ requestHandlers
41
+ );
42
+ }
43
+
44
+ // a subclass of ISocket that sends an auth token on the first request
45
+ // this is useful for client-side sockets that need to authenticate
46
+ // TODO(george): if we start using this for long-lived connections, we should add a way to refresh the token
47
+ class ISocketWithClientAuth<
48
+ ImplementedMethods,
49
+ CallableMethods,
50
+ RequestContext = void
51
+ > extends ISocket<ImplementedMethods, CallableMethods, RequestContext> {
52
+ private readonly authorization?: string;
53
+ private hasSentAuth = false;
54
+
55
+ constructor(
56
+ ws: WebSocket,
57
+ authorization: string | undefined,
58
+ requestHandlers: MethodHandlers<
59
+ ImplementedMethods,
60
+ CallableMethods,
61
+ RequestContext
62
+ >
63
+ ) {
64
+ super(ws, requestHandlers);
65
+ this.authorization = authorization;
66
+ }
67
+
68
+ // override `request` from the base class to send `authorization` when appropriate
69
+ async request<Params, Result>(
70
+ method: string,
71
+ params: Params
72
+ ): Promise<Result> {
73
+ // only send `authorization` on the first request
74
+ const authorization = this.hasSentAuth ? undefined : this.authorization;
75
+ const result = await super.request<Params, Result>(
76
+ method,
77
+ params,
78
+ authorization
79
+ );
80
+ this.hasSentAuth = true;
81
+ return result;
82
+ }
83
+ }
84
+
85
+ export async function connectISocket<
86
+ CallableMethods,
87
+ ImplementedMethods,
88
+ RequestContext = never
89
+ >(
90
+ wsUrl: string,
91
+ authorization: string | undefined,
92
+ requestHandlers: MethodHandlers<
93
+ ImplementedMethods,
94
+ CallableMethods,
95
+ RequestContext
96
+ >
97
+ ): Promise<ISocketClient<CallableMethods>> {
98
+ const ws = await connectWebSocket(wsUrl);
99
+ const isocket = new ISocketWithClientAuth(ws, authorization, requestHandlers);
100
+ return createISocketClient(isocket);
101
+ }
102
+
103
+ export function connectWebSocket(wsUrl: string): Promise<WebSocket> {
104
+ return new Promise((resolve, reject) => {
105
+ const ws = new WebSocket(wsUrl);
106
+
107
+ ws.addEventListener("open", () => {
108
+ // Resolve the promise with the WebSocket instance when the connection is open
109
+ resolve(ws);
110
+ });
111
+
112
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
113
+ // @ts-ignore
114
+ ws.addEventListener("error", (error: Error) => {
115
+ // Reject the promise if there's an error
116
+ reject(error);
117
+ });
118
+ });
119
+ }
120
+
121
+ type MethodHandler<Params, Result, PeerMethods, RequestContext> = (
122
+ params: Params,
123
+ peer: ISocketClient<PeerMethods>,
124
+ ctx: RequestContext
125
+ ) => Promise<Result>;
126
+
127
+ type MiddlewareHandler<Params, PeerMethods, RequestContext> = (
128
+ params: Params,
129
+ peerAuthorization: string | undefined,
130
+ peer: ISocketClient<PeerMethods>,
131
+ ctx: RequestContext
132
+ ) => Promise<void>;
133
+
134
+ type MethodHandlers<Methods, PeerMethods, RequestContext> = {
135
+ [Key in keyof Methods]: Methods[Key] extends (
136
+ params: infer Params
137
+ ) => Promise<infer Result>
138
+ ? [
139
+ ...middlewareHandlers: MiddlewareHandler<
140
+ Params,
141
+ PeerMethods,
142
+ RequestContext
143
+ >[],
144
+ handler: MethodHandler<Params, Result, PeerMethods, RequestContext>
145
+ ]
146
+ : Methods[Key] extends Record<string, unknown>
147
+ ? MethodHandlers<Methods[Key], PeerMethods, RequestContext>
148
+ : never;
149
+ };
150
+
151
+ type ISocketClientMethodCall<Methods> = {
152
+ [Key in keyof Methods]: Methods[Key] extends (
153
+ params: infer P
154
+ ) => Promise<infer R>
155
+ ? (params: P) => Promise<R>
156
+ : Methods[Key] extends Record<string, unknown>
157
+ ? ISocketClientMethodCall<Methods[Key]>
158
+ : never;
159
+ };
160
+
161
+ type ISocketClient<Methods> = {
162
+ close: () => void;
163
+ call: ISocketClientMethodCall<Methods>;
164
+ };
@@ -0,0 +1,104 @@
1
+ import axios, { AxiosRequestConfig, Method } from "axios";
2
+ import {
3
+ ApiResource,
4
+ GenericResource,
5
+ Signature,
6
+ SignatureResponse,
7
+ } from "../types";
8
+ import { getSanitizedApi } from "../utils";
9
+
10
+ export async function signResource({
11
+ token,
12
+ branchName,
13
+ resource,
14
+ agentUrl,
15
+ }: {
16
+ token: string;
17
+ branchName: string;
18
+ resource: ApiResource | GenericResource;
19
+ agentUrl: string;
20
+ }): Promise<Signature> {
21
+ const requestResource: Record<string, any> = {
22
+ branchName: branchName ?? "main",
23
+ };
24
+ if ((resource as ApiResource).api) {
25
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
26
+ requestResource.api = getSanitizedApi((resource as ApiResource).api);
27
+ } else {
28
+ requestResource.literal = (resource as GenericResource).literal;
29
+ }
30
+
31
+ const resp = await callAgent<SignatureResponse>({
32
+ baseUrl: agentUrl,
33
+ path: "v1/signature/sign",
34
+ method: "post",
35
+ token: token,
36
+ data: { resource: requestResource },
37
+ });
38
+ return resp.signature;
39
+ }
40
+
41
+ export async function verifyResources({
42
+ agentUrl,
43
+ token,
44
+ branchName,
45
+ resources,
46
+ }: {
47
+ resources: Array<GenericResource | ApiResource>;
48
+ token: string;
49
+ branchName: string;
50
+ agentUrl: string;
51
+ }): Promise<void> {
52
+ await callAgent<{ keyId: string }>({
53
+ baseUrl: agentUrl,
54
+ path: "v1/signature/verify",
55
+ method: "post",
56
+ token: token,
57
+ data: {
58
+ resources: resources.map((res) => {
59
+ if ((res as ApiResource).api) {
60
+ return {
61
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
62
+ api: getSanitizedApi((res as ApiResource).api),
63
+ branchName: branchName ?? "main",
64
+ };
65
+ }
66
+ return {
67
+ ...res,
68
+ branchName: branchName ?? "main",
69
+ };
70
+ }),
71
+ },
72
+ });
73
+ }
74
+
75
+ async function callAgent<T>({
76
+ baseUrl,
77
+ path,
78
+ method,
79
+ token,
80
+ data,
81
+ }: {
82
+ baseUrl: string;
83
+ path: string;
84
+ method: Method;
85
+ token: string;
86
+ data: any;
87
+ }): Promise<T> {
88
+ try {
89
+ const url = new URL(path, baseUrl);
90
+ const config: AxiosRequestConfig = {
91
+ url: url.toString(),
92
+ method: method,
93
+ headers: {
94
+ Authorization: "Bearer " + token,
95
+ },
96
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
97
+ data: data,
98
+ };
99
+ const resp = await axios<T>(config);
100
+ return resp.data;
101
+ } catch (error) {
102
+ throw new Error(`Failed to request the agent ${baseUrl}. Error: ${error}`);
103
+ }
104
+ }