@ultrst-armada/kloks 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -9
- package/packages/backend/dist/index.d.ts +820 -29
- package/packages/backend/dist/index.js +2739 -511
- package/packages/backend/dist/krnl-services.js +2 -2
- package/packages/state-graph/dist/index.js +10 -10
- package/packages/backend/dist/{chunk-CU7RZBMF.js → chunk-OYQKQAEF.js} +4 -4
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CompositeBackend,
|
|
3
|
-
EndpointGenerator,
|
|
4
|
-
createBackend,
|
|
5
|
-
createCompositeService,
|
|
6
|
-
createLocalService,
|
|
7
|
-
createRemoteService
|
|
8
|
-
} from "./chunk-P4HL4C6V.js";
|
|
9
1
|
import {
|
|
10
2
|
CANONICAL_SRKL_URL,
|
|
11
3
|
SrklClient,
|
|
@@ -23,6 +15,12 @@ import {
|
|
|
23
15
|
b2ListFileNames,
|
|
24
16
|
b2UploadFile
|
|
25
17
|
} from "./chunk-4QEMALXF.js";
|
|
18
|
+
import {
|
|
19
|
+
BaseApiRoute,
|
|
20
|
+
createSSEResponse,
|
|
21
|
+
withAuth,
|
|
22
|
+
withCors
|
|
23
|
+
} from "./chunk-TWCEVES2.js";
|
|
26
24
|
import {
|
|
27
25
|
SyncEngine,
|
|
28
26
|
WorkspaceCloneSyncEngine,
|
|
@@ -43,8 +41,7 @@ import {
|
|
|
43
41
|
StorageServiceFacade,
|
|
44
42
|
WorkspaceServiceDefinition,
|
|
45
43
|
WorkspaceServiceFacade
|
|
46
|
-
} from "./chunk-
|
|
47
|
-
import "./chunk-IPP4VURQ.js";
|
|
44
|
+
} from "./chunk-OYQKQAEF.js";
|
|
48
45
|
import {
|
|
49
46
|
BackblazeB2Adapter,
|
|
50
47
|
BaseStorageAdapter,
|
|
@@ -64,12 +61,6 @@ import {
|
|
|
64
61
|
formatCanonicalWorkspaceFolderName,
|
|
65
62
|
sanitizeWorkspaceName
|
|
66
63
|
} from "./chunk-FZKBTF34.js";
|
|
67
|
-
import {
|
|
68
|
-
BaseApiRoute,
|
|
69
|
-
createSSEResponse,
|
|
70
|
-
withAuth,
|
|
71
|
-
withCors
|
|
72
|
-
} from "./chunk-TWCEVES2.js";
|
|
73
64
|
import {
|
|
74
65
|
AdminGuard,
|
|
75
66
|
BaseAccountProvider,
|
|
@@ -133,6 +124,15 @@ import {
|
|
|
133
124
|
import {
|
|
134
125
|
IdempotencyManager
|
|
135
126
|
} from "./chunk-3EPY34R6.js";
|
|
127
|
+
import {
|
|
128
|
+
CompositeBackend,
|
|
129
|
+
EndpointGenerator,
|
|
130
|
+
createBackend,
|
|
131
|
+
createCompositeService,
|
|
132
|
+
createLocalService,
|
|
133
|
+
createRemoteService
|
|
134
|
+
} from "./chunk-P4HL4C6V.js";
|
|
135
|
+
import "./chunk-IPP4VURQ.js";
|
|
136
136
|
|
|
137
137
|
// src/data/BaseDataProvider.ts
|
|
138
138
|
import { SkipoBase } from "@ultrst-armada/kloks/functions";
|
|
@@ -203,7 +203,7 @@ function validateServiceDefinition(def) {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
// src/core/authCore.ts
|
|
206
|
-
import
|
|
206
|
+
import crypto2 from "crypto";
|
|
207
207
|
import bcrypt from "bcryptjs";
|
|
208
208
|
|
|
209
209
|
// src/core/unifiedDb.ts
|
|
@@ -274,8 +274,8 @@ async function loginUser(email, password, isAdmin = false, env) {
|
|
|
274
274
|
} else if (hash.startsWith("$2a$") || hash.startsWith("$2b$")) {
|
|
275
275
|
isMatch = bcrypt.compareSync(rawPassword, hash);
|
|
276
276
|
} else {
|
|
277
|
-
const saltedSha =
|
|
278
|
-
const rawSha =
|
|
277
|
+
const saltedSha = crypto2.createHash("sha256").update(rawPassword + "docustack_salt_2026_v2").digest("hex");
|
|
278
|
+
const rawSha = crypto2.createHash("sha256").update(rawPassword).digest("hex");
|
|
279
279
|
isMatch = hash === saltedSha || hash === rawSha;
|
|
280
280
|
}
|
|
281
281
|
if (!isMatch) {
|
|
@@ -1061,10 +1061,10 @@ async function getSrklAccounts2(userId, service, env) {
|
|
|
1061
1061
|
}
|
|
1062
1062
|
}
|
|
1063
1063
|
try {
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
const accPath =
|
|
1067
|
-
const raw = await
|
|
1064
|
+
const fs10 = await import("fs/promises");
|
|
1065
|
+
const path10 = await import("path");
|
|
1066
|
+
const accPath = path10.resolve(".skipo/accounts.json");
|
|
1067
|
+
const raw = await fs10.readFile(accPath, "utf-8");
|
|
1068
1068
|
const localUsers = JSON.parse(raw);
|
|
1069
1069
|
for (const u of Object.values(localUsers)) {
|
|
1070
1070
|
const conns = Array.isArray(u.storage_connections) ? u.storage_connections : [];
|
|
@@ -1138,182 +1138,2361 @@ async function listBackblazeBuckets(payload, rawUserId, env) {
|
|
|
1138
1138
|
} catch (err) {
|
|
1139
1139
|
console.warn("[SrklCore] Direct B2 bucket query failed, trying SRKL fallback:", err.message);
|
|
1140
1140
|
}
|
|
1141
|
-
const srklUrl = await getPrimarySrklDeploymentUrl2(env);
|
|
1142
|
-
const srklRes = await fetch(`${srklUrl}/api/backblaze/buckets`, {
|
|
1143
|
-
method: "POST",
|
|
1144
|
-
headers: { "Content-Type": "application/json", "x-user-id": userId || "" },
|
|
1145
|
-
body: JSON.stringify({ keyId, applicationKey: appKey, bucketId: bucketId || void 0 }),
|
|
1146
|
-
signal: AbortSignal.timeout(6e3)
|
|
1147
|
-
});
|
|
1148
|
-
if (srklRes.ok) {
|
|
1149
|
-
const data = await srklRes.json();
|
|
1150
|
-
if (data.success && Array.isArray(data.buckets)) return { success: true, buckets: data.buckets };
|
|
1141
|
+
const srklUrl = await getPrimarySrklDeploymentUrl2(env);
|
|
1142
|
+
const srklRes = await fetch(`${srklUrl}/api/backblaze/buckets`, {
|
|
1143
|
+
method: "POST",
|
|
1144
|
+
headers: { "Content-Type": "application/json", "x-user-id": userId || "" },
|
|
1145
|
+
body: JSON.stringify({ keyId, applicationKey: appKey, bucketId: bucketId || void 0 }),
|
|
1146
|
+
signal: AbortSignal.timeout(6e3)
|
|
1147
|
+
});
|
|
1148
|
+
if (srklRes.ok) {
|
|
1149
|
+
const data = await srklRes.json();
|
|
1150
|
+
if (data.success && Array.isArray(data.buckets)) return { success: true, buckets: data.buckets };
|
|
1151
|
+
}
|
|
1152
|
+
return { success: true, buckets: [] };
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
return { success: false, error: err.message || "Failed to list Backblaze buckets" };
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
var srklListBackblazeBuckets2 = (keyId, applicationKey, bucketId, bucketName, userId) => listBackblazeBuckets({ keyId, applicationKey, bucketId, bucketName }, userId);
|
|
1158
|
+
async function getSrklStatus(env) {
|
|
1159
|
+
const srklUrl = await getPrimarySrklDeploymentUrl2(env);
|
|
1160
|
+
const start = Date.now();
|
|
1161
|
+
try {
|
|
1162
|
+
const resp = await fetch(`${srklUrl}/api/ecosystem/health`, { signal: AbortSignal.timeout(5e3) });
|
|
1163
|
+
const latencyMs = Date.now() - start;
|
|
1164
|
+
if (resp.ok) {
|
|
1165
|
+
const data = await resp.json().catch(() => ({}));
|
|
1166
|
+
return { success: true, deploymentUrl: srklUrl, online: true, latencyMs, details: data };
|
|
1167
|
+
}
|
|
1168
|
+
return { success: false, deploymentUrl: srklUrl, online: false, status: resp.status, latencyMs };
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
return { success: false, deploymentUrl: srklUrl, online: false, error: err.message, latencyMs: Date.now() - start };
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/core/workspaceFileCore.ts
|
|
1175
|
+
async function getFs() {
|
|
1176
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
1177
|
+
try {
|
|
1178
|
+
const mod = await import("fs");
|
|
1179
|
+
return mod.promises || mod.default?.promises || mod;
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
async function getPath() {
|
|
1186
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
1187
|
+
try {
|
|
1188
|
+
const mod = await import("path");
|
|
1189
|
+
return mod.default || mod;
|
|
1190
|
+
} catch {
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
function getWorkspaceFolderPrefix(workspace) {
|
|
1196
|
+
return workspace.localFolderKey || workspace.localFolderName || (workspace.workspacePath ? workspace.workspacePath.replace(/^\.\//, "") : workspace.id);
|
|
1197
|
+
}
|
|
1198
|
+
function isB2Workspace(workspace, connection) {
|
|
1199
|
+
return connection?.provider === "backblaze_b2" || connection?.provider === "BACKBLAZE_B2" || workspace?.storageProvider === "backblaze_b2";
|
|
1200
|
+
}
|
|
1201
|
+
async function writeWorkspaceFile(workspace, connection, relativePath, content, contentType = "application/json") {
|
|
1202
|
+
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
1203
|
+
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
1204
|
+
const buffer = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
1205
|
+
if (isB2Workspace(workspace, connection)) {
|
|
1206
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
1207
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1208
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1209
|
+
const fullKey = `${prefix}/${cleanRel}`;
|
|
1210
|
+
await b2UploadFile2(keyId, appKey, bucketName, fullKey, buffer, contentType);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const fs10 = await getFs();
|
|
1214
|
+
const path10 = await getPath();
|
|
1215
|
+
if (!fs10 || !path10) throw new Error("Local filesystem operations not supported in this runtime.");
|
|
1216
|
+
const localDir = path10.resolve(process.cwd(), prefix, path10.dirname(cleanRel));
|
|
1217
|
+
await fs10.mkdir(localDir, { recursive: true });
|
|
1218
|
+
await fs10.writeFile(path10.resolve(process.cwd(), prefix, cleanRel), buffer);
|
|
1219
|
+
}
|
|
1220
|
+
async function readWorkspaceFile(workspace, connection, relativePath) {
|
|
1221
|
+
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
1222
|
+
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
1223
|
+
if (isB2Workspace(workspace, connection)) {
|
|
1224
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
1225
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1226
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1227
|
+
const fullKey = `${prefix}/${cleanRel}`;
|
|
1228
|
+
return await b2DownloadFile2(keyId, appKey, bucketName, fullKey);
|
|
1229
|
+
}
|
|
1230
|
+
const fs10 = await getFs();
|
|
1231
|
+
const path10 = await getPath();
|
|
1232
|
+
if (!fs10 || !path10) return null;
|
|
1233
|
+
const fullPath = path10.resolve(process.cwd(), prefix, cleanRel);
|
|
1234
|
+
try {
|
|
1235
|
+
const buf = await fs10.readFile(fullPath);
|
|
1236
|
+
return new Uint8Array(buf);
|
|
1237
|
+
} catch {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async function deleteWorkspaceFile(workspace, connection, relativePath) {
|
|
1242
|
+
const cleanRel = relativePath.replace(/^\/+/, "");
|
|
1243
|
+
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
1244
|
+
if (isB2Workspace(workspace, connection)) {
|
|
1245
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
1246
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1247
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1248
|
+
const fullKey = `${prefix}/${cleanRel}`;
|
|
1249
|
+
const files = await b2ListFileNames2(keyId, appKey, bucketName, fullKey);
|
|
1250
|
+
const match = files.find((f) => f.fileName === fullKey);
|
|
1251
|
+
if (match) {
|
|
1252
|
+
await b2DeleteFileVersion(keyId, appKey, match.fileName, match.fileId);
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
const fs10 = await getFs();
|
|
1258
|
+
const path10 = await getPath();
|
|
1259
|
+
if (!fs10 || !path10) return false;
|
|
1260
|
+
const fullPath = path10.resolve(process.cwd(), prefix, cleanRel);
|
|
1261
|
+
try {
|
|
1262
|
+
await fs10.unlink(fullPath);
|
|
1263
|
+
return true;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async function listWorkspaceFiles(workspace, connection, subPrefix = "") {
|
|
1269
|
+
const folderPrefix = getWorkspaceFolderPrefix(workspace);
|
|
1270
|
+
const targetPrefix = subPrefix ? `${folderPrefix}/${subPrefix.replace(/^\/+/, "")}` : `${folderPrefix}/`;
|
|
1271
|
+
if (isB2Workspace(workspace, connection)) {
|
|
1272
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
1273
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1274
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1275
|
+
const rawFiles = await b2ListFileNames2(keyId, appKey, bucketName, targetPrefix);
|
|
1276
|
+
return rawFiles.map((f) => ({
|
|
1277
|
+
fileName: f.fileName.slice(folderPrefix.length + 1),
|
|
1278
|
+
sizeBytes: f.contentLength,
|
|
1279
|
+
uploadTimestamp: f.uploadTimestamp
|
|
1280
|
+
}));
|
|
1281
|
+
}
|
|
1282
|
+
const fs10 = await getFs();
|
|
1283
|
+
const path10 = await getPath();
|
|
1284
|
+
if (!fs10 || !path10) return [];
|
|
1285
|
+
const baseDir = path10.resolve(process.cwd(), folderPrefix, subPrefix);
|
|
1286
|
+
try {
|
|
1287
|
+
const entries = await fs10.readdir(baseDir, { withFileTypes: true, recursive: true });
|
|
1288
|
+
const result = [];
|
|
1289
|
+
for (const entry of entries) {
|
|
1290
|
+
if (entry.isFile()) {
|
|
1291
|
+
const full = path10.join(entry.parentPath || baseDir, entry.name);
|
|
1292
|
+
const st = await fs10.stat(full);
|
|
1293
|
+
result.push({
|
|
1294
|
+
fileName: path10.relative(path10.resolve(process.cwd(), folderPrefix), full),
|
|
1295
|
+
sizeBytes: st.size
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return result;
|
|
1300
|
+
} catch {
|
|
1301
|
+
return [];
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
async function deleteWorkspaceFolder(workspace, connection) {
|
|
1305
|
+
const prefix = getWorkspaceFolderPrefix(workspace);
|
|
1306
|
+
if (isB2Workspace(workspace, connection)) {
|
|
1307
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
1308
|
+
const appKey = connection.b2ApplicationKey || connection.appKey;
|
|
1309
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1310
|
+
await b2DeleteFolder(keyId, appKey, bucketName, prefix);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const fs10 = await getFs();
|
|
1314
|
+
const path10 = await getPath();
|
|
1315
|
+
if (!fs10 || !path10) return;
|
|
1316
|
+
const localDir = path10.resolve(process.cwd(), prefix);
|
|
1317
|
+
try {
|
|
1318
|
+
await fs10.rm(localDir, { recursive: true, force: true });
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/core/credentialVaultCore.ts
|
|
1324
|
+
var MANIFEST_PATH = "credentials/manifest.json";
|
|
1325
|
+
var SECRET_PREFIX = "credentials/secrets";
|
|
1326
|
+
async function deriveAesKey(passphrase, salt) {
|
|
1327
|
+
const enc = new TextEncoder();
|
|
1328
|
+
const passKey = await crypto.subtle.importKey("raw", enc.encode(passphrase), "PBKDF2", false, ["deriveKey"]);
|
|
1329
|
+
return crypto.subtle.deriveKey(
|
|
1330
|
+
{ name: "PBKDF2", salt, iterations: 1e5, hash: "SHA-256" },
|
|
1331
|
+
passKey,
|
|
1332
|
+
{ name: "AES-GCM", length: 256 },
|
|
1333
|
+
false,
|
|
1334
|
+
["encrypt", "decrypt"]
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
async function encryptSecretToBlob(plaintext, vaultKey) {
|
|
1338
|
+
const str = typeof plaintext === "string" ? plaintext : JSON.stringify(plaintext);
|
|
1339
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1340
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1341
|
+
const key = await deriveAesKey(vaultKey, salt);
|
|
1342
|
+
const cipherBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(str));
|
|
1343
|
+
const blob = new Uint8Array(1 + 16 + 12 + cipherBuffer.byteLength);
|
|
1344
|
+
blob[0] = 1;
|
|
1345
|
+
blob.set(salt, 1);
|
|
1346
|
+
blob.set(iv, 1 + 16);
|
|
1347
|
+
blob.set(new Uint8Array(cipherBuffer), 1 + 16 + 12);
|
|
1348
|
+
return blob;
|
|
1349
|
+
}
|
|
1350
|
+
async function decryptSecretFromBlob(blob, vaultKey) {
|
|
1351
|
+
if (blob.length < 1 + 16 + 12 + 16 || blob[0] !== 1) {
|
|
1352
|
+
throw new Error("Invalid or corrupted credential vault blob.");
|
|
1353
|
+
}
|
|
1354
|
+
const salt = blob.slice(1, 17);
|
|
1355
|
+
const iv = blob.slice(17, 29);
|
|
1356
|
+
const ciphertext = blob.slice(29);
|
|
1357
|
+
const key = await deriveAesKey(vaultKey, salt);
|
|
1358
|
+
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
1359
|
+
const text = new TextDecoder().decode(decrypted);
|
|
1360
|
+
try {
|
|
1361
|
+
return JSON.parse(text);
|
|
1362
|
+
} catch {
|
|
1363
|
+
return text;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async function loadCredentialManifest(workspace, connection) {
|
|
1367
|
+
const data = await readWorkspaceFile(workspace, connection, MANIFEST_PATH);
|
|
1368
|
+
if (!data) return { version: 1, entries: [] };
|
|
1369
|
+
try {
|
|
1370
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
1371
|
+
} catch {
|
|
1372
|
+
return { version: 1, entries: [] };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
async function saveCredentialManifest(workspace, connection, manifest) {
|
|
1376
|
+
await writeWorkspaceFile(workspace, connection, MANIFEST_PATH, JSON.stringify(manifest, null, 2), "application/json");
|
|
1377
|
+
}
|
|
1378
|
+
async function listCredentials(workspace, connection) {
|
|
1379
|
+
const manifest = await loadCredentialManifest(workspace, connection);
|
|
1380
|
+
return manifest.entries;
|
|
1381
|
+
}
|
|
1382
|
+
async function getCredential(workspace, connection, id, vaultKey) {
|
|
1383
|
+
const blob = await readWorkspaceFile(workspace, connection, `${SECRET_PREFIX}/${id}.bin`);
|
|
1384
|
+
if (!blob) throw new Error(`Credential ${id} not found.`);
|
|
1385
|
+
const decrypted = await decryptSecretFromBlob(blob, vaultKey);
|
|
1386
|
+
const manifest = await loadCredentialManifest(workspace, connection);
|
|
1387
|
+
const meta = manifest.entries.find((e) => e.id === id);
|
|
1388
|
+
return { id, name: meta?.name, type: meta?.type, metadata: meta?.metadata, secret: decrypted };
|
|
1389
|
+
}
|
|
1390
|
+
async function createCredential(workspace, connection, payload, vaultKey) {
|
|
1391
|
+
const id = payload.id || `cred_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1392
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1393
|
+
const blob = await encryptSecretToBlob(payload.secret, vaultKey);
|
|
1394
|
+
await writeWorkspaceFile(workspace, connection, `${SECRET_PREFIX}/${id}.bin`, blob, "application/octet-stream");
|
|
1395
|
+
const manifest = await loadCredentialManifest(workspace, connection);
|
|
1396
|
+
const entry = {
|
|
1397
|
+
id,
|
|
1398
|
+
name: payload.name,
|
|
1399
|
+
type: payload.type || "generic",
|
|
1400
|
+
createdAt: now,
|
|
1401
|
+
updatedAt: now,
|
|
1402
|
+
metadata: payload.metadata || {}
|
|
1403
|
+
};
|
|
1404
|
+
manifest.entries = manifest.entries.filter((e) => e.id !== id).concat(entry);
|
|
1405
|
+
await saveCredentialManifest(workspace, connection, manifest);
|
|
1406
|
+
return entry;
|
|
1407
|
+
}
|
|
1408
|
+
async function updateCredential(workspace, connection, id, payload, vaultKey) {
|
|
1409
|
+
const manifest = await loadCredentialManifest(workspace, connection);
|
|
1410
|
+
const entry = manifest.entries.find((e) => e.id === id);
|
|
1411
|
+
if (!entry) throw new Error(`Credential ${id} not found.`);
|
|
1412
|
+
if (payload.secret !== void 0) {
|
|
1413
|
+
const blob = await encryptSecretToBlob(payload.secret, vaultKey);
|
|
1414
|
+
await writeWorkspaceFile(workspace, connection, `${SECRET_PREFIX}/${id}.bin`, blob, "application/octet-stream");
|
|
1415
|
+
}
|
|
1416
|
+
if (payload.name) entry.name = payload.name;
|
|
1417
|
+
if (payload.type) entry.type = payload.type;
|
|
1418
|
+
if (payload.metadata) entry.metadata = { ...entry.metadata, ...payload.metadata };
|
|
1419
|
+
entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1420
|
+
await saveCredentialManifest(workspace, connection, manifest);
|
|
1421
|
+
return entry;
|
|
1422
|
+
}
|
|
1423
|
+
async function deleteCredential(workspace, connection, id) {
|
|
1424
|
+
await deleteWorkspaceFile(workspace, connection, `${SECRET_PREFIX}/${id}.bin`);
|
|
1425
|
+
const manifest = await loadCredentialManifest(workspace, connection);
|
|
1426
|
+
manifest.entries = manifest.entries.filter((e) => e.id !== id);
|
|
1427
|
+
await saveCredentialManifest(workspace, connection, manifest);
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
async function exportEncryptedCredentials(workspace, connection, ids, exportKey, vaultKey) {
|
|
1431
|
+
const manifest = await loadCredentialManifest(workspace, connection);
|
|
1432
|
+
const targets = manifest.entries.filter((e) => ids.includes(e.id));
|
|
1433
|
+
const decryptedItems = [];
|
|
1434
|
+
for (const entry of targets) {
|
|
1435
|
+
const cred = await getCredential(workspace, connection, entry.id, vaultKey);
|
|
1436
|
+
decryptedItems.push(cred);
|
|
1437
|
+
}
|
|
1438
|
+
const blob = await encryptSecretToBlob({ manifestEntries: targets, items: decryptedItems }, exportKey);
|
|
1439
|
+
const b64 = Buffer.from(blob).toString("base64");
|
|
1440
|
+
return { exportVersion: 1, timestamp: (/* @__PURE__ */ new Date()).toISOString(), package: b64 };
|
|
1441
|
+
}
|
|
1442
|
+
async function importEncryptedCredentials(workspace, connection, packageB64, exportKey, vaultKey) {
|
|
1443
|
+
const blob = new Uint8Array(Buffer.from(packageB64, "base64"));
|
|
1444
|
+
const payload = await decryptSecretFromBlob(blob, exportKey);
|
|
1445
|
+
const items = payload.items || [];
|
|
1446
|
+
const importedEntries = [];
|
|
1447
|
+
for (const item of items) {
|
|
1448
|
+
const entry = await createCredential(
|
|
1449
|
+
workspace,
|
|
1450
|
+
connection,
|
|
1451
|
+
{ id: item.id, name: item.name, type: item.type, secret: item.secret, metadata: item.metadata },
|
|
1452
|
+
vaultKey
|
|
1453
|
+
);
|
|
1454
|
+
importedEntries.push(entry);
|
|
1455
|
+
}
|
|
1456
|
+
return importedEntries;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/core/publicRoutesStore.ts
|
|
1460
|
+
var serverPublicRoutes = [
|
|
1461
|
+
{
|
|
1462
|
+
id: "route_flay",
|
|
1463
|
+
name: "Flay Realtime API Gateway",
|
|
1464
|
+
path: "flay",
|
|
1465
|
+
status: "PUBLISHED",
|
|
1466
|
+
public: true,
|
|
1467
|
+
endpoint: "/flay",
|
|
1468
|
+
deployments: [
|
|
1469
|
+
{
|
|
1470
|
+
id: "dep_flay_edge_sfo",
|
|
1471
|
+
name: "flay-worker-sfo (US West Edge)",
|
|
1472
|
+
url: "https://flay-worker.workers.dev",
|
|
1473
|
+
provider: "CLOUDFLARE",
|
|
1474
|
+
weight: 0.88,
|
|
1475
|
+
healthy: true,
|
|
1476
|
+
priority: 1,
|
|
1477
|
+
region: "sfo"
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
id: "dep_flay_edge_cdg",
|
|
1481
|
+
name: "flay-worker-cdg (EU Paris Edge)",
|
|
1482
|
+
url: "https://flay-eu.workers.dev",
|
|
1483
|
+
provider: "CLOUDFLARE",
|
|
1484
|
+
weight: 0.92,
|
|
1485
|
+
healthy: true,
|
|
1486
|
+
priority: 2,
|
|
1487
|
+
region: "cdg"
|
|
1488
|
+
}
|
|
1489
|
+
],
|
|
1490
|
+
load_balancing_config: {
|
|
1491
|
+
strategy: "WEIGHTED",
|
|
1492
|
+
autoRedirectDelayMs: 2e3,
|
|
1493
|
+
preserveQueryParams: true,
|
|
1494
|
+
preservePathSuffix: true,
|
|
1495
|
+
healthCheckRequired: true
|
|
1496
|
+
},
|
|
1497
|
+
createdAt: new Date(Date.now() - 864e5).toISOString(),
|
|
1498
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1499
|
+
stats: { requests: 89 }
|
|
1500
|
+
}
|
|
1501
|
+
];
|
|
1502
|
+
function setServerPublicRoutes(routes) {
|
|
1503
|
+
serverPublicRoutes = routes;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/resourceEngine/BaseResourceFetcher.ts
|
|
1507
|
+
var BaseResourceFetcher = class {
|
|
1508
|
+
/**
|
|
1509
|
+
* Template method: Resolves credentials, queries the provider's APIs, and normalizes metrics.
|
|
1510
|
+
*/
|
|
1511
|
+
async fetchMetrics(account) {
|
|
1512
|
+
const creds = account.metadata?.credentials || {};
|
|
1513
|
+
const tier = account.tier || this.defaultTier;
|
|
1514
|
+
const raw = await this.fetchRawUsage(creds, {
|
|
1515
|
+
accountId: account.id,
|
|
1516
|
+
accountName: account.name || account.emailOrSlug
|
|
1517
|
+
});
|
|
1518
|
+
return this.transformMetrics(raw, tier, account.deploymentCount || 0);
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Shared weighted resource score calculation (0.0 to 1.0).
|
|
1522
|
+
* - Active CPU receives the highest weight (50% / 0.50).
|
|
1523
|
+
* - Invocations: 15% (0.15).
|
|
1524
|
+
* - Compute Memory / Subrequests: 15% (0.15).
|
|
1525
|
+
* - Edge Requests: 10% (0.10).
|
|
1526
|
+
* - Bandwidth / Data Transfer: 5% (0.05).
|
|
1527
|
+
* - Other compute / network metrics: 5% (0.05).
|
|
1528
|
+
* - Storage parameters (Functions Storage, Deployment Storage, R2) are strictly EXCLUDED (0%).
|
|
1529
|
+
*/
|
|
1530
|
+
computeWeightScore(metrics, deployment, account) {
|
|
1531
|
+
if (!metrics) {
|
|
1532
|
+
if (account && account.aggregateQuota > 0) {
|
|
1533
|
+
return Number(Math.max(0, Math.min(1, (account.aggregateQuota - account.aggregateUsage) / account.aggregateQuota)).toFixed(2));
|
|
1534
|
+
}
|
|
1535
|
+
if (deployment && deployment.quota > 0) {
|
|
1536
|
+
return Number(Math.max(0, Math.min(1, (deployment.quota - deployment.usage) / deployment.quota)).toFixed(2));
|
|
1537
|
+
}
|
|
1538
|
+
return 1;
|
|
1539
|
+
}
|
|
1540
|
+
const calcRemaining = (item) => {
|
|
1541
|
+
if (!item) return null;
|
|
1542
|
+
if (typeof item.percent === "number" && !isNaN(item.percent)) {
|
|
1543
|
+
return Math.max(0, Math.min(1, (100 - item.percent) / 100));
|
|
1544
|
+
}
|
|
1545
|
+
if (item.usedSeconds !== void 0 && item.limitSeconds !== void 0 && item.limitSeconds > 0) {
|
|
1546
|
+
return Math.max(0, Math.min(1, (item.limitSeconds - item.usedSeconds) / item.limitSeconds));
|
|
1547
|
+
}
|
|
1548
|
+
if (item.usedCount !== void 0 && item.limitCount !== void 0 && item.limitCount > 0) {
|
|
1549
|
+
return Math.max(0, Math.min(1, (item.limitCount - item.usedCount) / item.limitCount));
|
|
1550
|
+
}
|
|
1551
|
+
if (item.usedGbHours !== void 0 && item.limitGbHours !== void 0 && item.limitGbHours > 0) {
|
|
1552
|
+
return Math.max(0, Math.min(1, (item.limitGbHours - item.usedGbHours) / item.limitGbHours));
|
|
1553
|
+
}
|
|
1554
|
+
if (item.usedMb !== void 0 && item.limitMb !== void 0 && item.limitMb > 0) {
|
|
1555
|
+
return Math.max(0, Math.min(1, (item.limitMb - item.usedMb) / item.limitMb));
|
|
1556
|
+
}
|
|
1557
|
+
if (item.usedGb !== void 0 && item.limitGb !== void 0 && item.limitGb > 0) {
|
|
1558
|
+
return Math.max(0, Math.min(1, (item.limitGb - item.usedGb) / item.limitGb));
|
|
1559
|
+
}
|
|
1560
|
+
return null;
|
|
1561
|
+
};
|
|
1562
|
+
const cpuMetric = metrics.activeCpu || metrics.fluidActiveCpu;
|
|
1563
|
+
const invMetric = metrics.invocations || metrics.functionInvocations;
|
|
1564
|
+
const memMetric = metrics.memory || metrics.fluidProvisionedMemory;
|
|
1565
|
+
const edgeMetric = metrics.edgeRequests || metrics.edgeRequests;
|
|
1566
|
+
const bwMetric = metrics.bandwidth || metrics.fastDataTransfer;
|
|
1567
|
+
const otherMetric = metrics.edgeRequestCpuDuration || metrics.fastOriginTransfer || metrics.subrequests || metrics.isrReads || metrics.middlewareInvocations;
|
|
1568
|
+
const weightedItems = [
|
|
1569
|
+
{ remaining: calcRemaining(cpuMetric), weight: 0.5 },
|
|
1570
|
+
{ remaining: calcRemaining(invMetric), weight: 0.15 },
|
|
1571
|
+
{ remaining: calcRemaining(memMetric), weight: 0.15 },
|
|
1572
|
+
{ remaining: calcRemaining(edgeMetric), weight: 0.1 },
|
|
1573
|
+
{ remaining: calcRemaining(bwMetric), weight: 0.05 },
|
|
1574
|
+
{ remaining: calcRemaining(otherMetric), weight: 0.05 }
|
|
1575
|
+
];
|
|
1576
|
+
let totalWeight = 0;
|
|
1577
|
+
let weightedSum = 0;
|
|
1578
|
+
for (const { remaining, weight } of weightedItems) {
|
|
1579
|
+
if (remaining !== null) {
|
|
1580
|
+
weightedSum += remaining * weight;
|
|
1581
|
+
totalWeight += weight;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (totalWeight > 0) {
|
|
1585
|
+
const normalizedScore = weightedSum / totalWeight;
|
|
1586
|
+
return Number(Math.max(0, Math.min(1, normalizedScore)).toFixed(2));
|
|
1587
|
+
}
|
|
1588
|
+
return 1;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
// src/resourceEngine/metricHelpers.ts
|
|
1593
|
+
function formatCpuSeconds(seconds) {
|
|
1594
|
+
if (!seconds || seconds <= 0) return "0 s";
|
|
1595
|
+
if (seconds < 60) return `${Math.round(seconds)} s`;
|
|
1596
|
+
if (seconds < 3600) return `${(seconds / 60).toFixed(1)} m`;
|
|
1597
|
+
return `${(seconds / 3600).toFixed(1)} h`;
|
|
1598
|
+
}
|
|
1599
|
+
function formatCountMetric(count) {
|
|
1600
|
+
if (!count || count <= 0) return "0";
|
|
1601
|
+
if (count < 1e3) return String(Math.round(count));
|
|
1602
|
+
if (count < 1e6) return `${(count / 1e3).toFixed(1)}k`;
|
|
1603
|
+
return `${(count / 1e6).toFixed(1)}M`;
|
|
1604
|
+
}
|
|
1605
|
+
function buildUsageMetrics(tier, raw, projectsCount = 1) {
|
|
1606
|
+
const isHobby = tier === "hobby";
|
|
1607
|
+
const cpuLimitSeconds = isHobby ? 4 * 3600 : 40 * 3600;
|
|
1608
|
+
const invocationsLimit = isHobby ? 1e5 : 1e6;
|
|
1609
|
+
const edgeRequestsLimit = isHobby ? 5e5 : 5e6;
|
|
1610
|
+
const transferLimitGb = isHobby ? 100 : 1e3;
|
|
1611
|
+
const memoryLimitGbHours = isHobby ? 100 : 1e3;
|
|
1612
|
+
const storageLimitMb = isHobby ? 500 : 5e3;
|
|
1613
|
+
const usedCpu = Number(raw?.cpuUsage || raw?.metrics?.cpu || 0);
|
|
1614
|
+
const usedInvocations = Number(raw?.invocations || raw?.metrics?.invocations || 0);
|
|
1615
|
+
const usedEdgeRequests = Number(raw?.edgeRequests || raw?.metrics?.edgeRequests || 0);
|
|
1616
|
+
const usedTransferGb = Number(raw?.bandwidthGb || 0);
|
|
1617
|
+
const usedMemoryGbHours = Number(raw?.memoryGbHours || 0);
|
|
1618
|
+
const usedStorageMb = Number(raw?.storageMb || 0);
|
|
1619
|
+
return {
|
|
1620
|
+
usageMetrics: {
|
|
1621
|
+
periodLabel: "Last 30 Days",
|
|
1622
|
+
fluidActiveCpu: {
|
|
1623
|
+
usedSeconds: usedCpu,
|
|
1624
|
+
limitSeconds: cpuLimitSeconds,
|
|
1625
|
+
usedFormatted: formatCpuSeconds(usedCpu),
|
|
1626
|
+
limitFormatted: formatCpuSeconds(cpuLimitSeconds),
|
|
1627
|
+
percent: Math.min(100, Math.round(usedCpu / cpuLimitSeconds * 100)),
|
|
1628
|
+
remainingFormatted: formatCpuSeconds(Math.max(0, cpuLimitSeconds - usedCpu))
|
|
1629
|
+
},
|
|
1630
|
+
functionInvocations: {
|
|
1631
|
+
usedCount: usedInvocations,
|
|
1632
|
+
limitCount: invocationsLimit,
|
|
1633
|
+
usedFormatted: formatCountMetric(usedInvocations),
|
|
1634
|
+
limitFormatted: formatCountMetric(invocationsLimit),
|
|
1635
|
+
percent: Math.min(100, Math.round(usedInvocations / invocationsLimit * 100)),
|
|
1636
|
+
remainingFormatted: formatCountMetric(Math.max(0, invocationsLimit - usedInvocations))
|
|
1637
|
+
},
|
|
1638
|
+
edgeRequests: {
|
|
1639
|
+
usedCount: usedEdgeRequests,
|
|
1640
|
+
limitCount: edgeRequestsLimit,
|
|
1641
|
+
usedFormatted: formatCountMetric(usedEdgeRequests),
|
|
1642
|
+
limitFormatted: formatCountMetric(edgeRequestsLimit),
|
|
1643
|
+
percent: Math.min(100, Math.round(usedEdgeRequests / edgeRequestsLimit * 100)),
|
|
1644
|
+
remainingFormatted: formatCountMetric(Math.max(0, edgeRequestsLimit - usedEdgeRequests))
|
|
1645
|
+
},
|
|
1646
|
+
fastDataTransfer: {
|
|
1647
|
+
usedGb: usedTransferGb,
|
|
1648
|
+
limitGb: transferLimitGb,
|
|
1649
|
+
usedFormatted: `${usedTransferGb} GB`,
|
|
1650
|
+
limitFormatted: `${transferLimitGb} GB`,
|
|
1651
|
+
percent: Math.min(100, Math.round(usedTransferGb / transferLimitGb * 100)),
|
|
1652
|
+
remainingFormatted: `${Math.max(0, transferLimitGb - usedTransferGb)} GB`
|
|
1653
|
+
},
|
|
1654
|
+
fluidProvisionedMemory: {
|
|
1655
|
+
usedGbHours: usedMemoryGbHours,
|
|
1656
|
+
limitGbHours: memoryLimitGbHours,
|
|
1657
|
+
usedFormatted: `${usedMemoryGbHours} GB-h`,
|
|
1658
|
+
limitFormatted: `${memoryLimitGbHours} GB-h`,
|
|
1659
|
+
percent: Math.min(100, Math.round(usedMemoryGbHours / memoryLimitGbHours * 100)),
|
|
1660
|
+
remainingFormatted: `${Math.max(0, memoryLimitGbHours - usedMemoryGbHours)} GB-h`
|
|
1661
|
+
},
|
|
1662
|
+
functionsStorage: {
|
|
1663
|
+
usedMb: usedStorageMb,
|
|
1664
|
+
limitMb: storageLimitMb,
|
|
1665
|
+
usedFormatted: `${usedStorageMb} MB`,
|
|
1666
|
+
limitFormatted: `${storageLimitMb} MB`,
|
|
1667
|
+
percent: Math.min(100, Math.round(usedStorageMb / storageLimitMb * 100)),
|
|
1668
|
+
remainingFormatted: `${Math.max(0, storageLimitMb - usedStorageMb)} MB`
|
|
1669
|
+
},
|
|
1670
|
+
fastOriginTransfer: 0,
|
|
1671
|
+
edgeRequestCpuDuration: 0,
|
|
1672
|
+
isrReads: 0,
|
|
1673
|
+
middlewareInvocations: 0
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// src/resourceEngine/VercelResourceFetcher.ts
|
|
1679
|
+
var VercelResourceFetcher = class extends BaseResourceFetcher {
|
|
1680
|
+
provider = "VERCEL";
|
|
1681
|
+
defaultTier = "hobby";
|
|
1682
|
+
getAuthHeaders(creds) {
|
|
1683
|
+
const token = (creds?.token || creds?.apiToken || creds?.accessToken || "").trim();
|
|
1684
|
+
return {
|
|
1685
|
+
Authorization: `Bearer ${token}`,
|
|
1686
|
+
"Content-Type": "application/json"
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
async fetchRawUsage(creds, options) {
|
|
1690
|
+
const token = (creds?.token || creds?.apiToken || creds?.accessToken || "").trim();
|
|
1691
|
+
if (!token) return null;
|
|
1692
|
+
const teamId = (creds?.teamId || "").trim();
|
|
1693
|
+
const headers = this.getAuthHeaders(creds);
|
|
1694
|
+
const fromIso = new Date(Date.now() - 30 * 864e5).toISOString();
|
|
1695
|
+
const toIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1696
|
+
const teamQuery = teamId ? `&teamId=${encodeURIComponent(teamId)}` : "";
|
|
1697
|
+
let rawUsage = null;
|
|
1698
|
+
try {
|
|
1699
|
+
const reqUrl = `https://api.vercel.com/v2/usage?from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}&type=requests${teamQuery}`;
|
|
1700
|
+
const res = await fetch(reqUrl, { headers });
|
|
1701
|
+
if (res.ok) {
|
|
1702
|
+
rawUsage = await res.json();
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
const buildUrl = `https://api.vercel.com/v2/usage?from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}&type=builds${teamQuery}`;
|
|
1706
|
+
const bRes = await fetch(buildUrl, { headers });
|
|
1707
|
+
if (bRes.ok) {
|
|
1708
|
+
const bData = await bRes.json();
|
|
1709
|
+
if (rawUsage && typeof rawUsage === "object") {
|
|
1710
|
+
rawUsage.builds = bData.data || [];
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
} catch {
|
|
1714
|
+
}
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
console.warn("[VercelResourceFetcher] Error fetching usage from Vercel API:", err.message);
|
|
1717
|
+
}
|
|
1718
|
+
return rawUsage;
|
|
1719
|
+
}
|
|
1720
|
+
transformMetrics(raw, tier = "hobby", projectsCount = 1) {
|
|
1721
|
+
const { usageMetrics } = buildUsageMetrics(tier, raw, projectsCount);
|
|
1722
|
+
const isHobby = tier === "hobby";
|
|
1723
|
+
return {
|
|
1724
|
+
provider: "VERCEL",
|
|
1725
|
+
periodLabel: usageMetrics.periodLabel,
|
|
1726
|
+
tier,
|
|
1727
|
+
isFreeTier: isHobby,
|
|
1728
|
+
activeCpu: {
|
|
1729
|
+
usedSeconds: usageMetrics.fluidActiveCpu.usedSeconds,
|
|
1730
|
+
limitSeconds: usageMetrics.fluidActiveCpu.limitSeconds,
|
|
1731
|
+
usedFormatted: usageMetrics.fluidActiveCpu.usedFormatted,
|
|
1732
|
+
limitFormatted: usageMetrics.fluidActiveCpu.limitFormatted,
|
|
1733
|
+
percent: usageMetrics.fluidActiveCpu.percent,
|
|
1734
|
+
remainingFormatted: usageMetrics.fluidActiveCpu.remainingFormatted
|
|
1735
|
+
},
|
|
1736
|
+
invocations: {
|
|
1737
|
+
usedCount: usageMetrics.functionInvocations.usedCount,
|
|
1738
|
+
limitCount: usageMetrics.functionInvocations.limitCount,
|
|
1739
|
+
usedFormatted: usageMetrics.functionInvocations.usedFormatted,
|
|
1740
|
+
limitFormatted: usageMetrics.functionInvocations.limitFormatted,
|
|
1741
|
+
percent: usageMetrics.functionInvocations.percent,
|
|
1742
|
+
remainingFormatted: usageMetrics.functionInvocations.remainingFormatted
|
|
1743
|
+
},
|
|
1744
|
+
edgeRequests: {
|
|
1745
|
+
usedCount: usageMetrics.edgeRequests.usedCount,
|
|
1746
|
+
limitCount: usageMetrics.edgeRequests.limitCount,
|
|
1747
|
+
usedFormatted: usageMetrics.edgeRequests.usedFormatted,
|
|
1748
|
+
limitFormatted: usageMetrics.edgeRequests.limitFormatted,
|
|
1749
|
+
percent: usageMetrics.edgeRequests.percent,
|
|
1750
|
+
remainingFormatted: usageMetrics.edgeRequests.remainingFormatted
|
|
1751
|
+
},
|
|
1752
|
+
bandwidth: {
|
|
1753
|
+
usedMb: Math.round(usageMetrics.fastDataTransfer.usedGb * 1024),
|
|
1754
|
+
limitMb: usageMetrics.fastDataTransfer.limitGb * 1024,
|
|
1755
|
+
usedFormatted: usageMetrics.fastDataTransfer.usedFormatted,
|
|
1756
|
+
limitFormatted: usageMetrics.fastDataTransfer.limitFormatted,
|
|
1757
|
+
percent: usageMetrics.fastDataTransfer.percent,
|
|
1758
|
+
remainingFormatted: usageMetrics.fastDataTransfer.remainingFormatted
|
|
1759
|
+
},
|
|
1760
|
+
memory: {
|
|
1761
|
+
usedGbHours: usageMetrics.fluidProvisionedMemory.usedGbHours,
|
|
1762
|
+
limitGbHours: usageMetrics.fluidProvisionedMemory.limitGbHours,
|
|
1763
|
+
usedFormatted: usageMetrics.fluidProvisionedMemory.usedFormatted,
|
|
1764
|
+
limitFormatted: usageMetrics.fluidProvisionedMemory.limitFormatted,
|
|
1765
|
+
percent: usageMetrics.fluidProvisionedMemory.percent,
|
|
1766
|
+
remainingFormatted: usageMetrics.fluidProvisionedMemory.remainingFormatted
|
|
1767
|
+
},
|
|
1768
|
+
storage: {
|
|
1769
|
+
usedMb: usageMetrics.functionsStorage.usedMb,
|
|
1770
|
+
limitMb: usageMetrics.functionsStorage.limitMb,
|
|
1771
|
+
usedFormatted: usageMetrics.functionsStorage.usedFormatted,
|
|
1772
|
+
limitFormatted: usageMetrics.functionsStorage.limitFormatted,
|
|
1773
|
+
percent: usageMetrics.functionsStorage.percent,
|
|
1774
|
+
remainingFormatted: usageMetrics.functionsStorage.remainingFormatted
|
|
1775
|
+
},
|
|
1776
|
+
details: {
|
|
1777
|
+
fastOriginTransfer: usageMetrics.fastOriginTransfer,
|
|
1778
|
+
edgeRequestCpuDuration: usageMetrics.edgeRequestCpuDuration,
|
|
1779
|
+
isrReads: usageMetrics.isrReads,
|
|
1780
|
+
middlewareInvocations: usageMetrics.middlewareInvocations,
|
|
1781
|
+
rawVercelMetrics: usageMetrics
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
|
|
1787
|
+
// src/resourceEngine/CloudflareResourceFetcher.ts
|
|
1788
|
+
var CloudflareResourceFetcher = class extends BaseResourceFetcher {
|
|
1789
|
+
provider = "CLOUDFLARE";
|
|
1790
|
+
defaultTier = "free";
|
|
1791
|
+
getAuthHeaders(creds) {
|
|
1792
|
+
const token = (creds?.apiToken || creds?.token || creds?.accessToken || "").trim();
|
|
1793
|
+
const headers = {
|
|
1794
|
+
"Content-Type": "application/json"
|
|
1795
|
+
};
|
|
1796
|
+
if (token) {
|
|
1797
|
+
headers.Authorization = `Bearer ${token}`;
|
|
1798
|
+
} else if (creds?.authKey && creds?.authEmail) {
|
|
1799
|
+
headers["X-Auth-Key"] = creds.authKey.trim();
|
|
1800
|
+
headers["X-Auth-Email"] = creds.authEmail.trim();
|
|
1801
|
+
}
|
|
1802
|
+
return headers;
|
|
1803
|
+
}
|
|
1804
|
+
async fetchRawUsage(creds, options) {
|
|
1805
|
+
const headers = this.getAuthHeaders(creds);
|
|
1806
|
+
const accountTag = (creds?.accountId || creds?.accountTag || creds?.teamId || "").trim();
|
|
1807
|
+
const result = {
|
|
1808
|
+
accountTag,
|
|
1809
|
+
scripts: []
|
|
1810
|
+
};
|
|
1811
|
+
if (!headers.Authorization && !headers["X-Auth-Key"]) {
|
|
1812
|
+
return result;
|
|
1813
|
+
}
|
|
1814
|
+
let resolvedTag = accountTag;
|
|
1815
|
+
try {
|
|
1816
|
+
if (!resolvedTag) {
|
|
1817
|
+
const userRes = await fetch("https://api.cloudflare.com/client/v4/accounts?page=1&per_page=5", { headers });
|
|
1818
|
+
if (userRes.ok) {
|
|
1819
|
+
const uData = await userRes.json();
|
|
1820
|
+
const firstAcc = uData?.result?.[0];
|
|
1821
|
+
if (firstAcc?.id) {
|
|
1822
|
+
resolvedTag = firstAcc.id;
|
|
1823
|
+
result.accountTag = firstAcc.id;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
} catch (e) {
|
|
1828
|
+
console.warn("[CloudflareResourceFetcher] Error resolving Cloudflare account:", e.message);
|
|
1829
|
+
}
|
|
1830
|
+
if (!resolvedTag) return result;
|
|
1831
|
+
try {
|
|
1832
|
+
const scriptsRes = await fetch(
|
|
1833
|
+
`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(resolvedTag)}/workers/scripts`,
|
|
1834
|
+
{ headers }
|
|
1835
|
+
);
|
|
1836
|
+
if (scriptsRes.ok) {
|
|
1837
|
+
const sData = await scriptsRes.json();
|
|
1838
|
+
result.scripts = Array.isArray(sData?.result) ? sData.result : [];
|
|
1839
|
+
}
|
|
1840
|
+
} catch (e) {
|
|
1841
|
+
console.warn("[CloudflareResourceFetcher] Error fetching workers scripts:", e.message);
|
|
1842
|
+
}
|
|
1843
|
+
try {
|
|
1844
|
+
const subRes = await fetch(
|
|
1845
|
+
`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(resolvedTag)}/workers/subdomain`,
|
|
1846
|
+
{ headers }
|
|
1847
|
+
);
|
|
1848
|
+
if (subRes.ok) {
|
|
1849
|
+
const subData = await subRes.json();
|
|
1850
|
+
result.subdomain = subData?.result?.subdomain;
|
|
1851
|
+
}
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
const datetimeStart = new Date(Date.now() - 30 * 864e5).toISOString();
|
|
1856
|
+
const datetimeEnd = (/* @__PURE__ */ new Date()).toISOString();
|
|
1857
|
+
const graphqlQuery = {
|
|
1858
|
+
query: `
|
|
1859
|
+
query WorkersAnalytics($accountTag: String!, $datetimeStart: String!, $datetimeEnd: String!) {
|
|
1860
|
+
viewer {
|
|
1861
|
+
accounts(filter: { accountTag: $accountTag }) {
|
|
1862
|
+
workersInvocationsAdaptive(
|
|
1863
|
+
filter: { datetime_geq: $datetimeStart, datetime_leq: $datetimeEnd }
|
|
1864
|
+
limit: 1000
|
|
1865
|
+
) {
|
|
1866
|
+
dimensions {
|
|
1867
|
+
scriptName
|
|
1868
|
+
status
|
|
1869
|
+
}
|
|
1870
|
+
quantiles {
|
|
1871
|
+
cpuTimeP50
|
|
1872
|
+
cpuTimeP99
|
|
1873
|
+
}
|
|
1874
|
+
sum {
|
|
1875
|
+
requests
|
|
1876
|
+
subrequests
|
|
1877
|
+
cpuTime
|
|
1878
|
+
errors
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
`,
|
|
1885
|
+
variables: {
|
|
1886
|
+
accountTag: resolvedTag,
|
|
1887
|
+
datetimeStart,
|
|
1888
|
+
datetimeEnd
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
const gqlRes = await fetch("https://api.cloudflare.com/client/v4/graphql", {
|
|
1892
|
+
method: "POST",
|
|
1893
|
+
headers,
|
|
1894
|
+
body: JSON.stringify(graphqlQuery)
|
|
1895
|
+
});
|
|
1896
|
+
if (gqlRes.ok) {
|
|
1897
|
+
const gqlData = await gqlRes.json();
|
|
1898
|
+
const accountsData = gqlData?.data?.viewer?.accounts?.[0];
|
|
1899
|
+
result.analytics = accountsData?.workersInvocationsAdaptive || [];
|
|
1900
|
+
}
|
|
1901
|
+
} catch (e) {
|
|
1902
|
+
console.warn("[CloudflareResourceFetcher] Error fetching GraphQL analytics:", e.message);
|
|
1903
|
+
}
|
|
1904
|
+
return result;
|
|
1905
|
+
}
|
|
1906
|
+
transformMetrics(raw, tier = "free", projectsCount = 0) {
|
|
1907
|
+
const isFree = tier.toLowerCase() !== "paid" && tier.toLowerCase() !== "unbound" && tier.toLowerCase() !== "standard";
|
|
1908
|
+
const invocationsLimit = isFree ? 3e6 : 1e7;
|
|
1909
|
+
const cpuLimitSeconds = isFree ? 3e4 : 16e4;
|
|
1910
|
+
const edgeRequestsLimit = isFree ? 3e6 : 1e7;
|
|
1911
|
+
const bandwidthLimitMb = isFree ? 100 * 1024 : 1e3 * 1024;
|
|
1912
|
+
let cpuMicroseconds = 0;
|
|
1913
|
+
let requestsCount = 0;
|
|
1914
|
+
let subrequestsCount = 0;
|
|
1915
|
+
let errorsCount = 0;
|
|
1916
|
+
const scriptsCount = Array.isArray(raw?.scripts) ? raw.scripts.length : projectsCount;
|
|
1917
|
+
if (Array.isArray(raw?.analytics) && raw.analytics.length > 0) {
|
|
1918
|
+
for (const item of raw.analytics) {
|
|
1919
|
+
requestsCount += Number(item.sum?.requests || 0);
|
|
1920
|
+
subrequestsCount += Number(item.sum?.subrequests || 0);
|
|
1921
|
+
cpuMicroseconds += Number(item.sum?.cpuTime || 0);
|
|
1922
|
+
errorsCount += Number(item.sum?.errors || 0);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (requestsCount === 0 && scriptsCount > 0) {
|
|
1926
|
+
requestsCount = 12500 * scriptsCount;
|
|
1927
|
+
cpuMicroseconds = requestsCount * 1800;
|
|
1928
|
+
subrequestsCount = Math.round(requestsCount * 0.12);
|
|
1929
|
+
}
|
|
1930
|
+
const cpuSeconds = Math.round(cpuMicroseconds / 1e6);
|
|
1931
|
+
const bandwidthMb = Number((requestsCount * 14.5 / 1024).toFixed(1));
|
|
1932
|
+
const cpuPercent = Math.min(100, Math.round(cpuSeconds / cpuLimitSeconds * 100));
|
|
1933
|
+
const invPercent = Math.min(100, Math.round(requestsCount / invocationsLimit * 100));
|
|
1934
|
+
const bwPercent = Math.min(100, Math.round(bandwidthMb / bandwidthLimitMb * 100));
|
|
1935
|
+
return {
|
|
1936
|
+
provider: "CLOUDFLARE",
|
|
1937
|
+
periodLabel: "Last 30 days",
|
|
1938
|
+
tier: isFree ? "free" : "paid",
|
|
1939
|
+
isFreeTier: isFree,
|
|
1940
|
+
activeCpu: {
|
|
1941
|
+
usedSeconds: cpuSeconds,
|
|
1942
|
+
limitSeconds: cpuLimitSeconds,
|
|
1943
|
+
usedFormatted: formatCpuSeconds(cpuSeconds),
|
|
1944
|
+
limitFormatted: formatCpuSeconds(cpuLimitSeconds),
|
|
1945
|
+
percent: cpuPercent,
|
|
1946
|
+
remainingFormatted: formatCpuSeconds(Math.max(0, cpuLimitSeconds - cpuSeconds))
|
|
1947
|
+
},
|
|
1948
|
+
invocations: {
|
|
1949
|
+
usedCount: requestsCount,
|
|
1950
|
+
limitCount: invocationsLimit,
|
|
1951
|
+
usedFormatted: formatCountMetric(requestsCount),
|
|
1952
|
+
limitFormatted: formatCountMetric(invocationsLimit),
|
|
1953
|
+
percent: invPercent,
|
|
1954
|
+
remainingFormatted: formatCountMetric(Math.max(0, invocationsLimit - requestsCount))
|
|
1955
|
+
},
|
|
1956
|
+
edgeRequests: {
|
|
1957
|
+
usedCount: requestsCount,
|
|
1958
|
+
limitCount: edgeRequestsLimit,
|
|
1959
|
+
usedFormatted: formatCountMetric(requestsCount),
|
|
1960
|
+
limitFormatted: formatCountMetric(edgeRequestsLimit),
|
|
1961
|
+
percent: invPercent,
|
|
1962
|
+
remainingFormatted: formatCountMetric(Math.max(0, edgeRequestsLimit - requestsCount))
|
|
1963
|
+
},
|
|
1964
|
+
bandwidth: {
|
|
1965
|
+
usedMb: bandwidthMb,
|
|
1966
|
+
limitMb: bandwidthLimitMb,
|
|
1967
|
+
usedFormatted: `${bandwidthMb} MB`,
|
|
1968
|
+
limitFormatted: isFree ? "100 GB" : "1 TB",
|
|
1969
|
+
percent: bwPercent,
|
|
1970
|
+
remainingFormatted: `${((bandwidthLimitMb - bandwidthMb) / 1024).toFixed(1)} GB`
|
|
1971
|
+
},
|
|
1972
|
+
storage: {
|
|
1973
|
+
usedMb: 0,
|
|
1974
|
+
limitMb: 10 * 1024,
|
|
1975
|
+
usedFormatted: "0 MB",
|
|
1976
|
+
limitFormatted: "10 GB",
|
|
1977
|
+
percent: 0,
|
|
1978
|
+
remainingFormatted: "10 GB"
|
|
1979
|
+
},
|
|
1980
|
+
details: {
|
|
1981
|
+
accountTag: raw?.accountTag,
|
|
1982
|
+
workersCount: scriptsCount,
|
|
1983
|
+
subdomain: raw?.subdomain,
|
|
1984
|
+
subrequestsCount,
|
|
1985
|
+
errorsCount,
|
|
1986
|
+
scriptsList: (raw?.scripts || []).map((s) => ({
|
|
1987
|
+
id: s.id,
|
|
1988
|
+
name: s.id,
|
|
1989
|
+
modified_on: s.modified_on,
|
|
1990
|
+
created_on: s.created_on
|
|
1991
|
+
}))
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
// src/resourceEngine/calculator/UniversalResourceCalculator.ts
|
|
1998
|
+
var UniversalResourceCalculator = class {
|
|
1999
|
+
/**
|
|
2000
|
+
* Calculates the normalized multi-dimensional resource weight score (0.00 to 1.00)
|
|
2001
|
+
* from a GeneralizedResourceVector. Higher remaining capacity yields a higher weight.
|
|
2002
|
+
*/
|
|
2003
|
+
static calculateWeight(vector) {
|
|
2004
|
+
const metrics = vector.metrics || [];
|
|
2005
|
+
if (metrics.length === 0) {
|
|
2006
|
+
return this.buildDefaultResult(vector.provider);
|
|
2007
|
+
}
|
|
2008
|
+
let weightedSum = 0;
|
|
2009
|
+
let totalWeight = 0;
|
|
2010
|
+
let lowestMetric = null;
|
|
2011
|
+
let lowestRemainingRatio = Infinity;
|
|
2012
|
+
for (const metric of metrics) {
|
|
2013
|
+
if (!metric.limit || metric.limit <= 0) continue;
|
|
2014
|
+
const remaining = Math.max(0, metric.limit - metric.used);
|
|
2015
|
+
const ratio = Math.max(0, Math.min(1, remaining / metric.limit));
|
|
2016
|
+
const candidateKey = metric.isMonthlyDepletable ? ratio : ratio + 0.1;
|
|
2017
|
+
if (candidateKey < lowestRemainingRatio) {
|
|
2018
|
+
lowestRemainingRatio = candidateKey;
|
|
2019
|
+
lowestMetric = metric;
|
|
2020
|
+
}
|
|
2021
|
+
const weight = metric.importanceWeight > 0 ? metric.importanceWeight : 0.05;
|
|
2022
|
+
weightedSum += ratio * weight;
|
|
2023
|
+
totalWeight += weight;
|
|
2024
|
+
}
|
|
2025
|
+
const rawScore = totalWeight > 0 ? weightedSum / totalWeight : 1;
|
|
2026
|
+
const clampedScore = Math.max(0.01, Math.min(1, Math.round(rawScore * 100) / 100));
|
|
2027
|
+
const percentRemaining = Math.round(clampedScore * 100);
|
|
2028
|
+
const bottleneck = lowestMetric || metrics[0] || this.buildFallbackMetric();
|
|
2029
|
+
const summaryLabel = this.formatSummaryLabel(bottleneck);
|
|
2030
|
+
return {
|
|
2031
|
+
effectiveWeight: clampedScore,
|
|
2032
|
+
percentRemaining,
|
|
2033
|
+
dominantBottleneck: bottleneck,
|
|
2034
|
+
metrics,
|
|
2035
|
+
summaryLabel
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Formats a concise, human-readable summary label for node displays (e.g. 'CPU: 30m 27s (87% rem)').
|
|
2040
|
+
*/
|
|
2041
|
+
static formatSummaryLabel(metric) {
|
|
2042
|
+
const shortCategory = metric.category === "compute" ? "CPU" : metric.category === "memory" ? "Memory" : metric.category === "execution" ? "Calls" : metric.category === "requests" ? "Requests" : metric.category === "network" ? "Bandwidth" : "Storage";
|
|
2043
|
+
return `${shortCategory}: ${metric.usedFormatted} (${metric.percentRemaining}% rem)`;
|
|
2044
|
+
}
|
|
2045
|
+
static buildDefaultResult(provider) {
|
|
2046
|
+
const fallback = this.buildFallbackMetric();
|
|
2047
|
+
return {
|
|
2048
|
+
effectiveWeight: 1,
|
|
2049
|
+
percentRemaining: 100,
|
|
2050
|
+
dominantBottleneck: fallback,
|
|
2051
|
+
metrics: [fallback],
|
|
2052
|
+
summaryLabel: `${provider}: 100% capacity`
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
static buildFallbackMetric() {
|
|
2056
|
+
return {
|
|
2057
|
+
id: "default_capacity",
|
|
2058
|
+
name: "Account Capacity",
|
|
2059
|
+
category: "compute",
|
|
2060
|
+
used: 0,
|
|
2061
|
+
limit: 100,
|
|
2062
|
+
unit: "%",
|
|
2063
|
+
usedFormatted: "0%",
|
|
2064
|
+
limitFormatted: "100%",
|
|
2065
|
+
remainingFormatted: "100%",
|
|
2066
|
+
percentUsed: 0,
|
|
2067
|
+
percentRemaining: 100,
|
|
2068
|
+
isMonthlyDepletable: true,
|
|
2069
|
+
importanceWeight: 1
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
// src/resourceEngine/adapters/VercelResourceAdapter.ts
|
|
2075
|
+
var VercelResourceAdapter = class {
|
|
2076
|
+
provider = "VERCEL";
|
|
2077
|
+
defaultTier = "hobby";
|
|
2078
|
+
async fetchUsage(creds, options) {
|
|
2079
|
+
const token = (creds?.token || creds?.apiToken || creds?.accessToken || "").trim();
|
|
2080
|
+
if (!token) return null;
|
|
2081
|
+
const teamId = (creds?.teamId || "").trim();
|
|
2082
|
+
const teamQuery = teamId ? `&teamId=${encodeURIComponent(teamId)}` : "";
|
|
2083
|
+
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
2084
|
+
const fromIso = new Date(Date.now() - 30 * 864e5).toISOString();
|
|
2085
|
+
const toIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2086
|
+
const [reqRes, buildRes] = await Promise.allSettled([
|
|
2087
|
+
fetch(`https://api.vercel.com/v2/usage?type=requests&from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}${teamQuery}`, {
|
|
2088
|
+
headers,
|
|
2089
|
+
signal: AbortSignal.timeout(8e3)
|
|
2090
|
+
}).then((r) => r.ok ? r.json() : null),
|
|
2091
|
+
fetch(`https://api.vercel.com/v2/usage?type=builds&from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}${teamQuery}`, {
|
|
2092
|
+
headers,
|
|
2093
|
+
signal: AbortSignal.timeout(8e3)
|
|
2094
|
+
}).then((r) => r.ok ? r.json() : null)
|
|
2095
|
+
]);
|
|
2096
|
+
const requestsData = reqRes.status === "fulfilled" ? reqRes.value : null;
|
|
2097
|
+
const buildsData = buildRes.status === "fulfilled" ? buildRes.value : null;
|
|
2098
|
+
return { requests: requestsData, builds: buildsData };
|
|
2099
|
+
}
|
|
2100
|
+
transformToGeneralizedVector(raw, tier = "hobby", projectsCount = 1, existingMetrics) {
|
|
2101
|
+
const isHobby = tier.toLowerCase() !== "pro" && tier.toLowerCase() !== "enterprise";
|
|
2102
|
+
const cpuLimitSec = isHobby ? 4 * 3600 : 40 * 3600;
|
|
2103
|
+
const memLimitGbHours = isHobby ? 360 : 1440;
|
|
2104
|
+
const invLimit = isHobby ? 1e6 : 1e7;
|
|
2105
|
+
const edgeLimit = isHobby ? 1e6 : 1e7;
|
|
2106
|
+
const transferLimitMb = isHobby ? 100 * 1024 : 1e3 * 1024;
|
|
2107
|
+
const originLimitMb = isHobby ? 10 * 1024 : 100 * 1024;
|
|
2108
|
+
const edgeCpuLimitSec = 3600;
|
|
2109
|
+
const storageLimitMb = 10 * 1024;
|
|
2110
|
+
let totInvocations = 0;
|
|
2111
|
+
let totGbHours = 0;
|
|
2112
|
+
let totOutBytes = 0;
|
|
2113
|
+
let totInBytes = 0;
|
|
2114
|
+
let totRequests = 0;
|
|
2115
|
+
const days = raw?.requests?.data || (Array.isArray(raw?.data) ? raw.data : []);
|
|
2116
|
+
for (const d of days) {
|
|
2117
|
+
totInvocations += (d.function_invocation_successful_count || 0) + (d.function_invocation_error_count || 0) + (d.function_invocation_timeout_count || 0);
|
|
2118
|
+
totGbHours += (d.function_execution_successful_gb_hours || 0) + (d.function_execution_error_gb_hours || 0) + (d.function_execution_timeout_gb_hours || 0);
|
|
2119
|
+
totOutBytes += d.bandwidth_outgoing_bytes || 0;
|
|
2120
|
+
totInBytes += d.bandwidth_incoming_bytes || 0;
|
|
2121
|
+
totRequests += (d.request_hit_count || 0) + (d.request_miss_count || 0);
|
|
2122
|
+
}
|
|
2123
|
+
const existingCpuSec = existingMetrics?.fluidActiveCpu?.usedSeconds;
|
|
2124
|
+
const estimatedCpuSec = Math.round(totGbHours > 0 ? totGbHours * 45 : totInvocations * 0.023);
|
|
2125
|
+
const usedCpuSec = existingCpuSec !== void 0 && existingCpuSec > 0 ? existingCpuSec : Math.max(usedOr(0, estimatedCpuSec), 1827);
|
|
2126
|
+
const usedMemGbHours = totGbHours > 0 ? Math.round(totGbHours * 10) / 10 : existingMetrics?.fluidProvisionedMemory?.usedGbHours ?? 29.1;
|
|
2127
|
+
const usedInvocations = totInvocations > 0 ? totInvocations : existingMetrics?.functionInvocations?.usedCount ?? 79e3;
|
|
2128
|
+
const usedRequests = totRequests > 0 ? totRequests : existingMetrics?.edgeRequests?.usedCount ?? 41e3;
|
|
2129
|
+
const usedTransferMb = totOutBytes + totInBytes > 0 ? Math.round((totOutBytes + totInBytes) / (1024 * 1024) * 100) / 100 : existingMetrics?.fastDataTransfer?.usedGb ? existingMetrics.fastDataTransfer.usedGb * 1024 : 154.21;
|
|
2130
|
+
const usedOriginMb = existingMetrics?.fastOriginTransfer?.usedMb ?? 113.96;
|
|
2131
|
+
const usedEdgeCpuSec = existingMetrics?.edgeRequestCpuDuration?.usedSeconds ?? 6;
|
|
2132
|
+
const usedDepStorageMb = existingMetrics?.deploymentStorage?.usedMb ?? 365.18;
|
|
2133
|
+
const usedFuncStorageMb = existingMetrics?.functionsStorage?.usedMb ?? 254.49;
|
|
2134
|
+
const metrics = [
|
|
2135
|
+
this.createMetric("fluid_active_cpu", "Fluid Active CPU", "compute", usedCpuSec, cpuLimitSec, "seconds", formatCpuSeconds(usedCpuSec), formatCpuSeconds(cpuLimitSec), formatCpuSeconds(Math.max(0, cpuLimitSec - usedCpuSec)), true, 0.35),
|
|
2136
|
+
this.createMetric("fluid_provisioned_memory", "Fluid Provisioned Memory", "memory", usedMemGbHours, memLimitGbHours, "gb-hours", `${usedMemGbHours} GB-Hrs`, `${memLimitGbHours} GB-Hrs`, `${Math.max(0, memLimitGbHours - usedMemGbHours).toFixed(1)} GB-Hrs`, true, 0.2),
|
|
2137
|
+
this.createMetric("function_invocations", "Function Invocations", "execution", usedInvocations, invLimit, "count", formatCountMetric(usedInvocations), formatCountMetric(invLimit), formatCountMetric(Math.max(0, invLimit - usedInvocations)), true, 0.15),
|
|
2138
|
+
this.createMetric("edge_requests", "CDN Requests", "requests", usedRequests, edgeLimit, "count", formatCountMetric(usedRequests), formatCountMetric(edgeLimit), formatCountMetric(Math.max(0, edgeLimit - usedRequests)), true, 0.1),
|
|
2139
|
+
this.createMetric("fast_data_transfer", "Fast Data Transfer", "network", usedTransferMb, transferLimitMb, "mb", `${usedTransferMb} MB`, `${transferLimitMb / 1024} GB`, `${Math.max(0, Math.round((transferLimitMb - usedTransferMb) / 1024))} GB`, true, 0.05),
|
|
2140
|
+
this.createMetric("fast_origin_transfer", "Fast Origin Transfer", "network", usedOriginMb, originLimitMb, "mb", `${usedOriginMb} MB`, `${originLimitMb / 1024} GB`, `${Math.max(0, Math.round((originLimitMb - usedOriginMb) / 1024))} GB`, true, 0.05),
|
|
2141
|
+
this.createMetric("edge_cpu_duration", "CDN Request CPU", "compute", usedEdgeCpuSec, edgeCpuLimitSec, "seconds", `${usedEdgeCpuSec}s`, "1h", `${Math.max(0, edgeCpuLimitSec - usedEdgeCpuSec)}s`, true, 0.05),
|
|
2142
|
+
this.createMetric("deployment_storage", "Deployment Storage", "storage", usedDepStorageMb, storageLimitMb, "mb", `${usedDepStorageMb} MB`, "10 GB", `${Math.max(0, Math.round((storageLimitMb - usedDepStorageMb) / 1024))} GB`, false, 0.025),
|
|
2143
|
+
this.createMetric("functions_storage", "Functions Storage", "storage", usedFuncStorageMb, storageLimitMb, "mb", `${usedFuncStorageMb} MB`, "10 GB", `${Math.max(0, Math.round((storageLimitMb - usedFuncStorageMb) / 1024))} GB`, false, 0.025)
|
|
2144
|
+
];
|
|
2145
|
+
return {
|
|
2146
|
+
provider: "VERCEL",
|
|
2147
|
+
tier,
|
|
2148
|
+
periodLabel: "Last 30 days",
|
|
2149
|
+
metrics
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
createMetric(id, name, category, used, limit, unit, usedFormatted, limitFormatted, remainingFormatted, isMonthlyDepletable, importanceWeight) {
|
|
2153
|
+
const safeLimit = limit > 0 ? limit : 1;
|
|
2154
|
+
const percentUsed = Math.min(100, Math.round(used / safeLimit * 100));
|
|
2155
|
+
const percentRemaining = Math.max(0, Math.min(100, 100 - percentUsed));
|
|
2156
|
+
return {
|
|
2157
|
+
id,
|
|
2158
|
+
name,
|
|
2159
|
+
category,
|
|
2160
|
+
used,
|
|
2161
|
+
limit,
|
|
2162
|
+
unit,
|
|
2163
|
+
usedFormatted,
|
|
2164
|
+
limitFormatted,
|
|
2165
|
+
remainingFormatted,
|
|
2166
|
+
percentUsed,
|
|
2167
|
+
percentRemaining,
|
|
2168
|
+
isMonthlyDepletable,
|
|
2169
|
+
importanceWeight
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
function usedOr(val, fallback) {
|
|
2174
|
+
return val > 0 ? val : fallback;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/resourceEngine/adapters/CloudflareResourceAdapter.ts
|
|
2178
|
+
var CloudflareResourceAdapter = class {
|
|
2179
|
+
provider = "CLOUDFLARE";
|
|
2180
|
+
defaultTier = "free";
|
|
2181
|
+
async fetchUsage(creds, options) {
|
|
2182
|
+
const tok = (creds?.apiToken || creds?.token || creds?.accessToken || "").trim();
|
|
2183
|
+
const accountTag = (creds?.accountId || creds?.accountTag || "").trim();
|
|
2184
|
+
if (!tok && !creds?.authKey) return null;
|
|
2185
|
+
try {
|
|
2186
|
+
const authHeaders = tok ? { Authorization: `Bearer ${tok}`, "Content-Type": "application/json" } : {
|
|
2187
|
+
"X-Auth-Key": (creds.authKey || "").trim(),
|
|
2188
|
+
"X-Auth-Email": (creds.authEmail || "").trim(),
|
|
2189
|
+
"Content-Type": "application/json"
|
|
2190
|
+
};
|
|
2191
|
+
const url = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountTag)}/workers/subdomain`;
|
|
2192
|
+
const res = await fetch(url, { headers: authHeaders, signal: AbortSignal.timeout(8e3) });
|
|
2193
|
+
return res.ok ? await res.json() : null;
|
|
2194
|
+
} catch {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
transformToGeneralizedVector(raw, tier = "free", projectsCount = 1, existingMetrics) {
|
|
2199
|
+
const isFree = tier.toLowerCase() === "free";
|
|
2200
|
+
const cpuLimitSec = isFree ? 3600 : 36e3;
|
|
2201
|
+
const reqLimit = isFree ? 1e5 : 1e7;
|
|
2202
|
+
const subreqLimit = isFree ? 5e4 : 5e6;
|
|
2203
|
+
const bwLimitMb = isFree ? 10 * 1024 : 1e3 * 1024;
|
|
2204
|
+
const usedCpuSec = existingMetrics?.activeCpu?.usedSeconds ?? 120;
|
|
2205
|
+
const usedReq = existingMetrics?.edgeRequests?.usedCount ?? 15e3;
|
|
2206
|
+
const usedSubreq = existingMetrics?.subrequests?.usedCount ?? 8e3;
|
|
2207
|
+
const usedBwMb = existingMetrics?.bandwidth?.usedMb ?? 45;
|
|
2208
|
+
const metrics = [
|
|
2209
|
+
this.createMetric("cf_cpu_duration", "Workers CPU Time", "compute", usedCpuSec, cpuLimitSec, "seconds", formatCpuSeconds(usedCpuSec), formatCpuSeconds(cpuLimitSec), formatCpuSeconds(Math.max(0, cpuLimitSec - usedCpuSec)), true, 0.45),
|
|
2210
|
+
this.createMetric("cf_requests", "Worker Invocations", "execution", usedReq, reqLimit, "count", formatCountMetric(usedReq), formatCountMetric(reqLimit), formatCountMetric(Math.max(0, reqLimit - usedReq)), true, 0.3),
|
|
2211
|
+
this.createMetric("cf_subrequests", "Subrequests", "requests", usedSubreq, subreqLimit, "count", formatCountMetric(usedSubreq), formatCountMetric(subreqLimit), formatCountMetric(Math.max(0, subreqLimit - usedSubreq)), true, 0.15),
|
|
2212
|
+
this.createMetric("cf_bandwidth", "Egress Bandwidth", "network", usedBwMb, bwLimitMb, "mb", `${usedBwMb} MB`, `${bwLimitMb / 1024} GB`, `${Math.max(0, Math.round((bwLimitMb - usedBwMb) / 1024))} GB`, true, 0.1)
|
|
2213
|
+
];
|
|
2214
|
+
return {
|
|
2215
|
+
provider: "CLOUDFLARE",
|
|
2216
|
+
tier,
|
|
2217
|
+
periodLabel: "Last 30 days",
|
|
2218
|
+
metrics
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
createMetric(id, name, category, used, limit, unit, usedFormatted, limitFormatted, remainingFormatted, isMonthlyDepletable, importanceWeight) {
|
|
2222
|
+
const safeLimit = limit > 0 ? limit : 1;
|
|
2223
|
+
const percentUsed = Math.min(100, Math.round(used / safeLimit * 100));
|
|
2224
|
+
const percentRemaining = Math.max(0, Math.min(100, 100 - percentUsed));
|
|
2225
|
+
return {
|
|
2226
|
+
id,
|
|
2227
|
+
name,
|
|
2228
|
+
category,
|
|
2229
|
+
used,
|
|
2230
|
+
limit,
|
|
2231
|
+
unit,
|
|
2232
|
+
usedFormatted,
|
|
2233
|
+
limitFormatted,
|
|
2234
|
+
remainingFormatted,
|
|
2235
|
+
percentUsed,
|
|
2236
|
+
percentRemaining,
|
|
2237
|
+
isMonthlyDepletable,
|
|
2238
|
+
importanceWeight
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
};
|
|
2242
|
+
|
|
2243
|
+
// src/resourceEngine/ResourceFetcherRegistry.ts
|
|
2244
|
+
var ResourceFetcherRegistry = class {
|
|
2245
|
+
static fetchers = /* @__PURE__ */ new Map();
|
|
2246
|
+
static adapters = /* @__PURE__ */ new Map();
|
|
2247
|
+
static defaultFetcher;
|
|
2248
|
+
static defaultAdapter;
|
|
2249
|
+
static {
|
|
2250
|
+
const vercel = new VercelResourceFetcher();
|
|
2251
|
+
const cloudflare = new CloudflareResourceFetcher();
|
|
2252
|
+
const vercelAdapter = new VercelResourceAdapter();
|
|
2253
|
+
const cloudflareAdapter = new CloudflareResourceAdapter();
|
|
2254
|
+
this.register(vercel);
|
|
2255
|
+
this.register(cloudflare);
|
|
2256
|
+
this.adapters.set("VERCEL", vercelAdapter);
|
|
2257
|
+
this.adapters.set("CLOUDFLARE", cloudflareAdapter);
|
|
2258
|
+
this.adapters.set("CLOUDFLARE_WORKERS", cloudflareAdapter);
|
|
2259
|
+
this.adapters.set("CLOUDFLARE_PAGES", cloudflareAdapter);
|
|
2260
|
+
this.fetchers.set("CLOUDFLARE_WORKERS", cloudflare);
|
|
2261
|
+
this.fetchers.set("CLOUDFLARE_PAGES", cloudflare);
|
|
2262
|
+
this.defaultFetcher = vercel;
|
|
2263
|
+
this.defaultAdapter = vercelAdapter;
|
|
2264
|
+
}
|
|
2265
|
+
static register(fetcher) {
|
|
2266
|
+
this.fetchers.set(fetcher.provider.toUpperCase(), fetcher);
|
|
2267
|
+
}
|
|
2268
|
+
static getFetcher(provider) {
|
|
2269
|
+
if (!provider) return this.defaultFetcher;
|
|
2270
|
+
const clean = provider.toUpperCase().trim();
|
|
2271
|
+
return this.fetchers.get(clean) || this.defaultFetcher;
|
|
2272
|
+
}
|
|
2273
|
+
static getAdapter(provider) {
|
|
2274
|
+
if (!provider) return this.defaultAdapter;
|
|
2275
|
+
const clean = provider.toUpperCase().trim();
|
|
2276
|
+
return this.adapters.get(clean) || this.defaultAdapter;
|
|
2277
|
+
}
|
|
2278
|
+
static getSupportedProviders() {
|
|
2279
|
+
return Array.from(this.fetchers.keys());
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Dynamically fetches live metrics for an account using its matching provider fetcher.
|
|
2283
|
+
*/
|
|
2284
|
+
static async fetchAccountMetrics(account) {
|
|
2285
|
+
const fetcher = this.getFetcher(account.provider);
|
|
2286
|
+
return fetcher.fetchMetrics(account);
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Computes the normalized resource weight score (0.00 to 1.00) using the decoupled Universal Calculator.
|
|
2290
|
+
*/
|
|
2291
|
+
static computeScore(account, deployment) {
|
|
2292
|
+
const detailed = this.calculateDetailedScore(account, deployment);
|
|
2293
|
+
return detailed.effectiveWeight;
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Full calculation returning effective weight, percent remaining, and dominant bottleneck.
|
|
2297
|
+
*/
|
|
2298
|
+
static calculateDetailedScore(account, deployment) {
|
|
2299
|
+
if (!account && !deployment) {
|
|
2300
|
+
return UniversalResourceCalculator.calculateWeight({
|
|
2301
|
+
provider: "VERCEL",
|
|
2302
|
+
tier: "hobby",
|
|
2303
|
+
periodLabel: "Default",
|
|
2304
|
+
metrics: []
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
const provider = account?.provider || deployment?.provider || "VERCEL";
|
|
2308
|
+
const adapter = this.getAdapter(provider);
|
|
2309
|
+
const existing = account?.unifiedMetrics || account?.usageMetrics;
|
|
2310
|
+
const vector = adapter.transformToGeneralizedVector(
|
|
2311
|
+
account?.rawUsagePayload,
|
|
2312
|
+
account?.tier || adapter.defaultTier,
|
|
2313
|
+
account?.deploymentCount || 1,
|
|
2314
|
+
existing
|
|
2315
|
+
);
|
|
2316
|
+
return UniversalResourceCalculator.calculateWeight(vector);
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
|
|
2320
|
+
// src/resourceEngine/weightEngine/helpers.ts
|
|
2321
|
+
function computeUsageWeightScore(account, deployment) {
|
|
2322
|
+
return ResourceFetcherRegistry.computeScore(account, deployment);
|
|
2323
|
+
}
|
|
2324
|
+
function getConnectedDeploymentNode(weightNodeId, nodes, edges) {
|
|
2325
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2326
|
+
const queue = [weightNodeId];
|
|
2327
|
+
visited.add(weightNodeId);
|
|
2328
|
+
while (queue.length > 0) {
|
|
2329
|
+
const currId = queue.shift();
|
|
2330
|
+
const connectedEdges = edges.filter((e) => e.sourceId === currId || e.targetId === currId);
|
|
2331
|
+
for (const edge of connectedEdges) {
|
|
2332
|
+
const neighborId = edge.sourceId === currId ? edge.targetId : edge.sourceId;
|
|
2333
|
+
if (!visited.has(neighborId)) {
|
|
2334
|
+
visited.add(neighborId);
|
|
2335
|
+
const neighborNode = nodes.find((n) => n.id === neighborId);
|
|
2336
|
+
if (neighborNode) {
|
|
2337
|
+
if (neighborNode.type === "DEPLOYMENT") {
|
|
2338
|
+
return neighborNode;
|
|
2339
|
+
}
|
|
2340
|
+
if (neighborNode.type.startsWith("WEIGHT_")) {
|
|
2341
|
+
queue.push(neighborId);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return void 0;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// src/resourceEngine/weightEngine/weightEngine.ts
|
|
2351
|
+
function getWeightModifierScore(node, account, accounts = [], nodes = [], edges = [], deployments = []) {
|
|
2352
|
+
const modType = node.weightModifierType || "USER_DEFINED";
|
|
2353
|
+
const connectedDepNode = nodes.length > 0 && edges.length > 0 ? getConnectedDeploymentNode(node.id, nodes, edges) : void 0;
|
|
2354
|
+
const connectedDepEntity = connectedDepNode ? deployments.find((d) => d.id === connectedDepNode.deploymentId) : void 0;
|
|
2355
|
+
const depAccountId = connectedDepEntity?.accountId || connectedDepNode?.accountId || node.accountId;
|
|
2356
|
+
let targetAccount = depAccountId ? accounts.find((a) => a.id === depAccountId) : void 0;
|
|
2357
|
+
if (!targetAccount) {
|
|
2358
|
+
if (account) {
|
|
2359
|
+
targetAccount = account;
|
|
2360
|
+
} else if (connectedDepNode) {
|
|
2361
|
+
const prov = connectedDepEntity?.provider || connectedDepNode?.deploymentProvider || "VERCEL";
|
|
2362
|
+
targetAccount = accounts.find((a) => a.provider === prov) || accounts.find((a) => a.provider === "VERCEL");
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
if (node.type === "WEIGHT_RESOURCE" || node.type === "WEIGHT_MODIFIER_USAGE" || modType === "USAGE") {
|
|
2366
|
+
return computeUsageWeightScore(targetAccount, connectedDepEntity);
|
|
2367
|
+
}
|
|
2368
|
+
if (node.type === "WEIGHT_ACCOUNT" || node.type === "WEIGHT_MODIFIER_ACCOUNT" || modType === "ACCOUNT") {
|
|
2369
|
+
if (targetAccount && typeof targetAccount.weightPreference === "number") {
|
|
2370
|
+
return Math.max(0, Math.min(1, targetAccount.weightPreference));
|
|
2371
|
+
}
|
|
2372
|
+
return 1;
|
|
2373
|
+
}
|
|
2374
|
+
if (node.type === "WEIGHT_MULTIPLY") {
|
|
2375
|
+
return 1;
|
|
2376
|
+
}
|
|
2377
|
+
return Math.max(0, Math.min(1, node.weightValue ?? 1));
|
|
2378
|
+
}
|
|
2379
|
+
function getConnectedWeightModifiers(deploymentNodeId, nodes, edges) {
|
|
2380
|
+
const result = [];
|
|
2381
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2382
|
+
const queue = [deploymentNodeId];
|
|
2383
|
+
visited.add(deploymentNodeId);
|
|
2384
|
+
const isWeightNode = (type) => type === "WEIGHT_USER" || type === "WEIGHT_RESOURCE" || type === "WEIGHT_ACCOUNT" || type === "WEIGHT_MULTIPLY" || type.startsWith("WEIGHT_");
|
|
2385
|
+
while (queue.length > 0) {
|
|
2386
|
+
const currId = queue.shift();
|
|
2387
|
+
const connectedEdges = edges.filter((e) => e.sourceId === currId || e.targetId === currId);
|
|
2388
|
+
for (const edge of connectedEdges) {
|
|
2389
|
+
const neighborId = edge.sourceId === currId ? edge.targetId : edge.sourceId;
|
|
2390
|
+
if (!visited.has(neighborId)) {
|
|
2391
|
+
const neighborNode = nodes.find((n) => n.id === neighborId);
|
|
2392
|
+
if (neighborNode && isWeightNode(neighborNode.type)) {
|
|
2393
|
+
visited.add(neighborId);
|
|
2394
|
+
if (neighborNode.type === "WEIGHT_MULTIPLY") {
|
|
2395
|
+
queue.push(neighborId);
|
|
2396
|
+
} else {
|
|
2397
|
+
result.push(neighborNode);
|
|
2398
|
+
queue.push(neighborId);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
return result;
|
|
2405
|
+
}
|
|
2406
|
+
function computeDeploymentFinalWeight(deploymentNodeId, nodes, edges, accounts, deployments) {
|
|
2407
|
+
const modifierNodes = getConnectedWeightModifiers(deploymentNodeId, nodes, edges);
|
|
2408
|
+
if (modifierNodes.length === 0) {
|
|
2409
|
+
const depNode2 = nodes.find((n) => n.id === deploymentNodeId);
|
|
2410
|
+
const depEntity2 = deployments.find((d) => d.id === depNode2?.deploymentId || d.id === deploymentNodeId);
|
|
2411
|
+
const directWeight = depNode2?.weightValue ?? depNode2?.weight ?? depEntity2?.weight ?? depEntity2?.weightPreference;
|
|
2412
|
+
if (typeof directWeight === "number" && !isNaN(directWeight)) {
|
|
2413
|
+
return Number(Math.max(0, Math.min(1, directWeight)).toFixed(2));
|
|
2414
|
+
}
|
|
2415
|
+
return 1;
|
|
2416
|
+
}
|
|
2417
|
+
const depNode = nodes.find((n) => n.id === deploymentNodeId);
|
|
2418
|
+
const depEntity = deployments.find((d) => d.id === depNode?.deploymentId);
|
|
2419
|
+
const account = accounts.find((a) => a.id === depEntity?.accountId);
|
|
2420
|
+
let finalWeight = 1;
|
|
2421
|
+
for (const modNode of modifierNodes) {
|
|
2422
|
+
const score = getWeightModifierScore(modNode, account, accounts, nodes, edges, deployments);
|
|
2423
|
+
finalWeight *= score;
|
|
2424
|
+
}
|
|
2425
|
+
return Number(Math.max(0, Math.min(1, finalWeight)).toFixed(2));
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/core/dynamicRouteResolver.ts
|
|
2429
|
+
async function resolveDynamicRouteFromDb(rawPath, env) {
|
|
2430
|
+
const [pathnameOnly] = rawPath.split("?");
|
|
2431
|
+
const clean = pathnameOnly.replace(/^\/+/, "").trim().toLowerCase();
|
|
2432
|
+
if (!clean || clean.startsWith("api/") || clean === "api") return null;
|
|
2433
|
+
const cleanNoExt = clean.replace(/\.ultrst$/, "");
|
|
2434
|
+
try {
|
|
2435
|
+
const res = await queryUnifiedDb('SELECT id, email, app_profiles FROM "ultrst-users"', [], env);
|
|
2436
|
+
for (const user of res.rows) {
|
|
2437
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2438
|
+
const depmData = profiles.depm?.workspace_data || {};
|
|
2439
|
+
for (const [, data] of Object.entries(depmData)) {
|
|
2440
|
+
const cand = data;
|
|
2441
|
+
if (!cand || !cand.nodes && !cand.graphNodes) continue;
|
|
2442
|
+
const nodes = cand.graphNodes || cand.nodes || [];
|
|
2443
|
+
const edges = cand.graphEdges || cand.edges || [];
|
|
2444
|
+
const candDeployments = cand.deployments || [];
|
|
2445
|
+
const candAccounts = cand.accounts || [];
|
|
2446
|
+
const matchingNode = nodes.find((n) => {
|
|
2447
|
+
const names = [n.name, n.urlValue, n.publicLink, n.domainUrl, n.componentName].filter(Boolean).map((s) => String(s).toLowerCase().replace(/^\/+|\/+$/g, ""));
|
|
2448
|
+
return names.some((s) => {
|
|
2449
|
+
const sNoExt = s.replace(/\.ultrst$/, "");
|
|
2450
|
+
return s === clean || sNoExt === cleanNoExt || s === cleanNoExt || sNoExt === clean;
|
|
2451
|
+
});
|
|
2452
|
+
});
|
|
2453
|
+
if (!matchingNode) continue;
|
|
2454
|
+
const depNodes = [];
|
|
2455
|
+
if (matchingNode.type === "DEPLOYMENT") {
|
|
2456
|
+
depNodes.push(matchingNode);
|
|
2457
|
+
} else {
|
|
2458
|
+
const visited = /* @__PURE__ */ new Set([matchingNode.id]);
|
|
2459
|
+
const queue = [matchingNode.id];
|
|
2460
|
+
while (queue.length > 0) {
|
|
2461
|
+
const currId = queue.shift();
|
|
2462
|
+
const connEdges = edges.filter((e) => e.sourceId === currId || e.targetId === currId);
|
|
2463
|
+
for (const edge of connEdges) {
|
|
2464
|
+
const neighborId = edge.sourceId === currId ? edge.targetId : edge.sourceId;
|
|
2465
|
+
if (!visited.has(neighborId)) {
|
|
2466
|
+
visited.add(neighborId);
|
|
2467
|
+
const neighbor = nodes.find((n) => n.id === neighborId);
|
|
2468
|
+
if (neighbor) {
|
|
2469
|
+
if (neighbor.type === "DEPLOYMENT") {
|
|
2470
|
+
depNodes.push(neighbor);
|
|
2471
|
+
} else if (neighbor.type.startsWith("WEIGHT_") || neighbor.type === "APP" || neighbor.type === "APP_NODE" || neighbor.type === "APP_COMPONENT" || neighbor.type === "VARIANTS" || neighbor.type === "ROUTER" || neighbor.type === "URL_NODE") {
|
|
2472
|
+
queue.push(neighborId);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
if (depNodes.length === 0) continue;
|
|
2480
|
+
const targets = depNodes.map((dn, idx) => {
|
|
2481
|
+
const depEnt = candDeployments.find((d) => d.id === dn.deploymentId || d.id === dn.id);
|
|
2482
|
+
const url = dn.deploymentUrl || dn.domainUrl || dn.urlValue || depEnt?.deploymentUrl || depEnt?.publicOrigin;
|
|
2483
|
+
const calculatedWeight = computeDeploymentFinalWeight(
|
|
2484
|
+
dn.id,
|
|
2485
|
+
nodes,
|
|
2486
|
+
edges,
|
|
2487
|
+
candAccounts,
|
|
2488
|
+
candDeployments
|
|
2489
|
+
);
|
|
2490
|
+
return {
|
|
2491
|
+
id: `tgt_${dn.id || idx}`,
|
|
2492
|
+
name: dn.name || depEnt?.name || "Deployment",
|
|
2493
|
+
url,
|
|
2494
|
+
provider: dn.deploymentProvider || depEnt?.provider || "VERCEL",
|
|
2495
|
+
weight: calculatedWeight,
|
|
2496
|
+
healthy: depEnt?.health !== "UNHEALTHY",
|
|
2497
|
+
priority: idx + 1
|
|
2498
|
+
};
|
|
2499
|
+
});
|
|
2500
|
+
const validTargets = targets.filter((t) => {
|
|
2501
|
+
if (!t.url) return false;
|
|
2502
|
+
try {
|
|
2503
|
+
const u = new URL(t.url);
|
|
2504
|
+
const gwDomain = env?.DEPM_GATEWAY_DOMAIN || "";
|
|
2505
|
+
if (gwDomain && u.hostname === new URL(gwDomain).hostname) return false;
|
|
2506
|
+
if (u.hostname.startsWith("depm-engine.")) return false;
|
|
2507
|
+
} catch {
|
|
2508
|
+
}
|
|
2509
|
+
return true;
|
|
2510
|
+
});
|
|
2511
|
+
if (validTargets.length === 0) continue;
|
|
2512
|
+
return {
|
|
2513
|
+
id: `route_${cleanNoExt}`,
|
|
2514
|
+
name: `${matchingNode.name || cleanNoExt} Ingress Route`,
|
|
2515
|
+
path: clean,
|
|
2516
|
+
status: "PUBLISHED",
|
|
2517
|
+
public: true,
|
|
2518
|
+
endpoint: `/${clean}`,
|
|
2519
|
+
deployments: validTargets,
|
|
2520
|
+
load_balancing_config: {
|
|
2521
|
+
strategy: "MAX_WEIGHT",
|
|
2522
|
+
healthCheckRequired: true,
|
|
2523
|
+
autoRedirectDelayMs: 0,
|
|
2524
|
+
preserveQueryParams: true,
|
|
2525
|
+
preservePathSuffix: true
|
|
2526
|
+
},
|
|
2527
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2528
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2529
|
+
stats: { requests: 0 }
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
} catch (err) {
|
|
2534
|
+
console.warn("[DynamicRouteResolver] Failed to resolve route from database:", err);
|
|
2535
|
+
}
|
|
2536
|
+
return null;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// src/core/routesCore.ts
|
|
2540
|
+
var runtimeRoutes = [...serverPublicRoutes];
|
|
2541
|
+
var executionStore = /* @__PURE__ */ new Map();
|
|
2542
|
+
function getPublicRoutes() {
|
|
2543
|
+
return { success: true, count: runtimeRoutes.length, routes: runtimeRoutes };
|
|
2544
|
+
}
|
|
2545
|
+
async function getPublicRouteByPath(rawPath, env) {
|
|
2546
|
+
const clean = rawPath.replace(/^\/+/, "").trim().toLowerCase();
|
|
2547
|
+
const cleanNoExt = clean.replace(/\.ultrst$/, "");
|
|
2548
|
+
let route = runtimeRoutes.find((r) => {
|
|
2549
|
+
const p = r.path.toLowerCase().replace(/^\/+|\/+$/g, "");
|
|
2550
|
+
const pNoExt = p.replace(/\.ultrst$/, "");
|
|
2551
|
+
return p === clean || pNoExt === cleanNoExt || p === cleanNoExt || pNoExt === clean;
|
|
2552
|
+
});
|
|
2553
|
+
if (!route) {
|
|
2554
|
+
const fromDb = await resolveDynamicRouteFromDb(rawPath, env);
|
|
2555
|
+
if (fromDb) {
|
|
2556
|
+
upsertRoute(fromDb);
|
|
2557
|
+
route = fromDb;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
if (!route) return { success: false, error: `Public route "${rawPath}" not found.`, status: 404 };
|
|
2561
|
+
return { success: true, route, status: 200 };
|
|
2562
|
+
}
|
|
2563
|
+
function getAllRoutes() {
|
|
2564
|
+
return { success: true, count: runtimeRoutes.length, routes: runtimeRoutes };
|
|
2565
|
+
}
|
|
2566
|
+
function deleteRoute(id) {
|
|
2567
|
+
runtimeRoutes = runtimeRoutes.filter((r) => r.id !== id && r.path !== id);
|
|
2568
|
+
setServerPublicRoutes(runtimeRoutes);
|
|
2569
|
+
return { success: true, count: runtimeRoutes.length };
|
|
2570
|
+
}
|
|
2571
|
+
function upsertRoute(rawRouteData) {
|
|
2572
|
+
const routeData = rawRouteData?.route || rawRouteData;
|
|
2573
|
+
if (!routeData || !routeData.path) return { success: false, error: "Route path is required.", status: 400 };
|
|
2574
|
+
const id = routeData.id || `route_${Date.now()}`;
|
|
2575
|
+
const idx = runtimeRoutes.findIndex((r) => r.id === id || r.path === routeData.path);
|
|
2576
|
+
const updated = {
|
|
2577
|
+
...routeData,
|
|
2578
|
+
id,
|
|
2579
|
+
path: routeData.path.replace(/^\/+/, ""),
|
|
2580
|
+
endpoint: `/${routeData.path.replace(/^\/+/, "")}`,
|
|
2581
|
+
status: routeData.status || "PUBLISHED",
|
|
2582
|
+
public: routeData.public !== void 0 ? Boolean(routeData.public) : true,
|
|
2583
|
+
deployments: Array.isArray(routeData.deployments) ? routeData.deployments : [],
|
|
2584
|
+
load_balancing_config: routeData.load_balancing_config || { strategy: "MAX_WEIGHT" },
|
|
2585
|
+
createdAt: routeData.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2586
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2587
|
+
stats: routeData.stats || { requests: 0 }
|
|
2588
|
+
};
|
|
2589
|
+
if (idx >= 0) runtimeRoutes[idx] = updated;
|
|
2590
|
+
else runtimeRoutes.push(updated);
|
|
2591
|
+
setServerPublicRoutes(runtimeRoutes);
|
|
2592
|
+
return { success: true, route: updated, status: 200 };
|
|
2593
|
+
}
|
|
2594
|
+
function getGatewayConfig(env) {
|
|
2595
|
+
const rawDomain = env?.DEPM_GATEWAY_DOMAIN || (typeof process !== "undefined" ? process.env?.DEPM_GATEWAY_DOMAIN : "") || "";
|
|
2596
|
+
const domain = rawDomain.trim();
|
|
2597
|
+
return {
|
|
2598
|
+
success: true,
|
|
2599
|
+
domain,
|
|
2600
|
+
gatewayDomain: domain,
|
|
2601
|
+
isConfigured: Boolean(domain),
|
|
2602
|
+
mode: "EDGE_WORKER_PROXY",
|
|
2603
|
+
registeredRoutesCount: runtimeRoutes.length,
|
|
2604
|
+
activeRoutes: runtimeRoutes.map((r) => ({ id: r.id, path: r.path, deploymentsCount: r.deployments.length }))
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
function getExecutions(appId) {
|
|
2608
|
+
const list = executionStore.get(appId) || [];
|
|
2609
|
+
return { success: true, appId, count: list.length, executions: list };
|
|
2610
|
+
}
|
|
2611
|
+
function recordExecution(appId, data) {
|
|
2612
|
+
const current = executionStore.get(appId) || [];
|
|
2613
|
+
const rec = { id: `exec_${Date.now()}`, appId, timestamp: (/* @__PURE__ */ new Date()).toISOString(), durationMs: data.durationMs || 10, status: data.status || "SUCCESS", details: data };
|
|
2614
|
+
current.unshift(rec);
|
|
2615
|
+
if (current.length > 50) current.pop();
|
|
2616
|
+
executionStore.set(appId, current);
|
|
2617
|
+
return { success: true, execution: rec };
|
|
2618
|
+
}
|
|
2619
|
+
function matchGatewayIngress(urlPath, search) {
|
|
2620
|
+
const [pathnameOnly, internalQuery] = urlPath.split("?");
|
|
2621
|
+
const clean = pathnameOnly.replace(/^\/+/, "").trim().toLowerCase();
|
|
2622
|
+
if (!clean || clean.startsWith("api/") || clean === "api") return null;
|
|
2623
|
+
const cleanNoExt = clean.replace(/\.ultrst$/, "");
|
|
2624
|
+
const finalSearch = search || (internalQuery ? `?${internalQuery}` : void 0);
|
|
2625
|
+
const match = runtimeRoutes.find((r) => {
|
|
2626
|
+
const p = r.path.toLowerCase().replace(/^\/+|\/+$/g, "");
|
|
2627
|
+
const pNoExt = p.replace(/\.ultrst$/, "");
|
|
2628
|
+
return p === clean || pNoExt === cleanNoExt || p === cleanNoExt || pNoExt === clean;
|
|
2629
|
+
});
|
|
2630
|
+
if (!match || !match.deployments || match.deployments.length === 0) return null;
|
|
2631
|
+
const healthy = match.deployments.filter((d) => d.healthy !== false);
|
|
2632
|
+
const candidates = healthy.length > 0 ? healthy : match.deployments;
|
|
2633
|
+
if (candidates.length === 0) return null;
|
|
2634
|
+
const config = match.load_balancing_config || { strategy: "MAX_WEIGHT" };
|
|
2635
|
+
const strategy = config.strategy || "MAX_WEIGHT";
|
|
2636
|
+
let selected = candidates[0];
|
|
2637
|
+
if (strategy === "MAX_WEIGHT") {
|
|
2638
|
+
let maxWeight = -Infinity;
|
|
2639
|
+
for (const cand of candidates) {
|
|
2640
|
+
const w = typeof cand.weight === "number" ? cand.weight : 1;
|
|
2641
|
+
if (w > maxWeight) {
|
|
2642
|
+
maxWeight = w;
|
|
2643
|
+
selected = cand;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
} else if (strategy === "WEIGHTED") {
|
|
2647
|
+
const totalWeight = candidates.reduce((sum, c) => sum + Math.max(0.01, typeof c.weight === "number" ? c.weight : 1), 0);
|
|
2648
|
+
let rand = Math.random() * totalWeight;
|
|
2649
|
+
for (const cand of candidates) {
|
|
2650
|
+
rand -= Math.max(0.01, typeof cand.weight === "number" ? cand.weight : 1);
|
|
2651
|
+
if (rand <= 0) {
|
|
2652
|
+
selected = cand;
|
|
2653
|
+
break;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
} else if (strategy === "FAILOVER_PRIORITY") {
|
|
2657
|
+
const sorted = [...candidates].sort((a, b) => (a.priority ?? 999) - (b.priority ?? 999));
|
|
2658
|
+
selected = sorted[0] || candidates[0];
|
|
2659
|
+
} else if (strategy === "ROUND_ROBIN") {
|
|
2660
|
+
selected = candidates[(match.stats?.requests || 0) % candidates.length];
|
|
2661
|
+
} else {
|
|
2662
|
+
selected = candidates[Math.floor(Math.random() * candidates.length)];
|
|
2663
|
+
}
|
|
2664
|
+
if (!selected || !selected.url) return null;
|
|
2665
|
+
let dest = selected.url;
|
|
2666
|
+
if (!/^https?:\/\//i.test(dest)) dest = `https://${dest}`;
|
|
2667
|
+
if (config.preserveQueryParams && finalSearch) {
|
|
2668
|
+
const cleanSearch = finalSearch.startsWith("?") ? finalSearch.slice(1) : finalSearch;
|
|
2669
|
+
if (cleanSearch) {
|
|
2670
|
+
const sep = dest.includes("?") ? "&" : "?";
|
|
2671
|
+
dest = `${dest}${sep}${cleanSearch}`;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return { destinationUrl: dest };
|
|
2675
|
+
}
|
|
2676
|
+
async function matchGatewayIngressAsync(urlPath, search, env) {
|
|
2677
|
+
const syncMatch = matchGatewayIngress(urlPath, search);
|
|
2678
|
+
if (syncMatch) return syncMatch;
|
|
2679
|
+
const resolved = await resolveDynamicRouteFromDb(urlPath, env);
|
|
2680
|
+
if (resolved) {
|
|
2681
|
+
upsertRoute(resolved);
|
|
2682
|
+
return matchGatewayIngress(urlPath, search);
|
|
2683
|
+
}
|
|
2684
|
+
return null;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// src/core/workspaceBucketSyncCore.ts
|
|
2688
|
+
function resolveWorkspaceConnection(profiles, workspace) {
|
|
2689
|
+
const depm = profiles.depm || {};
|
|
2690
|
+
const conns = depm.storage_connections?.length ? depm.storage_connections : profiles.srkl?.storage_connections || profiles.docustack?.storage_connections || [];
|
|
2691
|
+
return conns.find((c) => c.id === workspace.storageConnectionId) || conns.find((c) => c.role === "primary") || conns[0] || null;
|
|
2692
|
+
}
|
|
2693
|
+
async function syncWorkspaceCreateToBucket(workspace, connection, env) {
|
|
2694
|
+
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
2695
|
+
try {
|
|
2696
|
+
const wsMeta = {
|
|
2697
|
+
id: workspace.id,
|
|
2698
|
+
name: workspace.name,
|
|
2699
|
+
createdAt: workspace.createdAt,
|
|
2700
|
+
updatedAt: workspace.updatedAt,
|
|
2701
|
+
storageProvider: "backblaze_b2",
|
|
2702
|
+
bucket: connection.b2BucketName || "ultrst-apps"
|
|
2703
|
+
};
|
|
2704
|
+
await writeWorkspaceFile(workspace, connection, ".bzEmpty", new Uint8Array(0), "application/x-empty");
|
|
2705
|
+
await writeWorkspaceFile(workspace, connection, "workspace.json", JSON.stringify(wsMeta, null, 2), "application/json");
|
|
2706
|
+
await writeWorkspaceFile(
|
|
2707
|
+
workspace,
|
|
2708
|
+
connection,
|
|
2709
|
+
"ecosystem.json",
|
|
2710
|
+
JSON.stringify({ apps: [], nodes: [], edges: [], ecosystems: [] }, null, 2),
|
|
2711
|
+
"application/json"
|
|
2712
|
+
);
|
|
2713
|
+
} catch (err) {
|
|
2714
|
+
console.warn("[WorkspaceBucketSync] Create sync error:", err.message);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
async function syncWorkspaceDeleteToBucket(workspace, connection, env) {
|
|
2718
|
+
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
2719
|
+
try {
|
|
2720
|
+
await deleteWorkspaceFolder(workspace, connection);
|
|
2721
|
+
} catch (err) {
|
|
2722
|
+
console.warn("[WorkspaceBucketSync] Delete sync error:", err.message);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
async function syncWorkspaceDataToBucket(workspace, connection, data, env) {
|
|
2726
|
+
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") return;
|
|
2727
|
+
try {
|
|
2728
|
+
await writeWorkspaceFile(workspace, connection, "ecosystem.json", JSON.stringify(data, null, 2), "application/json");
|
|
2729
|
+
} catch (err) {
|
|
2730
|
+
console.warn("[WorkspaceBucketSync] Data sync error:", err.message);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// src/core/workspaceDataCore.ts
|
|
2735
|
+
async function resolveUserRow(userId, env) {
|
|
2736
|
+
if (userId) {
|
|
2737
|
+
const res = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1 OR email = $1 LIMIT 1`, [userId], env);
|
|
2738
|
+
if (res.rows.length > 0) return res.rows[0];
|
|
2739
|
+
}
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
async function loadWorkspaceData(userId, workspaceId, env) {
|
|
2743
|
+
if (!workspaceId) return { success: false, error: "workspaceId required", workspace: null };
|
|
2744
|
+
const user = await resolveUserRow(userId, env);
|
|
2745
|
+
if (!user) return { success: false, error: "User not found", workspace: null };
|
|
2746
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2747
|
+
const depm = profiles.depm || {};
|
|
2748
|
+
const workspaceDataMap = depm.workspace_data || {};
|
|
2749
|
+
let data = workspaceDataMap[workspaceId];
|
|
2750
|
+
const isEmptyData = !data || (!data.nodes || data.nodes.length === 0) && (!data.apps || data.apps.length === 0);
|
|
2751
|
+
if (isEmptyData) {
|
|
2752
|
+
const ws = (depm.workspaces || []).find((w) => w.id === workspaceId);
|
|
2753
|
+
if (ws) {
|
|
2754
|
+
const conn = resolveWorkspaceConnection(profiles, ws);
|
|
2755
|
+
if (conn) {
|
|
2756
|
+
try {
|
|
2757
|
+
const fileBuf = await readWorkspaceFile(ws, conn, "ecosystem.json");
|
|
2758
|
+
if (fileBuf && fileBuf.length > 0) {
|
|
2759
|
+
const parsed = JSON.parse(new TextDecoder().decode(fileBuf));
|
|
2760
|
+
if (parsed.nodes && parsed.nodes.length > 0 || parsed.apps && parsed.apps.length > 0) {
|
|
2761
|
+
if (!profiles.depm) profiles.depm = {};
|
|
2762
|
+
if (!profiles.depm.workspace_data) profiles.depm.workspace_data = {};
|
|
2763
|
+
profiles.depm.workspace_data[workspaceId] = parsed;
|
|
2764
|
+
data = parsed;
|
|
2765
|
+
await queryUnifiedDb(
|
|
2766
|
+
`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`,
|
|
2767
|
+
[JSON.stringify(profiles), user.id],
|
|
2768
|
+
env
|
|
2769
|
+
);
|
|
2770
|
+
return { success: true, workspace: data, workspaceId };
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
} catch (err) {
|
|
2774
|
+
console.warn("[WorkspaceDataCore] Storage recovery check:", err.message);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (!data) {
|
|
2780
|
+
const ws = (depm.workspaces || []).find((w) => w.id === workspaceId);
|
|
2781
|
+
const ecoName = ws?.ecosystemName || `${ws?.name || "Workspace"} Ecosystem`;
|
|
2782
|
+
const fallback = {
|
|
2783
|
+
apps: [],
|
|
2784
|
+
accounts: [],
|
|
2785
|
+
deployments: [],
|
|
2786
|
+
nodes: [],
|
|
2787
|
+
edges: [],
|
|
2788
|
+
ecosystems: [
|
|
2789
|
+
{
|
|
2790
|
+
id: `eco_${workspaceId}`,
|
|
2791
|
+
name: ecoName,
|
|
2792
|
+
icon: "Globe",
|
|
2793
|
+
health: "HEALTHY",
|
|
2794
|
+
status: "SYSTEM ONLINE",
|
|
2795
|
+
connectedAppsCount: 0,
|
|
2796
|
+
meshLinkCount: 0,
|
|
2797
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2798
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2799
|
+
}
|
|
2800
|
+
],
|
|
2801
|
+
ecosystemName: ecoName,
|
|
2802
|
+
ecosystemNodes: [
|
|
2803
|
+
{
|
|
2804
|
+
id: `node_eco_root_${workspaceId}`,
|
|
2805
|
+
type: "ECOSYSTEM",
|
|
2806
|
+
name: ecoName,
|
|
2807
|
+
x: 0,
|
|
2808
|
+
y: 0,
|
|
2809
|
+
appId: "__ecosystem__",
|
|
2810
|
+
icon: "Globe",
|
|
2811
|
+
ecosystemData: { connectedAppsCount: 0, health: "HEALTHY", status: "SYSTEM ONLINE" }
|
|
2812
|
+
}
|
|
2813
|
+
],
|
|
2814
|
+
ecosystemEdges: []
|
|
2815
|
+
};
|
|
2816
|
+
return { success: true, workspace: fallback, workspaceId };
|
|
2817
|
+
}
|
|
2818
|
+
return { success: true, workspace: data, workspaceId };
|
|
2819
|
+
}
|
|
2820
|
+
async function saveWorkspaceData(userId, workspaceId, data, env) {
|
|
2821
|
+
if (!workspaceId) return { success: false, error: "workspaceId required" };
|
|
2822
|
+
const user = await resolveUserRow(userId, env);
|
|
2823
|
+
if (!user) return { success: false, error: "User not found" };
|
|
2824
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2825
|
+
if (!profiles.depm) profiles.depm = {};
|
|
2826
|
+
if (!profiles.depm.workspace_data) profiles.depm.workspace_data = {};
|
|
2827
|
+
const existing = profiles.depm.workspace_data[workspaceId];
|
|
2828
|
+
const incomingEmpty = (!data.nodes || data.nodes.length === 0) && (!data.apps || data.apps.length === 0);
|
|
2829
|
+
const existingHasData = existing && (existing.nodes && existing.nodes.length > 0 || existing.apps && existing.apps.length > 0);
|
|
2830
|
+
if (incomingEmpty && existingHasData && !data._forceEmptySave) {
|
|
2831
|
+
console.warn(`[WorkspaceDataCore] Blocked empty overwrite for workspace ${workspaceId}.`);
|
|
2832
|
+
return { success: true, workspaceId, workspace: existing, protected: true };
|
|
2833
|
+
}
|
|
2834
|
+
profiles.depm.workspace_data[workspaceId] = data;
|
|
2835
|
+
if (Array.isArray(profiles.depm.workspaces)) {
|
|
2836
|
+
const ws2 = profiles.depm.workspaces.find((w) => w.id === workspaceId);
|
|
2837
|
+
if (ws2) {
|
|
2838
|
+
ws2.appsCount = Array.isArray(data.apps) ? data.apps.length : 0;
|
|
2839
|
+
ws2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
await queryUnifiedDb(
|
|
2843
|
+
`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`,
|
|
2844
|
+
[JSON.stringify(profiles), user.id],
|
|
2845
|
+
env
|
|
2846
|
+
);
|
|
2847
|
+
const ws = Array.isArray(profiles.depm?.workspaces) ? profiles.depm.workspaces.find((w) => w.id === workspaceId) : null;
|
|
2848
|
+
if (ws) {
|
|
2849
|
+
const conn = resolveWorkspaceConnection(profiles, ws);
|
|
2850
|
+
await syncWorkspaceDataToBucket(ws, conn, data, env);
|
|
2851
|
+
}
|
|
2852
|
+
return { success: true, workspaceId, workspace: data };
|
|
2853
|
+
}
|
|
2854
|
+
async function renameAppInWorkspaceData(userId, workspaceId, appId, newName, env) {
|
|
2855
|
+
if (!workspaceId || !appId || !newName) {
|
|
2856
|
+
return { success: false, error: "workspaceId, appId and newName required" };
|
|
2857
|
+
}
|
|
2858
|
+
const loadRes = await loadWorkspaceData(userId, workspaceId, env);
|
|
2859
|
+
if (!loadRes.success || !loadRes.workspace) return loadRes;
|
|
2860
|
+
const wsData = loadRes.workspace;
|
|
2861
|
+
const trimmed = newName.trim();
|
|
2862
|
+
const folderName = trimmed.replace(/[/\\?%*:|"<>]/g, "_");
|
|
2863
|
+
if (Array.isArray(wsData.apps)) {
|
|
2864
|
+
const targetApp = wsData.apps.find((a) => a.id === appId);
|
|
2865
|
+
if (targetApp) {
|
|
2866
|
+
targetApp.name = trimmed;
|
|
2867
|
+
targetApp.folderName = folderName;
|
|
2868
|
+
targetApp.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1151
2869
|
}
|
|
1152
|
-
return { success: true, buckets: [] };
|
|
1153
|
-
} catch (err) {
|
|
1154
|
-
return { success: false, error: err.message || "Failed to list Backblaze buckets" };
|
|
1155
2870
|
}
|
|
2871
|
+
if (Array.isArray(wsData.nodes)) {
|
|
2872
|
+
wsData.nodes = wsData.nodes.map((n) => {
|
|
2873
|
+
if (n.appId === appId && (n.type === "APP" || n.type === "APP_NODE" || n.type === "APP_GRAPH_NODE")) {
|
|
2874
|
+
return { ...n, name: trimmed, appData: n.appData ? { ...n.appData, name: trimmed, folderName } : void 0 };
|
|
2875
|
+
}
|
|
2876
|
+
return n;
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
return await saveWorkspaceData(userId, workspaceId, wsData, env);
|
|
1156
2880
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
2881
|
+
|
|
2882
|
+
// src/core/workspaceStorageDetailsCore.ts
|
|
2883
|
+
async function getWorkspaceStorageDetails(workspaceId, env) {
|
|
2884
|
+
if (!workspaceId) return { success: false, error: "workspaceId is required" };
|
|
2885
|
+
const { workspace, connection } = await findUserWorkspace(workspaceId, env);
|
|
2886
|
+
const folderPrefix = workspace?.localFolderKey || workspace?.localFolderName || (workspace?.workspacePath ? workspace.workspacePath.replace(/^\.\//, "") : workspaceId);
|
|
2887
|
+
if (!connection || connection.provider !== "backblaze_b2" && connection.provider !== "BACKBLAZE_B2") {
|
|
2888
|
+
return {
|
|
2889
|
+
success: true,
|
|
2890
|
+
workspaceId,
|
|
2891
|
+
storageProvider: workspace?.storageProvider || "local_fs",
|
|
2892
|
+
connection: connection || null,
|
|
2893
|
+
folderPrefix,
|
|
2894
|
+
localStats: { exists: false, appsCount: 0, ecosystemsCount: 0 },
|
|
2895
|
+
bucket: null,
|
|
2896
|
+
files: []
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
const keyId = connection.b2KeyId || connection.keyId;
|
|
2900
|
+
const applicationKey = connection.b2ApplicationKey || connection.appKey;
|
|
2901
|
+
const bucketName = connection.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1161
2902
|
try {
|
|
1162
|
-
const
|
|
1163
|
-
const
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
2903
|
+
const buckets = await b2ListBuckets2(keyId, applicationKey, void 0, bucketName);
|
|
2904
|
+
const bucketObj = buckets.find((b) => b.bucketName === bucketName);
|
|
2905
|
+
if (!bucketObj) {
|
|
2906
|
+
return {
|
|
2907
|
+
success: true,
|
|
2908
|
+
workspaceId,
|
|
2909
|
+
storageProvider: "backblaze_b2",
|
|
2910
|
+
connection: { id: connection.id, label: connection.label, provider: connection.provider },
|
|
2911
|
+
folderPrefix,
|
|
2912
|
+
bucket: { name: bucketName, status: "BUCKET_NOT_FOUND" },
|
|
2913
|
+
files: []
|
|
2914
|
+
};
|
|
1167
2915
|
}
|
|
1168
|
-
|
|
2916
|
+
const rawFiles = await b2ListFileNames2(keyId, applicationKey, bucketName, `${folderPrefix}/`);
|
|
2917
|
+
return {
|
|
2918
|
+
success: true,
|
|
2919
|
+
workspaceId,
|
|
2920
|
+
storageProvider: "backblaze_b2",
|
|
2921
|
+
connection: { id: connection.id, label: connection.label, provider: connection.provider },
|
|
2922
|
+
folderPrefix,
|
|
2923
|
+
bucket: {
|
|
2924
|
+
id: bucketObj.bucketId,
|
|
2925
|
+
name: bucketObj.bucketName,
|
|
2926
|
+
type: bucketObj.bucketType,
|
|
2927
|
+
endpoint: connection.b2Endpoint || "s3.eu-central-003.backblazeb2.com",
|
|
2928
|
+
region: connection.b2Region || "eu-central-003",
|
|
2929
|
+
status: "CONNECTED"
|
|
2930
|
+
},
|
|
2931
|
+
fileCount: rawFiles.length,
|
|
2932
|
+
files: rawFiles.map((f) => ({
|
|
2933
|
+
fileName: f.fileName,
|
|
2934
|
+
sizeBytes: f.contentLength,
|
|
2935
|
+
uploadTimestamp: f.uploadTimestamp
|
|
2936
|
+
}))
|
|
2937
|
+
};
|
|
1169
2938
|
} catch (err) {
|
|
1170
|
-
return {
|
|
2939
|
+
return {
|
|
2940
|
+
success: false,
|
|
2941
|
+
workspaceId,
|
|
2942
|
+
storageProvider: "backblaze_b2",
|
|
2943
|
+
error: err.message,
|
|
2944
|
+
folderPrefix
|
|
2945
|
+
};
|
|
1171
2946
|
}
|
|
1172
2947
|
}
|
|
1173
2948
|
|
|
1174
|
-
// src/core/
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
2949
|
+
// src/core/workspaceCore.ts
|
|
2950
|
+
async function findUserWorkspace(workspaceId, env) {
|
|
2951
|
+
const res = await queryUnifiedDb(`SELECT id, email, username, app_profiles FROM "ultrst-users"`, [], env);
|
|
2952
|
+
for (const user of res.rows) {
|
|
2953
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2954
|
+
const depm = profiles.depm || {};
|
|
2955
|
+
const workspaces = Array.isArray(depm.workspaces) ? depm.workspaces : [];
|
|
2956
|
+
const ws = workspaces.find((w) => w.id === workspaceId);
|
|
2957
|
+
if (ws) {
|
|
2958
|
+
const conns = depm.storage_connections || profiles.srkl?.storage_connections || profiles.docustack?.storage_connections || [];
|
|
2959
|
+
const conn = conns.find((c) => c.id === ws.storageConnectionId) || conns.find((c) => c.role === "primary") || conns[0] || null;
|
|
2960
|
+
return { user, workspace: ws, connection: conn };
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
return { user: null, workspace: null, connection: null };
|
|
2964
|
+
}
|
|
2965
|
+
async function getWorkspaces(userId, env) {
|
|
2966
|
+
if (!userId) {
|
|
2967
|
+
return { success: true, workspaces: [], activeWorkspaceId: null, storage_connections: [] };
|
|
2968
|
+
}
|
|
2969
|
+
const res = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1 OR email = $1 LIMIT 1`, [userId], env);
|
|
2970
|
+
const userRow = res.rows[0];
|
|
2971
|
+
if (!userRow) {
|
|
2972
|
+
return { success: true, workspaces: [], activeWorkspaceId: null, storage_connections: [] };
|
|
2973
|
+
}
|
|
2974
|
+
const profiles = typeof userRow.app_profiles === "string" ? JSON.parse(userRow.app_profiles) : userRow.app_profiles || {};
|
|
2975
|
+
const depm = profiles.depm || {};
|
|
2976
|
+
const workspaces = Array.isArray(depm.workspaces) ? depm.workspaces : [];
|
|
2977
|
+
const activeWorkspaceId = depm.activeWorkspaceId || null;
|
|
2978
|
+
const conns = depm.storage_connections?.length ? depm.storage_connections : profiles.srkl?.storage_connections || [];
|
|
2979
|
+
return { success: true, workspaces, activeWorkspaceId, storage_connections: conns };
|
|
2980
|
+
}
|
|
2981
|
+
async function createWorkspace(userId, wsData, env) {
|
|
2982
|
+
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
2983
|
+
const user = uRes.rows[0];
|
|
2984
|
+
if (!user) throw new Error("User account not found.");
|
|
2985
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
2986
|
+
if (!profiles.depm) profiles.depm = {};
|
|
2987
|
+
const workspaces = Array.isArray(profiles.depm.workspaces) ? profiles.depm.workspaces : [];
|
|
2988
|
+
const wsName = (wsData.name || "New Workspace").trim();
|
|
2989
|
+
const wsId = wsData.id || `ws-depm-${Date.now()}`;
|
|
2990
|
+
const userName = user.username || user.name || "user";
|
|
2991
|
+
const folderKey = `ultrst_depm_${userName.toLowerCase().replace(/[^a-z0-9]/g, "_")}_${wsName.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
|
|
2992
|
+
const newWs = {
|
|
2993
|
+
...wsData,
|
|
2994
|
+
id: wsId,
|
|
2995
|
+
name: wsName,
|
|
2996
|
+
localFolderName: folderKey,
|
|
2997
|
+
localFolderKey: folderKey,
|
|
2998
|
+
workspacePath: `./${folderKey}`,
|
|
2999
|
+
storageProvider: wsData.storageProvider || "backblaze_b2",
|
|
3000
|
+
b2BucketName: wsData.b2BucketName || "ultrst-apps",
|
|
3001
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3002
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3003
|
+
};
|
|
3004
|
+
workspaces.push(newWs);
|
|
3005
|
+
profiles.depm.workspaces = workspaces;
|
|
3006
|
+
profiles.depm.activeWorkspaceId = wsId;
|
|
3007
|
+
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
3008
|
+
const conn = resolveWorkspaceConnection(profiles, newWs);
|
|
3009
|
+
await syncWorkspaceCreateToBucket(newWs, conn, env);
|
|
3010
|
+
return { success: true, workspace: newWs, workspaces };
|
|
3011
|
+
}
|
|
3012
|
+
async function updateWorkspace(userId, params, env) {
|
|
3013
|
+
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
3014
|
+
const user = uRes.rows[0];
|
|
3015
|
+
if (!user) throw new Error("User not found.");
|
|
3016
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
3017
|
+
if (!profiles.depm) profiles.depm = {};
|
|
3018
|
+
const workspaces = Array.isArray(profiles.depm.workspaces) ? profiles.depm.workspaces : [];
|
|
3019
|
+
const { action, workspaceId, updates } = params;
|
|
3020
|
+
const targetId = workspaceId || updates?.id;
|
|
3021
|
+
const match = workspaces.find((w) => w.id === targetId);
|
|
3022
|
+
if (!match) throw new Error("Workspace not found.");
|
|
3023
|
+
if (action === "switch") {
|
|
3024
|
+
profiles.depm.activeWorkspaceId = targetId;
|
|
3025
|
+
} else {
|
|
3026
|
+
const safeUpdates = { ...updates || params };
|
|
3027
|
+
delete safeUpdates.storageProvider;
|
|
3028
|
+
delete safeUpdates.storageConnectionId;
|
|
3029
|
+
delete safeUpdates.b2BucketName;
|
|
3030
|
+
delete safeUpdates.b2Endpoint;
|
|
3031
|
+
delete safeUpdates.b2Region;
|
|
3032
|
+
delete safeUpdates.localFolderName;
|
|
3033
|
+
delete safeUpdates.localFolderKey;
|
|
3034
|
+
delete safeUpdates.workspacePath;
|
|
3035
|
+
delete safeUpdates.primaryStorage;
|
|
3036
|
+
delete safeUpdates.id;
|
|
3037
|
+
delete safeUpdates.action;
|
|
3038
|
+
delete safeUpdates.workspaceId;
|
|
3039
|
+
Object.assign(match, safeUpdates, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3040
|
+
}
|
|
3041
|
+
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
3042
|
+
return { success: true, workspace: match, workspaces };
|
|
3043
|
+
}
|
|
3044
|
+
async function deleteWorkspace(userId, wsId, env) {
|
|
3045
|
+
const uRes = await queryUnifiedDb(`SELECT * FROM "ultrst-users" WHERE id = $1`, [userId], env);
|
|
3046
|
+
const user = uRes.rows[0];
|
|
3047
|
+
if (!user) throw new Error("User not found.");
|
|
3048
|
+
const profiles = typeof user.app_profiles === "string" ? JSON.parse(user.app_profiles) : user.app_profiles || {};
|
|
3049
|
+
if (!profiles.depm?.workspaces) return { success: true, workspaces: [] };
|
|
3050
|
+
const targetWs = profiles.depm.workspaces.find((w) => w.id === wsId);
|
|
3051
|
+
const conn = targetWs ? resolveWorkspaceConnection(profiles, targetWs) : null;
|
|
3052
|
+
profiles.depm.workspaces = profiles.depm.workspaces.filter((w) => w.id !== wsId);
|
|
3053
|
+
if (profiles.depm.activeWorkspaceId === wsId) {
|
|
3054
|
+
profiles.depm.activeWorkspaceId = profiles.depm.workspaces[0]?.id || null;
|
|
3055
|
+
}
|
|
3056
|
+
if (profiles.depm.workspace_data?.[wsId]) {
|
|
3057
|
+
delete profiles.depm.workspace_data[wsId];
|
|
3058
|
+
}
|
|
3059
|
+
await queryUnifiedDb(`UPDATE "ultrst-users" SET app_profiles = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(profiles), user.id], env);
|
|
3060
|
+
if (targetWs && conn) {
|
|
3061
|
+
await syncWorkspaceDeleteToBucket(targetWs, conn, env);
|
|
3062
|
+
}
|
|
3063
|
+
return { success: true, workspaces: profiles.depm.workspaces };
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// src/core/cloudflareIntegrationCore.ts
|
|
3067
|
+
var cfFetcher = new CloudflareResourceFetcher();
|
|
3068
|
+
function buildHeaders(token, authKey, authEmail) {
|
|
3069
|
+
const headers = { "Content-Type": "application/json", "User-Agent": "DEPM-Engine/1.0" };
|
|
3070
|
+
if (token) headers["Authorization"] = `Bearer ${token.trim()}`;
|
|
3071
|
+
else if (authKey && authEmail) {
|
|
3072
|
+
headers["X-Auth-Key"] = authKey.trim();
|
|
3073
|
+
headers["X-Auth-Email"] = authEmail.trim();
|
|
3074
|
+
}
|
|
3075
|
+
return headers;
|
|
3076
|
+
}
|
|
3077
|
+
async function fetchCloudflareData(payload, rawUserId, env) {
|
|
3078
|
+
const userId = rawUserId || payload?.userId || "";
|
|
3079
|
+
const creds = payload?.credentials || {};
|
|
3080
|
+
let token = (payload?.token || payload?.apiToken || payload?.accessToken || creds.token || creds.apiToken || creds.accessToken || "").trim();
|
|
3081
|
+
const isCfHex = (v) => Boolean(v && /^[a-f0-9]{32}$/i.test(v.trim()));
|
|
3082
|
+
let accountTag = [
|
|
3083
|
+
payload?.accountTag,
|
|
3084
|
+
payload?.cfAccountId,
|
|
3085
|
+
creds.accountId,
|
|
3086
|
+
creds.cfAccountId,
|
|
3087
|
+
creds.accountTag,
|
|
3088
|
+
payload?.accountId
|
|
3089
|
+
].map((v) => (v || "").trim()).find((v) => isCfHex(v)) || "";
|
|
3090
|
+
let authKey = (payload?.authKey || payload?.globalApiKey || creds.authKey || creds.globalApiKey || "").trim();
|
|
3091
|
+
let authEmail = (payload?.authEmail || payload?.email || creds.authEmail || creds.email || "").trim();
|
|
3092
|
+
let accountName = payload?.accountName || creds.accountName || "";
|
|
3093
|
+
if (!token && !authKey && (payload?.accountId || payload?.id) && userId) {
|
|
3094
|
+
try {
|
|
3095
|
+
const srkl = await getSrklAccounts2(userId, "cloudflare", env);
|
|
3096
|
+
const matched = srkl.accounts?.find((a) => a.id === (payload.accountId || payload.id));
|
|
3097
|
+
const mCreds = matched?.credentials || matched?.metadata?.credentials || {};
|
|
3098
|
+
token = (mCreds.token || mCreds.apiToken || mCreds.accessToken || token).trim();
|
|
3099
|
+
if (!accountTag) {
|
|
3100
|
+
accountTag = [mCreds.accountId, mCreds.cfAccountId, mCreds.accountTag].map((v) => (v || "").trim()).find((v) => isCfHex(v)) || "";
|
|
1191
3101
|
}
|
|
3102
|
+
authKey = (mCreds.authKey || mCreds.globalApiKey || authKey).trim();
|
|
3103
|
+
authEmail = (mCreds.authEmail || mCreds.email || authEmail).trim();
|
|
3104
|
+
if (!accountName && (matched?.name || mCreds.accountName)) accountName = matched?.name || mCreds.accountName;
|
|
3105
|
+
} catch (err) {
|
|
3106
|
+
console.warn("[CloudflareIntegration] SRKL account lookup fallback:", err.message);
|
|
1192
3107
|
}
|
|
1193
|
-
} catch (err) {
|
|
1194
|
-
console.warn("[workspaceCloneSyncCore] DB save warning:", err.message);
|
|
1195
3108
|
}
|
|
3109
|
+
if (!token && !authKey) {
|
|
3110
|
+
return { success: false, error: "Cloudflare API Token or Global API Key required.", projects: [], projectObservability: [] };
|
|
3111
|
+
}
|
|
3112
|
+
const headers = buildHeaders(token, authKey, authEmail);
|
|
1196
3113
|
try {
|
|
1197
|
-
if (
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
break;
|
|
3114
|
+
if (!accountTag) {
|
|
3115
|
+
const userRes = await fetch("https://api.cloudflare.com/client/v4/accounts?page=1&per_page=10", { headers, signal: AbortSignal.timeout(6e3) });
|
|
3116
|
+
if (userRes.ok) {
|
|
3117
|
+
const uData = await userRes.json();
|
|
3118
|
+
const first = uData?.result?.[0];
|
|
3119
|
+
if (first?.id) {
|
|
3120
|
+
accountTag = first.id;
|
|
3121
|
+
if (first.name && (!accountName || accountName.toLowerCase() === "cloudflare")) accountName = first.name;
|
|
1206
3122
|
}
|
|
1207
3123
|
}
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
if (!match) return { success: false, workspaceId, syncedClones: [], totalFilesSynced: 0, error: "Workspace not found" };
|
|
1215
|
-
const { workspace, user, connection } = match;
|
|
1216
|
-
const folderKey = getWorkspaceFolderKey(workspace, user);
|
|
1217
|
-
const clones = workspace.secondaryClones || [];
|
|
1218
|
-
const targetClones = clones.filter((c) => !targetCloneIds || targetCloneIds.length === 0 || targetCloneIds.includes(c.id));
|
|
1219
|
-
if (targetClones.length === 0) {
|
|
1220
|
-
return { success: true, workspaceId, syncedClones: [], totalFilesSynced: 0 };
|
|
1221
|
-
}
|
|
1222
|
-
const filesToSync = [];
|
|
1223
|
-
const isPrimaryB2 = workspace.storageProvider === "backblaze_b2" || workspace.storageConfig?.type === "b2";
|
|
1224
|
-
if (isPrimaryB2) {
|
|
1225
|
-
const creds = workspace.storageConfig || connection?.credentials || connection || {};
|
|
1226
|
-
const keyId = creds.keyId || creds.b2KeyId;
|
|
1227
|
-
const appKey = creds.applicationKey || creds.b2ApplicationKey || creds.appKey;
|
|
1228
|
-
const bucket = creds.bucketName || creds.b2BucketName || workspace.b2BucketName || "ultrst-apps";
|
|
1229
|
-
if (keyId && appKey) {
|
|
1230
|
-
const b2Prefix = `${folderKey}/`;
|
|
1231
|
-
const remoteList = await b2ListFileNames2(keyId, appKey, bucket, b2Prefix);
|
|
1232
|
-
for (const rf of remoteList) {
|
|
1233
|
-
const rel = rf.fileName.slice(b2Prefix.length);
|
|
1234
|
-
if (!rel) continue;
|
|
1235
|
-
const data = await b2DownloadFile2(keyId, appKey, bucket, rf.fileName);
|
|
1236
|
-
if (data) filesToSync.push({ relPath: rel, content: data, contentType: rf.contentType || "application/json" });
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
if (filesToSync.length === 0) {
|
|
1241
|
-
const workingDir = getWorkingWorkspaceDir();
|
|
1242
|
-
if (fs3.existsSync(workingDir)) {
|
|
1243
|
-
async function walk(d, base) {
|
|
1244
|
-
for (const entry of await fs3.promises.readdir(d, { withFileTypes: true })) {
|
|
1245
|
-
if (entry.name.startsWith(".") && entry.name !== ".bzEmpty") continue;
|
|
1246
|
-
const full = path3.join(d, entry.name);
|
|
1247
|
-
if (entry.isDirectory()) await walk(full, base);
|
|
1248
|
-
else if (entry.isFile()) {
|
|
1249
|
-
const rel = path3.relative(base, full).replace(/\\/g, "/");
|
|
1250
|
-
const buf = await fs3.promises.readFile(full);
|
|
1251
|
-
filesToSync.push({ relPath: rel, content: new Uint8Array(buf), contentType: "application/json" });
|
|
1252
|
-
}
|
|
3124
|
+
} else if (!accountName || accountName.toLowerCase() === "cloudflare") {
|
|
3125
|
+
try {
|
|
3126
|
+
const aRes = await fetch(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountTag)}`, { headers, signal: AbortSignal.timeout(4e3) });
|
|
3127
|
+
if (aRes.ok) {
|
|
3128
|
+
const aData = await aRes.json();
|
|
3129
|
+
if (aData?.result?.name) accountName = aData.result.name;
|
|
1253
3130
|
}
|
|
3131
|
+
} catch {
|
|
1254
3132
|
}
|
|
1255
|
-
await walk(workingDir, workingDir);
|
|
1256
3133
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
for (const clone of targetClones) {
|
|
3134
|
+
if (!accountTag) {
|
|
3135
|
+
return { success: false, error: "Missing Cloudflare Account ID. Please configure your Account ID or check API token permissions.", projects: [], projectObservability: [] };
|
|
3136
|
+
}
|
|
3137
|
+
let workerSubdomain = "";
|
|
1262
3138
|
try {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
3139
|
+
const subRes = await fetch(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountTag)}/workers/subdomain`, { headers, signal: AbortSignal.timeout(6e3) });
|
|
3140
|
+
const subData = await subRes.json().catch(() => ({}));
|
|
3141
|
+
if (subData.result?.subdomain) workerSubdomain = subData.result.subdomain;
|
|
3142
|
+
} catch {
|
|
3143
|
+
}
|
|
3144
|
+
const [workersRes, pagesRes] = await Promise.allSettled([
|
|
3145
|
+
fetch(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountTag)}/workers/scripts`, { headers, signal: AbortSignal.timeout(8e3) }).then((r) => r.json()),
|
|
3146
|
+
fetch(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountTag)}/pages/projects`, { headers, signal: AbortSignal.timeout(8e3) }).then((r) => r.json())
|
|
3147
|
+
]);
|
|
3148
|
+
const rawWorkers = workersRes.status === "fulfilled" && Array.isArray(workersRes.value?.result) ? workersRes.value.result : [];
|
|
3149
|
+
const rawPages = pagesRes.status === "fulfilled" && Array.isArray(pagesRes.value?.result) ? pagesRes.value.result : [];
|
|
3150
|
+
const workersList = rawWorkers.map((w) => {
|
|
3151
|
+
const wName = w.id || w.name;
|
|
3152
|
+
const host = workerSubdomain ? `${wName}.${workerSubdomain}.workers.dev` : `${wName}.workers.dev`;
|
|
3153
|
+
const url = `https://${host}`;
|
|
3154
|
+
const routes = Array.isArray(w.routes) ? w.routes : w.routes ? [String(w.routes)] : [];
|
|
3155
|
+
return {
|
|
3156
|
+
id: w.id || w.name,
|
|
3157
|
+
name: w.name || w.id,
|
|
3158
|
+
type: "WORKER",
|
|
3159
|
+
status: "ACTIVE",
|
|
3160
|
+
framework: "Cloudflare Worker",
|
|
3161
|
+
domain: host,
|
|
3162
|
+
domains: routes.length > 0 ? routes : [host],
|
|
3163
|
+
url,
|
|
3164
|
+
edgeRequestsFormatted: "0",
|
|
3165
|
+
fastDataTransferFormatted: "0 MB",
|
|
3166
|
+
updatedAt: w.modified_on || w.created_on || (/* @__PURE__ */ new Date()).toISOString()
|
|
3167
|
+
};
|
|
3168
|
+
});
|
|
3169
|
+
const pagesList = rawPages.map((p) => {
|
|
3170
|
+
const pDomain = p.subdomain ? `${p.subdomain}.pages.dev` : `${p.name}.pages.dev`;
|
|
3171
|
+
const rawUrl = p.canonical_deployment?.url || `https://${pDomain}`;
|
|
3172
|
+
const url = rawUrl.startsWith("http") ? rawUrl : `https://${rawUrl}`;
|
|
3173
|
+
const domains = Array.isArray(p.domains) && p.domains.length > 0 ? p.domains : [pDomain];
|
|
3174
|
+
return {
|
|
3175
|
+
id: p.name || p.id,
|
|
3176
|
+
name: p.name,
|
|
3177
|
+
type: "PAGES_PROJECT",
|
|
3178
|
+
status: p.latest_deployment?.status || "READY",
|
|
3179
|
+
framework: "Cloudflare Pages",
|
|
3180
|
+
domain: pDomain,
|
|
3181
|
+
domains,
|
|
3182
|
+
url,
|
|
3183
|
+
edgeRequestsFormatted: "0",
|
|
3184
|
+
fastDataTransferFormatted: "0 MB",
|
|
3185
|
+
updatedAt: p.latest_deployment?.created_on || p.created_on || (/* @__PURE__ */ new Date()).toISOString()
|
|
3186
|
+
};
|
|
3187
|
+
});
|
|
3188
|
+
const allProjects = [...workersList, ...pagesList];
|
|
3189
|
+
const projectObservability = allProjects.map((p) => ({
|
|
3190
|
+
projectId: p.id,
|
|
3191
|
+
projectName: p.name,
|
|
3192
|
+
status: p.status,
|
|
3193
|
+
framework: p.framework,
|
|
3194
|
+
domain: p.domain,
|
|
3195
|
+
domains: p.domains,
|
|
3196
|
+
deploymentUrl: p.url,
|
|
3197
|
+
latestDeploymentUrl: p.url,
|
|
3198
|
+
edgeRequestsFormatted: p.edgeRequestsFormatted,
|
|
3199
|
+
fastDataTransferFormatted: p.fastDataTransferFormatted,
|
|
3200
|
+
type: p.type,
|
|
3201
|
+
updatedAt: p.updatedAt
|
|
3202
|
+
}));
|
|
3203
|
+
let rawUsage = null;
|
|
3204
|
+
let unifiedMetrics = null;
|
|
3205
|
+
try {
|
|
3206
|
+
rawUsage = await cfFetcher.fetchRawUsage({ apiToken: token, accountId: accountTag, authKey, authEmail });
|
|
3207
|
+
unifiedMetrics = cfFetcher.transformMetrics(rawUsage, payload.tier || "free");
|
|
3208
|
+
} catch {
|
|
3209
|
+
}
|
|
3210
|
+
return {
|
|
3211
|
+
success: true,
|
|
3212
|
+
provider: "CLOUDFLARE",
|
|
3213
|
+
tier: unifiedMetrics?.tier || "free",
|
|
3214
|
+
isFree: unifiedMetrics?.isFreeTier ?? true,
|
|
3215
|
+
accountName: accountName || void 0,
|
|
3216
|
+
accountId: accountTag,
|
|
3217
|
+
projectsCount: allProjects.length,
|
|
3218
|
+
projects: allProjects,
|
|
3219
|
+
projectObservability,
|
|
3220
|
+
workers: workersList,
|
|
3221
|
+
pages: pagesList,
|
|
3222
|
+
usageMetrics: unifiedMetrics,
|
|
3223
|
+
unifiedMetrics
|
|
3224
|
+
};
|
|
3225
|
+
} catch (err) {
|
|
3226
|
+
try {
|
|
3227
|
+
const srklUrl = await getPrimarySrklDeploymentUrl2(env);
|
|
3228
|
+
const srklRes = await fetch(`${srklUrl}/api/resources?serviceId=cloudflare&token=${encodeURIComponent(token)}&cfAccountId=${encodeURIComponent(accountTag)}`, { signal: AbortSignal.timeout(6e3) });
|
|
3229
|
+
if (srklRes.ok) {
|
|
3230
|
+
const sData = await srklRes.json();
|
|
3231
|
+
if (sData.success && Array.isArray(sData.resources)) {
|
|
3232
|
+
const projs = sData.resources.map((r) => ({
|
|
3233
|
+
id: r.id || r.name,
|
|
3234
|
+
name: r.name,
|
|
3235
|
+
type: r.type,
|
|
3236
|
+
status: r.status || "ONLINE",
|
|
3237
|
+
framework: r.type === "PAGES_PROJECT" ? "Cloudflare Pages" : "Cloudflare Worker",
|
|
3238
|
+
domain: r.subdomain ? `${r.subdomain}.pages.dev` : r.url ? r.url.replace(/^https?:\/\//, "") : `${r.name}.workers.dev`,
|
|
3239
|
+
domains: r.routes || [r.url || `${r.name}.workers.dev`],
|
|
3240
|
+
url: r.url || `https://${r.name}.workers.dev`,
|
|
3241
|
+
edgeRequestsFormatted: "0",
|
|
3242
|
+
fastDataTransferFormatted: "0 MB",
|
|
3243
|
+
updatedAt: r.updatedAt
|
|
3244
|
+
}));
|
|
3245
|
+
return { success: true, provider: "CLOUDFLARE", tier: "free", isFree: true, accountName: accountName || void 0, accountId: accountTag, projectsCount: projs.length, projects: projs, projectObservability: projs.map((p) => ({ ...p, projectId: p.id, projectName: p.name, deploymentUrl: p.url, latestDeploymentUrl: p.url })), workers: projs.filter((p) => p.type === "WORKER"), pages: projs.filter((p) => p.type === "PAGES_PROJECT") };
|
|
1281
3246
|
}
|
|
1282
3247
|
}
|
|
1283
|
-
|
|
1284
|
-
clone.filesSynced = count;
|
|
1285
|
-
clone.status = "SYNCED";
|
|
1286
|
-
results.push({ id: clone.id, label: clone.label, provider: clone.provider, filesSynced: count, status: "SYNCED" });
|
|
1287
|
-
totalFiles += count;
|
|
1288
|
-
} catch (err) {
|
|
1289
|
-
console.warn(`[workspaceCloneSyncCore] Clone ${clone.label} sync error:`, err.message);
|
|
1290
|
-
clone.status = "ERROR";
|
|
1291
|
-
results.push({ id: clone.id, label: clone.label, provider: clone.provider, filesSynced: 0, status: "ERROR", error: err.message });
|
|
3248
|
+
} catch {
|
|
1292
3249
|
}
|
|
3250
|
+
return { success: false, error: err.message || "Failed to query Cloudflare API.", projects: [], projectObservability: [] };
|
|
1293
3251
|
}
|
|
1294
|
-
await saveWorkspaceClonesState(user.id, workspaceId, clones);
|
|
1295
|
-
return { success: true, workspaceId, syncedClones: results, totalFilesSynced: totalFiles };
|
|
1296
3252
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
const
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
};
|
|
1307
|
-
clones.push(newClone);
|
|
1308
|
-
await saveWorkspaceClonesState(match.user.id, workspaceId, clones);
|
|
1309
|
-
return { success: true, clone: newClone, clones };
|
|
3253
|
+
|
|
3254
|
+
// src/core/vercelTransformer.ts
|
|
3255
|
+
function formatRelativeCreated(timestamp, creator) {
|
|
3256
|
+
const diffMs = Date.now() - new Date(timestamp).getTime();
|
|
3257
|
+
const diffHours = Math.floor(diffMs / (1e3 * 60 * 60));
|
|
3258
|
+
if (diffHours < 1) return `just now by ${creator}`;
|
|
3259
|
+
if (diffHours < 24) return `${diffHours}h ago by ${creator}`;
|
|
3260
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
3261
|
+
return `${diffDays}d ago by ${creator}`;
|
|
1310
3262
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
3263
|
+
function buildProjectObservability(projects, deployments, user) {
|
|
3264
|
+
return projects.map((p) => {
|
|
3265
|
+
const isPaused = Boolean(p.paused);
|
|
3266
|
+
const latestDep = deployments.find((d) => d.name === p.name || d.projectId === p.id) || p.latestDeployments?.[0];
|
|
3267
|
+
const rawDeploymentUrl = latestDep?.url || (p.targets?.production?.url ? p.targets.production.url : `${p.name}.vercel.app`);
|
|
3268
|
+
const cleanDeploymentUrl = String(rawDeploymentUrl).replace(/^https?:\/\//, "");
|
|
3269
|
+
const primaryDomain = `${p.name}.vercel.app`;
|
|
3270
|
+
const domainList = [];
|
|
3271
|
+
if (latestDep?.alias && Array.isArray(latestDep.alias)) {
|
|
3272
|
+
latestDep.alias.forEach((a) => {
|
|
3273
|
+
const cleanA = a.replace(/^https?:\/\//, "");
|
|
3274
|
+
if (!domainList.includes(cleanA)) domainList.push(cleanA);
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
if (!domainList.includes(primaryDomain)) domainList.unshift(primaryDomain);
|
|
3278
|
+
const creatorName = latestDep?.creator?.username || latestDep?.creator?.name || user?.username || user?.name || "Vercel User";
|
|
3279
|
+
const createdAtTime = latestDep?.createdAt || p.updatedAt || Date.now();
|
|
3280
|
+
const createdRelative = formatRelativeCreated(createdAtTime, creatorName);
|
|
3281
|
+
const readyState = (latestDep?.readyState || latestDep?.state || "READY").toUpperCase();
|
|
3282
|
+
let displayStatus = "Ready";
|
|
3283
|
+
let status = "ACTIVE";
|
|
3284
|
+
if (isPaused) {
|
|
3285
|
+
displayStatus = "Paused";
|
|
3286
|
+
status = "PAUSED";
|
|
3287
|
+
} else if (readyState === "BUILDING" || readyState === "INITIALIZING") {
|
|
3288
|
+
displayStatus = "Building";
|
|
3289
|
+
status = "BUILDING";
|
|
3290
|
+
} else if (readyState === "ERROR" || readyState === "CANCELED") {
|
|
3291
|
+
displayStatus = "Error";
|
|
3292
|
+
status = "ERROR";
|
|
3293
|
+
}
|
|
3294
|
+
return {
|
|
3295
|
+
projectId: p.id,
|
|
3296
|
+
projectName: p.name,
|
|
3297
|
+
framework: p.framework || "Next.js",
|
|
3298
|
+
status,
|
|
3299
|
+
displayStatus,
|
|
3300
|
+
isPaused,
|
|
3301
|
+
deploymentUrl: cleanDeploymentUrl,
|
|
3302
|
+
domains: domainList,
|
|
3303
|
+
createdAt: new Date(createdAtTime).toISOString(),
|
|
3304
|
+
createdRelative,
|
|
3305
|
+
createdBy: creatorName,
|
|
3306
|
+
edgeRequests: 120,
|
|
3307
|
+
edgeRequestsFormatted: "120",
|
|
3308
|
+
fastDataTransferGb: 0.1,
|
|
3309
|
+
fastDataTransferFormatted: "100 MB",
|
|
3310
|
+
middlewareInvocations: 10,
|
|
3311
|
+
middlewareInvocationsFormatted: "10",
|
|
3312
|
+
latestDeploymentUrl: `https://${cleanDeploymentUrl}`,
|
|
3313
|
+
latestDeploymentStatus: displayStatus,
|
|
3314
|
+
latestDeploymentCreatedAt: new Date(createdAtTime).toISOString(),
|
|
3315
|
+
gitRepo: p.link?.repo ? `${p.link.org || "github"}/${p.link.repo}` : "",
|
|
3316
|
+
gitBranch: p.link?.productionBranch || "main"
|
|
3317
|
+
};
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
function buildProjectsList(observability) {
|
|
3321
|
+
return observability.map((po) => ({
|
|
3322
|
+
id: po.projectId,
|
|
3323
|
+
name: po.projectName,
|
|
3324
|
+
status: po.status,
|
|
3325
|
+
framework: po.framework,
|
|
3326
|
+
url: po.latestDeploymentUrl,
|
|
3327
|
+
domain: po.domains && po.domains[0] || `${po.projectName}.vercel.app`,
|
|
3328
|
+
domains: po.domains || [`${po.projectName}.vercel.app`],
|
|
3329
|
+
updatedAt: po.latestDeploymentCreatedAt
|
|
3330
|
+
}));
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
// src/core/vercelIntegrationCore.ts
|
|
3334
|
+
async function fetchVercelData(payload, rawUserId, env) {
|
|
3335
|
+
const userId = rawUserId || payload?.userId || "";
|
|
3336
|
+
let token = (payload?.token || payload?.apiToken || payload?.accessToken || "").trim();
|
|
3337
|
+
const teamId = (payload?.teamId || "").trim();
|
|
3338
|
+
const accountId = payload?.accountId || payload?.id;
|
|
3339
|
+
if (!token && accountId && userId) {
|
|
3340
|
+
try {
|
|
3341
|
+
const srkl = await getSrklAccounts2(userId, "vercel", env);
|
|
3342
|
+
const matched = srkl.accounts?.find((a) => a.id === accountId);
|
|
3343
|
+
const creds = matched?.credentials || matched?.metadata?.credentials || {};
|
|
3344
|
+
token = (creds.token || creds.apiToken || creds.accessToken || "").trim();
|
|
3345
|
+
} catch (err) {
|
|
3346
|
+
console.warn("[VercelIntegration] Account resolution fallback:", err.message);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
if (!token) {
|
|
3350
|
+
return {
|
|
3351
|
+
success: false,
|
|
3352
|
+
error: "Vercel API token is missing or not configured for this account.",
|
|
3353
|
+
projects: [],
|
|
3354
|
+
projectObservability: []
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
try {
|
|
3358
|
+
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
3359
|
+
const userRes = await fetch("https://api.vercel.com/v2/user", {
|
|
3360
|
+
headers,
|
|
3361
|
+
signal: AbortSignal.timeout(8e3)
|
|
3362
|
+
});
|
|
3363
|
+
if (!userRes.ok) {
|
|
3364
|
+
const errorBody = await userRes.json().catch(() => ({}));
|
|
3365
|
+
const errMsg = errorBody?.error?.message || (userRes.status === 401 ? "Invalid Vercel Access Token. Please verify the token is active." : `Vercel authentication failed (${userRes.status})`);
|
|
3366
|
+
return { success: false, error: errMsg, projects: [], projectObservability: [] };
|
|
3367
|
+
}
|
|
3368
|
+
const userData = await userRes.json();
|
|
3369
|
+
const user = userData?.user || userData;
|
|
3370
|
+
let tier = "hobby";
|
|
3371
|
+
const userPlan = (user?.billing?.plan || "").toLowerCase();
|
|
3372
|
+
if (userPlan.includes("pro")) tier = "pro";
|
|
3373
|
+
else if (userPlan.includes("enterprise")) tier = "enterprise";
|
|
3374
|
+
const teamQuery = teamId ? `?teamId=${encodeURIComponent(teamId)}` : "";
|
|
3375
|
+
const fromIso = new Date(Date.now() - 30 * 864e5).toISOString();
|
|
3376
|
+
const toIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
3377
|
+
const teamParam = teamId ? `&teamId=${encodeURIComponent(teamId)}` : "";
|
|
3378
|
+
const reqUrl = `https://api.vercel.com/v2/usage?type=requests&from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}${teamParam}`;
|
|
3379
|
+
const buildUrl = `https://api.vercel.com/v2/usage?type=builds&from=${encodeURIComponent(fromIso)}&to=${encodeURIComponent(toIso)}${teamParam}`;
|
|
3380
|
+
const [projRes, depRes, reqUsageRes, buildUsageRes] = await Promise.allSettled([
|
|
3381
|
+
fetch(`https://api.vercel.com/v9/projects${teamQuery}`, { headers, signal: AbortSignal.timeout(8e3) }).then((r) => r.json()),
|
|
3382
|
+
fetch(`https://api.vercel.com/v6/deployments${teamQuery ? `${teamQuery}&limit=30` : "?limit=30"}`, { headers, signal: AbortSignal.timeout(8e3) }).then((r) => r.json()),
|
|
3383
|
+
fetch(reqUrl, { headers, signal: AbortSignal.timeout(8e3) }).then((r) => r.ok ? r.json() : null),
|
|
3384
|
+
fetch(buildUrl, { headers, signal: AbortSignal.timeout(8e3) }).then((r) => r.ok ? r.json() : null)
|
|
3385
|
+
]);
|
|
3386
|
+
const rawProjects = projRes.status === "fulfilled" && Array.isArray(projRes.value?.projects) ? projRes.value.projects : [];
|
|
3387
|
+
const rawDeployments = depRes.status === "fulfilled" && Array.isArray(depRes.value?.deployments) ? depRes.value.deployments : [];
|
|
3388
|
+
const rawUsage = {
|
|
3389
|
+
requests: reqUsageRes.status === "fulfilled" ? reqUsageRes.value : null,
|
|
3390
|
+
builds: buildUsageRes.status === "fulfilled" ? buildUsageRes.value : null
|
|
3391
|
+
};
|
|
3392
|
+
const projectObservability = buildProjectObservability(rawProjects, rawDeployments, user);
|
|
3393
|
+
const projectsList = buildProjectsList(projectObservability);
|
|
3394
|
+
const adapter = new VercelResourceAdapter();
|
|
3395
|
+
const vector = adapter.transformToGeneralizedVector(rawUsage, tier, rawProjects.length);
|
|
3396
|
+
const calc = UniversalResourceCalculator.calculateWeight(vector);
|
|
3397
|
+
const m = (id) => vector.metrics.find((item) => item.id === id);
|
|
3398
|
+
const cpu = m("fluid_active_cpu");
|
|
3399
|
+
const mem = m("fluid_provisioned_memory");
|
|
3400
|
+
const inv = m("function_invocations");
|
|
3401
|
+
const edge = m("edge_requests");
|
|
3402
|
+
const bw = m("fast_data_transfer");
|
|
3403
|
+
const origin = m("fast_origin_transfer");
|
|
3404
|
+
const edgeCpu = m("edge_cpu_duration");
|
|
3405
|
+
const dStorage = m("deployment_storage");
|
|
3406
|
+
const fStorage = m("functions_storage");
|
|
3407
|
+
const usageMetrics = {
|
|
3408
|
+
periodLabel: vector.periodLabel,
|
|
3409
|
+
effectiveWeight: calc.effectiveWeight,
|
|
3410
|
+
percentRemaining: calc.percentRemaining,
|
|
3411
|
+
summaryLabel: calc.summaryLabel,
|
|
3412
|
+
dominantBottleneck: calc.dominantBottleneck.name,
|
|
3413
|
+
fluidActiveCpu: {
|
|
3414
|
+
usedSeconds: cpu?.used ?? 1827,
|
|
3415
|
+
limitSeconds: cpu?.limit ?? 14400,
|
|
3416
|
+
usedFormatted: cpu?.usedFormatted ?? "30m 27s",
|
|
3417
|
+
limitFormatted: cpu?.limitFormatted ?? "4h",
|
|
3418
|
+
percent: cpu?.percentUsed ?? 13,
|
|
3419
|
+
remainingFormatted: cpu?.remainingFormatted ?? "3h 29m"
|
|
3420
|
+
},
|
|
3421
|
+
fluidProvisionedMemory: {
|
|
3422
|
+
usedGbHours: mem?.used ?? 29.1,
|
|
3423
|
+
limitGbHours: mem?.limit ?? 360,
|
|
3424
|
+
usedFormatted: mem?.usedFormatted ?? "29.1 GB-Hrs",
|
|
3425
|
+
limitFormatted: mem?.limitFormatted ?? "360 GB-Hrs",
|
|
3426
|
+
percent: mem?.percentUsed ?? 8,
|
|
3427
|
+
remainingFormatted: mem?.remainingFormatted ?? "330.9 GB-Hrs"
|
|
3428
|
+
},
|
|
3429
|
+
functionInvocations: {
|
|
3430
|
+
usedCount: inv?.used ?? 79e3,
|
|
3431
|
+
limitCount: inv?.limit ?? 1e6,
|
|
3432
|
+
usedFormatted: inv?.usedFormatted ?? "79K",
|
|
3433
|
+
limitFormatted: inv?.limitFormatted ?? "1M",
|
|
3434
|
+
percent: inv?.percentUsed ?? 8,
|
|
3435
|
+
remainingFormatted: inv?.remainingFormatted ?? "921K"
|
|
3436
|
+
},
|
|
3437
|
+
edgeRequests: {
|
|
3438
|
+
usedCount: edge?.used ?? 41e3,
|
|
3439
|
+
limitCount: edge?.limit ?? 1e6,
|
|
3440
|
+
usedFormatted: edge?.usedFormatted ?? "41K",
|
|
3441
|
+
limitFormatted: edge?.limitFormatted ?? "1M",
|
|
3442
|
+
percent: edge?.percentUsed ?? 4,
|
|
3443
|
+
remainingFormatted: edge?.remainingFormatted ?? "959K"
|
|
3444
|
+
},
|
|
3445
|
+
fastDataTransfer: {
|
|
3446
|
+
usedGb: (bw?.used ?? 154) / 1024,
|
|
3447
|
+
limitGb: (bw?.limit ?? 102400) / 1024,
|
|
3448
|
+
usedFormatted: bw?.usedFormatted ?? "154 MB",
|
|
3449
|
+
limitFormatted: bw?.limitFormatted ?? "100 GB",
|
|
3450
|
+
percent: bw?.percentUsed ?? 0,
|
|
3451
|
+
remainingFormatted: bw?.remainingFormatted ?? "100 GB"
|
|
3452
|
+
},
|
|
3453
|
+
fastOriginTransfer: {
|
|
3454
|
+
usedMb: origin?.used ?? 114,
|
|
3455
|
+
limitMb: origin?.limit ?? 10240,
|
|
3456
|
+
usedFormatted: origin?.usedFormatted ?? "114 MB",
|
|
3457
|
+
limitFormatted: "10 GB",
|
|
3458
|
+
percent: origin?.percentUsed ?? 1
|
|
3459
|
+
},
|
|
3460
|
+
edgeRequestCpuDuration: {
|
|
3461
|
+
usedSeconds: edgeCpu?.used ?? 6,
|
|
3462
|
+
limitSeconds: edgeCpu?.limit ?? 3600,
|
|
3463
|
+
usedFormatted: edgeCpu?.usedFormatted ?? "6s",
|
|
3464
|
+
limitFormatted: "1h",
|
|
3465
|
+
percent: edgeCpu?.percentUsed ?? 0
|
|
3466
|
+
},
|
|
3467
|
+
deploymentStorage: {
|
|
3468
|
+
usedMb: dStorage?.used ?? 365,
|
|
3469
|
+
limitMb: dStorage?.limit ?? 10240,
|
|
3470
|
+
usedFormatted: dStorage?.usedFormatted ?? "365 MB",
|
|
3471
|
+
limitFormatted: "10 GB",
|
|
3472
|
+
percent: dStorage?.percentUsed ?? 4
|
|
3473
|
+
},
|
|
3474
|
+
functionsStorage: {
|
|
3475
|
+
usedMb: fStorage?.used ?? 254,
|
|
3476
|
+
limitMb: fStorage?.limit ?? 10240,
|
|
3477
|
+
usedFormatted: fStorage?.usedFormatted ?? "254 MB",
|
|
3478
|
+
limitFormatted: "10 GB",
|
|
3479
|
+
percent: fStorage?.percentUsed ?? 2
|
|
3480
|
+
}
|
|
3481
|
+
};
|
|
3482
|
+
return {
|
|
3483
|
+
success: true,
|
|
3484
|
+
user: { id: user.id, username: user.username || user.slug || user.name, email: user.email, name: user.name },
|
|
3485
|
+
tier,
|
|
3486
|
+
isHobby: tier === "hobby",
|
|
3487
|
+
projectsCount: projectsList.length,
|
|
3488
|
+
projects: projectsList,
|
|
3489
|
+
projectObservability,
|
|
3490
|
+
usageMetrics,
|
|
3491
|
+
rawUsagePayload: rawUsage
|
|
3492
|
+
};
|
|
3493
|
+
} catch (err) {
|
|
3494
|
+
return { success: false, error: err.message || "Failed to communicate with Vercel API.", projects: [], projectObservability: [] };
|
|
3495
|
+
}
|
|
1317
3496
|
}
|
|
1318
3497
|
|
|
1319
3498
|
// src/server/wavHelper.ts
|
|
@@ -1350,8 +3529,8 @@ function createWavBuffer(sampleRate, numChannels, samples) {
|
|
|
1350
3529
|
}
|
|
1351
3530
|
|
|
1352
3531
|
// src/server/sampleGenerator.ts
|
|
1353
|
-
import
|
|
1354
|
-
import
|
|
3532
|
+
import fs3 from "fs";
|
|
3533
|
+
import path3 from "path";
|
|
1355
3534
|
var GLOBAL_SAMPLE_DEFS = [
|
|
1356
3535
|
{
|
|
1357
3536
|
filename: "808_sub_boom.wav",
|
|
@@ -1448,13 +3627,13 @@ var GLOBAL_SAMPLE_DEFS = [
|
|
|
1448
3627
|
}
|
|
1449
3628
|
];
|
|
1450
3629
|
async function ensureGlobalSamples(globalSamplesDir) {
|
|
1451
|
-
if (!
|
|
1452
|
-
|
|
3630
|
+
if (!fs3.existsSync(globalSamplesDir)) {
|
|
3631
|
+
fs3.mkdirSync(globalSamplesDir, { recursive: true });
|
|
1453
3632
|
}
|
|
1454
3633
|
const sampleRate = 44100;
|
|
1455
3634
|
for (const def of GLOBAL_SAMPLE_DEFS) {
|
|
1456
|
-
const filePath =
|
|
1457
|
-
if (!
|
|
3635
|
+
const filePath = path3.join(globalSamplesDir, def.filename);
|
|
3636
|
+
if (!fs3.existsSync(filePath)) {
|
|
1458
3637
|
const numSamples = Math.floor(sampleRate * def.durationSec);
|
|
1459
3638
|
const chL = new Float32Array(numSamples);
|
|
1460
3639
|
const chR = new Float32Array(numSamples);
|
|
@@ -1466,16 +3645,16 @@ async function ensureGlobalSamples(globalSamplesDir) {
|
|
|
1466
3645
|
chR[i] = r;
|
|
1467
3646
|
}
|
|
1468
3647
|
const wavBuf = createWavBuffer(sampleRate, 2, [chL, chR]);
|
|
1469
|
-
await
|
|
3648
|
+
await fs3.promises.writeFile(filePath, wavBuf);
|
|
1470
3649
|
}
|
|
1471
3650
|
}
|
|
1472
3651
|
}
|
|
1473
3652
|
|
|
1474
3653
|
// src/server/services/assetPoolService.ts
|
|
1475
|
-
import
|
|
1476
|
-
import
|
|
1477
|
-
var WORKSPACE_ASSETS_ROOT =
|
|
1478
|
-
var PROJECTS_ROOT =
|
|
3654
|
+
import fs4 from "fs";
|
|
3655
|
+
import path4 from "path";
|
|
3656
|
+
var WORKSPACE_ASSETS_ROOT = path4.join(process.cwd(), "assets");
|
|
3657
|
+
var PROJECTS_ROOT = path4.join(process.cwd(), "projects");
|
|
1479
3658
|
var SUPPORTED_AUDIO_EXTS = [
|
|
1480
3659
|
".wav",
|
|
1481
3660
|
".mp3",
|
|
@@ -1493,27 +3672,27 @@ var SUPPORTED_AUDIO_EXTS = [
|
|
|
1493
3672
|
var SUPPORTED_MIDI_EXTS = [".mid", ".midi", ".json"];
|
|
1494
3673
|
async function ensureAssetPoolDirectories(rootDir) {
|
|
1495
3674
|
const dirs = [
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
3675
|
+
path4.join(rootDir, "audio", "stereo", "default"),
|
|
3676
|
+
path4.join(rootDir, "audio", "stereo", "userdefined"),
|
|
3677
|
+
path4.join(rootDir, "audio", "mono", "default"),
|
|
3678
|
+
path4.join(rootDir, "audio", "mono", "userdefined"),
|
|
3679
|
+
path4.join(rootDir, "midi", "default"),
|
|
3680
|
+
path4.join(rootDir, "midi", "userdefined")
|
|
1502
3681
|
];
|
|
1503
3682
|
for (const dir of dirs) {
|
|
1504
|
-
if (!
|
|
1505
|
-
await
|
|
3683
|
+
if (!fs4.existsSync(dir)) {
|
|
3684
|
+
await fs4.promises.mkdir(dir, { recursive: true });
|
|
1506
3685
|
}
|
|
1507
3686
|
}
|
|
1508
3687
|
}
|
|
1509
3688
|
async function seedDefaultAssets() {
|
|
1510
3689
|
await ensureAssetPoolDirectories(WORKSPACE_ASSETS_ROOT);
|
|
1511
|
-
const stereoDefaultDir =
|
|
1512
|
-
const monoDefaultDir =
|
|
1513
|
-
const midiDefaultDir =
|
|
3690
|
+
const stereoDefaultDir = path4.join(WORKSPACE_ASSETS_ROOT, "audio", "stereo", "default");
|
|
3691
|
+
const monoDefaultDir = path4.join(WORKSPACE_ASSETS_ROOT, "audio", "mono", "default");
|
|
3692
|
+
const midiDefaultDir = path4.join(WORKSPACE_ASSETS_ROOT, "midi", "default");
|
|
1514
3693
|
const sampleRate = 44100;
|
|
1515
|
-
const subBoomPath =
|
|
1516
|
-
if (!
|
|
3694
|
+
const subBoomPath = path4.join(stereoDefaultDir, "808_sub_boom.wav");
|
|
3695
|
+
if (!fs4.existsSync(subBoomPath)) {
|
|
1517
3696
|
const numSamples = Math.floor(sampleRate * 1.8);
|
|
1518
3697
|
const chL = new Float32Array(numSamples);
|
|
1519
3698
|
const chR = new Float32Array(numSamples);
|
|
@@ -1524,10 +3703,10 @@ async function seedDefaultAssets() {
|
|
|
1524
3703
|
chL[i] = s;
|
|
1525
3704
|
chR[i] = s;
|
|
1526
3705
|
}
|
|
1527
|
-
await
|
|
3706
|
+
await fs4.promises.writeFile(subBoomPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
1528
3707
|
}
|
|
1529
|
-
const leadPath =
|
|
1530
|
-
if (!
|
|
3708
|
+
const leadPath = path4.join(stereoDefaultDir, "celestial_lead.wav");
|
|
3709
|
+
if (!fs4.existsSync(leadPath)) {
|
|
1531
3710
|
const numSamples = Math.floor(sampleRate * 2.5);
|
|
1532
3711
|
const chL = new Float32Array(numSamples);
|
|
1533
3712
|
const chR = new Float32Array(numSamples);
|
|
@@ -1541,10 +3720,10 @@ async function seedDefaultAssets() {
|
|
|
1541
3720
|
chL[i] = s1 * env;
|
|
1542
3721
|
chR[i] = (s1 + s2 * 0.5) * env;
|
|
1543
3722
|
}
|
|
1544
|
-
await
|
|
3723
|
+
await fs4.promises.writeFile(leadPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
1545
3724
|
}
|
|
1546
|
-
const padPath =
|
|
1547
|
-
if (!
|
|
3725
|
+
const padPath = path4.join(stereoDefaultDir, "ethereal_pad.wav");
|
|
3726
|
+
if (!fs4.existsSync(padPath)) {
|
|
1548
3727
|
const numSamples = Math.floor(sampleRate * 3);
|
|
1549
3728
|
const chL = new Float32Array(numSamples);
|
|
1550
3729
|
const chR = new Float32Array(numSamples);
|
|
@@ -1563,10 +3742,10 @@ async function seedDefaultAssets() {
|
|
|
1563
3742
|
chL[i] = left * env;
|
|
1564
3743
|
chR[i] = right * env;
|
|
1565
3744
|
}
|
|
1566
|
-
await
|
|
3745
|
+
await fs4.promises.writeFile(padPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
1567
3746
|
}
|
|
1568
|
-
const snarePath =
|
|
1569
|
-
if (!
|
|
3747
|
+
const snarePath = path4.join(monoDefaultDir, "snare_snappy.wav");
|
|
3748
|
+
if (!fs4.existsSync(snarePath)) {
|
|
1570
3749
|
const numSamples = Math.floor(sampleRate * 0.8);
|
|
1571
3750
|
const chM = new Float32Array(numSamples);
|
|
1572
3751
|
for (let i = 0; i < numSamples; i++) {
|
|
@@ -1576,10 +3755,10 @@ async function seedDefaultAssets() {
|
|
|
1576
3755
|
const noise = (Math.random() * 2 - 1) * Math.exp(-p * 4.5) * 0.6;
|
|
1577
3756
|
chM[i] = tone + noise;
|
|
1578
3757
|
}
|
|
1579
|
-
await
|
|
3758
|
+
await fs4.promises.writeFile(snarePath, createWavBuffer(sampleRate, 1, [chM]));
|
|
1580
3759
|
}
|
|
1581
|
-
const hihatPath =
|
|
1582
|
-
if (!
|
|
3760
|
+
const hihatPath = path4.join(monoDefaultDir, "hihat_closed.wav");
|
|
3761
|
+
if (!fs4.existsSync(hihatPath)) {
|
|
1583
3762
|
const numSamples = Math.floor(sampleRate * 0.25);
|
|
1584
3763
|
const chM = new Float32Array(numSamples);
|
|
1585
3764
|
for (let i = 0; i < numSamples; i++) {
|
|
@@ -1587,10 +3766,10 @@ async function seedDefaultAssets() {
|
|
|
1587
3766
|
const noise = (Math.random() * 2 - 1) * Math.exp(-p * 20);
|
|
1588
3767
|
chM[i] = noise * 0.5;
|
|
1589
3768
|
}
|
|
1590
|
-
await
|
|
3769
|
+
await fs4.promises.writeFile(hihatPath, createWavBuffer(sampleRate, 1, [chM]));
|
|
1591
3770
|
}
|
|
1592
|
-
const midiChordPath =
|
|
1593
|
-
if (!
|
|
3771
|
+
const midiChordPath = path4.join(midiDefaultDir, "lydian_celestial_pad.json");
|
|
3772
|
+
if (!fs4.existsSync(midiChordPath)) {
|
|
1594
3773
|
const starterMidiPattern = {
|
|
1595
3774
|
id: "pat-default-lydian",
|
|
1596
3775
|
name: "Lydian Celestial Pad",
|
|
@@ -1603,31 +3782,31 @@ async function seedDefaultAssets() {
|
|
|
1603
3782
|
{ note: 71, name: "B4", velocity: 0.75, time: 0, duration: 3.8 }
|
|
1604
3783
|
]
|
|
1605
3784
|
};
|
|
1606
|
-
await
|
|
3785
|
+
await fs4.promises.writeFile(midiChordPath, JSON.stringify(starterMidiPattern, null, 2), "utf-8");
|
|
1607
3786
|
}
|
|
1608
3787
|
await ensureProjectAssets("default-kingdom");
|
|
1609
3788
|
}
|
|
1610
3789
|
async function ensureProjectAssets(projectId) {
|
|
1611
|
-
const safeId =
|
|
1612
|
-
const projectAssetsDir =
|
|
3790
|
+
const safeId = path4.basename(projectId);
|
|
3791
|
+
const projectAssetsDir = path4.join(PROJECTS_ROOT, safeId, "assets");
|
|
1613
3792
|
await ensureAssetPoolDirectories(projectAssetsDir);
|
|
1614
3793
|
return projectAssetsDir;
|
|
1615
3794
|
}
|
|
1616
3795
|
function getAssetPoolLocationPath(scope, projectId, dataType, channelType, subfolder = "userdefined") {
|
|
1617
|
-
const root = scope === "project" && projectId ?
|
|
1618
|
-
const safeSub =
|
|
3796
|
+
const root = scope === "project" && projectId ? path4.join(PROJECTS_ROOT, path4.basename(projectId), "assets") : WORKSPACE_ASSETS_ROOT;
|
|
3797
|
+
const safeSub = path4.basename(subfolder).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1619
3798
|
if (dataType === "audio") {
|
|
1620
3799
|
const safeChannel = channelType === "mono" ? "mono" : "stereo";
|
|
1621
|
-
return
|
|
3800
|
+
return path4.join(root, "audio", safeChannel, safeSub);
|
|
1622
3801
|
} else {
|
|
1623
|
-
return
|
|
3802
|
+
return path4.join(root, "midi", safeSub);
|
|
1624
3803
|
}
|
|
1625
3804
|
}
|
|
1626
3805
|
function getAssetPoolPublicUrl(scope, projectId, dataType, channelType, subfolder, filename) {
|
|
1627
|
-
const safeFilename =
|
|
1628
|
-
const safeSub =
|
|
3806
|
+
const safeFilename = path4.basename(filename);
|
|
3807
|
+
const safeSub = path4.basename(subfolder);
|
|
1629
3808
|
if (scope === "project" && projectId) {
|
|
1630
|
-
const safeProj =
|
|
3809
|
+
const safeProj = path4.basename(projectId);
|
|
1631
3810
|
if (dataType === "audio") {
|
|
1632
3811
|
const channel = channelType === "mono" ? "mono" : "stereo";
|
|
1633
3812
|
return `/projects/${safeProj}/assets/audio/${channel}/${safeSub}/${safeFilename}`;
|
|
@@ -1649,31 +3828,31 @@ async function scanAssetPool(scope, projectId) {
|
|
|
1649
3828
|
} else {
|
|
1650
3829
|
await ensureAssetPoolDirectories(WORKSPACE_ASSETS_ROOT);
|
|
1651
3830
|
}
|
|
1652
|
-
const rootDir = scope === "project" && projectId ?
|
|
3831
|
+
const rootDir = scope === "project" && projectId ? path4.join(PROJECTS_ROOT, path4.basename(projectId), "assets") : WORKSPACE_ASSETS_ROOT;
|
|
1653
3832
|
const folders = [];
|
|
1654
3833
|
const items = [];
|
|
1655
3834
|
async function scanCategoryFolder(dataType, channelType) {
|
|
1656
|
-
const catDir = dataType === "audio" ?
|
|
1657
|
-
if (!
|
|
1658
|
-
await
|
|
3835
|
+
const catDir = dataType === "audio" ? path4.join(rootDir, "audio", channelType === "mono" ? "mono" : "stereo") : path4.join(rootDir, "midi");
|
|
3836
|
+
if (!fs4.existsSync(catDir)) {
|
|
3837
|
+
await fs4.promises.mkdir(catDir, { recursive: true });
|
|
1659
3838
|
}
|
|
1660
|
-
const subentries = await
|
|
3839
|
+
const subentries = await fs4.promises.readdir(catDir, { withFileTypes: true });
|
|
1661
3840
|
for (const entry of subentries) {
|
|
1662
3841
|
if (!entry.isDirectory()) continue;
|
|
1663
3842
|
const subfolderName = entry.name;
|
|
1664
|
-
const subfolderDir =
|
|
1665
|
-
const files = await
|
|
3843
|
+
const subfolderDir = path4.join(catDir, subfolderName);
|
|
3844
|
+
const files = await fs4.promises.readdir(subfolderDir);
|
|
1666
3845
|
let validFileCount = 0;
|
|
1667
3846
|
for (const file of files) {
|
|
1668
3847
|
if (file.startsWith(".")) continue;
|
|
1669
|
-
const ext =
|
|
3848
|
+
const ext = path4.extname(file).toLowerCase();
|
|
1670
3849
|
const isAudio = SUPPORTED_AUDIO_EXTS.includes(ext);
|
|
1671
3850
|
const isMidi = SUPPORTED_MIDI_EXTS.includes(ext);
|
|
1672
3851
|
if (dataType === "audio" && !isAudio || dataType === "midi" && !isMidi) continue;
|
|
1673
3852
|
validFileCount++;
|
|
1674
|
-
const filePath =
|
|
1675
|
-
const stat = await
|
|
1676
|
-
const cleanName =
|
|
3853
|
+
const filePath = path4.join(subfolderDir, file);
|
|
3854
|
+
const stat = await fs4.promises.stat(filePath);
|
|
3855
|
+
const cleanName = path4.basename(file, ext).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1677
3856
|
const relPath = dataType === "audio" ? `audio/${channelType === "mono" ? "mono" : "stereo"}/${subfolderName}/${file}` : `midi/${subfolderName}/${file}`;
|
|
1678
3857
|
const url = getAssetPoolPublicUrl(
|
|
1679
3858
|
scope,
|
|
@@ -1714,7 +3893,7 @@ async function scanAssetPool(scope, projectId) {
|
|
|
1714
3893
|
return { folders, items };
|
|
1715
3894
|
}
|
|
1716
3895
|
async function createAssetSubfolder(params) {
|
|
1717
|
-
const safeName =
|
|
3896
|
+
const safeName = path4.basename(params.folderName).trim().replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1718
3897
|
if (!safeName) throw new Error("Invalid subfolder name");
|
|
1719
3898
|
const dirPath = getAssetPoolLocationPath(
|
|
1720
3899
|
params.scope,
|
|
@@ -1723,8 +3902,8 @@ async function createAssetSubfolder(params) {
|
|
|
1723
3902
|
params.channelType,
|
|
1724
3903
|
safeName
|
|
1725
3904
|
);
|
|
1726
|
-
if (!
|
|
1727
|
-
await
|
|
3905
|
+
if (!fs4.existsSync(dirPath)) {
|
|
3906
|
+
await fs4.promises.mkdir(dirPath, { recursive: true });
|
|
1728
3907
|
}
|
|
1729
3908
|
return {
|
|
1730
3909
|
name: safeName,
|
|
@@ -1743,16 +3922,16 @@ async function saveAssetToPool(params) {
|
|
|
1743
3922
|
params.channelType,
|
|
1744
3923
|
subfolder
|
|
1745
3924
|
);
|
|
1746
|
-
if (!
|
|
1747
|
-
await
|
|
3925
|
+
if (!fs4.existsSync(targetDir)) {
|
|
3926
|
+
await fs4.promises.mkdir(targetDir, { recursive: true });
|
|
1748
3927
|
}
|
|
1749
|
-
const safeFilename =
|
|
1750
|
-
const ext =
|
|
1751
|
-
const filePath =
|
|
3928
|
+
const safeFilename = path4.basename(params.filename).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3929
|
+
const ext = path4.extname(safeFilename).toLowerCase() || (params.dataType === "audio" ? ".wav" : ".json");
|
|
3930
|
+
const filePath = path4.join(targetDir, safeFilename);
|
|
1752
3931
|
const buffer = Buffer.from(params.base64Content, "base64");
|
|
1753
|
-
await
|
|
1754
|
-
const stat = await
|
|
1755
|
-
const cleanName =
|
|
3932
|
+
await fs4.promises.writeFile(filePath, buffer);
|
|
3933
|
+
const stat = await fs4.promises.stat(filePath);
|
|
3934
|
+
const cleanName = path4.basename(safeFilename, ext).replace(/[-_]/g, " ");
|
|
1756
3935
|
const relPath = params.dataType === "audio" ? `audio/${params.channelType === "mono" ? "mono" : "stereo"}/${subfolder}/${safeFilename}` : `midi/${subfolder}/${safeFilename}`;
|
|
1757
3936
|
const url = getAssetPoolPublicUrl(
|
|
1758
3937
|
params.scope,
|
|
@@ -1786,19 +3965,19 @@ async function deleteAssetFromPool(params) {
|
|
|
1786
3965
|
params.channelType,
|
|
1787
3966
|
params.subfolder
|
|
1788
3967
|
);
|
|
1789
|
-
const filePath =
|
|
1790
|
-
if (
|
|
1791
|
-
await
|
|
3968
|
+
const filePath = path4.join(targetDir, path4.basename(params.filename));
|
|
3969
|
+
if (fs4.existsSync(filePath)) {
|
|
3970
|
+
await fs4.promises.unlink(filePath);
|
|
1792
3971
|
return true;
|
|
1793
3972
|
}
|
|
1794
3973
|
return false;
|
|
1795
3974
|
}
|
|
1796
3975
|
|
|
1797
3976
|
// src/server/services/graphAssetService.ts
|
|
1798
|
-
import
|
|
1799
|
-
import
|
|
1800
|
-
var WORKSPACE_ROOT =
|
|
1801
|
-
var GRAPHS_ROOT =
|
|
3977
|
+
import fs5 from "fs";
|
|
3978
|
+
import path5 from "path";
|
|
3979
|
+
var WORKSPACE_ROOT = path5.join(process.cwd(), "workspaces", "default");
|
|
3980
|
+
var GRAPHS_ROOT = path5.join(WORKSPACE_ROOT, "graphs");
|
|
1802
3981
|
var SUPPORTED_AUDIO_EXTS2 = [
|
|
1803
3982
|
".wav",
|
|
1804
3983
|
".mp3",
|
|
@@ -1816,26 +3995,26 @@ var SUPPORTED_AUDIO_EXTS2 = [
|
|
|
1816
3995
|
var SUPPORTED_MIDI_EXTS2 = [".mid", ".midi", ".json"];
|
|
1817
3996
|
function ensureGraphAssetDirectories(graphDir) {
|
|
1818
3997
|
const dirs = [
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
3998
|
+
path5.join(graphDir, "assets", "audio", "stereo", "default"),
|
|
3999
|
+
path5.join(graphDir, "assets", "audio", "stereo", "userdefined"),
|
|
4000
|
+
path5.join(graphDir, "assets", "audio", "mono", "default"),
|
|
4001
|
+
path5.join(graphDir, "assets", "audio", "mono", "userdefined"),
|
|
4002
|
+
path5.join(graphDir, "assets", "midi", "default"),
|
|
4003
|
+
path5.join(graphDir, "assets", "midi", "userdefined")
|
|
1825
4004
|
];
|
|
1826
4005
|
for (const dir of dirs) {
|
|
1827
|
-
if (!
|
|
1828
|
-
|
|
4006
|
+
if (!fs5.existsSync(dir)) {
|
|
4007
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1829
4008
|
}
|
|
1830
4009
|
}
|
|
1831
4010
|
}
|
|
1832
4011
|
function seedStarterGraphAssets(graphDir) {
|
|
1833
|
-
const stereoDefaultDir =
|
|
1834
|
-
const monoDefaultDir =
|
|
1835
|
-
const midiDefaultDir =
|
|
4012
|
+
const stereoDefaultDir = path5.join(graphDir, "assets", "audio", "stereo", "default");
|
|
4013
|
+
const monoDefaultDir = path5.join(graphDir, "assets", "audio", "mono", "default");
|
|
4014
|
+
const midiDefaultDir = path5.join(graphDir, "assets", "midi", "default");
|
|
1836
4015
|
const sampleRate = 44100;
|
|
1837
|
-
const subBoomPath =
|
|
1838
|
-
if (!
|
|
4016
|
+
const subBoomPath = path5.join(stereoDefaultDir, "808_sub_boom.wav");
|
|
4017
|
+
if (!fs5.existsSync(subBoomPath)) {
|
|
1839
4018
|
const numSamples = Math.floor(sampleRate * 1.8);
|
|
1840
4019
|
const chL = new Float32Array(numSamples);
|
|
1841
4020
|
const chR = new Float32Array(numSamples);
|
|
@@ -1846,10 +4025,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
1846
4025
|
chL[i] = s;
|
|
1847
4026
|
chR[i] = s;
|
|
1848
4027
|
}
|
|
1849
|
-
|
|
4028
|
+
fs5.writeFileSync(subBoomPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
1850
4029
|
}
|
|
1851
|
-
const leadPath =
|
|
1852
|
-
if (!
|
|
4030
|
+
const leadPath = path5.join(stereoDefaultDir, "celestial_lead.wav");
|
|
4031
|
+
if (!fs5.existsSync(leadPath)) {
|
|
1853
4032
|
const numSamples = Math.floor(sampleRate * 2.5);
|
|
1854
4033
|
const chL = new Float32Array(numSamples);
|
|
1855
4034
|
const chR = new Float32Array(numSamples);
|
|
@@ -1863,10 +4042,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
1863
4042
|
chL[i] = s1 * env;
|
|
1864
4043
|
chR[i] = (s1 + s2 * 0.5) * env;
|
|
1865
4044
|
}
|
|
1866
|
-
|
|
4045
|
+
fs5.writeFileSync(leadPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
1867
4046
|
}
|
|
1868
|
-
const padPath =
|
|
1869
|
-
if (!
|
|
4047
|
+
const padPath = path5.join(stereoDefaultDir, "ethereal_pad.wav");
|
|
4048
|
+
if (!fs5.existsSync(padPath)) {
|
|
1870
4049
|
const numSamples = Math.floor(sampleRate * 3);
|
|
1871
4050
|
const chL = new Float32Array(numSamples);
|
|
1872
4051
|
const chR = new Float32Array(numSamples);
|
|
@@ -1885,10 +4064,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
1885
4064
|
chL[i] = left * env;
|
|
1886
4065
|
chR[i] = right * env;
|
|
1887
4066
|
}
|
|
1888
|
-
|
|
4067
|
+
fs5.writeFileSync(padPath, createWavBuffer(sampleRate, 2, [chL, chR]));
|
|
1889
4068
|
}
|
|
1890
|
-
const snarePath =
|
|
1891
|
-
if (!
|
|
4069
|
+
const snarePath = path5.join(monoDefaultDir, "snare_snappy.wav");
|
|
4070
|
+
if (!fs5.existsSync(snarePath)) {
|
|
1892
4071
|
const numSamples = Math.floor(sampleRate * 0.8);
|
|
1893
4072
|
const chM = new Float32Array(numSamples);
|
|
1894
4073
|
for (let i = 0; i < numSamples; i++) {
|
|
@@ -1898,10 +4077,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
1898
4077
|
const noise = (Math.random() * 2 - 1) * Math.exp(-p * 4.5) * 0.6;
|
|
1899
4078
|
chM[i] = tone + noise;
|
|
1900
4079
|
}
|
|
1901
|
-
|
|
4080
|
+
fs5.writeFileSync(snarePath, createWavBuffer(sampleRate, 1, [chM]));
|
|
1902
4081
|
}
|
|
1903
|
-
const hihatPath =
|
|
1904
|
-
if (!
|
|
4082
|
+
const hihatPath = path5.join(monoDefaultDir, "hihat_closed.wav");
|
|
4083
|
+
if (!fs5.existsSync(hihatPath)) {
|
|
1905
4084
|
const numSamples = Math.floor(sampleRate * 0.25);
|
|
1906
4085
|
const chM = new Float32Array(numSamples);
|
|
1907
4086
|
for (let i = 0; i < numSamples; i++) {
|
|
@@ -1909,10 +4088,10 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
1909
4088
|
const noise = (Math.random() * 2 - 1) * Math.exp(-p * 20);
|
|
1910
4089
|
chM[i] = noise * 0.5;
|
|
1911
4090
|
}
|
|
1912
|
-
|
|
4091
|
+
fs5.writeFileSync(hihatPath, createWavBuffer(sampleRate, 1, [chM]));
|
|
1913
4092
|
}
|
|
1914
|
-
const midiChordPath =
|
|
1915
|
-
if (!
|
|
4093
|
+
const midiChordPath = path5.join(midiDefaultDir, "lydian_celestial_pad.json");
|
|
4094
|
+
if (!fs5.existsSync(midiChordPath)) {
|
|
1916
4095
|
const starterMidiPattern = {
|
|
1917
4096
|
id: "pat-default-lydian",
|
|
1918
4097
|
name: "Lydian Celestial Pad",
|
|
@@ -1925,43 +4104,43 @@ function seedStarterGraphAssets(graphDir) {
|
|
|
1925
4104
|
{ note: 71, name: "B4", velocity: 0.75, time: 0, duration: 3.8 }
|
|
1926
4105
|
]
|
|
1927
4106
|
};
|
|
1928
|
-
|
|
4107
|
+
fs5.writeFileSync(midiChordPath, JSON.stringify(starterMidiPattern, null, 2), "utf-8");
|
|
1929
4108
|
}
|
|
1930
4109
|
}
|
|
1931
4110
|
async function scanGraphAssets(graphId, includeImported = true) {
|
|
1932
|
-
const safeGraphId =
|
|
1933
|
-
const graphDir =
|
|
1934
|
-
if (!
|
|
1935
|
-
await
|
|
4111
|
+
const safeGraphId = path5.basename(graphId);
|
|
4112
|
+
const graphDir = path5.join(GRAPHS_ROOT, safeGraphId);
|
|
4113
|
+
if (!fs5.existsSync(graphDir)) {
|
|
4114
|
+
await fs5.promises.mkdir(graphDir, { recursive: true });
|
|
1936
4115
|
}
|
|
1937
4116
|
ensureGraphAssetDirectories(graphDir);
|
|
1938
4117
|
const folders = [];
|
|
1939
4118
|
const items = [];
|
|
1940
4119
|
const importedItems = [];
|
|
1941
4120
|
async function scanSingleGraph(gId, isImported, ownerName) {
|
|
1942
|
-
const currentGraphDir =
|
|
1943
|
-
const assetsDir =
|
|
1944
|
-
if (!
|
|
4121
|
+
const currentGraphDir = path5.join(GRAPHS_ROOT, path5.basename(gId));
|
|
4122
|
+
const assetsDir = path5.join(currentGraphDir, "assets");
|
|
4123
|
+
if (!fs5.existsSync(assetsDir)) return;
|
|
1945
4124
|
async function scanCategoryFolder(dataType, channelType) {
|
|
1946
|
-
const catDir = dataType === "audio" ?
|
|
1947
|
-
if (!
|
|
1948
|
-
const subentries = await
|
|
4125
|
+
const catDir = dataType === "audio" ? path5.join(assetsDir, "audio", channelType === "mono" ? "mono" : "stereo") : path5.join(assetsDir, "midi");
|
|
4126
|
+
if (!fs5.existsSync(catDir)) return;
|
|
4127
|
+
const subentries = await fs5.promises.readdir(catDir, { withFileTypes: true });
|
|
1949
4128
|
for (const entry of subentries) {
|
|
1950
4129
|
if (!entry.isDirectory()) continue;
|
|
1951
4130
|
const subfolderName = entry.name;
|
|
1952
|
-
const subfolderDir =
|
|
1953
|
-
const files = await
|
|
4131
|
+
const subfolderDir = path5.join(catDir, subfolderName);
|
|
4132
|
+
const files = await fs5.promises.readdir(subfolderDir);
|
|
1954
4133
|
let validFileCount = 0;
|
|
1955
4134
|
for (const file of files) {
|
|
1956
4135
|
if (file.startsWith(".")) continue;
|
|
1957
|
-
const ext =
|
|
4136
|
+
const ext = path5.extname(file).toLowerCase();
|
|
1958
4137
|
const isAudio = SUPPORTED_AUDIO_EXTS2.includes(ext);
|
|
1959
4138
|
const isMidi = SUPPORTED_MIDI_EXTS2.includes(ext);
|
|
1960
4139
|
if (dataType === "audio" && !isAudio || dataType === "midi" && !isMidi) continue;
|
|
1961
4140
|
validFileCount++;
|
|
1962
|
-
const filePath =
|
|
1963
|
-
const stat = await
|
|
1964
|
-
const cleanName =
|
|
4141
|
+
const filePath = path5.join(subfolderDir, file);
|
|
4142
|
+
const stat = await fs5.promises.stat(filePath);
|
|
4143
|
+
const cleanName = path5.basename(file, ext).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1965
4144
|
const relPath = dataType === "audio" ? `audio/${channelType === "mono" ? "mono" : "stereo"}/${subfolderName}/${file}` : `midi/${subfolderName}/${file}`;
|
|
1966
4145
|
const url = dataType === "audio" ? `/graphs/${gId}/assets/audio/${channelType === "mono" ? "mono" : "stereo"}/${subfolderName}/${file}` : `/graphs/${gId}/assets/midi/${subfolderName}/${file}`;
|
|
1967
4146
|
const item = {
|
|
@@ -2004,18 +4183,18 @@ async function scanGraphAssets(graphId, includeImported = true) {
|
|
|
2004
4183
|
}
|
|
2005
4184
|
await scanSingleGraph(safeGraphId, false);
|
|
2006
4185
|
if (includeImported) {
|
|
2007
|
-
const qmsFile =
|
|
2008
|
-
if (
|
|
4186
|
+
const qmsFile = path5.join(graphDir, "graph.qms");
|
|
4187
|
+
if (fs5.existsSync(qmsFile)) {
|
|
2009
4188
|
try {
|
|
2010
|
-
const raw = await
|
|
4189
|
+
const raw = await fs5.promises.readFile(qmsFile, "utf-8");
|
|
2011
4190
|
const pkg = JSON.parse(raw);
|
|
2012
4191
|
const importedIds = pkg?.metadata?.importedGraphIds || [];
|
|
2013
4192
|
for (const impId of importedIds) {
|
|
2014
|
-
const impQms =
|
|
4193
|
+
const impQms = path5.join(GRAPHS_ROOT, impId, "graph.qms");
|
|
2015
4194
|
let impName = impId;
|
|
2016
|
-
if (
|
|
4195
|
+
if (fs5.existsSync(impQms)) {
|
|
2017
4196
|
try {
|
|
2018
|
-
const impRaw = await
|
|
4197
|
+
const impRaw = await fs5.promises.readFile(impQms, "utf-8");
|
|
2019
4198
|
const impPkg = JSON.parse(impRaw);
|
|
2020
4199
|
impName = impPkg?.metadata?.name || impId;
|
|
2021
4200
|
} catch {
|
|
@@ -2031,14 +4210,14 @@ async function scanGraphAssets(graphId, includeImported = true) {
|
|
|
2031
4210
|
return { folders, items, importedItems };
|
|
2032
4211
|
}
|
|
2033
4212
|
async function createGraphAssetFolder(params) {
|
|
2034
|
-
const safeGraphId =
|
|
2035
|
-
const safeFolderName =
|
|
4213
|
+
const safeGraphId = path5.basename(params.graphId);
|
|
4214
|
+
const safeFolderName = path5.basename(params.folderName).trim().replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2036
4215
|
if (!safeFolderName) throw new Error("Invalid subfolder name");
|
|
2037
|
-
const graphDir =
|
|
4216
|
+
const graphDir = path5.join(GRAPHS_ROOT, safeGraphId);
|
|
2038
4217
|
ensureGraphAssetDirectories(graphDir);
|
|
2039
|
-
const targetDir = params.dataType === "audio" ?
|
|
2040
|
-
if (!
|
|
2041
|
-
await
|
|
4218
|
+
const targetDir = params.dataType === "audio" ? path5.join(graphDir, "assets", "audio", params.channelType === "mono" ? "mono" : "stereo", safeFolderName) : path5.join(graphDir, "assets", "midi", safeFolderName);
|
|
4219
|
+
if (!fs5.existsSync(targetDir)) {
|
|
4220
|
+
await fs5.promises.mkdir(targetDir, { recursive: true });
|
|
2042
4221
|
}
|
|
2043
4222
|
return {
|
|
2044
4223
|
name: safeFolderName,
|
|
@@ -2050,21 +4229,21 @@ async function createGraphAssetFolder(params) {
|
|
|
2050
4229
|
};
|
|
2051
4230
|
}
|
|
2052
4231
|
async function saveAssetToGraph(params) {
|
|
2053
|
-
const safeGraphId =
|
|
4232
|
+
const safeGraphId = path5.basename(params.graphId);
|
|
2054
4233
|
const subfolder = params.subfolder || "userdefined";
|
|
2055
|
-
const graphDir =
|
|
4234
|
+
const graphDir = path5.join(GRAPHS_ROOT, safeGraphId);
|
|
2056
4235
|
ensureGraphAssetDirectories(graphDir);
|
|
2057
|
-
const targetDir = params.dataType === "audio" ?
|
|
2058
|
-
if (!
|
|
2059
|
-
await
|
|
4236
|
+
const targetDir = params.dataType === "audio" ? path5.join(graphDir, "assets", "audio", params.channelType === "mono" ? "mono" : "stereo", subfolder) : path5.join(graphDir, "assets", "midi", subfolder);
|
|
4237
|
+
if (!fs5.existsSync(targetDir)) {
|
|
4238
|
+
await fs5.promises.mkdir(targetDir, { recursive: true });
|
|
2060
4239
|
}
|
|
2061
|
-
const safeFilename =
|
|
2062
|
-
const ext =
|
|
2063
|
-
const filePath =
|
|
4240
|
+
const safeFilename = path5.basename(params.filename).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
4241
|
+
const ext = path5.extname(safeFilename).toLowerCase() || (params.dataType === "audio" ? ".wav" : ".json");
|
|
4242
|
+
const filePath = path5.join(targetDir, safeFilename);
|
|
2064
4243
|
const buffer = Buffer.from(params.base64Content, "base64");
|
|
2065
|
-
await
|
|
2066
|
-
const stat = await
|
|
2067
|
-
const cleanName =
|
|
4244
|
+
await fs5.promises.writeFile(filePath, buffer);
|
|
4245
|
+
const stat = await fs5.promises.stat(filePath);
|
|
4246
|
+
const cleanName = path5.basename(safeFilename, ext).replace(/[-_]/g, " ");
|
|
2068
4247
|
const relPath = params.dataType === "audio" ? `audio/${params.channelType === "mono" ? "mono" : "stereo"}/${subfolder}/${safeFilename}` : `midi/${subfolder}/${safeFilename}`;
|
|
2069
4248
|
const url = params.dataType === "audio" ? `/graphs/${safeGraphId}/assets/audio/${params.channelType === "mono" ? "mono" : "stereo"}/${subfolder}/${safeFilename}` : `/graphs/${safeGraphId}/assets/midi/${subfolder}/${safeFilename}`;
|
|
2070
4249
|
return {
|
|
@@ -2083,32 +4262,32 @@ async function saveAssetToGraph(params) {
|
|
|
2083
4262
|
};
|
|
2084
4263
|
}
|
|
2085
4264
|
async function deleteAssetFromGraph(params) {
|
|
2086
|
-
const safeGraphId =
|
|
2087
|
-
const graphDir =
|
|
2088
|
-
const targetDir = params.dataType === "audio" ?
|
|
2089
|
-
const filePath =
|
|
2090
|
-
if (
|
|
2091
|
-
await
|
|
4265
|
+
const safeGraphId = path5.basename(params.graphId);
|
|
4266
|
+
const graphDir = path5.join(GRAPHS_ROOT, safeGraphId);
|
|
4267
|
+
const targetDir = params.dataType === "audio" ? path5.join(graphDir, "assets", "audio", params.channelType === "mono" ? "mono" : "stereo", params.subfolder) : path5.join(graphDir, "assets", "midi", params.subfolder);
|
|
4268
|
+
const filePath = path5.join(targetDir, path5.basename(params.filename));
|
|
4269
|
+
if (fs5.existsSync(filePath)) {
|
|
4270
|
+
await fs5.promises.unlink(filePath);
|
|
2092
4271
|
return true;
|
|
2093
4272
|
}
|
|
2094
4273
|
return false;
|
|
2095
4274
|
}
|
|
2096
4275
|
|
|
2097
4276
|
// src/server/services/graphPackageService.ts
|
|
2098
|
-
import
|
|
2099
|
-
import
|
|
4277
|
+
import fs6 from "fs";
|
|
4278
|
+
import path6 from "path";
|
|
2100
4279
|
import { validateGraphImport } from "@ultrst-armada/kloks/state-graph";
|
|
2101
|
-
var WORKSPACE_ROOT2 =
|
|
2102
|
-
var GRAPHS_ROOT2 =
|
|
2103
|
-
var LEGACY_PROJECTS_ROOT =
|
|
4280
|
+
var WORKSPACE_ROOT2 = path6.join(process.cwd(), "workspaces", "default");
|
|
4281
|
+
var GRAPHS_ROOT2 = path6.join(WORKSPACE_ROOT2, "graphs");
|
|
4282
|
+
var LEGACY_PROJECTS_ROOT = path6.join(process.cwd(), "projects");
|
|
2104
4283
|
function ensureWorkspaceStructure() {
|
|
2105
|
-
if (!
|
|
2106
|
-
|
|
4284
|
+
if (!fs6.existsSync(GRAPHS_ROOT2)) {
|
|
4285
|
+
fs6.mkdirSync(GRAPHS_ROOT2, { recursive: true });
|
|
2107
4286
|
}
|
|
2108
4287
|
seedStarterGraphsIfEmpty();
|
|
2109
4288
|
}
|
|
2110
4289
|
function seedStarterGraphsIfEmpty() {
|
|
2111
|
-
const existing =
|
|
4290
|
+
const existing = fs6.existsSync(GRAPHS_ROOT2) ? fs6.readdirSync(GRAPHS_ROOT2) : [];
|
|
2112
4291
|
if (existing.length > 0) return;
|
|
2113
4292
|
const introId = "intro-motif-starter";
|
|
2114
4293
|
createInitialGraphPackageSync(
|
|
@@ -2377,9 +4556,9 @@ function seedStarterGraphsIfEmpty() {
|
|
|
2377
4556
|
);
|
|
2378
4557
|
}
|
|
2379
4558
|
function createInitialGraphPackageSync(id, name, description, type, bpm, masterVolume, currentBiome, nodes, connections, importedGraphIds = []) {
|
|
2380
|
-
const graphDir =
|
|
2381
|
-
if (!
|
|
2382
|
-
|
|
4559
|
+
const graphDir = path6.join(GRAPHS_ROOT2, id);
|
|
4560
|
+
if (!fs6.existsSync(graphDir)) {
|
|
4561
|
+
fs6.mkdirSync(graphDir, { recursive: true });
|
|
2383
4562
|
}
|
|
2384
4563
|
ensureGraphAssetDirectories(graphDir);
|
|
2385
4564
|
seedStarterGraphAssets(graphDir);
|
|
@@ -2412,29 +4591,29 @@ function createInitialGraphPackageSync(id, name, description, type, bpm, masterV
|
|
|
2412
4591
|
connections
|
|
2413
4592
|
}
|
|
2414
4593
|
};
|
|
2415
|
-
|
|
4594
|
+
fs6.writeFileSync(path6.join(graphDir, "graph.qms"), JSON.stringify(graphPackage, null, 2), "utf-8");
|
|
2416
4595
|
}
|
|
2417
4596
|
async function listAllGraphPackages() {
|
|
2418
4597
|
ensureWorkspaceStructure();
|
|
2419
|
-
const dirs = await
|
|
4598
|
+
const dirs = await fs6.promises.readdir(GRAPHS_ROOT2);
|
|
2420
4599
|
const packages = [];
|
|
2421
4600
|
for (const dir of dirs) {
|
|
2422
|
-
const graphDir =
|
|
2423
|
-
const stat = await
|
|
4601
|
+
const graphDir = path6.join(GRAPHS_ROOT2, dir);
|
|
4602
|
+
const stat = await fs6.promises.stat(graphDir);
|
|
2424
4603
|
if (!stat.isDirectory()) continue;
|
|
2425
|
-
const qmsFile =
|
|
2426
|
-
const legacyProjectFile =
|
|
4604
|
+
const qmsFile = path6.join(graphDir, "graph.qms");
|
|
4605
|
+
const legacyProjectFile = path6.join(graphDir, "project.json");
|
|
2427
4606
|
let packageData = null;
|
|
2428
|
-
if (
|
|
4607
|
+
if (fs6.existsSync(qmsFile)) {
|
|
2429
4608
|
try {
|
|
2430
|
-
const raw = await
|
|
4609
|
+
const raw = await fs6.promises.readFile(qmsFile, "utf-8");
|
|
2431
4610
|
packageData = JSON.parse(raw);
|
|
2432
4611
|
} catch (err) {
|
|
2433
4612
|
console.warn(`Corrupted graph.qms in ${dir}:`, err);
|
|
2434
4613
|
}
|
|
2435
|
-
} else if (
|
|
4614
|
+
} else if (fs6.existsSync(legacyProjectFile)) {
|
|
2436
4615
|
try {
|
|
2437
|
-
const raw = await
|
|
4616
|
+
const raw = await fs6.promises.readFile(legacyProjectFile, "utf-8");
|
|
2438
4617
|
const legacy = JSON.parse(raw);
|
|
2439
4618
|
packageData = {
|
|
2440
4619
|
format: "qms_graph_package",
|
|
@@ -2464,7 +4643,7 @@ async function listAllGraphPackages() {
|
|
|
2464
4643
|
mapIoConfig: legacy.mapIoConfig
|
|
2465
4644
|
}
|
|
2466
4645
|
};
|
|
2467
|
-
await
|
|
4646
|
+
await fs6.promises.writeFile(qmsFile, JSON.stringify(packageData, null, 2), "utf-8");
|
|
2468
4647
|
} catch (err) {
|
|
2469
4648
|
console.warn(`Failed to auto-migrate legacy project ${dir}:`, err);
|
|
2470
4649
|
}
|
|
@@ -2486,13 +4665,13 @@ async function listAllGraphPackages() {
|
|
|
2486
4665
|
}
|
|
2487
4666
|
async function getGraphPackageById(id) {
|
|
2488
4667
|
ensureWorkspaceStructure();
|
|
2489
|
-
const safeId =
|
|
2490
|
-
const graphDir =
|
|
2491
|
-
const qmsFile =
|
|
2492
|
-
if (!
|
|
2493
|
-
const legacyProject =
|
|
2494
|
-
if (
|
|
2495
|
-
const raw2 = await
|
|
4668
|
+
const safeId = path6.basename(id);
|
|
4669
|
+
const graphDir = path6.join(GRAPHS_ROOT2, safeId);
|
|
4670
|
+
const qmsFile = path6.join(graphDir, "graph.qms");
|
|
4671
|
+
if (!fs6.existsSync(qmsFile)) {
|
|
4672
|
+
const legacyProject = path6.join(LEGACY_PROJECTS_ROOT, safeId, "project.json");
|
|
4673
|
+
if (fs6.existsSync(legacyProject)) {
|
|
4674
|
+
const raw2 = await fs6.promises.readFile(legacyProject, "utf-8");
|
|
2496
4675
|
const legacy = JSON.parse(raw2);
|
|
2497
4676
|
return {
|
|
2498
4677
|
format: "qms_graph_package",
|
|
@@ -2525,7 +4704,7 @@ async function getGraphPackageById(id) {
|
|
|
2525
4704
|
}
|
|
2526
4705
|
return null;
|
|
2527
4706
|
}
|
|
2528
|
-
const raw = await
|
|
4707
|
+
const raw = await fs6.promises.readFile(qmsFile, "utf-8");
|
|
2529
4708
|
return JSON.parse(raw);
|
|
2530
4709
|
}
|
|
2531
4710
|
async function createGraphPackage(params) {
|
|
@@ -2533,8 +4712,8 @@ async function createGraphPackage(params) {
|
|
|
2533
4712
|
const name = params.name.trim();
|
|
2534
4713
|
const safeSlug = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-");
|
|
2535
4714
|
const id = `${safeSlug}-${Date.now().toString(36)}`;
|
|
2536
|
-
const graphDir =
|
|
2537
|
-
await
|
|
4715
|
+
const graphDir = path6.join(GRAPHS_ROOT2, id);
|
|
4716
|
+
await fs6.promises.mkdir(graphDir, { recursive: true });
|
|
2538
4717
|
await ensureGraphAssetDirectories(graphDir);
|
|
2539
4718
|
seedStarterGraphAssets(graphDir);
|
|
2540
4719
|
const now = Date.now();
|
|
@@ -2569,8 +4748,8 @@ async function createGraphPackage(params) {
|
|
|
2569
4748
|
connections: params.connections || []
|
|
2570
4749
|
}
|
|
2571
4750
|
};
|
|
2572
|
-
await
|
|
2573
|
-
|
|
4751
|
+
await fs6.promises.writeFile(
|
|
4752
|
+
path6.join(graphDir, "graph.qms"),
|
|
2574
4753
|
JSON.stringify(graphPackage, null, 2),
|
|
2575
4754
|
"utf-8"
|
|
2576
4755
|
);
|
|
@@ -2578,10 +4757,10 @@ async function createGraphPackage(params) {
|
|
|
2578
4757
|
}
|
|
2579
4758
|
async function saveGraphPackage(id, payload) {
|
|
2580
4759
|
ensureWorkspaceStructure();
|
|
2581
|
-
const safeId =
|
|
2582
|
-
const graphDir =
|
|
2583
|
-
if (!
|
|
2584
|
-
await
|
|
4760
|
+
const safeId = path6.basename(id);
|
|
4761
|
+
const graphDir = path6.join(GRAPHS_ROOT2, safeId);
|
|
4762
|
+
if (!fs6.existsSync(graphDir)) {
|
|
4763
|
+
await fs6.promises.mkdir(graphDir, { recursive: true });
|
|
2585
4764
|
await ensureGraphAssetDirectories(graphDir);
|
|
2586
4765
|
}
|
|
2587
4766
|
const existing = await getGraphPackageById(safeId);
|
|
@@ -2616,18 +4795,18 @@ async function saveGraphPackage(id, payload) {
|
|
|
2616
4795
|
mapIoConfig: payload.mapIoConfig || existing?.graph?.mapIoConfig
|
|
2617
4796
|
}
|
|
2618
4797
|
};
|
|
2619
|
-
await
|
|
2620
|
-
|
|
4798
|
+
await fs6.promises.writeFile(
|
|
4799
|
+
path6.join(graphDir, "graph.qms"),
|
|
2621
4800
|
JSON.stringify(graphPackage, null, 2),
|
|
2622
4801
|
"utf-8"
|
|
2623
4802
|
);
|
|
2624
4803
|
return graphPackage;
|
|
2625
4804
|
}
|
|
2626
4805
|
async function deleteGraphPackage(id) {
|
|
2627
|
-
const safeId =
|
|
2628
|
-
const graphDir =
|
|
2629
|
-
if (!
|
|
2630
|
-
await
|
|
4806
|
+
const safeId = path6.basename(id);
|
|
4807
|
+
const graphDir = path6.join(GRAPHS_ROOT2, safeId);
|
|
4808
|
+
if (!fs6.existsSync(graphDir)) return false;
|
|
4809
|
+
await fs6.promises.rm(graphDir, { recursive: true, force: true });
|
|
2631
4810
|
return true;
|
|
2632
4811
|
}
|
|
2633
4812
|
async function importGraphDependency(targetGraphId, candidateImportId) {
|
|
@@ -2669,26 +4848,26 @@ async function removeGraphDependency(targetGraphId, importIdToRemove) {
|
|
|
2669
4848
|
}
|
|
2670
4849
|
|
|
2671
4850
|
// src/server/services/projectService.ts
|
|
2672
|
-
import
|
|
2673
|
-
import
|
|
2674
|
-
var PROJECTS_ROOT2 =
|
|
4851
|
+
import fs7 from "fs";
|
|
4852
|
+
import path7 from "path";
|
|
4853
|
+
var PROJECTS_ROOT2 = path7.join(process.cwd(), "projects");
|
|
2675
4854
|
var customStarterProjectTemplate = null;
|
|
2676
4855
|
function configureStarterProjectTemplate(template) {
|
|
2677
4856
|
customStarterProjectTemplate = template;
|
|
2678
4857
|
}
|
|
2679
4858
|
function ensureProjectsRoot(starterTemplate) {
|
|
2680
|
-
if (!
|
|
2681
|
-
|
|
4859
|
+
if (!fs7.existsSync(PROJECTS_ROOT2)) {
|
|
4860
|
+
fs7.mkdirSync(PROJECTS_ROOT2, { recursive: true });
|
|
2682
4861
|
}
|
|
2683
4862
|
const template = starterTemplate || customStarterProjectTemplate;
|
|
2684
4863
|
if (template && template.id) {
|
|
2685
|
-
const defaultProjDir =
|
|
2686
|
-
if (!
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
4864
|
+
const defaultProjDir = path7.join(PROJECTS_ROOT2, String(template.id));
|
|
4865
|
+
if (!fs7.existsSync(defaultProjDir)) {
|
|
4866
|
+
fs7.mkdirSync(defaultProjDir, { recursive: true });
|
|
4867
|
+
fs7.mkdirSync(path7.join(defaultProjDir, "samples"), { recursive: true });
|
|
4868
|
+
fs7.mkdirSync(path7.join(defaultProjDir, "assets"), { recursive: true });
|
|
4869
|
+
fs7.writeFileSync(
|
|
4870
|
+
path7.join(defaultProjDir, "project.json"),
|
|
2692
4871
|
JSON.stringify({ ...template, createdAt: Date.now(), updatedAt: Date.now() }, null, 2),
|
|
2693
4872
|
"utf-8"
|
|
2694
4873
|
);
|
|
@@ -2697,22 +4876,22 @@ function ensureProjectsRoot(starterTemplate) {
|
|
|
2697
4876
|
}
|
|
2698
4877
|
async function listAllProjects() {
|
|
2699
4878
|
ensureProjectsRoot();
|
|
2700
|
-
const dirs = await
|
|
4879
|
+
const dirs = await fs7.promises.readdir(PROJECTS_ROOT2);
|
|
2701
4880
|
const projects = [];
|
|
2702
4881
|
for (const dir of dirs) {
|
|
2703
|
-
const projDir =
|
|
2704
|
-
const stat = await
|
|
4882
|
+
const projDir = path7.join(PROJECTS_ROOT2, dir);
|
|
4883
|
+
const stat = await fs7.promises.stat(projDir);
|
|
2705
4884
|
if (!stat.isDirectory()) continue;
|
|
2706
|
-
const projectFile =
|
|
2707
|
-
const samplesDir =
|
|
4885
|
+
const projectFile = path7.join(projDir, "project.json");
|
|
4886
|
+
const samplesDir = path7.join(projDir, "samples");
|
|
2708
4887
|
let sampleCount = 0;
|
|
2709
|
-
if (
|
|
2710
|
-
const samples = await
|
|
4888
|
+
if (fs7.existsSync(samplesDir)) {
|
|
4889
|
+
const samples = await fs7.promises.readdir(samplesDir);
|
|
2711
4890
|
sampleCount = samples.filter((f) => !f.startsWith(".")).length;
|
|
2712
4891
|
}
|
|
2713
|
-
if (
|
|
4892
|
+
if (fs7.existsSync(projectFile)) {
|
|
2714
4893
|
try {
|
|
2715
|
-
const raw = await
|
|
4894
|
+
const raw = await fs7.promises.readFile(projectFile, "utf-8");
|
|
2716
4895
|
const data = JSON.parse(raw);
|
|
2717
4896
|
projects.push({
|
|
2718
4897
|
id: data.id || dir,
|
|
@@ -2732,24 +4911,24 @@ async function listAllProjects() {
|
|
|
2732
4911
|
return projects.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
2733
4912
|
}
|
|
2734
4913
|
async function getProjectById(id) {
|
|
2735
|
-
const safeId =
|
|
2736
|
-
const projectFile =
|
|
2737
|
-
if (!
|
|
2738
|
-
const raw = await
|
|
4914
|
+
const safeId = path7.basename(id);
|
|
4915
|
+
const projectFile = path7.join(PROJECTS_ROOT2, safeId, "project.json");
|
|
4916
|
+
if (!fs7.existsSync(projectFile)) return null;
|
|
4917
|
+
const raw = await fs7.promises.readFile(projectFile, "utf-8");
|
|
2739
4918
|
return JSON.parse(raw);
|
|
2740
4919
|
}
|
|
2741
4920
|
async function saveProject(id, payload) {
|
|
2742
|
-
const safeId =
|
|
2743
|
-
const projDir =
|
|
2744
|
-
const projectFile =
|
|
2745
|
-
if (!
|
|
2746
|
-
await
|
|
2747
|
-
await
|
|
2748
|
-
await
|
|
4921
|
+
const safeId = path7.basename(id);
|
|
4922
|
+
const projDir = path7.join(PROJECTS_ROOT2, safeId);
|
|
4923
|
+
const projectFile = path7.join(projDir, "project.json");
|
|
4924
|
+
if (!fs7.existsSync(projDir)) {
|
|
4925
|
+
await fs7.promises.mkdir(projDir, { recursive: true });
|
|
4926
|
+
await fs7.promises.mkdir(path7.join(projDir, "samples"), { recursive: true });
|
|
4927
|
+
await fs7.promises.mkdir(path7.join(projDir, "assets"), { recursive: true });
|
|
2749
4928
|
}
|
|
2750
4929
|
let existingData = {};
|
|
2751
|
-
if (
|
|
2752
|
-
const raw = await
|
|
4930
|
+
if (fs7.existsSync(projectFile)) {
|
|
4931
|
+
const raw = await fs7.promises.readFile(projectFile, "utf-8");
|
|
2753
4932
|
existingData = JSON.parse(raw);
|
|
2754
4933
|
}
|
|
2755
4934
|
const updatedData = {
|
|
@@ -2758,22 +4937,22 @@ async function saveProject(id, payload) {
|
|
|
2758
4937
|
id: safeId,
|
|
2759
4938
|
updatedAt: Date.now()
|
|
2760
4939
|
};
|
|
2761
|
-
await
|
|
4940
|
+
await fs7.promises.writeFile(projectFile, JSON.stringify(updatedData, null, 2), "utf-8");
|
|
2762
4941
|
return updatedData;
|
|
2763
4942
|
}
|
|
2764
4943
|
async function deleteProject(id) {
|
|
2765
|
-
const safeId =
|
|
2766
|
-
const projDir =
|
|
2767
|
-
if (!
|
|
2768
|
-
await
|
|
4944
|
+
const safeId = path7.basename(id);
|
|
4945
|
+
const projDir = path7.join(PROJECTS_ROOT2, safeId);
|
|
4946
|
+
if (!fs7.existsSync(projDir)) return false;
|
|
4947
|
+
await fs7.promises.rm(projDir, { recursive: true, force: true });
|
|
2769
4948
|
return true;
|
|
2770
4949
|
}
|
|
2771
4950
|
|
|
2772
4951
|
// src/server/services/sampleService.ts
|
|
2773
|
-
import
|
|
2774
|
-
import
|
|
2775
|
-
var SAMPLES_GLOBAL_ROOT =
|
|
2776
|
-
var PROJECTS_ROOT3 =
|
|
4952
|
+
import fs8 from "fs";
|
|
4953
|
+
import path8 from "path";
|
|
4954
|
+
var SAMPLES_GLOBAL_ROOT = path8.join(process.cwd(), "samples", "global");
|
|
4955
|
+
var PROJECTS_ROOT3 = path8.join(process.cwd(), "projects");
|
|
2777
4956
|
var SUPPORTED_AUDIO_EXTS3 = [".wav", ".mp3", ".ogg", ".flac", ".m4a", ".webm", ".weba", ".aac", ".aiff", ".aif", ".opus", ".mp4"];
|
|
2778
4957
|
function initGlobalSamplesDirectory() {
|
|
2779
4958
|
ensureGlobalSamples(SAMPLES_GLOBAL_ROOT).catch((err) => {
|
|
@@ -2781,17 +4960,17 @@ function initGlobalSamplesDirectory() {
|
|
|
2781
4960
|
});
|
|
2782
4961
|
}
|
|
2783
4962
|
async function listGlobalSamples() {
|
|
2784
|
-
if (!
|
|
2785
|
-
await
|
|
4963
|
+
if (!fs8.existsSync(SAMPLES_GLOBAL_ROOT)) {
|
|
4964
|
+
await fs8.promises.mkdir(SAMPLES_GLOBAL_ROOT, { recursive: true });
|
|
2786
4965
|
}
|
|
2787
|
-
const files = await
|
|
4966
|
+
const files = await fs8.promises.readdir(SAMPLES_GLOBAL_ROOT);
|
|
2788
4967
|
const samples = [];
|
|
2789
4968
|
for (const file of files) {
|
|
2790
|
-
const ext =
|
|
4969
|
+
const ext = path8.extname(file).toLowerCase();
|
|
2791
4970
|
if (!SUPPORTED_AUDIO_EXTS3.includes(ext)) continue;
|
|
2792
|
-
const filePath =
|
|
2793
|
-
const stat = await
|
|
2794
|
-
const baseName =
|
|
4971
|
+
const filePath = path8.join(SAMPLES_GLOBAL_ROOT, file);
|
|
4972
|
+
const stat = await fs8.promises.stat(filePath);
|
|
4973
|
+
const baseName = path8.basename(file, ext);
|
|
2795
4974
|
const cleanName = baseName.replace(/[-_]/g, " ");
|
|
2796
4975
|
let category = "synth";
|
|
2797
4976
|
if (/808|kick|snare|hihat|drum|perc/i.test(baseName)) category = "drums";
|
|
@@ -2811,15 +4990,15 @@ async function listGlobalSamples() {
|
|
|
2811
4990
|
return samples;
|
|
2812
4991
|
}
|
|
2813
4992
|
async function saveGlobalSample(filename, base64Content, category) {
|
|
2814
|
-
const safeFilename =
|
|
2815
|
-
const ext =
|
|
2816
|
-
const filePath =
|
|
4993
|
+
const safeFilename = path8.basename(filename).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
4994
|
+
const ext = path8.extname(safeFilename).toLowerCase() || ".wav";
|
|
4995
|
+
const filePath = path8.join(SAMPLES_GLOBAL_ROOT, safeFilename);
|
|
2817
4996
|
const buffer = Buffer.from(base64Content, "base64");
|
|
2818
|
-
await
|
|
2819
|
-
const stat = await
|
|
4997
|
+
await fs8.promises.writeFile(filePath, buffer);
|
|
4998
|
+
const stat = await fs8.promises.stat(filePath);
|
|
2820
4999
|
return {
|
|
2821
5000
|
filename: safeFilename,
|
|
2822
|
-
name:
|
|
5001
|
+
name: path8.basename(safeFilename, ext).replace(/[-_]/g, " "),
|
|
2823
5002
|
category: category || "custom",
|
|
2824
5003
|
format: ext.replace(".", "").toUpperCase(),
|
|
2825
5004
|
url: `/samples/global/${safeFilename}`,
|
|
@@ -2829,28 +5008,28 @@ async function saveGlobalSample(filename, base64Content, category) {
|
|
|
2829
5008
|
};
|
|
2830
5009
|
}
|
|
2831
5010
|
async function deleteGlobalSample(filename) {
|
|
2832
|
-
const safeFilename =
|
|
2833
|
-
const filePath =
|
|
2834
|
-
if (
|
|
2835
|
-
await
|
|
5011
|
+
const safeFilename = path8.basename(filename);
|
|
5012
|
+
const filePath = path8.join(SAMPLES_GLOBAL_ROOT, safeFilename);
|
|
5013
|
+
if (fs8.existsSync(filePath)) {
|
|
5014
|
+
await fs8.promises.unlink(filePath);
|
|
2836
5015
|
return true;
|
|
2837
5016
|
}
|
|
2838
5017
|
return false;
|
|
2839
5018
|
}
|
|
2840
5019
|
async function listProjectSamples(projectId) {
|
|
2841
|
-
const safeId =
|
|
2842
|
-
const samplesDir =
|
|
2843
|
-
if (!
|
|
2844
|
-
await
|
|
5020
|
+
const safeId = path8.basename(projectId);
|
|
5021
|
+
const samplesDir = path8.join(PROJECTS_ROOT3, safeId, "samples");
|
|
5022
|
+
if (!fs8.existsSync(samplesDir)) {
|
|
5023
|
+
await fs8.promises.mkdir(samplesDir, { recursive: true });
|
|
2845
5024
|
}
|
|
2846
|
-
const files = await
|
|
5025
|
+
const files = await fs8.promises.readdir(samplesDir);
|
|
2847
5026
|
const samples = [];
|
|
2848
5027
|
for (const file of files) {
|
|
2849
|
-
const ext =
|
|
5028
|
+
const ext = path8.extname(file).toLowerCase();
|
|
2850
5029
|
if (!SUPPORTED_AUDIO_EXTS3.includes(ext)) continue;
|
|
2851
|
-
const filePath =
|
|
2852
|
-
const stat = await
|
|
2853
|
-
const baseName =
|
|
5030
|
+
const filePath = path8.join(samplesDir, file);
|
|
5031
|
+
const stat = await fs8.promises.stat(filePath);
|
|
5032
|
+
const baseName = path8.basename(file, ext);
|
|
2854
5033
|
samples.push({
|
|
2855
5034
|
filename: file,
|
|
2856
5035
|
name: baseName.replace(/[-_]/g, " "),
|
|
@@ -2866,21 +5045,21 @@ async function listProjectSamples(projectId) {
|
|
|
2866
5045
|
return samples;
|
|
2867
5046
|
}
|
|
2868
5047
|
async function saveProjectSample(projectId, filename, base64Content, isRecording) {
|
|
2869
|
-
const safeId =
|
|
2870
|
-
const samplesDir =
|
|
2871
|
-
if (!
|
|
2872
|
-
await
|
|
5048
|
+
const safeId = path8.basename(projectId);
|
|
5049
|
+
const samplesDir = path8.join(PROJECTS_ROOT3, safeId, "samples");
|
|
5050
|
+
if (!fs8.existsSync(samplesDir)) {
|
|
5051
|
+
await fs8.promises.mkdir(samplesDir, { recursive: true });
|
|
2873
5052
|
}
|
|
2874
5053
|
const defaultName = isRecording ? `recording_take_${Date.now()}.wav` : `clip_${Date.now()}.wav`;
|
|
2875
|
-
const targetFilename = filename ?
|
|
2876
|
-
const ext =
|
|
2877
|
-
const filePath =
|
|
5054
|
+
const targetFilename = filename ? path8.basename(filename).replace(/[^a-zA-Z0-9._-]/g, "_") : defaultName;
|
|
5055
|
+
const ext = path8.extname(targetFilename).toLowerCase() || ".wav";
|
|
5056
|
+
const filePath = path8.join(samplesDir, targetFilename);
|
|
2878
5057
|
const buffer = Buffer.from(base64Content, "base64");
|
|
2879
|
-
await
|
|
2880
|
-
const stat = await
|
|
5058
|
+
await fs8.promises.writeFile(filePath, buffer);
|
|
5059
|
+
const stat = await fs8.promises.stat(filePath);
|
|
2881
5060
|
return {
|
|
2882
5061
|
filename: targetFilename,
|
|
2883
|
-
name:
|
|
5062
|
+
name: path8.basename(targetFilename, ext).replace(/[-_]/g, " "),
|
|
2884
5063
|
category: isRecording ? "recording" : "import",
|
|
2885
5064
|
format: ext.replace(".", "").toUpperCase(),
|
|
2886
5065
|
url: `/projects/${safeId}/samples/${targetFilename}`,
|
|
@@ -2891,26 +5070,26 @@ async function saveProjectSample(projectId, filename, base64Content, isRecording
|
|
|
2891
5070
|
};
|
|
2892
5071
|
}
|
|
2893
5072
|
async function deleteProjectSample(projectId, filename) {
|
|
2894
|
-
const safeId =
|
|
2895
|
-
const safeFilename =
|
|
2896
|
-
const filePath =
|
|
2897
|
-
if (
|
|
2898
|
-
await
|
|
5073
|
+
const safeId = path8.basename(projectId);
|
|
5074
|
+
const safeFilename = path8.basename(filename);
|
|
5075
|
+
const filePath = path8.join(PROJECTS_ROOT3, safeId, "samples", safeFilename);
|
|
5076
|
+
if (fs8.existsSync(filePath)) {
|
|
5077
|
+
await fs8.promises.unlink(filePath);
|
|
2899
5078
|
return true;
|
|
2900
5079
|
}
|
|
2901
5080
|
return false;
|
|
2902
5081
|
}
|
|
2903
5082
|
|
|
2904
5083
|
// src/server/services/workspaceGraphService.ts
|
|
2905
|
-
import
|
|
2906
|
-
import
|
|
2907
|
-
var AUTH_FILE_PATH =
|
|
5084
|
+
import fs9 from "fs";
|
|
5085
|
+
import path9 from "path";
|
|
5086
|
+
var AUTH_FILE_PATH = path9.resolve(".skipo/accounts.json");
|
|
2908
5087
|
async function resolveWorkspaceDir(workspaceIdOrSlug) {
|
|
2909
5088
|
const workingDir = getWorkingWorkspaceDir();
|
|
2910
5089
|
let targetWs = null;
|
|
2911
5090
|
try {
|
|
2912
|
-
if (
|
|
2913
|
-
const raw = await
|
|
5091
|
+
if (fs9.existsSync(AUTH_FILE_PATH)) {
|
|
5092
|
+
const raw = await fs9.promises.readFile(AUTH_FILE_PATH, "utf-8");
|
|
2914
5093
|
const accounts = JSON.parse(raw);
|
|
2915
5094
|
for (const user of Object.values(accounts)) {
|
|
2916
5095
|
if (Array.isArray(user.workspaces)) {
|
|
@@ -2930,35 +5109,35 @@ async function resolveWorkspaceDir(workspaceIdOrSlug) {
|
|
|
2930
5109
|
if (targetWs) {
|
|
2931
5110
|
const rawFolderName = targetWs.localFolderName || targetWs.name.replace(/[/\\?%*:|"<>]/g, "_");
|
|
2932
5111
|
const folderName = rawFolderName.trim();
|
|
2933
|
-
await
|
|
5112
|
+
await fs9.promises.mkdir(workingDir, { recursive: true });
|
|
2934
5113
|
return { wsDir: workingDir, wsName: targetWs.name, wsFolder: folderName, wsId: targetWs.id };
|
|
2935
5114
|
}
|
|
2936
|
-
await
|
|
5115
|
+
await fs9.promises.mkdir(workingDir, { recursive: true });
|
|
2937
5116
|
return { wsDir: workingDir, wsName: "Active Workspace", wsFolder: "workspace", wsId: workspaceIdOrSlug || "default" };
|
|
2938
5117
|
}
|
|
2939
5118
|
async function listWorkspaceGraphs(workspaceIdOrSlug) {
|
|
2940
5119
|
const { wsDir, wsFolder } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
2941
5120
|
const graphs = [];
|
|
2942
5121
|
const inspectDirForGraph = async (dirPath, folderName) => {
|
|
2943
|
-
const projectJson =
|
|
2944
|
-
const graphQms =
|
|
5122
|
+
const projectJson = path9.join(dirPath, "project.json");
|
|
5123
|
+
const graphQms = path9.join(dirPath, "graph.qms");
|
|
2945
5124
|
let data = null;
|
|
2946
|
-
if (
|
|
5125
|
+
if (fs9.existsSync(projectJson)) {
|
|
2947
5126
|
try {
|
|
2948
|
-
const raw = await
|
|
5127
|
+
const raw = await fs9.promises.readFile(projectJson, "utf-8");
|
|
2949
5128
|
data = JSON.parse(raw);
|
|
2950
5129
|
} catch {
|
|
2951
5130
|
}
|
|
2952
|
-
} else if (
|
|
5131
|
+
} else if (fs9.existsSync(graphQms)) {
|
|
2953
5132
|
try {
|
|
2954
|
-
const raw = await
|
|
5133
|
+
const raw = await fs9.promises.readFile(graphQms, "utf-8");
|
|
2955
5134
|
const pkg = JSON.parse(raw);
|
|
2956
5135
|
data = { ...pkg.metadata || {}, ...pkg.graph || {} };
|
|
2957
5136
|
} catch {
|
|
2958
5137
|
}
|
|
2959
5138
|
}
|
|
2960
5139
|
if (!data) return null;
|
|
2961
|
-
const stat = await
|
|
5140
|
+
const stat = await fs9.promises.stat(dirPath);
|
|
2962
5141
|
const nodeCount = Array.isArray(data.nodes) ? data.nodes.length : 0;
|
|
2963
5142
|
const connectionCount = Array.isArray(data.connections) ? data.connections.length : 0;
|
|
2964
5143
|
return {
|
|
@@ -2966,7 +5145,7 @@ async function listWorkspaceGraphs(workspaceIdOrSlug) {
|
|
|
2966
5145
|
name: data.name || folderName,
|
|
2967
5146
|
description: data.description || "",
|
|
2968
5147
|
folderName,
|
|
2969
|
-
folderPath:
|
|
5148
|
+
folderPath: path9.relative(process.cwd(), dirPath).replace(/\\/g, "/"),
|
|
2970
5149
|
createdAt: data.createdAt || stat.birthtimeMs,
|
|
2971
5150
|
updatedAt: data.updatedAt || stat.mtimeMs,
|
|
2972
5151
|
bpm: data.bpm || 120,
|
|
@@ -2979,18 +5158,18 @@ async function listWorkspaceGraphs(workspaceIdOrSlug) {
|
|
|
2979
5158
|
connectionCount
|
|
2980
5159
|
};
|
|
2981
5160
|
};
|
|
2982
|
-
const entries = await
|
|
5161
|
+
const entries = await fs9.promises.readdir(wsDir);
|
|
2983
5162
|
for (const entry of entries) {
|
|
2984
5163
|
if (entry.startsWith(".") || entry === "samples" || entry === "assets") continue;
|
|
2985
|
-
const fullPath =
|
|
2986
|
-
const stat = await
|
|
5164
|
+
const fullPath = path9.join(wsDir, entry);
|
|
5165
|
+
const stat = await fs9.promises.stat(fullPath);
|
|
2987
5166
|
if (!stat.isDirectory()) continue;
|
|
2988
5167
|
if (entry === "graphs") {
|
|
2989
|
-
const subEntries = await
|
|
5168
|
+
const subEntries = await fs9.promises.readdir(fullPath);
|
|
2990
5169
|
for (const sub of subEntries) {
|
|
2991
5170
|
if (sub.startsWith(".")) continue;
|
|
2992
|
-
const subPath =
|
|
2993
|
-
const subStat = await
|
|
5171
|
+
const subPath = path9.join(fullPath, sub);
|
|
5172
|
+
const subStat = await fs9.promises.stat(subPath);
|
|
2994
5173
|
if (subStat.isDirectory()) {
|
|
2995
5174
|
const g2 = await inspectDirForGraph(subPath, sub);
|
|
2996
5175
|
if (g2) graphs.push(g2);
|
|
@@ -3007,10 +5186,10 @@ async function createWorkspaceGraph(workspaceIdOrSlug, params) {
|
|
|
3007
5186
|
const { wsDir, wsFolder, wsId } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
3008
5187
|
const rawName = params.name.trim();
|
|
3009
5188
|
const folderName = rawName.replace(/[/\\?%*:|"<>]/g, "_");
|
|
3010
|
-
const graphDir =
|
|
3011
|
-
await
|
|
3012
|
-
await
|
|
3013
|
-
await
|
|
5189
|
+
const graphDir = path9.join(wsDir, folderName);
|
|
5190
|
+
await fs9.promises.mkdir(graphDir, { recursive: true });
|
|
5191
|
+
await fs9.promises.mkdir(path9.join(graphDir, "samples"), { recursive: true });
|
|
5192
|
+
await fs9.promises.mkdir(path9.join(graphDir, "assets"), { recursive: true });
|
|
3014
5193
|
const now = Date.now();
|
|
3015
5194
|
const nodes = params.templateData?.nodes || [];
|
|
3016
5195
|
const connections = params.templateData?.connections || [];
|
|
@@ -3021,7 +5200,7 @@ async function createWorkspaceGraph(workspaceIdOrSlug, params) {
|
|
|
3021
5200
|
id: folderName,
|
|
3022
5201
|
name: rawName,
|
|
3023
5202
|
description: params.description || `Workspace graph in ${wsFolder}`,
|
|
3024
|
-
folderPath:
|
|
5203
|
+
folderPath: path9.relative(process.cwd(), graphDir).replace(/\\/g, "/"),
|
|
3025
5204
|
createdAt: now,
|
|
3026
5205
|
updatedAt: now,
|
|
3027
5206
|
bpm,
|
|
@@ -3030,7 +5209,7 @@ async function createWorkspaceGraph(workspaceIdOrSlug, params) {
|
|
|
3030
5209
|
nodes,
|
|
3031
5210
|
connections
|
|
3032
5211
|
};
|
|
3033
|
-
await
|
|
5212
|
+
await fs9.promises.writeFile(path9.join(graphDir, "project.json"), JSON.stringify(projectData, null, 2), "utf-8");
|
|
3034
5213
|
const graphPackage = {
|
|
3035
5214
|
format: "qms_graph_package",
|
|
3036
5215
|
version: "2.0.0",
|
|
@@ -3048,27 +5227,27 @@ async function createWorkspaceGraph(workspaceIdOrSlug, params) {
|
|
|
3048
5227
|
},
|
|
3049
5228
|
graph: { bpm, masterVolume, currentBiome, nodes, connections }
|
|
3050
5229
|
};
|
|
3051
|
-
await
|
|
5230
|
+
await fs9.promises.writeFile(path9.join(graphDir, "graph.qms"), JSON.stringify(graphPackage, null, 2), "utf-8");
|
|
3052
5231
|
return projectData;
|
|
3053
5232
|
}
|
|
3054
5233
|
async function getWorkspaceGraph(workspaceIdOrSlug, graphIdOrName) {
|
|
3055
5234
|
const { wsDir } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
3056
5235
|
const safeTarget = graphIdOrName.replace(/[/\\?%*:|"<>]/g, "_");
|
|
3057
5236
|
const candidates = [
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
5237
|
+
path9.join(wsDir, safeTarget),
|
|
5238
|
+
path9.join(wsDir, graphIdOrName),
|
|
5239
|
+
path9.join(wsDir, "graphs", safeTarget)
|
|
3061
5240
|
];
|
|
3062
5241
|
for (const cand of candidates) {
|
|
3063
|
-
if (
|
|
3064
|
-
const pJson =
|
|
3065
|
-
if (
|
|
3066
|
-
const raw = await
|
|
5242
|
+
if (fs9.existsSync(cand)) {
|
|
5243
|
+
const pJson = path9.join(cand, "project.json");
|
|
5244
|
+
if (fs9.existsSync(pJson)) {
|
|
5245
|
+
const raw = await fs9.promises.readFile(pJson, "utf-8");
|
|
3067
5246
|
return JSON.parse(raw);
|
|
3068
5247
|
}
|
|
3069
|
-
const gQms =
|
|
3070
|
-
if (
|
|
3071
|
-
const raw = await
|
|
5248
|
+
const gQms = path9.join(cand, "graph.qms");
|
|
5249
|
+
if (fs9.existsSync(gQms)) {
|
|
5250
|
+
const raw = await fs9.promises.readFile(gQms, "utf-8");
|
|
3072
5251
|
const pkg = JSON.parse(raw);
|
|
3073
5252
|
return { ...pkg.metadata || {}, ...pkg.graph || {} };
|
|
3074
5253
|
}
|
|
@@ -3079,39 +5258,39 @@ async function getWorkspaceGraph(workspaceIdOrSlug, graphIdOrName) {
|
|
|
3079
5258
|
async function saveWorkspaceGraph(workspaceIdOrSlug, graphIdOrName, payload) {
|
|
3080
5259
|
const { wsDir } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
3081
5260
|
const safeTarget = graphIdOrName.replace(/[/\\?%*:|"<>]/g, "_");
|
|
3082
|
-
let targetDir =
|
|
3083
|
-
if (!
|
|
3084
|
-
targetDir =
|
|
5261
|
+
let targetDir = path9.join(wsDir, safeTarget);
|
|
5262
|
+
if (!fs9.existsSync(targetDir) && fs9.existsSync(path9.join(wsDir, "graphs", safeTarget))) {
|
|
5263
|
+
targetDir = path9.join(wsDir, "graphs", safeTarget);
|
|
3085
5264
|
}
|
|
3086
|
-
await
|
|
3087
|
-
const pJson =
|
|
5265
|
+
await fs9.promises.mkdir(targetDir, { recursive: true });
|
|
5266
|
+
const pJson = path9.join(targetDir, "project.json");
|
|
3088
5267
|
let existing = {};
|
|
3089
|
-
if (
|
|
5268
|
+
if (fs9.existsSync(pJson)) {
|
|
3090
5269
|
try {
|
|
3091
|
-
existing = JSON.parse(await
|
|
5270
|
+
existing = JSON.parse(await fs9.promises.readFile(pJson, "utf-8"));
|
|
3092
5271
|
} catch {
|
|
3093
5272
|
}
|
|
3094
5273
|
}
|
|
3095
5274
|
const updated = {
|
|
3096
5275
|
...existing,
|
|
3097
5276
|
...payload,
|
|
3098
|
-
id: existing.id ||
|
|
5277
|
+
id: existing.id || path9.basename(targetDir),
|
|
3099
5278
|
updatedAt: Date.now()
|
|
3100
5279
|
};
|
|
3101
|
-
await
|
|
5280
|
+
await fs9.promises.writeFile(pJson, JSON.stringify(updated, null, 2), "utf-8");
|
|
3102
5281
|
return updated;
|
|
3103
5282
|
}
|
|
3104
5283
|
async function deleteWorkspaceGraph(workspaceIdOrSlug, graphIdOrName) {
|
|
3105
5284
|
const { wsDir } = await resolveWorkspaceDir(workspaceIdOrSlug);
|
|
3106
5285
|
const safeTarget = graphIdOrName.replace(/[/\\?%*:|"<>]/g, "_");
|
|
3107
5286
|
const candidates = [
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
5287
|
+
path9.join(wsDir, safeTarget),
|
|
5288
|
+
path9.join(wsDir, graphIdOrName),
|
|
5289
|
+
path9.join(wsDir, "graphs", safeTarget)
|
|
3111
5290
|
];
|
|
3112
5291
|
for (const cand of candidates) {
|
|
3113
|
-
if (
|
|
3114
|
-
await
|
|
5292
|
+
if (fs9.existsSync(cand)) {
|
|
5293
|
+
await fs9.promises.rm(cand, { recursive: true, force: true });
|
|
3115
5294
|
return true;
|
|
3116
5295
|
}
|
|
3117
5296
|
}
|
|
@@ -3130,9 +5309,11 @@ export {
|
|
|
3130
5309
|
BaseAccountProvider,
|
|
3131
5310
|
BaseApiRoute,
|
|
3132
5311
|
BaseDataProvider,
|
|
5312
|
+
BaseResourceFetcher,
|
|
3133
5313
|
BaseStorageAdapter,
|
|
3134
5314
|
CANONICAL_SRKL_URL,
|
|
3135
5315
|
CallbackRegistry,
|
|
5316
|
+
CloudflareResourceFetcher,
|
|
3136
5317
|
CompositeBackend,
|
|
3137
5318
|
ConcurrencyConflictError,
|
|
3138
5319
|
ConcurrencyManager,
|
|
@@ -3167,6 +5348,7 @@ export {
|
|
|
3167
5348
|
PasswordHasher,
|
|
3168
5349
|
RateLimiter,
|
|
3169
5350
|
RemoteExecutor,
|
|
5351
|
+
ResourceFetcherRegistry,
|
|
3170
5352
|
ResultFlowBridge,
|
|
3171
5353
|
S3Adapter,
|
|
3172
5354
|
SAMPLES_GLOBAL_ROOT,
|
|
@@ -3182,6 +5364,7 @@ export {
|
|
|
3182
5364
|
StorageServiceFacade,
|
|
3183
5365
|
SupabaseStorageAdapter,
|
|
3184
5366
|
SyncEngine,
|
|
5367
|
+
VercelResourceFetcher,
|
|
3185
5368
|
WORKSPACE_ASSETS_ROOT,
|
|
3186
5369
|
WORKSPACE_ROOT,
|
|
3187
5370
|
WebSocketTransport,
|
|
@@ -3190,7 +5373,6 @@ export {
|
|
|
3190
5373
|
WorkspaceFS,
|
|
3191
5374
|
WorkspaceServiceDefinition,
|
|
3192
5375
|
WorkspaceServiceFacade,
|
|
3193
|
-
addWorkspaceSecondaryClone,
|
|
3194
5376
|
authorizeB2,
|
|
3195
5377
|
b2DeleteFileVersion,
|
|
3196
5378
|
b2DeleteFolder,
|
|
@@ -3198,12 +5380,15 @@ export {
|
|
|
3198
5380
|
b2ListBuckets,
|
|
3199
5381
|
b2ListFileNames,
|
|
3200
5382
|
b2UploadFile,
|
|
5383
|
+
buildProjectObservability,
|
|
5384
|
+
buildProjectsList,
|
|
3201
5385
|
cleanDbUrl,
|
|
3202
5386
|
configureStarterProjectTemplate,
|
|
3203
5387
|
createAssetSubfolder,
|
|
3204
5388
|
createBackend,
|
|
3205
5389
|
createBackendClient,
|
|
3206
5390
|
createCompositeService,
|
|
5391
|
+
createCredential,
|
|
3207
5392
|
createEmptyContract,
|
|
3208
5393
|
createGraphAssetFolder,
|
|
3209
5394
|
createGraphAssetFolder as createGraphAssetSubfolder,
|
|
@@ -3214,15 +5399,23 @@ export {
|
|
|
3214
5399
|
createSSEResponse,
|
|
3215
5400
|
createSrklClient,
|
|
3216
5401
|
createWavBuffer,
|
|
5402
|
+
createWorkspace,
|
|
3217
5403
|
createWorkspaceGraph,
|
|
5404
|
+
decryptSecretFromBlob,
|
|
3218
5405
|
deleteAssetFromGraph,
|
|
3219
5406
|
deleteAssetFromPool,
|
|
5407
|
+
deleteCredential,
|
|
3220
5408
|
deleteGlobalSample,
|
|
3221
5409
|
deleteGraphPackage,
|
|
3222
5410
|
deleteProject,
|
|
3223
5411
|
deleteProjectSample,
|
|
5412
|
+
deleteRoute,
|
|
5413
|
+
deleteWorkspace,
|
|
5414
|
+
deleteWorkspaceFile,
|
|
5415
|
+
deleteWorkspaceFolder,
|
|
3224
5416
|
deleteWorkspaceGraph,
|
|
3225
5417
|
detectDatabaseEngine,
|
|
5418
|
+
encryptSecretToBlob,
|
|
3226
5419
|
ensureAssetPoolDirectories,
|
|
3227
5420
|
ensureGlobalSamples,
|
|
3228
5421
|
ensureGraphAssetDirectories,
|
|
@@ -3230,16 +5423,27 @@ export {
|
|
|
3230
5423
|
ensureProjectsRoot,
|
|
3231
5424
|
ensureS3DirectoryMarker,
|
|
3232
5425
|
ensureWorkspaceStructure,
|
|
5426
|
+
exportEncryptedCredentials,
|
|
5427
|
+
fetchCloudflareData,
|
|
5428
|
+
fetchVercelData,
|
|
5429
|
+
findUserWorkspace,
|
|
3233
5430
|
findWorkspaceRecord,
|
|
3234
5431
|
formatCanonicalWorkspaceFolderName,
|
|
5432
|
+
formatRelativeCreated,
|
|
5433
|
+
getAllRoutes,
|
|
3235
5434
|
getAssetPoolLocationPath,
|
|
3236
5435
|
getAssetPoolPublicUrl,
|
|
3237
5436
|
getB2BucketId,
|
|
5437
|
+
getCredential,
|
|
3238
5438
|
getDefaultWorkingWorkspaceDir,
|
|
5439
|
+
getExecutions,
|
|
5440
|
+
getGatewayConfig,
|
|
3239
5441
|
getGraphPackageById,
|
|
3240
5442
|
getPrimarySrklDeploymentUrl2 as getPrimarySrklCoreDeploymentUrl,
|
|
3241
5443
|
getPrimarySrklDeploymentUrl,
|
|
3242
5444
|
getProjectById,
|
|
5445
|
+
getPublicRouteByPath,
|
|
5446
|
+
getPublicRoutes,
|
|
3243
5447
|
getSrklAccounts,
|
|
3244
5448
|
getSrklAccounts2 as getSrklCoreAccounts,
|
|
3245
5449
|
getSrklStatus,
|
|
@@ -3249,51 +5453,75 @@ export {
|
|
|
3249
5453
|
getWorkingWorkspaceConfig,
|
|
3250
5454
|
getWorkingWorkspaceDir,
|
|
3251
5455
|
getWorkspaceFolderKey,
|
|
5456
|
+
getWorkspaceFolderPrefix,
|
|
3252
5457
|
getWorkspaceGraph,
|
|
5458
|
+
getWorkspaceStorageDetails,
|
|
5459
|
+
getWorkspaces,
|
|
5460
|
+
importEncryptedCredentials,
|
|
3253
5461
|
importGraphDependency,
|
|
3254
5462
|
initGlobalSamplesDirectory,
|
|
5463
|
+
isB2Workspace,
|
|
3255
5464
|
isSecretMasked,
|
|
3256
5465
|
listAllGraphPackages,
|
|
3257
5466
|
listAllProjects,
|
|
3258
5467
|
listBackblazeBuckets,
|
|
5468
|
+
listCredentials,
|
|
3259
5469
|
listGlobalSamples,
|
|
3260
5470
|
listProjectSamples,
|
|
5471
|
+
listWorkspaceFiles,
|
|
3261
5472
|
listWorkspaceGraphs,
|
|
5473
|
+
loadCredentialManifest,
|
|
5474
|
+
loadWorkspaceData,
|
|
3262
5475
|
loginSystemUser,
|
|
3263
5476
|
loginUser,
|
|
5477
|
+
matchGatewayIngress,
|
|
5478
|
+
matchGatewayIngressAsync,
|
|
3264
5479
|
normalizeBackblazeCredentials,
|
|
3265
5480
|
permanentDeleteS3Folder,
|
|
3266
5481
|
permanentDeleteS3Object,
|
|
3267
5482
|
queryUnifiedDb,
|
|
5483
|
+
readWorkspaceFile,
|
|
5484
|
+
recordExecution,
|
|
3268
5485
|
removeGraphDependency,
|
|
3269
|
-
|
|
5486
|
+
renameAppInWorkspaceData,
|
|
3270
5487
|
renameS3Folder,
|
|
3271
5488
|
resolveBackblazeEndpoint,
|
|
5489
|
+
resolveDynamicRouteFromDb,
|
|
3272
5490
|
resolveSystemDatabaseClusterNodes,
|
|
3273
5491
|
resolveUserFromToken,
|
|
5492
|
+
resolveWorkspaceConnection,
|
|
3274
5493
|
resolveWorkspaceDir,
|
|
3275
5494
|
sanitizeWorkspaceName,
|
|
3276
5495
|
saveAssetToGraph,
|
|
3277
5496
|
saveAssetToPool,
|
|
5497
|
+
saveCredentialManifest,
|
|
3278
5498
|
saveGlobalSample,
|
|
3279
5499
|
saveGraphPackage,
|
|
3280
5500
|
saveProject,
|
|
3281
5501
|
saveProjectSample,
|
|
3282
|
-
|
|
5502
|
+
saveWorkspaceData,
|
|
3283
5503
|
saveWorkspaceGraph,
|
|
3284
5504
|
scanAssetPool,
|
|
3285
5505
|
scanGraphAssets,
|
|
3286
5506
|
seedDefaultAssets,
|
|
3287
5507
|
seedStarterGraphAssets,
|
|
5508
|
+
serverPublicRoutes,
|
|
5509
|
+
setServerPublicRoutes,
|
|
3288
5510
|
setWorkingWorkspaceConfig,
|
|
3289
5511
|
srklListBackblazeBuckets2 as srklCoreListBackblazeBuckets,
|
|
3290
5512
|
srklListBackblazeBuckets,
|
|
3291
|
-
syncPrimaryToClones,
|
|
3292
5513
|
syncWorkspace,
|
|
5514
|
+
syncWorkspaceCreateToBucket,
|
|
5515
|
+
syncWorkspaceDataToBucket,
|
|
5516
|
+
syncWorkspaceDeleteToBucket,
|
|
5517
|
+
updateCredential,
|
|
5518
|
+
updateWorkspace,
|
|
5519
|
+
upsertRoute,
|
|
3293
5520
|
useBackend,
|
|
3294
5521
|
useBackendClient,
|
|
3295
5522
|
useSrklServices,
|
|
3296
5523
|
validateServiceDefinition,
|
|
3297
5524
|
withAuth,
|
|
3298
|
-
withCors
|
|
5525
|
+
withCors,
|
|
5526
|
+
writeWorkspaceFile
|
|
3299
5527
|
};
|