@valbuild/server 0.94.0 → 0.96.0
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/dist/declarations/src/ValServer.d.ts +1 -1
- package/dist/valbuild-server.cjs.dev.js +968 -131
- package/dist/valbuild-server.cjs.prod.js +968 -131
- package/dist/valbuild-server.esm.js +968 -131
- package/package.json +4 -4
|
@@ -2374,8 +2374,8 @@ class ValOps {
|
|
|
2374
2374
|
}
|
|
2375
2375
|
|
|
2376
2376
|
// #region createPatch
|
|
2377
|
-
async createPatch(path, patch, patchId, parentRef, authorId) {
|
|
2378
|
-
const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId);
|
|
2377
|
+
async createPatch(path, patch, patchId, parentRef, sessionId, authorId) {
|
|
2378
|
+
const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId);
|
|
2379
2379
|
if (result.isErr(saveRes)) {
|
|
2380
2380
|
console.error(`Could not save source patch at path: '${path}'. Error: ${saveRes.error.errorType === "other" ? saveRes.error.message : saveRes.error.errorType}`);
|
|
2381
2381
|
if (saveRes.error.errorType === "patch-head-conflict") {
|
|
@@ -2579,6 +2579,76 @@ class ValOpsFS extends ValOps {
|
|
|
2579
2579
|
async onInit() {
|
|
2580
2580
|
// do nothing
|
|
2581
2581
|
}
|
|
2582
|
+
async getPresignedAuthNonce(project, corsOrigin, auth) {
|
|
2583
|
+
const authHeader = "pat" in auth ? {
|
|
2584
|
+
"x-val-pat": auth.pat
|
|
2585
|
+
} : {
|
|
2586
|
+
Authorization: `Bearer ${auth.apiKey}`
|
|
2587
|
+
};
|
|
2588
|
+
try {
|
|
2589
|
+
const res = await fetch(`${this.contentUrl}/v1/${project}/presigned-auth-nonce`, {
|
|
2590
|
+
method: "POST",
|
|
2591
|
+
headers: {
|
|
2592
|
+
...authHeader,
|
|
2593
|
+
"Content-Type": "application/json"
|
|
2594
|
+
},
|
|
2595
|
+
body: JSON.stringify({
|
|
2596
|
+
corsOrigin
|
|
2597
|
+
})
|
|
2598
|
+
});
|
|
2599
|
+
if (res.ok) {
|
|
2600
|
+
const json = await res.json();
|
|
2601
|
+
const parsed = z.object({
|
|
2602
|
+
nonce: z.string(),
|
|
2603
|
+
expiresAt: z.string()
|
|
2604
|
+
}).safeParse(json);
|
|
2605
|
+
if (parsed.success) {
|
|
2606
|
+
return {
|
|
2607
|
+
status: "success",
|
|
2608
|
+
data: {
|
|
2609
|
+
nonce: parsed.data.nonce,
|
|
2610
|
+
baseUrl: `${this.contentUrl}/v1/${project}`
|
|
2611
|
+
}
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
console.error("Could not parse presigned auth nonce response. Error: " + fromError(parsed.error));
|
|
2615
|
+
return {
|
|
2616
|
+
status: "error",
|
|
2617
|
+
statusCode: 500,
|
|
2618
|
+
error: {
|
|
2619
|
+
message: "Could not get presigned auth nonce. The response from the content host was not in the expected format."
|
|
2620
|
+
}
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
if (res.status === 401) {
|
|
2624
|
+
return {
|
|
2625
|
+
status: "error",
|
|
2626
|
+
statusCode: 401,
|
|
2627
|
+
error: {
|
|
2628
|
+
message: "Could not get presigned auth nonce. The local PAT was rejected by the content host. Try re-running `val login`."
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
const unknownErrorMessage = `Could not get presigned auth nonce. HTTP error: ${res.status} ${res.statusText}`;
|
|
2633
|
+
console.error(unknownErrorMessage);
|
|
2634
|
+
return {
|
|
2635
|
+
status: "error",
|
|
2636
|
+
statusCode: 500,
|
|
2637
|
+
error: {
|
|
2638
|
+
message: unknownErrorMessage
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2641
|
+
} catch (e) {
|
|
2642
|
+
console.error("Could not get presigned auth nonce (connection error?):", e);
|
|
2643
|
+
return {
|
|
2644
|
+
status: "error",
|
|
2645
|
+
statusCode: 500,
|
|
2646
|
+
error: {
|
|
2647
|
+
message: `Could not get presigned auth nonce. Error: ${e instanceof Error ? e.message : JSON.stringify(e)}`
|
|
2648
|
+
}
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2582
2652
|
async getCommitSummary() {
|
|
2583
2653
|
return {
|
|
2584
2654
|
error: {
|
|
@@ -2767,11 +2837,17 @@ class ValOpsFS extends ValOps {
|
|
|
2767
2837
|
parentPatchId: dir
|
|
2768
2838
|
});
|
|
2769
2839
|
} else {
|
|
2770
|
-
|
|
2840
|
+
const patchId = parsedPatch.data.patchId;
|
|
2841
|
+
if (includes && includes.length > 0 && !includes.includes(patchId)) {
|
|
2771
2842
|
return;
|
|
2772
2843
|
}
|
|
2773
|
-
patches[
|
|
2774
|
-
|
|
2844
|
+
patches[patchId] = {
|
|
2845
|
+
path: parsedPatch.data.path,
|
|
2846
|
+
patch: parsedPatch.data.patch,
|
|
2847
|
+
parentRef: parsedPatch.data.parentRef,
|
|
2848
|
+
baseSha: parsedPatch.data.baseSha,
|
|
2849
|
+
createdAt: parsedPatch.data.createdAt,
|
|
2850
|
+
authorId: parsedPatch.data.authorId,
|
|
2775
2851
|
appliedAt: null
|
|
2776
2852
|
};
|
|
2777
2853
|
}
|
|
@@ -2942,7 +3018,7 @@ class ValOpsFS extends ValOps {
|
|
|
2942
3018
|
};
|
|
2943
3019
|
}
|
|
2944
3020
|
}
|
|
2945
|
-
async saveSourceFilePatch(path, patch, patchId, parentRef, authorId) {
|
|
3021
|
+
async saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId) {
|
|
2946
3022
|
const patchDir = this.getParentPatchIdFromParentRef(parentRef);
|
|
2947
3023
|
try {
|
|
2948
3024
|
const baseSha = await this.getBaseSha();
|
|
@@ -2952,6 +3028,7 @@ class ValOpsFS extends ValOps {
|
|
|
2952
3028
|
parentRef,
|
|
2953
3029
|
path,
|
|
2954
3030
|
authorId,
|
|
3031
|
+
sessionId,
|
|
2955
3032
|
baseSha,
|
|
2956
3033
|
coreVersion: Internal.VERSION.core,
|
|
2957
3034
|
createdAt: new Date().toISOString()
|
|
@@ -3390,12 +3467,6 @@ class ValOpsFS extends ValOps {
|
|
|
3390
3467
|
return result.ok(Object.fromEntries(Object.entries(patches.patches).map(([patchId, value]) => [patchId, this.getParentPatchIdFromParentRef(value.parentRef)])));
|
|
3391
3468
|
}
|
|
3392
3469
|
|
|
3393
|
-
// #region profiles
|
|
3394
|
-
async getProfiles() {
|
|
3395
|
-
// We do not have profiles in FS mode
|
|
3396
|
-
return [];
|
|
3397
|
-
}
|
|
3398
|
-
|
|
3399
3470
|
// #region fs file path helpers
|
|
3400
3471
|
getPatchesDir() {
|
|
3401
3472
|
return path__default.join(this.rootDir, ValOpsFS.VAL_DIR, "patches");
|
|
@@ -3504,7 +3575,9 @@ const FSPatch = z.object({
|
|
|
3504
3575
|
parentRef: ParentRef,
|
|
3505
3576
|
authorId: z.string().refine(p => true).nullable(),
|
|
3506
3577
|
createdAt: z.string().datetime(),
|
|
3507
|
-
coreVersion: z.string().nullable()
|
|
3578
|
+
coreVersion: z.string().nullable(),
|
|
3579
|
+
// TODO: use this to check if patch is compatible with current core version?
|
|
3580
|
+
sessionId: z.string().nullable()
|
|
3508
3581
|
});
|
|
3509
3582
|
const FSPatchBase = z.object({
|
|
3510
3583
|
baseSha: z.string().refine(p => true),
|
|
@@ -3606,17 +3679,6 @@ const CommitResponse = z.object({
|
|
|
3606
3679
|
commit: CommitSha,
|
|
3607
3680
|
branch: z.string()
|
|
3608
3681
|
});
|
|
3609
|
-
const ProfilesResponse = z.object({
|
|
3610
|
-
profiles: z.array(z.object({
|
|
3611
|
-
profileId: z.string(),
|
|
3612
|
-
fullName: z.string(),
|
|
3613
|
-
email: z.string().optional(),
|
|
3614
|
-
// TODO: make this required once this can be guaranteed
|
|
3615
|
-
avatar: z.object({
|
|
3616
|
-
url: z.string()
|
|
3617
|
-
}).nullable()
|
|
3618
|
-
}))
|
|
3619
|
-
});
|
|
3620
3682
|
const NonceResponse = z.object({
|
|
3621
3683
|
nonce: z.string(),
|
|
3622
3684
|
url: z.string()
|
|
@@ -3908,7 +3970,7 @@ class ValOpsHttp extends ValOps {
|
|
|
3908
3970
|
return {
|
|
3909
3971
|
status: "error",
|
|
3910
3972
|
error: {
|
|
3911
|
-
message: "Could not get nonce." + message
|
|
3973
|
+
message: "Could not get nonce. " + message
|
|
3912
3974
|
}
|
|
3913
3975
|
};
|
|
3914
3976
|
}
|
|
@@ -4067,7 +4129,7 @@ class ValOpsHttp extends ValOps {
|
|
|
4067
4129
|
};
|
|
4068
4130
|
}
|
|
4069
4131
|
}
|
|
4070
|
-
async saveSourceFilePatch(path, patch, patchId, parentRef, authorId) {
|
|
4132
|
+
async saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId) {
|
|
4071
4133
|
const baseSha = await this.getBaseSha();
|
|
4072
4134
|
return fetch(`${this.contentUrl}/v1/${this.project}/patches`, {
|
|
4073
4135
|
method: "POST",
|
|
@@ -4079,6 +4141,7 @@ class ValOpsHttp extends ValOps {
|
|
|
4079
4141
|
path,
|
|
4080
4142
|
patch,
|
|
4081
4143
|
authorId,
|
|
4144
|
+
sessionId,
|
|
4082
4145
|
patchId,
|
|
4083
4146
|
parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
|
|
4084
4147
|
baseSha,
|
|
@@ -4502,31 +4565,6 @@ class ValOpsHttp extends ValOps {
|
|
|
4502
4565
|
};
|
|
4503
4566
|
}
|
|
4504
4567
|
}
|
|
4505
|
-
|
|
4506
|
-
// #region profiles
|
|
4507
|
-
async getProfiles() {
|
|
4508
|
-
var _res$headers$get6;
|
|
4509
|
-
const res = await fetch(`${this.contentUrl}/v1/${this.project}/profiles`, {
|
|
4510
|
-
headers: {
|
|
4511
|
-
...this.authHeaders,
|
|
4512
|
-
"Content-Type": "application/json"
|
|
4513
|
-
}
|
|
4514
|
-
});
|
|
4515
|
-
if (res.ok) {
|
|
4516
|
-
const parsed = ProfilesResponse.safeParse(await res.json());
|
|
4517
|
-
if (parsed.error) {
|
|
4518
|
-
console.error("Could not parse profiles response", parsed.error);
|
|
4519
|
-
throw Error(`Could not get profiles from remote server: wrong format. You might need to upgrade Val.`);
|
|
4520
|
-
}
|
|
4521
|
-
return parsed.data.profiles;
|
|
4522
|
-
}
|
|
4523
|
-
if ((_res$headers$get6 = res.headers.get("Content-Type")) !== null && _res$headers$get6 !== void 0 && _res$headers$get6.includes("application/json")) {
|
|
4524
|
-
const json = await res.json();
|
|
4525
|
-
const message = getErrorMessageFromUnknownJson(json, "Unknown error");
|
|
4526
|
-
throw Error(`Could not get profiles (status: ${res.status}): ${message}`);
|
|
4527
|
-
}
|
|
4528
|
-
throw Error(`Could not get profiles. Got status: ${res.status}`);
|
|
4529
|
-
}
|
|
4530
4568
|
}
|
|
4531
4569
|
|
|
4532
4570
|
const host = process.env.VAL_CONTENT_URL || DEFAULT_CONTENT_HOST;
|
|
@@ -4679,6 +4717,24 @@ function hasRemoteFileSchema(schema) {
|
|
|
4679
4717
|
|
|
4680
4718
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
4681
4719
|
const ValServer = (valModules, options, callbacks) => {
|
|
4720
|
+
const AIContentBlock = z.union([z.object({
|
|
4721
|
+
type: z.literal("text"),
|
|
4722
|
+
text: z.string()
|
|
4723
|
+
}), z.object({
|
|
4724
|
+
type: z.literal("image_url"),
|
|
4725
|
+
url: z.string()
|
|
4726
|
+
})]);
|
|
4727
|
+
const AIMessageContent = z.union([z.string(), z.array(AIContentBlock)]);
|
|
4728
|
+
const ProfilesResponse = z.object({
|
|
4729
|
+
profiles: z.array(z.object({
|
|
4730
|
+
profileId: z.string(),
|
|
4731
|
+
fullName: z.string(),
|
|
4732
|
+
email: z.string().optional(),
|
|
4733
|
+
avatar: z.object({
|
|
4734
|
+
url: z.string()
|
|
4735
|
+
}).nullable()
|
|
4736
|
+
}))
|
|
4737
|
+
});
|
|
4682
4738
|
let serverOps;
|
|
4683
4739
|
if (options.mode === "fs") {
|
|
4684
4740
|
serverOps = new ValOpsFS(options.valContentUrl, options.cwd, valModules, {
|
|
@@ -5466,15 +5522,38 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5466
5522
|
};
|
|
5467
5523
|
}
|
|
5468
5524
|
if (serverOps instanceof ValOpsFS) {
|
|
5469
|
-
// In FS mode
|
|
5470
|
-
//
|
|
5471
|
-
//
|
|
5525
|
+
// In FS mode patch-file uploads are buffered through this server (no remote round-trip),
|
|
5526
|
+
// so baseUrl points at /api/val/upload. AI image uploads, however, go straight to the
|
|
5527
|
+
// content host — we resolve a contentBaseUrl + a PAT-issued nonce here so the browser
|
|
5528
|
+
// can POST directly without exposing the PAT.
|
|
5472
5529
|
const host = `/api/val`;
|
|
5530
|
+
let contentBaseUrl = null;
|
|
5531
|
+
let contentAuthNonce = null;
|
|
5532
|
+
if (!options.project) {
|
|
5533
|
+
console.warn("Direct content-host uploads (AI images) disabled: no `project` set in val.config (and VAL_PROJECT env var is not set).");
|
|
5534
|
+
} else {
|
|
5535
|
+
const authDataRes = await getRemoteFileAuth();
|
|
5536
|
+
if (authDataRes.status !== 200) {
|
|
5537
|
+
console.warn("Direct content-host uploads (AI images) disabled: " + authDataRes.json.message);
|
|
5538
|
+
} else {
|
|
5539
|
+
const corsOrigin = "*"; // TODO: add cors origin
|
|
5540
|
+
const presignedAuthNonce = await serverOps.getPresignedAuthNonce(options.project, corsOrigin, authDataRes.json.remoteFileAuth);
|
|
5541
|
+
if (presignedAuthNonce.status === "success") {
|
|
5542
|
+
contentBaseUrl = presignedAuthNonce.data.baseUrl;
|
|
5543
|
+
contentAuthNonce = presignedAuthNonce.data.nonce;
|
|
5544
|
+
} else {
|
|
5545
|
+
console.warn("Direct content-host uploads (AI images) disabled: " + presignedAuthNonce.error.message);
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5473
5549
|
return {
|
|
5474
5550
|
status: 200,
|
|
5475
5551
|
json: {
|
|
5476
5552
|
nonce: null,
|
|
5477
|
-
baseUrl: `${host}/upload
|
|
5553
|
+
baseUrl: `${host}/upload`,
|
|
5554
|
+
// NOTE: this is the /upload/patches endpoint - the client will add /patches/:patchId/files to this and post to it
|
|
5555
|
+
contentBaseUrl,
|
|
5556
|
+
contentAuthNonce
|
|
5478
5557
|
}
|
|
5479
5558
|
};
|
|
5480
5559
|
}
|
|
@@ -5500,7 +5579,9 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5500
5579
|
status: 200,
|
|
5501
5580
|
json: {
|
|
5502
5581
|
nonce: presignedAuthNonce.data.nonce,
|
|
5503
|
-
baseUrl: presignedAuthNonce.data.baseUrl
|
|
5582
|
+
baseUrl: presignedAuthNonce.data.baseUrl,
|
|
5583
|
+
contentBaseUrl: presignedAuthNonce.data.baseUrl,
|
|
5584
|
+
contentAuthNonce: presignedAuthNonce.data.nonce
|
|
5504
5585
|
}
|
|
5505
5586
|
};
|
|
5506
5587
|
}
|
|
@@ -5528,10 +5609,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5528
5609
|
}
|
|
5529
5610
|
const patches = req.body.patches;
|
|
5530
5611
|
let parentRef = req.body.parentRef;
|
|
5612
|
+
const sessionId = req.body.sessionId ?? null;
|
|
5531
5613
|
const authorId = "id" in auth ? auth.id : null;
|
|
5532
5614
|
const newPatchIds = [];
|
|
5533
5615
|
for (const patch of patches) {
|
|
5534
|
-
const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, authorId);
|
|
5616
|
+
const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, sessionId, authorId);
|
|
5535
5617
|
if (result.isErr(createPatchRes)) {
|
|
5536
5618
|
if (createPatchRes.error.errorType === "patch-head-conflict") {
|
|
5537
5619
|
return {
|
|
@@ -5892,13 +5974,87 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5892
5974
|
}
|
|
5893
5975
|
};
|
|
5894
5976
|
}
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5977
|
+
if (!options.project) {
|
|
5978
|
+
return {
|
|
5979
|
+
status: 500,
|
|
5980
|
+
json: {
|
|
5981
|
+
message: "Project is not configured"
|
|
5982
|
+
}
|
|
5983
|
+
};
|
|
5984
|
+
}
|
|
5985
|
+
const authDataRes = await getRemoteFileAuth();
|
|
5986
|
+
if (authDataRes.status !== 200) {
|
|
5987
|
+
if (serverOps instanceof ValOpsFS && authDataRes.json.errorCode === "pat-error") {
|
|
5988
|
+
return {
|
|
5989
|
+
status: 200,
|
|
5990
|
+
json: {
|
|
5991
|
+
profiles: []
|
|
5992
|
+
}
|
|
5993
|
+
};
|
|
5994
|
+
}
|
|
5995
|
+
return {
|
|
5996
|
+
status: 500,
|
|
5997
|
+
json: {
|
|
5998
|
+
message: authDataRes.json.message
|
|
5999
|
+
}
|
|
6000
|
+
};
|
|
6001
|
+
}
|
|
6002
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6003
|
+
const execFetch = async headers => {
|
|
6004
|
+
try {
|
|
6005
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/profiles`;
|
|
6006
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6007
|
+
method: "GET",
|
|
6008
|
+
headers
|
|
6009
|
+
});
|
|
6010
|
+
if (!upstreamRes.ok) {
|
|
6011
|
+
const text = await upstreamRes.text();
|
|
6012
|
+
const isAuthError = upstreamRes.status === 401 || upstreamRes.status === 403;
|
|
6013
|
+
return {
|
|
6014
|
+
status: isAuthError ? 401 : 500,
|
|
6015
|
+
json: {
|
|
6016
|
+
message: isAuthError ? `Profile authentication failed: ${upstreamRes.status} ${text}` : `Profiles failed: ${upstreamRes.status} ${text}`
|
|
6017
|
+
}
|
|
6018
|
+
};
|
|
6019
|
+
}
|
|
6020
|
+
const parseRes = ProfilesResponse.safeParse(await upstreamRes.json());
|
|
6021
|
+
if (!parseRes.success) {
|
|
6022
|
+
return {
|
|
6023
|
+
status: 500,
|
|
6024
|
+
json: {
|
|
6025
|
+
message: "Could not parse profiles response: " + fromError(parseRes.error).toString()
|
|
6026
|
+
}
|
|
6027
|
+
};
|
|
6028
|
+
}
|
|
6029
|
+
return {
|
|
6030
|
+
status: 200,
|
|
6031
|
+
json: {
|
|
6032
|
+
profiles: parseRes.data.profiles
|
|
6033
|
+
}
|
|
6034
|
+
};
|
|
6035
|
+
} catch (err) {
|
|
6036
|
+
return {
|
|
6037
|
+
status: 500,
|
|
6038
|
+
json: {
|
|
6039
|
+
message: err instanceof Error ? err.message : "Profiles request failed"
|
|
6040
|
+
}
|
|
6041
|
+
};
|
|
5900
6042
|
}
|
|
5901
6043
|
};
|
|
6044
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6045
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6046
|
+
}
|
|
6047
|
+
if (!options.valSecret) {
|
|
6048
|
+
return {
|
|
6049
|
+
status: 500,
|
|
6050
|
+
json: {
|
|
6051
|
+
message: "Secret is not configured"
|
|
6052
|
+
}
|
|
6053
|
+
};
|
|
6054
|
+
}
|
|
6055
|
+
return withAuth(options.valSecret, cookies, "profiles", data => {
|
|
6056
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6057
|
+
});
|
|
5902
6058
|
}
|
|
5903
6059
|
},
|
|
5904
6060
|
"/commit-summary": {
|
|
@@ -6080,84 +6236,750 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
6080
6236
|
}
|
|
6081
6237
|
}
|
|
6082
6238
|
},
|
|
6083
|
-
//#region
|
|
6084
|
-
"/
|
|
6085
|
-
|
|
6086
|
-
const
|
|
6087
|
-
const
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
|
6096
|
-
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
|
6097
|
-
let cacheControl;
|
|
6098
|
-
let fileBuffer;
|
|
6099
|
-
let mimeType;
|
|
6100
|
-
const remote = query.remote === "true";
|
|
6101
|
-
if (query.patch_id) {
|
|
6102
|
-
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id, remote);
|
|
6103
|
-
mimeType = Internal.filenameToMimeType(filePath);
|
|
6104
|
-
// TODO: reenable this:
|
|
6105
|
-
// cacheControl = "public, max-age=20000, immutable";
|
|
6106
|
-
} else {
|
|
6107
|
-
if (serverOps instanceof ValOpsHttp && remote) {
|
|
6108
|
-
console.error(`Remote file: ${filePath} requested without patch id. This is most likely a bug in Val.`);
|
|
6109
|
-
}
|
|
6110
|
-
fileBuffer = await serverOps.getBinaryFile(filePath);
|
|
6239
|
+
//#region ai proxy
|
|
6240
|
+
"/ai/initialize": {
|
|
6241
|
+
POST: async req => {
|
|
6242
|
+
const cookies = req.cookies;
|
|
6243
|
+
const auth = getAuth(cookies);
|
|
6244
|
+
if (auth.error) {
|
|
6245
|
+
return {
|
|
6246
|
+
status: 401,
|
|
6247
|
+
json: {
|
|
6248
|
+
message: auth.error
|
|
6249
|
+
}
|
|
6250
|
+
};
|
|
6111
6251
|
}
|
|
6112
|
-
if (
|
|
6252
|
+
if (!options.project) {
|
|
6113
6253
|
return {
|
|
6114
|
-
status:
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
"Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
|
|
6119
|
-
},
|
|
6120
|
-
body: bufferToReadableStream(fileBuffer)
|
|
6254
|
+
status: 401,
|
|
6255
|
+
json: {
|
|
6256
|
+
message: "Project is not configured"
|
|
6257
|
+
}
|
|
6121
6258
|
};
|
|
6122
|
-
}
|
|
6259
|
+
}
|
|
6260
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6261
|
+
if (authDataRes.status !== 200) {
|
|
6123
6262
|
return {
|
|
6124
|
-
status:
|
|
6263
|
+
status: 401,
|
|
6125
6264
|
json: {
|
|
6126
|
-
message:
|
|
6265
|
+
message: authDataRes.json.message
|
|
6266
|
+
}
|
|
6267
|
+
};
|
|
6268
|
+
}
|
|
6269
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6270
|
+
const execFetch = async headers => {
|
|
6271
|
+
try {
|
|
6272
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/initialize`;
|
|
6273
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6274
|
+
method: "POST",
|
|
6275
|
+
headers,
|
|
6276
|
+
body: JSON.stringify({})
|
|
6277
|
+
});
|
|
6278
|
+
if (!upstreamRes.ok) {
|
|
6279
|
+
const text = await upstreamRes.text();
|
|
6280
|
+
return {
|
|
6281
|
+
status: 500,
|
|
6282
|
+
json: {
|
|
6283
|
+
message: `AI initialize failed: ${upstreamRes.status} ${text}`
|
|
6284
|
+
}
|
|
6285
|
+
};
|
|
6286
|
+
}
|
|
6287
|
+
const json = await upstreamRes.json();
|
|
6288
|
+
const wsUrl = options.valContentUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:") + `/v1/${options.project}/ai/connect`;
|
|
6289
|
+
return {
|
|
6290
|
+
status: 200,
|
|
6291
|
+
json: {
|
|
6292
|
+
nonce: json.nonce,
|
|
6293
|
+
wsUrl
|
|
6294
|
+
}
|
|
6295
|
+
};
|
|
6296
|
+
} catch (err) {
|
|
6297
|
+
return {
|
|
6298
|
+
status: 500,
|
|
6299
|
+
json: {
|
|
6300
|
+
message: err instanceof Error ? err.message : "AI initialize error"
|
|
6301
|
+
}
|
|
6302
|
+
};
|
|
6303
|
+
}
|
|
6304
|
+
};
|
|
6305
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6306
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6307
|
+
}
|
|
6308
|
+
if (!options.valSecret) {
|
|
6309
|
+
return {
|
|
6310
|
+
status: 500,
|
|
6311
|
+
json: {
|
|
6312
|
+
message: "Secret is not configured"
|
|
6127
6313
|
}
|
|
6128
6314
|
};
|
|
6129
6315
|
}
|
|
6316
|
+
return withAuth(options.valSecret, cookies, "ai/initialize", data => {
|
|
6317
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6318
|
+
});
|
|
6130
6319
|
}
|
|
6131
|
-
}
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
}
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6320
|
+
},
|
|
6321
|
+
"/ai/sessions": {
|
|
6322
|
+
GET: async req => {
|
|
6323
|
+
const cookies = req.cookies;
|
|
6324
|
+
const auth = getAuth(cookies);
|
|
6325
|
+
if (auth.error) {
|
|
6326
|
+
return {
|
|
6327
|
+
status: 401,
|
|
6328
|
+
json: {
|
|
6329
|
+
message: auth.error
|
|
6330
|
+
}
|
|
6331
|
+
};
|
|
6332
|
+
}
|
|
6333
|
+
if (!options.project) {
|
|
6334
|
+
return {
|
|
6335
|
+
status: 500,
|
|
6336
|
+
json: {
|
|
6337
|
+
message: "Project is not configured"
|
|
6338
|
+
}
|
|
6339
|
+
};
|
|
6340
|
+
}
|
|
6341
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6342
|
+
if (authDataRes.status !== 200) {
|
|
6343
|
+
return {
|
|
6344
|
+
status: 500,
|
|
6345
|
+
json: {
|
|
6346
|
+
message: authDataRes.json.message
|
|
6347
|
+
}
|
|
6348
|
+
};
|
|
6349
|
+
}
|
|
6350
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6351
|
+
const execFetch = async headers => {
|
|
6352
|
+
try {
|
|
6353
|
+
const SessionsResponse = z.object({
|
|
6354
|
+
sessions: z.array(z.object({
|
|
6355
|
+
id: z.string(),
|
|
6356
|
+
name: z.string().nullable(),
|
|
6357
|
+
createdAt: z.string(),
|
|
6358
|
+
updatedAt: z.string()
|
|
6359
|
+
})),
|
|
6360
|
+
nextCursor: z.object({
|
|
6361
|
+
updatedAt: z.string(),
|
|
6362
|
+
id: z.string()
|
|
6363
|
+
}).nullable().optional()
|
|
6364
|
+
});
|
|
6365
|
+
const params = new URLSearchParams();
|
|
6366
|
+
if (req.query.limit) params.set("limit", req.query.limit);
|
|
6367
|
+
if (req.query.cursor_updatedAt) {
|
|
6368
|
+
params.set("cursor_updatedAt", req.query.cursor_updatedAt);
|
|
6369
|
+
}
|
|
6370
|
+
if (req.query.cursor_id) {
|
|
6371
|
+
params.set("cursor_id", req.query.cursor_id);
|
|
6372
|
+
}
|
|
6373
|
+
const qs = params.toString();
|
|
6374
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions${qs ? `?${qs}` : ""}`;
|
|
6375
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6376
|
+
headers
|
|
6377
|
+
});
|
|
6378
|
+
if (!upstreamRes.ok) {
|
|
6379
|
+
const text = await upstreamRes.text();
|
|
6380
|
+
return {
|
|
6381
|
+
status: 500,
|
|
6382
|
+
json: {
|
|
6383
|
+
message: `AI sessions failed: ${upstreamRes.status} ${text}`
|
|
6384
|
+
}
|
|
6385
|
+
};
|
|
6386
|
+
}
|
|
6387
|
+
const json = SessionsResponse.safeParse(await upstreamRes.json());
|
|
6388
|
+
if (!json.success) {
|
|
6389
|
+
return {
|
|
6390
|
+
status: 500,
|
|
6391
|
+
json: {
|
|
6392
|
+
message: "Could not parse AI sessions response: " + fromError(json.error).toString()
|
|
6393
|
+
}
|
|
6394
|
+
};
|
|
6395
|
+
}
|
|
6396
|
+
return {
|
|
6397
|
+
status: 200,
|
|
6398
|
+
json: json.data
|
|
6399
|
+
};
|
|
6400
|
+
} catch (err) {
|
|
6401
|
+
return {
|
|
6402
|
+
status: 500,
|
|
6403
|
+
json: {
|
|
6404
|
+
message: err instanceof Error ? err.message : "AI sessions error"
|
|
6405
|
+
}
|
|
6406
|
+
};
|
|
6407
|
+
}
|
|
6408
|
+
};
|
|
6409
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6410
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6411
|
+
}
|
|
6412
|
+
if (!options.valSecret) {
|
|
6413
|
+
return {
|
|
6414
|
+
status: 500,
|
|
6415
|
+
json: {
|
|
6416
|
+
message: "Secret is not configured"
|
|
6417
|
+
}
|
|
6418
|
+
};
|
|
6419
|
+
}
|
|
6420
|
+
return withAuth(options.valSecret, cookies, "ai/sessions", data => {
|
|
6421
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6422
|
+
});
|
|
6423
|
+
},
|
|
6424
|
+
PATCH: async req => {
|
|
6425
|
+
const cookies = req.cookies;
|
|
6426
|
+
const auth = getAuth(cookies);
|
|
6427
|
+
if (auth.error) {
|
|
6428
|
+
return {
|
|
6429
|
+
status: 401,
|
|
6430
|
+
json: {
|
|
6431
|
+
message: auth.error
|
|
6432
|
+
}
|
|
6433
|
+
};
|
|
6434
|
+
}
|
|
6435
|
+
if (!options.project) {
|
|
6436
|
+
return {
|
|
6437
|
+
status: 500,
|
|
6438
|
+
json: {
|
|
6439
|
+
message: "Project is not configured"
|
|
6440
|
+
}
|
|
6441
|
+
};
|
|
6442
|
+
}
|
|
6443
|
+
const pathParts = (req.path || "").split("/").filter(Boolean);
|
|
6444
|
+
const sessionId = pathParts[0];
|
|
6445
|
+
if (!sessionId) {
|
|
6446
|
+
return {
|
|
6447
|
+
status: 500,
|
|
6448
|
+
json: {
|
|
6449
|
+
message: "Missing sessionId in path"
|
|
6450
|
+
}
|
|
6451
|
+
};
|
|
6452
|
+
}
|
|
6453
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6454
|
+
if (authDataRes.status !== 200) {
|
|
6455
|
+
return {
|
|
6456
|
+
status: 500,
|
|
6457
|
+
json: {
|
|
6458
|
+
message: authDataRes.json.message
|
|
6459
|
+
}
|
|
6460
|
+
};
|
|
6461
|
+
}
|
|
6462
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6463
|
+
const execFetch = async headers => {
|
|
6464
|
+
try {
|
|
6465
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions/${encodeURIComponent(sessionId)}`;
|
|
6466
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6467
|
+
method: "PATCH",
|
|
6468
|
+
headers,
|
|
6469
|
+
body: JSON.stringify({
|
|
6470
|
+
name: req.body.name
|
|
6471
|
+
})
|
|
6472
|
+
});
|
|
6473
|
+
if (!upstreamRes.ok) {
|
|
6474
|
+
const text = await upstreamRes.text();
|
|
6475
|
+
return {
|
|
6476
|
+
status: 500,
|
|
6477
|
+
json: {
|
|
6478
|
+
message: `AI session rename failed: ${upstreamRes.status} ${text}`
|
|
6479
|
+
}
|
|
6480
|
+
};
|
|
6481
|
+
}
|
|
6482
|
+
return {
|
|
6483
|
+
status: 200,
|
|
6484
|
+
json: {}
|
|
6485
|
+
};
|
|
6486
|
+
} catch (err) {
|
|
6487
|
+
return {
|
|
6488
|
+
status: 500,
|
|
6489
|
+
json: {
|
|
6490
|
+
message: err instanceof Error ? err.message : "AI session rename error"
|
|
6491
|
+
}
|
|
6492
|
+
};
|
|
6493
|
+
}
|
|
6494
|
+
};
|
|
6495
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6496
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6497
|
+
}
|
|
6498
|
+
if (!options.valSecret) {
|
|
6499
|
+
return {
|
|
6500
|
+
status: 500,
|
|
6501
|
+
json: {
|
|
6502
|
+
message: "Secret is not configured"
|
|
6503
|
+
}
|
|
6504
|
+
};
|
|
6505
|
+
}
|
|
6506
|
+
return withAuth(options.valSecret, cookies, "ai/sessions/rename", data => {
|
|
6507
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6508
|
+
});
|
|
6509
|
+
}
|
|
6510
|
+
},
|
|
6511
|
+
"/ai/messages": {
|
|
6512
|
+
GET: async req => {
|
|
6513
|
+
const cookies = req.cookies;
|
|
6514
|
+
const auth = getAuth(cookies);
|
|
6515
|
+
if (auth.error) {
|
|
6516
|
+
return {
|
|
6517
|
+
status: 401,
|
|
6518
|
+
json: {
|
|
6519
|
+
message: auth.error
|
|
6520
|
+
}
|
|
6521
|
+
};
|
|
6522
|
+
}
|
|
6523
|
+
if (!options.project) {
|
|
6524
|
+
return {
|
|
6525
|
+
status: 500,
|
|
6526
|
+
json: {
|
|
6527
|
+
message: "Project is not configured"
|
|
6528
|
+
}
|
|
6529
|
+
};
|
|
6530
|
+
}
|
|
6531
|
+
const pathParts = (req.path || "").split("/").filter(Boolean);
|
|
6532
|
+
const sessionId = pathParts[0];
|
|
6533
|
+
if (!sessionId) {
|
|
6534
|
+
return {
|
|
6535
|
+
status: 500,
|
|
6536
|
+
json: {
|
|
6537
|
+
message: "Missing sessionId in path"
|
|
6538
|
+
}
|
|
6539
|
+
};
|
|
6540
|
+
}
|
|
6541
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6542
|
+
if (authDataRes.status !== 200) {
|
|
6543
|
+
return {
|
|
6544
|
+
status: 500,
|
|
6545
|
+
json: {
|
|
6546
|
+
message: authDataRes.json.message
|
|
6547
|
+
}
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6551
|
+
const execFetch = async headers => {
|
|
6552
|
+
try {
|
|
6553
|
+
const SessionMessagesResponse = z.object({
|
|
6554
|
+
messages: z.array(z.object({
|
|
6555
|
+
role: z.string(),
|
|
6556
|
+
content: AIMessageContent
|
|
6557
|
+
})),
|
|
6558
|
+
nextCursor: z.object({
|
|
6559
|
+
updatedAt: z.string(),
|
|
6560
|
+
id: z.string()
|
|
6561
|
+
}).nullable().optional()
|
|
6562
|
+
});
|
|
6563
|
+
const params = new URLSearchParams();
|
|
6564
|
+
if (req.query.limit) params.set("limit", req.query.limit);
|
|
6565
|
+
if (req.query.cursor_updatedAt) params.set("cursor_updatedAt", req.query.cursor_updatedAt);
|
|
6566
|
+
if (req.query.cursor_id) params.set("cursor_id", req.query.cursor_id);
|
|
6567
|
+
const qs = params.toString();
|
|
6568
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions/${encodeURIComponent(sessionId)}/messages` + (qs ? `?${qs}` : "");
|
|
6569
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6570
|
+
headers
|
|
6571
|
+
});
|
|
6572
|
+
if (!upstreamRes.ok) {
|
|
6573
|
+
const text = await upstreamRes.text();
|
|
6574
|
+
return {
|
|
6575
|
+
status: 500,
|
|
6576
|
+
json: {
|
|
6577
|
+
message: `AI session messages failed: ${upstreamRes.status} ${text}`
|
|
6578
|
+
}
|
|
6579
|
+
};
|
|
6580
|
+
}
|
|
6581
|
+
const json = SessionMessagesResponse.safeParse(await upstreamRes.json());
|
|
6582
|
+
if (!json.success) {
|
|
6583
|
+
return {
|
|
6584
|
+
status: 500,
|
|
6585
|
+
json: {
|
|
6586
|
+
message: "Could not parse AI session messages response: " + fromError(json.error).toString()
|
|
6587
|
+
}
|
|
6588
|
+
};
|
|
6589
|
+
}
|
|
6590
|
+
return {
|
|
6591
|
+
status: 200,
|
|
6592
|
+
json: json.data
|
|
6593
|
+
};
|
|
6594
|
+
} catch (err) {
|
|
6595
|
+
return {
|
|
6596
|
+
status: 500,
|
|
6597
|
+
json: {
|
|
6598
|
+
message: err instanceof Error ? err.message : "AI session messages error"
|
|
6599
|
+
}
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
};
|
|
6603
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6604
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6605
|
+
}
|
|
6606
|
+
if (!options.valSecret) {
|
|
6607
|
+
return {
|
|
6608
|
+
status: 500,
|
|
6609
|
+
json: {
|
|
6610
|
+
message: "Secret is not configured"
|
|
6611
|
+
}
|
|
6612
|
+
};
|
|
6613
|
+
}
|
|
6614
|
+
return withAuth(options.valSecret, cookies, "ai/sessions/messages", data => {
|
|
6615
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6616
|
+
});
|
|
6617
|
+
}
|
|
6618
|
+
},
|
|
6619
|
+
"/ai/session-image-to-patch-file": {
|
|
6620
|
+
POST: async req => {
|
|
6621
|
+
const cookies = req.cookies;
|
|
6622
|
+
const auth = getAuth(cookies);
|
|
6623
|
+
if (auth.error) {
|
|
6624
|
+
return {
|
|
6625
|
+
status: 401,
|
|
6626
|
+
json: {
|
|
6627
|
+
message: auth.error
|
|
6628
|
+
}
|
|
6629
|
+
};
|
|
6630
|
+
}
|
|
6631
|
+
if (!options.project) {
|
|
6632
|
+
return {
|
|
6633
|
+
status: 500,
|
|
6634
|
+
json: {
|
|
6635
|
+
message: "Project is not configured"
|
|
6636
|
+
}
|
|
6637
|
+
};
|
|
6638
|
+
}
|
|
6639
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6640
|
+
if (authDataRes.status !== 200) {
|
|
6641
|
+
return {
|
|
6642
|
+
status: 500,
|
|
6643
|
+
json: {
|
|
6644
|
+
message: authDataRes.json.message
|
|
6645
|
+
}
|
|
6646
|
+
};
|
|
6647
|
+
}
|
|
6648
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6649
|
+
let headers;
|
|
6650
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6651
|
+
headers = getProfileAuthHeaders(authData, null, "application/json");
|
|
6652
|
+
} else {
|
|
6653
|
+
if (!("id" in auth) || !auth.id) {
|
|
6654
|
+
return {
|
|
6655
|
+
status: 401,
|
|
6656
|
+
json: {
|
|
6657
|
+
message: "Unauthorized"
|
|
6658
|
+
}
|
|
6659
|
+
};
|
|
6660
|
+
}
|
|
6661
|
+
headers = getProfileAuthHeaders(authData, {
|
|
6662
|
+
sub: auth.id
|
|
6663
|
+
}, "application/json");
|
|
6664
|
+
}
|
|
6665
|
+
const execFetch = async () => {
|
|
6666
|
+
try {
|
|
6667
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/patches/${encodeURIComponent(req.body.patchId)}/files/from-session-file`;
|
|
6668
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6669
|
+
method: "POST",
|
|
6670
|
+
headers,
|
|
6671
|
+
body: JSON.stringify({
|
|
6672
|
+
files: req.body.files.map(f => ({
|
|
6673
|
+
filePath: f.filePath,
|
|
6674
|
+
key: f.key,
|
|
6675
|
+
...(f.isRemote !== undefined ? {
|
|
6676
|
+
isRemote: f.isRemote
|
|
6677
|
+
} : {})
|
|
6678
|
+
}))
|
|
6679
|
+
})
|
|
6680
|
+
});
|
|
6681
|
+
if (!upstreamRes.ok) {
|
|
6682
|
+
var _upstreamJson;
|
|
6683
|
+
const text = await upstreamRes.text();
|
|
6684
|
+
let upstreamJson = null;
|
|
6685
|
+
try {
|
|
6686
|
+
upstreamJson = JSON.parse(text);
|
|
6687
|
+
} catch {
|
|
6688
|
+
upstreamJson = null;
|
|
6689
|
+
}
|
|
6690
|
+
if (upstreamRes.status === 400 && upstreamJson) {
|
|
6691
|
+
return {
|
|
6692
|
+
status: 400,
|
|
6693
|
+
json: {
|
|
6694
|
+
message: upstreamJson.message ?? `AI session image to patch failed: ${text}`,
|
|
6695
|
+
...(upstreamJson.details ? {
|
|
6696
|
+
details: upstreamJson.details
|
|
6697
|
+
} : {})
|
|
6698
|
+
}
|
|
6699
|
+
};
|
|
6700
|
+
}
|
|
6701
|
+
return {
|
|
6702
|
+
status: 500,
|
|
6703
|
+
json: {
|
|
6704
|
+
message: `AI session image to patch failed: ${upstreamRes.status} ${((_upstreamJson = upstreamJson) === null || _upstreamJson === void 0 ? void 0 : _upstreamJson.message) ?? text}`
|
|
6705
|
+
}
|
|
6706
|
+
};
|
|
6707
|
+
}
|
|
6708
|
+
const rawUpstreamJson = await upstreamRes.json();
|
|
6709
|
+
const UpstreamResponse = z.object({
|
|
6710
|
+
patchId: z.string(),
|
|
6711
|
+
files: z.array(z.object({
|
|
6712
|
+
filePath: z.string(),
|
|
6713
|
+
metadata: z.object({
|
|
6714
|
+
width: z.number(),
|
|
6715
|
+
height: z.number(),
|
|
6716
|
+
mimeType: z.string()
|
|
6717
|
+
})
|
|
6718
|
+
}))
|
|
6719
|
+
});
|
|
6720
|
+
const json = UpstreamResponse.safeParse(rawUpstreamJson);
|
|
6721
|
+
if (!json.success) {
|
|
6722
|
+
return {
|
|
6723
|
+
status: 500,
|
|
6724
|
+
json: {
|
|
6725
|
+
message: "Could not parse AI session image patch response: " + fromError(json.error).toString()
|
|
6726
|
+
}
|
|
6727
|
+
};
|
|
6728
|
+
}
|
|
6729
|
+
if (json.data.patchId !== req.body.patchId) {
|
|
6730
|
+
return {
|
|
6731
|
+
status: 500,
|
|
6732
|
+
json: {
|
|
6733
|
+
message: `AI session image to patch upstream returned mismatched patchId: expected '${req.body.patchId}', got '${json.data.patchId}'`
|
|
6734
|
+
}
|
|
6735
|
+
};
|
|
6736
|
+
}
|
|
6737
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6738
|
+
// Mirror the binaries from the content host into local patch
|
|
6739
|
+
// storage so /files?patch_id=... can serve them. Match upstream
|
|
6740
|
+
// entries to client-provided keys by filePath.
|
|
6741
|
+
const keysByFilePath = new Map(req.body.files.map(f => [f.filePath, f.key]));
|
|
6742
|
+
for (const file of json.data.files) {
|
|
6743
|
+
const sessionKey = keysByFilePath.get(file.filePath);
|
|
6744
|
+
if (!sessionKey) {
|
|
6745
|
+
return {
|
|
6746
|
+
status: 500,
|
|
6747
|
+
json: {
|
|
6748
|
+
message: `AI session image to patch: upstream returned file '${file.filePath}' which was not in the request`
|
|
6749
|
+
}
|
|
6750
|
+
};
|
|
6751
|
+
}
|
|
6752
|
+
const downloadUrl = `${options.valContentUrl}/v1/${options.project}/ai/images?key=${encodeURIComponent(sessionKey)}`;
|
|
6753
|
+
let binaryRes;
|
|
6754
|
+
try {
|
|
6755
|
+
binaryRes = await fetch(downloadUrl, {
|
|
6756
|
+
headers
|
|
6757
|
+
});
|
|
6758
|
+
} catch (err) {
|
|
6759
|
+
return {
|
|
6760
|
+
status: 500,
|
|
6761
|
+
json: {
|
|
6762
|
+
message: `Could not download AI session image '${file.filePath}' from content host: ${err instanceof Error ? err.message : String(err)}`
|
|
6763
|
+
}
|
|
6764
|
+
};
|
|
6765
|
+
}
|
|
6766
|
+
if (!binaryRes.ok) {
|
|
6767
|
+
return {
|
|
6768
|
+
status: 500,
|
|
6769
|
+
json: {
|
|
6770
|
+
message: `Could not download AI session image '${file.filePath}' from content host: ${binaryRes.status} ${binaryRes.statusText}`
|
|
6771
|
+
}
|
|
6772
|
+
};
|
|
6773
|
+
}
|
|
6774
|
+
const arrayBuffer = await binaryRes.arrayBuffer();
|
|
6775
|
+
const base64 = Buffer.from(arrayBuffer).toString("base64");
|
|
6776
|
+
const dataUrl = `data:${file.metadata.mimeType};base64,${base64}`;
|
|
6777
|
+
const type = file.metadata.mimeType.startsWith("image/") ? "image" : "file";
|
|
6778
|
+
const saveRes = await serverOps.saveBase64EncodedBinaryFileFromPatch(file.filePath, req.body.parentRef, req.body.patchId, dataUrl, type, file.metadata);
|
|
6779
|
+
if (saveRes.error) {
|
|
6780
|
+
return {
|
|
6781
|
+
status: 500,
|
|
6782
|
+
json: {
|
|
6783
|
+
message: `Could not save AI session image '${file.filePath}' to local patch storage: ${saveRes.error.message}`
|
|
6784
|
+
}
|
|
6785
|
+
};
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
return {
|
|
6790
|
+
status: 200,
|
|
6791
|
+
json: {
|
|
6792
|
+
patchId: req.body.patchId,
|
|
6793
|
+
files: json.data.files
|
|
6794
|
+
}
|
|
6795
|
+
};
|
|
6796
|
+
} catch (err) {
|
|
6797
|
+
return {
|
|
6798
|
+
status: 500,
|
|
6799
|
+
json: {
|
|
6800
|
+
message: err instanceof Error ? err.message : "AI session image to patch error"
|
|
6801
|
+
}
|
|
6802
|
+
};
|
|
6803
|
+
}
|
|
6804
|
+
};
|
|
6805
|
+
return execFetch();
|
|
6806
|
+
}
|
|
6807
|
+
},
|
|
6808
|
+
"/ai/images": {
|
|
6809
|
+
PATCH: async req => {
|
|
6810
|
+
const cookies = req.cookies;
|
|
6811
|
+
const auth = getAuth(cookies);
|
|
6812
|
+
if (auth.error) {
|
|
6813
|
+
return {
|
|
6814
|
+
status: 401,
|
|
6815
|
+
json: {
|
|
6816
|
+
message: auth.error
|
|
6817
|
+
}
|
|
6818
|
+
};
|
|
6819
|
+
}
|
|
6820
|
+
if (!options.project) {
|
|
6821
|
+
return {
|
|
6822
|
+
status: 500,
|
|
6823
|
+
json: {
|
|
6824
|
+
message: "Project is not configured"
|
|
6825
|
+
}
|
|
6826
|
+
};
|
|
6827
|
+
}
|
|
6828
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6829
|
+
if (authDataRes.status !== 200) {
|
|
6830
|
+
return {
|
|
6831
|
+
status: 500,
|
|
6832
|
+
json: {
|
|
6833
|
+
message: authDataRes.json.message
|
|
6834
|
+
}
|
|
6835
|
+
};
|
|
6836
|
+
}
|
|
6837
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6838
|
+
let headers;
|
|
6839
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6840
|
+
headers = getProfileAuthHeaders(authData, null, "application/json");
|
|
6841
|
+
} else {
|
|
6842
|
+
if (!("id" in auth) || !auth.id) {
|
|
6843
|
+
return {
|
|
6844
|
+
status: 401,
|
|
6845
|
+
json: {
|
|
6846
|
+
message: "Unauthorized"
|
|
6847
|
+
}
|
|
6848
|
+
};
|
|
6849
|
+
}
|
|
6850
|
+
headers = getProfileAuthHeaders(authData, {
|
|
6851
|
+
sub: auth.id
|
|
6852
|
+
}, "application/json");
|
|
6853
|
+
}
|
|
6854
|
+
const execFetch = async () => {
|
|
6855
|
+
try {
|
|
6856
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/images`;
|
|
6857
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6858
|
+
method: "PATCH",
|
|
6859
|
+
headers,
|
|
6860
|
+
body: JSON.stringify({
|
|
6861
|
+
key: req.body.key,
|
|
6862
|
+
metadata: req.body.metadata,
|
|
6863
|
+
contentType: req.body.contentType
|
|
6864
|
+
})
|
|
6865
|
+
});
|
|
6866
|
+
if (!upstreamRes.ok) {
|
|
6867
|
+
const text = await upstreamRes.text();
|
|
6868
|
+
return {
|
|
6869
|
+
status: 500,
|
|
6870
|
+
json: {
|
|
6871
|
+
message: `AI images patch failed: ${upstreamRes.status} ${text}`
|
|
6872
|
+
}
|
|
6873
|
+
};
|
|
6874
|
+
}
|
|
6875
|
+
const UpstreamResponse = z.object({
|
|
6876
|
+
key: z.string()
|
|
6877
|
+
});
|
|
6878
|
+
const json = UpstreamResponse.safeParse(await upstreamRes.json());
|
|
6879
|
+
if (!json.success) {
|
|
6880
|
+
return {
|
|
6881
|
+
status: 500,
|
|
6882
|
+
json: {
|
|
6883
|
+
message: "Could not parse AI images patch response: " + fromError(json.error).toString()
|
|
6884
|
+
}
|
|
6885
|
+
};
|
|
6886
|
+
}
|
|
6887
|
+
return {
|
|
6888
|
+
status: 200,
|
|
6889
|
+
json: {
|
|
6890
|
+
key: json.data.key
|
|
6891
|
+
}
|
|
6892
|
+
};
|
|
6893
|
+
} catch (err) {
|
|
6894
|
+
return {
|
|
6895
|
+
status: 500,
|
|
6896
|
+
json: {
|
|
6897
|
+
message: err instanceof Error ? err.message : "AI images patch error"
|
|
6898
|
+
}
|
|
6899
|
+
};
|
|
6900
|
+
}
|
|
6901
|
+
};
|
|
6902
|
+
return execFetch();
|
|
6903
|
+
}
|
|
6904
|
+
},
|
|
6905
|
+
//#region files
|
|
6906
|
+
"/files": {
|
|
6907
|
+
GET: async req => {
|
|
6908
|
+
const query = req.query;
|
|
6909
|
+
const filePath = req.path;
|
|
6910
|
+
// NOTE: no auth here since you would need the patch_id to get something that is not published.
|
|
6911
|
+
// For everything that is published, well they are already public so no auth required there...
|
|
6912
|
+
// We could imagine adding auth just to be a 200% certain,
|
|
6913
|
+
// However that won't work since images are requested by the nextjs backend as a part of image optimization (again: as an example) which is a backend-to-backend op (no cookies, ...).
|
|
6914
|
+
// So: 1) patch ids are not possible to guess (but possible to brute force)
|
|
6915
|
+
// 2) the process of shimming a patch into the frontend would be quite challenging (so just trying out this attack would require a lot of effort)
|
|
6916
|
+
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
|
6917
|
+
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
|
6918
|
+
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
|
6919
|
+
let cacheControl;
|
|
6920
|
+
let fileBuffer;
|
|
6921
|
+
let mimeType;
|
|
6922
|
+
const remote = query.remote === "true";
|
|
6923
|
+
if (query.patch_id) {
|
|
6924
|
+
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id, remote);
|
|
6925
|
+
mimeType = Internal.filenameToMimeType(filePath);
|
|
6926
|
+
// TODO: reenable this:
|
|
6927
|
+
// cacheControl = "public, max-age=20000, immutable";
|
|
6928
|
+
} else {
|
|
6929
|
+
if (serverOps instanceof ValOpsHttp && remote) {
|
|
6930
|
+
console.error(`Remote file: ${filePath} requested without patch id. This is most likely a bug in Val.`);
|
|
6931
|
+
}
|
|
6932
|
+
fileBuffer = await serverOps.getBinaryFile(filePath);
|
|
6933
|
+
}
|
|
6934
|
+
if (fileBuffer) {
|
|
6935
|
+
return {
|
|
6936
|
+
status: 200,
|
|
6937
|
+
headers: {
|
|
6938
|
+
// TODO: we could use ETag and return 304 instead
|
|
6939
|
+
"Content-Type": mimeType || "application/octet-stream",
|
|
6940
|
+
"Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
|
|
6941
|
+
},
|
|
6942
|
+
body: bufferToReadableStream(fileBuffer)
|
|
6943
|
+
};
|
|
6944
|
+
} else {
|
|
6945
|
+
return {
|
|
6946
|
+
status: 404,
|
|
6947
|
+
json: {
|
|
6948
|
+
message: "File not found"
|
|
6949
|
+
}
|
|
6950
|
+
};
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
};
|
|
6955
|
+
};
|
|
6956
|
+
function formatPatchSourceError(error) {
|
|
6957
|
+
if ("message" in error) {
|
|
6958
|
+
return error.message;
|
|
6959
|
+
} else if (Array.isArray(error)) {
|
|
6960
|
+
return error.map(formatPatchSourceError).join("\n");
|
|
6961
|
+
} else {
|
|
6962
|
+
const _exhaustiveCheck = error;
|
|
6963
|
+
return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
function verifyCallbackReq(stateCookie, queryParams) {
|
|
6967
|
+
if (typeof stateCookie !== "string") {
|
|
6968
|
+
return {
|
|
6969
|
+
success: false,
|
|
6970
|
+
error: "No state cookie"
|
|
6971
|
+
};
|
|
6972
|
+
}
|
|
6973
|
+
const {
|
|
6974
|
+
code,
|
|
6975
|
+
state: tokenFromQuery
|
|
6976
|
+
} = queryParams;
|
|
6977
|
+
if (typeof code !== "string") {
|
|
6978
|
+
return {
|
|
6979
|
+
success: false,
|
|
6980
|
+
error: "No code query param"
|
|
6981
|
+
};
|
|
6982
|
+
}
|
|
6161
6983
|
if (typeof tokenFromQuery !== "string") {
|
|
6162
6984
|
return {
|
|
6163
6985
|
success: false,
|
|
@@ -6301,6 +7123,21 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
|
|
|
6301
7123
|
};
|
|
6302
7124
|
}
|
|
6303
7125
|
}
|
|
7126
|
+
function getProfileAuthHeaders(auth, data, type) {
|
|
7127
|
+
if ("pat" in auth) {
|
|
7128
|
+
return {
|
|
7129
|
+
"x-val-pat": auth.pat,
|
|
7130
|
+
"Content-Type": type
|
|
7131
|
+
};
|
|
7132
|
+
}
|
|
7133
|
+
if ("apiKey" in auth && data) {
|
|
7134
|
+
return {
|
|
7135
|
+
...getAuthHeaders(auth.apiKey, type),
|
|
7136
|
+
"x-val-profile-id": data.sub
|
|
7137
|
+
};
|
|
7138
|
+
}
|
|
7139
|
+
throw new Error("Invalid auth");
|
|
7140
|
+
}
|
|
6304
7141
|
function getAuthHeaders(token, type) {
|
|
6305
7142
|
if (!type) {
|
|
6306
7143
|
return {
|