@edge-base/server 0.1.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.
Files changed (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,579 @@
1
+ /**
2
+ * Analytics query builder.
3
+ *
4
+ * Generates SQL for Cloudflare Analytics Engine (cloud) and normalizes
5
+ * responses into a common format consumed by the admin dashboard.
6
+ *
7
+ * Data Point Layout (Analytics Engine):
8
+ * index1: userId ('anonymous' if not authenticated)
9
+ * blob1: method blob6: subcategory
10
+ * blob2: path blob7: target1
11
+ * blob3: status (str) blob8: target2
12
+ * blob4: error blob9: operation
13
+ * blob5: category blob10: region
14
+ * double1: status double4: requestSize
15
+ * double2: duration double5: responseSize
16
+ * double3: timestamp double6: resultCount
17
+ *
18
+ * LogsDO SQLite queries are handled directly in logs-do.ts.
19
+ * This module focuses on Analytics Engine SQL generation + response transform.
20
+ */
21
+
22
+ // ─── Types ───
23
+
24
+ export interface QueryParams {
25
+ range: string; // '1h'|'6h'|'24h'|'7d'|'30d'|'90d'
26
+ category?: string; // filter by category
27
+ metric: string; // 'overview'|'timeSeries'|'breakdown'|'topEndpoints'
28
+ groupBy?: string; // 'minute'|'tenMinute'|'hour'|'day'
29
+ excludeCategory?: string; // exclude a category (e.g. 'admin' to hide dashboard traffic)
30
+ start?: string; // ISO timestamp for custom start range
31
+ end?: string; // ISO timestamp for custom end range
32
+ }
33
+
34
+ export type AnalyticsGroupBy = 'minute' | 'tenMinute' | 'hour' | 'day';
35
+ export type OverviewAutoRange = '1h' | '6h' | '24h';
36
+
37
+ export interface AnalyticsSummary {
38
+ totalRequests: number;
39
+ totalErrors: number;
40
+ avgLatency: number;
41
+ uniqueUsers: number;
42
+ }
43
+
44
+ export interface TimeSeriesPoint {
45
+ timestamp: number;
46
+ requests: number;
47
+ errors: number;
48
+ avgLatency: number;
49
+ uniqueUsers: number;
50
+ }
51
+
52
+ export interface BreakdownItem {
53
+ label: string;
54
+ count: number;
55
+ percentage: number;
56
+ avgLatency?: number;
57
+ errorRate?: number;
58
+ }
59
+
60
+ export interface TopItem {
61
+ label: string;
62
+ count: number;
63
+ avgLatency: number;
64
+ errorRate: number;
65
+ }
66
+
67
+ export interface AnalyticsResponse {
68
+ timeSeries: TimeSeriesPoint[];
69
+ summary: AnalyticsSummary;
70
+ breakdown: BreakdownItem[];
71
+ topItems: TopItem[];
72
+ }
73
+
74
+ // ─── Time Range ───
75
+
76
+ export function parseTimeRange(range: string, start?: string, end?: string): { startTs: number; endTs: number } {
77
+ if (start && end) {
78
+ const startTs = new Date(start).getTime();
79
+ const endTs = new Date(end).getTime();
80
+ if (Number.isFinite(startTs) && Number.isFinite(endTs) && endTs >= startTs) {
81
+ return { startTs, endTs };
82
+ }
83
+ }
84
+
85
+ const now = Date.now();
86
+ let startTs: number;
87
+
88
+ switch (range) {
89
+ case '1h': startTs = now - 3600_000; break;
90
+ case '6h': startTs = now - 6 * 3600_000; break;
91
+ case '24h': startTs = now - 86400_000; break;
92
+ case '7d': startTs = now - 7 * 86400_000; break;
93
+ case '30d': startTs = now - 30 * 86400_000; break;
94
+ case '90d': startTs = now - 90 * 86400_000; break;
95
+ default: startTs = now - 86400_000; break;
96
+ }
97
+
98
+ return { startTs, endTs: now };
99
+ }
100
+
101
+ export function resolveAnalyticsGroupBy(
102
+ range: string,
103
+ start?: string,
104
+ end?: string,
105
+ requestedGroupBy?: string,
106
+ ): AnalyticsGroupBy {
107
+ if (
108
+ requestedGroupBy === 'minute' ||
109
+ requestedGroupBy === 'tenMinute' ||
110
+ requestedGroupBy === 'hour' ||
111
+ requestedGroupBy === 'day'
112
+ ) {
113
+ return requestedGroupBy;
114
+ }
115
+
116
+ if (start && end) {
117
+ const startMs = new Date(start).getTime();
118
+ const endMs = new Date(end).getTime();
119
+ if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs) {
120
+ const diffHours = (endMs - startMs) / 3_600_000;
121
+ if (diffHours <= 1) return 'minute';
122
+ if (diffHours <= 6) return 'tenMinute';
123
+ if (diffHours <= 48) return 'hour';
124
+ return 'day';
125
+ }
126
+ }
127
+
128
+ switch (range) {
129
+ case '1h':
130
+ return 'minute';
131
+ case '6h':
132
+ return 'tenMinute';
133
+ case '7d':
134
+ case '30d':
135
+ case '90d':
136
+ return 'day';
137
+ case '24h':
138
+ default:
139
+ return 'hour';
140
+ }
141
+ }
142
+
143
+ export function chooseOverviewAutoRange(oldestTimestamp: number | null, now = Date.now()): OverviewAutoRange {
144
+ if (oldestTimestamp == null || !Number.isFinite(oldestTimestamp)) return '1h';
145
+
146
+ const historyMs = Math.max(0, now - oldestTimestamp);
147
+ if (historyMs <= 3 * 3_600_000) return '1h';
148
+ if (historyMs <= 12 * 3_600_000) return '6h';
149
+ return '24h';
150
+ }
151
+
152
+ /** Get the Analytics Engine timestamp format for a given timestamp (seconds) */
153
+ function toAETimestamp(ts: number): string {
154
+ return new Date(ts).toISOString().replace('T', ' ').replace('Z', '');
155
+ }
156
+
157
+ /** Get group-by interval SQL expression for Analytics Engine */
158
+ function aeGroupByInterval(groupBy: string): string {
159
+ switch (groupBy) {
160
+ case 'minute': return "toStartOfInterval(timestamp, INTERVAL '1' MINUTE)";
161
+ case 'tenMinute': return "toStartOfInterval(timestamp, INTERVAL '10' MINUTE)";
162
+ case 'hour': return "toStartOfInterval(timestamp, INTERVAL '1' HOUR)";
163
+ case 'day': return "toStartOfInterval(timestamp, INTERVAL '1' DAY)";
164
+ default: return "toStartOfInterval(timestamp, INTERVAL '1' HOUR)";
165
+ }
166
+ }
167
+
168
+ // ─── Escaping ───
169
+
170
+ /** Basic SQL string escaping to prevent injection in interpolated values */
171
+ function escapeSql(str: string): string {
172
+ return str.replace(/'/g, "''");
173
+ }
174
+
175
+ // ─── Analytics Engine SQL Builders ───
176
+
177
+ const AE_DATASET = 'ANALYTICS';
178
+ const SERVER_ERROR_STATUS = 500;
179
+
180
+ /** Build Analytics Engine SQL for overview query (summary + timeSeries + breakdown + topItems) */
181
+ export function buildOverviewSQL(params: QueryParams): string[] {
182
+ const { startTs, endTs } = parseTimeRange(params.range, params.start, params.end);
183
+ const start = toAETimestamp(startTs);
184
+ const end = toAETimestamp(endTs);
185
+ const catFilter = params.category ? ` AND blob5 = '${escapeSql(params.category)}'` : '';
186
+ const excludeFilter = params.excludeCategory ? ` AND blob5 != '${escapeSql(params.excludeCategory)}'` : '';
187
+ const interval = aeGroupByInterval(params.groupBy || 'hour');
188
+
189
+ // 1. Summary
190
+ const summarySQL = `
191
+ SELECT
192
+ SUM(_sample_interval) as totalRequests,
193
+ SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) as totalErrors,
194
+ AVG(double2) as avgLatency,
195
+ COUNT(DISTINCT index1) as uniqueUsers
196
+ FROM ${AE_DATASET}
197
+ WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}${excludeFilter}
198
+ `;
199
+
200
+ // 2. Time series
201
+ const tsSQL = `
202
+ SELECT
203
+ ${interval} as ts,
204
+ SUM(_sample_interval) as requests,
205
+ SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) as errors,
206
+ AVG(double2) as avgLatency,
207
+ COUNT(DISTINCT index1) as uniqueUsers
208
+ FROM ${AE_DATASET}
209
+ WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}${excludeFilter}
210
+ GROUP BY ts
211
+ ORDER BY ts
212
+ `;
213
+
214
+ // 3. Category breakdown
215
+ const breakdownSQL = `
216
+ SELECT
217
+ blob5 as label,
218
+ SUM(_sample_interval) as count
219
+ FROM ${AE_DATASET}
220
+ WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}${excludeFilter}
221
+ GROUP BY blob5
222
+ ORDER BY count DESC
223
+ LIMIT 20
224
+ `;
225
+
226
+ // 4. Top endpoints
227
+ const topSQL = `
228
+ SELECT
229
+ blob2 as label,
230
+ SUM(_sample_interval) as count,
231
+ AVG(double2) as avgLatency,
232
+ SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) * 100.0 / SUM(_sample_interval) as errorRate
233
+ FROM ${AE_DATASET}
234
+ WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}${excludeFilter}
235
+ GROUP BY blob2
236
+ ORDER BY count DESC
237
+ LIMIT 10
238
+ `;
239
+
240
+ return [summarySQL, tsSQL, breakdownSQL, topSQL];
241
+ }
242
+
243
+ /** Build Analytics Engine SQL for time series only */
244
+ export function buildTimeSeriesSQL(params: QueryParams): string {
245
+ const { startTs, endTs } = parseTimeRange(params.range, params.start, params.end);
246
+ const start = toAETimestamp(startTs);
247
+ const end = toAETimestamp(endTs);
248
+ const catFilter = params.category ? ` AND blob5 = '${escapeSql(params.category)}'` : '';
249
+ const interval = aeGroupByInterval(params.groupBy || 'hour');
250
+
251
+ return `
252
+ SELECT
253
+ ${interval} as ts,
254
+ SUM(_sample_interval) as requests,
255
+ SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) as errors,
256
+ AVG(double2) as avgLatency,
257
+ COUNT(DISTINCT index1) as uniqueUsers
258
+ FROM ${AE_DATASET}
259
+ WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}
260
+ GROUP BY ts
261
+ ORDER BY ts
262
+ `;
263
+ }
264
+
265
+ /** Build Analytics Engine SQL for breakdown */
266
+ export function buildBreakdownSQL(params: QueryParams): string {
267
+ const { startTs, endTs } = parseTimeRange(params.range, params.start, params.end);
268
+ const start = toAETimestamp(startTs);
269
+ const end = toAETimestamp(endTs);
270
+ const catFilter = params.category ? ` AND blob5 = '${escapeSql(params.category)}'` : '';
271
+
272
+ // If filtering by category, break down by subcategory; otherwise by category
273
+ const groupCol = params.category ? 'blob6' : 'blob5';
274
+
275
+ return `
276
+ SELECT
277
+ ${groupCol} as label,
278
+ SUM(_sample_interval) as count,
279
+ AVG(double2) as avgLatency,
280
+ SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) * 100.0 / SUM(_sample_interval) as errorRate
281
+ FROM ${AE_DATASET}
282
+ WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}
283
+ GROUP BY ${groupCol}
284
+ ORDER BY count DESC
285
+ LIMIT 20
286
+ `;
287
+ }
288
+
289
+ /** Build Analytics Engine SQL for top endpoints */
290
+ export function buildTopEndpointsSQL(params: QueryParams): string {
291
+ const { startTs, endTs } = parseTimeRange(params.range, params.start, params.end);
292
+ const start = toAETimestamp(startTs);
293
+ const end = toAETimestamp(endTs);
294
+ const catFilter = params.category ? ` AND blob5 = '${escapeSql(params.category)}'` : '';
295
+
296
+ return `
297
+ SELECT
298
+ blob2 as label,
299
+ SUM(_sample_interval) as count,
300
+ AVG(double2) as avgLatency,
301
+ SUM(IF(double1 >= ${SERVER_ERROR_STATUS}, _sample_interval, 0)) * 100.0 / SUM(_sample_interval) as errorRate
302
+ FROM ${AE_DATASET}
303
+ WHERE timestamp >= '${start}' AND timestamp < '${end}'${catFilter}
304
+ GROUP BY blob2
305
+ ORDER BY count DESC
306
+ LIMIT 20
307
+ `;
308
+ }
309
+
310
+ // ─── Response Transformers ───
311
+
312
+ /** Transform Analytics Engine API response to standard format */
313
+ export function transformAEResponse(
314
+ summaryData: AEQueryResult,
315
+ timeSeriesData: AEQueryResult,
316
+ breakdownData: AEQueryResult,
317
+ topData: AEQueryResult,
318
+ ): AnalyticsResponse {
319
+ // Summary
320
+ const summaryRow = summaryData.data?.[0] || {};
321
+ const summary: AnalyticsSummary = {
322
+ totalRequests: Number(summaryRow.totalRequests) || 0,
323
+ totalErrors: Number(summaryRow.totalErrors) || 0,
324
+ avgLatency: Number(summaryRow.avgLatency) || 0,
325
+ uniqueUsers: Number(summaryRow.uniqueUsers) || 0,
326
+ };
327
+
328
+ // Time series
329
+ const timeSeries: TimeSeriesPoint[] = (timeSeriesData.data || []).map(row => ({
330
+ timestamp: new Date(row.ts as string).getTime(),
331
+ requests: Number(row.requests) || 0,
332
+ errors: Number(row.errors) || 0,
333
+ avgLatency: Number(row.avgLatency) || 0,
334
+ uniqueUsers: Number(row.uniqueUsers) || 0,
335
+ }));
336
+
337
+ // Breakdown
338
+ const rawBreakdown = breakdownData.data || [];
339
+ const totalBd = rawBreakdown.reduce((sum, r) => sum + (Number(r.count) || 0), 0);
340
+ const breakdown: BreakdownItem[] = rawBreakdown.map(row => ({
341
+ label: String(row.label || 'other'),
342
+ count: Number(row.count) || 0,
343
+ percentage: totalBd > 0 ? Math.round((Number(row.count) / totalBd) * 1000) / 10 : 0,
344
+ avgLatency: Number(row.avgLatency) || 0,
345
+ errorRate: Number(row.errorRate) || 0,
346
+ }));
347
+
348
+ // Top items
349
+ const topItems: TopItem[] = (topData.data || []).map(row => ({
350
+ label: String(row.label || ''),
351
+ count: Number(row.count) || 0,
352
+ avgLatency: Number(row.avgLatency) || 0,
353
+ errorRate: Number(row.errorRate) || 0,
354
+ }));
355
+
356
+ return { timeSeries, summary, breakdown, topItems };
357
+ }
358
+
359
+ /** Empty analytics response (used when no backend is available) */
360
+ export function emptyResponse(): AnalyticsResponse {
361
+ return {
362
+ timeSeries: [],
363
+ summary: { totalRequests: 0, totalErrors: 0, avgLatency: 0, uniqueUsers: 0 },
364
+ breakdown: [],
365
+ topItems: [],
366
+ };
367
+ }
368
+
369
+ // ─── Analytics Engine API Types ───
370
+
371
+ export interface AEQueryResult {
372
+ data: Record<string, unknown>[];
373
+ meta?: Record<string, unknown>[];
374
+ rows?: number;
375
+ }
376
+
377
+ /**
378
+ * Execute a SQL query against the Analytics Engine SQL API.
379
+ * Requires CF_ACCOUNT_ID and CF_API_TOKEN environment variables.
380
+ */
381
+ export async function queryAnalyticsEngine(
382
+ sql: string,
383
+ accountId: string,
384
+ apiToken: string,
385
+ ): Promise<AEQueryResult> {
386
+ const resp = await fetch(
387
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/analytics_engine/sql`,
388
+ {
389
+ method: 'POST',
390
+ body: sql,
391
+ headers: {
392
+ Authorization: `Bearer ${apiToken}`,
393
+ 'Content-Type': 'text/plain',
394
+ },
395
+ },
396
+ );
397
+
398
+ if (!resp.ok) {
399
+ const text = await resp.text();
400
+ throw new Error(`Analytics Engine query failed (${resp.status}): ${text}`);
401
+ }
402
+
403
+ return (await resp.json()) as AEQueryResult;
404
+ }
405
+
406
+ async function queryAnalyticsHistoryStart(
407
+ accountId: string,
408
+ apiToken: string,
409
+ excludeCategory?: string,
410
+ ): Promise<number | null> {
411
+ const excludeFilter = excludeCategory ? ` WHERE blob5 != '${escapeSql(excludeCategory)}'` : '';
412
+ const result = await queryAnalyticsEngine(
413
+ `SELECT MIN(timestamp) as oldestTs FROM ${AE_DATASET}${excludeFilter}`,
414
+ accountId,
415
+ apiToken,
416
+ );
417
+ const raw = result.data?.[0]?.oldestTs;
418
+ if (!raw) return null;
419
+ const parsed = typeof raw === 'number' ? raw : new Date(String(raw)).getTime();
420
+ return Number.isFinite(parsed) ? parsed : null;
421
+ }
422
+
423
+ export async function resolveOverviewAutoRange(
424
+ env: { ANALYTICS?: AnalyticsEngineDataset; CF_ACCOUNT_ID?: string; CF_API_TOKEN?: string; LOGS?: DurableObjectNamespace },
425
+ excludeCategory?: string,
426
+ ): Promise<OverviewAutoRange> {
427
+ if (env.ANALYTICS && env.CF_ACCOUNT_ID && env.CF_API_TOKEN) {
428
+ try {
429
+ const oldestTimestamp = await queryAnalyticsHistoryStart(
430
+ env.CF_ACCOUNT_ID,
431
+ env.CF_API_TOKEN,
432
+ excludeCategory,
433
+ );
434
+ return chooseOverviewAutoRange(oldestTimestamp);
435
+ } catch (err) {
436
+ console.error('[Analytics] Failed to resolve history start from AE:', err);
437
+ }
438
+ }
439
+
440
+ if (env.LOGS) {
441
+ try {
442
+ const logsDO = env.LOGS.get(env.LOGS.idFromName('logs:main'));
443
+ const params = new URLSearchParams();
444
+ if (excludeCategory) params.set('excludeCategory', excludeCategory);
445
+ const resp = await logsDO.fetch(
446
+ new Request(`http://internal/internal/logs/history?${params.toString()}`),
447
+ );
448
+ if (resp.ok) {
449
+ const body = (await resp.json()) as { oldestTimestamp?: number | null };
450
+ return chooseOverviewAutoRange(body.oldestTimestamp ?? null);
451
+ }
452
+ } catch (err) {
453
+ console.error('[Analytics] Failed to resolve history start from LogsDO:', err);
454
+ }
455
+ }
456
+
457
+ return '1h';
458
+ }
459
+
460
+ // ─── Shared Query Executor ───
461
+
462
+ /**
463
+ * Execute analytics query against the appropriate backend (AE or LogsDO).
464
+ * Used by both /admin/api/data/analytics and /api/analytics/query.
465
+ */
466
+ export async function executeAnalyticsQuery(
467
+ env: { ANALYTICS?: AnalyticsEngineDataset; CF_ACCOUNT_ID?: string; CF_API_TOKEN?: string; LOGS?: DurableObjectNamespace },
468
+ params: QueryParams,
469
+ ): Promise<AnalyticsResponse> {
470
+ // Cloud: Analytics Engine SQL API
471
+ if (env.ANALYTICS && env.CF_ACCOUNT_ID && env.CF_API_TOKEN) {
472
+ try {
473
+ if (params.metric === 'overview') {
474
+ const sqls = buildOverviewSQL(params);
475
+ const [summary, timeSeries, breakdown, top] = await Promise.all(
476
+ sqls.map(sql => queryAnalyticsEngine(sql, env.CF_ACCOUNT_ID!, env.CF_API_TOKEN!)),
477
+ );
478
+ return transformAEResponse(summary, timeSeries, breakdown, top);
479
+ }
480
+
481
+ if (params.metric === 'timeSeries') {
482
+ const sql = buildTimeSeriesSQL(params);
483
+ const result = await queryAnalyticsEngine(sql, env.CF_ACCOUNT_ID!, env.CF_API_TOKEN!);
484
+ return {
485
+ ...emptyResponse(),
486
+ timeSeries: (result.data || []).map(row => ({
487
+ timestamp: new Date(row.ts as string).getTime(),
488
+ requests: Number(row.requests) || 0,
489
+ errors: Number(row.errors) || 0,
490
+ avgLatency: Number(row.avgLatency) || 0,
491
+ uniqueUsers: Number(row.uniqueUsers) || 0,
492
+ })),
493
+ };
494
+ }
495
+
496
+ if (params.metric === 'breakdown') {
497
+ const sql = buildBreakdownSQL(params);
498
+ const result = await queryAnalyticsEngine(sql, env.CF_ACCOUNT_ID!, env.CF_API_TOKEN!);
499
+ const rows = result.data || [];
500
+ const total = rows.reduce((sum, r) => sum + (Number(r.count) || 0), 0);
501
+ return {
502
+ ...emptyResponse(),
503
+ breakdown: rows.map(r => ({
504
+ label: String(r.label || 'other'),
505
+ count: Number(r.count) || 0,
506
+ percentage: total > 0 ? Math.round((Number(r.count) / total) * 1000) / 10 : 0,
507
+ avgLatency: Number(r.avgLatency) || 0,
508
+ errorRate: Number(r.errorRate) || 0,
509
+ })),
510
+ };
511
+ }
512
+
513
+ if (params.metric === 'topEndpoints') {
514
+ const sql = buildTopEndpointsSQL(params);
515
+ const result = await queryAnalyticsEngine(sql, env.CF_ACCOUNT_ID!, env.CF_API_TOKEN!);
516
+ return {
517
+ ...emptyResponse(),
518
+ topItems: (result.data || []).map(r => ({
519
+ label: String(r.label || ''),
520
+ count: Number(r.count) || 0,
521
+ avgLatency: Number(r.avgLatency) || 0,
522
+ errorRate: Number(r.errorRate) || 0,
523
+ })),
524
+ };
525
+ }
526
+ } catch (err) {
527
+ console.error('[Analytics] AE query failed:', err);
528
+ // Fall through to LogsDO
529
+ }
530
+ }
531
+
532
+ // Docker/Self-hosted: LogsDO SQLite query proxy
533
+ if (env.LOGS) {
534
+ try {
535
+ const logsDO = env.LOGS.get(env.LOGS.idFromName('logs:main'));
536
+ const queryParams = new URLSearchParams({
537
+ range: params.range,
538
+ category: params.category || '',
539
+ metric: params.metric,
540
+ groupBy: params.groupBy || 'hour',
541
+ excludeCategory: params.excludeCategory || '',
542
+ });
543
+ if (params.start) queryParams.set('start', params.start);
544
+ if (params.end) queryParams.set('end', params.end);
545
+ const resp = await logsDO.fetch(
546
+ new Request(`http://internal/internal/logs/query?${queryParams}`),
547
+ );
548
+ if (!resp.ok) {
549
+ console.error('[Analytics] LogsDO query returned', resp.status);
550
+ return emptyResponse();
551
+ }
552
+ return (await resp.json()) as AnalyticsResponse;
553
+ } catch (err) {
554
+ console.error('[Analytics] LogsDO query failed:', err);
555
+ }
556
+ }
557
+
558
+ // No analytics backend available
559
+ return emptyResponse();
560
+ }
561
+
562
+ // ─── Type Helpers for executeAnalyticsQuery ───
563
+
564
+ interface AnalyticsEngineDataset {
565
+ writeDataPoint(event: { indexes?: string[]; blobs?: string[]; doubles?: number[] }): void;
566
+ }
567
+
568
+ interface DurableObjectNamespace {
569
+ idFromName(name: string): DurableObjectId;
570
+ get(id: DurableObjectId): DurableObjectStub;
571
+ }
572
+
573
+ interface DurableObjectId {
574
+ toString(): string;
575
+ }
576
+
577
+ interface DurableObjectStub {
578
+ fetch(request: Request): Promise<Response>;
579
+ }