@hasna/logs 0.3.26 → 0.3.28

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 (130) hide show
  1. package/README.md +33 -10
  2. package/dashboard/dist/assets/index-C0wZYq1m.js +53 -0
  3. package/dashboard/dist/assets/index-DGNrK5qb.css +1 -0
  4. package/dashboard/dist/index.html +14 -0
  5. package/dist/cli/index.js +8511 -177
  6. package/dist/count-bmj4r2zb.js +10 -0
  7. package/dist/{diagnose-e0w5rwbc.js → diagnose-3q5cy9ra.js} +2 -2
  8. package/dist/{export-c3eqjste.js → export-cngdb9fh.js} +1 -1
  9. package/dist/{http-zm3ph78w.js → http-r0xc3d2s.js} +79 -8
  10. package/dist/index-931pbyn5.js +141 -0
  11. package/dist/index-b5c72f1p.js +7 -0
  12. package/dist/{index-7w7v7hnr.js → index-by1pdzbr.js} +14 -5
  13. package/dist/{index-3dr7d80h.js → index-e1930v9b.js} +12 -8
  14. package/dist/{index-eh9bkbpa.js → index-e72k53yq.js} +10 -2
  15. package/dist/{index-edn08m6f.js → index-gcd14q2f.js} +9 -6
  16. package/dist/index-hq6kzaah.js +26 -0
  17. package/dist/index-j34f36wy.js +5672 -0
  18. package/dist/{index-5qznfyah.js → index-q27bgpr1.js} +1086 -1646
  19. package/dist/index-qk8dbvbc.js +1859 -0
  20. package/dist/index-t3x838zw.js +2583 -0
  21. package/dist/{index-gc0zvs88.js → index-y2y0mdtd.js} +596 -37
  22. package/dist/{index-ww5ggfv3.js → index-zkb3z95a.js} +12 -9
  23. package/dist/index.js +2990 -22
  24. package/dist/{jobs-ypmmc2ma.js → jobs-hsgyhfvm.js} +2 -1
  25. package/dist/mcp/index.js +1473 -4286
  26. package/dist/{query-7jwj05er.js → query-c5a43zx3.js} +3 -2
  27. package/dist/server/index.js +2944 -417
  28. package/dist/storage.js +50 -0
  29. package/package.json +27 -8
  30. package/biome.json +0 -13
  31. package/bun.lock +0 -376
  32. package/dashboard/README.md +0 -73
  33. package/dashboard/bun.lock +0 -526
  34. package/dashboard/eslint.config.js +0 -23
  35. package/dashboard/index.html +0 -13
  36. package/dashboard/package.json +0 -32
  37. package/dashboard/src/App.css +0 -184
  38. package/dashboard/src/App.tsx +0 -49
  39. package/dashboard/src/api.ts +0 -33
  40. package/dashboard/src/assets/hero.png +0 -0
  41. package/dashboard/src/assets/react.svg +0 -1
  42. package/dashboard/src/assets/vite.svg +0 -1
  43. package/dashboard/src/index.css +0 -111
  44. package/dashboard/src/main.tsx +0 -10
  45. package/dashboard/src/pages/Alerts.tsx +0 -69
  46. package/dashboard/src/pages/Issues.tsx +0 -50
  47. package/dashboard/src/pages/Perf.tsx +0 -75
  48. package/dashboard/src/pages/Projects.tsx +0 -67
  49. package/dashboard/src/pages/Summary.tsx +0 -67
  50. package/dashboard/src/pages/Tail.tsx +0 -65
  51. package/dashboard/tsconfig.app.json +0 -28
  52. package/dashboard/tsconfig.json +0 -7
  53. package/dashboard/tsconfig.node.json +0 -26
  54. package/dashboard/vite.config.ts +0 -14
  55. package/dist/count-x3n7qg3c.js +0 -9
  56. package/dist/index-997bkzr2.js +0 -15
  57. package/dist/index-pen6t0yc.js +0 -10794
  58. package/sdk/package.json +0 -27
  59. package/sdk/src/index.ts +0 -143
  60. package/sdk/src/types.ts +0 -56
  61. package/src/cli/entrypoints.test.ts +0 -63
  62. package/src/cli/index.ts +0 -471
  63. package/src/db/index.test.ts +0 -33
  64. package/src/db/index.ts +0 -189
  65. package/src/db/migrations/001_alert_rules.ts +0 -21
  66. package/src/db/migrations/002_issues.ts +0 -21
  67. package/src/db/migrations/003_retention.ts +0 -15
  68. package/src/db/migrations/004_page_auth.ts +0 -13
  69. package/src/db/pg-migrations.ts +0 -167
  70. package/src/index.ts +0 -1
  71. package/src/lib/alerts.test.ts +0 -67
  72. package/src/lib/alerts.ts +0 -117
  73. package/src/lib/browser-script.test.ts +0 -35
  74. package/src/lib/browser-script.ts +0 -31
  75. package/src/lib/compare.test.ts +0 -52
  76. package/src/lib/compare.ts +0 -85
  77. package/src/lib/count.test.ts +0 -44
  78. package/src/lib/count.ts +0 -55
  79. package/src/lib/diagnose.test.ts +0 -55
  80. package/src/lib/diagnose.ts +0 -91
  81. package/src/lib/export.test.ts +0 -66
  82. package/src/lib/export.ts +0 -65
  83. package/src/lib/github.ts +0 -38
  84. package/src/lib/health.test.ts +0 -48
  85. package/src/lib/health.ts +0 -51
  86. package/src/lib/ingest.test.ts +0 -57
  87. package/src/lib/ingest.ts +0 -78
  88. package/src/lib/issues.test.ts +0 -79
  89. package/src/lib/issues.ts +0 -70
  90. package/src/lib/jobs.test.ts +0 -69
  91. package/src/lib/jobs.ts +0 -63
  92. package/src/lib/lighthouse.ts +0 -65
  93. package/src/lib/package-meta.test.ts +0 -43
  94. package/src/lib/package-meta.ts +0 -80
  95. package/src/lib/page-auth.test.ts +0 -54
  96. package/src/lib/page-auth.ts +0 -48
  97. package/src/lib/parse-time.test.ts +0 -37
  98. package/src/lib/parse-time.ts +0 -14
  99. package/src/lib/perf.test.ts +0 -45
  100. package/src/lib/perf.ts +0 -46
  101. package/src/lib/projects.test.ts +0 -73
  102. package/src/lib/projects.ts +0 -69
  103. package/src/lib/query.test.ts +0 -104
  104. package/src/lib/query.ts +0 -84
  105. package/src/lib/retention.test.ts +0 -42
  106. package/src/lib/retention.ts +0 -62
  107. package/src/lib/rotate.test.ts +0 -37
  108. package/src/lib/rotate.ts +0 -27
  109. package/src/lib/scanner.ts +0 -131
  110. package/src/lib/scheduler.ts +0 -63
  111. package/src/lib/session-context.ts +0 -28
  112. package/src/lib/summarize.test.ts +0 -38
  113. package/src/lib/summarize.ts +0 -23
  114. package/src/mcp/http.test.ts +0 -92
  115. package/src/mcp/http.ts +0 -135
  116. package/src/mcp/index.test.ts +0 -27
  117. package/src/mcp/index.ts +0 -444
  118. package/src/server/index.ts +0 -61
  119. package/src/server/routes/alerts.ts +0 -32
  120. package/src/server/routes/issues.ts +0 -43
  121. package/src/server/routes/jobs.ts +0 -32
  122. package/src/server/routes/logs.ts +0 -113
  123. package/src/server/routes/perf.ts +0 -23
  124. package/src/server/routes/projects.ts +0 -67
  125. package/src/server/routes/stream.ts +0 -43
  126. package/src/server/server.test.ts +0 -194
  127. package/src/types/index.ts +0 -119
  128. package/tsconfig.json +0 -22
  129. /package/dashboard/{public → dist}/favicon.svg +0 -0
  130. /package/dashboard/{public → dist}/icons.svg +0 -0
@@ -5,12 +5,17 @@ import {
5
5
  runRetentionForProject,
6
6
  setPageAuth,
7
7
  setRetentionPolicy,
8
- startScheduler
9
- } from "../index-gc0zvs88.js";
8
+ startScheduler,
9
+ structuredLogPayloadToEntries,
10
+ validateStructuredLogReferences
11
+ } from "../index-y2y0mdtd.js";
12
+ import {
13
+ countLogs
14
+ } from "../index-gcd14q2f.js";
10
15
  import {
11
16
  exportToCsv,
12
17
  exportToJson
13
- } from "../index-eh9bkbpa.js";
18
+ } from "../index-e72k53yq.js";
14
19
  import {
15
20
  getHealth
16
21
  } from "../index-cpvq9np9.js";
@@ -20,43 +25,60 @@ import {
20
25
  createProject,
21
26
  deleteAlertRule,
22
27
  exitIfMetadataRequest,
23
- getDb,
24
- getIssue,
28
+ exportEventsToJson,
29
+ getEvent,
25
30
  getLatestSnapshot,
26
31
  getPerfTrend,
27
32
  getProject,
33
+ getTestReport,
34
+ hasBufferedEventCatalogEvent,
35
+ hasBufferedLogEvent,
36
+ hasOption,
28
37
  ingestBatch,
29
38
  ingestLog,
39
+ ingestUniversalEvent,
30
40
  listAlertRules,
31
- listIssues,
32
41
  listPages,
33
42
  listProjects,
34
43
  readOptionValue,
35
44
  resolveProjectId,
45
+ searchEvents,
46
+ searchTestReports,
47
+ subscribeEventCatalogEvents,
48
+ subscribeLogEvents,
36
49
  summarizeLogs,
37
50
  updateAlertRule,
38
- updateIssueStatus,
39
- updateProject
40
- } from "../index-pen6t0yc.js";
51
+ updateProject,
52
+ validateUniversalEventInput
53
+ } from "../index-qk8dbvbc.js";
54
+ import {
55
+ getDb,
56
+ getIssue,
57
+ listIssues,
58
+ updateIssueStatus
59
+ } from "../index-t3x838zw.js";
41
60
  import {
42
61
  createJob,
43
62
  deleteJob,
44
63
  listJobs,
45
64
  updateJob
46
- } from "../index-3dr7d80h.js";
65
+ } from "../index-e1930v9b.js";
47
66
  import {
48
67
  getLogContext,
49
68
  searchLogs,
50
69
  tailLogs
51
- } from "../index-ww5ggfv3.js";
52
- import {
53
- countLogs
54
- } from "../index-edn08m6f.js";
70
+ } from "../index-zkb3z95a.js";
71
+ import"../index-b5c72f1p.js";
55
72
  import {
56
73
  parseTime
57
- } from "../index-997bkzr2.js";
74
+ } from "../index-hq6kzaah.js";
58
75
  import"../index-re3ntm60.js";
59
76
 
77
+ // src/server/index.ts
78
+ import { existsSync } from "fs";
79
+ import { dirname as dirname2, resolve } from "path";
80
+ import { fileURLToPath } from "url";
81
+
60
82
  // node_modules/hono/dist/compose.js
61
83
  var compose = (middleware, onError, onNotFound) => {
62
84
  return (context, next) => {
@@ -429,7 +451,7 @@ var HonoRequest = class {
429
451
  return headerData;
430
452
  }
431
453
  async parseBody(options) {
432
- return this.bodyCache.parsedBody ??= await parseBody(this, options);
454
+ return parseBody(this, options);
433
455
  }
434
456
  #cachedBody = (key) => {
435
457
  const { bodyCache, raw } = this;
@@ -457,6 +479,9 @@ var HonoRequest = class {
457
479
  arrayBuffer() {
458
480
  return this.#cachedBody("arrayBuffer");
459
481
  }
482
+ bytes() {
483
+ return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer));
484
+ }
460
485
  blob() {
461
486
  return this.#cachedBody("blob");
462
487
  }
@@ -793,7 +818,7 @@ var Hono = class _Hono {
793
818
  handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res;
794
819
  handler[COMPOSED_HANDLER] = r.handler;
795
820
  }
796
- subApp.#addRoute(r.method, r.path, handler);
821
+ subApp.#addRoute(r.method, r.path, handler, r.basePath);
797
822
  });
798
823
  return this;
799
824
  }
@@ -840,7 +865,7 @@ var Hono = class _Hono {
840
865
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
841
866
  return (request) => {
842
867
  const url = new URL(request.url);
843
- url.pathname = url.pathname.slice(pathPrefixLength) || "/";
868
+ url.pathname = this.getPath(request).slice(pathPrefixLength) || "/";
844
869
  return new Request(url, request);
845
870
  };
846
871
  })();
@@ -854,10 +879,15 @@ var Hono = class _Hono {
854
879
  this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler);
855
880
  return this;
856
881
  }
857
- #addRoute(method, path, handler) {
882
+ #addRoute(method, path, handler, baseRoutePath) {
858
883
  method = method.toUpperCase();
859
884
  path = mergePath(this._basePath, path);
860
- const r = { basePath: this._basePath, path, method, handler };
885
+ const r = {
886
+ basePath: baseRoutePath !== undefined ? mergePath(this._basePath, baseRoutePath) : this._basePath,
887
+ path,
888
+ method,
889
+ handler
890
+ };
861
891
  this.router.add(method, path, [handler, r]);
862
892
  this.routes.push(r);
863
893
  }
@@ -1595,97 +1625,12 @@ var Hono2 = class extends Hono {
1595
1625
  }
1596
1626
  };
1597
1627
 
1598
- // node_modules/hono/dist/middleware/cors/index.js
1599
- var cors = (options) => {
1600
- const defaults = {
1601
- origin: "*",
1602
- allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
1603
- allowHeaders: [],
1604
- exposeHeaders: []
1605
- };
1606
- const opts = {
1607
- ...defaults,
1608
- ...options
1609
- };
1610
- const findAllowOrigin = ((optsOrigin) => {
1611
- if (typeof optsOrigin === "string") {
1612
- if (optsOrigin === "*") {
1613
- return () => optsOrigin;
1614
- } else {
1615
- return (origin) => optsOrigin === origin ? origin : null;
1616
- }
1617
- } else if (typeof optsOrigin === "function") {
1618
- return optsOrigin;
1619
- } else {
1620
- return (origin) => optsOrigin.includes(origin) ? origin : null;
1621
- }
1622
- })(opts.origin);
1623
- const findAllowMethods = ((optsAllowMethods) => {
1624
- if (typeof optsAllowMethods === "function") {
1625
- return optsAllowMethods;
1626
- } else if (Array.isArray(optsAllowMethods)) {
1627
- return () => optsAllowMethods;
1628
- } else {
1629
- return () => [];
1630
- }
1631
- })(opts.allowMethods);
1632
- return async function cors2(c, next) {
1633
- function set(key, value) {
1634
- c.res.headers.set(key, value);
1635
- }
1636
- const allowOrigin = await findAllowOrigin(c.req.header("origin") || "", c);
1637
- if (allowOrigin) {
1638
- set("Access-Control-Allow-Origin", allowOrigin);
1639
- }
1640
- if (opts.credentials) {
1641
- set("Access-Control-Allow-Credentials", "true");
1642
- }
1643
- if (opts.exposeHeaders?.length) {
1644
- set("Access-Control-Expose-Headers", opts.exposeHeaders.join(","));
1645
- }
1646
- if (c.req.method === "OPTIONS") {
1647
- if (opts.origin !== "*") {
1648
- set("Vary", "Origin");
1649
- }
1650
- if (opts.maxAge != null) {
1651
- set("Access-Control-Max-Age", opts.maxAge.toString());
1652
- }
1653
- const allowMethods = await findAllowMethods(c.req.header("origin") || "", c);
1654
- if (allowMethods.length) {
1655
- set("Access-Control-Allow-Methods", allowMethods.join(","));
1656
- }
1657
- let headers = opts.allowHeaders;
1658
- if (!headers?.length) {
1659
- const requestHeaders = c.req.header("Access-Control-Request-Headers");
1660
- if (requestHeaders) {
1661
- headers = requestHeaders.split(/\s*,\s*/);
1662
- }
1663
- }
1664
- if (headers?.length) {
1665
- set("Access-Control-Allow-Headers", headers.join(","));
1666
- c.res.headers.append("Vary", "Access-Control-Request-Headers");
1667
- }
1668
- c.res.headers.delete("Content-Length");
1669
- c.res.headers.delete("Content-Type");
1670
- return new Response(null, {
1671
- headers: c.res.headers,
1672
- status: 204,
1673
- statusText: "No Content"
1674
- });
1675
- }
1676
- await next();
1677
- if (opts.origin !== "*") {
1678
- c.header("Vary", "Origin", { append: true });
1679
- }
1680
- };
1681
- };
1682
-
1683
1628
  // node_modules/hono/dist/adapter/bun/serve-static.js
1684
1629
  import { stat } from "fs/promises";
1685
1630
  import { join } from "path";
1686
1631
 
1687
1632
  // node_modules/hono/dist/utils/compress.js
1688
- var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/(?!event-stream(?:[;\s]|$))[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
1633
+ var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/(?!event-stream(?:[;\s]|$))[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|msgpack|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|vnd\.msgpack|wasm|x-httpd-php|x-javascript|x-msgpack|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml|msgpack))(?:[;\s]|$)/i;
1689
1634
 
1690
1635
  // node_modules/hono/dist/utils/mime.js
1691
1636
  var getMimeType = (filename, mimes = baseMimes) => {
@@ -1694,11 +1639,7 @@ var getMimeType = (filename, mimes = baseMimes) => {
1694
1639
  if (!match2) {
1695
1640
  return;
1696
1641
  }
1697
- let mimeType = mimes[match2[1].toLowerCase()];
1698
- if (mimeType && mimeType.startsWith("text")) {
1699
- mimeType += "; charset=utf-8";
1700
- }
1701
- return mimeType;
1642
+ return mimes[match2[1].toLowerCase()];
1702
1643
  };
1703
1644
  var _baseMimes = {
1704
1645
  aac: "audio/aac",
@@ -1707,25 +1648,25 @@ var _baseMimes = {
1707
1648
  av1: "video/av1",
1708
1649
  bin: "application/octet-stream",
1709
1650
  bmp: "image/bmp",
1710
- css: "text/css",
1711
- csv: "text/csv",
1651
+ css: "text/css; charset=utf-8",
1652
+ csv: "text/csv; charset=utf-8",
1712
1653
  eot: "application/vnd.ms-fontobject",
1713
1654
  epub: "application/epub+zip",
1714
1655
  gif: "image/gif",
1715
1656
  gz: "application/gzip",
1716
- htm: "text/html",
1717
- html: "text/html",
1657
+ htm: "text/html; charset=utf-8",
1658
+ html: "text/html; charset=utf-8",
1718
1659
  ico: "image/x-icon",
1719
- ics: "text/calendar",
1660
+ ics: "text/calendar; charset=utf-8",
1720
1661
  jpeg: "image/jpeg",
1721
1662
  jpg: "image/jpeg",
1722
- js: "text/javascript",
1663
+ js: "text/javascript; charset=utf-8",
1723
1664
  json: "application/json",
1724
1665
  jsonld: "application/ld+json",
1725
1666
  map: "application/json",
1726
1667
  mid: "audio/x-midi",
1727
1668
  midi: "audio/x-midi",
1728
- mjs: "text/javascript",
1669
+ mjs: "text/javascript; charset=utf-8",
1729
1670
  mp3: "audio/mpeg",
1730
1671
  mp4: "video/mp4",
1731
1672
  mpeg: "video/mpeg",
@@ -1737,12 +1678,12 @@ var _baseMimes = {
1737
1678
  pdf: "application/pdf",
1738
1679
  png: "image/png",
1739
1680
  rtf: "application/rtf",
1740
- svg: "image/svg+xml",
1681
+ svg: "image/svg+xml; charset=utf-8",
1741
1682
  tif: "image/tiff",
1742
1683
  tiff: "image/tiff",
1743
1684
  ts: "video/mp2t",
1744
1685
  ttf: "font/ttf",
1745
- txt: "text/plain",
1686
+ txt: "text/plain; charset=utf-8",
1746
1687
  wasm: "application/wasm",
1747
1688
  webm: "video/webm",
1748
1689
  weba: "audio/webm",
@@ -1750,8 +1691,8 @@ var _baseMimes = {
1750
1691
  webp: "image/webp",
1751
1692
  woff: "font/woff",
1752
1693
  woff2: "font/woff2",
1753
- xhtml: "application/xhtml+xml",
1754
- xml: "application/xml",
1694
+ xhtml: "application/xhtml+xml; charset=utf-8",
1695
+ xml: "application/xml; charset=utf-8",
1755
1696
  zip: "application/zip",
1756
1697
  "3gp": "video/3gpp",
1757
1698
  "3g2": "video/3gpp2",
@@ -1798,7 +1739,7 @@ var serveStatic = (options) => {
1798
1739
  } else {
1799
1740
  try {
1800
1741
  filename = tryDecodeURI(c.req.path);
1801
- if (/(?:^|[\/\\])\.\.(?:$|[\/\\])/.test(filename)) {
1742
+ if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}|\\/.test(filename)) {
1802
1743
  throw new Error;
1803
1744
  }
1804
1745
  } catch {
@@ -1843,7 +1784,7 @@ var serveStatic = (options) => {
1843
1784
  };
1844
1785
 
1845
1786
  // node_modules/hono/dist/adapter/bun/serve-static.js
1846
- var serveStatic2 = (options) => {
1787
+ var serveStatic2 = (options = {}) => {
1847
1788
  return async function serveStatic22(c, next) {
1848
1789
  const getContent = async (path) => {
1849
1790
  const file = Bun.file(path);
@@ -1951,14 +1892,96 @@ var upgradeWebSocket = defineWebSocketHelper((c, events) => {
1951
1892
  return;
1952
1893
  });
1953
1894
 
1895
+ // node_modules/hono/dist/middleware/cors/index.js
1896
+ var cors = (options) => {
1897
+ const opts = {
1898
+ origin: "*",
1899
+ allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
1900
+ allowHeaders: [],
1901
+ exposeHeaders: [],
1902
+ ...options
1903
+ };
1904
+ const findAllowOrigin = ((optsOrigin) => {
1905
+ if (typeof optsOrigin === "string") {
1906
+ if (optsOrigin === "*") {
1907
+ return () => optsOrigin;
1908
+ } else {
1909
+ return (origin) => optsOrigin === origin ? origin : null;
1910
+ }
1911
+ } else if (typeof optsOrigin === "function") {
1912
+ return optsOrigin;
1913
+ } else {
1914
+ return (origin) => optsOrigin.includes(origin) ? origin : null;
1915
+ }
1916
+ })(opts.origin);
1917
+ const findAllowMethods = ((optsAllowMethods) => {
1918
+ if (typeof optsAllowMethods === "function") {
1919
+ return optsAllowMethods;
1920
+ } else if (Array.isArray(optsAllowMethods)) {
1921
+ return () => optsAllowMethods;
1922
+ } else {
1923
+ return () => [];
1924
+ }
1925
+ })(opts.allowMethods);
1926
+ return async function cors2(c, next) {
1927
+ function set(key, value) {
1928
+ c.res.headers.set(key, value);
1929
+ }
1930
+ const allowOrigin = await findAllowOrigin(c.req.header("origin") || "", c);
1931
+ if (allowOrigin) {
1932
+ set("Access-Control-Allow-Origin", allowOrigin);
1933
+ }
1934
+ if (opts.credentials) {
1935
+ set("Access-Control-Allow-Credentials", "true");
1936
+ }
1937
+ if (opts.exposeHeaders?.length) {
1938
+ set("Access-Control-Expose-Headers", opts.exposeHeaders.join(","));
1939
+ }
1940
+ if (c.req.method === "OPTIONS") {
1941
+ if (opts.origin !== "*") {
1942
+ set("Vary", "Origin");
1943
+ }
1944
+ if (opts.maxAge != null) {
1945
+ set("Access-Control-Max-Age", opts.maxAge.toString());
1946
+ }
1947
+ const allowMethods = await findAllowMethods(c.req.header("origin") || "", c);
1948
+ if (allowMethods.length) {
1949
+ set("Access-Control-Allow-Methods", allowMethods.join(","));
1950
+ }
1951
+ let headers = opts.allowHeaders;
1952
+ if (!headers?.length) {
1953
+ const requestHeaders = c.req.header("Access-Control-Request-Headers");
1954
+ if (requestHeaders) {
1955
+ headers = requestHeaders.split(/\s*,\s*/);
1956
+ }
1957
+ }
1958
+ if (headers?.length) {
1959
+ set("Access-Control-Allow-Headers", headers.join(","));
1960
+ c.res.headers.append("Vary", "Access-Control-Request-Headers");
1961
+ }
1962
+ c.res.headers.delete("Content-Length");
1963
+ c.res.headers.delete("Content-Type");
1964
+ return new Response(null, {
1965
+ headers: c.res.headers,
1966
+ status: 204,
1967
+ statusText: "No Content"
1968
+ });
1969
+ }
1970
+ await next();
1971
+ if (opts.origin !== "*") {
1972
+ c.header("Vary", "Origin", { append: true });
1973
+ }
1974
+ };
1975
+ };
1976
+
1954
1977
  // src/lib/browser-script.ts
1955
1978
  function getBrowserScript(serverUrl) {
1956
1979
  return `(function(){
1957
- var cfg={url:'${serverUrl}',projectId:null};
1980
+ var cfg={url:'${serverUrl}',projectId:null,browserToken:null};
1958
1981
  var el=document.currentScript;
1959
- if(el){cfg.projectId=el.getAttribute('data-project')||null;}
1982
+ if(el){cfg.projectId=el.getAttribute('data-project')||null;cfg.browserToken=el.getAttribute('data-browser-token')||el.getAttribute('data-write-token')||null;}
1960
1983
  var q=[];
1961
- function flush(){if(!q.length)return;var b=q.splice(0);fetch(cfg.url+'/api/logs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b),keepalive:true}).catch(function(){});}
1984
+ function flush(){if(!q.length)return;var b=q.splice(0);var h={'Content-Type':'application/json'};if(cfg.browserToken)h['X-Logs-Browser-Token']=cfg.browserToken;fetch(cfg.url+'/api/logs',{method:'POST',headers:h,body:JSON.stringify(b),keepalive:true}).catch(function(){});}
1962
1985
  setInterval(flush,2000);
1963
1986
  function push(level,msg,extra){
1964
1987
  q.push(Object.assign({level:level,message:String(msg),source:'script',url:location.href,timestamp:new Date().toISOString()},cfg.projectId?{project_id:cfg.projectId}:{},extra||{}));
@@ -1975,112 +1998,1421 @@ window.__logs={push:push,flush:flush,config:cfg};
1975
1998
  })();`;
1976
1999
  }
1977
2000
 
1978
- // src/server/routes/alerts.ts
1979
- function alertsRoutes(db) {
1980
- const app = new Hono2;
1981
- app.post("/", async (c) => {
1982
- const body = await c.req.json();
1983
- if (!body.project_id || !body.name)
1984
- return c.json({ error: "project_id and name required" }, 422);
1985
- return c.json(createAlertRule(db, body), 201);
1986
- });
1987
- app.get("/", (c) => {
1988
- const { project_id } = c.req.query();
1989
- return c.json(listAlertRules(db, project_id || undefined));
1990
- });
1991
- app.put("/:id", async (c) => {
1992
- const body = await c.req.json();
1993
- const updated = updateAlertRule(db, c.req.param("id"), body);
1994
- if (!updated)
1995
- return c.json({ error: "not found" }, 404);
1996
- return c.json(updated);
1997
- });
1998
- app.delete("/:id", (c) => {
1999
- deleteAlertRule(db, c.req.param("id"));
2000
- return c.json({ deleted: true });
2001
+ // src/lib/browser-ingest-tokens.ts
2002
+ import { createHash, randomBytes } from "crypto";
2003
+ function createBrowserIngestToken(db, projectId, opts = {}) {
2004
+ const token = `olb_${randomBytes(32).toString("hex")}`;
2005
+ const tokenHash = hashBrowserToken(token);
2006
+ const allowedOrigins = normalizeAllowedOrigins(opts.allowed_origins ?? []);
2007
+ const row = db.prepare(`
2008
+ INSERT INTO browser_ingest_tokens (project_id, token_hash, token_prefix, name, allowed_origins)
2009
+ VALUES ($project_id, $token_hash, $token_prefix, $name, $allowed_origins)
2010
+ RETURNING id, project_id, token_prefix, name, allowed_origins, enabled, created_at, last_used_at
2011
+ `).get({
2012
+ $project_id: projectId,
2013
+ $token_hash: tokenHash,
2014
+ $token_prefix: token.slice(0, 12),
2015
+ $name: opts.name ?? null,
2016
+ $allowed_origins: allowedOrigins.length > 0 ? JSON.stringify(allowedOrigins) : null
2001
2017
  });
2002
- return app;
2018
+ return { ...row, token };
2003
2019
  }
2004
-
2005
- // src/server/routes/issues.ts
2006
- function issuesRoutes(db) {
2007
- const app = new Hono2;
2008
- app.get("/", (c) => {
2009
- const { project_id, status, limit } = c.req.query();
2010
- return c.json(listIssues(db, project_id || undefined, status || undefined, limit ? Number(limit) : 50));
2011
- });
2012
- app.get("/:id", (c) => {
2013
- const issue = getIssue(db, c.req.param("id"));
2014
- if (!issue)
2015
- return c.json({ error: "not found" }, 404);
2016
- return c.json(issue);
2017
- });
2018
- app.get("/:id/logs", (c) => {
2019
- const issue = getIssue(db, c.req.param("id"));
2020
- if (!issue)
2021
- return c.json({ error: "not found" }, 404);
2022
- const rows = searchLogs(db, {
2023
- project_id: issue.project_id ?? undefined,
2024
- level: issue.level,
2025
- service: issue.service ?? undefined,
2026
- text: issue.message_template.slice(0, 50),
2027
- limit: 50
2028
- });
2029
- return c.json(rows);
2030
- });
2031
- app.put("/:id", async (c) => {
2032
- const { status } = await c.req.json();
2033
- if (!["open", "resolved", "ignored"].includes(status))
2034
- return c.json({ error: "invalid status" }, 422);
2035
- const updated = updateIssueStatus(db, c.req.param("id"), status);
2036
- if (!updated)
2037
- return c.json({ error: "not found" }, 404);
2038
- return c.json(updated);
2039
- });
2040
- return app;
2020
+ function listBrowserIngestTokens(db, projectId) {
2021
+ return db.prepare(`
2022
+ SELECT id, project_id, token_prefix, name, allowed_origins, enabled, created_at, last_used_at
2023
+ FROM browser_ingest_tokens
2024
+ WHERE project_id = ?
2025
+ ORDER BY created_at DESC
2026
+ `).all(projectId);
2041
2027
  }
2042
-
2043
- // src/server/routes/jobs.ts
2044
- function jobsRoutes(db) {
2045
- const app = new Hono2;
2046
- app.post("/", async (c) => {
2047
- const body = await c.req.json();
2048
- if (!body.project_id || !body.schedule)
2049
- return c.json({ error: "project_id and schedule are required" }, 422);
2050
- return c.json(createJob(db, body), 201);
2051
- });
2052
- app.get("/", (c) => {
2053
- const { project_id } = c.req.query();
2054
- return c.json(listJobs(db, project_id || undefined));
2055
- });
2056
- app.put("/:id", async (c) => {
2057
- const body = await c.req.json();
2058
- const updated = updateJob(db, c.req.param("id"), body);
2059
- if (!updated)
2060
- return c.json({ error: "not found" }, 404);
2061
- return c.json(updated);
2062
- });
2063
- app.delete("/:id", (c) => {
2064
- deleteJob(db, c.req.param("id"));
2065
- return c.json({ deleted: true });
2066
- });
2067
- return app;
2028
+ function revokeBrowserIngestToken(db, projectId, tokenId) {
2029
+ const result = db.prepare("UPDATE browser_ingest_tokens SET enabled = 0 WHERE id = ? AND project_id = ? AND enabled = 1").run(tokenId, projectId);
2030
+ return result.changes > 0;
2031
+ }
2032
+ function validateBrowserIngestToken(db, token, origin) {
2033
+ if (!token || !token.startsWith("olb_"))
2034
+ return null;
2035
+ const row = db.prepare(`
2036
+ SELECT id, project_id, token_prefix, allowed_origins
2037
+ FROM browser_ingest_tokens
2038
+ WHERE token_hash = ? AND enabled = 1
2039
+ `).get(hashBrowserToken(token));
2040
+ if (!row)
2041
+ return null;
2042
+ const allowedOrigins = parseAllowedOrigins(row.allowed_origins);
2043
+ if (allowedOrigins.length > 0) {
2044
+ if (!origin)
2045
+ return null;
2046
+ const normalizedOrigin = normalizeOrigin(origin);
2047
+ if (!normalizedOrigin || !allowedOrigins.includes(normalizedOrigin))
2048
+ return null;
2049
+ }
2050
+ return {
2051
+ id: row.id,
2052
+ project_id: row.project_id,
2053
+ token_prefix: row.token_prefix,
2054
+ allowed_origins: allowedOrigins
2055
+ };
2056
+ }
2057
+ function touchBrowserIngestToken(db, tokenId) {
2058
+ db.prepare("UPDATE browser_ingest_tokens SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?").run(tokenId);
2059
+ }
2060
+ function normalizeAllowedOrigins(origins) {
2061
+ const normalized = new Set;
2062
+ for (const origin of origins) {
2063
+ const value = normalizeOrigin(origin);
2064
+ if (value)
2065
+ normalized.add(value);
2066
+ }
2067
+ return [...normalized];
2068
+ }
2069
+ function parseAllowedOrigins(value) {
2070
+ if (!value)
2071
+ return [];
2072
+ try {
2073
+ const parsed = JSON.parse(value);
2074
+ return Array.isArray(parsed) ? normalizeAllowedOrigins(parsed.filter((item) => typeof item === "string")) : [];
2075
+ } catch {
2076
+ return [];
2077
+ }
2078
+ }
2079
+ function normalizeOrigin(origin) {
2080
+ try {
2081
+ return new URL(origin).origin;
2082
+ } catch {
2083
+ return null;
2084
+ }
2085
+ }
2086
+ function hashBrowserToken(token) {
2087
+ return createHash("sha256").update(token).digest("hex");
2088
+ }
2089
+
2090
+ // src/server/auth.ts
2091
+ var TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
2092
+ function getConfiguredApiToken() {
2093
+ const token = process.env.HASNA_LOGS_API_TOKEN?.trim() || process.env.LOGS_API_TOKEN?.trim();
2094
+ return token || null;
2095
+ }
2096
+ function isApiRequestAuthorized(c) {
2097
+ const token = getConfiguredApiToken();
2098
+ if (!token)
2099
+ return isLocalOpenModeEnabled() && isTrustedLocalRequest(c);
2100
+ const authorization = c.req.header("authorization") ?? "";
2101
+ const bearer = /^Bearer\s+(.+)$/i.exec(authorization)?.[1];
2102
+ const headerToken = c.req.header("x-logs-token");
2103
+ return bearer === token || headerToken === token;
2104
+ }
2105
+ function apiUnauthorizedResponse(c) {
2106
+ return c.json({
2107
+ error: "Unauthorized. Configure HASNA_LOGS_API_TOKEN/LOGS_API_TOKEN or explicitly enable trusted local mode with --local-open."
2108
+ }, 401);
2109
+ }
2110
+ function requireApiTokenOrBrowserIngest(db) {
2111
+ return async (c, next) => {
2112
+ if (isApiRequestAuthorized(c)) {
2113
+ await next();
2114
+ return;
2115
+ }
2116
+ if (isBrowserWriteRequest(c) && getBrowserIngestAuthorization(db, c)) {
2117
+ await next();
2118
+ return;
2119
+ }
2120
+ return apiUnauthorizedResponse(c);
2121
+ };
2122
+ }
2123
+ function authorizeLogIngest(db, c) {
2124
+ const token = getConfiguredApiToken();
2125
+ if (!token) {
2126
+ const browserToken2 = getBrowserIngestAuthorization(db, c);
2127
+ if (browserToken2)
2128
+ return { kind: "browser-token", token: browserToken2 };
2129
+ return isLocalOpenModeEnabled() && isTrustedLocalRequest(c) ? { kind: "trusted-local" } : null;
2130
+ }
2131
+ if (isApiRequestAuthorized(c))
2132
+ return { kind: "api-token" };
2133
+ const browserToken = getBrowserIngestAuthorization(db, c);
2134
+ return browserToken ? { kind: "browser-token", token: browserToken } : null;
2135
+ }
2136
+ function getBrowserIngestAuthorization(db, c) {
2137
+ const token = c.req.header("x-logs-browser-token") ?? c.req.header("x-logs-write-token");
2138
+ return validateBrowserIngestToken(db, token, c.req.header("origin"));
2139
+ }
2140
+ function isBrowserWriteRequest(c) {
2141
+ if (c.req.method.toUpperCase() !== "POST")
2142
+ return false;
2143
+ const path = new URL(c.req.url).pathname.replace(/\/+$/, "");
2144
+ return path === "/api/logs" || path === "/api/events";
2145
+ }
2146
+ function isLocalOpenModeEnabled() {
2147
+ return ["HASNA_LOGS_LOCAL_OPEN", "LOGS_LOCAL_OPEN"].some((name) => TRUE_ENV_VALUES.has(process.env[name]?.trim().toLowerCase() ?? ""));
2148
+ }
2149
+ function isTrustedLocalRequest(c) {
2150
+ const url = new URL(c.req.url);
2151
+ const host = forwardedHost(c.req.header("x-forwarded-host")) ?? hostWithoutPort(c.req.header("host")) ?? url.hostname;
2152
+ return isLocalHost(host) && isLocalOrigin(c.req.header("origin"));
2153
+ }
2154
+ function forwardedHost(value) {
2155
+ const first = value?.split(",")[0]?.trim();
2156
+ return first ? hostWithoutPort(first) : null;
2157
+ }
2158
+ function hostWithoutPort(value) {
2159
+ if (!value)
2160
+ return null;
2161
+ if (value.startsWith("["))
2162
+ return value.slice(1, value.indexOf("]"));
2163
+ return value.split(":")[0] || null;
2164
+ }
2165
+ function isLocalOrigin(origin) {
2166
+ if (!origin)
2167
+ return true;
2168
+ try {
2169
+ return isLocalHost(new URL(origin).hostname);
2170
+ } catch {
2171
+ return false;
2172
+ }
2173
+ }
2174
+ function isLocalHost(host) {
2175
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
2176
+ }
2177
+
2178
+ // src/server/cors.ts
2179
+ function resolveCorsOrigin(origin) {
2180
+ if (!origin)
2181
+ return "";
2182
+ const configured = readCorsOrigins();
2183
+ if (configured.includes("*"))
2184
+ return origin;
2185
+ if (configured.includes(origin))
2186
+ return origin;
2187
+ if (isLocalOrigin2(origin))
2188
+ return origin;
2189
+ return "";
2190
+ }
2191
+ function readCorsOrigins() {
2192
+ const value = process.env.HASNA_LOGS_CORS_ORIGINS ?? process.env.LOGS_CORS_ORIGINS ?? "";
2193
+ return value.split(",").map((origin) => origin.trim()).filter(Boolean);
2194
+ }
2195
+ function isLocalOrigin2(origin) {
2196
+ try {
2197
+ const parsed = new URL(origin);
2198
+ return ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname);
2199
+ } catch {
2200
+ return false;
2201
+ }
2202
+ }
2203
+
2204
+ // src/server/request.ts
2205
+ async function readJsonObject(c, opts = {}) {
2206
+ const contentType = c.req.header("content-type") ?? "";
2207
+ if (!contentType.toLowerCase().includes("application/json")) {
2208
+ return {
2209
+ ok: false,
2210
+ status: 415,
2211
+ message: "Content-Type must be application/json"
2212
+ };
2213
+ }
2214
+ const maxPayloadBytes = opts.maxPayloadBytes ?? readPositiveInt("HASNA_LOGS_MAX_PAYLOAD_BYTES", 1048576);
2215
+ const contentLength = Number(c.req.header("content-length") ?? "0");
2216
+ if (Number.isFinite(contentLength) && contentLength > maxPayloadBytes) {
2217
+ return {
2218
+ ok: false,
2219
+ status: 413,
2220
+ message: `Payload exceeds ${maxPayloadBytes} bytes`
2221
+ };
2222
+ }
2223
+ let raw2 = "";
2224
+ try {
2225
+ raw2 = await c.req.text();
2226
+ } catch {
2227
+ return { ok: false, status: 400, message: "Unable to read request body" };
2228
+ }
2229
+ if (Buffer.byteLength(raw2, "utf8") > maxPayloadBytes) {
2230
+ return {
2231
+ ok: false,
2232
+ status: 413,
2233
+ message: `Payload exceeds ${maxPayloadBytes} bytes`
2234
+ };
2235
+ }
2236
+ let body;
2237
+ try {
2238
+ body = JSON.parse(raw2);
2239
+ } catch {
2240
+ return { ok: false, status: 400, message: "Invalid JSON body" };
2241
+ }
2242
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
2243
+ return { ok: false, status: 422, message: "body must be an object" };
2244
+ }
2245
+ const objectBody = body;
2246
+ if (opts.allowedKeys) {
2247
+ const allowed = new Set(opts.allowedKeys);
2248
+ for (const key of Object.keys(objectBody)) {
2249
+ if (!allowed.has(key)) {
2250
+ return {
2251
+ ok: false,
2252
+ status: 422,
2253
+ message: `body.${key} is not a supported field`
2254
+ };
2255
+ }
2256
+ }
2257
+ }
2258
+ return { ok: true, value: objectBody };
2259
+ }
2260
+ function requiredString(body, key) {
2261
+ const value = body[key];
2262
+ if (typeof value !== "string" || value.length === 0) {
2263
+ return {
2264
+ ok: false,
2265
+ status: 422,
2266
+ message: `body.${key} must be a non-empty string`
2267
+ };
2268
+ }
2269
+ return { ok: true, value };
2270
+ }
2271
+ function optionalString(body, key) {
2272
+ const value = body[key];
2273
+ if (value === undefined)
2274
+ return { ok: true, value: undefined };
2275
+ if (typeof value !== "string") {
2276
+ return { ok: false, status: 422, message: `body.${key} must be a string` };
2277
+ }
2278
+ return { ok: true, value };
2279
+ }
2280
+ function optionalStringArray(body, key, opts = {}) {
2281
+ const value = body[key];
2282
+ if (value === undefined)
2283
+ return { ok: true, value: undefined };
2284
+ if (!Array.isArray(value)) {
2285
+ return {
2286
+ ok: false,
2287
+ status: 422,
2288
+ message: `body.${key} must be an array of strings`
2289
+ };
2290
+ }
2291
+ if (opts.maxItems !== undefined && value.length > opts.maxItems) {
2292
+ return {
2293
+ ok: false,
2294
+ status: 422,
2295
+ message: `body.${key} must have at most ${opts.maxItems} item(s)`
2296
+ };
2297
+ }
2298
+ const strings = [];
2299
+ for (const item of value) {
2300
+ if (typeof item !== "string" || item.length === 0) {
2301
+ return {
2302
+ ok: false,
2303
+ status: 422,
2304
+ message: `body.${key} must be an array of non-empty strings`
2305
+ };
2306
+ }
2307
+ strings.push(item);
2308
+ }
2309
+ return { ok: true, value: strings };
2310
+ }
2311
+ function optionalNumber(body, key, opts = {}) {
2312
+ const value = body[key];
2313
+ if (value === undefined)
2314
+ return { ok: true, value: undefined };
2315
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2316
+ return { ok: false, status: 422, message: `body.${key} must be a number` };
2317
+ }
2318
+ if (opts.integer && !Number.isInteger(value)) {
2319
+ return {
2320
+ ok: false,
2321
+ status: 422,
2322
+ message: `body.${key} must be an integer`
2323
+ };
2324
+ }
2325
+ if (opts.min !== undefined && value < opts.min) {
2326
+ return {
2327
+ ok: false,
2328
+ status: 422,
2329
+ message: `body.${key} must be >= ${opts.min}`
2330
+ };
2331
+ }
2332
+ if (opts.max !== undefined && value > opts.max) {
2333
+ return {
2334
+ ok: false,
2335
+ status: 422,
2336
+ message: `body.${key} must be <= ${opts.max}`
2337
+ };
2338
+ }
2339
+ return { ok: true, value };
2340
+ }
2341
+ function optionalEnum(body, key, values) {
2342
+ const value = body[key];
2343
+ if (value === undefined)
2344
+ return { ok: true, value: undefined };
2345
+ if (typeof value !== "string" || !values.includes(value)) {
2346
+ return {
2347
+ ok: false,
2348
+ status: 422,
2349
+ message: `body.${key} must be one of ${values.join(", ")}`
2350
+ };
2351
+ }
2352
+ return { ok: true, value };
2353
+ }
2354
+ function requiredEnum(body, key, values) {
2355
+ const value = body[key];
2356
+ if (typeof value !== "string" || !values.includes(value)) {
2357
+ return {
2358
+ ok: false,
2359
+ status: 422,
2360
+ message: `body.${key} must be one of ${values.join(", ")}`
2361
+ };
2362
+ }
2363
+ return { ok: true, value };
2364
+ }
2365
+ function readPositiveInt(name, fallback) {
2366
+ const parsed = Number.parseInt(process.env[name] ?? "", 10);
2367
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
2368
+ }
2369
+
2370
+ // src/server/routes/alerts.ts
2371
+ var ALERT_CREATE_KEYS = [
2372
+ "project_id",
2373
+ "name",
2374
+ "service",
2375
+ "level",
2376
+ "threshold_count",
2377
+ "window_seconds",
2378
+ "action",
2379
+ "webhook_url"
2380
+ ];
2381
+ var ALERT_UPDATE_KEYS = [
2382
+ "enabled",
2383
+ "threshold_count",
2384
+ "window_seconds",
2385
+ "webhook_url"
2386
+ ];
2387
+ var LOG_LEVELS = ["debug", "info", "warn", "error", "fatal"];
2388
+ var ALERT_ACTIONS = ["webhook", "log"];
2389
+ function alertsRoutes(db) {
2390
+ const app = new Hono2;
2391
+ app.post("/", async (c) => {
2392
+ const parsed = await readJsonObject(c, { allowedKeys: ALERT_CREATE_KEYS });
2393
+ if (!parsed.ok)
2394
+ return c.json({ error: parsed.message }, parsed.status);
2395
+ const alertInput = validateAlertCreate(parsed.value);
2396
+ if (!alertInput.ok)
2397
+ return c.json({ error: alertInput.message }, alertInput.status);
2398
+ const body = alertInput.value;
2399
+ return c.json(createAlertRule(db, body), 201);
2400
+ });
2401
+ app.get("/", (c) => {
2402
+ const { project_id } = c.req.query();
2403
+ return c.json(listAlertRules(db, project_id || undefined));
2404
+ });
2405
+ app.put("/:id", async (c) => {
2406
+ const parsed = await readJsonObject(c, { allowedKeys: ALERT_UPDATE_KEYS });
2407
+ if (!parsed.ok)
2408
+ return c.json({ error: parsed.message }, parsed.status);
2409
+ const alertInput = validateAlertUpdate(parsed.value);
2410
+ if (!alertInput.ok)
2411
+ return c.json({ error: alertInput.message }, alertInput.status);
2412
+ const body = alertInput.value;
2413
+ const updated = updateAlertRule(db, c.req.param("id"), body);
2414
+ if (!updated)
2415
+ return c.json({ error: "not found" }, 404);
2416
+ return c.json(updated);
2417
+ });
2418
+ app.delete("/:id", (c) => {
2419
+ deleteAlertRule(db, c.req.param("id"));
2420
+ return c.json({ deleted: true });
2421
+ });
2422
+ return app;
2423
+ }
2424
+ function validateAlertCreate(body) {
2425
+ const project_id = requiredString(body, "project_id");
2426
+ if (!project_id.ok)
2427
+ return project_id;
2428
+ const name = requiredString(body, "name");
2429
+ if (!name.ok)
2430
+ return name;
2431
+ const service = optionalString(body, "service");
2432
+ if (!service.ok)
2433
+ return service;
2434
+ const level = optionalEnum(body, "level", LOG_LEVELS);
2435
+ if (!level.ok)
2436
+ return level;
2437
+ const threshold_count = optionalNumber(body, "threshold_count", {
2438
+ integer: true,
2439
+ min: 1
2440
+ });
2441
+ if (!threshold_count.ok)
2442
+ return threshold_count;
2443
+ const window_seconds = optionalNumber(body, "window_seconds", {
2444
+ integer: true,
2445
+ min: 1
2446
+ });
2447
+ if (!window_seconds.ok)
2448
+ return window_seconds;
2449
+ const action = optionalEnum(body, "action", ALERT_ACTIONS);
2450
+ if (!action.ok)
2451
+ return action;
2452
+ const webhook_url = optionalString(body, "webhook_url");
2453
+ if (!webhook_url.ok)
2454
+ return webhook_url;
2455
+ if (webhook_url.value !== undefined && !isUrl(webhook_url.value)) {
2456
+ return {
2457
+ ok: false,
2458
+ status: 422,
2459
+ message: "body.webhook_url must be a valid URL"
2460
+ };
2461
+ }
2462
+ return {
2463
+ ok: true,
2464
+ value: {
2465
+ project_id: project_id.value,
2466
+ name: name.value,
2467
+ service: service.value,
2468
+ level: level.value,
2469
+ threshold_count: threshold_count.value,
2470
+ window_seconds: window_seconds.value,
2471
+ action: action.value,
2472
+ webhook_url: webhook_url.value
2473
+ }
2474
+ };
2475
+ }
2476
+ function validateAlertUpdate(body) {
2477
+ const enabled = optionalNumber(body, "enabled", {
2478
+ integer: true,
2479
+ min: 0,
2480
+ max: 1
2481
+ });
2482
+ if (!enabled.ok)
2483
+ return enabled;
2484
+ const threshold_count = optionalNumber(body, "threshold_count", {
2485
+ integer: true,
2486
+ min: 1
2487
+ });
2488
+ if (!threshold_count.ok)
2489
+ return threshold_count;
2490
+ const window_seconds = optionalNumber(body, "window_seconds", {
2491
+ integer: true,
2492
+ min: 1
2493
+ });
2494
+ if (!window_seconds.ok)
2495
+ return window_seconds;
2496
+ const webhook_url = optionalString(body, "webhook_url");
2497
+ if (!webhook_url.ok)
2498
+ return webhook_url;
2499
+ if (webhook_url.value !== undefined && webhook_url.value !== "" && !isUrl(webhook_url.value)) {
2500
+ return {
2501
+ ok: false,
2502
+ status: 422,
2503
+ message: "body.webhook_url must be a valid URL"
2504
+ };
2505
+ }
2506
+ return {
2507
+ ok: true,
2508
+ value: {
2509
+ enabled: enabled.value,
2510
+ threshold_count: threshold_count.value,
2511
+ window_seconds: window_seconds.value,
2512
+ webhook_url: webhook_url.value
2513
+ }
2514
+ };
2515
+ }
2516
+ function isUrl(value) {
2517
+ try {
2518
+ new URL(value);
2519
+ return true;
2520
+ } catch {
2521
+ return false;
2522
+ }
2523
+ }
2524
+
2525
+ // src/server/routes/events.ts
2526
+ import { createHash as createHash2 } from "crypto";
2527
+
2528
+ // node_modules/hono/dist/utils/stream.js
2529
+ var StreamingApi = class {
2530
+ writer;
2531
+ encoder;
2532
+ writable;
2533
+ abortSubscribers = [];
2534
+ responseReadable;
2535
+ aborted = false;
2536
+ closed = false;
2537
+ constructor(writable, _readable) {
2538
+ this.writable = writable;
2539
+ this.writer = writable.getWriter();
2540
+ this.encoder = new TextEncoder;
2541
+ const reader = _readable.getReader();
2542
+ this.abortSubscribers.push(async () => {
2543
+ await reader.cancel();
2544
+ });
2545
+ this.responseReadable = new ReadableStream({
2546
+ async pull(controller) {
2547
+ const { done, value } = await reader.read();
2548
+ done ? controller.close() : controller.enqueue(value);
2549
+ },
2550
+ cancel: () => {
2551
+ if (!this.closed) {
2552
+ this.abort();
2553
+ }
2554
+ }
2555
+ });
2556
+ }
2557
+ async write(input) {
2558
+ try {
2559
+ if (typeof input === "string") {
2560
+ input = this.encoder.encode(input);
2561
+ }
2562
+ await this.writer.write(input);
2563
+ } catch {}
2564
+ return this;
2565
+ }
2566
+ async writeln(input) {
2567
+ await this.write(input + `
2568
+ `);
2569
+ return this;
2570
+ }
2571
+ sleep(ms) {
2572
+ return new Promise((res) => setTimeout(res, ms));
2573
+ }
2574
+ async close() {
2575
+ this.closed = true;
2576
+ try {
2577
+ await this.writer.close();
2578
+ } catch {}
2579
+ }
2580
+ async pipe(body) {
2581
+ this.writer.releaseLock();
2582
+ await body.pipeTo(this.writable, { preventClose: true });
2583
+ this.writer = this.writable.getWriter();
2584
+ }
2585
+ onAbort(listener) {
2586
+ this.abortSubscribers.push(listener);
2587
+ }
2588
+ abort() {
2589
+ if (!this.aborted) {
2590
+ this.aborted = true;
2591
+ this.abortSubscribers.forEach((subscriber) => subscriber());
2592
+ }
2593
+ }
2594
+ };
2595
+
2596
+ // node_modules/hono/dist/helper/streaming/utils.js
2597
+ var isOldBunVersion = () => {
2598
+ const version = typeof Bun !== "undefined" ? Bun.version : undefined;
2599
+ if (version === undefined) {
2600
+ return false;
2601
+ }
2602
+ const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
2603
+ isOldBunVersion = () => result;
2604
+ return result;
2605
+ };
2606
+
2607
+ // node_modules/hono/dist/helper/streaming/sse.js
2608
+ var SSEStreamingApi = class extends StreamingApi {
2609
+ constructor(writable, readable) {
2610
+ super(writable, readable);
2611
+ }
2612
+ async writeSSE(message) {
2613
+ const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
2614
+ const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
2615
+ return `data: ${line}`;
2616
+ }).join(`
2617
+ `);
2618
+ for (const key of ["event", "id", "retry"]) {
2619
+ if (message[key] && /[\r\n]/.test(message[key])) {
2620
+ throw new Error(`${key} must not contain "\\r" or "\\n"`);
2621
+ }
2622
+ }
2623
+ const sseData = [
2624
+ message.event && `event: ${message.event}`,
2625
+ dataLines,
2626
+ message.id && `id: ${message.id}`,
2627
+ message.retry && `retry: ${message.retry}`
2628
+ ].filter(Boolean).join(`
2629
+ `) + `
2630
+
2631
+ `;
2632
+ await this.write(sseData);
2633
+ }
2634
+ };
2635
+ var run = async (stream, cb, onError) => {
2636
+ try {
2637
+ await cb(stream);
2638
+ } catch (e) {
2639
+ if (e instanceof Error && onError) {
2640
+ await onError(e, stream);
2641
+ await stream.writeSSE({
2642
+ event: "error",
2643
+ data: e.message
2644
+ });
2645
+ } else {
2646
+ console.error(e);
2647
+ }
2648
+ } finally {
2649
+ stream.close();
2650
+ }
2651
+ };
2652
+ var contextStash = /* @__PURE__ */ new WeakMap;
2653
+ var streamSSE = (c, cb, onError) => {
2654
+ const { readable, writable } = new TransformStream;
2655
+ const stream = new SSEStreamingApi(writable, readable);
2656
+ if (isOldBunVersion()) {
2657
+ c.req.raw.signal.addEventListener("abort", () => {
2658
+ if (!stream.closed) {
2659
+ stream.abort();
2660
+ }
2661
+ });
2662
+ }
2663
+ contextStash.set(stream.responseReadable, c);
2664
+ c.header("Transfer-Encoding", "chunked");
2665
+ c.header("Content-Type", "text/event-stream");
2666
+ c.header("Cache-Control", "no-cache");
2667
+ c.header("Connection", "keep-alive");
2668
+ run(stream, cb, onError);
2669
+ return c.newResponse(stream.responseReadable);
2670
+ };
2671
+
2672
+ // src/server/routes/events.ts
2673
+ function eventsRoutes(db) {
2674
+ const app = new Hono2;
2675
+ app.post("/", async (c) => {
2676
+ const parsed = await readEventIngestBody(db, c);
2677
+ if (!parsed.ok)
2678
+ return c.json({ error: parsed.message }, parsed.status);
2679
+ try {
2680
+ if (!parsed.batch && parsed.events.length === 1) {
2681
+ const [event] = parsed.events;
2682
+ if (!event)
2683
+ return c.json({ error: "body must contain at least one event" }, 422);
2684
+ const result = ingestUniversalEvent(db, event);
2685
+ if (parsed.authorization.kind === "browser-token")
2686
+ touchBrowserIngestToken(db, parsed.authorization.token.id);
2687
+ return c.json(result.event, result.inserted ? 201 : 200);
2688
+ }
2689
+ const results = parsed.events.map((event) => ingestUniversalEvent(db, event));
2690
+ if (parsed.authorization.kind === "browser-token")
2691
+ touchBrowserIngestToken(db, parsed.authorization.token.id);
2692
+ return c.json({
2693
+ inserted: results.filter((result) => result.inserted).length,
2694
+ events: results.map((result) => result.event)
2695
+ }, 201);
2696
+ } catch (error) {
2697
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 422);
2698
+ }
2699
+ });
2700
+ app.get("/", (c) => {
2701
+ return c.json(searchEvents(db, queryFromRequest(c.req.query())));
2702
+ });
2703
+ app.get("/export", (c) => {
2704
+ const query = queryFromRequest(c.req.query());
2705
+ const chunks = [];
2706
+ exportEventsToJson(db, query, (line) => chunks.push(line));
2707
+ c.header("Content-Type", "application/json");
2708
+ c.header("Content-Disposition", "attachment; filename=events.json");
2709
+ return c.text(chunks.join(`
2710
+ `));
2711
+ });
2712
+ app.get("/stream", (c) => {
2713
+ const query = c.req.query();
2714
+ const filter = streamFilterFromRequest(query);
2715
+ const includeRaw = query.include_raw === "true";
2716
+ const eventNameMode = query.event_name === "event" ? "event" : "type";
2717
+ const requestedLastId = c.req.header("last-event-id") || query.last_event_id || null;
2718
+ const debugOptions = streamDebugOptionsFromRequest(query);
2719
+ return streamSSE(c, async (stream2) => {
2720
+ const writer = debugOptions.writeDelayMs > 0 ? delayedSseWriter(stream2, debugOptions.writeDelayMs) : stream2;
2721
+ const seen = new Set;
2722
+ let lastId = requestedLastId;
2723
+ let rowidCursor = null;
2724
+ if (lastId) {
2725
+ const anchor = eventCursorById(db, lastId);
2726
+ if (!anchor) {
2727
+ const requestedLastId2 = lastId;
2728
+ const latest = latestEventCursor(db, filter);
2729
+ lastId = latest?.event_id ?? null;
2730
+ rowidCursor = latest?.rowid ?? 0;
2731
+ await writeEventOverflow(writer, {
2732
+ reason: "last_event_id_unknown",
2733
+ dropped: 0,
2734
+ last_event_id: lastId,
2735
+ requested_last_event_id: requestedLastId2
2736
+ });
2737
+ } else {
2738
+ if (!hasBufferedEventCatalogEvent(lastId)) {
2739
+ await writeEventOverflow(writer, {
2740
+ reason: "buffer_miss_sqlite_catchup",
2741
+ dropped: 0,
2742
+ last_event_id: lastId
2743
+ });
2744
+ }
2745
+ const catchup = await writeCatchupEventsAfterRowid(db, writer, filter, anchor.rowid, seen, includeRaw, eventNameMode);
2746
+ rowidCursor = catchup.last_rowid;
2747
+ if (catchup.last_id) {
2748
+ lastId = catchup.last_id;
2749
+ }
2750
+ }
2751
+ } else {
2752
+ const latest = latestEventCursor(db, filter);
2753
+ lastId = latest?.event_id ?? null;
2754
+ rowidCursor = latest?.rowid ?? 0;
2755
+ }
2756
+ const subscription = subscribeEventCatalogEvents(filter, debugOptions.subscriberQueue ? { maxQueue: debugOptions.subscriberQueue } : undefined);
2757
+ let pendingBusEvent = subscription.next();
2758
+ try {
2759
+ while (true) {
2760
+ const next = await Promise.race([
2761
+ pendingBusEvent.then((result) => ({
2762
+ kind: "bus",
2763
+ result
2764
+ })),
2765
+ sleep(500).then(() => ({ kind: "tick" }))
2766
+ ]);
2767
+ if (next.kind === "tick") {
2768
+ const catchup2 = await writeCatchupEventsAfterRowid(db, writer, filter, rowidCursor ?? 0, seen, includeRaw, eventNameMode);
2769
+ rowidCursor = catchup2.last_rowid;
2770
+ if (catchup2.last_id)
2771
+ lastId = catchup2.last_id;
2772
+ continue;
2773
+ }
2774
+ if (next.result.done)
2775
+ break;
2776
+ const event = next.result.value;
2777
+ pendingBusEvent = subscription.next();
2778
+ if (event.kind === "overflow") {
2779
+ await writeEventOverflow(writer, event);
2780
+ const catchup2 = await writeCatchupEventsAfterRowid(db, writer, filter, rowidCursor ?? 0, seen, includeRaw, eventNameMode);
2781
+ rowidCursor = catchup2.last_rowid;
2782
+ if (catchup2.last_id)
2783
+ lastId = catchup2.last_id;
2784
+ continue;
2785
+ }
2786
+ const catchup = await writeCatchupEventsAfterRowid(db, writer, filter, rowidCursor ?? 0, seen, includeRaw, eventNameMode);
2787
+ rowidCursor = catchup.last_rowid;
2788
+ if (catchup.last_id)
2789
+ lastId = catchup.last_id;
2790
+ if (seen.has(event.id))
2791
+ continue;
2792
+ await writeEventCatalogEntryById(db, writer, event.id, includeRaw, event.entry, eventNameMode);
2793
+ seen.add(event.id);
2794
+ lastId = event.id;
2795
+ const cursor = eventCursorById(db, event.id);
2796
+ if (cursor)
2797
+ rowidCursor = Math.max(rowidCursor ?? 0, cursor.rowid);
2798
+ trimSeen(seen);
2799
+ }
2800
+ } finally {
2801
+ await subscription.return?.();
2802
+ }
2803
+ });
2804
+ });
2805
+ app.get("/:event_id", (c) => {
2806
+ const includeRaw = c.req.query("include_raw") !== "false";
2807
+ const event = getEvent(db, c.req.param("event_id"), includeRaw);
2808
+ if (!event)
2809
+ return c.json({ error: "Event not found" }, 404);
2810
+ return c.json(event);
2811
+ });
2812
+ return app;
2813
+ }
2814
+ async function readEventIngestBody(db, c) {
2815
+ const authorization = authorizeLogIngest(db, c);
2816
+ if (!authorization) {
2817
+ return { ok: false, status: 401, message: "Unauthorized" };
2818
+ }
2819
+ const request = c.req.raw;
2820
+ const contentType = c.req.header("content-type") ?? "";
2821
+ if (!contentType.toLowerCase().includes("application/json")) {
2822
+ return {
2823
+ ok: false,
2824
+ status: 415,
2825
+ message: "Content-Type must be application/json"
2826
+ };
2827
+ }
2828
+ const maxPayloadBytes = readPositiveInt("HASNA_LOGS_MAX_PAYLOAD_BYTES", 1048576);
2829
+ const contentLength = Number(c.req.header("content-length") ?? "0");
2830
+ if (Number.isFinite(contentLength) && contentLength > maxPayloadBytes) {
2831
+ return {
2832
+ ok: false,
2833
+ status: 413,
2834
+ message: `Payload exceeds ${maxPayloadBytes} bytes`
2835
+ };
2836
+ }
2837
+ let raw2 = "";
2838
+ try {
2839
+ raw2 = await request.text();
2840
+ } catch {
2841
+ return { ok: false, status: 400, message: "Unable to read request body" };
2842
+ }
2843
+ if (Buffer.byteLength(raw2, "utf8") > maxPayloadBytes) {
2844
+ return {
2845
+ ok: false,
2846
+ status: 413,
2847
+ message: `Payload exceeds ${maxPayloadBytes} bytes`
2848
+ };
2849
+ }
2850
+ let body;
2851
+ try {
2852
+ body = JSON.parse(raw2);
2853
+ } catch {
2854
+ return { ok: false, status: 400, message: "Invalid JSON body" };
2855
+ }
2856
+ const maxBatchSize = readPositiveInt("HASNA_LOGS_MAX_EVENT_BATCH_SIZE", readPositiveInt("HASNA_LOGS_MAX_BATCH_SIZE", 1000));
2857
+ const batch = Array.isArray(body) || Boolean(body && typeof body === "object" && !Array.isArray(body) && Array.isArray(body.events));
2858
+ const rawEvents = Array.isArray(body) ? body : eventArrayFromObject(body);
2859
+ if (rawEvents.length > maxBatchSize) {
2860
+ return {
2861
+ ok: false,
2862
+ status: 413,
2863
+ message: `Batch exceeds ${maxBatchSize} events`
2864
+ };
2865
+ }
2866
+ if (rawEvents.length === 0) {
2867
+ return {
2868
+ ok: false,
2869
+ status: 422,
2870
+ message: "body must contain at least one event"
2871
+ };
2872
+ }
2873
+ try {
2874
+ const events = rawEvents.map((event, index) => {
2875
+ const validated = validateUniversalEventInput(event, `event[${index}]`);
2876
+ return applyEventIngestAuthorization(validated, authorization, `event[${index}]`);
2877
+ });
2878
+ return { ok: true, events, batch, authorization };
2879
+ } catch (error) {
2880
+ return {
2881
+ ok: false,
2882
+ status: 422,
2883
+ message: error instanceof Error ? error.message : String(error)
2884
+ };
2885
+ }
2886
+ }
2887
+ var BROWSER_EVENT_TYPES = new Set([
2888
+ "log",
2889
+ "exception",
2890
+ "span",
2891
+ "metric",
2892
+ "network",
2893
+ "replay",
2894
+ "session"
2895
+ ]);
2896
+ var BROWSER_FORBIDDEN_IDENTITY_FIELDS = [
2897
+ "project_id",
2898
+ "page_id",
2899
+ "machine_id",
2900
+ "repo_id",
2901
+ "process_id",
2902
+ "run_id",
2903
+ "artifact_id"
2904
+ ];
2905
+ var BROWSER_FORBIDDEN_IDENTITY_FIELD_SET = new Set(BROWSER_FORBIDDEN_IDENTITY_FIELDS);
2906
+ function applyEventIngestAuthorization(event, authorization, path) {
2907
+ if (authorization.kind !== "browser-token")
2908
+ return event;
2909
+ if (!BROWSER_EVENT_TYPES.has(event.type)) {
2910
+ throw new Error(`${path}.type cannot be ${event.type} when using a browser ingest token`);
2911
+ }
2912
+ if (event.source !== undefined && event.source !== "script" && event.source !== "browser") {
2913
+ throw new Error(`${path}.source must be script or browser when using a browser ingest token`);
2914
+ }
2915
+ for (const field of BROWSER_FORBIDDEN_IDENTITY_FIELDS) {
2916
+ if (event[field] !== undefined && event[field] !== null) {
2917
+ throw new Error(`${path}.${field} cannot be set when using a browser ingest token`);
2918
+ }
2919
+ }
2920
+ rejectBrowserScopedIdentity(event.attributes, `${path}.attributes`);
2921
+ rejectBrowserScopedIdentity(event.metadata, `${path}.metadata`);
2922
+ const browserMetadata = {
2923
+ browser_token_id: authorization.token.id,
2924
+ browser_token_prefix: authorization.token.token_prefix,
2925
+ ingest_scope: "browser"
2926
+ };
2927
+ const producerId = event.event_id ?? event.id ?? event.source_event_id ?? undefined;
2928
+ const source = event.source ?? "browser";
2929
+ return {
2930
+ ...event,
2931
+ event_id: producerId ? browserScopedEventId(authorization.token.project_id, source, producerId) : undefined,
2932
+ source_event_id: event.source_event_id ?? event.event_id ?? event.id ?? undefined,
2933
+ project_id: authorization.token.project_id,
2934
+ source,
2935
+ attributes: {
2936
+ ...event.attributes ?? {},
2937
+ ...browserMetadata,
2938
+ project_id: authorization.token.project_id
2939
+ },
2940
+ metadata: {
2941
+ ...event.metadata ?? {},
2942
+ ...browserMetadata
2943
+ }
2944
+ };
2945
+ }
2946
+ function browserScopedEventId(projectId, source, producerId) {
2947
+ const digest = createHash2("sha256").update(projectId).update("\x00").update(source).update("\x00").update(producerId).digest("hex").slice(0, 32);
2948
+ return `evt_browser_${digest}`;
2949
+ }
2950
+ function rejectBrowserScopedIdentity(value, path) {
2951
+ if (!value)
2952
+ return;
2953
+ for (const key of Object.keys(value)) {
2954
+ if (BROWSER_FORBIDDEN_IDENTITY_FIELD_SET.has(key) && value[key] !== undefined && value[key] !== null) {
2955
+ throw new Error(`${path}.${key} cannot be set when using a browser ingest token`);
2956
+ }
2957
+ }
2958
+ }
2959
+ async function writeCatchupEventsAfterRowid(db, stream2, filter, afterRowid, seen, includeRaw, eventNameMode) {
2960
+ let cursor = afterRowid;
2961
+ let lastWritten = null;
2962
+ let total = 0;
2963
+ while (total < 1000) {
2964
+ const rows = queryEventsAfterRowid(db, filter, cursor, 100);
2965
+ if (rows.length === 0)
2966
+ break;
2967
+ for (const row of rows) {
2968
+ cursor = row.rowid;
2969
+ if (seen.has(row.event_id))
2970
+ continue;
2971
+ const written = await writeEventCatalogEntryById(db, stream2, row.event_id, includeRaw, undefined, eventNameMode);
2972
+ if (!written)
2973
+ continue;
2974
+ seen.add(row.event_id);
2975
+ lastWritten = row.event_id;
2976
+ total += 1;
2977
+ trimSeen(seen);
2978
+ }
2979
+ if (rows.length < 100)
2980
+ break;
2981
+ }
2982
+ if (total >= 1000) {
2983
+ await writeEventOverflow(stream2, {
2984
+ reason: "sqlite_catchup_truncated",
2985
+ dropped: 0,
2986
+ last_event_id: lastWritten
2987
+ });
2988
+ }
2989
+ return { last_id: lastWritten, last_rowid: cursor };
2990
+ }
2991
+ function latestEventCursor(db, filter) {
2992
+ const { where, params } = buildEventRecordWhere(filter, null);
2993
+ const row = db.prepare(`SELECT rowid, event_id FROM event_records ${where} ORDER BY rowid DESC LIMIT 1`).get(...params);
2994
+ return row ?? null;
2995
+ }
2996
+ function eventCursorById(db, eventId) {
2997
+ const row = db.prepare("SELECT rowid, event_id FROM event_records WHERE event_id = ?").get(eventId);
2998
+ return row ?? null;
2999
+ }
3000
+ function queryEventsAfterRowid(db, filter, rowid, limit) {
3001
+ const { where, params } = buildEventRecordWhere(filter, rowid);
3002
+ return db.prepare(`SELECT rowid, event_id FROM event_records ${where} ORDER BY rowid ASC LIMIT ?`).all(...params, limit);
3003
+ }
3004
+ function buildEventRecordWhere(filter, afterRowid) {
3005
+ const conditions = [];
3006
+ const params = [];
3007
+ if (afterRowid !== null) {
3008
+ conditions.push("rowid > ?");
3009
+ params.push(afterRowid);
3010
+ }
3011
+ addListCondition(conditions, params, "event_type", filter.event_type);
3012
+ addListCondition(conditions, params, "source", filter.source);
3013
+ addListCondition(conditions, params, "severity", filter.severity);
3014
+ addScalarCondition(conditions, params, "project_id", filter.project_id);
3015
+ addScalarCondition(conditions, params, "page_id", filter.page_id);
3016
+ addScalarCondition(conditions, params, "machine_id", filter.machine_id);
3017
+ addScalarCondition(conditions, params, "repo_id", filter.repo_id);
3018
+ addScalarCondition(conditions, params, "app_id", filter.app_id);
3019
+ addScalarCondition(conditions, params, "process_id", filter.process_id);
3020
+ addScalarCondition(conditions, params, "run_id", filter.run_id);
3021
+ addScalarCondition(conditions, params, "trace_id", filter.trace_id);
3022
+ addScalarCondition(conditions, params, "span_id", filter.span_id);
3023
+ addScalarCondition(conditions, params, "session_id", filter.session_id);
3024
+ addScalarCondition(conditions, params, "release_id", filter.release_id);
3025
+ addScalarCondition(conditions, params, "environment", filter.environment);
3026
+ return {
3027
+ where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "",
3028
+ params
3029
+ };
3030
+ }
3031
+ function streamFilterFromRequest(query) {
3032
+ return {
3033
+ event_type: splitCsv(query.type || query.event_type),
3034
+ source: splitCsv(query.source),
3035
+ severity: splitCsv(query.severity || query.level),
3036
+ project_id: query.project_id || undefined,
3037
+ page_id: query.page_id || undefined,
3038
+ machine_id: query.machine_id || undefined,
3039
+ repo_id: query.repo_id || undefined,
3040
+ app_id: query.app_id || undefined,
3041
+ process_id: query.process_id || undefined,
3042
+ run_id: query.run_id || undefined,
3043
+ trace_id: query.trace_id || undefined,
3044
+ span_id: query.span_id || undefined,
3045
+ session_id: query.session_id || undefined,
3046
+ release_id: query.release_id || undefined,
3047
+ environment: query.environment || undefined
3048
+ };
3049
+ }
3050
+ function splitCsv(value) {
3051
+ const values = value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
3052
+ return values.length > 0 ? values : undefined;
3053
+ }
3054
+ function streamDebugOptionsFromRequest(query) {
3055
+ if (process.env.HASNA_LOGS_STREAM_TEST_HOOKS !== "1") {
3056
+ return { writeDelayMs: 0 };
3057
+ }
3058
+ return {
3059
+ subscriberQueue: boundedIntQuery(query.debug_subscriber_queue, {
3060
+ min: 1,
3061
+ max: 1e4
3062
+ }),
3063
+ writeDelayMs: boundedIntQuery(query.debug_write_delay_ms, { min: 0, max: 5000 }) ?? 0
3064
+ };
3065
+ }
3066
+ function boundedIntQuery(value, bounds) {
3067
+ if (value === undefined || value.length === 0)
3068
+ return;
3069
+ const parsed = Number.parseInt(value, 10);
3070
+ if (!Number.isFinite(parsed) || parsed < bounds.min)
3071
+ return;
3072
+ return Math.min(parsed, bounds.max);
3073
+ }
3074
+ function delayedSseWriter(stream2, delayMs) {
3075
+ return {
3076
+ async writeSSE(message) {
3077
+ await sleep(delayMs);
3078
+ await stream2.writeSSE(message);
3079
+ }
3080
+ };
3081
+ }
3082
+ function addScalarCondition(conditions, params, column, value) {
3083
+ if (!value)
3084
+ return;
3085
+ conditions.push(`${column} = ?`);
3086
+ params.push(value);
3087
+ }
3088
+ function addListCondition(conditions, params, column, values) {
3089
+ if (!values || values.length === 0)
3090
+ return;
3091
+ conditions.push(`${column} IN (${values.map(() => "?").join(",")})`);
3092
+ params.push(...values);
3093
+ }
3094
+ async function writeEventCatalogEntry(stream2, entry, eventNameMode) {
3095
+ await stream2.writeSSE({
3096
+ data: JSON.stringify(entry),
3097
+ id: entry.event_id,
3098
+ event: eventNameMode === "event" ? "event" : entry.event_type
3099
+ });
3100
+ }
3101
+ async function writeEventCatalogEntryById(db, stream2, eventId, includeRaw, fallback, eventNameMode = "type") {
3102
+ try {
3103
+ const entry = includeRaw ? getEvent(db, eventId, true) : fallback ?? getEvent(db, eventId, false);
3104
+ if (!entry)
3105
+ return false;
3106
+ await writeEventCatalogEntry(stream2, entry, eventNameMode);
3107
+ return true;
3108
+ } catch (error) {
3109
+ await writeEventOverflow(stream2, {
3110
+ reason: "raw_event_unreadable",
3111
+ dropped: 0,
3112
+ last_event_id: eventId
3113
+ });
3114
+ const entry = fallback ?? getEvent(db, eventId, false);
3115
+ if (!entry)
3116
+ return false;
3117
+ await writeEventCatalogEntry(stream2, entry, eventNameMode);
3118
+ return true;
3119
+ }
3120
+ }
3121
+ async function writeEventOverflow(stream2, data) {
3122
+ await stream2.writeSSE({
3123
+ event: "overflow",
3124
+ data: JSON.stringify({
3125
+ type: "overflow",
3126
+ reason: data.reason,
3127
+ dropped: data.dropped,
3128
+ last_event_id: "last_event_id" in data ? data.last_event_id : null,
3129
+ requested_last_event_id: "requested_last_event_id" in data ? data.requested_last_event_id : null,
3130
+ created_at: "created_at" in data ? data.created_at : new Date().toISOString()
3131
+ })
3132
+ });
3133
+ }
3134
+ function trimSeen(seen) {
3135
+ while (seen.size > 2000) {
3136
+ const first = seen.values().next().value;
3137
+ if (!first)
3138
+ return;
3139
+ seen.delete(first);
3140
+ }
3141
+ }
3142
+ function sleep(ms) {
3143
+ return new Promise((resolve) => setTimeout(resolve, ms));
3144
+ }
3145
+ function eventArrayFromObject(body) {
3146
+ if (body && typeof body === "object" && !Array.isArray(body) && Array.isArray(body.events)) {
3147
+ return body.events;
3148
+ }
3149
+ return [body];
3150
+ }
3151
+ function queryFromRequest(query) {
3152
+ return {
3153
+ event_type: query.type || query.event_type || undefined,
3154
+ source: query.source || undefined,
3155
+ severity: query.severity || query.level || undefined,
3156
+ project_id: query.project_id || undefined,
3157
+ page_id: query.page_id || undefined,
3158
+ machine_id: query.machine_id || undefined,
3159
+ repo_id: query.repo_id || undefined,
3160
+ app_id: query.app_id || undefined,
3161
+ process_id: query.process_id || undefined,
3162
+ run_id: query.run_id || undefined,
3163
+ trace_id: query.trace_id || undefined,
3164
+ span_id: query.span_id || undefined,
3165
+ session_id: query.session_id || undefined,
3166
+ release_id: query.release_id || undefined,
3167
+ environment: query.environment || undefined,
3168
+ since: query.since || undefined,
3169
+ until: query.until || undefined,
3170
+ text: query.text || undefined,
3171
+ limit: query.limit ? Number(query.limit) : 100,
3172
+ offset: query.offset ? Number(query.offset) : 0,
3173
+ include_raw: query.include_raw === "true"
3174
+ };
3175
+ }
3176
+
3177
+ // src/server/routes/issues.ts
3178
+ var ISSUE_UPDATE_KEYS = ["status"];
3179
+ var ISSUE_STATUSES = ["open", "resolved", "ignored"];
3180
+ function issuesRoutes(db) {
3181
+ const app = new Hono2;
3182
+ app.get("/", (c) => {
3183
+ const { project_id, status, limit } = c.req.query();
3184
+ return c.json(listIssues(db, project_id || undefined, status || undefined, limit ? Number(limit) : 50));
3185
+ });
3186
+ app.get("/:id", (c) => {
3187
+ const issue = getIssue(db, c.req.param("id"));
3188
+ if (!issue)
3189
+ return c.json({ error: "not found" }, 404);
3190
+ return c.json(issue);
3191
+ });
3192
+ app.get("/:id/logs", (c) => {
3193
+ const issue = getIssue(db, c.req.param("id"));
3194
+ if (!issue)
3195
+ return c.json({ error: "not found" }, 404);
3196
+ const rows = searchLogs(db, {
3197
+ project_id: issue.project_id ?? undefined,
3198
+ level: issue.level,
3199
+ service: issue.service ?? undefined,
3200
+ text: issue.message_template.slice(0, 50),
3201
+ limit: 50
3202
+ });
3203
+ return c.json(rows);
3204
+ });
3205
+ app.put("/:id", async (c) => {
3206
+ const parsed = await readJsonObject(c, { allowedKeys: ISSUE_UPDATE_KEYS });
3207
+ if (!parsed.ok)
3208
+ return c.json({ error: parsed.message }, parsed.status);
3209
+ const statusInput = requiredEnum(parsed.value, "status", ISSUE_STATUSES);
3210
+ if (!statusInput.ok)
3211
+ return c.json({ error: statusInput.message }, statusInput.status);
3212
+ const status = statusInput.value;
3213
+ const updated = updateIssueStatus(db, c.req.param("id"), status);
3214
+ if (!updated)
3215
+ return c.json({ error: "not found" }, 404);
3216
+ return c.json(updated);
3217
+ });
3218
+ return app;
3219
+ }
3220
+
3221
+ // src/server/routes/jobs.ts
3222
+ var JOB_CREATE_KEYS = ["project_id", "schedule", "page_id"];
3223
+ var JOB_UPDATE_KEYS = ["enabled", "schedule", "last_run_at"];
3224
+ function jobsRoutes(db) {
3225
+ const app = new Hono2;
3226
+ app.post("/", async (c) => {
3227
+ const parsed = await readJsonObject(c, { allowedKeys: JOB_CREATE_KEYS });
3228
+ if (!parsed.ok)
3229
+ return c.json({ error: parsed.message }, parsed.status);
3230
+ const jobInput = validateJobCreate(parsed.value);
3231
+ if (!jobInput.ok)
3232
+ return c.json({ error: jobInput.message }, jobInput.status);
3233
+ const body = jobInput.value;
3234
+ return c.json(createJob(db, body), 201);
3235
+ });
3236
+ app.get("/", (c) => {
3237
+ const { project_id } = c.req.query();
3238
+ return c.json(listJobs(db, project_id || undefined));
3239
+ });
3240
+ app.put("/:id", async (c) => {
3241
+ const parsed = await readJsonObject(c, { allowedKeys: JOB_UPDATE_KEYS });
3242
+ if (!parsed.ok)
3243
+ return c.json({ error: parsed.message }, parsed.status);
3244
+ const jobInput = validateJobUpdate(parsed.value);
3245
+ if (!jobInput.ok)
3246
+ return c.json({ error: jobInput.message }, jobInput.status);
3247
+ const body = jobInput.value;
3248
+ const updated = updateJob(db, c.req.param("id"), body);
3249
+ if (!updated)
3250
+ return c.json({ error: "not found" }, 404);
3251
+ return c.json(updated);
3252
+ });
3253
+ app.delete("/:id", (c) => {
3254
+ deleteJob(db, c.req.param("id"));
3255
+ return c.json({ deleted: true });
3256
+ });
3257
+ return app;
3258
+ }
3259
+ function validateJobCreate(body) {
3260
+ const project_id = requiredString(body, "project_id");
3261
+ if (!project_id.ok)
3262
+ return project_id;
3263
+ const schedule = requiredString(body, "schedule");
3264
+ if (!schedule.ok)
3265
+ return schedule;
3266
+ const page_id = optionalString(body, "page_id");
3267
+ if (!page_id.ok)
3268
+ return page_id;
3269
+ return {
3270
+ ok: true,
3271
+ value: {
3272
+ project_id: project_id.value,
3273
+ schedule: schedule.value,
3274
+ page_id: page_id.value
3275
+ }
3276
+ };
3277
+ }
3278
+ function validateJobUpdate(body) {
3279
+ const enabled = optionalNumber(body, "enabled", {
3280
+ integer: true,
3281
+ min: 0,
3282
+ max: 1
3283
+ });
3284
+ if (!enabled.ok)
3285
+ return enabled;
3286
+ const schedule = optionalString(body, "schedule");
3287
+ if (!schedule.ok)
3288
+ return schedule;
3289
+ const last_run_at = optionalString(body, "last_run_at");
3290
+ if (!last_run_at.ok)
3291
+ return last_run_at;
3292
+ return {
3293
+ ok: true,
3294
+ value: {
3295
+ enabled: enabled.value,
3296
+ schedule: schedule.value,
3297
+ last_run_at: last_run_at.value
3298
+ }
3299
+ };
2068
3300
  }
2069
3301
 
2070
3302
  // src/server/routes/logs.ts
3303
+ var LOG_LEVELS2 = new Set([
3304
+ "debug",
3305
+ "info",
3306
+ "warn",
3307
+ "error",
3308
+ "fatal"
3309
+ ]);
3310
+ var LOG_SOURCES = new Set([
3311
+ "sdk",
3312
+ "script",
3313
+ "scanner",
3314
+ "browser",
3315
+ "node",
3316
+ "bun",
3317
+ "next",
3318
+ "vite",
3319
+ "cli",
3320
+ "build",
3321
+ "test",
3322
+ "mcp",
3323
+ "agent",
3324
+ "otel",
3325
+ "system",
3326
+ "pino",
3327
+ "winston",
3328
+ "structured"
3329
+ ]);
3330
+ var PRIVACY_CLASSES = new Set([
3331
+ "public",
3332
+ "internal",
3333
+ "sensitive",
3334
+ "secret",
3335
+ "pii"
3336
+ ]);
3337
+ var LOG_ENTRY_KEYS = new Set([
3338
+ "id",
3339
+ "timestamp",
3340
+ "source_event_id",
3341
+ "project_id",
3342
+ "page_id",
3343
+ "level",
3344
+ "source",
3345
+ "service",
3346
+ "message",
3347
+ "privacy",
3348
+ "machine_id",
3349
+ "repo_id",
3350
+ "app_id",
3351
+ "process_id",
3352
+ "run_id",
3353
+ "trace_id",
3354
+ "span_id",
3355
+ "parent_span_id",
3356
+ "session_id",
3357
+ "release_id",
3358
+ "environment",
3359
+ "agent",
3360
+ "url",
3361
+ "stack_trace",
3362
+ "metadata"
3363
+ ]);
2071
3364
  function logsRoutes(db) {
2072
3365
  const app = new Hono2;
3366
+ app.post("/structured", async (c) => {
3367
+ const validation = await validateStructuredIngestRequest(db, c);
3368
+ if (!validation.ok)
3369
+ return c.json({ error: validation.message }, validation.status);
3370
+ const rows = validation.entries.map((entry) => ingestLog(db, entry));
3371
+ return c.json({
3372
+ inserted: rows.length,
3373
+ events: rows.map((row) => ({
3374
+ id: row.id,
3375
+ timestamp: row.timestamp,
3376
+ level: row.level,
3377
+ source: row.source,
3378
+ service: row.service,
3379
+ message: row.message,
3380
+ trace_id: row.trace_id
3381
+ }))
3382
+ }, 201);
3383
+ });
2073
3384
  app.post("/", async (c) => {
2074
- const body = await c.req.json();
2075
- if (Array.isArray(body)) {
2076
- const rows = ingestBatch(db, body);
3385
+ const validation = await validateIngestRequest(db, c);
3386
+ if (!validation.ok)
3387
+ return c.json({ error: validation.message }, validation.status);
3388
+ if (validation.batch) {
3389
+ const rows = ingestBatch(db, validation.entries);
3390
+ if (validation.authorization.kind === "browser-token")
3391
+ touchBrowserIngestToken(db, validation.authorization.token.id);
2077
3392
  return c.json({ inserted: rows.length }, 201);
2078
3393
  }
2079
- const row = ingestLog(db, body);
3394
+ const [entry] = validation.entries;
3395
+ if (!entry)
3396
+ return c.json({ error: "No log entry provided" }, 422);
3397
+ const row = ingestLog(db, entry);
3398
+ if (validation.authorization.kind === "browser-token")
3399
+ touchBrowserIngestToken(db, validation.authorization.token.id);
2080
3400
  return c.json(row, 201);
2081
3401
  });
2082
3402
  app.get("/", (c) => {
2083
- const { project_id, page_id, level, service, since, until, text, trace_id, limit, offset, fields } = c.req.query();
3403
+ const {
3404
+ project_id,
3405
+ page_id,
3406
+ level,
3407
+ service,
3408
+ since,
3409
+ until,
3410
+ text,
3411
+ trace_id,
3412
+ limit,
3413
+ offset,
3414
+ fields
3415
+ } = c.req.query();
2084
3416
  const rows = searchLogs(db, {
2085
3417
  project_id: project_id || undefined,
2086
3418
  page_id: page_id || undefined,
@@ -2127,31 +3459,944 @@ function logsRoutes(db) {
2127
3459
  since: parseTime(since || "1h"),
2128
3460
  limit: limit ? Number(limit) : 20
2129
3461
  });
2130
- return c.json(rows.map((r) => ({ id: r.id, timestamp: r.timestamp, level: r.level, message: r.message, service: r.service, age_seconds: Math.floor((Date.now() - new Date(r.timestamp).getTime()) / 1000) })));
3462
+ return c.json(rows.map((r) => ({
3463
+ id: r.id,
3464
+ timestamp: r.timestamp,
3465
+ level: r.level,
3466
+ message: r.message,
3467
+ service: r.service,
3468
+ age_seconds: Math.floor((Date.now() - new Date(r.timestamp).getTime()) / 1000)
3469
+ })));
2131
3470
  });
2132
3471
  app.get("/:trace_id/context", (c) => {
2133
3472
  const rows = getLogContext(db, c.req.param("trace_id"));
2134
3473
  return c.json(rows);
2135
3474
  });
2136
- app.get("/export", (c) => {
2137
- const { project_id, since, until, level, service, format, limit } = c.req.query();
2138
- const opts = { project_id: project_id || undefined, since: since || undefined, until: until || undefined, level: level || undefined, service: service || undefined, limit: limit ? Number(limit) : undefined };
2139
- if (format === "csv") {
2140
- c.header("Content-Type", "text/csv");
2141
- c.header("Content-Disposition", "attachment; filename=logs.csv");
2142
- const chunks2 = [];
2143
- exportToCsv(db, opts, (s) => chunks2.push(s));
2144
- return c.text(chunks2.join(""));
2145
- }
2146
- c.header("Content-Type", "application/json");
2147
- c.header("Content-Disposition", "attachment; filename=logs.json");
2148
- const chunks = [];
2149
- exportToJson(db, opts, (s) => chunks.push(s));
2150
- return c.text(chunks.join(`
2151
- `));
3475
+ app.get("/export", (c) => {
3476
+ const { project_id, since, until, level, service, format, limit } = c.req.query();
3477
+ const opts = {
3478
+ project_id: project_id || undefined,
3479
+ since: since || undefined,
3480
+ until: until || undefined,
3481
+ level: level || undefined,
3482
+ service: service || undefined,
3483
+ limit: limit ? Number(limit) : undefined
3484
+ };
3485
+ if (format === "csv") {
3486
+ c.header("Content-Type", "text/csv");
3487
+ c.header("Content-Disposition", "attachment; filename=logs.csv");
3488
+ const chunks2 = [];
3489
+ exportToCsv(db, opts, (s) => chunks2.push(s));
3490
+ return c.text(chunks2.join(""));
3491
+ }
3492
+ c.header("Content-Type", "application/json");
3493
+ c.header("Content-Disposition", "attachment; filename=logs.json");
3494
+ const chunks = [];
3495
+ exportToJson(db, opts, (s) => chunks.push(s));
3496
+ return c.text(chunks.join(`
3497
+ `));
3498
+ });
3499
+ return app;
3500
+ }
3501
+ async function validateStructuredIngestRequest(db, c) {
3502
+ const authorization = authorizeLogIngest(db, c);
3503
+ if (!authorization || authorization.kind === "browser-token") {
3504
+ return {
3505
+ ok: false,
3506
+ status: 401,
3507
+ message: "Structured server log ingest requires a trusted API token"
3508
+ };
3509
+ }
3510
+ const contentType = c.req.header("content-type") ?? "";
3511
+ if (!contentType.toLowerCase().includes("application/json")) {
3512
+ return {
3513
+ ok: false,
3514
+ status: 415,
3515
+ message: "Content-Type must be application/json"
3516
+ };
3517
+ }
3518
+ const maxPayloadBytes = readPositiveInt2("HASNA_LOGS_MAX_PAYLOAD_BYTES", 1048576);
3519
+ const contentLength = Number(c.req.header("content-length") ?? "0");
3520
+ if (Number.isFinite(contentLength) && contentLength > maxPayloadBytes) {
3521
+ return {
3522
+ ok: false,
3523
+ status: 413,
3524
+ message: `Payload exceeds ${maxPayloadBytes} bytes`
3525
+ };
3526
+ }
3527
+ let raw2 = "";
3528
+ try {
3529
+ raw2 = await c.req.text();
3530
+ } catch {
3531
+ return { ok: false, status: 400, message: "Unable to read request body" };
3532
+ }
3533
+ if (Buffer.byteLength(raw2, "utf8") > maxPayloadBytes) {
3534
+ return {
3535
+ ok: false,
3536
+ status: 413,
3537
+ message: `Payload exceeds ${maxPayloadBytes} bytes`
3538
+ };
3539
+ }
3540
+ let body;
3541
+ try {
3542
+ body = JSON.parse(raw2);
3543
+ } catch {
3544
+ return { ok: false, status: 400, message: "Invalid JSON body" };
3545
+ }
3546
+ const maxBatchSize = readPositiveInt2("HASNA_LOGS_MAX_BATCH_SIZE", 1000);
3547
+ const count = structuredPayloadCount(body);
3548
+ if (count > maxBatchSize) {
3549
+ return {
3550
+ ok: false,
3551
+ status: 413,
3552
+ message: `Batch exceeds ${maxBatchSize} entries`
3553
+ };
3554
+ }
3555
+ let result;
3556
+ try {
3557
+ result = structuredLogPayloadToEntries(body, structuredOptionsFromRequest(c));
3558
+ } catch (error) {
3559
+ return {
3560
+ ok: false,
3561
+ status: 422,
3562
+ message: error instanceof Error ? error.message : String(error)
3563
+ };
3564
+ }
3565
+ for (let index = 0;index < result.entries.length; index += 1) {
3566
+ const entry = result.entries[index];
3567
+ if (entry?.source === "browser" || entry?.source === "script") {
3568
+ return {
3569
+ ok: false,
3570
+ status: 422,
3571
+ message: `entry[${index}].source cannot be browser or script for structured server log ingest`
3572
+ };
3573
+ }
3574
+ }
3575
+ try {
3576
+ validateStructuredLogReferences(db, result.entries);
3577
+ } catch (error) {
3578
+ return {
3579
+ ok: false,
3580
+ status: 422,
3581
+ message: error instanceof Error ? error.message : String(error)
3582
+ };
3583
+ }
3584
+ const maxMessageChars = readPositiveInt2("HASNA_LOGS_MAX_MESSAGE_CHARS", 262144);
3585
+ for (let index = 0;index < result.entries.length; index += 1) {
3586
+ const entry = result.entries[index];
3587
+ if (entry && entry.message.length > maxMessageChars) {
3588
+ return {
3589
+ ok: false,
3590
+ status: 413,
3591
+ message: `entry[${index}].message is too large`
3592
+ };
3593
+ }
3594
+ }
3595
+ return {
3596
+ ok: true,
3597
+ entries: result.entries,
3598
+ batch: result.batch,
3599
+ authorization
3600
+ };
3601
+ }
3602
+ async function validateIngestRequest(db, c) {
3603
+ const authorization = authorizeLogIngest(db, c);
3604
+ if (!authorization) {
3605
+ return { ok: false, status: 401, message: "Unauthorized" };
3606
+ }
3607
+ const contentType = c.req.header("content-type") ?? "";
3608
+ if (!contentType.toLowerCase().includes("application/json")) {
3609
+ return {
3610
+ ok: false,
3611
+ status: 415,
3612
+ message: "Content-Type must be application/json"
3613
+ };
3614
+ }
3615
+ const maxPayloadBytes = readPositiveInt2("HASNA_LOGS_MAX_PAYLOAD_BYTES", 1048576);
3616
+ const contentLength = Number(c.req.header("content-length") ?? "0");
3617
+ if (Number.isFinite(contentLength) && contentLength > maxPayloadBytes) {
3618
+ return {
3619
+ ok: false,
3620
+ status: 413,
3621
+ message: `Payload exceeds ${maxPayloadBytes} bytes`
3622
+ };
3623
+ }
3624
+ let raw2 = "";
3625
+ try {
3626
+ raw2 = await c.req.text();
3627
+ } catch {
3628
+ return { ok: false, status: 400, message: "Unable to read request body" };
3629
+ }
3630
+ if (Buffer.byteLength(raw2, "utf8") > maxPayloadBytes) {
3631
+ return {
3632
+ ok: false,
3633
+ status: 413,
3634
+ message: `Payload exceeds ${maxPayloadBytes} bytes`
3635
+ };
3636
+ }
3637
+ let body;
3638
+ try {
3639
+ body = JSON.parse(raw2);
3640
+ } catch {
3641
+ return { ok: false, status: 400, message: "Invalid JSON body" };
3642
+ }
3643
+ const maxBatchSize = readPositiveInt2("HASNA_LOGS_MAX_BATCH_SIZE", 1000);
3644
+ if (Array.isArray(body)) {
3645
+ if (body.length > maxBatchSize) {
3646
+ return {
3647
+ ok: false,
3648
+ status: 413,
3649
+ message: `Batch exceeds ${maxBatchSize} entries`
3650
+ };
3651
+ }
3652
+ const entries = [];
3653
+ for (let index = 0;index < body.length; index += 1) {
3654
+ const result2 = validateLogEntry(body[index], `entry[${index}]`);
3655
+ if (!result2.ok)
3656
+ return result2;
3657
+ const authorized2 = applyLogIngestAuthorization(result2.entry, authorization, `entry[${index}]`);
3658
+ if (!authorized2.ok)
3659
+ return authorized2;
3660
+ entries.push(authorized2.entry);
3661
+ }
3662
+ return { ok: true, entries, batch: true, authorization };
3663
+ }
3664
+ const result = validateLogEntry(body, "entry");
3665
+ if (!result.ok)
3666
+ return result;
3667
+ const authorized = applyLogIngestAuthorization(result.entry, authorization, "entry");
3668
+ if (!authorized.ok)
3669
+ return authorized;
3670
+ return { ok: true, entries: [authorized.entry], batch: false, authorization };
3671
+ }
3672
+ function structuredOptionsFromRequest(c) {
3673
+ const query = c.req.query();
3674
+ const format = query.format;
3675
+ if (format !== undefined && format !== "auto" && format !== "pino" && format !== "winston" && format !== "json") {
3676
+ throw new Error("format must be auto, pino, winston, or json");
3677
+ }
3678
+ return {
3679
+ format,
3680
+ source: query.source,
3681
+ service: query.service,
3682
+ project_id: query.project_id,
3683
+ page_id: query.page_id,
3684
+ machine_id: query.machine_id,
3685
+ repo_id: query.repo_id,
3686
+ app_id: query.app_id,
3687
+ process_id: query.process_id,
3688
+ run_id: query.run_id,
3689
+ trace_id: query.trace_id,
3690
+ span_id: query.span_id,
3691
+ parent_span_id: query.parent_span_id,
3692
+ session_id: query.session_id,
3693
+ release_id: query.release_id,
3694
+ environment: query.environment,
3695
+ agent: query.agent,
3696
+ url: query.url
3697
+ };
3698
+ }
3699
+ function structuredPayloadCount(value) {
3700
+ if (Array.isArray(value))
3701
+ return value.length;
3702
+ if (value && typeof value === "object" && !Array.isArray(value) && Array.isArray(value.logs)) {
3703
+ return value.logs.length;
3704
+ }
3705
+ return 1;
3706
+ }
3707
+ function applyLogIngestAuthorization(entry, authorization, path) {
3708
+ if (authorization.kind !== "browser-token")
3709
+ return { ok: true, entry };
3710
+ if (entry.source !== undefined && entry.source !== "script" && entry.source !== "browser") {
3711
+ return {
3712
+ ok: false,
3713
+ status: 422,
3714
+ message: `${path}.source must be script or browser when using a browser ingest token`
3715
+ };
3716
+ }
3717
+ return {
3718
+ ok: true,
3719
+ entry: {
3720
+ ...entry,
3721
+ project_id: authorization.token.project_id,
3722
+ source: entry.source ?? "browser"
3723
+ }
3724
+ };
3725
+ }
3726
+ function validateLogEntry(value, path) {
3727
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3728
+ return { ok: false, status: 422, message: `${path} must be an object` };
3729
+ }
3730
+ const input = value;
3731
+ for (const key of Object.keys(input)) {
3732
+ if (!LOG_ENTRY_KEYS.has(key)) {
3733
+ return {
3734
+ ok: false,
3735
+ status: 422,
3736
+ message: `${path}.${key} is not a supported log field`
3737
+ };
3738
+ }
3739
+ }
3740
+ if (!LOG_LEVELS2.has(input.level)) {
3741
+ return {
3742
+ ok: false,
3743
+ status: 422,
3744
+ message: `${path}.level must be one of debug, info, warn, error, fatal`
3745
+ };
3746
+ }
3747
+ if (typeof input.message !== "string" || input.message.length === 0) {
3748
+ return {
3749
+ ok: false,
3750
+ status: 422,
3751
+ message: `${path}.message must be a non-empty string`
3752
+ };
3753
+ }
3754
+ if (input.message.length > readPositiveInt2("HASNA_LOGS_MAX_MESSAGE_CHARS", 262144)) {
3755
+ return { ok: false, status: 413, message: `${path}.message is too large` };
3756
+ }
3757
+ if (input.source !== undefined && !LOG_SOURCES.has(input.source)) {
3758
+ return {
3759
+ ok: false,
3760
+ status: 422,
3761
+ message: `${path}.source is not supported`
3762
+ };
3763
+ }
3764
+ if (input.privacy !== undefined && !PRIVACY_CLASSES.has(input.privacy)) {
3765
+ return {
3766
+ ok: false,
3767
+ status: 422,
3768
+ message: `${path}.privacy is not supported`
3769
+ };
3770
+ }
3771
+ if (input.metadata !== undefined && (!input.metadata || typeof input.metadata !== "object" || Array.isArray(input.metadata))) {
3772
+ return {
3773
+ ok: false,
3774
+ status: 422,
3775
+ message: `${path}.metadata must be an object`
3776
+ };
3777
+ }
3778
+ const entry = {
3779
+ level: input.level,
3780
+ message: input.message
3781
+ };
3782
+ const entryRecord = entry;
3783
+ const optionalStringKeys = [
3784
+ "id",
3785
+ "timestamp",
3786
+ "source_event_id",
3787
+ "project_id",
3788
+ "page_id",
3789
+ "source",
3790
+ "service",
3791
+ "privacy",
3792
+ "machine_id",
3793
+ "repo_id",
3794
+ "app_id",
3795
+ "process_id",
3796
+ "run_id",
3797
+ "trace_id",
3798
+ "span_id",
3799
+ "parent_span_id",
3800
+ "session_id",
3801
+ "release_id",
3802
+ "environment",
3803
+ "agent",
3804
+ "url",
3805
+ "stack_trace"
3806
+ ];
3807
+ for (const key of optionalStringKeys) {
3808
+ const copied = copyOptionalString(input, entryRecord, key, path);
3809
+ if (!copied.ok)
3810
+ return copied;
3811
+ }
3812
+ if (input.metadata !== undefined)
3813
+ entry.metadata = input.metadata;
3814
+ return { ok: true, entry };
3815
+ }
3816
+ function copyOptionalString(input, entry, key, path) {
3817
+ const value = input[key];
3818
+ if (value === undefined)
3819
+ return { ok: true };
3820
+ if (typeof value !== "string") {
3821
+ return {
3822
+ ok: false,
3823
+ status: 422,
3824
+ message: `${path}.${key} must be a string`
3825
+ };
3826
+ }
3827
+ entry[key] = value;
3828
+ return { ok: true };
3829
+ }
3830
+ function readPositiveInt2(name, fallback) {
3831
+ const parsed = Number.parseInt(process.env[name] ?? "", 10);
3832
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3833
+ }
3834
+
3835
+ // src/lib/otlp-ingest.ts
3836
+ import { createHash as createHash3 } from "crypto";
3837
+ function ingestOtlpTraces(db, payload) {
3838
+ const events = [];
3839
+ for (const [resourceIndex, resourceSpan] of requiredObjectArray(payload, "resourceSpans", "payload.resourceSpans").entries()) {
3840
+ const resource = attributesFromContainer(resourceSpan.resource);
3841
+ for (const [scopeIndex, scopeSpan] of objectArray(resourceSpan.scopeSpans, "payload.resourceSpans[].scopeSpans").entries()) {
3842
+ const scope = scopeInfo(scopeSpan.scope);
3843
+ const context = { resource, scope, resourceIndex, scopeIndex };
3844
+ for (const span of objectArray(scopeSpan.spans, "payload.resourceSpans[].scopeSpans[].spans")) {
3845
+ events.push(spanToEvent(span, context));
3846
+ }
3847
+ }
3848
+ }
3849
+ return ingestOtlpEvents(db, "traces", events);
3850
+ }
3851
+ function ingestOtlpLogs(db, payload) {
3852
+ const events = [];
3853
+ for (const [resourceIndex, resourceLog] of requiredObjectArray(payload, "resourceLogs", "payload.resourceLogs").entries()) {
3854
+ const resource = attributesFromContainer(resourceLog.resource);
3855
+ for (const [scopeIndex, scopeLog] of objectArray(resourceLog.scopeLogs, "payload.resourceLogs[].scopeLogs").entries()) {
3856
+ const scope = scopeInfo(scopeLog.scope);
3857
+ const context = { resource, scope, resourceIndex, scopeIndex };
3858
+ for (const [recordIndex, logRecord] of objectArray(scopeLog.logRecords, "payload.resourceLogs[].scopeLogs[].logRecords").entries()) {
3859
+ events.push(logRecordToEvent(logRecord, context, recordIndex));
3860
+ }
3861
+ }
3862
+ }
3863
+ return ingestOtlpEvents(db, "logs", events);
3864
+ }
3865
+ function ingestOtlpMetrics(db, payload) {
3866
+ const events = [];
3867
+ for (const [resourceIndex, resourceMetric] of requiredObjectArray(payload, "resourceMetrics", "payload.resourceMetrics").entries()) {
3868
+ const resource = attributesFromContainer(resourceMetric.resource);
3869
+ for (const [scopeIndex, scopeMetric] of objectArray(resourceMetric.scopeMetrics, "payload.resourceMetrics[].scopeMetrics").entries()) {
3870
+ const scope = scopeInfo(scopeMetric.scope);
3871
+ const context = { resource, scope, resourceIndex, scopeIndex };
3872
+ for (const [metricIndex, metric] of objectArray(scopeMetric.metrics, "payload.resourceMetrics[].scopeMetrics[].metrics").entries()) {
3873
+ events.push(...metricToEvents(metric, context, metricIndex));
3874
+ }
3875
+ }
3876
+ }
3877
+ return ingestOtlpEvents(db, "metrics", events);
3878
+ }
3879
+ function ingestOtlpEvents(db, signal, inputs) {
3880
+ const events = [];
3881
+ let inserted = 0;
3882
+ for (const input of inputs) {
3883
+ const result = ingestUniversalEvent(db, input);
3884
+ if (result.inserted)
3885
+ inserted += 1;
3886
+ events.push(result.event);
3887
+ }
3888
+ return {
3889
+ signal,
3890
+ accepted: inputs.length,
3891
+ inserted,
3892
+ duplicates: inputs.length - inserted,
3893
+ events
3894
+ };
3895
+ }
3896
+ function spanToEvent(span, context) {
3897
+ const attributes = attributesFromArray(span.attributes);
3898
+ const traceId = optionalString2(span.traceId);
3899
+ const spanId = optionalString2(span.spanId);
3900
+ const parentSpanId = optionalString2(span.parentSpanId);
3901
+ const startTime = unixNanoToIso(span.startTimeUnixNano);
3902
+ const endTime = unixNanoToIso(span.endTimeUnixNano);
3903
+ const spanDurationMs = durationMs(span.startTimeUnixNano, span.endTimeUnixNano);
3904
+ const status = objectValue(span.status);
3905
+ const statusCode = optionalString2(status?.code) ?? stringFromUnknown(status?.code);
3906
+ const isError = statusCode === "2" || statusCode === "STATUS_CODE_ERROR" || statusCode === "ERROR";
3907
+ const name = optionalString2(span.name) ?? "OTLP span";
3908
+ const spanKind = optionalString2(span.kind) ?? stringFromUnknown(span.kind);
3909
+ return {
3910
+ type: "span",
3911
+ source: "otel",
3912
+ source_event_id: traceId && spanId ? `otlp:span:${traceId}:${spanId}` : stableSourceEventId("span", span),
3913
+ event_time: startTime,
3914
+ severity: isError ? "error" : "info",
3915
+ trace_id: traceId,
3916
+ span_id: spanId,
3917
+ parent_span_id: parentSpanId,
3918
+ app_id: resourceString(context.resource, "service.name"),
3919
+ machine_id: resourceString(context.resource, "host.id") ?? resourceString(context.resource, "host.name"),
3920
+ environment: resourceString(context.resource, "deployment.environment"),
3921
+ message: name,
3922
+ attributes: compactObject({
3923
+ category: "otlp_span",
3924
+ signal: "traces",
3925
+ name,
3926
+ operation: attributes["http.route"] ?? attributes["rpc.method"] ?? attributes["db.operation.name"] ?? spanKind,
3927
+ status: isError ? "error" : "ok",
3928
+ started_at: startTime,
3929
+ ended_at: endTime,
3930
+ duration_ms: spanDurationMs,
3931
+ resource: context.resource,
3932
+ scope: context.scope,
3933
+ span_attributes: attributes,
3934
+ otel: compactObject({
3935
+ signal: "traces",
3936
+ span_kind: spanKind,
3937
+ status_code: statusCode,
3938
+ status_message: optionalString2(status?.message),
3939
+ dropped_attributes_count: numberFromUnknown(span.droppedAttributesCount),
3940
+ dropped_events_count: numberFromUnknown(span.droppedEventsCount),
3941
+ dropped_links_count: numberFromUnknown(span.droppedLinksCount),
3942
+ start_time_unix_nano: stringFromUnknown(span.startTimeUnixNano),
3943
+ end_time_unix_nano: stringFromUnknown(span.endTimeUnixNano)
3944
+ })
3945
+ }),
3946
+ body: {
3947
+ span: compactObject({
3948
+ name,
3949
+ kind: spanKind,
3950
+ status,
3951
+ events: spanEvents(span.events),
3952
+ links: spanLinks(span.links)
3953
+ })
3954
+ }
3955
+ };
3956
+ }
3957
+ function logRecordToEvent(logRecord, context, recordIndex) {
3958
+ const attributes = attributesFromArray(logRecord.attributes);
3959
+ const body = otlpAnyValue(logRecord.body);
3960
+ const traceId = optionalString2(logRecord.traceId);
3961
+ const spanId = optionalString2(logRecord.spanId);
3962
+ const severityNumber = numberFromUnknown(logRecord.severityNumber);
3963
+ const severityText = optionalString2(logRecord.severityText);
3964
+ const eventTime = unixNanoToIso(logRecord.timeUnixNano) ?? unixNanoToIso(logRecord.observedTimeUnixNano);
3965
+ const message = typeof body === "string" ? body : severityText ? `OTLP ${severityText} log` : "OTLP log";
3966
+ return {
3967
+ type: "log",
3968
+ source: "otel",
3969
+ source_event_id: stableSourceEventId("log", {
3970
+ traceId,
3971
+ spanId,
3972
+ timeUnixNano: logRecord.timeUnixNano,
3973
+ observedTimeUnixNano: logRecord.observedTimeUnixNano,
3974
+ severityNumber,
3975
+ severityText,
3976
+ resourceIndex: context.resourceIndex,
3977
+ scopeIndex: context.scopeIndex,
3978
+ recordIndex,
3979
+ resource: context.resource,
3980
+ scope: context.scope,
3981
+ body,
3982
+ attributes
3983
+ }),
3984
+ event_time: eventTime,
3985
+ severity: otlpSeverity(severityNumber, severityText),
3986
+ trace_id: traceId,
3987
+ span_id: spanId,
3988
+ app_id: resourceString(context.resource, "service.name"),
3989
+ machine_id: resourceString(context.resource, "host.id") ?? resourceString(context.resource, "host.name"),
3990
+ environment: resourceString(context.resource, "deployment.environment"),
3991
+ message,
3992
+ attributes: compactObject({
3993
+ category: "otlp_log",
3994
+ signal: "logs",
3995
+ resource: context.resource,
3996
+ scope: context.scope,
3997
+ log_attributes: attributes,
3998
+ otel: compactObject({
3999
+ signal: "logs",
4000
+ severity_number: severityNumber,
4001
+ severity_text: severityText,
4002
+ time_unix_nano: stringFromUnknown(logRecord.timeUnixNano),
4003
+ observed_time_unix_nano: stringFromUnknown(logRecord.observedTimeUnixNano),
4004
+ dropped_attributes_count: numberFromUnknown(logRecord.droppedAttributesCount),
4005
+ flags: numberFromUnknown(logRecord.flags)
4006
+ })
4007
+ }),
4008
+ body: {
4009
+ log: compactObject({
4010
+ body,
4011
+ severity_number: severityNumber,
4012
+ severity_text: severityText
4013
+ })
4014
+ }
4015
+ };
4016
+ }
4017
+ function metricToEvents(metric, context, metricIndex) {
4018
+ const name = optionalString2(metric.name) ?? "otel.metric";
4019
+ const description = optionalString2(metric.description);
4020
+ const unit = optionalString2(metric.unit);
4021
+ const points = metricPoints(metric);
4022
+ return points.dataPoints.map((point, index) => {
4023
+ const attributes = attributesFromArray(point.attributes);
4024
+ const eventTime = unixNanoToIso(point.timeUnixNano) ?? unixNanoToIso(point.startTimeUnixNano);
4025
+ return {
4026
+ type: "metric",
4027
+ source: "otel",
4028
+ source_event_id: stableSourceEventId("metric", {
4029
+ name,
4030
+ kind: points.kind,
4031
+ index,
4032
+ startTimeUnixNano: point.startTimeUnixNano,
4033
+ timeUnixNano: point.timeUnixNano,
4034
+ resourceIndex: context.resourceIndex,
4035
+ scopeIndex: context.scopeIndex,
4036
+ metricIndex,
4037
+ pointIndex: index,
4038
+ resource: context.resource,
4039
+ scope: context.scope,
4040
+ attributes,
4041
+ value: metricPointValue(points.kind, point)
4042
+ }),
4043
+ event_time: eventTime,
4044
+ severity: "info",
4045
+ app_id: resourceString(context.resource, "service.name"),
4046
+ machine_id: resourceString(context.resource, "host.id") ?? resourceString(context.resource, "host.name"),
4047
+ environment: resourceString(context.resource, "deployment.environment"),
4048
+ message: name,
4049
+ attributes: compactObject({
4050
+ category: "otlp_metric",
4051
+ signal: "metrics",
4052
+ name,
4053
+ description,
4054
+ unit,
4055
+ resource: context.resource,
4056
+ scope: context.scope,
4057
+ metric_attributes: attributes,
4058
+ otel: compactObject({
4059
+ signal: "metrics",
4060
+ metric_kind: points.kind,
4061
+ aggregation_temporality: points.aggregationTemporality,
4062
+ is_monotonic: points.isMonotonic,
4063
+ start_time_unix_nano: stringFromUnknown(point.startTimeUnixNano),
4064
+ time_unix_nano: stringFromUnknown(point.timeUnixNano),
4065
+ flags: numberFromUnknown(point.flags)
4066
+ })
4067
+ }),
4068
+ body: {
4069
+ metric: compactObject({
4070
+ name,
4071
+ description,
4072
+ unit,
4073
+ kind: points.kind,
4074
+ value: metricPointValue(points.kind, point)
4075
+ })
4076
+ }
4077
+ };
4078
+ });
4079
+ }
4080
+ function metricPoints(metric) {
4081
+ const metricKinds = [
4082
+ "gauge",
4083
+ "sum",
4084
+ "histogram",
4085
+ "exponentialHistogram",
4086
+ "summary"
4087
+ ];
4088
+ for (const kind of metricKinds) {
4089
+ if (!(kind in metric))
4090
+ continue;
4091
+ const container = objectValue(metric[kind]);
4092
+ if (!container)
4093
+ throw new Error(`metric.${kind} must be an object`);
4094
+ return {
4095
+ kind,
4096
+ dataPoints: objectArray(container.dataPoints, `metric.${kind}.dataPoints`),
4097
+ aggregationTemporality: stringOrNumber(container.aggregationTemporality),
4098
+ isMonotonic: typeof container.isMonotonic === "boolean" ? container.isMonotonic : undefined
4099
+ };
4100
+ }
4101
+ throw new Error(`metric must contain one of ${metricKinds.join(", ")}`);
4102
+ }
4103
+ function metricPointValue(kind, point) {
4104
+ if (kind === "gauge" || kind === "sum") {
4105
+ return numberOrString(point.asDouble) ?? numberOrString(point.asInt);
4106
+ }
4107
+ if (kind === "histogram") {
4108
+ return compactObject({
4109
+ count: numberOrString(point.count),
4110
+ sum: numberOrString(point.sum),
4111
+ min: numberOrString(point.min),
4112
+ max: numberOrString(point.max),
4113
+ bucket_counts: primitiveArray(point.bucketCounts),
4114
+ explicit_bounds: primitiveArray(point.explicitBounds)
4115
+ });
4116
+ }
4117
+ if (kind === "exponentialHistogram") {
4118
+ return compactObject({
4119
+ count: numberOrString(point.count),
4120
+ sum: numberOrString(point.sum),
4121
+ min: numberOrString(point.min),
4122
+ max: numberOrString(point.max),
4123
+ scale: numberFromUnknown(point.scale),
4124
+ zero_count: numberOrString(point.zeroCount),
4125
+ positive: point.positive,
4126
+ negative: point.negative
4127
+ });
4128
+ }
4129
+ if (kind === "summary") {
4130
+ return compactObject({
4131
+ count: numberOrString(point.count),
4132
+ sum: numberOrString(point.sum),
4133
+ quantile_values: primitiveArray(point.quantileValues)
4134
+ });
4135
+ }
4136
+ return point;
4137
+ }
4138
+ function attributesFromContainer(value) {
4139
+ return attributesFromArray(objectValue(value)?.attributes);
4140
+ }
4141
+ function attributesFromArray(value) {
4142
+ const result = {};
4143
+ for (const entry of objectArray(value, "attributes")) {
4144
+ const key = optionalString2(entry.key);
4145
+ if (!key)
4146
+ continue;
4147
+ result[key] = otlpAnyValue(entry.value);
4148
+ }
4149
+ return result;
4150
+ }
4151
+ function scopeInfo(value) {
4152
+ const scope = objectValue(value);
4153
+ if (!scope)
4154
+ return {};
4155
+ return compactObject({
4156
+ name: optionalString2(scope.name),
4157
+ version: optionalString2(scope.version),
4158
+ attributes: attributesFromArray(scope.attributes),
4159
+ dropped_attributes_count: numberFromUnknown(scope.droppedAttributesCount)
4160
+ });
4161
+ }
4162
+ function otlpAnyValue(value) {
4163
+ const object = objectValue(value);
4164
+ if (!object)
4165
+ return;
4166
+ if ("stringValue" in object)
4167
+ return optionalString2(object.stringValue) ?? stringFromUnknown(object.stringValue);
4168
+ if ("boolValue" in object)
4169
+ return object.boolValue === true;
4170
+ if ("intValue" in object)
4171
+ return numberOrString(object.intValue);
4172
+ if ("doubleValue" in object)
4173
+ return numberOrString(object.doubleValue);
4174
+ if ("bytesValue" in object)
4175
+ return optionalString2(object.bytesValue);
4176
+ if ("arrayValue" in object)
4177
+ return objectArray(objectValue(object.arrayValue)?.values, "arrayValue.values").map(otlpAnyValue);
4178
+ if ("kvlistValue" in object)
4179
+ return attributesFromArray(objectValue(object.kvlistValue)?.values);
4180
+ return;
4181
+ }
4182
+ function spanEvents(value) {
4183
+ return objectArray(value, "span.events").map((event) => compactObject({
4184
+ name: optionalString2(event.name),
4185
+ time_unix_nano: stringFromUnknown(event.timeUnixNano),
4186
+ time: unixNanoToIso(event.timeUnixNano),
4187
+ attributes: attributesFromArray(event.attributes),
4188
+ dropped_attributes_count: numberFromUnknown(event.droppedAttributesCount)
4189
+ }));
4190
+ }
4191
+ function spanLinks(value) {
4192
+ return objectArray(value, "span.links").map((link) => compactObject({
4193
+ trace_id: optionalString2(link.traceId),
4194
+ span_id: optionalString2(link.spanId),
4195
+ trace_state: optionalString2(link.traceState),
4196
+ attributes: attributesFromArray(link.attributes),
4197
+ dropped_attributes_count: numberFromUnknown(link.droppedAttributesCount)
4198
+ }));
4199
+ }
4200
+ function otlpSeverity(severityNumber, severityText) {
4201
+ if (severityNumber !== undefined) {
4202
+ if (severityNumber >= 21)
4203
+ return "fatal";
4204
+ if (severityNumber >= 17)
4205
+ return "error";
4206
+ if (severityNumber >= 13)
4207
+ return "warn";
4208
+ if (severityNumber >= 1 && severityNumber <= 8)
4209
+ return "debug";
4210
+ }
4211
+ const text = severityText?.toLowerCase() ?? "";
4212
+ if (text.includes("fatal") || text.includes("panic"))
4213
+ return "fatal";
4214
+ if (text.includes("error") || text.includes("err"))
4215
+ return "error";
4216
+ if (text.includes("warn"))
4217
+ return "warn";
4218
+ if (text.includes("debug") || text.includes("trace"))
4219
+ return "debug";
4220
+ return "info";
4221
+ }
4222
+ function unixNanoToIso(value) {
4223
+ const ns = bigintFromUnknown(value);
4224
+ if (ns === undefined)
4225
+ return;
4226
+ const ms = ns / 1000000n;
4227
+ if (ms > BigInt(Number.MAX_SAFE_INTEGER) || ms < BigInt(Number.MIN_SAFE_INTEGER))
4228
+ return;
4229
+ const date = new Date(Number(ms));
4230
+ return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
4231
+ }
4232
+ function durationMs(start, end) {
4233
+ const startNs = bigintFromUnknown(start);
4234
+ const endNs = bigintFromUnknown(end);
4235
+ if (startNs === undefined || endNs === undefined || endNs < startNs)
4236
+ return;
4237
+ return Number(endNs - startNs) / 1e6;
4238
+ }
4239
+ function requiredObjectArray(value, key, path) {
4240
+ const object = objectValue(value);
4241
+ if (!object)
4242
+ throw new Error("OTLP payload must be an object");
4243
+ const array = object[key];
4244
+ if (!Array.isArray(array))
4245
+ throw new Error(`${path} must be an array`);
4246
+ return array.map((item, index) => {
4247
+ const itemObject = objectValue(item);
4248
+ if (!itemObject)
4249
+ throw new Error(`${path}[${index}] must be an object`);
4250
+ return itemObject;
4251
+ });
4252
+ }
4253
+ function objectArray(value, path) {
4254
+ if (value === undefined || value === null)
4255
+ return [];
4256
+ if (!Array.isArray(value))
4257
+ throw new Error(`${path} must be an array`);
4258
+ return value.map((item, index) => {
4259
+ const itemObject = objectValue(item);
4260
+ if (!itemObject)
4261
+ throw new Error(`${path}[${index}] must be an object`);
4262
+ return itemObject;
4263
+ });
4264
+ }
4265
+ function objectValue(value) {
4266
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
4267
+ }
4268
+ function optionalString2(value) {
4269
+ return typeof value === "string" && value.length > 0 ? value : undefined;
4270
+ }
4271
+ function stringFromUnknown(value) {
4272
+ if (value === undefined || value === null || value === "")
4273
+ return;
4274
+ if (typeof value === "string")
4275
+ return value;
4276
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
4277
+ return String(value);
4278
+ return;
4279
+ }
4280
+ function resourceString(resource, key) {
4281
+ return optionalString2(resource[key]) ?? stringFromUnknown(resource[key]);
4282
+ }
4283
+ function stringOrNumber(value) {
4284
+ if (typeof value === "number" && Number.isFinite(value))
4285
+ return value;
4286
+ return stringFromUnknown(value);
4287
+ }
4288
+ function numberOrString(value) {
4289
+ if (typeof value === "number" && Number.isFinite(value))
4290
+ return value;
4291
+ if (typeof value === "string" && value.length > 0) {
4292
+ const parsed = Number(value);
4293
+ return Number.isSafeInteger(parsed) ? parsed : value;
4294
+ }
4295
+ return;
4296
+ }
4297
+ function numberFromUnknown(value) {
4298
+ if (typeof value === "number" && Number.isFinite(value))
4299
+ return value;
4300
+ if (typeof value === "string" && value.length > 0) {
4301
+ const parsed = Number(value);
4302
+ return Number.isFinite(parsed) ? parsed : undefined;
4303
+ }
4304
+ return;
4305
+ }
4306
+ function bigintFromUnknown(value) {
4307
+ if (typeof value === "bigint")
4308
+ return value;
4309
+ if (typeof value === "number" && Number.isFinite(value))
4310
+ return BigInt(Math.trunc(value));
4311
+ if (typeof value === "string" && /^\d+$/.test(value))
4312
+ return BigInt(value);
4313
+ return;
4314
+ }
4315
+ function primitiveArray(value) {
4316
+ if (!Array.isArray(value))
4317
+ return;
4318
+ return value.filter((item) => item === null || ["string", "number", "boolean"].includes(typeof item));
4319
+ }
4320
+ function compactObject(input) {
4321
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
4322
+ }
4323
+ function stableSourceEventId(kind, value) {
4324
+ return `otlp:${kind}:${createHash3("sha256").update(stableJson(value)).digest("hex").slice(0, 32)}`;
4325
+ }
4326
+ function stableJson(value) {
4327
+ if (value === null || value === undefined)
4328
+ return "null";
4329
+ if (Array.isArray(value))
4330
+ return `[${value.map(stableJson).join(",")}]`;
4331
+ if (typeof value === "object") {
4332
+ const object = value;
4333
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
4334
+ }
4335
+ return JSON.stringify(value);
4336
+ }
4337
+
4338
+ // src/server/routes/otel.ts
4339
+ function otelRoutes(db) {
4340
+ const app = new Hono2;
4341
+ app.post("/v1/traces", async (c) => {
4342
+ const authError = requireServerOtlpIngest(db, c);
4343
+ if (authError)
4344
+ return authError;
4345
+ const payload = await readJsonObject(c);
4346
+ if (!payload.ok)
4347
+ return c.json({ error: payload.message }, payload.status);
4348
+ return writeOtlpResponse(c, () => ingestOtlpTraces(db, payload.value));
4349
+ });
4350
+ app.post("/v1/logs", async (c) => {
4351
+ const authError = requireServerOtlpIngest(db, c);
4352
+ if (authError)
4353
+ return authError;
4354
+ const payload = await readJsonObject(c);
4355
+ if (!payload.ok)
4356
+ return c.json({ error: payload.message }, payload.status);
4357
+ return writeOtlpResponse(c, () => ingestOtlpLogs(db, payload.value));
4358
+ });
4359
+ app.post("/v1/metrics", async (c) => {
4360
+ const authError = requireServerOtlpIngest(db, c);
4361
+ if (authError)
4362
+ return authError;
4363
+ const payload = await readJsonObject(c);
4364
+ if (!payload.ok)
4365
+ return c.json({ error: payload.message }, payload.status);
4366
+ return writeOtlpResponse(c, () => ingestOtlpMetrics(db, payload.value));
2152
4367
  });
2153
4368
  return app;
2154
4369
  }
4370
+ function requireServerOtlpIngest(db, c) {
4371
+ const authorization = authorizeLogIngest(db, c);
4372
+ if (!authorization)
4373
+ return c.json({ error: "Unauthorized" }, 401);
4374
+ if (authorization.kind === "browser-token") {
4375
+ return c.json({ error: "Browser ingest tokens cannot write OTLP telemetry" }, 403);
4376
+ }
4377
+ return null;
4378
+ }
4379
+ function writeOtlpResponse(c, ingest) {
4380
+ try {
4381
+ const summary = ingest();
4382
+ return c.json({
4383
+ partialSuccess: {},
4384
+ signal: summary.signal,
4385
+ accepted: summary.accepted,
4386
+ inserted: summary.inserted,
4387
+ duplicates: summary.duplicates,
4388
+ events: summary.events.map((event) => ({
4389
+ event_id: event.event_id,
4390
+ event_type: event.event_type,
4391
+ source: event.source,
4392
+ trace_id: event.trace_id,
4393
+ span_id: event.span_id
4394
+ }))
4395
+ }, 200);
4396
+ } catch (error) {
4397
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 422);
4398
+ }
4399
+ }
2155
4400
 
2156
4401
  // src/server/routes/perf.ts
2157
4402
  function perfRoutes(db) {
@@ -2178,13 +4423,17 @@ async function syncGithubRepo(db, project) {
2178
4423
  if (!project.github_repo)
2179
4424
  return project;
2180
4425
  const repo = project.github_repo.replace(/^https?:\/\/github\.com\//, "");
2181
- const headers = { Accept: "application/vnd.github.v3+json" };
4426
+ const headers = {
4427
+ Accept: "application/vnd.github.v3+json"
4428
+ };
2182
4429
  if (process.env.GITHUB_TOKEN)
2183
- headers["Authorization"] = `Bearer ${process.env.GITHUB_TOKEN}`;
4430
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
2184
4431
  try {
2185
4432
  const [repoRes, commitRes] = await Promise.all([
2186
4433
  fetch(`https://api.github.com/repos/${repo}`, { headers }),
2187
- fetch(`https://api.github.com/repos/${repo}/commits?per_page=1`, { headers })
4434
+ fetch(`https://api.github.com/repos/${repo}/commits?per_page=1`, {
4435
+ headers
4436
+ })
2188
4437
  ]);
2189
4438
  if (!repoRes.ok)
2190
4439
  return project;
@@ -2202,12 +4451,35 @@ async function syncGithubRepo(db, project) {
2202
4451
  }
2203
4452
 
2204
4453
  // src/server/routes/projects.ts
4454
+ var PROJECT_CREATE_KEYS = [
4455
+ "name",
4456
+ "github_repo",
4457
+ "base_url",
4458
+ "description"
4459
+ ];
4460
+ var PAGE_CREATE_KEYS = ["url", "path", "name"];
4461
+ var RETENTION_KEYS = [
4462
+ "max_rows",
4463
+ "debug_ttl_hours",
4464
+ "info_ttl_hours",
4465
+ "warn_ttl_hours",
4466
+ "error_ttl_hours"
4467
+ ];
4468
+ var PAGE_AUTH_KEYS = ["type", "credentials"];
4469
+ var PAGE_AUTH_TYPES = ["cookie", "bearer", "basic"];
4470
+ var BROWSER_TOKEN_KEYS = ["name", "allowed_origins"];
2205
4471
  function projectsRoutes(db) {
2206
4472
  const app = new Hono2;
2207
4473
  app.post("/", async (c) => {
2208
- const body = await c.req.json();
2209
- if (!body.name)
2210
- return c.json({ error: "name is required" }, 422);
4474
+ const parsed = await readJsonObject(c, {
4475
+ allowedKeys: PROJECT_CREATE_KEYS
4476
+ });
4477
+ if (!parsed.ok)
4478
+ return c.json({ error: parsed.message }, parsed.status);
4479
+ const projectInput = validateProjectCreate(parsed.value);
4480
+ if (!projectInput.ok)
4481
+ return c.json({ error: projectInput.message }, projectInput.status);
4482
+ const body = projectInput.value;
2211
4483
  const project = createProject(db, body);
2212
4484
  return c.json(project, 201);
2213
4485
  });
@@ -2219,15 +4491,51 @@ function projectsRoutes(db) {
2219
4491
  return c.json(project);
2220
4492
  });
2221
4493
  app.post("/:id/pages", async (c) => {
2222
- const body = await c.req.json();
2223
- if (!body.url)
2224
- return c.json({ error: "url is required" }, 422);
4494
+ const parsed = await readJsonObject(c, { allowedKeys: PAGE_CREATE_KEYS });
4495
+ if (!parsed.ok)
4496
+ return c.json({ error: parsed.message }, parsed.status);
4497
+ const pageInput = validatePageCreate(parsed.value);
4498
+ if (!pageInput.ok)
4499
+ return c.json({ error: pageInput.message }, pageInput.status);
4500
+ const body = pageInput.value;
2225
4501
  const page = createPage(db, { ...body, project_id: c.req.param("id") });
2226
4502
  return c.json(page, 201);
2227
4503
  });
2228
4504
  app.get("/:id/pages", (c) => c.json(listPages(db, c.req.param("id"))));
4505
+ app.post("/:id/browser-tokens", async (c) => {
4506
+ const project = getProject(db, c.req.param("id"));
4507
+ if (!project)
4508
+ return c.json({ error: "not found" }, 404);
4509
+ const parsed = await readJsonObject(c, { allowedKeys: BROWSER_TOKEN_KEYS });
4510
+ if (!parsed.ok)
4511
+ return c.json({ error: parsed.message }, parsed.status);
4512
+ const tokenInput = validateBrowserTokenCreate(parsed.value);
4513
+ if (!tokenInput.ok)
4514
+ return c.json({ error: tokenInput.message }, tokenInput.status);
4515
+ const token = createBrowserIngestToken(db, project.id, tokenInput.value);
4516
+ return c.json(token, 201);
4517
+ });
4518
+ app.get("/:id/browser-tokens", (c) => {
4519
+ const project = getProject(db, c.req.param("id"));
4520
+ if (!project)
4521
+ return c.json({ error: "not found" }, 404);
4522
+ return c.json(listBrowserIngestTokens(db, project.id));
4523
+ });
4524
+ app.delete("/:id/browser-tokens/:token_id", (c) => {
4525
+ const project = getProject(db, c.req.param("id"));
4526
+ if (!project)
4527
+ return c.json({ error: "not found" }, 404);
4528
+ const revoked = revokeBrowserIngestToken(db, project.id, c.req.param("token_id"));
4529
+ return c.json({ revoked });
4530
+ });
2229
4531
  app.put("/:id/retention", async (c) => {
2230
- const body = await c.req.json();
4532
+ const parsed = await readJsonObject(c, { allowedKeys: RETENTION_KEYS });
4533
+ if (!parsed.ok)
4534
+ return c.json({ error: parsed.message }, parsed.status);
4535
+ const retentionInput = validateRetention(parsed.value);
4536
+ if (!retentionInput.ok)
4537
+ return c.json({ error: retentionInput.message }, retentionInput.status);
4538
+ const body = retentionInput.value;
2231
4539
  setRetentionPolicy(db, c.req.param("id"), body);
2232
4540
  return c.json({ updated: true });
2233
4541
  });
@@ -2236,9 +4544,13 @@ function projectsRoutes(db) {
2236
4544
  return c.json(result);
2237
4545
  });
2238
4546
  app.post("/:id/pages/:page_id/auth", async (c) => {
2239
- const { type, credentials } = await c.req.json();
2240
- if (!type || !credentials)
2241
- return c.json({ error: "type and credentials required" }, 422);
4547
+ const parsed = await readJsonObject(c, { allowedKeys: PAGE_AUTH_KEYS });
4548
+ if (!parsed.ok)
4549
+ return c.json({ error: parsed.message }, parsed.status);
4550
+ const authInput = validatePageAuth(parsed.value);
4551
+ if (!authInput.ok)
4552
+ return c.json({ error: authInput.message }, authInput.status);
4553
+ const { type, credentials } = authInput.value;
2242
4554
  const result = setPageAuth(db, c.req.param("page_id"), type, credentials);
2243
4555
  return c.json({ id: result.id, type: result.type, created_at: result.created_at }, 201);
2244
4556
  });
@@ -2257,211 +4569,409 @@ function projectsRoutes(db) {
2257
4569
  });
2258
4570
  return app;
2259
4571
  }
2260
-
2261
- // node_modules/hono/dist/utils/stream.js
2262
- var StreamingApi = class {
2263
- writer;
2264
- encoder;
2265
- writable;
2266
- abortSubscribers = [];
2267
- responseReadable;
2268
- aborted = false;
2269
- closed = false;
2270
- constructor(writable, _readable) {
2271
- this.writable = writable;
2272
- this.writer = writable.getWriter();
2273
- this.encoder = new TextEncoder;
2274
- const reader = _readable.getReader();
2275
- this.abortSubscribers.push(async () => {
2276
- await reader.cancel();
2277
- });
2278
- this.responseReadable = new ReadableStream({
2279
- async pull(controller) {
2280
- const { done, value } = await reader.read();
2281
- done ? controller.close() : controller.enqueue(value);
2282
- },
2283
- cancel: () => {
2284
- this.abort();
2285
- }
2286
- });
2287
- }
2288
- async write(input) {
2289
- try {
2290
- if (typeof input === "string") {
2291
- input = this.encoder.encode(input);
2292
- }
2293
- await this.writer.write(input);
2294
- } catch {}
2295
- return this;
2296
- }
2297
- async writeln(input) {
2298
- await this.write(input + `
2299
- `);
2300
- return this;
2301
- }
2302
- sleep(ms) {
2303
- return new Promise((res) => setTimeout(res, ms));
2304
- }
2305
- async close() {
2306
- try {
2307
- await this.writer.close();
2308
- } catch {}
2309
- this.closed = true;
2310
- }
2311
- async pipe(body) {
2312
- this.writer.releaseLock();
2313
- await body.pipeTo(this.writable, { preventClose: true });
2314
- this.writer = this.writable.getWriter();
2315
- }
2316
- onAbort(listener) {
2317
- this.abortSubscribers.push(listener);
4572
+ function validateProjectCreate(body) {
4573
+ const name = requiredString(body, "name");
4574
+ if (!name.ok)
4575
+ return name;
4576
+ const github_repo = optionalString(body, "github_repo");
4577
+ if (!github_repo.ok)
4578
+ return github_repo;
4579
+ const base_url = optionalString(body, "base_url");
4580
+ if (!base_url.ok)
4581
+ return base_url;
4582
+ if (base_url.value !== undefined && !isUrl2(base_url.value)) {
4583
+ return {
4584
+ ok: false,
4585
+ status: 422,
4586
+ message: "body.base_url must be a valid URL"
4587
+ };
2318
4588
  }
2319
- abort() {
2320
- if (!this.aborted) {
2321
- this.aborted = true;
2322
- this.abortSubscribers.forEach((subscriber) => subscriber());
4589
+ const description = optionalString(body, "description");
4590
+ if (!description.ok)
4591
+ return description;
4592
+ return {
4593
+ ok: true,
4594
+ value: {
4595
+ name: name.value,
4596
+ github_repo: github_repo.value,
4597
+ base_url: base_url.value,
4598
+ description: description.value
2323
4599
  }
4600
+ };
4601
+ }
4602
+ function validatePageCreate(body) {
4603
+ const url = requiredString(body, "url");
4604
+ if (!url.ok)
4605
+ return url;
4606
+ if (!isUrl2(url.value))
4607
+ return { ok: false, status: 422, message: "body.url must be a valid URL" };
4608
+ const path = optionalString(body, "path");
4609
+ if (!path.ok)
4610
+ return path;
4611
+ const name = optionalString(body, "name");
4612
+ if (!name.ok)
4613
+ return name;
4614
+ return {
4615
+ ok: true,
4616
+ value: { url: url.value, path: path.value, name: name.value }
4617
+ };
4618
+ }
4619
+ function validateRetention(body) {
4620
+ const value = {};
4621
+ for (const key of RETENTION_KEYS) {
4622
+ const field = optionalNumber(body, key, { integer: true, min: 1 });
4623
+ if (!field.ok)
4624
+ return field;
4625
+ if (field.value !== undefined)
4626
+ value[key] = field.value;
2324
4627
  }
2325
- };
2326
-
2327
- // node_modules/hono/dist/helper/streaming/utils.js
2328
- var isOldBunVersion = () => {
2329
- const version = typeof Bun !== "undefined" ? Bun.version : undefined;
2330
- if (version === undefined) {
2331
- return false;
2332
- }
2333
- const result = version.startsWith("1.1") || version.startsWith("1.0") || version.startsWith("0.");
2334
- isOldBunVersion = () => result;
2335
- return result;
2336
- };
2337
-
2338
- // node_modules/hono/dist/helper/streaming/sse.js
2339
- var SSEStreamingApi = class extends StreamingApi {
2340
- constructor(writable, readable) {
2341
- super(writable, readable);
2342
- }
2343
- async writeSSE(message) {
2344
- const data = await resolveCallback(message.data, HtmlEscapedCallbackPhase.Stringify, false, {});
2345
- const dataLines = data.split(/\r\n|\r|\n/).map((line) => {
2346
- return `data: ${line}`;
2347
- }).join(`
2348
- `);
2349
- for (const key of ["event", "id", "retry"]) {
2350
- if (message[key] && /[\r\n]/.test(message[key])) {
2351
- throw new Error(`${key} must not contain "\\r" or "\\n"`);
2352
- }
4628
+ return { ok: true, value };
4629
+ }
4630
+ function validatePageAuth(body) {
4631
+ const type = requiredEnum(body, "type", PAGE_AUTH_TYPES);
4632
+ if (!type.ok)
4633
+ return type;
4634
+ const credentials = requiredString(body, "credentials");
4635
+ if (!credentials.ok)
4636
+ return credentials;
4637
+ return {
4638
+ ok: true,
4639
+ value: { type: type.value, credentials: credentials.value }
4640
+ };
4641
+ }
4642
+ function validateBrowserTokenCreate(body) {
4643
+ const name = optionalString(body, "name");
4644
+ if (!name.ok)
4645
+ return name;
4646
+ const allowedOrigins = optionalStringArray(body, "allowed_origins", {
4647
+ maxItems: 20
4648
+ });
4649
+ if (!allowedOrigins.ok)
4650
+ return allowedOrigins;
4651
+ if (allowedOrigins.value) {
4652
+ const normalizedOrigins = [];
4653
+ for (const origin of allowedOrigins.value) {
4654
+ const normalized = normalizeOrigin2(origin);
4655
+ if (!normalized)
4656
+ return {
4657
+ ok: false,
4658
+ status: 422,
4659
+ message: "body.allowed_origins must contain valid URL origins"
4660
+ };
4661
+ normalizedOrigins.push(normalized);
2353
4662
  }
2354
- const sseData = [
2355
- message.event && `event: ${message.event}`,
2356
- dataLines,
2357
- message.id && `id: ${message.id}`,
2358
- message.retry && `retry: ${message.retry}`
2359
- ].filter(Boolean).join(`
2360
- `) + `
2361
-
2362
- `;
2363
- await this.write(sseData);
4663
+ return {
4664
+ ok: true,
4665
+ value: {
4666
+ name: name.value,
4667
+ allowed_origins: [...new Set(normalizedOrigins)]
4668
+ }
4669
+ };
2364
4670
  }
2365
- };
2366
- var run = async (stream, cb, onError) => {
4671
+ return { ok: true, value: { name: name.value } };
4672
+ }
4673
+ function isUrl2(value) {
2367
4674
  try {
2368
- await cb(stream);
2369
- } catch (e) {
2370
- if (e instanceof Error && onError) {
2371
- await onError(e, stream);
2372
- await stream.writeSSE({
2373
- event: "error",
2374
- data: e.message
2375
- });
2376
- } else {
2377
- console.error(e);
2378
- }
2379
- } finally {
2380
- stream.close();
4675
+ new URL(value);
4676
+ return true;
4677
+ } catch {
4678
+ return false;
2381
4679
  }
2382
- };
2383
- var contextStash = /* @__PURE__ */ new WeakMap;
2384
- var streamSSE = (c, cb, onError) => {
2385
- const { readable, writable } = new TransformStream;
2386
- const stream = new SSEStreamingApi(writable, readable);
2387
- if (isOldBunVersion()) {
2388
- c.req.raw.signal.addEventListener("abort", () => {
2389
- if (!stream.closed) {
2390
- stream.abort();
2391
- }
2392
- });
4680
+ }
4681
+ function normalizeOrigin2(value) {
4682
+ try {
4683
+ return new URL(value).origin;
4684
+ } catch {
4685
+ return null;
2393
4686
  }
2394
- contextStash.set(stream.responseReadable, c);
2395
- c.header("Transfer-Encoding", "chunked");
2396
- c.header("Content-Type", "text/event-stream");
2397
- c.header("Cache-Control", "no-cache");
2398
- c.header("Connection", "keep-alive");
2399
- run(stream, cb, onError);
2400
- return c.newResponse(stream.responseReadable);
2401
- };
4687
+ }
2402
4688
 
2403
4689
  // src/server/routes/stream.ts
2404
4690
  function streamRoutes(db) {
2405
4691
  const app = new Hono2;
2406
4692
  app.get("/", (c) => {
2407
- const { project_id, level, service } = c.req.query();
4693
+ const { project_id, level, service, last_event_id } = c.req.query();
4694
+ const filter = {
4695
+ project_id: project_id || undefined,
4696
+ levels: level ? level.split(",").filter(Boolean) : undefined,
4697
+ service: service || undefined
4698
+ };
4699
+ const requestedLastId = c.req.header("last-event-id") || last_event_id || null;
2408
4700
  return streamSSE(c, async (stream2) => {
2409
- let lastId = null;
2410
- const latest = db.prepare("SELECT id FROM logs ORDER BY timestamp DESC LIMIT 1").get();
2411
- lastId = latest?.id ?? null;
2412
- while (true) {
2413
- const conditions = [];
2414
- const params = {};
2415
- if (lastId) {
2416
- conditions.push("rowid > (SELECT rowid FROM logs WHERE id = $lastId)");
2417
- params.$lastId = lastId;
2418
- }
2419
- if (project_id) {
2420
- conditions.push("project_id = $project_id");
2421
- params.$project_id = project_id;
2422
- }
2423
- if (level) {
2424
- conditions.push("level IN (" + level.split(",").map((l, i) => `$l${i}`).join(",") + ")");
2425
- level.split(",").forEach((l, i) => {
2426
- params[`$l${i}`] = l;
4701
+ const seen = new Set;
4702
+ let lastId = requestedLastId;
4703
+ if (lastId) {
4704
+ if (!logIdExists(db, lastId)) {
4705
+ const requestedLastId2 = lastId;
4706
+ lastId = latestLogId(db, filter);
4707
+ await writeOverflow(stream2, {
4708
+ reason: "last_event_id_unknown",
4709
+ dropped: 0,
4710
+ last_event_id: lastId,
4711
+ requested_last_event_id: requestedLastId2
2427
4712
  });
4713
+ } else {
4714
+ if (!hasBufferedLogEvent(lastId)) {
4715
+ await writeOverflow(stream2, {
4716
+ reason: "buffer_miss_sqlite_catchup",
4717
+ dropped: 0,
4718
+ last_event_id: lastId
4719
+ });
4720
+ }
4721
+ const catchup = await writeCatchupRows(db, stream2, filter, lastId, seen);
4722
+ if (catchup.last_id) {
4723
+ lastId = catchup.last_id;
4724
+ }
2428
4725
  }
2429
- if (service) {
2430
- conditions.push("service = $service");
2431
- params.$service = service;
2432
- }
2433
- const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
2434
- const rows = db.prepare(`SELECT * FROM logs ${where} ORDER BY timestamp ASC LIMIT 50`).all(params);
2435
- for (const row of rows) {
2436
- await stream2.writeSSE({ data: JSON.stringify(row), id: row.id, event: row.level });
2437
- lastId = row.id;
4726
+ } else {
4727
+ lastId = latestLogId(db, filter);
4728
+ }
4729
+ const subscription = subscribeLogEvents(filter);
4730
+ let pendingBusEvent = subscription.next();
4731
+ try {
4732
+ while (true) {
4733
+ const next = await Promise.race([
4734
+ pendingBusEvent.then((result) => ({
4735
+ kind: "bus",
4736
+ result
4737
+ })),
4738
+ sleep2(500).then(() => ({ kind: "tick" }))
4739
+ ]);
4740
+ if (next.kind === "tick") {
4741
+ if (!lastId) {
4742
+ lastId = latestLogId(db, filter);
4743
+ continue;
4744
+ }
4745
+ const catchup = await writeCatchupRows(db, stream2, filter, lastId, seen);
4746
+ if (catchup.last_id)
4747
+ lastId = catchup.last_id;
4748
+ continue;
4749
+ }
4750
+ if (next.result.done)
4751
+ break;
4752
+ const event = next.result.value;
4753
+ pendingBusEvent = subscription.next();
4754
+ if (event.kind === "overflow") {
4755
+ await writeOverflow(stream2, event);
4756
+ if (lastId) {
4757
+ const catchup = await writeCatchupRows(db, stream2, filter, lastId, seen);
4758
+ if (catchup.last_id)
4759
+ lastId = catchup.last_id;
4760
+ }
4761
+ continue;
4762
+ }
4763
+ if (seen.has(event.id))
4764
+ continue;
4765
+ await writeLogRow(stream2, event.row);
4766
+ seen.add(event.id);
4767
+ lastId = event.id;
4768
+ trimSeen2(seen);
2438
4769
  }
2439
- await stream2.sleep(500);
4770
+ } finally {
4771
+ await subscription.return?.();
2440
4772
  }
2441
4773
  });
2442
4774
  });
2443
4775
  return app;
2444
4776
  }
4777
+ async function writeCatchupRows(db, stream2, filter, lastId, seen) {
4778
+ const anchor = db.prepare("SELECT rowid FROM logs WHERE id = ?").get(lastId);
4779
+ if (!anchor)
4780
+ return { anchor_found: false, last_id: null };
4781
+ let cursor = anchor.rowid;
4782
+ let lastWritten = null;
4783
+ let total = 0;
4784
+ while (total < 1000) {
4785
+ const rows = queryRowsAfterRowid(db, filter, cursor, 100);
4786
+ if (rows.length === 0)
4787
+ break;
4788
+ for (const row of rows) {
4789
+ cursor = row.rowid;
4790
+ if (seen.has(row.id))
4791
+ continue;
4792
+ await writeLogRow(stream2, row);
4793
+ seen.add(row.id);
4794
+ lastWritten = row.id;
4795
+ total += 1;
4796
+ trimSeen2(seen);
4797
+ }
4798
+ if (rows.length < 100)
4799
+ break;
4800
+ }
4801
+ if (total >= 1000) {
4802
+ await writeOverflow(stream2, {
4803
+ reason: "sqlite_catchup_truncated",
4804
+ dropped: 0,
4805
+ last_event_id: lastWritten ?? lastId
4806
+ });
4807
+ }
4808
+ return { anchor_found: true, last_id: lastWritten };
4809
+ }
4810
+ function latestLogId(db, filter) {
4811
+ const { where, params } = buildWhere(filter, null);
4812
+ const row = db.prepare(`SELECT id FROM logs ${where} ORDER BY rowid DESC LIMIT 1`).get(params);
4813
+ return row?.id ?? null;
4814
+ }
4815
+ function logIdExists(db, id) {
4816
+ const row = db.prepare("SELECT 1 AS found FROM logs WHERE id = ?").get(id);
4817
+ return Boolean(row);
4818
+ }
4819
+ function queryRowsAfterRowid(db, filter, rowid, limit) {
4820
+ const { where, params } = buildWhere(filter, rowid);
4821
+ return db.prepare(`SELECT rowid, * FROM logs ${where} ORDER BY rowid ASC LIMIT $limit`).all({ ...params, $limit: limit });
4822
+ }
4823
+ function buildWhere(filter, afterRowid) {
4824
+ const conditions = [];
4825
+ const params = {};
4826
+ if (afterRowid !== null) {
4827
+ conditions.push("rowid > $rowid");
4828
+ params.$rowid = afterRowid;
4829
+ }
4830
+ if (filter.project_id) {
4831
+ conditions.push("project_id = $project_id");
4832
+ params.$project_id = filter.project_id;
4833
+ }
4834
+ if (filter.levels && filter.levels.length > 0) {
4835
+ const placeholders = filter.levels.map((_, index) => `$level${index}`).join(",");
4836
+ filter.levels.forEach((level, index) => {
4837
+ params[`$level${index}`] = level;
4838
+ });
4839
+ conditions.push(`level IN (${placeholders})`);
4840
+ }
4841
+ if (filter.service) {
4842
+ conditions.push("service = $service");
4843
+ params.$service = filter.service;
4844
+ }
4845
+ return {
4846
+ where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "",
4847
+ params
4848
+ };
4849
+ }
4850
+ async function writeLogRow(stream2, row) {
4851
+ await stream2.writeSSE({
4852
+ data: JSON.stringify(row),
4853
+ id: row.id,
4854
+ event: row.level
4855
+ });
4856
+ }
4857
+ async function writeOverflow(stream2, data) {
4858
+ await stream2.writeSSE({
4859
+ event: "overflow",
4860
+ data: JSON.stringify({
4861
+ type: "overflow",
4862
+ reason: data.reason,
4863
+ dropped: data.dropped,
4864
+ last_event_id: "last_event_id" in data ? data.last_event_id : null,
4865
+ requested_last_event_id: "requested_last_event_id" in data ? data.requested_last_event_id : null,
4866
+ created_at: "created_at" in data ? data.created_at : new Date().toISOString()
4867
+ })
4868
+ });
4869
+ }
4870
+ function trimSeen2(seen) {
4871
+ while (seen.size > 2000) {
4872
+ const first = seen.values().next().value;
4873
+ if (!first)
4874
+ return;
4875
+ seen.delete(first);
4876
+ }
4877
+ }
4878
+ function sleep2(ms) {
4879
+ return new Promise((resolve) => setTimeout(resolve, ms));
4880
+ }
4881
+
4882
+ // src/server/routes/test-reports.ts
4883
+ function testReportsRoutes(db) {
4884
+ const app = new Hono2;
4885
+ app.get("/", (c) => {
4886
+ return c.json(searchTestReports(db, queryFromRequest2(c.req.query())));
4887
+ });
4888
+ app.get("/:report_id", (c) => {
4889
+ const report = getTestReport(db, c.req.param("report_id"), c.req.query("include_cases") !== "false");
4890
+ if (!report)
4891
+ return c.json({ error: "Test report not found" }, 404);
4892
+ return c.json(report);
4893
+ });
4894
+ return app;
4895
+ }
4896
+ function queryFromRequest2(query) {
4897
+ return {
4898
+ report_id: query.report_id,
4899
+ event_id: query.event_id,
4900
+ project_id: query.project_id,
4901
+ machine_id: query.machine_id,
4902
+ repo_id: query.repo_id,
4903
+ app_id: query.app_id,
4904
+ process_id: query.process_id,
4905
+ run_id: query.run_id,
4906
+ environment: query.environment,
4907
+ source: query.source,
4908
+ parser: query.parser,
4909
+ parse_status: query.parse_status,
4910
+ path: query.path,
4911
+ case_status: query.case_status,
4912
+ outcome: testReportOutcome(query.outcome),
4913
+ min_failures: query.min_failures ? Number(query.min_failures) : undefined,
4914
+ min_errors: query.min_errors ? Number(query.min_errors) : undefined,
4915
+ min_skipped: query.min_skipped ? Number(query.min_skipped) : undefined,
4916
+ since: query.since,
4917
+ until: query.until,
4918
+ text: query.text,
4919
+ include_cases: query.include_cases === "true" || query.include_cases === "1",
4920
+ limit: query.limit ? Number(query.limit) : undefined,
4921
+ offset: query.offset ? Number(query.offset) : undefined
4922
+ };
4923
+ }
4924
+ function testReportOutcome(value) {
4925
+ if (value === "failed" || value === "error" || value === "nonpassing" || value === "skipped" || value === "passed" || value === "parse_problem") {
4926
+ return value;
4927
+ }
4928
+ return;
4929
+ }
2445
4930
 
2446
4931
  // src/server/index.ts
2447
4932
  exitIfMetadataRequest({
2448
4933
  name: "logs-serve",
2449
4934
  description: "Start the @hasna/logs REST API server.",
2450
- options: [" -p, --port <n> Port to listen on (default: LOGS_PORT or 3460)"]
4935
+ options: [
4936
+ " -p, --port <n> Port to listen on (default: LOGS_PORT or 3460)",
4937
+ " --token <tok> Require this API token for /api/* requests",
4938
+ " --local-open Explicitly allow trusted local loopback API requests without a token"
4939
+ ]
2451
4940
  });
2452
4941
  var portArg = readOptionValue(["--port", "-p"]);
4942
+ var tokenArg = readOptionValue(["--token"]);
4943
+ if (tokenArg)
4944
+ process.env.HASNA_LOGS_API_TOKEN = tokenArg;
4945
+ if (hasOption(["--local-open"]))
4946
+ process.env.HASNA_LOGS_LOCAL_OPEN = "1";
2453
4947
  var PORT = Number(portArg ?? process.env.LOGS_PORT ?? 3460);
2454
4948
  var db = getDb();
2455
4949
  var app = new Hono2;
2456
- app.use("*", cors());
4950
+ var serverDir = dirname2(fileURLToPath(import.meta.url));
4951
+ var dashboardRoot = resolveDashboardRoot();
4952
+ app.use("*", cors({
4953
+ origin: (origin) => resolveCorsOrigin(origin),
4954
+ allowHeaders: [
4955
+ "Content-Type",
4956
+ "Authorization",
4957
+ "X-Logs-Token",
4958
+ "X-Logs-Browser-Token",
4959
+ "X-Logs-Write-Token"
4960
+ ],
4961
+ allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
4962
+ }));
2457
4963
  app.get("/script.js", (c) => {
2458
4964
  const host = `${c.req.header("x-forwarded-proto") ?? "http"}://${c.req.header("host") ?? `localhost:${PORT}`}`;
2459
4965
  c.header("Content-Type", "application/javascript");
2460
4966
  c.header("Cache-Control", "public, max-age=300");
2461
4967
  return c.text(getBrowserScript(host));
2462
4968
  });
4969
+ app.use("/api/*", requireApiTokenOrBrowserIngest(db));
2463
4970
  app.route("/api/logs", logsRoutes(db));
2464
4971
  app.route("/api/logs/stream", streamRoutes(db));
4972
+ app.route("/api/events", eventsRoutes(db));
4973
+ app.route("/api/test-reports", testReportsRoutes(db));
4974
+ app.route("/api/otel", otelRoutes(db));
2465
4975
  app.route("/api/projects", projectsRoutes(db));
2466
4976
  app.route("/api/jobs", jobsRoutes(db));
2467
4977
  app.route("/api/alerts", alertsRoutes(db));
@@ -2469,14 +4979,31 @@ app.route("/api/issues", issuesRoutes(db));
2469
4979
  app.route("/api/perf", perfRoutes(db));
2470
4980
  app.get("/health", (c) => c.json(getHealth(db)));
2471
4981
  app.get("/dashboard", (c) => c.redirect("/dashboard/"));
2472
- app.use("/dashboard/*", serveStatic2({ root: "./dashboard/dist", rewriteRequestPath: (p) => p.replace(/^\/dashboard/, "") }));
2473
- app.get("/", (c) => c.json({ service: "@hasna/logs", port: PORT, status: "ok", dashboard: `http://localhost:${PORT}/dashboard/` }));
4982
+ app.use("/dashboard/*", serveStatic2({
4983
+ root: dashboardRoot,
4984
+ rewriteRequestPath: (p) => p.replace(/^\/dashboard/, "")
4985
+ }));
4986
+ app.get("/", (c) => c.json({
4987
+ service: "@hasna/logs",
4988
+ port: PORT,
4989
+ status: "ok",
4990
+ dashboard: `http://localhost:${PORT}/dashboard/`
4991
+ }));
2474
4992
  startScheduler(db);
2475
- console.log(`@hasna/logs server running on http://localhost:${PORT}`);
4993
+ var apiAuthMode = getConfiguredApiToken() ? "token" : isLocalOpenModeEnabled() ? "local-open" : "locked";
4994
+ console.log(`@hasna/logs server running on http://localhost:${PORT} (api auth: ${apiAuthMode})`);
2476
4995
  var server_default = {
2477
4996
  port: PORT,
2478
4997
  fetch: app.fetch
2479
4998
  };
4999
+ function resolveDashboardRoot() {
5000
+ const cwdDashboardRoot = resolve(process.cwd(), "dashboard/dist");
5001
+ const candidates = [
5002
+ cwdDashboardRoot,
5003
+ resolve(serverDir, "../../dashboard/dist")
5004
+ ];
5005
+ return candidates.find((candidate) => existsSync(candidate)) ?? cwdDashboardRoot;
5006
+ }
2480
5007
  export {
2481
5008
  server_default as default
2482
5009
  };