@valbuild/server 0.12.0 → 0.13.3
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.
- package/jest.config.js +4 -0
- package/package.json +5 -3
- package/src/LocalValServer.ts +94 -0
- package/src/ProxyValServer.ts +403 -0
- package/src/SerializedModuleContent.ts +8 -0
- package/src/Service.ts +108 -0
- package/src/ValFS.ts +22 -0
- package/src/ValFSHost.ts +66 -0
- package/src/ValModuleLoader.test.ts +75 -0
- package/src/ValModuleLoader.ts +128 -0
- package/src/ValQuickJSRuntime.ts +47 -0
- package/src/ValServer.ts +23 -0
- package/src/ValSourceFileHandler.ts +57 -0
- package/src/createRequestHandler.ts +24 -0
- package/src/expressHelpers.ts +5 -0
- package/src/getCompilerOptions.ts +50 -0
- package/src/hosting.ts +156 -0
- package/src/index.ts +12 -0
- package/src/jwt.ts +83 -0
- package/src/patch/ts/ops.test.ts +820 -0
- package/src/patch/ts/ops.ts +803 -0
- package/src/patch/ts/syntax.ts +371 -0
- package/src/patch/ts/valModule.test.ts +26 -0
- package/src/patch/ts/valModule.ts +110 -0
- package/src/patch/validation.ts +73 -0
- package/src/patchValFile.ts +102 -0
- package/src/readValFile.test.ts +49 -0
- package/src/readValFile.ts +73 -0
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/server",
|
|
3
|
+
"private": false,
|
|
4
|
+
"description": "Val - integrated server",
|
|
3
5
|
"main": "dist/valbuild-server.cjs.js",
|
|
4
6
|
"module": "dist/valbuild-server.esm.js",
|
|
5
7
|
"exports": {
|
|
@@ -10,7 +12,7 @@
|
|
|
10
12
|
"./package.json": "./package.json"
|
|
11
13
|
},
|
|
12
14
|
"types": "dist/valbuild-server.cjs.d.ts",
|
|
13
|
-
"version": "0.
|
|
15
|
+
"version": "0.13.3",
|
|
14
16
|
"scripts": {
|
|
15
17
|
"typecheck": "tsc --noEmit",
|
|
16
18
|
"test": "jest",
|
|
@@ -23,8 +25,8 @@
|
|
|
23
25
|
"concurrently": "^7.6.0"
|
|
24
26
|
},
|
|
25
27
|
"dependencies": {
|
|
26
|
-
"@valbuild/core": "~0.
|
|
27
|
-
"@valbuild/ui": "~0.
|
|
28
|
+
"@valbuild/core": "~0.13.3",
|
|
29
|
+
"@valbuild/ui": "~0.13.3",
|
|
28
30
|
"express": "^4.18.2",
|
|
29
31
|
"quickjs-emscripten": "^0.21.1",
|
|
30
32
|
"ts-morph": "^17.0.1",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { Service } from "./Service";
|
|
3
|
+
import { PatchJSON } from "./patch/validation";
|
|
4
|
+
import { result } from "@valbuild/core/fp";
|
|
5
|
+
import { parsePatch, PatchError } from "@valbuild/core/patch";
|
|
6
|
+
import { getPathFromParams } from "./expressHelpers";
|
|
7
|
+
import { ValServer } from "./ValServer";
|
|
8
|
+
import { Internal } from "@valbuild/core";
|
|
9
|
+
|
|
10
|
+
export type LocalValServerOptions = {
|
|
11
|
+
service: Service;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class LocalValServer implements ValServer {
|
|
15
|
+
constructor(readonly options: LocalValServerOptions) {}
|
|
16
|
+
|
|
17
|
+
async session(_req: express.Request, res: express.Response): Promise<void> {
|
|
18
|
+
res.json({
|
|
19
|
+
mode: "local",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async getIds(
|
|
24
|
+
req: express.Request<{ 0: string }>,
|
|
25
|
+
res: express.Response
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
console.log(req.params);
|
|
29
|
+
const path = getPathFromParams(req.params);
|
|
30
|
+
const [moduleId, modulePath] = Internal.splitModuleIdAndModulePath(path);
|
|
31
|
+
const valModule = await this.options.service.get(moduleId, modulePath);
|
|
32
|
+
|
|
33
|
+
res.json(valModule);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(err);
|
|
36
|
+
res.sendStatus(500);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async patchIds(
|
|
41
|
+
req: express.Request<{ 0: string }>,
|
|
42
|
+
res: express.Response
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
// First validate that the body has the right structure
|
|
45
|
+
const patchJSON = PatchJSON.safeParse(req.body);
|
|
46
|
+
if (!patchJSON.success) {
|
|
47
|
+
res.status(401).json(patchJSON.error.issues);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Then parse/validate
|
|
51
|
+
const patch = parsePatch(patchJSON.data);
|
|
52
|
+
if (result.isErr(patch)) {
|
|
53
|
+
res.status(401).json(patch.error);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const id = getPathFromParams(req.params);
|
|
57
|
+
try {
|
|
58
|
+
const valModule = await this.options.service.patch(id, patch.value);
|
|
59
|
+
res.json(valModule);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err instanceof PatchError) {
|
|
62
|
+
res.status(401).send(err.message);
|
|
63
|
+
} else {
|
|
64
|
+
console.error(err);
|
|
65
|
+
res
|
|
66
|
+
.status(500)
|
|
67
|
+
.send(err instanceof Error ? err.message : "Unknown error");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
private async badRequest(
|
|
72
|
+
req: express.Request,
|
|
73
|
+
res: express.Response
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
console.debug("Local server does handle this request", req.url);
|
|
76
|
+
res.sendStatus(400);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
commit(req: express.Request, res: express.Response): Promise<void> {
|
|
80
|
+
return this.badRequest(req, res);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
authorize(req: express.Request, res: express.Response): Promise<void> {
|
|
84
|
+
return this.badRequest(req, res);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
callback(req: express.Request, res: express.Response): Promise<void> {
|
|
88
|
+
return this.badRequest(req, res);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
logout(req: express.Request, res: express.Response): Promise<void> {
|
|
92
|
+
return this.badRequest(req, res);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { decodeJwt, encodeJwt, getExpire } from "./jwt";
|
|
4
|
+
import { PatchJSON } from "./patch/validation";
|
|
5
|
+
import { result } from "@valbuild/core/fp";
|
|
6
|
+
import { getPathFromParams } from "./expressHelpers";
|
|
7
|
+
import { ValServer } from "./ValServer";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { parsePatch } from "@valbuild/core/patch";
|
|
10
|
+
|
|
11
|
+
const VAL_SESSION_COOKIE = "val_session";
|
|
12
|
+
const VAL_STATE_COOKIE = "val_state";
|
|
13
|
+
|
|
14
|
+
export type ProxyValServerOptions = {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
route: string;
|
|
17
|
+
valSecret: string;
|
|
18
|
+
valBuildUrl: string;
|
|
19
|
+
gitCommit: string;
|
|
20
|
+
gitBranch: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class ProxyValServer implements ValServer {
|
|
24
|
+
constructor(readonly options: ProxyValServerOptions) {}
|
|
25
|
+
|
|
26
|
+
async authorize(req: express.Request, res: express.Response): Promise<void> {
|
|
27
|
+
const { redirect_to } = req.query;
|
|
28
|
+
if (typeof redirect_to !== "string") {
|
|
29
|
+
res.redirect(
|
|
30
|
+
this.getAppErrorUrl("Login failed: missing redirect_to param")
|
|
31
|
+
);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const token = crypto.randomUUID();
|
|
35
|
+
const redirectUrl = new URL(redirect_to);
|
|
36
|
+
const appAuthorizeUrl = this.getAuthorizeUrl(
|
|
37
|
+
`${redirectUrl.origin}/${this.options.route}`,
|
|
38
|
+
token
|
|
39
|
+
);
|
|
40
|
+
res
|
|
41
|
+
.cookie(VAL_STATE_COOKIE, createStateCookie({ redirect_to, token }), {
|
|
42
|
+
httpOnly: true,
|
|
43
|
+
sameSite: "lax",
|
|
44
|
+
expires: new Date(Date.now() + 1000 * 60 * 60), // 1 hour
|
|
45
|
+
})
|
|
46
|
+
.redirect(appAuthorizeUrl);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async callback(req: express.Request, res: express.Response): Promise<void> {
|
|
50
|
+
const { success: callbackReqSuccess, error: callbackReqError } =
|
|
51
|
+
verifyCallbackReq(req.cookies[VAL_STATE_COOKIE], req.query);
|
|
52
|
+
res.clearCookie(VAL_STATE_COOKIE); // we don't need this anymore
|
|
53
|
+
|
|
54
|
+
if (callbackReqError !== null) {
|
|
55
|
+
res.redirect(
|
|
56
|
+
this.getAppErrorUrl(
|
|
57
|
+
`Authorization callback failed. Details: ${callbackReqError}`
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = await this.consumeCode(callbackReqSuccess.code);
|
|
64
|
+
if (data === null) {
|
|
65
|
+
res.redirect(this.getAppErrorUrl("Failed to exchange code for user"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const exp = getExpire();
|
|
69
|
+
const cookie = encodeJwt(
|
|
70
|
+
{
|
|
71
|
+
...data,
|
|
72
|
+
exp, // this is the client side exp
|
|
73
|
+
},
|
|
74
|
+
this.options.valSecret
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
res
|
|
78
|
+
.cookie(VAL_SESSION_COOKIE, cookie, {
|
|
79
|
+
httpOnly: true,
|
|
80
|
+
sameSite: "strict",
|
|
81
|
+
secure: true,
|
|
82
|
+
expires: new Date(exp * 1000), // NOTE: this is not used for authorization, only for authentication
|
|
83
|
+
})
|
|
84
|
+
.redirect(callbackReqSuccess.redirect_uri || "/");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async logout(_req: express.Request, res: express.Response): Promise<void> {
|
|
88
|
+
res
|
|
89
|
+
.clearCookie(VAL_SESSION_COOKIE)
|
|
90
|
+
.clearCookie(VAL_STATE_COOKIE)
|
|
91
|
+
.sendStatus(200);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async withAuth<T>(
|
|
95
|
+
req: express.Request,
|
|
96
|
+
res: express.Response,
|
|
97
|
+
handler: (data: IntegratedServerJwtPayload) => Promise<T>
|
|
98
|
+
): Promise<T | undefined> {
|
|
99
|
+
const cookie = req.cookies[VAL_SESSION_COOKIE];
|
|
100
|
+
if (typeof cookie === "string") {
|
|
101
|
+
const verification = IntegratedServerJwtPayload.safeParse(
|
|
102
|
+
decodeJwt(cookie, this.options.valSecret)
|
|
103
|
+
);
|
|
104
|
+
if (!verification.success) {
|
|
105
|
+
res.sendStatus(401);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
return handler(verification.data);
|
|
109
|
+
} else {
|
|
110
|
+
res.sendStatus(401);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async session(req: express.Request, res: express.Response): Promise<void> {
|
|
115
|
+
return this.withAuth(req, res, async (data) => {
|
|
116
|
+
const url = new URL(
|
|
117
|
+
"/api/val/auth/user/session",
|
|
118
|
+
this.options.valBuildUrl
|
|
119
|
+
);
|
|
120
|
+
const fetchRes = await fetch(url, {
|
|
121
|
+
headers: this.getAuthHeaders(data.token, "application/json"),
|
|
122
|
+
});
|
|
123
|
+
if (fetchRes.ok) {
|
|
124
|
+
res
|
|
125
|
+
.status(fetchRes.status)
|
|
126
|
+
.json({ mode: "proxy", ...(await fetchRes.json()) });
|
|
127
|
+
} else {
|
|
128
|
+
res.sendStatus(fetchRes.status);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getIds(
|
|
134
|
+
req: express.Request<{ 0: string }>,
|
|
135
|
+
res: express.Response
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
return this.withAuth(req, res, async ({ token }) => {
|
|
138
|
+
const id = getPathFromParams(req.params);
|
|
139
|
+
const url = new URL(
|
|
140
|
+
`/api/val/modules/${encodeURIComponent(this.options.gitCommit)}${id}`,
|
|
141
|
+
this.options.valBuildUrl
|
|
142
|
+
);
|
|
143
|
+
const fetchRes = await fetch(url, {
|
|
144
|
+
headers: this.getAuthHeaders(token),
|
|
145
|
+
});
|
|
146
|
+
if (fetchRes.ok) {
|
|
147
|
+
res.status(fetchRes.status).json(await fetchRes.json());
|
|
148
|
+
} else {
|
|
149
|
+
res.sendStatus(fetchRes.status);
|
|
150
|
+
}
|
|
151
|
+
}).catch((e) => {
|
|
152
|
+
res.status(500).send({ error: { message: e?.message, status: 500 } });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async patchIds(
|
|
157
|
+
req: express.Request<{ 0: string }>,
|
|
158
|
+
res: express.Response
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
this.withAuth(req, res, async ({ token }) => {
|
|
161
|
+
// First validate that the body has the right structure
|
|
162
|
+
const patchJSON = PatchJSON.safeParse(req.body);
|
|
163
|
+
if (!patchJSON.success) {
|
|
164
|
+
res.status(401).json(patchJSON.error.issues);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Then parse/validate
|
|
168
|
+
const patch = parsePatch(patchJSON.data);
|
|
169
|
+
if (result.isErr(patch)) {
|
|
170
|
+
res.status(401).json(patch.error);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const id = getPathFromParams(req.params);
|
|
174
|
+
const url = new URL(
|
|
175
|
+
`/api/val/modules/${encodeURIComponent(this.options.gitCommit)}${id}`,
|
|
176
|
+
this.options.valBuildUrl
|
|
177
|
+
);
|
|
178
|
+
// Proxy patch to val.build
|
|
179
|
+
const fetchRes = await fetch(url, {
|
|
180
|
+
method: "PATCH",
|
|
181
|
+
headers: this.getAuthHeaders(token, "application/json-patch+json"),
|
|
182
|
+
body: JSON.stringify(patch),
|
|
183
|
+
});
|
|
184
|
+
if (fetchRes.ok) {
|
|
185
|
+
res.status(fetchRes.status).json(await fetchRes.json());
|
|
186
|
+
} else {
|
|
187
|
+
res.sendStatus(fetchRes.status);
|
|
188
|
+
}
|
|
189
|
+
}).catch((e) => {
|
|
190
|
+
res.status(500).send({ error: { message: e?.message, status: 500 } });
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async commit(req: express.Request, res: express.Response): Promise<void> {
|
|
195
|
+
this.withAuth(req, res, async ({ token }) => {
|
|
196
|
+
const url = new URL(
|
|
197
|
+
`/api/val/commit/${encodeURIComponent(this.options.gitBranch)}`,
|
|
198
|
+
this.options.valBuildUrl
|
|
199
|
+
);
|
|
200
|
+
const fetchRes = await fetch(url, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: this.getAuthHeaders(token),
|
|
203
|
+
});
|
|
204
|
+
if (fetchRes.ok) {
|
|
205
|
+
res.status(fetchRes.status).json(await fetchRes.json());
|
|
206
|
+
} else {
|
|
207
|
+
res.sendStatus(fetchRes.status);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private getAuthHeaders(
|
|
213
|
+
token: string,
|
|
214
|
+
type?: "application/json" | "application/json-patch+json"
|
|
215
|
+
):
|
|
216
|
+
| { Authorization: string }
|
|
217
|
+
| { "Content-Type": string; Authorization: string } {
|
|
218
|
+
if (!type) {
|
|
219
|
+
return {
|
|
220
|
+
Authorization: `Bearer ${token}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
"Content-Type": type,
|
|
225
|
+
Authorization: `Bearer ${token}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async consumeCode(code: string): Promise<{
|
|
230
|
+
sub: string;
|
|
231
|
+
exp: number;
|
|
232
|
+
org: string;
|
|
233
|
+
project: string;
|
|
234
|
+
token: string;
|
|
235
|
+
} | null> {
|
|
236
|
+
const url = new URL("/api/val/auth/user/token", this.options.valBuildUrl);
|
|
237
|
+
url.searchParams.set("code", encodeURIComponent(code));
|
|
238
|
+
return fetch(url, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: this.getAuthHeaders(this.options.apiKey, "application/json"), // NOTE: we use apiKey as auth on this endpoint (we do not have a token yet)
|
|
241
|
+
})
|
|
242
|
+
.then(async (res) => {
|
|
243
|
+
if (res.status === 200) {
|
|
244
|
+
const token = await res.text();
|
|
245
|
+
const verification = ValAppJwtPayload.safeParse(decodeJwt(token));
|
|
246
|
+
if (!verification.success) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
...verification.data,
|
|
251
|
+
token,
|
|
252
|
+
};
|
|
253
|
+
} else {
|
|
254
|
+
console.debug("Failed to get data from code: ", res.status);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
.catch((err) => {
|
|
259
|
+
console.debug("Failed to get user from code: ", err);
|
|
260
|
+
return null;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private getAuthorizeUrl(publicValApiRoute: string, token: string): string {
|
|
265
|
+
const url = new URL("/authorize", this.options.valBuildUrl);
|
|
266
|
+
url.searchParams.set(
|
|
267
|
+
"redirect_uri",
|
|
268
|
+
encodeURIComponent(`${publicValApiRoute}/callback`)
|
|
269
|
+
);
|
|
270
|
+
url.searchParams.set("state", token);
|
|
271
|
+
return url.toString();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private getAppErrorUrl(error: string): string {
|
|
275
|
+
const url = new URL("/authorize", this.options.valBuildUrl);
|
|
276
|
+
url.searchParams.set("error", encodeURIComponent(error));
|
|
277
|
+
return url.toString();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function verifyCallbackReq(
|
|
282
|
+
stateCookie: string,
|
|
283
|
+
queryParams: Record<string, unknown>
|
|
284
|
+
):
|
|
285
|
+
| {
|
|
286
|
+
success: { code: string; redirect_uri?: string };
|
|
287
|
+
error: null;
|
|
288
|
+
}
|
|
289
|
+
| { success: false; error: string } {
|
|
290
|
+
if (typeof stateCookie !== "string") {
|
|
291
|
+
return { success: false, error: "No state cookie" };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const { code, state: tokenFromQuery } = queryParams;
|
|
295
|
+
|
|
296
|
+
if (typeof code !== "string") {
|
|
297
|
+
return { success: false, error: "No code query param" };
|
|
298
|
+
}
|
|
299
|
+
if (typeof tokenFromQuery !== "string") {
|
|
300
|
+
return { success: false, error: "No state query param" };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const { success: cookieStateSuccess, error: cookieStateError } =
|
|
304
|
+
getStateFromCookie(stateCookie);
|
|
305
|
+
|
|
306
|
+
if (cookieStateError !== null) {
|
|
307
|
+
return { success: false, error: cookieStateError };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (cookieStateSuccess.token !== tokenFromQuery) {
|
|
311
|
+
return { success: false, error: "Invalid state token" };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
success: { code, redirect_uri: cookieStateSuccess.redirect_to },
|
|
316
|
+
error: null,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
type StateCookie = {
|
|
321
|
+
redirect_to: string;
|
|
322
|
+
token: string;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
function getStateFromCookie(stateCookie: string):
|
|
326
|
+
| {
|
|
327
|
+
success: StateCookie;
|
|
328
|
+
error: null;
|
|
329
|
+
}
|
|
330
|
+
| { success: false; error: string } {
|
|
331
|
+
try {
|
|
332
|
+
const decoded = Buffer.from(stateCookie, "base64").toString("utf8");
|
|
333
|
+
const parsed = JSON.parse(decoded) as unknown;
|
|
334
|
+
|
|
335
|
+
if (!parsed) {
|
|
336
|
+
return {
|
|
337
|
+
success: false,
|
|
338
|
+
error: "Invalid state cookie: could not parse",
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (typeof parsed !== "object") {
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
error: "Invalid state cookie: parsed object is not an object",
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if ("token" in parsed && "redirect_to" in parsed) {
|
|
348
|
+
const { token, redirect_to } = parsed;
|
|
349
|
+
if (typeof token !== "string") {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
error: "Invalid state cookie: no token in parsed object",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (typeof redirect_to !== "string") {
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: "Invalid state cookie: no redirect_to in parsed object",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
success: {
|
|
363
|
+
token,
|
|
364
|
+
redirect_to,
|
|
365
|
+
},
|
|
366
|
+
error: null,
|
|
367
|
+
};
|
|
368
|
+
} else {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
error: "Invalid state cookie: no token or redirect_to in parsed object",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
return {
|
|
376
|
+
success: false,
|
|
377
|
+
error: "Invalid state cookie: could not parse",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function createStateCookie(state: StateCookie): string {
|
|
383
|
+
return Buffer.from(JSON.stringify(state), "utf8").toString("base64");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const ValAppJwtPayload = z.object({
|
|
387
|
+
sub: z.string(),
|
|
388
|
+
exp: z.number(),
|
|
389
|
+
project: z.string(),
|
|
390
|
+
org: z.string(),
|
|
391
|
+
});
|
|
392
|
+
type ValAppJwtPayload = z.infer<typeof ValAppJwtPayload>;
|
|
393
|
+
|
|
394
|
+
const IntegratedServerJwtPayload = z.object({
|
|
395
|
+
sub: z.string(),
|
|
396
|
+
exp: z.number(),
|
|
397
|
+
token: z.string(),
|
|
398
|
+
org: z.string(),
|
|
399
|
+
project: z.string(),
|
|
400
|
+
});
|
|
401
|
+
export type IntegratedServerJwtPayload = z.infer<
|
|
402
|
+
typeof IntegratedServerJwtPayload
|
|
403
|
+
>;
|
package/src/Service.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { newQuickJSWASMModule, QuickJSRuntime } from "quickjs-emscripten";
|
|
2
|
+
import { patchValFile } from "./patchValFile";
|
|
3
|
+
import { readValFile } from "./readValFile";
|
|
4
|
+
import { Patch } from "@valbuild/core/patch";
|
|
5
|
+
import { ValModuleLoader } from "./ValModuleLoader";
|
|
6
|
+
import { newValQuickJSRuntime } from "./ValQuickJSRuntime";
|
|
7
|
+
import { ValSourceFileHandler } from "./ValSourceFileHandler";
|
|
8
|
+
import ts from "typescript";
|
|
9
|
+
import { getCompilerOptions } from "./getCompilerOptions";
|
|
10
|
+
import { IValFSHost } from "./ValFSHost";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import { SerializedModuleContent } from "./SerializedModuleContent";
|
|
13
|
+
import {
|
|
14
|
+
ModuleId,
|
|
15
|
+
ModulePath,
|
|
16
|
+
Internal,
|
|
17
|
+
SourcePath,
|
|
18
|
+
Schema,
|
|
19
|
+
SelectorSource,
|
|
20
|
+
} from "@valbuild/core";
|
|
21
|
+
|
|
22
|
+
export type ServiceOptions = {
|
|
23
|
+
/**
|
|
24
|
+
* Relative path to the val.config.js file from the root directory.
|
|
25
|
+
*
|
|
26
|
+
* @example src/val.config
|
|
27
|
+
*/
|
|
28
|
+
valConfigPath: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function createService(
|
|
32
|
+
projectRoot: string,
|
|
33
|
+
opts: ServiceOptions,
|
|
34
|
+
host: IValFSHost = {
|
|
35
|
+
...ts.sys,
|
|
36
|
+
writeFile: fs.writeFileSync,
|
|
37
|
+
}
|
|
38
|
+
): Promise<Service> {
|
|
39
|
+
const compilerOptions = getCompilerOptions(projectRoot, host);
|
|
40
|
+
const sourceFileHandler = new ValSourceFileHandler(
|
|
41
|
+
projectRoot,
|
|
42
|
+
compilerOptions,
|
|
43
|
+
host
|
|
44
|
+
);
|
|
45
|
+
const loader = new ValModuleLoader(
|
|
46
|
+
projectRoot,
|
|
47
|
+
compilerOptions,
|
|
48
|
+
sourceFileHandler,
|
|
49
|
+
host
|
|
50
|
+
);
|
|
51
|
+
const module = await newQuickJSWASMModule();
|
|
52
|
+
const runtime = await newValQuickJSRuntime(module, loader);
|
|
53
|
+
return new Service(opts, sourceFileHandler, runtime);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class Service {
|
|
57
|
+
readonly valConfigPath: string;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
{ valConfigPath }: ServiceOptions,
|
|
61
|
+
private readonly sourceFileHandler: ValSourceFileHandler,
|
|
62
|
+
private readonly runtime: QuickJSRuntime
|
|
63
|
+
) {
|
|
64
|
+
this.valConfigPath = valConfigPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async get(
|
|
68
|
+
moduleId: ModuleId,
|
|
69
|
+
modulePath: ModulePath
|
|
70
|
+
): Promise<SerializedModuleContent> {
|
|
71
|
+
const valModule = await readValFile(
|
|
72
|
+
moduleId,
|
|
73
|
+
this.valConfigPath,
|
|
74
|
+
this.runtime
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const resolved = Internal.resolvePath(
|
|
78
|
+
modulePath,
|
|
79
|
+
valModule.source,
|
|
80
|
+
valModule.schema
|
|
81
|
+
);
|
|
82
|
+
return {
|
|
83
|
+
path: [moduleId, resolved.path].join(".") as SourcePath,
|
|
84
|
+
schema:
|
|
85
|
+
resolved.schema instanceof Schema<SelectorSource>
|
|
86
|
+
? resolved.schema.serialize()
|
|
87
|
+
: resolved.schema,
|
|
88
|
+
source: resolved.source,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async patch(
|
|
93
|
+
moduleId: string,
|
|
94
|
+
patch: Patch
|
|
95
|
+
): Promise<SerializedModuleContent> {
|
|
96
|
+
return patchValFile(
|
|
97
|
+
moduleId,
|
|
98
|
+
this.valConfigPath,
|
|
99
|
+
patch,
|
|
100
|
+
this.sourceFileHandler,
|
|
101
|
+
this.runtime
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
dispose() {
|
|
106
|
+
this.runtime.dispose();
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/ValFS.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A filesystem that can update and read files.
|
|
3
|
+
* It does not support creating new files or directories.
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
export interface ValFS {
|
|
7
|
+
readDirectory(
|
|
8
|
+
rootDir: string,
|
|
9
|
+
extensions: readonly string[],
|
|
10
|
+
excludes: readonly string[] | undefined,
|
|
11
|
+
includes: readonly string[],
|
|
12
|
+
depth?: number | undefined
|
|
13
|
+
): readonly string[];
|
|
14
|
+
|
|
15
|
+
writeFile(filePath: string, data: string, encoding: "binary" | "utf8"): void;
|
|
16
|
+
|
|
17
|
+
fileExists(filePath: string): boolean;
|
|
18
|
+
|
|
19
|
+
readFile(filePath: string): string | undefined;
|
|
20
|
+
|
|
21
|
+
realpath(path: string): string;
|
|
22
|
+
}
|