@surf-ai/sdk 0.1.6-beta → 1.0.0-alpha.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/README.md +86 -375
- package/dist/db/index.cjs +216 -39
- package/dist/db/index.d.cts +37 -3
- package/dist/db/index.d.ts +37 -3
- package/dist/db/index.js +210 -38
- package/dist/server/index.cjs +250 -268
- package/dist/server/index.d.cts +9 -15
- package/dist/server/index.d.ts +9 -15
- package/dist/server/index.js +269 -195
- package/package.json +6 -29
- package/dist/chunk-J4OMYO3F.js +0 -70
- package/dist/client-3YMIRPDV.js +0 -8
- package/dist/react/index.d.ts +0 -3650
- package/dist/react/index.js +0 -1175
- package/src/theme/index.css +0 -314
package/dist/server/index.cjs
CHANGED
|
@@ -5,9 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __esm = (fn, res) => function __init() {
|
|
9
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
-
};
|
|
11
8
|
var __export = (target, all) => {
|
|
12
9
|
for (var name in all)
|
|
13
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -30,32 +27,70 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
27
|
));
|
|
31
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
29
|
|
|
33
|
-
// src/
|
|
34
|
-
var
|
|
35
|
-
__export(
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
// src/server/index.ts
|
|
31
|
+
var server_exports = {};
|
|
32
|
+
__export(server_exports, {
|
|
33
|
+
createServer: () => createServer,
|
|
34
|
+
dataApi: () => dataApi
|
|
38
35
|
});
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
module.exports = __toCommonJS(server_exports);
|
|
37
|
+
|
|
38
|
+
// src/server/runtime.ts
|
|
39
|
+
var import_express = __toESM(require("express"), 1);
|
|
40
|
+
var import_cors = __toESM(require("cors"), 1);
|
|
41
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
42
|
+
var import_path = __toESM(require("path"), 1);
|
|
43
|
+
var import_croner = require("croner");
|
|
44
|
+
|
|
45
|
+
// src/core/config.ts
|
|
46
|
+
var DEFAULT_API_BASE_URL = "https://api.ask.surf/gateway/v1";
|
|
47
|
+
function trimTrailingSlashes(value) {
|
|
48
|
+
return String(value || "").replace(/\/+$/, "");
|
|
41
49
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
headers: { Authorization: `Bearer ${appToken}` }
|
|
53
|
-
};
|
|
50
|
+
function readSurfApiConfig() {
|
|
51
|
+
return {
|
|
52
|
+
baseUrl: trimTrailingSlashes(process.env.SURF_API_BASE_URL || DEFAULT_API_BASE_URL),
|
|
53
|
+
apiKey: process.env.SURF_API_KEY
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function requireSurfApiConfig() {
|
|
57
|
+
const config = readSurfApiConfig();
|
|
58
|
+
if (!config.apiKey) {
|
|
59
|
+
throw new Error("SURF_API_KEY is required");
|
|
54
60
|
}
|
|
55
|
-
return { baseUrl:
|
|
61
|
+
return { baseUrl: config.baseUrl, apiKey: config.apiKey };
|
|
62
|
+
}
|
|
63
|
+
function readAdminApiKey() {
|
|
64
|
+
return process.env.SURF_API_KEY;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/db/schema-sync.ts
|
|
68
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
69
|
+
|
|
70
|
+
// src/core/transport.ts
|
|
71
|
+
function sleep(ms) {
|
|
72
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
73
|
}
|
|
57
74
|
function normalizePath(path2) {
|
|
58
|
-
return String(path2 || "").replace(/^\/+/, "")
|
|
75
|
+
return String(path2 || "").replace(/^\/+/, "");
|
|
76
|
+
}
|
|
77
|
+
function buildUrl(path2, params) {
|
|
78
|
+
const { baseUrl } = requireSurfApiConfig();
|
|
79
|
+
const url = new URL(`${baseUrl}/${normalizePath(path2)}`);
|
|
80
|
+
if (params) {
|
|
81
|
+
for (const [key, value] of Object.entries(params)) {
|
|
82
|
+
if (value != null) {
|
|
83
|
+
url.searchParams.set(key, String(value));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return url.toString();
|
|
88
|
+
}
|
|
89
|
+
function buildHeaders(extra) {
|
|
90
|
+
const { apiKey } = requireSurfApiConfig();
|
|
91
|
+
const headers = new Headers(extra);
|
|
92
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
93
|
+
return headers;
|
|
59
94
|
}
|
|
60
95
|
async function fetchJson(url, init, retries = 1) {
|
|
61
96
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
@@ -65,80 +100,211 @@ async function fetchJson(url, init, retries = 1) {
|
|
|
65
100
|
throw new Error(`API error ${res.status}: ${text2.slice(0, 200)}`);
|
|
66
101
|
}
|
|
67
102
|
const text = await res.text();
|
|
68
|
-
if (text)
|
|
69
|
-
|
|
103
|
+
if (text) {
|
|
104
|
+
return JSON.parse(text);
|
|
105
|
+
}
|
|
106
|
+
if (attempt < retries) {
|
|
107
|
+
await sleep(1e3);
|
|
108
|
+
}
|
|
70
109
|
}
|
|
71
110
|
throw new Error(`Empty response from ${url}`);
|
|
72
111
|
}
|
|
73
|
-
async function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (params) {
|
|
77
|
-
for (const [k, v] of Object.entries(params)) {
|
|
78
|
-
if (v != null) cleaned[k] = String(v);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const qs = Object.keys(cleaned).length ? "?" + new URLSearchParams(cleaned).toString() : "";
|
|
82
|
-
return fetchJson(`${config.baseUrl}/${normalizePath(path2)}${qs}`, {
|
|
83
|
-
headers: config.headers
|
|
112
|
+
async function getJson(path2, params) {
|
|
113
|
+
return fetchJson(buildUrl(path2, params), {
|
|
114
|
+
headers: buildHeaders()
|
|
84
115
|
});
|
|
85
116
|
}
|
|
86
|
-
async function
|
|
87
|
-
|
|
88
|
-
return fetchJson(`${config.baseUrl}/${normalizePath(path2)}`, {
|
|
117
|
+
async function postJson(path2, body) {
|
|
118
|
+
return fetchJson(buildUrl(path2), {
|
|
89
119
|
method: "POST",
|
|
90
|
-
headers: {
|
|
120
|
+
headers: buildHeaders({
|
|
121
|
+
"Content-Type": "application/json"
|
|
122
|
+
}),
|
|
91
123
|
body: body ? JSON.stringify(body) : void 0
|
|
92
124
|
});
|
|
93
125
|
}
|
|
94
|
-
var DEFAULT_PUBLIC_URL;
|
|
95
|
-
var init_client = __esm({
|
|
96
|
-
"src/data/client.ts"() {
|
|
97
|
-
"use strict";
|
|
98
|
-
DEFAULT_PUBLIC_URL = "https://api.ask.surf/gateway/v1";
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
126
|
|
|
102
|
-
// src/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
// src/data/client.ts
|
|
128
|
+
async function get(path2, params) {
|
|
129
|
+
return getJson(path2, params);
|
|
130
|
+
}
|
|
131
|
+
async function post(path2, body) {
|
|
132
|
+
return postJson(path2, body);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/db/schema-sync.ts
|
|
136
|
+
var syncing = false;
|
|
137
|
+
async function syncSchema(options) {
|
|
138
|
+
const { schemaPath, retries = 3, retryDelay = 2e3 } = options;
|
|
139
|
+
for (let i = 0; i < retries; i++) {
|
|
140
|
+
try {
|
|
141
|
+
await doSyncSchema(schemaPath);
|
|
142
|
+
return;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error(`DB schema sync attempt ${i + 1}/${retries} failed: ${err.message}`);
|
|
145
|
+
if (i < retries - 1) await new Promise((r) => setTimeout(r, retryDelay * (i + 1)));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
console.error("DB schema sync failed after all retries");
|
|
149
|
+
}
|
|
150
|
+
async function doSyncSchema(schemaPath) {
|
|
151
|
+
if (syncing) return;
|
|
152
|
+
syncing = true;
|
|
153
|
+
try {
|
|
154
|
+
if (!import_fs.default.existsSync(schemaPath)) return;
|
|
155
|
+
let schema;
|
|
156
|
+
if (schemaPath.endsWith(".ts")) {
|
|
157
|
+
try {
|
|
158
|
+
schema = await import(`${schemaPath}?t=${Date.now()}`);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (err instanceof SyntaxError) {
|
|
161
|
+
console.log("DB: schema file has syntax error, waiting for next change...");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (err.message.includes("Cannot find module") || err.message.includes("is not a function")) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
try {
|
|
171
|
+
delete require.cache[require.resolve(schemaPath)];
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
schema = require(schemaPath);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof SyntaxError) {
|
|
178
|
+
console.log("DB: schema file has syntax error, waiting for next change...");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (err.message.includes("Cannot find module") || err.message.includes("is not a function")) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let getTableConfig;
|
|
188
|
+
try {
|
|
189
|
+
getTableConfig = require("drizzle-orm/pg-core").getTableConfig;
|
|
190
|
+
} catch {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const tables = Object.values(schema).filter(
|
|
194
|
+
(t) => t && typeof t === "object" && /* @__PURE__ */ Symbol.for("drizzle:Name") in t
|
|
195
|
+
);
|
|
196
|
+
if (tables.length === 0) return;
|
|
197
|
+
await post("db/provision", {});
|
|
198
|
+
const existing = (await get("db/tables")).map((t) => t.name);
|
|
199
|
+
const missing = tables.filter((t) => !existing.includes(getTableConfig(t).name));
|
|
200
|
+
if (missing.length > 0) {
|
|
201
|
+
const { generateDrizzleJson, generateMigration } = require("drizzle-kit/api");
|
|
202
|
+
const missingSchema = {};
|
|
203
|
+
for (const t of missing) missingSchema[getTableConfig(t).name] = t;
|
|
204
|
+
const sqls = await generateMigration(generateDrizzleJson({}), generateDrizzleJson(missingSchema));
|
|
205
|
+
for (const sql of sqls) {
|
|
206
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
207
|
+
try {
|
|
208
|
+
await post("db/query", { sql, params: [] });
|
|
209
|
+
console.log(`DB: Executed: ${sql.slice(0, 80)}...`);
|
|
210
|
+
break;
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (attempt === 0) {
|
|
213
|
+
console.warn(`DB: Retrying after: ${err.message}`);
|
|
214
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
215
|
+
} else {
|
|
216
|
+
console.error(`DB: Failed: ${sql.slice(0, 80)}... \u2014 ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const existingTables = tables.filter((t) => existing.includes(getTableConfig(t).name));
|
|
223
|
+
for (const t of existingTables) {
|
|
224
|
+
const cfg = getTableConfig(t);
|
|
225
|
+
try {
|
|
226
|
+
const live = await get("db/table-schema", { table: cfg.name });
|
|
227
|
+
const liveCols = new Set((live.columns || []).map((c) => c.name));
|
|
228
|
+
for (const col of cfg.columns) {
|
|
229
|
+
if (!liveCols.has(col.name)) {
|
|
230
|
+
const colType = col.getSQLType();
|
|
231
|
+
const ddl = `ALTER TABLE "${cfg.name}" ADD COLUMN IF NOT EXISTS "${col.name}" ${colType}`;
|
|
232
|
+
try {
|
|
233
|
+
await post("db/query", { sql: ddl, params: [] });
|
|
234
|
+
console.log(`DB: Added column ${col.name} to ${cfg.name}`);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.warn(`DB: Failed to add column ${col.name} to ${cfg.name}: ${err.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.warn(`DB: Column check failed for ${cfg.name}: ${err.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
syncing = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function watchSchema(schemaPath, options) {
|
|
249
|
+
const { debounceMs = 1e3, retries = 2, retryDelay = 1500 } = options || {};
|
|
250
|
+
if (!import_fs.default.existsSync(schemaPath)) return () => {
|
|
251
|
+
};
|
|
252
|
+
let debounce = null;
|
|
253
|
+
import_fs.default.watchFile(schemaPath, { interval: 2e3 }, () => {
|
|
254
|
+
if (debounce) clearTimeout(debounce);
|
|
255
|
+
debounce = setTimeout(async () => {
|
|
256
|
+
console.log("DB: schema file changed, re-syncing tables...");
|
|
257
|
+
try {
|
|
258
|
+
await syncSchema({ schemaPath, retries, retryDelay });
|
|
259
|
+
console.log("DB: schema re-sync complete");
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(`DB: schema re-sync failed: ${err.message}`);
|
|
262
|
+
}
|
|
263
|
+
}, debounceMs);
|
|
264
|
+
});
|
|
265
|
+
return () => {
|
|
266
|
+
import_fs.default.unwatchFile(schemaPath);
|
|
267
|
+
if (debounce) clearTimeout(debounce);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
109
270
|
|
|
110
271
|
// src/server/runtime.ts
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
272
|
+
function requireBearerAuth(req, res, next) {
|
|
273
|
+
const apiKey = readAdminApiKey();
|
|
274
|
+
if (!apiKey) {
|
|
275
|
+
return res.status(503).json({ error: "SURF_API_KEY is not configured" });
|
|
276
|
+
}
|
|
277
|
+
if (req.headers.authorization !== `Bearer ${apiKey}`) {
|
|
278
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
279
|
+
}
|
|
280
|
+
next();
|
|
281
|
+
}
|
|
117
282
|
function createServer(options = {}) {
|
|
118
|
-
const
|
|
283
|
+
const rawPort = process.env.BACKEND_PORT;
|
|
284
|
+
const port = options.port ?? (rawPort ? Number.parseInt(rawPort, 10) : void 0);
|
|
285
|
+
if (!Number.isInteger(port)) {
|
|
286
|
+
throw new Error("createServer requires a port via options.port or BACKEND_PORT env var");
|
|
287
|
+
}
|
|
119
288
|
const routesDir = options.routesDir || import_path.default.join(process.cwd(), "routes");
|
|
120
289
|
const cronDir = options.cronDir || process.cwd();
|
|
121
|
-
const enableProxy = options.proxy !== false;
|
|
122
290
|
const app = (0, import_express.default)();
|
|
123
291
|
app.use((0, import_cors.default)());
|
|
124
|
-
if (enableProxy) {
|
|
125
|
-
setupProxy(app, port);
|
|
126
|
-
}
|
|
127
292
|
app.use(import_express.default.json());
|
|
128
293
|
app.get("/api/health", (_req, res) => {
|
|
129
294
|
res.json({ status: "ok" });
|
|
130
295
|
});
|
|
131
|
-
if (
|
|
132
|
-
for (const file of
|
|
296
|
+
if (import_fs2.default.existsSync(routesDir)) {
|
|
297
|
+
for (const file of import_fs2.default.readdirSync(routesDir)) {
|
|
133
298
|
if (!file.endsWith(".js") && !file.endsWith(".ts")) continue;
|
|
134
299
|
const name = file.replace(/\.(js|ts)$/, "");
|
|
135
300
|
try {
|
|
136
|
-
const
|
|
137
|
-
const handler = route.default || route;
|
|
301
|
+
const handler = require(import_path.default.join(routesDir, file));
|
|
138
302
|
if (typeof handler === "function") {
|
|
139
303
|
app.use(`/api/${name}`, handler);
|
|
140
304
|
console.log(`Route registered: /api/${name}`);
|
|
305
|
+
continue;
|
|
141
306
|
}
|
|
307
|
+
throw new Error(`Route module must export a handler function via module.exports`);
|
|
142
308
|
} catch (err) {
|
|
143
309
|
console.error(`Failed to load route ${file}: ${err.message}`);
|
|
144
310
|
}
|
|
@@ -162,157 +328,15 @@ function createServer(options = {}) {
|
|
|
162
328
|
}
|
|
163
329
|
};
|
|
164
330
|
}
|
|
165
|
-
function env2(surfName, legacyName) {
|
|
166
|
-
return process.env[surfName] || process.env[legacyName];
|
|
167
|
-
}
|
|
168
|
-
function setupProxy(app, port) {
|
|
169
|
-
const gatewayUrl = env2("SURF_DEPLOYED_GATEWAY_URL", "GATEWAY_URL");
|
|
170
|
-
const appToken = env2("SURF_DEPLOYED_APP_TOKEN", "APP_TOKEN");
|
|
171
|
-
const proxyBase = env2("SURF_SANDBOX_PROXY_BASE", "DATA_PROXY_BASE");
|
|
172
|
-
const isDeployed = Boolean(gatewayUrl && appToken);
|
|
173
|
-
const bufferResponse = (0, import_http_proxy_middleware.responseInterceptor)(async (buf) => buf);
|
|
174
|
-
if (isDeployed) {
|
|
175
|
-
app.use("/proxy", (0, import_http_proxy_middleware.createProxyMiddleware)({
|
|
176
|
-
target: gatewayUrl,
|
|
177
|
-
changeOrigin: true,
|
|
178
|
-
selfHandleResponse: true,
|
|
179
|
-
pathRewrite: (p) => "/gateway/v1" + p,
|
|
180
|
-
headers: {
|
|
181
|
-
Authorization: `Bearer ${appToken}`,
|
|
182
|
-
"Accept-Encoding": "identity"
|
|
183
|
-
},
|
|
184
|
-
on: { proxyRes: bufferResponse }
|
|
185
|
-
}));
|
|
186
|
-
const loopback = `http://127.0.0.1:${port}/proxy`;
|
|
187
|
-
process.env.SURF_SANDBOX_PROXY_BASE = loopback;
|
|
188
|
-
process.env.DATA_PROXY_BASE = loopback;
|
|
189
|
-
} else if (proxyBase) {
|
|
190
|
-
const target = proxyBase.replace(/\/proxy$/, "");
|
|
191
|
-
app.use((0, import_http_proxy_middleware.createProxyMiddleware)({
|
|
192
|
-
target,
|
|
193
|
-
changeOrigin: true,
|
|
194
|
-
pathFilter: "/proxy",
|
|
195
|
-
on: {
|
|
196
|
-
proxyReq: (proxyReq, req) => {
|
|
197
|
-
console.log(`[proxy] >> ${req.method} ${req.originalUrl}`);
|
|
198
|
-
},
|
|
199
|
-
proxyRes: (proxyRes, req) => {
|
|
200
|
-
console.log(`[proxy] << ${proxyRes.statusCode} ${req.method} ${req.originalUrl}`);
|
|
201
|
-
},
|
|
202
|
-
error: (err, req, res) => {
|
|
203
|
-
console.error(`[proxy] !! ${req.method} ${req.originalUrl} error: ${err.message}`);
|
|
204
|
-
if (!res.headersSent) res.status(502).json({ error: err.message });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}));
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
331
|
var schemaSync = { run: async () => {
|
|
211
332
|
}, watch: () => {
|
|
212
333
|
} };
|
|
213
334
|
function setupSchemaSync(app, schemaDir) {
|
|
214
|
-
let syncing = false;
|
|
215
335
|
let schemaReady = false;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
syncing = true;
|
|
219
|
-
try {
|
|
220
|
-
const schemaPath = import_path.default.join(schemaDir, "schema.js");
|
|
221
|
-
if (!import_fs.default.existsSync(schemaPath)) return;
|
|
222
|
-
try {
|
|
223
|
-
delete require.cache[require.resolve(schemaPath)];
|
|
224
|
-
} catch {
|
|
225
|
-
}
|
|
226
|
-
let schema;
|
|
227
|
-
try {
|
|
228
|
-
schema = require(schemaPath);
|
|
229
|
-
} catch (err) {
|
|
230
|
-
if (err instanceof SyntaxError) {
|
|
231
|
-
console.log("DB: schema.js has syntax error, waiting for next change...");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
if (err.message.includes("Cannot find module") || err.message.includes("is not a function")) {
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
throw err;
|
|
238
|
-
}
|
|
239
|
-
let getTableConfig;
|
|
240
|
-
try {
|
|
241
|
-
getTableConfig = require("drizzle-orm/pg-core").getTableConfig;
|
|
242
|
-
} catch {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
const tables = Object.values(schema).filter(
|
|
246
|
-
(t) => t && typeof t === "object" && /* @__PURE__ */ Symbol.for("drizzle:Name") in t
|
|
247
|
-
);
|
|
248
|
-
if (tables.length === 0) return;
|
|
249
|
-
const { get: dbGet, post: dbPost } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
250
|
-
await dbPost("db/provision");
|
|
251
|
-
const existing = (await dbGet("db/tables")).map((t) => t.name);
|
|
252
|
-
const missing = tables.filter((t) => !existing.includes(getTableConfig(t).name));
|
|
253
|
-
if (missing.length > 0) {
|
|
254
|
-
const { generateDrizzleJson, generateMigration } = require("drizzle-kit/api");
|
|
255
|
-
const missingSchema = {};
|
|
256
|
-
for (const t of missing) missingSchema[getTableConfig(t).name] = t;
|
|
257
|
-
const sqls = await generateMigration(generateDrizzleJson({}), generateDrizzleJson(missingSchema));
|
|
258
|
-
for (const sql of sqls) {
|
|
259
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
260
|
-
try {
|
|
261
|
-
await dbPost("db/query", { sql, params: [] });
|
|
262
|
-
console.log(`DB: Executed: ${sql.slice(0, 80)}...`);
|
|
263
|
-
break;
|
|
264
|
-
} catch (err) {
|
|
265
|
-
if (attempt === 0) {
|
|
266
|
-
console.warn(`DB: Retrying after: ${err.message}`);
|
|
267
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
268
|
-
} else {
|
|
269
|
-
console.error(`DB: Failed: ${sql.slice(0, 80)}... \u2014 ${err.message}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const existingTables = tables.filter((t) => existing.includes(getTableConfig(t).name));
|
|
276
|
-
for (const t of existingTables) {
|
|
277
|
-
const cfg = getTableConfig(t);
|
|
278
|
-
try {
|
|
279
|
-
const live = await dbGet("db/table-schema", { table: cfg.name });
|
|
280
|
-
const liveCols = new Set((live.columns || []).map((c) => c.name));
|
|
281
|
-
for (const col of cfg.columns) {
|
|
282
|
-
if (!liveCols.has(col.name)) {
|
|
283
|
-
const colType = col.getSQLType();
|
|
284
|
-
const ddl = `ALTER TABLE "${cfg.name}" ADD COLUMN IF NOT EXISTS "${col.name}" ${colType}`;
|
|
285
|
-
try {
|
|
286
|
-
await dbPost("db/query", { sql: ddl, params: [] });
|
|
287
|
-
console.log(`DB: Added column ${col.name} to ${cfg.name}`);
|
|
288
|
-
} catch (err) {
|
|
289
|
-
console.warn(`DB: Failed to add column ${col.name} to ${cfg.name}: ${err.message}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
} catch (err) {
|
|
294
|
-
console.warn(`DB: Column check failed for ${cfg.name}: ${err.message}`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
} finally {
|
|
298
|
-
syncing = false;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
async function syncWithRetry(retries = 3, delay = 2e3) {
|
|
302
|
-
for (let i = 0; i < retries; i++) {
|
|
303
|
-
try {
|
|
304
|
-
await doSyncSchema();
|
|
305
|
-
return;
|
|
306
|
-
} catch (err) {
|
|
307
|
-
console.error(`DB schema sync attempt ${i + 1}/${retries} failed: ${err.message}`);
|
|
308
|
-
if (i < retries - 1) await new Promise((r) => setTimeout(r, delay * (i + 1)));
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
console.error("DB schema sync failed after all retries");
|
|
312
|
-
}
|
|
313
|
-
app.post("/api/__sync-schema", async (_req, res) => {
|
|
336
|
+
const schemaPath = import_path.default.join(schemaDir, "schema.js");
|
|
337
|
+
app.post("/api/__sync-schema", requireBearerAuth, async (_req, res) => {
|
|
314
338
|
try {
|
|
315
|
-
await
|
|
339
|
+
await syncSchema({ schemaPath, retries: 2, retryDelay: 1500 });
|
|
316
340
|
res.json({ ok: true });
|
|
317
341
|
} catch (err) {
|
|
318
342
|
res.status(500).json({ ok: false, error: err.message });
|
|
@@ -324,7 +348,7 @@ function setupSchemaSync(app, schemaDir) {
|
|
|
324
348
|
});
|
|
325
349
|
schemaSync.run = async () => {
|
|
326
350
|
try {
|
|
327
|
-
await
|
|
351
|
+
await syncSchema({ schemaPath });
|
|
328
352
|
schemaReady = true;
|
|
329
353
|
console.log("Schema sync complete, API ready");
|
|
330
354
|
} catch {
|
|
@@ -333,21 +357,7 @@ function setupSchemaSync(app, schemaDir) {
|
|
|
333
357
|
}
|
|
334
358
|
};
|
|
335
359
|
schemaSync.watch = () => {
|
|
336
|
-
|
|
337
|
-
if (!import_fs.default.existsSync(schemaPath)) return;
|
|
338
|
-
let debounce = null;
|
|
339
|
-
import_fs.default.watchFile(schemaPath, { interval: 2e3 }, () => {
|
|
340
|
-
if (debounce) clearTimeout(debounce);
|
|
341
|
-
debounce = setTimeout(async () => {
|
|
342
|
-
console.log("DB: schema.js changed, re-syncing tables...");
|
|
343
|
-
try {
|
|
344
|
-
await syncWithRetry(2, 1500);
|
|
345
|
-
console.log("DB: schema re-sync complete");
|
|
346
|
-
} catch (err) {
|
|
347
|
-
console.error(`DB: schema re-sync failed: ${err.message}`);
|
|
348
|
-
}
|
|
349
|
-
}, 1e3);
|
|
350
|
-
});
|
|
360
|
+
watchSchema(schemaPath);
|
|
351
361
|
};
|
|
352
362
|
}
|
|
353
363
|
function setupCron(app, cronDir) {
|
|
@@ -363,10 +373,10 @@ function setupCron(app, cronDir) {
|
|
|
363
373
|
}
|
|
364
374
|
cronJobs.clear();
|
|
365
375
|
const cronPath = import_path.default.join(cronDir, "cron.json");
|
|
366
|
-
if (!
|
|
376
|
+
if (!import_fs2.default.existsSync(cronPath)) return;
|
|
367
377
|
let tasks;
|
|
368
378
|
try {
|
|
369
|
-
tasks = JSON.parse(
|
|
379
|
+
tasks = JSON.parse(import_fs2.default.readFileSync(cronPath, "utf-8"));
|
|
370
380
|
} catch (e) {
|
|
371
381
|
console.error("Failed to parse cron.json:", e.message);
|
|
372
382
|
return;
|
|
@@ -414,16 +424,7 @@ function setupCron(app, cronDir) {
|
|
|
414
424
|
console.log(`Cron registered: [${task.id}] ${task.name} (${task.schedule})`);
|
|
415
425
|
}
|
|
416
426
|
}
|
|
417
|
-
|
|
418
|
-
app.use("/api/cron", (req, res, next) => {
|
|
419
|
-
const appToken = envFn("SURF_DEPLOYED_APP_TOKEN", "APP_TOKEN");
|
|
420
|
-
if (!appToken) return next();
|
|
421
|
-
const auth = req.headers.authorization;
|
|
422
|
-
if (!auth || auth !== `Bearer ${appToken}`) {
|
|
423
|
-
return res.status(401).json({ error: "Unauthorized" });
|
|
424
|
-
}
|
|
425
|
-
next();
|
|
426
|
-
});
|
|
427
|
+
app.use("/api/cron", requireBearerAuth);
|
|
427
428
|
app.get("/api/cron", (_req, res) => {
|
|
428
429
|
res.json(cronTasks.map((t) => {
|
|
429
430
|
const state = cronState.get(t.id) || { lastRunAt: null, lastStatus: null, lastError: null };
|
|
@@ -456,7 +457,7 @@ function setupCron(app, cronDir) {
|
|
|
456
457
|
const cronPath = import_path.default.join(cronDir, "cron.json");
|
|
457
458
|
let tasks = [];
|
|
458
459
|
try {
|
|
459
|
-
if (
|
|
460
|
+
if (import_fs2.default.existsSync(cronPath)) tasks = JSON.parse(import_fs2.default.readFileSync(cronPath, "utf8"));
|
|
460
461
|
} catch {
|
|
461
462
|
tasks = [];
|
|
462
463
|
}
|
|
@@ -465,7 +466,7 @@ function setupCron(app, cronDir) {
|
|
|
465
466
|
}
|
|
466
467
|
const newTask = { id, name, schedule, handler, enabled, timeout };
|
|
467
468
|
tasks.push(newTask);
|
|
468
|
-
|
|
469
|
+
import_fs2.default.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
|
|
469
470
|
loadCronJobs();
|
|
470
471
|
res.status(201).json(newTask);
|
|
471
472
|
});
|
|
@@ -473,7 +474,7 @@ function setupCron(app, cronDir) {
|
|
|
473
474
|
const cronPath = import_path.default.join(cronDir, "cron.json");
|
|
474
475
|
let tasks = [];
|
|
475
476
|
try {
|
|
476
|
-
if (
|
|
477
|
+
if (import_fs2.default.existsSync(cronPath)) tasks = JSON.parse(import_fs2.default.readFileSync(cronPath, "utf8"));
|
|
477
478
|
} catch {
|
|
478
479
|
tasks = [];
|
|
479
480
|
}
|
|
@@ -494,7 +495,7 @@ function setupCron(app, cronDir) {
|
|
|
494
495
|
}
|
|
495
496
|
}
|
|
496
497
|
tasks[idx] = { ...tasks[idx], ...updates, id: req.params.id };
|
|
497
|
-
|
|
498
|
+
import_fs2.default.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
|
|
498
499
|
loadCronJobs();
|
|
499
500
|
res.json(tasks[idx]);
|
|
500
501
|
});
|
|
@@ -502,14 +503,14 @@ function setupCron(app, cronDir) {
|
|
|
502
503
|
const cronPath = import_path.default.join(cronDir, "cron.json");
|
|
503
504
|
let tasks = [];
|
|
504
505
|
try {
|
|
505
|
-
if (
|
|
506
|
+
if (import_fs2.default.existsSync(cronPath)) tasks = JSON.parse(import_fs2.default.readFileSync(cronPath, "utf8"));
|
|
506
507
|
} catch {
|
|
507
508
|
tasks = [];
|
|
508
509
|
}
|
|
509
510
|
const idx = tasks.findIndex((t) => t.id === req.params.id);
|
|
510
511
|
if (idx === -1) return res.status(404).json({ error: "Task not found" });
|
|
511
512
|
tasks.splice(idx, 1);
|
|
512
|
-
|
|
513
|
+
import_fs2.default.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
|
|
513
514
|
cronState.delete(req.params.id);
|
|
514
515
|
loadCronJobs();
|
|
515
516
|
res.json({ ok: true });
|
|
@@ -527,11 +528,7 @@ function setupCron(app, cronDir) {
|
|
|
527
528
|
loadCronJobs();
|
|
528
529
|
}
|
|
529
530
|
|
|
530
|
-
// src/data/data-api.ts
|
|
531
|
-
init_client();
|
|
532
|
-
|
|
533
531
|
// src/data/categories/exchange.ts
|
|
534
|
-
init_client();
|
|
535
532
|
var exchange = {
|
|
536
533
|
/** Returns order book bid/ask levels with computed stats. **Included fields:** spread, spread percentage, mid-price, and total bid/ask depth. Use `limit` to control the number of price levels (1–100, default 20). Set `type=swap` to query perpetual contract order books instead of spot. */
|
|
537
534
|
depth: (params) => get("exchange/depth", params),
|
|
@@ -550,7 +547,6 @@ var exchange = {
|
|
|
550
547
|
};
|
|
551
548
|
|
|
552
549
|
// src/data/categories/fund.ts
|
|
553
|
-
init_client();
|
|
554
550
|
var fund = {
|
|
555
551
|
/** Returns a fund's **profile metadata**. **Included fields:** X accounts, team members, recent research, invested project count. This does NOT return the list of investments — use `/fund/portfolio` for that. **Lookup:** by UUID (`id`) or name (`q`). Returns 404 if not found. */
|
|
556
552
|
detail: (params) => get("fund/detail", params),
|
|
@@ -561,7 +557,6 @@ var fund = {
|
|
|
561
557
|
};
|
|
562
558
|
|
|
563
559
|
// src/data/categories/kalshi.ts
|
|
564
|
-
init_client();
|
|
565
560
|
var kalshi = {
|
|
566
561
|
/** Returns Kalshi events with nested markets, optionally filtered by `event_ticker`. Each event includes market count and a list of markets. **Data refresh:** ~30 minutes */
|
|
567
562
|
events: (params) => get("prediction-market/kalshi/events", params),
|
|
@@ -580,7 +575,6 @@ var kalshi = {
|
|
|
580
575
|
};
|
|
581
576
|
|
|
582
577
|
// src/data/categories/market.ts
|
|
583
|
-
init_client();
|
|
584
578
|
var market = {
|
|
585
579
|
/** Returns daily ETF flow history for US spot ETFs. **Included fields:** net flow (USD), token price, per-ticker breakdown. Sorted by date descending. `symbol`: `BTC` or `ETH`. */
|
|
586
580
|
etf: (params) => get("market/etf", params),
|
|
@@ -607,7 +601,6 @@ var market = {
|
|
|
607
601
|
};
|
|
608
602
|
|
|
609
603
|
// src/data/categories/matching.ts
|
|
610
|
-
init_client();
|
|
611
604
|
var matching = {
|
|
612
605
|
/** Returns daily volume and open interest comparison for a specific matched market pair. Both `polymarket_condition_id` and `kalshi_market_ticker` are required. **Volume units:** Polymarket = USD, Kalshi = contracts. */
|
|
613
606
|
market_daily: (params) => get("prediction-market/matching/daily", params),
|
|
@@ -618,7 +611,6 @@ var matching = {
|
|
|
618
611
|
};
|
|
619
612
|
|
|
620
613
|
// src/data/categories/news.ts
|
|
621
|
-
init_client();
|
|
622
614
|
var news = {
|
|
623
615
|
/** Returns the full content of a single news article by its ID (returned as `id` in feed and search results). */
|
|
624
616
|
detail: (params) => get("news/detail", params),
|
|
@@ -627,7 +619,6 @@ var news = {
|
|
|
627
619
|
};
|
|
628
620
|
|
|
629
621
|
// src/data/categories/onchain.ts
|
|
630
|
-
init_client();
|
|
631
622
|
var onchain = {
|
|
632
623
|
/** Returns bridge protocols ranked by total USD volume over a specified time range. */
|
|
633
624
|
bridge_ranking: (params) => get("onchain/bridge/ranking", params),
|
|
@@ -646,7 +637,6 @@ var onchain = {
|
|
|
646
637
|
};
|
|
647
638
|
|
|
648
639
|
// src/data/categories/polymarket.ts
|
|
649
|
-
init_client();
|
|
650
640
|
var polymarket = {
|
|
651
641
|
/** Returns trade and redemption activity for a Polymarket wallet. **Data refresh:** ~30 minutes */
|
|
652
642
|
activity: (params) => get("prediction-market/polymarket/activity", params),
|
|
@@ -669,14 +659,12 @@ var polymarket = {
|
|
|
669
659
|
};
|
|
670
660
|
|
|
671
661
|
// src/data/categories/prediction_market.ts
|
|
672
|
-
init_client();
|
|
673
662
|
var prediction_market = {
|
|
674
663
|
/** Returns daily notional volume and open interest aggregated by category across Kalshi and Polymarket. **Filters:** `source` or `category`. **Data refresh:** daily */
|
|
675
664
|
category_metrics: (params) => get("prediction-market/category-metrics", params)
|
|
676
665
|
};
|
|
677
666
|
|
|
678
667
|
// src/data/categories/project.ts
|
|
679
|
-
init_client();
|
|
680
668
|
var project = {
|
|
681
669
|
/** Returns time-series DeFi metrics for a project. **Available metrics:** `volume`, `fee`, `fees`, `revenue`, `tvl`, `users`. **Lookup:** by UUID (`id`) or name (`q`). Filter by `chain` and date range (`from`/`to`). Returns 404 if the project is not found. **Note:** this endpoint only returns data for DeFi protocol projects (e.g. `aave`, `uniswap`, `lido`, `makerdao`). Use `q` with a DeFi protocol name. */
|
|
682
670
|
defi_metrics: (params) => get("project/defi/metrics", params),
|
|
@@ -687,7 +675,6 @@ var project = {
|
|
|
687
675
|
};
|
|
688
676
|
|
|
689
677
|
// src/data/categories/search.ts
|
|
690
|
-
init_client();
|
|
691
678
|
var search = {
|
|
692
679
|
/** Searches and filters airdrop opportunities. **Filters:** keyword, status, reward type, task type. Returns paginated results with optional task details. */
|
|
693
680
|
airdrop: (params) => get("search/airdrop", params),
|
|
@@ -714,7 +701,6 @@ var search = {
|
|
|
714
701
|
};
|
|
715
702
|
|
|
716
703
|
// src/data/categories/social.ts
|
|
717
|
-
init_client();
|
|
718
704
|
var social = {
|
|
719
705
|
/** Returns a **point-in-time snapshot** of social analytics for a project. **Available fields** (via `fields`): `sentiment`, `follower_geo`, `smart_followers`. **Lookup:** by X account ID (`x_id`) or project name (`q`, e.g. `uniswap`, `solana`). The `q` parameter must be a crypto project name, not a personal Twitter handle. Returns 404 if the project has no linked Twitter account. For sentiment **trends over time**, use `/social/mindshare` instead. */
|
|
720
706
|
detail: (params) => get("social/detail", params),
|
|
@@ -741,7 +727,6 @@ var social = {
|
|
|
741
727
|
};
|
|
742
728
|
|
|
743
729
|
// src/data/categories/token.ts
|
|
744
|
-
init_client();
|
|
745
730
|
var token = {
|
|
746
731
|
/** Returns recent DEX swap events for a token contract address. **Covered DEXes:** `uniswap`, `sushiswap`, `curve`, `balancer`. **Included fields:** trading pair, amounts, USD value, taker address. **Data refresh:** ~24 hours · **Chains:** Ethereum, Base */
|
|
747
732
|
dex_trades: (params) => get("token/dex-trades", params),
|
|
@@ -754,7 +739,6 @@ var token = {
|
|
|
754
739
|
};
|
|
755
740
|
|
|
756
741
|
// src/data/categories/v2.ts
|
|
757
|
-
init_client();
|
|
758
742
|
var v2 = {
|
|
759
743
|
/** Historical orderbook snapshots for a Kalshi market. Returns yes-side bid/ask levels. Timestamps in milliseconds, prices in cents. Data refresh: ~5 minutes */
|
|
760
744
|
kalshi_orderbooks: (params) => get("gateway/v2/kalshi/orderbooks", params),
|
|
@@ -769,7 +753,6 @@ var v2 = {
|
|
|
769
753
|
};
|
|
770
754
|
|
|
771
755
|
// src/data/categories/wallet.ts
|
|
772
|
-
init_client();
|
|
773
756
|
var wallet = {
|
|
774
757
|
/** Returns multiple wallet sub-resources in a single request. **Available fields** (via `fields`): `balance`, `tokens`, `labels`, `nft`. **Lookup:** by `address`. Partial failures return available fields with per-field error info. Returns 422 if `fields` is invalid. */
|
|
775
758
|
detail: (params) => get("wallet/detail", params),
|
|
@@ -786,7 +769,6 @@ var wallet = {
|
|
|
786
769
|
};
|
|
787
770
|
|
|
788
771
|
// src/data/categories/web.ts
|
|
789
|
-
init_client();
|
|
790
772
|
var web = {
|
|
791
773
|
/** Fetches a web page and converts it to clean, LLM-friendly markdown. **Options:** - `target_selector` — extract specific page sections - `remove_selector` — strip unwanted elements Returns 400 if the URL is invalid or unreachable. */
|
|
792
774
|
fetch: (params) => get("web/fetch", params)
|