orez 0.2.27 → 0.2.29

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.
Files changed (150) hide show
  1. package/package.json +3 -4
  2. package/src/admin/admin-data.test.ts +0 -348
  3. package/src/admin/http-proxy.ts +0 -252
  4. package/src/admin/log-store.ts +0 -192
  5. package/src/admin/server.ts +0 -471
  6. package/src/admin/ui.ts +0 -1322
  7. package/src/bench/proxy-throughput.bench.ts +0 -343
  8. package/src/bench/serial-mutations.bench.ts +0 -270
  9. package/src/browser.ts +0 -203
  10. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  11. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  12. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  13. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  14. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  15. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  16. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  17. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  18. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  19. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  20. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  21. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  22. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  23. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  24. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  25. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  26. package/src/cf-do/ARCHITECTURE.md +0 -93
  27. package/src/cf-do/CHAT_E2E.md +0 -213
  28. package/src/cf-do/watermark.test.ts +0 -103
  29. package/src/cf-do/watermark.ts +0 -118
  30. package/src/cf-do/worker.ts +0 -1041
  31. package/src/cf-do/wrangler.toml +0 -11
  32. package/src/cf-pglite/README.md +0 -19
  33. package/src/change-tracking.ts +0 -25
  34. package/src/child-process.test.ts +0 -147
  35. package/src/child-process.ts +0 -90
  36. package/src/cli-entry.ts +0 -72
  37. package/src/cli.test.ts +0 -40
  38. package/src/cli.ts +0 -1214
  39. package/src/config.ts +0 -150
  40. package/src/do-sql-tracking.test.ts +0 -19
  41. package/src/do-sql-tracking.ts +0 -19
  42. package/src/index.ts +0 -1215
  43. package/src/integration/integration.test.ts +0 -517
  44. package/src/integration/native-binary.guard.test.ts +0 -13
  45. package/src/integration/native-startup.test.ts +0 -44
  46. package/src/integration/replication-latency.test.ts +0 -428
  47. package/src/integration/restore-live-stress.test.ts +0 -433
  48. package/src/integration/restore-reset.test.ts +0 -400
  49. package/src/integration/restore.test.ts +0 -274
  50. package/src/integration/test-permissions.ts +0 -147
  51. package/src/load-config.ts +0 -46
  52. package/src/log.ts +0 -96
  53. package/src/mutex.ts +0 -47
  54. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  55. package/src/pg-proxy-browser.ts +0 -2022
  56. package/src/pg-proxy-do-backend.test.ts +0 -3890
  57. package/src/pg-proxy-do-backend.ts +0 -7191
  58. package/src/pg-proxy.ts +0 -1087
  59. package/src/pg-sqlite-compiler/README.md +0 -53
  60. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  61. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  62. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  63. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  64. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  65. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  66. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  67. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  71. package/src/pg-sqlite-compiler/index.ts +0 -73
  72. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  73. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  74. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  75. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  76. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  77. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  78. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  79. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  80. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  81. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  82. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  83. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  84. package/src/pg-sqlite-compiler/types.ts +0 -63
  85. package/src/pglite-ipc.test.ts +0 -116
  86. package/src/pglite-ipc.ts +0 -266
  87. package/src/pglite-manager.ts +0 -557
  88. package/src/pglite-web-proxy.test.ts +0 -57
  89. package/src/pglite-web-proxy.ts +0 -221
  90. package/src/pglite-web-worker.ts +0 -152
  91. package/src/pglite-worker-thread.ts +0 -253
  92. package/src/port.ts +0 -25
  93. package/src/process-title.ts +0 -9
  94. package/src/recovery.ts +0 -155
  95. package/src/replication/change-tracker.test.ts +0 -357
  96. package/src/replication/change-tracker.ts +0 -279
  97. package/src/replication/handler.test.ts +0 -511
  98. package/src/replication/handler.ts +0 -1190
  99. package/src/replication/pgoutput-encoder.test.ts +0 -697
  100. package/src/replication/pgoutput-encoder.ts +0 -373
  101. package/src/replication/tcp-replication.test.ts +0 -876
  102. package/src/replication/zero-compat.test.ts +0 -1150
  103. package/src/restore-stress.test.ts +0 -188
  104. package/src/s3-local.ts +0 -203
  105. package/src/shim/hooks.mjs +0 -120
  106. package/src/shim/register.mjs +0 -4
  107. package/src/sqlite-mode/apply-mode.ts +0 -224
  108. package/src/sqlite-mode/index.ts +0 -15
  109. package/src/sqlite-mode/native-binary.ts +0 -89
  110. package/src/sqlite-mode/package-resolve.ts +0 -17
  111. package/src/sqlite-mode/resolve-mode.ts +0 -80
  112. package/src/sqlite-mode/shim-template.ts +0 -159
  113. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  114. package/src/sqlite-mode/types.ts +0 -30
  115. package/src/vite-plugin.ts +0 -67
  116. package/src/wasm-sqlite.test.ts +0 -537
  117. package/src/worker/browser-admin.ts +0 -52
  118. package/src/worker/browser-build-config.test.ts +0 -71
  119. package/src/worker/browser-build-config.ts +0 -109
  120. package/src/worker/browser-embed-admin.test.ts +0 -75
  121. package/src/worker/browser-embed.ts +0 -345
  122. package/src/worker/cf-patches.ts +0 -384
  123. package/src/worker/embed-integration.test.ts +0 -321
  124. package/src/worker/index.ts +0 -138
  125. package/src/worker/shims/fastify.test.ts +0 -255
  126. package/src/worker/shims/fastify.ts +0 -306
  127. package/src/worker/shims/http-service.test.ts +0 -355
  128. package/src/worker/shims/http-service.ts +0 -293
  129. package/src/worker/shims/node-stub.ts +0 -290
  130. package/src/worker/shims/oxfmt.ts +0 -3
  131. package/src/worker/shims/postgres-browser.ts +0 -59
  132. package/src/worker/shims/postgres-socket.test.ts +0 -576
  133. package/src/worker/shims/postgres-socket.ts +0 -310
  134. package/src/worker/shims/postgres.test.ts +0 -364
  135. package/src/worker/shims/postgres.ts +0 -1454
  136. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  137. package/src/worker/shims/sqlite-browser.ts +0 -175
  138. package/src/worker/shims/sqlite.test.ts +0 -786
  139. package/src/worker/shims/sqlite.ts +0 -978
  140. package/src/worker/shims/stream-browser.ts +0 -15
  141. package/src/worker/shims/ws-browser.test.ts +0 -205
  142. package/src/worker/shims/ws-browser.ts +0 -248
  143. package/src/worker/shims/ws.test.ts +0 -288
  144. package/src/worker/shims/ws.ts +0 -467
  145. package/src/worker/shims/zero-process-env.ts +0 -11
  146. package/src/worker/types.ts +0 -75
  147. package/src/worker/worker-integration.test.ts +0 -223
  148. package/src/worker/worker.test.ts +0 -136
  149. package/src/worker/zero-cache-embed-cf.ts +0 -463
  150. package/src/worker/zero-cache-embed.ts +0 -277
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orez",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
4
4
  "description": "PGlite-powered zero-sync development backend. No Docker required.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,8 +44,7 @@
44
44
  "orez": "dist/cli-entry.js"
45
45
  },
46
46
  "files": [
47
- "dist",
48
- "src"
47
+ "dist"
49
48
  ],
50
49
  "scripts": {
51
50
  "build": "tsc && chmod +x dist/cli-entry.js",
@@ -88,7 +87,7 @@
88
87
  "@electric-sql/pglite": "0.4.1",
89
88
  "@electric-sql/pglite-tools": "^0.3.1",
90
89
  "@pgsql/traverse": "17.2.6",
91
- "bedrock-sqlite": "0.2.27",
90
+ "bedrock-sqlite": "0.2.28",
92
91
  "citty": "^0.2.0",
93
92
  "pg-gateway": "0.3.0-beta.4",
94
93
  "pgsql-parser": "^17.9.11",
@@ -1,348 +0,0 @@
1
- /**
2
- * integration tests for the admin data explorer endpoints.
3
- *
4
- * spins up pglite instances + admin server directly (no zero-cache)
5
- * and exercises the /api/db/* and /api/sqlite/* endpoints.
6
- */
7
- import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
8
- import { resolve } from 'node:path'
9
-
10
- import { PGlite } from '@electric-sql/pglite'
11
- import { afterAll, beforeAll, describe, expect, test } from 'vitest'
12
-
13
- import { startAdminServer } from './server.js'
14
-
15
- import type { ZeroLiteConfig } from '../config.js'
16
- import type { LogStore } from './log-store.js'
17
- import type { Server } from 'node:http'
18
-
19
- const TEST_PORT = 16400 + Math.floor(Math.random() * 500)
20
- const DATA_DIR = `.orez-admin-data-test-${Date.now()}`
21
-
22
- function stubLogStore(): LogStore {
23
- const entries: any[] = []
24
- return {
25
- push() {},
26
- query() {
27
- return { entries, cursor: 0 }
28
- },
29
- getAll() {
30
- return entries
31
- },
32
- clear() {
33
- entries.length = 0
34
- },
35
- }
36
- }
37
-
38
- function stubConfig(): ZeroLiteConfig {
39
- return {
40
- dataDir: resolve(DATA_DIR),
41
- pgPort: 0,
42
- zeroPort: 0,
43
- adminPort: TEST_PORT,
44
- pgUser: 'test',
45
- pgPassword: 'test',
46
- migrationsDir: '',
47
- seedFile: '',
48
- skipZeroCache: true,
49
- disableWasmSqlite: false,
50
- forceWasmSqlite: false,
51
- useWorkerThreads: false,
52
- singleDb: false,
53
- readReplicas: 0,
54
- logLevel: 'info',
55
- pgliteOptions: {},
56
- checkpointIntervalMs: 0,
57
- maxLogFileSize: 0,
58
- disableDiskLogs: true,
59
- }
60
- }
61
-
62
- describe('admin data explorer', { timeout: 60_000 }, () => {
63
- let server: Server
64
- let postgres: PGlite
65
- let cvr: PGlite
66
- let cdb: PGlite
67
- const base = `http://127.0.0.1:${TEST_PORT}`
68
-
69
- beforeAll(async () => {
70
- mkdirSync(DATA_DIR, { recursive: true })
71
-
72
- postgres = new PGlite()
73
- cvr = new PGlite()
74
- cdb = new PGlite()
75
- await Promise.all([postgres.waitReady, cvr.waitReady, cdb.waitReady])
76
-
77
- // create test tables
78
- await postgres.exec(`
79
- CREATE TABLE public.users (
80
- id serial PRIMARY KEY,
81
- name text NOT NULL,
82
- email text,
83
- active boolean DEFAULT true
84
- );
85
- INSERT INTO public.users (name, email) VALUES
86
- ('alice', 'alice@test.com'),
87
- ('bob', 'bob@test.com'),
88
- ('charlie', 'charlie@test.com');
89
- `)
90
-
91
- await postgres.exec(`
92
- CREATE TABLE public.posts (
93
- id serial PRIMARY KEY,
94
- user_id int REFERENCES users(id),
95
- title text NOT NULL,
96
- body text
97
- );
98
- INSERT INTO public.posts (user_id, title, body) VALUES
99
- (1, 'hello world', 'first post'),
100
- (1, 'second post', 'more content'),
101
- (2, 'bob writes', NULL);
102
- `)
103
-
104
- server = await startAdminServer({
105
- port: TEST_PORT,
106
- logStore: stubLogStore(),
107
- config: stubConfig(),
108
- zeroEnv: {},
109
- startTime: Date.now(),
110
- db: { postgres, cvr, cdb, postgresReplicas: [] } as any,
111
- })
112
- })
113
-
114
- afterAll(async () => {
115
- server?.close()
116
- await Promise.all([postgres?.close(), cvr?.close(), cdb?.close()])
117
- rmSync(DATA_DIR, { recursive: true, force: true })
118
- })
119
-
120
- // --- html ---
121
-
122
- test('GET / serves html', async () => {
123
- const res = await fetch(`${base}/`)
124
- expect(res.status).toBe(200)
125
- expect(res.headers.get('content-type')).toContain('text/html')
126
- const html = await res.text()
127
- expect(html).toContain('oreZ admin')
128
- expect(html).toContain('data-db="sqlite"')
129
- })
130
-
131
- test('GET /data serves html', async () => {
132
- const res = await fetch(`${base}/data`)
133
- expect(res.status).toBe(200)
134
- const html = await res.text()
135
- expect(html).toContain('sql-editor')
136
- })
137
-
138
- // --- /api/db/tables ---
139
-
140
- test('lists postgres tables', async () => {
141
- const res = await fetch(`${base}/api/db/tables?db=postgres`)
142
- expect(res.status).toBe(200)
143
- const data = await res.json()
144
- expect(data.tables).toBeDefined()
145
- const names = data.tables.map((t: any) => t.table_name)
146
- expect(names).toContain('users')
147
- expect(names).toContain('posts')
148
- })
149
-
150
- test('lists cvr tables (empty)', async () => {
151
- const res = await fetch(`${base}/api/db/tables?db=cvr`)
152
- expect(res.status).toBe(200)
153
- const data = await res.json()
154
- expect(data.tables).toBeDefined()
155
- expect(data.tables.length).toBe(0)
156
- })
157
-
158
- test('rejects unknown db name', async () => {
159
- const res = await fetch(`${base}/api/db/tables?db=nope`)
160
- expect(res.status).toBe(400)
161
- const data = await res.json()
162
- expect(data.error).toContain('unknown db')
163
- })
164
-
165
- // --- /api/db/table-data ---
166
-
167
- test('browses table data', async () => {
168
- const res = await fetch(`${base}/api/db/table-data?db=postgres&table=users`)
169
- expect(res.status).toBe(200)
170
- const data = await res.json()
171
- expect(data.columns).toBeDefined()
172
- expect(data.columns.length).toBeGreaterThanOrEqual(4)
173
- expect(data.rows.length).toBe(3)
174
- expect(data.total).toBe(3)
175
- // check column metadata
176
- const colNames = data.columns.map((c: any) => c.name)
177
- expect(colNames).toContain('id')
178
- expect(colNames).toContain('name')
179
- expect(colNames).toContain('email')
180
- })
181
-
182
- test('table-data supports search', async () => {
183
- const res = await fetch(
184
- `${base}/api/db/table-data?db=postgres&table=users&search=alice`
185
- )
186
- const data = await res.json()
187
- expect(data.rows.length).toBe(1)
188
- expect(data.rows[0].name).toBe('alice')
189
- expect(data.total).toBe(1)
190
- })
191
-
192
- test('table-data supports pagination', async () => {
193
- const res = await fetch(
194
- `${base}/api/db/table-data?db=postgres&table=users&limit=2&offset=0`
195
- )
196
- const data = await res.json()
197
- expect(data.rows.length).toBe(2)
198
- expect(data.total).toBe(3)
199
-
200
- const page2 = await fetch(
201
- `${base}/api/db/table-data?db=postgres&table=users&limit=2&offset=2`
202
- )
203
- const data2 = await page2.json()
204
- expect(data2.rows.length).toBe(1)
205
- })
206
-
207
- test('table-data with schema-qualified name', async () => {
208
- const res = await fetch(`${base}/api/db/table-data?db=postgres&table=public.posts`)
209
- const data = await res.json()
210
- expect(data.rows.length).toBe(3)
211
- // check NULL values come through
212
- const bobPost = data.rows.find((r: any) => r.title === 'bob writes')
213
- expect(bobPost).toBeDefined()
214
- expect(bobPost.body).toBeNull()
215
- })
216
-
217
- test('table-data missing table param', async () => {
218
- const res = await fetch(`${base}/api/db/table-data?db=postgres`)
219
- expect(res.status).toBe(400)
220
- const data = await res.json()
221
- expect(data.error).toContain('missing table')
222
- })
223
-
224
- // --- /api/db/query ---
225
-
226
- test('runs arbitrary SQL', async () => {
227
- const res = await fetch(`${base}/api/db/query`, {
228
- method: 'POST',
229
- headers: { 'Content-Type': 'application/json' },
230
- body: JSON.stringify({
231
- db: 'postgres',
232
- sql: 'SELECT name, email FROM users ORDER BY name',
233
- }),
234
- })
235
- expect(res.status).toBe(200)
236
- const data = await res.json()
237
- expect(data.fields).toEqual(['name', 'email'])
238
- expect(data.rowCount).toBe(3)
239
- expect(data.rows[0].name).toBe('alice')
240
- expect(data.durationMs).toBeGreaterThanOrEqual(0)
241
- })
242
-
243
- test('returns error for bad SQL', async () => {
244
- const res = await fetch(`${base}/api/db/query`, {
245
- method: 'POST',
246
- headers: { 'Content-Type': 'application/json' },
247
- body: JSON.stringify({ db: 'postgres', sql: 'SELECT * FROM nonexistent' }),
248
- })
249
- expect(res.status).toBe(400)
250
- const data = await res.json()
251
- expect(data.error).toBeTruthy()
252
- })
253
-
254
- test('query with joins', async () => {
255
- const res = await fetch(`${base}/api/db/query`, {
256
- method: 'POST',
257
- headers: { 'Content-Type': 'application/json' },
258
- body: JSON.stringify({
259
- db: 'postgres',
260
- sql: `SELECT u.name, p.title FROM users u JOIN posts p ON p.user_id = u.id ORDER BY p.id`,
261
- }),
262
- })
263
- const data = await res.json()
264
- expect(data.rowCount).toBe(3)
265
- expect(data.fields).toEqual(['name', 'title'])
266
- })
267
-
268
- test('query missing sql', async () => {
269
- const res = await fetch(`${base}/api/db/query`, {
270
- method: 'POST',
271
- headers: { 'Content-Type': 'application/json' },
272
- body: JSON.stringify({ db: 'postgres' }),
273
- })
274
- expect(res.status).toBe(400)
275
- const data = await res.json()
276
- expect(data.error).toContain('missing sql')
277
- })
278
-
279
- // --- sqlite ---
280
-
281
- test('sqlite tables returns 404 when no replica', async () => {
282
- const res = await fetch(`${base}/api/sqlite/tables`)
283
- expect(res.status).toBe(404)
284
- const data = await res.json()
285
- expect(data.error).toContain('not found')
286
- })
287
-
288
- test('sqlite endpoints work when replica file exists', async () => {
289
- // create a fake zero-replica.db using bedrock-sqlite
290
- // @ts-expect-error - CJS module
291
- const bedrock: any = await import('bedrock-sqlite')
292
- const Ctor = bedrock.Database || bedrock.default?.Database || bedrock.default
293
- const replicaPath = resolve(DATA_DIR, 'zero-replica.db')
294
- const setupDb = new Ctor(replicaPath)
295
- setupDb.exec(`
296
- CREATE TABLE widgets (
297
- id INTEGER PRIMARY KEY,
298
- label TEXT NOT NULL,
299
- count INTEGER
300
- );
301
- INSERT INTO widgets (label, count) VALUES
302
- ('alpha', 1),
303
- ('beta', 2),
304
- ('gamma', 3);
305
- `)
306
- setupDb.close()
307
-
308
- // list tables
309
- const tablesRes = await fetch(`${base}/api/sqlite/tables`)
310
- expect(tablesRes.status).toBe(200)
311
- const tables = await tablesRes.json()
312
- expect(tables.tables.some((t: any) => t.name === 'widgets')).toBe(true)
313
-
314
- // browse table data
315
- const browseRes = await fetch(`${base}/api/sqlite/table-data?table=widgets`)
316
- expect(browseRes.status).toBe(200)
317
- const browse = await browseRes.json()
318
- expect(browse.rows.length).toBe(3)
319
- expect(browse.total).toBe(3)
320
-
321
- // search
322
- const searchRes = await fetch(
323
- `${base}/api/sqlite/table-data?table=widgets&search=beta`
324
- )
325
- const search = await searchRes.json()
326
- expect(search.rows.length).toBe(1)
327
- expect(search.rows[0].label).toBe('beta')
328
-
329
- // raw query
330
- const queryRes = await fetch(`${base}/api/sqlite/query`, {
331
- method: 'POST',
332
- headers: { 'Content-Type': 'application/json' },
333
- body: JSON.stringify({ sql: 'SELECT count(*) as c FROM widgets' }),
334
- })
335
- expect(queryRes.status).toBe(200)
336
- const q = await queryRes.json()
337
- expect(q.rows[0].c).toBe(3)
338
- expect(q.fields).toContain('c')
339
- })
340
-
341
- // --- CORS ---
342
-
343
- test('OPTIONS returns CORS headers', async () => {
344
- const res = await fetch(`${base}/api/db/tables`, { method: 'OPTIONS' })
345
- expect(res.status).toBe(200)
346
- expect(res.headers.get('access-control-allow-origin')).toBe('*')
347
- })
348
- })
@@ -1,252 +0,0 @@
1
- import { createServer, connect, type Socket, type Server } from 'node:net'
2
-
3
- import type { ZeroLiteConfig } from '../config.js'
4
- import type { LogStore } from './log-store.js'
5
-
6
- export interface HttpLogEntry {
7
- id: number
8
- ts: number
9
- method: string
10
- path: string
11
- status: number
12
- duration: number
13
- reqSize: number
14
- resSize: number
15
- reqHeaders: Record<string, string>
16
- resHeaders: Record<string, string>
17
- }
18
-
19
- export interface HttpLogStore {
20
- push(entry: Omit<HttpLogEntry, 'id'>): void
21
- query(opts?: { since?: number; path?: string }): {
22
- entries: HttpLogEntry[]
23
- cursor: number
24
- }
25
- clear(): void
26
- }
27
-
28
- const MAX_ENTRIES = 10_000
29
- const TRIM_BATCH = Math.floor(MAX_ENTRIES * 0.1)
30
-
31
- export function createHttpLogStore(): HttpLogStore {
32
- const entries: HttpLogEntry[] = []
33
- let nextId = 1
34
-
35
- function push(entry: Omit<HttpLogEntry, 'id'>) {
36
- const full: HttpLogEntry = { ...entry, id: nextId++ }
37
- entries.push(full)
38
- if (entries.length > MAX_ENTRIES + TRIM_BATCH) {
39
- entries.splice(0, entries.length - MAX_ENTRIES)
40
- }
41
- }
42
-
43
- function query(opts?: { since?: number; path?: string }) {
44
- let result: HttpLogEntry[] = entries
45
- if (opts?.since) {
46
- const since = opts.since
47
- let lo = 0
48
- let hi = result.length
49
- while (lo < hi) {
50
- const mid = (lo + hi) >>> 1
51
- if (result[mid].id <= since) lo = mid + 1
52
- else hi = mid
53
- }
54
- result = result.slice(lo)
55
- }
56
- if (opts?.path) {
57
- const p = opts.path
58
- result = result.filter((e) => e.path.includes(p))
59
- }
60
- return {
61
- entries: result,
62
- cursor: entries.length > 0 ? entries[entries.length - 1].id : 0,
63
- }
64
- }
65
-
66
- function clear() {
67
- entries.length = 0
68
- }
69
-
70
- return { push, query, clear }
71
- }
72
-
73
- function parseHeaders(raw: string): Record<string, string> {
74
- const out: Record<string, string> = {}
75
- const lines = raw.split('\r\n')
76
- for (let i = 1; i < lines.length; i++) {
77
- if (lines[i] === '') break
78
- const idx = lines[i].indexOf(': ')
79
- if (idx > 0) {
80
- out[lines[i].slice(0, idx).toLowerCase()] = lines[i].slice(idx + 2)
81
- }
82
- }
83
- return out
84
- }
85
-
86
- // public API routes served directly by the proxy (read-only, no auth)
87
- // these are available at the sprite's public URL under /__orez/
88
- const CORS =
89
- 'Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: GET, OPTIONS\r\nAccess-Control-Allow-Headers: *'
90
-
91
- function httpResponse(
92
- status: number,
93
- body: string,
94
- contentType = 'application/json'
95
- ): Buffer {
96
- const headers = `HTTP/1.1 ${status} ${status === 200 ? 'OK' : 'Error'}\r\nContent-Type: ${contentType}\r\nContent-Length: ${Buffer.byteLength(body)}\r\n${CORS}\r\nConnection: close\r\n\r\n`
97
- return Buffer.from(headers + body)
98
- }
99
-
100
- function handleOrezRoute(
101
- path: string,
102
- method: string,
103
- logStore?: LogStore,
104
- config?: ZeroLiteConfig,
105
- startTime?: number
106
- ): Buffer | null {
107
- if (method === 'OPTIONS') {
108
- return httpResponse(200, '')
109
- }
110
-
111
- if (method !== 'GET') {
112
- return httpResponse(405, JSON.stringify({ error: 'method not allowed' }))
113
- }
114
-
115
- const url = new URL(path, 'http://localhost')
116
- const route = url.pathname.replace(/^\/__orez/, '')
117
-
118
- if (route === '/api/logs' && logStore) {
119
- const source = url.searchParams.get('source') || undefined
120
- const level = url.searchParams.get('level') || undefined
121
- const sinceStr = url.searchParams.get('since')
122
- const limitStr = url.searchParams.get('limit')
123
- const since = sinceStr ? Number(sinceStr) : undefined
124
- const limit = limitStr ? Number(limitStr) : undefined
125
- return httpResponse(
126
- 200,
127
- JSON.stringify(logStore.query({ source, level, since, limit }))
128
- )
129
- }
130
-
131
- if (route === '/api/status' && config) {
132
- return httpResponse(
133
- 200,
134
- JSON.stringify({
135
- uptime: Math.floor((Date.now() - (startTime || Date.now())) / 1000),
136
- logLevel: config.logLevel,
137
- sqliteMode: config.disableWasmSqlite ? 'native' : 'wasm',
138
- })
139
- )
140
- }
141
-
142
- return httpResponse(404, JSON.stringify({ error: 'not found' }))
143
- }
144
-
145
- // raw tcp proxy that avoids bun's broken node:http upgrade handling.
146
- // bun silently drops socket.write() data in http server upgrade events,
147
- // so we do everything at the net level instead.
148
- //
149
- // intercepts /__orez/* paths to serve read-only API (logs, status)
150
- // directly without forwarding to zero-cache.
151
- export function startHttpProxy(opts: {
152
- listenPort: number
153
- targetPort: number
154
- httpLog: HttpLogStore
155
- logStore?: LogStore
156
- config?: ZeroLiteConfig
157
- startTime?: number
158
- }): Promise<Server> {
159
- const { listenPort, targetPort, httpLog, logStore, config, startTime } = opts
160
-
161
- const server = createServer((client: Socket) => {
162
- const start = Date.now()
163
-
164
- let logged = false
165
- let reqMethod = ''
166
- let reqPath = ''
167
- let reqHeaders: Record<string, string> = {}
168
-
169
- // intercept first client chunk to extract request info
170
- client.once('data', (chunk: Buffer) => {
171
- const str = chunk.toString('utf8')
172
- const firstLine = str.split('\r\n')[0] || ''
173
- const parts = firstLine.split(' ')
174
- reqMethod = parts[0] || 'GET'
175
- reqPath = parts[1] || '/'
176
- reqHeaders = parseHeaders(str)
177
-
178
- // intercept /__orez/ paths — serve directly, don't forward to zero-cache
179
- // check char 0 first to skip the startsWith on hot-path sync/ws traffic
180
- if (
181
- reqPath.charCodeAt(0) === 47 &&
182
- reqPath.charCodeAt(1) === 95 &&
183
- reqPath.startsWith('/__orez/')
184
- ) {
185
- const response = handleOrezRoute(reqPath, reqMethod, logStore, config, startTime)
186
- if (response) {
187
- client.write(response)
188
- client.end()
189
- httpLog.push({
190
- ts: start,
191
- method: reqMethod,
192
- path: reqPath,
193
- status: 200,
194
- duration: Date.now() - start,
195
- reqSize: chunk.length,
196
- resSize: response.length,
197
- reqHeaders,
198
- resHeaders: {},
199
- })
200
- return
201
- }
202
- }
203
-
204
- // forward to zero-cache
205
- const target = connect(targetPort, '127.0.0.1')
206
-
207
- target.setKeepAlive(true, 30_000)
208
- target.setTimeout(0)
209
- client.setKeepAlive(true, 30_000)
210
- client.setTimeout(0)
211
-
212
- target.write(chunk)
213
- client.pipe(target)
214
-
215
- // intercept first target chunk to extract response info and log
216
- target.once('data', (resChunk: Buffer) => {
217
- const resStr = resChunk.toString('utf8')
218
- const resFirstLine = resStr.split('\r\n')[0] || ''
219
- const status = parseInt(resFirstLine.split(' ')[1]) || 0
220
- const resHeaders = parseHeaders(resStr)
221
-
222
- if (!logged) {
223
- logged = true
224
- httpLog.push({
225
- ts: start,
226
- method: status === 101 ? 'WS' : reqMethod,
227
- path: reqPath,
228
- status,
229
- duration: Date.now() - start,
230
- reqSize: 0,
231
- resSize: resChunk.length,
232
- reqHeaders,
233
- resHeaders,
234
- })
235
- }
236
-
237
- client.write(resChunk)
238
- target.pipe(client)
239
- })
240
-
241
- target.on('error', () => client.destroy())
242
- client.on('error', () => target.destroy())
243
- target.on('close', () => client.destroy())
244
- client.on('close', () => target.destroy())
245
- })
246
- })
247
-
248
- return new Promise((resolve, reject) => {
249
- server.listen(listenPort, '127.0.0.1', () => resolve(server as any))
250
- server.on('error', reject)
251
- })
252
- }