@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
|
@@ -2405,8 +2405,8 @@ class ValOps {
|
|
|
2405
2405
|
}
|
|
2406
2406
|
|
|
2407
2407
|
// #region createPatch
|
|
2408
|
-
async createPatch(path, patch, patchId, parentRef, authorId) {
|
|
2409
|
-
const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId);
|
|
2408
|
+
async createPatch(path, patch, patchId, parentRef, sessionId, authorId) {
|
|
2409
|
+
const saveRes = await this.saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId);
|
|
2410
2410
|
if (fp.result.isErr(saveRes)) {
|
|
2411
2411
|
console.error(`Could not save source patch at path: '${path}'. Error: ${saveRes.error.errorType === "other" ? saveRes.error.message : saveRes.error.errorType}`);
|
|
2412
2412
|
if (saveRes.error.errorType === "patch-head-conflict") {
|
|
@@ -2610,6 +2610,76 @@ class ValOpsFS extends ValOps {
|
|
|
2610
2610
|
async onInit() {
|
|
2611
2611
|
// do nothing
|
|
2612
2612
|
}
|
|
2613
|
+
async getPresignedAuthNonce(project, corsOrigin, auth) {
|
|
2614
|
+
const authHeader = "pat" in auth ? {
|
|
2615
|
+
"x-val-pat": auth.pat
|
|
2616
|
+
} : {
|
|
2617
|
+
Authorization: `Bearer ${auth.apiKey}`
|
|
2618
|
+
};
|
|
2619
|
+
try {
|
|
2620
|
+
const res = await fetch(`${this.contentUrl}/v1/${project}/presigned-auth-nonce`, {
|
|
2621
|
+
method: "POST",
|
|
2622
|
+
headers: {
|
|
2623
|
+
...authHeader,
|
|
2624
|
+
"Content-Type": "application/json"
|
|
2625
|
+
},
|
|
2626
|
+
body: JSON.stringify({
|
|
2627
|
+
corsOrigin
|
|
2628
|
+
})
|
|
2629
|
+
});
|
|
2630
|
+
if (res.ok) {
|
|
2631
|
+
const json = await res.json();
|
|
2632
|
+
const parsed = zod.z.object({
|
|
2633
|
+
nonce: zod.z.string(),
|
|
2634
|
+
expiresAt: zod.z.string()
|
|
2635
|
+
}).safeParse(json);
|
|
2636
|
+
if (parsed.success) {
|
|
2637
|
+
return {
|
|
2638
|
+
status: "success",
|
|
2639
|
+
data: {
|
|
2640
|
+
nonce: parsed.data.nonce,
|
|
2641
|
+
baseUrl: `${this.contentUrl}/v1/${project}`
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
console.error("Could not parse presigned auth nonce response. Error: " + zodValidationError.fromError(parsed.error));
|
|
2646
|
+
return {
|
|
2647
|
+
status: "error",
|
|
2648
|
+
statusCode: 500,
|
|
2649
|
+
error: {
|
|
2650
|
+
message: "Could not get presigned auth nonce. The response from the content host was not in the expected format."
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
if (res.status === 401) {
|
|
2655
|
+
return {
|
|
2656
|
+
status: "error",
|
|
2657
|
+
statusCode: 401,
|
|
2658
|
+
error: {
|
|
2659
|
+
message: "Could not get presigned auth nonce. The local PAT was rejected by the content host. Try re-running `val login`."
|
|
2660
|
+
}
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
const unknownErrorMessage = `Could not get presigned auth nonce. HTTP error: ${res.status} ${res.statusText}`;
|
|
2664
|
+
console.error(unknownErrorMessage);
|
|
2665
|
+
return {
|
|
2666
|
+
status: "error",
|
|
2667
|
+
statusCode: 500,
|
|
2668
|
+
error: {
|
|
2669
|
+
message: unknownErrorMessage
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
} catch (e) {
|
|
2673
|
+
console.error("Could not get presigned auth nonce (connection error?):", e);
|
|
2674
|
+
return {
|
|
2675
|
+
status: "error",
|
|
2676
|
+
statusCode: 500,
|
|
2677
|
+
error: {
|
|
2678
|
+
message: `Could not get presigned auth nonce. Error: ${e instanceof Error ? e.message : JSON.stringify(e)}`
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2613
2683
|
async getCommitSummary() {
|
|
2614
2684
|
return {
|
|
2615
2685
|
error: {
|
|
@@ -2798,11 +2868,17 @@ class ValOpsFS extends ValOps {
|
|
|
2798
2868
|
parentPatchId: dir
|
|
2799
2869
|
});
|
|
2800
2870
|
} else {
|
|
2801
|
-
|
|
2871
|
+
const patchId = parsedPatch.data.patchId;
|
|
2872
|
+
if (includes && includes.length > 0 && !includes.includes(patchId)) {
|
|
2802
2873
|
return;
|
|
2803
2874
|
}
|
|
2804
|
-
patches[
|
|
2805
|
-
|
|
2875
|
+
patches[patchId] = {
|
|
2876
|
+
path: parsedPatch.data.path,
|
|
2877
|
+
patch: parsedPatch.data.patch,
|
|
2878
|
+
parentRef: parsedPatch.data.parentRef,
|
|
2879
|
+
baseSha: parsedPatch.data.baseSha,
|
|
2880
|
+
createdAt: parsedPatch.data.createdAt,
|
|
2881
|
+
authorId: parsedPatch.data.authorId,
|
|
2806
2882
|
appliedAt: null
|
|
2807
2883
|
};
|
|
2808
2884
|
}
|
|
@@ -2973,7 +3049,7 @@ class ValOpsFS extends ValOps {
|
|
|
2973
3049
|
};
|
|
2974
3050
|
}
|
|
2975
3051
|
}
|
|
2976
|
-
async saveSourceFilePatch(path, patch, patchId, parentRef, authorId) {
|
|
3052
|
+
async saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId) {
|
|
2977
3053
|
const patchDir = this.getParentPatchIdFromParentRef(parentRef);
|
|
2978
3054
|
try {
|
|
2979
3055
|
const baseSha = await this.getBaseSha();
|
|
@@ -2983,6 +3059,7 @@ class ValOpsFS extends ValOps {
|
|
|
2983
3059
|
parentRef,
|
|
2984
3060
|
path,
|
|
2985
3061
|
authorId,
|
|
3062
|
+
sessionId,
|
|
2986
3063
|
baseSha,
|
|
2987
3064
|
coreVersion: core.Internal.VERSION.core,
|
|
2988
3065
|
createdAt: new Date().toISOString()
|
|
@@ -3421,12 +3498,6 @@ class ValOpsFS extends ValOps {
|
|
|
3421
3498
|
return fp.result.ok(Object.fromEntries(Object.entries(patches.patches).map(([patchId, value]) => [patchId, this.getParentPatchIdFromParentRef(value.parentRef)])));
|
|
3422
3499
|
}
|
|
3423
3500
|
|
|
3424
|
-
// #region profiles
|
|
3425
|
-
async getProfiles() {
|
|
3426
|
-
// We do not have profiles in FS mode
|
|
3427
|
-
return [];
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
3501
|
// #region fs file path helpers
|
|
3431
3502
|
getPatchesDir() {
|
|
3432
3503
|
return path__namespace["default"].join(this.rootDir, ValOpsFS.VAL_DIR, "patches");
|
|
@@ -3535,7 +3606,9 @@ const FSPatch = zod.z.object({
|
|
|
3535
3606
|
parentRef: internal.ParentRef,
|
|
3536
3607
|
authorId: zod.z.string().refine(p => true).nullable(),
|
|
3537
3608
|
createdAt: zod.z.string().datetime(),
|
|
3538
|
-
coreVersion: zod.z.string().nullable()
|
|
3609
|
+
coreVersion: zod.z.string().nullable(),
|
|
3610
|
+
// TODO: use this to check if patch is compatible with current core version?
|
|
3611
|
+
sessionId: zod.z.string().nullable()
|
|
3539
3612
|
});
|
|
3540
3613
|
const FSPatchBase = zod.z.object({
|
|
3541
3614
|
baseSha: zod.z.string().refine(p => true),
|
|
@@ -3637,17 +3710,6 @@ const CommitResponse = zod.z.object({
|
|
|
3637
3710
|
commit: CommitSha,
|
|
3638
3711
|
branch: zod.z.string()
|
|
3639
3712
|
});
|
|
3640
|
-
const ProfilesResponse = zod.z.object({
|
|
3641
|
-
profiles: zod.z.array(zod.z.object({
|
|
3642
|
-
profileId: zod.z.string(),
|
|
3643
|
-
fullName: zod.z.string(),
|
|
3644
|
-
email: zod.z.string().optional(),
|
|
3645
|
-
// TODO: make this required once this can be guaranteed
|
|
3646
|
-
avatar: zod.z.object({
|
|
3647
|
-
url: zod.z.string()
|
|
3648
|
-
}).nullable()
|
|
3649
|
-
}))
|
|
3650
|
-
});
|
|
3651
3713
|
const NonceResponse = zod.z.object({
|
|
3652
3714
|
nonce: zod.z.string(),
|
|
3653
3715
|
url: zod.z.string()
|
|
@@ -3939,7 +4001,7 @@ class ValOpsHttp extends ValOps {
|
|
|
3939
4001
|
return {
|
|
3940
4002
|
status: "error",
|
|
3941
4003
|
error: {
|
|
3942
|
-
message: "Could not get nonce." + message
|
|
4004
|
+
message: "Could not get nonce. " + message
|
|
3943
4005
|
}
|
|
3944
4006
|
};
|
|
3945
4007
|
}
|
|
@@ -4098,7 +4160,7 @@ class ValOpsHttp extends ValOps {
|
|
|
4098
4160
|
};
|
|
4099
4161
|
}
|
|
4100
4162
|
}
|
|
4101
|
-
async saveSourceFilePatch(path, patch, patchId, parentRef, authorId) {
|
|
4163
|
+
async saveSourceFilePatch(path, patch, patchId, parentRef, authorId, sessionId) {
|
|
4102
4164
|
const baseSha = await this.getBaseSha();
|
|
4103
4165
|
return fetch(`${this.contentUrl}/v1/${this.project}/patches`, {
|
|
4104
4166
|
method: "POST",
|
|
@@ -4110,6 +4172,7 @@ class ValOpsHttp extends ValOps {
|
|
|
4110
4172
|
path,
|
|
4111
4173
|
patch,
|
|
4112
4174
|
authorId,
|
|
4175
|
+
sessionId,
|
|
4113
4176
|
patchId,
|
|
4114
4177
|
parentPatchId: parentRef.type === "patch" ? parentRef.patchId : null,
|
|
4115
4178
|
baseSha,
|
|
@@ -4533,31 +4596,6 @@ class ValOpsHttp extends ValOps {
|
|
|
4533
4596
|
};
|
|
4534
4597
|
}
|
|
4535
4598
|
}
|
|
4536
|
-
|
|
4537
|
-
// #region profiles
|
|
4538
|
-
async getProfiles() {
|
|
4539
|
-
var _res$headers$get6;
|
|
4540
|
-
const res = await fetch(`${this.contentUrl}/v1/${this.project}/profiles`, {
|
|
4541
|
-
headers: {
|
|
4542
|
-
...this.authHeaders,
|
|
4543
|
-
"Content-Type": "application/json"
|
|
4544
|
-
}
|
|
4545
|
-
});
|
|
4546
|
-
if (res.ok) {
|
|
4547
|
-
const parsed = ProfilesResponse.safeParse(await res.json());
|
|
4548
|
-
if (parsed.error) {
|
|
4549
|
-
console.error("Could not parse profiles response", parsed.error);
|
|
4550
|
-
throw Error(`Could not get profiles from remote server: wrong format. You might need to upgrade Val.`);
|
|
4551
|
-
}
|
|
4552
|
-
return parsed.data.profiles;
|
|
4553
|
-
}
|
|
4554
|
-
if ((_res$headers$get6 = res.headers.get("Content-Type")) !== null && _res$headers$get6 !== void 0 && _res$headers$get6.includes("application/json")) {
|
|
4555
|
-
const json = await res.json();
|
|
4556
|
-
const message = internal.getErrorMessageFromUnknownJson(json, "Unknown error");
|
|
4557
|
-
throw Error(`Could not get profiles (status: ${res.status}): ${message}`);
|
|
4558
|
-
}
|
|
4559
|
-
throw Error(`Could not get profiles. Got status: ${res.status}`);
|
|
4560
|
-
}
|
|
4561
4599
|
}
|
|
4562
4600
|
|
|
4563
4601
|
const host = process.env.VAL_CONTENT_URL || core.DEFAULT_CONTENT_HOST;
|
|
@@ -4710,6 +4748,24 @@ function hasRemoteFileSchema(schema) {
|
|
|
4710
4748
|
|
|
4711
4749
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
4712
4750
|
const ValServer = (valModules, options, callbacks) => {
|
|
4751
|
+
const AIContentBlock = zod.z.union([zod.z.object({
|
|
4752
|
+
type: zod.z.literal("text"),
|
|
4753
|
+
text: zod.z.string()
|
|
4754
|
+
}), zod.z.object({
|
|
4755
|
+
type: zod.z.literal("image_url"),
|
|
4756
|
+
url: zod.z.string()
|
|
4757
|
+
})]);
|
|
4758
|
+
const AIMessageContent = zod.z.union([zod.z.string(), zod.z.array(AIContentBlock)]);
|
|
4759
|
+
const ProfilesResponse = zod.z.object({
|
|
4760
|
+
profiles: zod.z.array(zod.z.object({
|
|
4761
|
+
profileId: zod.z.string(),
|
|
4762
|
+
fullName: zod.z.string(),
|
|
4763
|
+
email: zod.z.string().optional(),
|
|
4764
|
+
avatar: zod.z.object({
|
|
4765
|
+
url: zod.z.string()
|
|
4766
|
+
}).nullable()
|
|
4767
|
+
}))
|
|
4768
|
+
});
|
|
4713
4769
|
let serverOps;
|
|
4714
4770
|
if (options.mode === "fs") {
|
|
4715
4771
|
serverOps = new ValOpsFS(options.valContentUrl, options.cwd, valModules, {
|
|
@@ -5497,15 +5553,38 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5497
5553
|
};
|
|
5498
5554
|
}
|
|
5499
5555
|
if (serverOps instanceof ValOpsFS) {
|
|
5500
|
-
// In FS mode
|
|
5501
|
-
//
|
|
5502
|
-
//
|
|
5556
|
+
// In FS mode patch-file uploads are buffered through this server (no remote round-trip),
|
|
5557
|
+
// so baseUrl points at /api/val/upload. AI image uploads, however, go straight to the
|
|
5558
|
+
// content host — we resolve a contentBaseUrl + a PAT-issued nonce here so the browser
|
|
5559
|
+
// can POST directly without exposing the PAT.
|
|
5503
5560
|
const host = `/api/val`;
|
|
5561
|
+
let contentBaseUrl = null;
|
|
5562
|
+
let contentAuthNonce = null;
|
|
5563
|
+
if (!options.project) {
|
|
5564
|
+
console.warn("Direct content-host uploads (AI images) disabled: no `project` set in val.config (and VAL_PROJECT env var is not set).");
|
|
5565
|
+
} else {
|
|
5566
|
+
const authDataRes = await getRemoteFileAuth();
|
|
5567
|
+
if (authDataRes.status !== 200) {
|
|
5568
|
+
console.warn("Direct content-host uploads (AI images) disabled: " + authDataRes.json.message);
|
|
5569
|
+
} else {
|
|
5570
|
+
const corsOrigin = "*"; // TODO: add cors origin
|
|
5571
|
+
const presignedAuthNonce = await serverOps.getPresignedAuthNonce(options.project, corsOrigin, authDataRes.json.remoteFileAuth);
|
|
5572
|
+
if (presignedAuthNonce.status === "success") {
|
|
5573
|
+
contentBaseUrl = presignedAuthNonce.data.baseUrl;
|
|
5574
|
+
contentAuthNonce = presignedAuthNonce.data.nonce;
|
|
5575
|
+
} else {
|
|
5576
|
+
console.warn("Direct content-host uploads (AI images) disabled: " + presignedAuthNonce.error.message);
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5504
5580
|
return {
|
|
5505
5581
|
status: 200,
|
|
5506
5582
|
json: {
|
|
5507
5583
|
nonce: null,
|
|
5508
|
-
baseUrl: `${host}/upload
|
|
5584
|
+
baseUrl: `${host}/upload`,
|
|
5585
|
+
// NOTE: this is the /upload/patches endpoint - the client will add /patches/:patchId/files to this and post to it
|
|
5586
|
+
contentBaseUrl,
|
|
5587
|
+
contentAuthNonce
|
|
5509
5588
|
}
|
|
5510
5589
|
};
|
|
5511
5590
|
}
|
|
@@ -5531,7 +5610,9 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5531
5610
|
status: 200,
|
|
5532
5611
|
json: {
|
|
5533
5612
|
nonce: presignedAuthNonce.data.nonce,
|
|
5534
|
-
baseUrl: presignedAuthNonce.data.baseUrl
|
|
5613
|
+
baseUrl: presignedAuthNonce.data.baseUrl,
|
|
5614
|
+
contentBaseUrl: presignedAuthNonce.data.baseUrl,
|
|
5615
|
+
contentAuthNonce: presignedAuthNonce.data.nonce
|
|
5535
5616
|
}
|
|
5536
5617
|
};
|
|
5537
5618
|
}
|
|
@@ -5559,10 +5640,11 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5559
5640
|
}
|
|
5560
5641
|
const patches = req.body.patches;
|
|
5561
5642
|
let parentRef = req.body.parentRef;
|
|
5643
|
+
const sessionId = req.body.sessionId ?? null;
|
|
5562
5644
|
const authorId = "id" in auth ? auth.id : null;
|
|
5563
5645
|
const newPatchIds = [];
|
|
5564
5646
|
for (const patch of patches) {
|
|
5565
|
-
const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, authorId);
|
|
5647
|
+
const createPatchRes = await serverOps.createPatch(patch.path, patch.patch, patch.patchId, parentRef, sessionId, authorId);
|
|
5566
5648
|
if (fp.result.isErr(createPatchRes)) {
|
|
5567
5649
|
if (createPatchRes.error.errorType === "patch-head-conflict") {
|
|
5568
5650
|
return {
|
|
@@ -5923,13 +6005,87 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
5923
6005
|
}
|
|
5924
6006
|
};
|
|
5925
6007
|
}
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
6008
|
+
if (!options.project) {
|
|
6009
|
+
return {
|
|
6010
|
+
status: 500,
|
|
6011
|
+
json: {
|
|
6012
|
+
message: "Project is not configured"
|
|
6013
|
+
}
|
|
6014
|
+
};
|
|
6015
|
+
}
|
|
6016
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6017
|
+
if (authDataRes.status !== 200) {
|
|
6018
|
+
if (serverOps instanceof ValOpsFS && authDataRes.json.errorCode === "pat-error") {
|
|
6019
|
+
return {
|
|
6020
|
+
status: 200,
|
|
6021
|
+
json: {
|
|
6022
|
+
profiles: []
|
|
6023
|
+
}
|
|
6024
|
+
};
|
|
6025
|
+
}
|
|
6026
|
+
return {
|
|
6027
|
+
status: 500,
|
|
6028
|
+
json: {
|
|
6029
|
+
message: authDataRes.json.message
|
|
6030
|
+
}
|
|
6031
|
+
};
|
|
6032
|
+
}
|
|
6033
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6034
|
+
const execFetch = async headers => {
|
|
6035
|
+
try {
|
|
6036
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/profiles`;
|
|
6037
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6038
|
+
method: "GET",
|
|
6039
|
+
headers
|
|
6040
|
+
});
|
|
6041
|
+
if (!upstreamRes.ok) {
|
|
6042
|
+
const text = await upstreamRes.text();
|
|
6043
|
+
const isAuthError = upstreamRes.status === 401 || upstreamRes.status === 403;
|
|
6044
|
+
return {
|
|
6045
|
+
status: isAuthError ? 401 : 500,
|
|
6046
|
+
json: {
|
|
6047
|
+
message: isAuthError ? `Profile authentication failed: ${upstreamRes.status} ${text}` : `Profiles failed: ${upstreamRes.status} ${text}`
|
|
6048
|
+
}
|
|
6049
|
+
};
|
|
6050
|
+
}
|
|
6051
|
+
const parseRes = ProfilesResponse.safeParse(await upstreamRes.json());
|
|
6052
|
+
if (!parseRes.success) {
|
|
6053
|
+
return {
|
|
6054
|
+
status: 500,
|
|
6055
|
+
json: {
|
|
6056
|
+
message: "Could not parse profiles response: " + zodValidationError.fromError(parseRes.error).toString()
|
|
6057
|
+
}
|
|
6058
|
+
};
|
|
6059
|
+
}
|
|
6060
|
+
return {
|
|
6061
|
+
status: 200,
|
|
6062
|
+
json: {
|
|
6063
|
+
profiles: parseRes.data.profiles
|
|
6064
|
+
}
|
|
6065
|
+
};
|
|
6066
|
+
} catch (err) {
|
|
6067
|
+
return {
|
|
6068
|
+
status: 500,
|
|
6069
|
+
json: {
|
|
6070
|
+
message: err instanceof Error ? err.message : "Profiles request failed"
|
|
6071
|
+
}
|
|
6072
|
+
};
|
|
5931
6073
|
}
|
|
5932
6074
|
};
|
|
6075
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6076
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6077
|
+
}
|
|
6078
|
+
if (!options.valSecret) {
|
|
6079
|
+
return {
|
|
6080
|
+
status: 500,
|
|
6081
|
+
json: {
|
|
6082
|
+
message: "Secret is not configured"
|
|
6083
|
+
}
|
|
6084
|
+
};
|
|
6085
|
+
}
|
|
6086
|
+
return withAuth(options.valSecret, cookies, "profiles", data => {
|
|
6087
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6088
|
+
});
|
|
5933
6089
|
}
|
|
5934
6090
|
},
|
|
5935
6091
|
"/commit-summary": {
|
|
@@ -6111,84 +6267,750 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
6111
6267
|
}
|
|
6112
6268
|
}
|
|
6113
6269
|
},
|
|
6114
|
-
//#region
|
|
6115
|
-
"/
|
|
6116
|
-
|
|
6117
|
-
const
|
|
6118
|
-
const
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
|
6127
|
-
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
|
6128
|
-
let cacheControl;
|
|
6129
|
-
let fileBuffer;
|
|
6130
|
-
let mimeType;
|
|
6131
|
-
const remote = query.remote === "true";
|
|
6132
|
-
if (query.patch_id) {
|
|
6133
|
-
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id, remote);
|
|
6134
|
-
mimeType = core.Internal.filenameToMimeType(filePath);
|
|
6135
|
-
// TODO: reenable this:
|
|
6136
|
-
// cacheControl = "public, max-age=20000, immutable";
|
|
6137
|
-
} else {
|
|
6138
|
-
if (serverOps instanceof ValOpsHttp && remote) {
|
|
6139
|
-
console.error(`Remote file: ${filePath} requested without patch id. This is most likely a bug in Val.`);
|
|
6140
|
-
}
|
|
6141
|
-
fileBuffer = await serverOps.getBinaryFile(filePath);
|
|
6270
|
+
//#region ai proxy
|
|
6271
|
+
"/ai/initialize": {
|
|
6272
|
+
POST: async req => {
|
|
6273
|
+
const cookies = req.cookies;
|
|
6274
|
+
const auth = getAuth(cookies);
|
|
6275
|
+
if (auth.error) {
|
|
6276
|
+
return {
|
|
6277
|
+
status: 401,
|
|
6278
|
+
json: {
|
|
6279
|
+
message: auth.error
|
|
6280
|
+
}
|
|
6281
|
+
};
|
|
6142
6282
|
}
|
|
6143
|
-
if (
|
|
6283
|
+
if (!options.project) {
|
|
6144
6284
|
return {
|
|
6145
|
-
status:
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
"Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
|
|
6150
|
-
},
|
|
6151
|
-
body: bufferToReadableStream(fileBuffer)
|
|
6285
|
+
status: 401,
|
|
6286
|
+
json: {
|
|
6287
|
+
message: "Project is not configured"
|
|
6288
|
+
}
|
|
6152
6289
|
};
|
|
6153
|
-
}
|
|
6290
|
+
}
|
|
6291
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6292
|
+
if (authDataRes.status !== 200) {
|
|
6154
6293
|
return {
|
|
6155
|
-
status:
|
|
6294
|
+
status: 401,
|
|
6156
6295
|
json: {
|
|
6157
|
-
message:
|
|
6296
|
+
message: authDataRes.json.message
|
|
6297
|
+
}
|
|
6298
|
+
};
|
|
6299
|
+
}
|
|
6300
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6301
|
+
const execFetch = async headers => {
|
|
6302
|
+
try {
|
|
6303
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/initialize`;
|
|
6304
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6305
|
+
method: "POST",
|
|
6306
|
+
headers,
|
|
6307
|
+
body: JSON.stringify({})
|
|
6308
|
+
});
|
|
6309
|
+
if (!upstreamRes.ok) {
|
|
6310
|
+
const text = await upstreamRes.text();
|
|
6311
|
+
return {
|
|
6312
|
+
status: 500,
|
|
6313
|
+
json: {
|
|
6314
|
+
message: `AI initialize failed: ${upstreamRes.status} ${text}`
|
|
6315
|
+
}
|
|
6316
|
+
};
|
|
6317
|
+
}
|
|
6318
|
+
const json = await upstreamRes.json();
|
|
6319
|
+
const wsUrl = options.valContentUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:") + `/v1/${options.project}/ai/connect`;
|
|
6320
|
+
return {
|
|
6321
|
+
status: 200,
|
|
6322
|
+
json: {
|
|
6323
|
+
nonce: json.nonce,
|
|
6324
|
+
wsUrl
|
|
6325
|
+
}
|
|
6326
|
+
};
|
|
6327
|
+
} catch (err) {
|
|
6328
|
+
return {
|
|
6329
|
+
status: 500,
|
|
6330
|
+
json: {
|
|
6331
|
+
message: err instanceof Error ? err.message : "AI initialize error"
|
|
6332
|
+
}
|
|
6333
|
+
};
|
|
6334
|
+
}
|
|
6335
|
+
};
|
|
6336
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6337
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6338
|
+
}
|
|
6339
|
+
if (!options.valSecret) {
|
|
6340
|
+
return {
|
|
6341
|
+
status: 500,
|
|
6342
|
+
json: {
|
|
6343
|
+
message: "Secret is not configured"
|
|
6158
6344
|
}
|
|
6159
6345
|
};
|
|
6160
6346
|
}
|
|
6347
|
+
return withAuth(options.valSecret, cookies, "ai/initialize", data => {
|
|
6348
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6349
|
+
});
|
|
6161
6350
|
}
|
|
6162
|
-
}
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
}
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6351
|
+
},
|
|
6352
|
+
"/ai/sessions": {
|
|
6353
|
+
GET: async req => {
|
|
6354
|
+
const cookies = req.cookies;
|
|
6355
|
+
const auth = getAuth(cookies);
|
|
6356
|
+
if (auth.error) {
|
|
6357
|
+
return {
|
|
6358
|
+
status: 401,
|
|
6359
|
+
json: {
|
|
6360
|
+
message: auth.error
|
|
6361
|
+
}
|
|
6362
|
+
};
|
|
6363
|
+
}
|
|
6364
|
+
if (!options.project) {
|
|
6365
|
+
return {
|
|
6366
|
+
status: 500,
|
|
6367
|
+
json: {
|
|
6368
|
+
message: "Project is not configured"
|
|
6369
|
+
}
|
|
6370
|
+
};
|
|
6371
|
+
}
|
|
6372
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6373
|
+
if (authDataRes.status !== 200) {
|
|
6374
|
+
return {
|
|
6375
|
+
status: 500,
|
|
6376
|
+
json: {
|
|
6377
|
+
message: authDataRes.json.message
|
|
6378
|
+
}
|
|
6379
|
+
};
|
|
6380
|
+
}
|
|
6381
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6382
|
+
const execFetch = async headers => {
|
|
6383
|
+
try {
|
|
6384
|
+
const SessionsResponse = zod.z.object({
|
|
6385
|
+
sessions: zod.z.array(zod.z.object({
|
|
6386
|
+
id: zod.z.string(),
|
|
6387
|
+
name: zod.z.string().nullable(),
|
|
6388
|
+
createdAt: zod.z.string(),
|
|
6389
|
+
updatedAt: zod.z.string()
|
|
6390
|
+
})),
|
|
6391
|
+
nextCursor: zod.z.object({
|
|
6392
|
+
updatedAt: zod.z.string(),
|
|
6393
|
+
id: zod.z.string()
|
|
6394
|
+
}).nullable().optional()
|
|
6395
|
+
});
|
|
6396
|
+
const params = new URLSearchParams();
|
|
6397
|
+
if (req.query.limit) params.set("limit", req.query.limit);
|
|
6398
|
+
if (req.query.cursor_updatedAt) {
|
|
6399
|
+
params.set("cursor_updatedAt", req.query.cursor_updatedAt);
|
|
6400
|
+
}
|
|
6401
|
+
if (req.query.cursor_id) {
|
|
6402
|
+
params.set("cursor_id", req.query.cursor_id);
|
|
6403
|
+
}
|
|
6404
|
+
const qs = params.toString();
|
|
6405
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions${qs ? `?${qs}` : ""}`;
|
|
6406
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6407
|
+
headers
|
|
6408
|
+
});
|
|
6409
|
+
if (!upstreamRes.ok) {
|
|
6410
|
+
const text = await upstreamRes.text();
|
|
6411
|
+
return {
|
|
6412
|
+
status: 500,
|
|
6413
|
+
json: {
|
|
6414
|
+
message: `AI sessions failed: ${upstreamRes.status} ${text}`
|
|
6415
|
+
}
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
const json = SessionsResponse.safeParse(await upstreamRes.json());
|
|
6419
|
+
if (!json.success) {
|
|
6420
|
+
return {
|
|
6421
|
+
status: 500,
|
|
6422
|
+
json: {
|
|
6423
|
+
message: "Could not parse AI sessions response: " + zodValidationError.fromError(json.error).toString()
|
|
6424
|
+
}
|
|
6425
|
+
};
|
|
6426
|
+
}
|
|
6427
|
+
return {
|
|
6428
|
+
status: 200,
|
|
6429
|
+
json: json.data
|
|
6430
|
+
};
|
|
6431
|
+
} catch (err) {
|
|
6432
|
+
return {
|
|
6433
|
+
status: 500,
|
|
6434
|
+
json: {
|
|
6435
|
+
message: err instanceof Error ? err.message : "AI sessions error"
|
|
6436
|
+
}
|
|
6437
|
+
};
|
|
6438
|
+
}
|
|
6439
|
+
};
|
|
6440
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6441
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6442
|
+
}
|
|
6443
|
+
if (!options.valSecret) {
|
|
6444
|
+
return {
|
|
6445
|
+
status: 500,
|
|
6446
|
+
json: {
|
|
6447
|
+
message: "Secret is not configured"
|
|
6448
|
+
}
|
|
6449
|
+
};
|
|
6450
|
+
}
|
|
6451
|
+
return withAuth(options.valSecret, cookies, "ai/sessions", data => {
|
|
6452
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6453
|
+
});
|
|
6454
|
+
},
|
|
6455
|
+
PATCH: async req => {
|
|
6456
|
+
const cookies = req.cookies;
|
|
6457
|
+
const auth = getAuth(cookies);
|
|
6458
|
+
if (auth.error) {
|
|
6459
|
+
return {
|
|
6460
|
+
status: 401,
|
|
6461
|
+
json: {
|
|
6462
|
+
message: auth.error
|
|
6463
|
+
}
|
|
6464
|
+
};
|
|
6465
|
+
}
|
|
6466
|
+
if (!options.project) {
|
|
6467
|
+
return {
|
|
6468
|
+
status: 500,
|
|
6469
|
+
json: {
|
|
6470
|
+
message: "Project is not configured"
|
|
6471
|
+
}
|
|
6472
|
+
};
|
|
6473
|
+
}
|
|
6474
|
+
const pathParts = (req.path || "").split("/").filter(Boolean);
|
|
6475
|
+
const sessionId = pathParts[0];
|
|
6476
|
+
if (!sessionId) {
|
|
6477
|
+
return {
|
|
6478
|
+
status: 500,
|
|
6479
|
+
json: {
|
|
6480
|
+
message: "Missing sessionId in path"
|
|
6481
|
+
}
|
|
6482
|
+
};
|
|
6483
|
+
}
|
|
6484
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6485
|
+
if (authDataRes.status !== 200) {
|
|
6486
|
+
return {
|
|
6487
|
+
status: 500,
|
|
6488
|
+
json: {
|
|
6489
|
+
message: authDataRes.json.message
|
|
6490
|
+
}
|
|
6491
|
+
};
|
|
6492
|
+
}
|
|
6493
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6494
|
+
const execFetch = async headers => {
|
|
6495
|
+
try {
|
|
6496
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions/${encodeURIComponent(sessionId)}`;
|
|
6497
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6498
|
+
method: "PATCH",
|
|
6499
|
+
headers,
|
|
6500
|
+
body: JSON.stringify({
|
|
6501
|
+
name: req.body.name
|
|
6502
|
+
})
|
|
6503
|
+
});
|
|
6504
|
+
if (!upstreamRes.ok) {
|
|
6505
|
+
const text = await upstreamRes.text();
|
|
6506
|
+
return {
|
|
6507
|
+
status: 500,
|
|
6508
|
+
json: {
|
|
6509
|
+
message: `AI session rename failed: ${upstreamRes.status} ${text}`
|
|
6510
|
+
}
|
|
6511
|
+
};
|
|
6512
|
+
}
|
|
6513
|
+
return {
|
|
6514
|
+
status: 200,
|
|
6515
|
+
json: {}
|
|
6516
|
+
};
|
|
6517
|
+
} catch (err) {
|
|
6518
|
+
return {
|
|
6519
|
+
status: 500,
|
|
6520
|
+
json: {
|
|
6521
|
+
message: err instanceof Error ? err.message : "AI session rename error"
|
|
6522
|
+
}
|
|
6523
|
+
};
|
|
6524
|
+
}
|
|
6525
|
+
};
|
|
6526
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6527
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6528
|
+
}
|
|
6529
|
+
if (!options.valSecret) {
|
|
6530
|
+
return {
|
|
6531
|
+
status: 500,
|
|
6532
|
+
json: {
|
|
6533
|
+
message: "Secret is not configured"
|
|
6534
|
+
}
|
|
6535
|
+
};
|
|
6536
|
+
}
|
|
6537
|
+
return withAuth(options.valSecret, cookies, "ai/sessions/rename", data => {
|
|
6538
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6539
|
+
});
|
|
6540
|
+
}
|
|
6541
|
+
},
|
|
6542
|
+
"/ai/messages": {
|
|
6543
|
+
GET: async req => {
|
|
6544
|
+
const cookies = req.cookies;
|
|
6545
|
+
const auth = getAuth(cookies);
|
|
6546
|
+
if (auth.error) {
|
|
6547
|
+
return {
|
|
6548
|
+
status: 401,
|
|
6549
|
+
json: {
|
|
6550
|
+
message: auth.error
|
|
6551
|
+
}
|
|
6552
|
+
};
|
|
6553
|
+
}
|
|
6554
|
+
if (!options.project) {
|
|
6555
|
+
return {
|
|
6556
|
+
status: 500,
|
|
6557
|
+
json: {
|
|
6558
|
+
message: "Project is not configured"
|
|
6559
|
+
}
|
|
6560
|
+
};
|
|
6561
|
+
}
|
|
6562
|
+
const pathParts = (req.path || "").split("/").filter(Boolean);
|
|
6563
|
+
const sessionId = pathParts[0];
|
|
6564
|
+
if (!sessionId) {
|
|
6565
|
+
return {
|
|
6566
|
+
status: 500,
|
|
6567
|
+
json: {
|
|
6568
|
+
message: "Missing sessionId in path"
|
|
6569
|
+
}
|
|
6570
|
+
};
|
|
6571
|
+
}
|
|
6572
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6573
|
+
if (authDataRes.status !== 200) {
|
|
6574
|
+
return {
|
|
6575
|
+
status: 500,
|
|
6576
|
+
json: {
|
|
6577
|
+
message: authDataRes.json.message
|
|
6578
|
+
}
|
|
6579
|
+
};
|
|
6580
|
+
}
|
|
6581
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6582
|
+
const execFetch = async headers => {
|
|
6583
|
+
try {
|
|
6584
|
+
const SessionMessagesResponse = zod.z.object({
|
|
6585
|
+
messages: zod.z.array(zod.z.object({
|
|
6586
|
+
role: zod.z.string(),
|
|
6587
|
+
content: AIMessageContent
|
|
6588
|
+
})),
|
|
6589
|
+
nextCursor: zod.z.object({
|
|
6590
|
+
updatedAt: zod.z.string(),
|
|
6591
|
+
id: zod.z.string()
|
|
6592
|
+
}).nullable().optional()
|
|
6593
|
+
});
|
|
6594
|
+
const params = new URLSearchParams();
|
|
6595
|
+
if (req.query.limit) params.set("limit", req.query.limit);
|
|
6596
|
+
if (req.query.cursor_updatedAt) params.set("cursor_updatedAt", req.query.cursor_updatedAt);
|
|
6597
|
+
if (req.query.cursor_id) params.set("cursor_id", req.query.cursor_id);
|
|
6598
|
+
const qs = params.toString();
|
|
6599
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/sessions/${encodeURIComponent(sessionId)}/messages` + (qs ? `?${qs}` : "");
|
|
6600
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6601
|
+
headers
|
|
6602
|
+
});
|
|
6603
|
+
if (!upstreamRes.ok) {
|
|
6604
|
+
const text = await upstreamRes.text();
|
|
6605
|
+
return {
|
|
6606
|
+
status: 500,
|
|
6607
|
+
json: {
|
|
6608
|
+
message: `AI session messages failed: ${upstreamRes.status} ${text}`
|
|
6609
|
+
}
|
|
6610
|
+
};
|
|
6611
|
+
}
|
|
6612
|
+
const json = SessionMessagesResponse.safeParse(await upstreamRes.json());
|
|
6613
|
+
if (!json.success) {
|
|
6614
|
+
return {
|
|
6615
|
+
status: 500,
|
|
6616
|
+
json: {
|
|
6617
|
+
message: "Could not parse AI session messages response: " + zodValidationError.fromError(json.error).toString()
|
|
6618
|
+
}
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
return {
|
|
6622
|
+
status: 200,
|
|
6623
|
+
json: json.data
|
|
6624
|
+
};
|
|
6625
|
+
} catch (err) {
|
|
6626
|
+
return {
|
|
6627
|
+
status: 500,
|
|
6628
|
+
json: {
|
|
6629
|
+
message: err instanceof Error ? err.message : "AI session messages error"
|
|
6630
|
+
}
|
|
6631
|
+
};
|
|
6632
|
+
}
|
|
6633
|
+
};
|
|
6634
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6635
|
+
return execFetch(getProfileAuthHeaders(authData, null, "application/json"));
|
|
6636
|
+
}
|
|
6637
|
+
if (!options.valSecret) {
|
|
6638
|
+
return {
|
|
6639
|
+
status: 500,
|
|
6640
|
+
json: {
|
|
6641
|
+
message: "Secret is not configured"
|
|
6642
|
+
}
|
|
6643
|
+
};
|
|
6644
|
+
}
|
|
6645
|
+
return withAuth(options.valSecret, cookies, "ai/sessions/messages", data => {
|
|
6646
|
+
return execFetch(getProfileAuthHeaders(authData, data, "application/json"));
|
|
6647
|
+
});
|
|
6648
|
+
}
|
|
6649
|
+
},
|
|
6650
|
+
"/ai/session-image-to-patch-file": {
|
|
6651
|
+
POST: async req => {
|
|
6652
|
+
const cookies = req.cookies;
|
|
6653
|
+
const auth = getAuth(cookies);
|
|
6654
|
+
if (auth.error) {
|
|
6655
|
+
return {
|
|
6656
|
+
status: 401,
|
|
6657
|
+
json: {
|
|
6658
|
+
message: auth.error
|
|
6659
|
+
}
|
|
6660
|
+
};
|
|
6661
|
+
}
|
|
6662
|
+
if (!options.project) {
|
|
6663
|
+
return {
|
|
6664
|
+
status: 500,
|
|
6665
|
+
json: {
|
|
6666
|
+
message: "Project is not configured"
|
|
6667
|
+
}
|
|
6668
|
+
};
|
|
6669
|
+
}
|
|
6670
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6671
|
+
if (authDataRes.status !== 200) {
|
|
6672
|
+
return {
|
|
6673
|
+
status: 500,
|
|
6674
|
+
json: {
|
|
6675
|
+
message: authDataRes.json.message
|
|
6676
|
+
}
|
|
6677
|
+
};
|
|
6678
|
+
}
|
|
6679
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6680
|
+
let headers;
|
|
6681
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6682
|
+
headers = getProfileAuthHeaders(authData, null, "application/json");
|
|
6683
|
+
} else {
|
|
6684
|
+
if (!("id" in auth) || !auth.id) {
|
|
6685
|
+
return {
|
|
6686
|
+
status: 401,
|
|
6687
|
+
json: {
|
|
6688
|
+
message: "Unauthorized"
|
|
6689
|
+
}
|
|
6690
|
+
};
|
|
6691
|
+
}
|
|
6692
|
+
headers = getProfileAuthHeaders(authData, {
|
|
6693
|
+
sub: auth.id
|
|
6694
|
+
}, "application/json");
|
|
6695
|
+
}
|
|
6696
|
+
const execFetch = async () => {
|
|
6697
|
+
try {
|
|
6698
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/patches/${encodeURIComponent(req.body.patchId)}/files/from-session-file`;
|
|
6699
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6700
|
+
method: "POST",
|
|
6701
|
+
headers,
|
|
6702
|
+
body: JSON.stringify({
|
|
6703
|
+
files: req.body.files.map(f => ({
|
|
6704
|
+
filePath: f.filePath,
|
|
6705
|
+
key: f.key,
|
|
6706
|
+
...(f.isRemote !== undefined ? {
|
|
6707
|
+
isRemote: f.isRemote
|
|
6708
|
+
} : {})
|
|
6709
|
+
}))
|
|
6710
|
+
})
|
|
6711
|
+
});
|
|
6712
|
+
if (!upstreamRes.ok) {
|
|
6713
|
+
var _upstreamJson;
|
|
6714
|
+
const text = await upstreamRes.text();
|
|
6715
|
+
let upstreamJson = null;
|
|
6716
|
+
try {
|
|
6717
|
+
upstreamJson = JSON.parse(text);
|
|
6718
|
+
} catch {
|
|
6719
|
+
upstreamJson = null;
|
|
6720
|
+
}
|
|
6721
|
+
if (upstreamRes.status === 400 && upstreamJson) {
|
|
6722
|
+
return {
|
|
6723
|
+
status: 400,
|
|
6724
|
+
json: {
|
|
6725
|
+
message: upstreamJson.message ?? `AI session image to patch failed: ${text}`,
|
|
6726
|
+
...(upstreamJson.details ? {
|
|
6727
|
+
details: upstreamJson.details
|
|
6728
|
+
} : {})
|
|
6729
|
+
}
|
|
6730
|
+
};
|
|
6731
|
+
}
|
|
6732
|
+
return {
|
|
6733
|
+
status: 500,
|
|
6734
|
+
json: {
|
|
6735
|
+
message: `AI session image to patch failed: ${upstreamRes.status} ${((_upstreamJson = upstreamJson) === null || _upstreamJson === void 0 ? void 0 : _upstreamJson.message) ?? text}`
|
|
6736
|
+
}
|
|
6737
|
+
};
|
|
6738
|
+
}
|
|
6739
|
+
const rawUpstreamJson = await upstreamRes.json();
|
|
6740
|
+
const UpstreamResponse = zod.z.object({
|
|
6741
|
+
patchId: zod.z.string(),
|
|
6742
|
+
files: zod.z.array(zod.z.object({
|
|
6743
|
+
filePath: zod.z.string(),
|
|
6744
|
+
metadata: zod.z.object({
|
|
6745
|
+
width: zod.z.number(),
|
|
6746
|
+
height: zod.z.number(),
|
|
6747
|
+
mimeType: zod.z.string()
|
|
6748
|
+
})
|
|
6749
|
+
}))
|
|
6750
|
+
});
|
|
6751
|
+
const json = UpstreamResponse.safeParse(rawUpstreamJson);
|
|
6752
|
+
if (!json.success) {
|
|
6753
|
+
return {
|
|
6754
|
+
status: 500,
|
|
6755
|
+
json: {
|
|
6756
|
+
message: "Could not parse AI session image patch response: " + zodValidationError.fromError(json.error).toString()
|
|
6757
|
+
}
|
|
6758
|
+
};
|
|
6759
|
+
}
|
|
6760
|
+
if (json.data.patchId !== req.body.patchId) {
|
|
6761
|
+
return {
|
|
6762
|
+
status: 500,
|
|
6763
|
+
json: {
|
|
6764
|
+
message: `AI session image to patch upstream returned mismatched patchId: expected '${req.body.patchId}', got '${json.data.patchId}'`
|
|
6765
|
+
}
|
|
6766
|
+
};
|
|
6767
|
+
}
|
|
6768
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6769
|
+
// Mirror the binaries from the content host into local patch
|
|
6770
|
+
// storage so /files?patch_id=... can serve them. Match upstream
|
|
6771
|
+
// entries to client-provided keys by filePath.
|
|
6772
|
+
const keysByFilePath = new Map(req.body.files.map(f => [f.filePath, f.key]));
|
|
6773
|
+
for (const file of json.data.files) {
|
|
6774
|
+
const sessionKey = keysByFilePath.get(file.filePath);
|
|
6775
|
+
if (!sessionKey) {
|
|
6776
|
+
return {
|
|
6777
|
+
status: 500,
|
|
6778
|
+
json: {
|
|
6779
|
+
message: `AI session image to patch: upstream returned file '${file.filePath}' which was not in the request`
|
|
6780
|
+
}
|
|
6781
|
+
};
|
|
6782
|
+
}
|
|
6783
|
+
const downloadUrl = `${options.valContentUrl}/v1/${options.project}/ai/images?key=${encodeURIComponent(sessionKey)}`;
|
|
6784
|
+
let binaryRes;
|
|
6785
|
+
try {
|
|
6786
|
+
binaryRes = await fetch(downloadUrl, {
|
|
6787
|
+
headers
|
|
6788
|
+
});
|
|
6789
|
+
} catch (err) {
|
|
6790
|
+
return {
|
|
6791
|
+
status: 500,
|
|
6792
|
+
json: {
|
|
6793
|
+
message: `Could not download AI session image '${file.filePath}' from content host: ${err instanceof Error ? err.message : String(err)}`
|
|
6794
|
+
}
|
|
6795
|
+
};
|
|
6796
|
+
}
|
|
6797
|
+
if (!binaryRes.ok) {
|
|
6798
|
+
return {
|
|
6799
|
+
status: 500,
|
|
6800
|
+
json: {
|
|
6801
|
+
message: `Could not download AI session image '${file.filePath}' from content host: ${binaryRes.status} ${binaryRes.statusText}`
|
|
6802
|
+
}
|
|
6803
|
+
};
|
|
6804
|
+
}
|
|
6805
|
+
const arrayBuffer = await binaryRes.arrayBuffer();
|
|
6806
|
+
const base64 = Buffer.from(arrayBuffer).toString("base64");
|
|
6807
|
+
const dataUrl = `data:${file.metadata.mimeType};base64,${base64}`;
|
|
6808
|
+
const type = file.metadata.mimeType.startsWith("image/") ? "image" : "file";
|
|
6809
|
+
const saveRes = await serverOps.saveBase64EncodedBinaryFileFromPatch(file.filePath, req.body.parentRef, req.body.patchId, dataUrl, type, file.metadata);
|
|
6810
|
+
if (saveRes.error) {
|
|
6811
|
+
return {
|
|
6812
|
+
status: 500,
|
|
6813
|
+
json: {
|
|
6814
|
+
message: `Could not save AI session image '${file.filePath}' to local patch storage: ${saveRes.error.message}`
|
|
6815
|
+
}
|
|
6816
|
+
};
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
return {
|
|
6821
|
+
status: 200,
|
|
6822
|
+
json: {
|
|
6823
|
+
patchId: req.body.patchId,
|
|
6824
|
+
files: json.data.files
|
|
6825
|
+
}
|
|
6826
|
+
};
|
|
6827
|
+
} catch (err) {
|
|
6828
|
+
return {
|
|
6829
|
+
status: 500,
|
|
6830
|
+
json: {
|
|
6831
|
+
message: err instanceof Error ? err.message : "AI session image to patch error"
|
|
6832
|
+
}
|
|
6833
|
+
};
|
|
6834
|
+
}
|
|
6835
|
+
};
|
|
6836
|
+
return execFetch();
|
|
6837
|
+
}
|
|
6838
|
+
},
|
|
6839
|
+
"/ai/images": {
|
|
6840
|
+
PATCH: async req => {
|
|
6841
|
+
const cookies = req.cookies;
|
|
6842
|
+
const auth = getAuth(cookies);
|
|
6843
|
+
if (auth.error) {
|
|
6844
|
+
return {
|
|
6845
|
+
status: 401,
|
|
6846
|
+
json: {
|
|
6847
|
+
message: auth.error
|
|
6848
|
+
}
|
|
6849
|
+
};
|
|
6850
|
+
}
|
|
6851
|
+
if (!options.project) {
|
|
6852
|
+
return {
|
|
6853
|
+
status: 500,
|
|
6854
|
+
json: {
|
|
6855
|
+
message: "Project is not configured"
|
|
6856
|
+
}
|
|
6857
|
+
};
|
|
6858
|
+
}
|
|
6859
|
+
const authDataRes = await getRemoteFileAuth();
|
|
6860
|
+
if (authDataRes.status !== 200) {
|
|
6861
|
+
return {
|
|
6862
|
+
status: 500,
|
|
6863
|
+
json: {
|
|
6864
|
+
message: authDataRes.json.message
|
|
6865
|
+
}
|
|
6866
|
+
};
|
|
6867
|
+
}
|
|
6868
|
+
const authData = authDataRes.json.remoteFileAuth;
|
|
6869
|
+
let headers;
|
|
6870
|
+
if (serverOps instanceof ValOpsFS) {
|
|
6871
|
+
headers = getProfileAuthHeaders(authData, null, "application/json");
|
|
6872
|
+
} else {
|
|
6873
|
+
if (!("id" in auth) || !auth.id) {
|
|
6874
|
+
return {
|
|
6875
|
+
status: 401,
|
|
6876
|
+
json: {
|
|
6877
|
+
message: "Unauthorized"
|
|
6878
|
+
}
|
|
6879
|
+
};
|
|
6880
|
+
}
|
|
6881
|
+
headers = getProfileAuthHeaders(authData, {
|
|
6882
|
+
sub: auth.id
|
|
6883
|
+
}, "application/json");
|
|
6884
|
+
}
|
|
6885
|
+
const execFetch = async () => {
|
|
6886
|
+
try {
|
|
6887
|
+
const upstreamUrl = `${options.valContentUrl}/v1/${options.project}/ai/images`;
|
|
6888
|
+
const upstreamRes = await fetch(upstreamUrl, {
|
|
6889
|
+
method: "PATCH",
|
|
6890
|
+
headers,
|
|
6891
|
+
body: JSON.stringify({
|
|
6892
|
+
key: req.body.key,
|
|
6893
|
+
metadata: req.body.metadata,
|
|
6894
|
+
contentType: req.body.contentType
|
|
6895
|
+
})
|
|
6896
|
+
});
|
|
6897
|
+
if (!upstreamRes.ok) {
|
|
6898
|
+
const text = await upstreamRes.text();
|
|
6899
|
+
return {
|
|
6900
|
+
status: 500,
|
|
6901
|
+
json: {
|
|
6902
|
+
message: `AI images patch failed: ${upstreamRes.status} ${text}`
|
|
6903
|
+
}
|
|
6904
|
+
};
|
|
6905
|
+
}
|
|
6906
|
+
const UpstreamResponse = zod.z.object({
|
|
6907
|
+
key: zod.z.string()
|
|
6908
|
+
});
|
|
6909
|
+
const json = UpstreamResponse.safeParse(await upstreamRes.json());
|
|
6910
|
+
if (!json.success) {
|
|
6911
|
+
return {
|
|
6912
|
+
status: 500,
|
|
6913
|
+
json: {
|
|
6914
|
+
message: "Could not parse AI images patch response: " + zodValidationError.fromError(json.error).toString()
|
|
6915
|
+
}
|
|
6916
|
+
};
|
|
6917
|
+
}
|
|
6918
|
+
return {
|
|
6919
|
+
status: 200,
|
|
6920
|
+
json: {
|
|
6921
|
+
key: json.data.key
|
|
6922
|
+
}
|
|
6923
|
+
};
|
|
6924
|
+
} catch (err) {
|
|
6925
|
+
return {
|
|
6926
|
+
status: 500,
|
|
6927
|
+
json: {
|
|
6928
|
+
message: err instanceof Error ? err.message : "AI images patch error"
|
|
6929
|
+
}
|
|
6930
|
+
};
|
|
6931
|
+
}
|
|
6932
|
+
};
|
|
6933
|
+
return execFetch();
|
|
6934
|
+
}
|
|
6935
|
+
},
|
|
6936
|
+
//#region files
|
|
6937
|
+
"/files": {
|
|
6938
|
+
GET: async req => {
|
|
6939
|
+
const query = req.query;
|
|
6940
|
+
const filePath = req.path;
|
|
6941
|
+
// NOTE: no auth here since you would need the patch_id to get something that is not published.
|
|
6942
|
+
// For everything that is published, well they are already public so no auth required there...
|
|
6943
|
+
// We could imagine adding auth just to be a 200% certain,
|
|
6944
|
+
// 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, ...).
|
|
6945
|
+
// So: 1) patch ids are not possible to guess (but possible to brute force)
|
|
6946
|
+
// 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)
|
|
6947
|
+
// 3) the benefit an attacker would get is an image that is not yet published (i.e. most cases: not very interesting)
|
|
6948
|
+
// Thus: attack surface + ease of attack + benefit = low probability of attack
|
|
6949
|
+
// If we couldn't argue that patch ids are secret enough, then this would be a problem.
|
|
6950
|
+
let cacheControl;
|
|
6951
|
+
let fileBuffer;
|
|
6952
|
+
let mimeType;
|
|
6953
|
+
const remote = query.remote === "true";
|
|
6954
|
+
if (query.patch_id) {
|
|
6955
|
+
fileBuffer = await serverOps.getBase64EncodedBinaryFileFromPatch(filePath, query.patch_id, remote);
|
|
6956
|
+
mimeType = core.Internal.filenameToMimeType(filePath);
|
|
6957
|
+
// TODO: reenable this:
|
|
6958
|
+
// cacheControl = "public, max-age=20000, immutable";
|
|
6959
|
+
} else {
|
|
6960
|
+
if (serverOps instanceof ValOpsHttp && remote) {
|
|
6961
|
+
console.error(`Remote file: ${filePath} requested without patch id. This is most likely a bug in Val.`);
|
|
6962
|
+
}
|
|
6963
|
+
fileBuffer = await serverOps.getBinaryFile(filePath);
|
|
6964
|
+
}
|
|
6965
|
+
if (fileBuffer) {
|
|
6966
|
+
return {
|
|
6967
|
+
status: 200,
|
|
6968
|
+
headers: {
|
|
6969
|
+
// TODO: we could use ETag and return 304 instead
|
|
6970
|
+
"Content-Type": mimeType || "application/octet-stream",
|
|
6971
|
+
"Cache-Control": cacheControl || "public, max-age=0, must-revalidate"
|
|
6972
|
+
},
|
|
6973
|
+
body: bufferToReadableStream(fileBuffer)
|
|
6974
|
+
};
|
|
6975
|
+
} else {
|
|
6976
|
+
return {
|
|
6977
|
+
status: 404,
|
|
6978
|
+
json: {
|
|
6979
|
+
message: "File not found"
|
|
6980
|
+
}
|
|
6981
|
+
};
|
|
6982
|
+
}
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
};
|
|
6986
|
+
};
|
|
6987
|
+
function formatPatchSourceError(error) {
|
|
6988
|
+
if ("message" in error) {
|
|
6989
|
+
return error.message;
|
|
6990
|
+
} else if (Array.isArray(error)) {
|
|
6991
|
+
return error.map(formatPatchSourceError).join("\n");
|
|
6992
|
+
} else {
|
|
6993
|
+
const _exhaustiveCheck = error;
|
|
6994
|
+
return "Unknown patch source error: " + JSON.stringify(_exhaustiveCheck);
|
|
6995
|
+
}
|
|
6996
|
+
}
|
|
6997
|
+
function verifyCallbackReq(stateCookie, queryParams) {
|
|
6998
|
+
if (typeof stateCookie !== "string") {
|
|
6999
|
+
return {
|
|
7000
|
+
success: false,
|
|
7001
|
+
error: "No state cookie"
|
|
7002
|
+
};
|
|
7003
|
+
}
|
|
7004
|
+
const {
|
|
7005
|
+
code,
|
|
7006
|
+
state: tokenFromQuery
|
|
7007
|
+
} = queryParams;
|
|
7008
|
+
if (typeof code !== "string") {
|
|
7009
|
+
return {
|
|
7010
|
+
success: false,
|
|
7011
|
+
error: "No code query param"
|
|
7012
|
+
};
|
|
7013
|
+
}
|
|
6192
7014
|
if (typeof tokenFromQuery !== "string") {
|
|
6193
7015
|
return {
|
|
6194
7016
|
success: false,
|
|
@@ -6332,6 +7154,21 @@ async function withAuth(secret, cookies, errorMessageType, handler) {
|
|
|
6332
7154
|
};
|
|
6333
7155
|
}
|
|
6334
7156
|
}
|
|
7157
|
+
function getProfileAuthHeaders(auth, data, type) {
|
|
7158
|
+
if ("pat" in auth) {
|
|
7159
|
+
return {
|
|
7160
|
+
"x-val-pat": auth.pat,
|
|
7161
|
+
"Content-Type": type
|
|
7162
|
+
};
|
|
7163
|
+
}
|
|
7164
|
+
if ("apiKey" in auth && data) {
|
|
7165
|
+
return {
|
|
7166
|
+
...getAuthHeaders(auth.apiKey, type),
|
|
7167
|
+
"x-val-profile-id": data.sub
|
|
7168
|
+
};
|
|
7169
|
+
}
|
|
7170
|
+
throw new Error("Invalid auth");
|
|
7171
|
+
}
|
|
6335
7172
|
function getAuthHeaders(token, type) {
|
|
6336
7173
|
if (!type) {
|
|
6337
7174
|
return {
|