@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.d.cts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Express server runtime — replaces scaffold server.js
|
|
4
|
+
* Express server runtime — replaces scaffold server.js.
|
|
5
5
|
*
|
|
6
6
|
* Handles:
|
|
7
|
-
* - /proxy/* passthrough (env-aware: sandbox → OutboundProxy, deployed → hermod)
|
|
8
7
|
* - Auto-loading routes from routes/*.js → /api/{name}
|
|
9
8
|
* - Cron job system from cron.json
|
|
10
9
|
* - DB schema sync on startup
|
|
@@ -12,18 +11,16 @@ import express from 'express';
|
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
interface ServerOptions {
|
|
15
|
-
/** Port to listen on
|
|
14
|
+
/** Port to listen on. Falls back to the PORT env var when omitted. */
|
|
16
15
|
port?: number;
|
|
17
16
|
/** Directory containing route files (default: ./routes) */
|
|
18
17
|
routesDir?: string;
|
|
19
18
|
/** Directory containing cron.json (default: .) */
|
|
20
19
|
cronDir?: string;
|
|
21
|
-
/** Enable /proxy/* passthrough (default: true) */
|
|
22
|
-
proxy?: boolean;
|
|
23
20
|
}
|
|
24
21
|
declare function createServer(options?: ServerOptions): {
|
|
25
22
|
app: express.Express;
|
|
26
|
-
port: number;
|
|
23
|
+
port: number | undefined;
|
|
27
24
|
start(): Promise<void>;
|
|
28
25
|
};
|
|
29
26
|
|
|
@@ -3454,17 +3451,14 @@ interface V2PolymarketVolumeChartParams {
|
|
|
3454
3451
|
}
|
|
3455
3452
|
|
|
3456
3453
|
/**
|
|
3457
|
-
*
|
|
3454
|
+
* 1.0 data API client.
|
|
3458
3455
|
*
|
|
3459
|
-
* Reads
|
|
3460
|
-
* -
|
|
3461
|
-
* -
|
|
3462
|
-
* - Neither → public (api.ask.surf, caller provides own auth)
|
|
3456
|
+
* Reads:
|
|
3457
|
+
* - SURF_API_BASE_URL (default: https://api.asksurf.ai/gateway/v1)
|
|
3458
|
+
* - SURF_API_KEY (required)
|
|
3463
3459
|
*
|
|
3464
|
-
*
|
|
3465
|
-
*
|
|
3466
|
-
* GATEWAY_URL → SURF_DEPLOYED_GATEWAY_URL
|
|
3467
|
-
* APP_TOKEN → SURF_DEPLOYED_APP_TOKEN
|
|
3460
|
+
* All requests are sent directly to the configured API base URL using
|
|
3461
|
+
* Authorization: Bearer <SURF_API_KEY>.
|
|
3468
3462
|
*/
|
|
3469
3463
|
/** Low-level GET — escape hatch for endpoints not yet in the SDK. */
|
|
3470
3464
|
declare function get<T = any>(path: string, params?: Record<string, any>): Promise<T>;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Express server runtime — replaces scaffold server.js
|
|
4
|
+
* Express server runtime — replaces scaffold server.js.
|
|
5
5
|
*
|
|
6
6
|
* Handles:
|
|
7
|
-
* - /proxy/* passthrough (env-aware: sandbox → OutboundProxy, deployed → hermod)
|
|
8
7
|
* - Auto-loading routes from routes/*.js → /api/{name}
|
|
9
8
|
* - Cron job system from cron.json
|
|
10
9
|
* - DB schema sync on startup
|
|
@@ -12,18 +11,16 @@ import express from 'express';
|
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
interface ServerOptions {
|
|
15
|
-
/** Port to listen on
|
|
14
|
+
/** Port to listen on. Falls back to the PORT env var when omitted. */
|
|
16
15
|
port?: number;
|
|
17
16
|
/** Directory containing route files (default: ./routes) */
|
|
18
17
|
routesDir?: string;
|
|
19
18
|
/** Directory containing cron.json (default: .) */
|
|
20
19
|
cronDir?: string;
|
|
21
|
-
/** Enable /proxy/* passthrough (default: true) */
|
|
22
|
-
proxy?: boolean;
|
|
23
20
|
}
|
|
24
21
|
declare function createServer(options?: ServerOptions): {
|
|
25
22
|
app: express.Express;
|
|
26
|
-
port: number;
|
|
23
|
+
port: number | undefined;
|
|
27
24
|
start(): Promise<void>;
|
|
28
25
|
};
|
|
29
26
|
|
|
@@ -3454,17 +3451,14 @@ interface V2PolymarketVolumeChartParams {
|
|
|
3454
3451
|
}
|
|
3455
3452
|
|
|
3456
3453
|
/**
|
|
3457
|
-
*
|
|
3454
|
+
* 1.0 data API client.
|
|
3458
3455
|
*
|
|
3459
|
-
* Reads
|
|
3460
|
-
* -
|
|
3461
|
-
* -
|
|
3462
|
-
* - Neither → public (api.ask.surf, caller provides own auth)
|
|
3456
|
+
* Reads:
|
|
3457
|
+
* - SURF_API_BASE_URL (default: https://api.asksurf.ai/gateway/v1)
|
|
3458
|
+
* - SURF_API_KEY (required)
|
|
3463
3459
|
*
|
|
3464
|
-
*
|
|
3465
|
-
*
|
|
3466
|
-
* GATEWAY_URL → SURF_DEPLOYED_GATEWAY_URL
|
|
3467
|
-
* APP_TOKEN → SURF_DEPLOYED_APP_TOKEN
|
|
3460
|
+
* All requests are sent directly to the configured API base URL using
|
|
3461
|
+
* Authorization: Bearer <SURF_API_KEY>.
|
|
3468
3462
|
*/
|
|
3469
3463
|
/** Low-level GET — escape hatch for endpoints not yet in the SDK. */
|
|
3470
3464
|
declare function get<T = any>(path: string, params?: Record<string, any>): Promise<T>;
|
package/dist/server/index.js
CHANGED
|
@@ -1,41 +1,280 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
6
7
|
|
|
7
8
|
// src/server/runtime.ts
|
|
8
9
|
import express from "express";
|
|
9
10
|
import cors from "cors";
|
|
10
|
-
import
|
|
11
|
+
import fs2 from "fs";
|
|
11
12
|
import path from "path";
|
|
12
|
-
import { createProxyMiddleware, responseInterceptor } from "http-proxy-middleware";
|
|
13
13
|
import { Cron } from "croner";
|
|
14
|
+
|
|
15
|
+
// src/core/config.ts
|
|
16
|
+
var DEFAULT_API_BASE_URL = "https://api.ask.surf/gateway/v1";
|
|
17
|
+
function trimTrailingSlashes(value) {
|
|
18
|
+
return String(value || "").replace(/\/+$/, "");
|
|
19
|
+
}
|
|
20
|
+
function readSurfApiConfig() {
|
|
21
|
+
return {
|
|
22
|
+
baseUrl: trimTrailingSlashes(process.env.SURF_API_BASE_URL || DEFAULT_API_BASE_URL),
|
|
23
|
+
apiKey: process.env.SURF_API_KEY
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function requireSurfApiConfig() {
|
|
27
|
+
const config = readSurfApiConfig();
|
|
28
|
+
if (!config.apiKey) {
|
|
29
|
+
throw new Error("SURF_API_KEY is required");
|
|
30
|
+
}
|
|
31
|
+
return { baseUrl: config.baseUrl, apiKey: config.apiKey };
|
|
32
|
+
}
|
|
33
|
+
function readAdminApiKey() {
|
|
34
|
+
return process.env.SURF_API_KEY;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/db/schema-sync.ts
|
|
38
|
+
import fs from "fs";
|
|
39
|
+
|
|
40
|
+
// src/core/transport.ts
|
|
41
|
+
function sleep(ms) {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
43
|
+
}
|
|
44
|
+
function normalizePath(path2) {
|
|
45
|
+
return String(path2 || "").replace(/^\/+/, "");
|
|
46
|
+
}
|
|
47
|
+
function buildUrl(path2, params) {
|
|
48
|
+
const { baseUrl } = requireSurfApiConfig();
|
|
49
|
+
const url = new URL(`${baseUrl}/${normalizePath(path2)}`);
|
|
50
|
+
if (params) {
|
|
51
|
+
for (const [key, value] of Object.entries(params)) {
|
|
52
|
+
if (value != null) {
|
|
53
|
+
url.searchParams.set(key, String(value));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return url.toString();
|
|
58
|
+
}
|
|
59
|
+
function buildHeaders(extra) {
|
|
60
|
+
const { apiKey } = requireSurfApiConfig();
|
|
61
|
+
const headers = new Headers(extra);
|
|
62
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
63
|
+
return headers;
|
|
64
|
+
}
|
|
65
|
+
async function fetchJson(url, init, retries = 1) {
|
|
66
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
67
|
+
const res = await fetch(url, init);
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const text2 = await res.text();
|
|
70
|
+
throw new Error(`API error ${res.status}: ${text2.slice(0, 200)}`);
|
|
71
|
+
}
|
|
72
|
+
const text = await res.text();
|
|
73
|
+
if (text) {
|
|
74
|
+
return JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
if (attempt < retries) {
|
|
77
|
+
await sleep(1e3);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Empty response from ${url}`);
|
|
81
|
+
}
|
|
82
|
+
async function getJson(path2, params) {
|
|
83
|
+
return fetchJson(buildUrl(path2, params), {
|
|
84
|
+
headers: buildHeaders()
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async function postJson(path2, body) {
|
|
88
|
+
return fetchJson(buildUrl(path2), {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: buildHeaders({
|
|
91
|
+
"Content-Type": "application/json"
|
|
92
|
+
}),
|
|
93
|
+
body: body ? JSON.stringify(body) : void 0
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/data/client.ts
|
|
98
|
+
async function get(path2, params) {
|
|
99
|
+
return getJson(path2, params);
|
|
100
|
+
}
|
|
101
|
+
async function post(path2, body) {
|
|
102
|
+
return postJson(path2, body);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/db/schema-sync.ts
|
|
106
|
+
var syncing = false;
|
|
107
|
+
async function syncSchema(options) {
|
|
108
|
+
const { schemaPath, retries = 3, retryDelay = 2e3 } = options;
|
|
109
|
+
for (let i = 0; i < retries; i++) {
|
|
110
|
+
try {
|
|
111
|
+
await doSyncSchema(schemaPath);
|
|
112
|
+
return;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(`DB schema sync attempt ${i + 1}/${retries} failed: ${err.message}`);
|
|
115
|
+
if (i < retries - 1) await new Promise((r) => setTimeout(r, retryDelay * (i + 1)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
console.error("DB schema sync failed after all retries");
|
|
119
|
+
}
|
|
120
|
+
async function doSyncSchema(schemaPath) {
|
|
121
|
+
if (syncing) return;
|
|
122
|
+
syncing = true;
|
|
123
|
+
try {
|
|
124
|
+
if (!fs.existsSync(schemaPath)) return;
|
|
125
|
+
let schema;
|
|
126
|
+
if (schemaPath.endsWith(".ts")) {
|
|
127
|
+
try {
|
|
128
|
+
schema = await import(`${schemaPath}?t=${Date.now()}`);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err instanceof SyntaxError) {
|
|
131
|
+
console.log("DB: schema file has syntax error, waiting for next change...");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (err.message.includes("Cannot find module") || err.message.includes("is not a function")) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
try {
|
|
141
|
+
delete __require.cache[__require.resolve(schemaPath)];
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
schema = __require(schemaPath);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err instanceof SyntaxError) {
|
|
148
|
+
console.log("DB: schema file has syntax error, waiting for next change...");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (err.message.includes("Cannot find module") || err.message.includes("is not a function")) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
let getTableConfig;
|
|
158
|
+
try {
|
|
159
|
+
getTableConfig = __require("drizzle-orm/pg-core").getTableConfig;
|
|
160
|
+
} catch {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const tables = Object.values(schema).filter(
|
|
164
|
+
(t) => t && typeof t === "object" && /* @__PURE__ */ Symbol.for("drizzle:Name") in t
|
|
165
|
+
);
|
|
166
|
+
if (tables.length === 0) return;
|
|
167
|
+
await post("db/provision", {});
|
|
168
|
+
const existing = (await get("db/tables")).map((t) => t.name);
|
|
169
|
+
const missing = tables.filter((t) => !existing.includes(getTableConfig(t).name));
|
|
170
|
+
if (missing.length > 0) {
|
|
171
|
+
const { generateDrizzleJson, generateMigration } = __require("drizzle-kit/api");
|
|
172
|
+
const missingSchema = {};
|
|
173
|
+
for (const t of missing) missingSchema[getTableConfig(t).name] = t;
|
|
174
|
+
const sqls = await generateMigration(generateDrizzleJson({}), generateDrizzleJson(missingSchema));
|
|
175
|
+
for (const sql of sqls) {
|
|
176
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
177
|
+
try {
|
|
178
|
+
await post("db/query", { sql, params: [] });
|
|
179
|
+
console.log(`DB: Executed: ${sql.slice(0, 80)}...`);
|
|
180
|
+
break;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (attempt === 0) {
|
|
183
|
+
console.warn(`DB: Retrying after: ${err.message}`);
|
|
184
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
185
|
+
} else {
|
|
186
|
+
console.error(`DB: Failed: ${sql.slice(0, 80)}... \u2014 ${err.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const existingTables = tables.filter((t) => existing.includes(getTableConfig(t).name));
|
|
193
|
+
for (const t of existingTables) {
|
|
194
|
+
const cfg = getTableConfig(t);
|
|
195
|
+
try {
|
|
196
|
+
const live = await get("db/table-schema", { table: cfg.name });
|
|
197
|
+
const liveCols = new Set((live.columns || []).map((c) => c.name));
|
|
198
|
+
for (const col of cfg.columns) {
|
|
199
|
+
if (!liveCols.has(col.name)) {
|
|
200
|
+
const colType = col.getSQLType();
|
|
201
|
+
const ddl = `ALTER TABLE "${cfg.name}" ADD COLUMN IF NOT EXISTS "${col.name}" ${colType}`;
|
|
202
|
+
try {
|
|
203
|
+
await post("db/query", { sql: ddl, params: [] });
|
|
204
|
+
console.log(`DB: Added column ${col.name} to ${cfg.name}`);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.warn(`DB: Failed to add column ${col.name} to ${cfg.name}: ${err.message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.warn(`DB: Column check failed for ${cfg.name}: ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
syncing = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function watchSchema(schemaPath, options) {
|
|
219
|
+
const { debounceMs = 1e3, retries = 2, retryDelay = 1500 } = options || {};
|
|
220
|
+
if (!fs.existsSync(schemaPath)) return () => {
|
|
221
|
+
};
|
|
222
|
+
let debounce = null;
|
|
223
|
+
fs.watchFile(schemaPath, { interval: 2e3 }, () => {
|
|
224
|
+
if (debounce) clearTimeout(debounce);
|
|
225
|
+
debounce = setTimeout(async () => {
|
|
226
|
+
console.log("DB: schema file changed, re-syncing tables...");
|
|
227
|
+
try {
|
|
228
|
+
await syncSchema({ schemaPath, retries, retryDelay });
|
|
229
|
+
console.log("DB: schema re-sync complete");
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`DB: schema re-sync failed: ${err.message}`);
|
|
232
|
+
}
|
|
233
|
+
}, debounceMs);
|
|
234
|
+
});
|
|
235
|
+
return () => {
|
|
236
|
+
fs.unwatchFile(schemaPath);
|
|
237
|
+
if (debounce) clearTimeout(debounce);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/server/runtime.ts
|
|
242
|
+
function requireBearerAuth(req, res, next) {
|
|
243
|
+
const apiKey = readAdminApiKey();
|
|
244
|
+
if (!apiKey) {
|
|
245
|
+
return res.status(503).json({ error: "SURF_API_KEY is not configured" });
|
|
246
|
+
}
|
|
247
|
+
if (req.headers.authorization !== `Bearer ${apiKey}`) {
|
|
248
|
+
return res.status(401).json({ error: "Unauthorized" });
|
|
249
|
+
}
|
|
250
|
+
next();
|
|
251
|
+
}
|
|
14
252
|
function createServer(options = {}) {
|
|
15
|
-
const
|
|
253
|
+
const rawPort = process.env.BACKEND_PORT;
|
|
254
|
+
const port = options.port ?? (rawPort ? Number.parseInt(rawPort, 10) : void 0);
|
|
255
|
+
if (!Number.isInteger(port)) {
|
|
256
|
+
throw new Error("createServer requires a port via options.port or BACKEND_PORT env var");
|
|
257
|
+
}
|
|
16
258
|
const routesDir = options.routesDir || path.join(process.cwd(), "routes");
|
|
17
259
|
const cronDir = options.cronDir || process.cwd();
|
|
18
|
-
const enableProxy = options.proxy !== false;
|
|
19
260
|
const app = express();
|
|
20
261
|
app.use(cors());
|
|
21
|
-
if (enableProxy) {
|
|
22
|
-
setupProxy(app, port);
|
|
23
|
-
}
|
|
24
262
|
app.use(express.json());
|
|
25
263
|
app.get("/api/health", (_req, res) => {
|
|
26
264
|
res.json({ status: "ok" });
|
|
27
265
|
});
|
|
28
|
-
if (
|
|
29
|
-
for (const file of
|
|
266
|
+
if (fs2.existsSync(routesDir)) {
|
|
267
|
+
for (const file of fs2.readdirSync(routesDir)) {
|
|
30
268
|
if (!file.endsWith(".js") && !file.endsWith(".ts")) continue;
|
|
31
269
|
const name = file.replace(/\.(js|ts)$/, "");
|
|
32
270
|
try {
|
|
33
|
-
const
|
|
34
|
-
const handler = route.default || route;
|
|
271
|
+
const handler = __require(path.join(routesDir, file));
|
|
35
272
|
if (typeof handler === "function") {
|
|
36
273
|
app.use(`/api/${name}`, handler);
|
|
37
274
|
console.log(`Route registered: /api/${name}`);
|
|
275
|
+
continue;
|
|
38
276
|
}
|
|
277
|
+
throw new Error(`Route module must export a handler function via module.exports`);
|
|
39
278
|
} catch (err) {
|
|
40
279
|
console.error(`Failed to load route ${file}: ${err.message}`);
|
|
41
280
|
}
|
|
@@ -59,157 +298,15 @@ function createServer(options = {}) {
|
|
|
59
298
|
}
|
|
60
299
|
};
|
|
61
300
|
}
|
|
62
|
-
function env(surfName, legacyName) {
|
|
63
|
-
return process.env[surfName] || process.env[legacyName];
|
|
64
|
-
}
|
|
65
|
-
function setupProxy(app, port) {
|
|
66
|
-
const gatewayUrl = env("SURF_DEPLOYED_GATEWAY_URL", "GATEWAY_URL");
|
|
67
|
-
const appToken = env("SURF_DEPLOYED_APP_TOKEN", "APP_TOKEN");
|
|
68
|
-
const proxyBase = env("SURF_SANDBOX_PROXY_BASE", "DATA_PROXY_BASE");
|
|
69
|
-
const isDeployed = Boolean(gatewayUrl && appToken);
|
|
70
|
-
const bufferResponse = responseInterceptor(async (buf) => buf);
|
|
71
|
-
if (isDeployed) {
|
|
72
|
-
app.use("/proxy", createProxyMiddleware({
|
|
73
|
-
target: gatewayUrl,
|
|
74
|
-
changeOrigin: true,
|
|
75
|
-
selfHandleResponse: true,
|
|
76
|
-
pathRewrite: (p) => "/gateway/v1" + p,
|
|
77
|
-
headers: {
|
|
78
|
-
Authorization: `Bearer ${appToken}`,
|
|
79
|
-
"Accept-Encoding": "identity"
|
|
80
|
-
},
|
|
81
|
-
on: { proxyRes: bufferResponse }
|
|
82
|
-
}));
|
|
83
|
-
const loopback = `http://127.0.0.1:${port}/proxy`;
|
|
84
|
-
process.env.SURF_SANDBOX_PROXY_BASE = loopback;
|
|
85
|
-
process.env.DATA_PROXY_BASE = loopback;
|
|
86
|
-
} else if (proxyBase) {
|
|
87
|
-
const target = proxyBase.replace(/\/proxy$/, "");
|
|
88
|
-
app.use(createProxyMiddleware({
|
|
89
|
-
target,
|
|
90
|
-
changeOrigin: true,
|
|
91
|
-
pathFilter: "/proxy",
|
|
92
|
-
on: {
|
|
93
|
-
proxyReq: (proxyReq, req) => {
|
|
94
|
-
console.log(`[proxy] >> ${req.method} ${req.originalUrl}`);
|
|
95
|
-
},
|
|
96
|
-
proxyRes: (proxyRes, req) => {
|
|
97
|
-
console.log(`[proxy] << ${proxyRes.statusCode} ${req.method} ${req.originalUrl}`);
|
|
98
|
-
},
|
|
99
|
-
error: (err, req, res) => {
|
|
100
|
-
console.error(`[proxy] !! ${req.method} ${req.originalUrl} error: ${err.message}`);
|
|
101
|
-
if (!res.headersSent) res.status(502).json({ error: err.message });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}));
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
301
|
var schemaSync = { run: async () => {
|
|
108
302
|
}, watch: () => {
|
|
109
303
|
} };
|
|
110
304
|
function setupSchemaSync(app, schemaDir) {
|
|
111
|
-
let syncing = false;
|
|
112
305
|
let schemaReady = false;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
syncing = true;
|
|
116
|
-
try {
|
|
117
|
-
const schemaPath = path.join(schemaDir, "schema.js");
|
|
118
|
-
if (!fs.existsSync(schemaPath)) return;
|
|
119
|
-
try {
|
|
120
|
-
delete __require.cache[__require.resolve(schemaPath)];
|
|
121
|
-
} catch {
|
|
122
|
-
}
|
|
123
|
-
let schema;
|
|
124
|
-
try {
|
|
125
|
-
schema = __require(schemaPath);
|
|
126
|
-
} catch (err) {
|
|
127
|
-
if (err instanceof SyntaxError) {
|
|
128
|
-
console.log("DB: schema.js has syntax error, waiting for next change...");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (err.message.includes("Cannot find module") || err.message.includes("is not a function")) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
throw err;
|
|
135
|
-
}
|
|
136
|
-
let getTableConfig;
|
|
137
|
-
try {
|
|
138
|
-
getTableConfig = __require("drizzle-orm/pg-core").getTableConfig;
|
|
139
|
-
} catch {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const tables = Object.values(schema).filter(
|
|
143
|
-
(t) => t && typeof t === "object" && /* @__PURE__ */ Symbol.for("drizzle:Name") in t
|
|
144
|
-
);
|
|
145
|
-
if (tables.length === 0) return;
|
|
146
|
-
const { get: dbGet, post: dbPost } = await import("../client-3YMIRPDV.js");
|
|
147
|
-
await dbPost("db/provision");
|
|
148
|
-
const existing = (await dbGet("db/tables")).map((t) => t.name);
|
|
149
|
-
const missing = tables.filter((t) => !existing.includes(getTableConfig(t).name));
|
|
150
|
-
if (missing.length > 0) {
|
|
151
|
-
const { generateDrizzleJson, generateMigration } = __require("drizzle-kit/api");
|
|
152
|
-
const missingSchema = {};
|
|
153
|
-
for (const t of missing) missingSchema[getTableConfig(t).name] = t;
|
|
154
|
-
const sqls = await generateMigration(generateDrizzleJson({}), generateDrizzleJson(missingSchema));
|
|
155
|
-
for (const sql of sqls) {
|
|
156
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
157
|
-
try {
|
|
158
|
-
await dbPost("db/query", { sql, params: [] });
|
|
159
|
-
console.log(`DB: Executed: ${sql.slice(0, 80)}...`);
|
|
160
|
-
break;
|
|
161
|
-
} catch (err) {
|
|
162
|
-
if (attempt === 0) {
|
|
163
|
-
console.warn(`DB: Retrying after: ${err.message}`);
|
|
164
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
165
|
-
} else {
|
|
166
|
-
console.error(`DB: Failed: ${sql.slice(0, 80)}... \u2014 ${err.message}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const existingTables = tables.filter((t) => existing.includes(getTableConfig(t).name));
|
|
173
|
-
for (const t of existingTables) {
|
|
174
|
-
const cfg = getTableConfig(t);
|
|
175
|
-
try {
|
|
176
|
-
const live = await dbGet("db/table-schema", { table: cfg.name });
|
|
177
|
-
const liveCols = new Set((live.columns || []).map((c) => c.name));
|
|
178
|
-
for (const col of cfg.columns) {
|
|
179
|
-
if (!liveCols.has(col.name)) {
|
|
180
|
-
const colType = col.getSQLType();
|
|
181
|
-
const ddl = `ALTER TABLE "${cfg.name}" ADD COLUMN IF NOT EXISTS "${col.name}" ${colType}`;
|
|
182
|
-
try {
|
|
183
|
-
await dbPost("db/query", { sql: ddl, params: [] });
|
|
184
|
-
console.log(`DB: Added column ${col.name} to ${cfg.name}`);
|
|
185
|
-
} catch (err) {
|
|
186
|
-
console.warn(`DB: Failed to add column ${col.name} to ${cfg.name}: ${err.message}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
} catch (err) {
|
|
191
|
-
console.warn(`DB: Column check failed for ${cfg.name}: ${err.message}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
} finally {
|
|
195
|
-
syncing = false;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
async function syncWithRetry(retries = 3, delay = 2e3) {
|
|
199
|
-
for (let i = 0; i < retries; i++) {
|
|
200
|
-
try {
|
|
201
|
-
await doSyncSchema();
|
|
202
|
-
return;
|
|
203
|
-
} catch (err) {
|
|
204
|
-
console.error(`DB schema sync attempt ${i + 1}/${retries} failed: ${err.message}`);
|
|
205
|
-
if (i < retries - 1) await new Promise((r) => setTimeout(r, delay * (i + 1)));
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
console.error("DB schema sync failed after all retries");
|
|
209
|
-
}
|
|
210
|
-
app.post("/api/__sync-schema", async (_req, res) => {
|
|
306
|
+
const schemaPath = path.join(schemaDir, "schema.js");
|
|
307
|
+
app.post("/api/__sync-schema", requireBearerAuth, async (_req, res) => {
|
|
211
308
|
try {
|
|
212
|
-
await
|
|
309
|
+
await syncSchema({ schemaPath, retries: 2, retryDelay: 1500 });
|
|
213
310
|
res.json({ ok: true });
|
|
214
311
|
} catch (err) {
|
|
215
312
|
res.status(500).json({ ok: false, error: err.message });
|
|
@@ -221,7 +318,7 @@ function setupSchemaSync(app, schemaDir) {
|
|
|
221
318
|
});
|
|
222
319
|
schemaSync.run = async () => {
|
|
223
320
|
try {
|
|
224
|
-
await
|
|
321
|
+
await syncSchema({ schemaPath });
|
|
225
322
|
schemaReady = true;
|
|
226
323
|
console.log("Schema sync complete, API ready");
|
|
227
324
|
} catch {
|
|
@@ -230,21 +327,7 @@ function setupSchemaSync(app, schemaDir) {
|
|
|
230
327
|
}
|
|
231
328
|
};
|
|
232
329
|
schemaSync.watch = () => {
|
|
233
|
-
|
|
234
|
-
if (!fs.existsSync(schemaPath)) return;
|
|
235
|
-
let debounce = null;
|
|
236
|
-
fs.watchFile(schemaPath, { interval: 2e3 }, () => {
|
|
237
|
-
if (debounce) clearTimeout(debounce);
|
|
238
|
-
debounce = setTimeout(async () => {
|
|
239
|
-
console.log("DB: schema.js changed, re-syncing tables...");
|
|
240
|
-
try {
|
|
241
|
-
await syncWithRetry(2, 1500);
|
|
242
|
-
console.log("DB: schema re-sync complete");
|
|
243
|
-
} catch (err) {
|
|
244
|
-
console.error(`DB: schema re-sync failed: ${err.message}`);
|
|
245
|
-
}
|
|
246
|
-
}, 1e3);
|
|
247
|
-
});
|
|
330
|
+
watchSchema(schemaPath);
|
|
248
331
|
};
|
|
249
332
|
}
|
|
250
333
|
function setupCron(app, cronDir) {
|
|
@@ -260,10 +343,10 @@ function setupCron(app, cronDir) {
|
|
|
260
343
|
}
|
|
261
344
|
cronJobs.clear();
|
|
262
345
|
const cronPath = path.join(cronDir, "cron.json");
|
|
263
|
-
if (!
|
|
346
|
+
if (!fs2.existsSync(cronPath)) return;
|
|
264
347
|
let tasks;
|
|
265
348
|
try {
|
|
266
|
-
tasks = JSON.parse(
|
|
349
|
+
tasks = JSON.parse(fs2.readFileSync(cronPath, "utf-8"));
|
|
267
350
|
} catch (e) {
|
|
268
351
|
console.error("Failed to parse cron.json:", e.message);
|
|
269
352
|
return;
|
|
@@ -311,16 +394,7 @@ function setupCron(app, cronDir) {
|
|
|
311
394
|
console.log(`Cron registered: [${task.id}] ${task.name} (${task.schedule})`);
|
|
312
395
|
}
|
|
313
396
|
}
|
|
314
|
-
|
|
315
|
-
app.use("/api/cron", (req, res, next) => {
|
|
316
|
-
const appToken = envFn("SURF_DEPLOYED_APP_TOKEN", "APP_TOKEN");
|
|
317
|
-
if (!appToken) return next();
|
|
318
|
-
const auth = req.headers.authorization;
|
|
319
|
-
if (!auth || auth !== `Bearer ${appToken}`) {
|
|
320
|
-
return res.status(401).json({ error: "Unauthorized" });
|
|
321
|
-
}
|
|
322
|
-
next();
|
|
323
|
-
});
|
|
397
|
+
app.use("/api/cron", requireBearerAuth);
|
|
324
398
|
app.get("/api/cron", (_req, res) => {
|
|
325
399
|
res.json(cronTasks.map((t) => {
|
|
326
400
|
const state = cronState.get(t.id) || { lastRunAt: null, lastStatus: null, lastError: null };
|
|
@@ -353,7 +427,7 @@ function setupCron(app, cronDir) {
|
|
|
353
427
|
const cronPath = path.join(cronDir, "cron.json");
|
|
354
428
|
let tasks = [];
|
|
355
429
|
try {
|
|
356
|
-
if (
|
|
430
|
+
if (fs2.existsSync(cronPath)) tasks = JSON.parse(fs2.readFileSync(cronPath, "utf8"));
|
|
357
431
|
} catch {
|
|
358
432
|
tasks = [];
|
|
359
433
|
}
|
|
@@ -362,7 +436,7 @@ function setupCron(app, cronDir) {
|
|
|
362
436
|
}
|
|
363
437
|
const newTask = { id, name, schedule, handler, enabled, timeout };
|
|
364
438
|
tasks.push(newTask);
|
|
365
|
-
|
|
439
|
+
fs2.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
|
|
366
440
|
loadCronJobs();
|
|
367
441
|
res.status(201).json(newTask);
|
|
368
442
|
});
|
|
@@ -370,7 +444,7 @@ function setupCron(app, cronDir) {
|
|
|
370
444
|
const cronPath = path.join(cronDir, "cron.json");
|
|
371
445
|
let tasks = [];
|
|
372
446
|
try {
|
|
373
|
-
if (
|
|
447
|
+
if (fs2.existsSync(cronPath)) tasks = JSON.parse(fs2.readFileSync(cronPath, "utf8"));
|
|
374
448
|
} catch {
|
|
375
449
|
tasks = [];
|
|
376
450
|
}
|
|
@@ -391,7 +465,7 @@ function setupCron(app, cronDir) {
|
|
|
391
465
|
}
|
|
392
466
|
}
|
|
393
467
|
tasks[idx] = { ...tasks[idx], ...updates, id: req.params.id };
|
|
394
|
-
|
|
468
|
+
fs2.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
|
|
395
469
|
loadCronJobs();
|
|
396
470
|
res.json(tasks[idx]);
|
|
397
471
|
});
|
|
@@ -399,14 +473,14 @@ function setupCron(app, cronDir) {
|
|
|
399
473
|
const cronPath = path.join(cronDir, "cron.json");
|
|
400
474
|
let tasks = [];
|
|
401
475
|
try {
|
|
402
|
-
if (
|
|
476
|
+
if (fs2.existsSync(cronPath)) tasks = JSON.parse(fs2.readFileSync(cronPath, "utf8"));
|
|
403
477
|
} catch {
|
|
404
478
|
tasks = [];
|
|
405
479
|
}
|
|
406
480
|
const idx = tasks.findIndex((t) => t.id === req.params.id);
|
|
407
481
|
if (idx === -1) return res.status(404).json({ error: "Task not found" });
|
|
408
482
|
tasks.splice(idx, 1);
|
|
409
|
-
|
|
483
|
+
fs2.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
|
|
410
484
|
cronState.delete(req.params.id);
|
|
411
485
|
loadCronJobs();
|
|
412
486
|
res.json({ ok: true });
|