@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.
@@ -1,10 +1,9 @@
1
1
  import express from 'express';
2
2
 
3
3
  /**
4
- * Express server runtime — replaces scaffold server.js + routes/proxy.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 (default: PORT env, fallback 3001) */
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
- * Environment-aware data API client.
3454
+ * 1.0 data API client.
3458
3455
  *
3459
- * Reads env vars to determine routing (prefixed vars take priority):
3460
- * - SURF_SANDBOX_PROXY_BASE → sandbox (OutboundProxy handles auth)
3461
- * - SURF_DEPLOYED_GATEWAY_URL + SURF_DEPLOYED_APP_TOKEN → deployed (hermod with Bearer)
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
- * Backward-compatible aliases:
3465
- * DATA_PROXY_BASE SURF_SANDBOX_PROXY_BASE
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>;
@@ -1,10 +1,9 @@
1
1
  import express from 'express';
2
2
 
3
3
  /**
4
- * Express server runtime — replaces scaffold server.js + routes/proxy.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 (default: PORT env, fallback 3001) */
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
- * Environment-aware data API client.
3454
+ * 1.0 data API client.
3458
3455
  *
3459
- * Reads env vars to determine routing (prefixed vars take priority):
3460
- * - SURF_SANDBOX_PROXY_BASE → sandbox (OutboundProxy handles auth)
3461
- * - SURF_DEPLOYED_GATEWAY_URL + SURF_DEPLOYED_APP_TOKEN → deployed (hermod with Bearer)
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
- * Backward-compatible aliases:
3465
- * DATA_PROXY_BASE SURF_SANDBOX_PROXY_BASE
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>;
@@ -1,41 +1,280 @@
1
- import {
2
- __require,
3
- get,
4
- post
5
- } from "../chunk-J4OMYO3F.js";
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 fs from "fs";
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 port = options.port || parseInt(process.env.PORT || "3001", 10);
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 (fs.existsSync(routesDir)) {
29
- for (const file of fs.readdirSync(routesDir)) {
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 route = __require(path.join(routesDir, file));
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
- async function doSyncSchema() {
114
- if (syncing) return;
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 syncWithRetry(2, 1500);
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 syncWithRetry();
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
- const schemaPath = path.join(schemaDir, "schema.js");
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 (!fs.existsSync(cronPath)) return;
346
+ if (!fs2.existsSync(cronPath)) return;
264
347
  let tasks;
265
348
  try {
266
- tasks = JSON.parse(fs.readFileSync(cronPath, "utf-8"));
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
- const envFn = (s, l) => process.env[s] || process.env[l];
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 (fs.existsSync(cronPath)) tasks = JSON.parse(fs.readFileSync(cronPath, "utf8"));
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
- fs.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
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 (fs.existsSync(cronPath)) tasks = JSON.parse(fs.readFileSync(cronPath, "utf8"));
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
- fs.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
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 (fs.existsSync(cronPath)) tasks = JSON.parse(fs.readFileSync(cronPath, "utf8"));
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
- fs.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
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 });