@surf-ai/sdk 0.1.6-beta → 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @surf-ai/sdk
2
2
 
3
- Surf platform SDK data API client, Express server runtime, React hooks, and database helpers.
3
+ Surf SDK 1.0 for backend apps, typed Surf data access, and database helpers.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,425 +8,136 @@ Surf platform SDK — data API client, Express server runtime, React hooks, and
8
8
  bun add @surf-ai/sdk
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Configuration
12
12
 
13
- ### Frontend (React hooks)
13
+ SDK 1.0 uses a single direct-auth model.
14
14
 
15
- ```tsx
16
- import { useMarketPrice, cn, useToast } from '@surf-ai/sdk/react'
17
-
18
- function App() {
19
- const { data, isLoading } = useMarketPrice({ symbol: 'BTC', time_range: '1d' })
20
- return (
21
- <div className={cn('p-4', isLoading && 'opacity-50')}>
22
- BTC: ${data?.data?.[0]?.value}
23
- </div>
24
- )
25
- }
26
- ```
27
-
28
- ### Backend (data API)
29
-
30
- ```js
31
- const { dataApi } = require('@surf-ai/sdk/server')
15
+ | Env Var | Default | Purpose |
16
+ | --- | --- | --- |
17
+ | `SURF_API_BASE_URL` | `https://api.ask.surf/gateway/v1` | Full Surf API base URL |
18
+ | `SURF_API_KEY` | none | Bearer token used for upstream requests and protected runtime endpoints |
19
+ | `PORT` | none | Express server port when `createServer({ port })` is not provided |
32
20
 
33
- // Typed methods grouped by category
34
- const btc = await dataApi.market.price({ symbol: 'BTC', time_range: '1d' })
35
- const holders = await dataApi.token.holders({ address: '0x...', chain: 'ethereum' })
36
- const trades = await dataApi.onchain.sql({ sql: 'SELECT ...', max_rows: 100 })
21
+ All upstream SDK requests use:
37
22
 
38
- // Escape hatch for new endpoints
39
- const data = await dataApi.get('newcategory/endpoint', { foo: 'bar' })
23
+ ```http
24
+ Authorization: Bearer <SURF_API_KEY>
40
25
  ```
41
26
 
42
- ### Backend (Express server)
27
+ ## Subpath exports
43
28
 
44
- ```js
45
- const { createServer } = require('@surf-ai/sdk/server')
46
-
47
- // Starts Express with /proxy/*, route auto-loading, DB sync, cron, health check
48
- createServer({ port: 3001 }).start()
49
- ```
29
+ | Import | What it provides |
30
+ | --- | --- |
31
+ | `@surf-ai/sdk/server` | `createServer()`, `dataApi` |
32
+ | `@surf-ai/sdk/db` | `dbProvision()`, `dbQuery()`, `dbTables()`, `dbTableSchema()`, `dbStatus()` |
50
33
 
51
- Routes in `routes/*.js` are auto-mounted at `/api/{name}`:
34
+ ## Data API usage
52
35
 
53
36
  ```js
54
- // routes/btc.js → /api/btc
55
37
  const { dataApi } = require('@surf-ai/sdk/server')
56
- const router = require('express').Router()
57
38
 
58
- router.get('/', async (req, res) => {
59
- const data = await dataApi.market.price({ symbol: 'BTC' })
60
- res.json(data)
39
+ const btc = await dataApi.market.price({ symbol: 'BTC', time_range: '1d' })
40
+ const holders = await dataApi.token.holders({
41
+ address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
42
+ chain: 'ethereum',
61
43
  })
62
44
 
63
- module.exports = router
64
- ```
65
-
66
- ## Subpath Exports
67
-
68
- | Import | What |
69
- |--------|------|
70
- | `@surf-ai/sdk/server` | `createServer()`, `dataApi` — Express runtime + typed data API |
71
- | `@surf-ai/sdk/react` | `useMarketPrice()`, `cn()`, `useToast()` — React hooks + utilities |
72
- | `@surf-ai/sdk/db` | `dbQuery()`, `dbProvision()`, `dbTables()` — Drizzle/Neon database |
73
-
74
- ## Built-in Endpoints
75
-
76
- `createServer()` provides these automatically:
77
-
78
- | Endpoint | Method | Purpose |
79
- |----------|--------|---------|
80
- | `/api/health` | GET | Health check — `{ status: 'ok' }` |
81
- | `/api/__sync-schema` | POST | Sync `db/schema.js` tables to database |
82
- | `/api/cron` | GET | List cron jobs and status |
83
- | `/api/cron` | POST | Update cron.json and reload |
84
- | `/proxy/*` | ANY | Data API passthrough to hermod |
85
-
86
- Routes in `routes/*.js` are auto-loaded as `/api/{filename}`.
87
-
88
- DB schema is auto-synced on startup and when `db/schema.js` changes (file watcher).
89
-
90
- ## Environment Variables
91
-
92
- The SDK auto-detects the runtime mode from environment variables. New prefixed names take priority; legacy names are supported for backward compatibility.
93
-
94
- ### Data API routing
95
-
96
- | Env Var | Legacy | Mode | Set By |
97
- |---------|--------|------|--------|
98
- | `SURF_SANDBOX_PROXY_BASE` | `DATA_PROXY_BASE` | Sandbox | urania executor |
99
- | `SURF_DEPLOYED_GATEWAY_URL` | `GATEWAY_URL` | Deployed | Bifrost |
100
- | `SURF_DEPLOYED_APP_TOKEN` | `APP_TOKEN` | Deployed | Bifrost |
101
-
102
- **Routing logic:**
103
- ```
104
- if SURF_SANDBOX_PROXY_BASE → sandbox (OutboundProxy handles auth)
105
- elif SURF_DEPLOYED_GATEWAY_URL + SURF_DEPLOYED_APP_TOKEN → deployed (hermod with Bearer)
106
- else → public (api.ask.surf, no auth)
45
+ // Escape hatch for endpoints that do not have a typed helper yet.
46
+ const custom = await dataApi.get('market/price', { symbol: 'ETH', time_range: '1d' })
107
47
  ```
108
48
 
109
- ### Server
49
+ Available categories include:
110
50
 
111
- | Env Var | Default | Purpose |
112
- |---------|---------|---------|
113
- | `PORT` | `3001` | Express listen port |
51
+ - `market`
52
+ - `token`
53
+ - `wallet`
54
+ - `onchain`
55
+ - `social`
56
+ - `project`
57
+ - `news`
58
+ - `exchange`
59
+ - `fund`
60
+ - `search`
61
+ - `web`
62
+ - `polymarket`
63
+ - `kalshi`
64
+ - `prediction_market`
114
65
 
115
- ### Vite dev server (scaffold, not SDK)
116
-
117
- | Env Var | Default | Purpose |
118
- |---------|---------|---------|
119
- | `VITE_PORT` | `5173` | Frontend dev server port |
120
- | `VITE_BACKEND_PORT` | `3001` | Backend port for Vite proxy target |
121
-
122
- ### How routing works
123
-
124
- ```
125
- Sandbox (urania preview):
126
- Frontend hook: useMarketPrice()
127
- → fetch /proxy/market/price (same-origin to Vite)
128
- → Vite proxy → Express /proxy/* → OutboundProxy (JWT) → hermod
129
-
130
- Backend route: dataApi.market.price()
131
- → fetch SURF_SANDBOX_PROXY_BASE/market/price
132
- → OutboundProxy (JWT) → hermod
133
-
134
- Deployed (surf.computer):
135
- Frontend hook: useMarketPrice()
136
- → fetch /proxy/market/price (same-origin to Express)
137
- → Express /proxy/* → hermod (APP_TOKEN)
138
-
139
- Backend route: dataApi.market.price()
140
- → fetch http://127.0.0.1:PORT/proxy/market/price (loopback)
141
- → Express /proxy/* → hermod (APP_TOKEN)
142
-
143
- Public (local dev):
144
- Frontend hook: useMarketPrice()
145
- → fetch /proxy/market/price (same-origin to Vite)
146
- → Vite proxy → Express /proxy/* → hermod (GATEWAY_URL + APP_TOKEN)
147
-
148
- Backend route: dataApi.market.price()
149
- → fetch SURF_DEPLOYED_GATEWAY_URL/gateway/v1/market/price (Bearer APP_TOKEN)
150
- ```
151
-
152
- ## Available Categories
153
-
154
- | Category | Example Methods |
155
- |----------|----------------|
156
- | `market` | `price`, `ranking`, `etf`, `futures`, `options`, `fear_greed` |
157
- | `token` | `holders`, `transfers`, `dex_trades`, `tokenomics` |
158
- | `wallet` | `detail`, `net_worth`, `labels_batch`, `transfers` |
159
- | `onchain` | `sql`, `tx`, `gas_price`, `schema`, `structured_query` |
160
- | `social` | `detail`, `mindshare`, `tweets`, `user`, `ranking` |
161
- | `project` | `detail`, `defi_metrics`, `defi_ranking` |
162
- | `news` | `detail`, `feed` |
163
- | `exchange` | `price`, `depth`, `klines`, `funding_history`, `perp` |
164
- | `fund` | `detail`, `portfolio`, `ranking` |
165
- | `search` | `project`, `news`, `wallet`, `web` |
166
- | `web` | `fetch` |
167
- | `polymarket` | `events`, `markets`, `prices`, `volumes` |
168
- | `kalshi` | `events`, `markets`, `prices`, `volumes` |
169
- | `prediction_market` | `category_metrics` |
170
-
171
- ## Database
172
-
173
- Per-user PostgreSQL (Neon) with Drizzle ORM. Auto-provisioned, auto-synced on server startup.
174
-
175
- ### Setup
176
-
177
- Define tables in `backend/db/schema.js`:
66
+ ## Server runtime
178
67
 
179
68
  ```js
180
- const { pgTable, serial, text, integer, boolean, timestamp, real, jsonb } = require('drizzle-orm/pg-core')
181
-
182
- exports.users = pgTable('users', {
183
- id: serial('id').primaryKey(),
184
- name: text('name').notNull(),
185
- email: text('email'),
186
- created_at: timestamp('created_at', { withTimezone: true }).defaultNow(),
187
- })
188
- ```
189
-
190
- Tables are auto-created when the server starts and when `schema.js` changes (file watcher). You can also call `POST /api/__sync-schema` explicitly.
191
-
192
- ### Querying (in backend routes)
193
-
194
- ```js
195
- // backend/routes/users.js
196
- const { drizzle } = require('drizzle-orm/neon-http')
197
- const { dbQuery } = require('@surf-ai/sdk/db')
198
- const { eq, desc, count } = require('drizzle-orm')
199
- const schema = require('../db/schema')
200
-
201
- // IMPORTANT: arrayMode must be true for Drizzle to work correctly
202
- const db = drizzle(async (sql, params, method) => {
203
- const result = await dbQuery(sql, params, { arrayMode: true })
204
- return { rows: result.rows || [] }
205
- })
206
-
207
- router.get('/', async (req, res) => {
208
- const users = await db.select().from(schema.users).orderBy(desc(schema.users.created_at)).limit(20)
209
- res.json(users)
210
- })
211
-
212
- router.post('/', async (req, res) => {
213
- const [user] = await db.insert(schema.users).values(req.body).returning()
214
- res.json(user)
215
- })
216
-
217
- router.patch('/:id', async (req, res) => {
218
- const [user] = await db.update(schema.users).set(req.body).where(eq(schema.users.id, +req.params.id)).returning()
219
- res.json(user)
220
- })
221
-
222
- router.delete('/:id', async (req, res) => {
223
- await db.delete(schema.users).where(eq(schema.users.id, +req.params.id))
224
- res.json({ ok: true })
225
- })
226
- ```
227
-
228
- ### Raw SQL (escape hatch)
69
+ const { createServer } = require('@surf-ai/sdk/server')
229
70
 
230
- ```js
231
- const { dbQuery } = require('@surf-ai/sdk/db')
232
- const result = await dbQuery('SELECT symbol, SUM(volume) FROM trades GROUP BY symbol ORDER BY 2 DESC LIMIT $1', [20])
71
+ createServer({ port: 3001 }).start()
233
72
  ```
234
73
 
235
- ### DB Proxy Endpoints
236
-
237
- | Method | Path | Purpose |
238
- |--------|------|---------|
239
- | POST | `/proxy/db/provision` | Create database (idempotent) |
240
- | POST | `/proxy/db/query` | Execute SQL query |
241
- | GET | `/proxy/db/tables` | List all tables |
242
- | GET | `/proxy/db/table-schema?table=X` | Column definitions for table X |
243
- | GET | `/proxy/db/status` | Connection status |
244
- | POST | `/api/__sync-schema` | Force schema sync from `db/schema.js` |
74
+ `createServer()` provides:
245
75
 
246
- ### Safety Rules
76
+ - Auto-loading of `routes/*.js` and `routes/*.ts` as `/api/{name}`
77
+ - `GET /api/health`
78
+ - `POST /api/__sync-schema`
79
+ - `GET/POST/PATCH/DELETE /api/cron`
80
+ - `POST /api/cron/:id/run`
81
+ - Schema sync on startup and when `db/schema.js` changes
247
82
 
248
- - **NEVER** `DROP TABLE` or `TRUNCATE` with existing data — use `ALTER TABLE ADD COLUMN IF NOT EXISTS`
249
- - **NEVER** `DELETE FROM` without `WHERE`
250
- - **Always** call `GET /proxy/db/tables` before creating tables — check what exists first
251
- - **Never** seed data into non-empty tables — check row count first
252
- - Limits: 30s query timeout, 5000 max rows, 50 max tables
83
+ `GET /api/health` is public.
253
84
 
254
- ## Cron Jobs
85
+ These runtime endpoints require `Authorization: Bearer <SURF_API_KEY>`:
255
86
 
256
- Built-in cron system powered by `croner`. Managed via `cron.json` + handler files.
87
+ - `POST /api/__sync-schema`
88
+ - `GET /api/cron`
89
+ - `POST /api/cron`
90
+ - `PATCH /api/cron/:id`
91
+ - `DELETE /api/cron/:id`
92
+ - `POST /api/cron/:id/run`
257
93
 
258
- ### When to Use
94
+ Routes you define in `routes/*` stay public unless your app adds its own auth.
259
95
 
260
- - **Side effects** (DB writes, alerts, cache refresh) → cron job
261
- - **Display refresh** (show latest price) → `useQuery({ refetchInterval: 30000 })`
262
-
263
- ### Setup
264
-
265
- 1. Create `backend/cron.json`:
266
-
267
- ```json
268
- [
269
- {
270
- "id": "refresh-prices",
271
- "name": "Refresh token prices",
272
- "schedule": "*/5 * * * *",
273
- "handler": "tasks/refresh-prices.js",
274
- "enabled": true,
275
- "timeout": 120
276
- }
277
- ]
278
- ```
279
-
280
- 2. Create handler in `backend/tasks/`:
96
+ Example route:
281
97
 
282
98
  ```js
283
- // backend/tasks/refresh-prices.js
99
+ const router = require('express').Router()
284
100
  const { dataApi } = require('@surf-ai/sdk/server')
285
101
 
286
- module.exports = {
287
- async handler() {
288
- const data = await dataApi.market.price({ symbol: 'BTC' })
289
- // process and store...
290
- },
291
- }
292
- ```
293
-
294
- ### Cron fields
295
-
296
- | Field | Type | Required | Description |
297
- |-------|------|----------|-------------|
298
- | `id` | string | yes | Unique identifier |
299
- | `name` | string | yes | Display name |
300
- | `schedule` | string | yes | Cron expression (min 1-minute interval) |
301
- | `handler` | string | yes | Path from `backend/` to handler file |
302
- | `enabled` | boolean | yes | Active or not |
303
- | `timeout` | number | no | Max seconds (default 300) |
304
-
305
- ### Common schedules
306
-
307
- | Expression | Meaning |
308
- |-----------|---------|
309
- | `*/5 * * * *` | Every 5 minutes |
310
- | `0 * * * *` | Every hour |
311
- | `0 0 * * *` | Daily at midnight |
312
- | `0 9 * * 1` | Monday at 9 AM |
313
-
314
- ### Management API
315
-
316
- | Method | Path | Purpose |
317
- |--------|------|---------|
318
- | GET | `/api/cron` | List all jobs with status |
319
- | POST | `/api/cron` | Create/update jobs (updates cron.json) |
320
- | PATCH | `/api/cron/:id` | Update a single job |
321
- | DELETE | `/api/cron/:id` | Remove a job |
322
- | POST | `/api/cron/:id/run` | Manually trigger a job |
323
-
324
- Rules: handlers must be idempotent, never use `setInterval` in server.js, `croner` is pre-installed (never `npm install` it).
102
+ router.get('/', async (_req, res) => {
103
+ const data = await dataApi.market.price({ symbol: 'BTC', time_range: '1d' })
104
+ res.json(data)
105
+ })
325
106
 
326
- ## Web Search & Fetch
107
+ module.exports = router
108
+ ```
327
109
 
328
- Search the web and scrape pages through the data proxy:
110
+ ## Database helpers
329
111
 
330
112
  ```js
331
- // Backend
332
- const { dataApi } = require('@surf-ai/sdk/server')
333
-
334
- // Search
335
- const results = await dataApi.search.web({ q: 'BTC ETF approval', limit: 10 })
336
-
337
- // Fetch page as markdown
338
- const page = await dataApi.web.fetch({ url: 'https://example.com', target_selector: '.article' })
339
- ```
340
-
341
- ```tsx
342
- // Frontend
343
- import { useSearchWeb, useWebFetch } from '@surf-ai/sdk/react'
113
+ const { dbProvision, dbQuery, dbTables, dbTableSchema, dbStatus } = require('@surf-ai/sdk/db')
344
114
 
345
- const { data } = useSearchWeb({ q: 'BTC ETF', limit: 5 })
346
- const { data: page } = useWebFetch({ url: 'https://example.com' })
115
+ await dbProvision()
116
+ const result = await dbQuery('SELECT * FROM users WHERE id = $1', [123], { arrayMode: true })
117
+ const tables = await dbTables()
118
+ const schema = await dbTableSchema('users')
119
+ const status = await dbStatus()
347
120
  ```
348
121
 
349
- Search params: `q` (required), `limit`, `offset`, `site` (domain filter). Fetch params: `url` (required), `target_selector`, `remove_selector`, `timeout`.
122
+ Define tables in `db/schema.js` and the runtime will provision the database and create missing tables and columns during startup.
350
123
 
351
- ## Data Strategy
352
-
353
- ### Market vs Exchange
354
-
355
- - **`market`** = aggregated cross-exchange (market cap, total OI, Fear & Greed, ETF flows). Use for: "show BTC price", "market overview"
356
- - **`exchange`** = per-exchange real-time (order book, klines, funding rate). Use for: "Binance BTC order book", "compare funding rates"
357
-
358
- | Need | Use |
359
- |------|-----|
360
- | Price history, rankings, sentiment | `market` |
361
- | Total derivatives OI, liquidations, ETF flows | `market` |
362
- | Order book, klines from a specific exchange | `exchange` |
363
- | Funding rate, long/short for specific pair | `exchange` |
364
-
365
- ### Data complexity tiers
366
-
367
- | Complexity | Approach |
368
- |-----------|----------|
369
- | Single endpoint, read-only | Frontend hook directly (`useMarketPrice`) |
370
- | Combine multiple endpoints | Backend route with `Promise.all` + multiple `dataApi` calls |
371
- | External API not in proxy | Backend route + `process.env` for API keys |
372
- | On-chain SQL analytics | `dataApi.onchain.sql()` (see `onchain` skill for ClickHouse schema) |
373
-
374
- ### Backend composition pattern
124
+ Example schema:
375
125
 
376
126
  ```js
377
- // backend/routes/overview.js combine multiple data sources
378
- const { dataApi } = require('@surf-ai/sdk/server')
379
- const router = require('express').Router()
127
+ const { pgTable, serial, text, timestamp } = require('drizzle-orm/pg-core')
380
128
 
381
- router.get('/', async (req, res) => {
382
- const { symbol } = req.query
383
- const [price, holders, social] = await Promise.all([
384
- dataApi.market.price({ symbol }),
385
- dataApi.token.holders({ address: req.query.address, chain: 'ethereum', limit: 10 }),
386
- dataApi.social.detail({ username: req.query.twitter }),
387
- ])
388
- res.json({ price: price.data?.[0], topHolders: holders.data, social: social.data })
129
+ exports.users = pgTable('users', {
130
+ id: serial('id').primaryKey(),
131
+ name: text('name').notNull(),
132
+ email: text('email'),
133
+ created_at: timestamp('created_at', { withTimezone: true }).defaultNow(),
389
134
  })
390
-
391
- module.exports = router
392
135
  ```
393
136
 
394
- ## Codegen
395
-
396
- API methods and React hooks are auto-generated from hermod's OpenAPI spec via the surf CLI:
137
+ ## 1.0 migration notes
397
138
 
398
- ```bash
399
- # Regenerate all endpoints
400
- python scripts/gen_sdk.py
401
-
402
- # Regenerate specific endpoints
403
- python scripts/gen_sdk.py --ops market-price token-holders
404
-
405
- # Build
406
- bun run build
407
- ```
408
-
409
- Requires `surf` CLI installed and authenticated (`surf login`).
410
-
411
- ## Development
412
-
413
- ```bash
414
- bun install
415
- bun run build # compile TypeScript
416
- bun test # run tests
417
- bun run codegen # regenerate from OpenAPI spec
418
- ```
419
-
420
- ## Testing
421
-
422
- ```bash
423
- # Unit tests
424
- bun test ./tests/data-client.test.ts
425
-
426
- # E2E (auto-detects mode from env vars)
427
- bun test ./tests/e2e-all-envs.test.ts
428
-
429
- # E2E with specific mode:
430
- SURF_SANDBOX_PROXY_BASE=http://127.0.0.1:9999/s/<session>/proxy bun test ./tests/e2e-all-envs.test.ts
431
- SURF_DEPLOYED_GATEWAY_URL=https://api.ask.surf SURF_DEPLOYED_APP_TOKEN=<token> bun test ./tests/e2e-all-envs.test.ts
432
- ```
139
+ - `SURF_API_BASE_URL` and `SURF_API_KEY` are the only supported SDK auth variables.
140
+ - The runtime no longer mounts `/proxy/*`.
141
+ - The `@surf-ai/sdk/react` subpath has been removed.
142
+ - Route modules must export the handler directly with `module.exports = router`.
143
+ - `createServer()` requires a port from `options.port` or `process.env.PORT`.
@@ -0,0 +1,100 @@
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
+ });
7
+
8
+ // src/core/config.ts
9
+ var DEFAULT_API_BASE_URL = "https://api.ask.surf/gateway/v1";
10
+ function trimTrailingSlashes(value) {
11
+ return String(value || "").replace(/\/+$/, "");
12
+ }
13
+ function readSurfApiConfig() {
14
+ return {
15
+ baseUrl: trimTrailingSlashes(process.env.SURF_API_BASE_URL || DEFAULT_API_BASE_URL),
16
+ apiKey: process.env.SURF_API_KEY
17
+ };
18
+ }
19
+ function requireSurfApiConfig() {
20
+ const config = readSurfApiConfig();
21
+ if (!config.apiKey) {
22
+ throw new Error("SURF_API_KEY is required");
23
+ }
24
+ return { baseUrl: config.baseUrl, apiKey: config.apiKey };
25
+ }
26
+ function readAdminApiKey() {
27
+ return process.env.SURF_API_KEY;
28
+ }
29
+
30
+ // src/core/transport.ts
31
+ function sleep(ms) {
32
+ return new Promise((resolve) => setTimeout(resolve, ms));
33
+ }
34
+ function normalizePath(path) {
35
+ return String(path || "").replace(/^\/+/, "");
36
+ }
37
+ function buildUrl(path, params) {
38
+ const { baseUrl } = requireSurfApiConfig();
39
+ const url = new URL(`${baseUrl}/${normalizePath(path)}`);
40
+ if (params) {
41
+ for (const [key, value] of Object.entries(params)) {
42
+ if (value != null) {
43
+ url.searchParams.set(key, String(value));
44
+ }
45
+ }
46
+ }
47
+ return url.toString();
48
+ }
49
+ function buildHeaders(extra) {
50
+ const { apiKey } = requireSurfApiConfig();
51
+ const headers = new Headers(extra);
52
+ headers.set("Authorization", `Bearer ${apiKey}`);
53
+ return headers;
54
+ }
55
+ async function fetchJson(url, init, retries = 1) {
56
+ for (let attempt = 0; attempt <= retries; attempt++) {
57
+ const res = await fetch(url, init);
58
+ if (!res.ok) {
59
+ const text2 = await res.text();
60
+ throw new Error(`API error ${res.status}: ${text2.slice(0, 200)}`);
61
+ }
62
+ const text = await res.text();
63
+ if (text) {
64
+ return JSON.parse(text);
65
+ }
66
+ if (attempt < retries) {
67
+ await sleep(1e3);
68
+ }
69
+ }
70
+ throw new Error(`Empty response from ${url}`);
71
+ }
72
+ async function getJson(path, params) {
73
+ return fetchJson(buildUrl(path, params), {
74
+ headers: buildHeaders()
75
+ });
76
+ }
77
+ async function postJson(path, body) {
78
+ return fetchJson(buildUrl(path), {
79
+ method: "POST",
80
+ headers: buildHeaders({
81
+ "Content-Type": "application/json"
82
+ }),
83
+ body: body ? JSON.stringify(body) : void 0
84
+ });
85
+ }
86
+
87
+ // src/data/client.ts
88
+ async function get(path, params) {
89
+ return getJson(path, params);
90
+ }
91
+ async function post(path, body) {
92
+ return postJson(path, body);
93
+ }
94
+
95
+ export {
96
+ __require,
97
+ readAdminApiKey,
98
+ get,
99
+ post
100
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  get,
3
3
  post
4
- } from "./chunk-J4OMYO3F.js";
4
+ } from "./chunk-4NA3GKVD.js";
5
5
  export {
6
6
  get,
7
7
  post