ReviewOS

also looking at this

loghqorg/loghq

Deploy: uniform auth session model (main → production)

#7
Merged glennmichael123 wants to merge main into production
282 files +3028 -64856

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

app/Archive/sql.tsmodified+8-0
Changes to app/Archive/sql.ts
@@ -141,6 +141,8 @@ export interface ArchiveFilters {
141141 channel?: string
142142 environment?: string
143143 release?: string
144 traceId?: string
145 requestId?: string
144146 /** Free text matched against the message. */
145147 q?: string
146148 fromDay?: string
@@ -187,6 +189,12 @@ function filterClauses(f: ArchiveFilters): string[] {
187189 if (f.release)
188190 where.push(`"release" = ${sqlQuote(String(f.release).slice(0, 255))}`)
189191
192 if (f.traceId)
193 where.push(`"trace_id" = ${sqlQuote(String(f.traceId).slice(0, 64))}`)
194
195 if (f.requestId)
196 where.push(`"request_id" = ${sqlQuote(String(f.requestId).slice(0, 64))}`)
197
190198 if (f.q) {
191199 // `%` and `_` are left literal rather than escaped: a user typing them into
192200 // a log search means them as wildcards more often than not, and this reads
routes/analytics.tsmodified+2-0
Changes to routes/analytics.ts
@@ -127,6 +127,8 @@ route.get('/api/projects/{projectId}/archive/search', async (request: any) => {
127127 channel: q.channel ? String(q.channel) : undefined,
128128 environment: q.environment ? String(q.environment) : undefined,
129129 release: q.release ? String(q.release) : undefined,
130 traceId: q.trace ? String(q.trace) : undefined,
131 requestId: q.request ? String(q.request) : undefined,
130132 q: q.q ? String(q.q) : undefined,
131133 fromDay: q.from ? String(q.from) : undefined,
132134 toDay: q.to ? String(q.to) : undefined,
tests/unit/alert-channels.test.tsadded+22-0
Changes to tests/unit/alert-channels.test.ts
@@ -0,0 +1,22 @@
1import { describe, expect, test } from 'bun:test'
2import { validateWebhook } from '../../app/Errors/channels'
3
4describe('alert channel URL policy', () => {
5 test('accepts genuine Slack and Discord incoming webhooks', () => {
6 expect(validateWebhook('slack', 'https://hooks.slack.com/services/T/B/token')).toEqual({ ok: true })
7 expect(validateWebhook('discord', 'https://discord.com/api/webhooks/123/token')).toEqual({ ok: true })
8 expect(validateWebhook('discord', 'https://discord.com/api/v10/webhooks/123/token')).toEqual({ ok: true })
9 })
10
11 test('rejects non-HTTPS and lookalike provider hosts', () => {
12 expect(validateWebhook('slack', 'http://hooks.slack.com/services/T/B/token').ok).toBe(false)
13 expect(validateWebhook('slack', 'https://hooks.slack.com.attacker.test/services/T/B/token').ok).toBe(false)
14 expect(validateWebhook('discord', 'https://discord.com.attacker.test/api/webhooks/123/token').ok).toBe(false)
15 })
16
17 test('rejects provider pages that are not webhook endpoints', () => {
18 expect(validateWebhook('slack', 'https://hooks.slack.com/').ok).toBe(false)
19 expect(validateWebhook('discord', 'https://discord.com/channels/123').ok).toBe(false)
20 expect(validateWebhook('email', 'https://hooks.slack.com/services/T/B/token').ok).toBe(false)
21 })
22})
tests/unit/archive-sql.test.tsmodified+17-0
Changes to tests/unit/archive-sql.test.ts
@@ -164,6 +164,23 @@ describe('buildSearchSql', () => {
164164 expect(sql).not.toMatch(/OR '1'='1'/)
165165 })
166166
167 test('keeps trace and request correlation available after archival', () => {
168 const sql = buildSearchSql('logs', ['project_id=demo/day=2026-09-12/logs.parquet'], {
169 traceId: 'trace-42',
170 requestId: 'request-9',
171 })
172 expect(sql).toContain('"trace_id" = \'trace-42\'')
173 expect(sql).toContain('"request_id" = \'request-9\'')
174 })
175
176 test('caps archive correlation filters to their stored column width', () => {
177 const sql = buildSearchSql('logs', ['project_id=demo/day=2026-09-12/logs.parquet'], {
178 traceId: 't'.repeat(100),
179 })
180 expect(sql).toContain(`"trace_id" = '${'t'.repeat(64)}'`)
181 expect(sql).not.toContain('t'.repeat(65))
182 })
183
167184 test('pages on the whole (timestamp, id) pair, not timestamp alone', () => {
168185 // Entries share a millisecond routinely, and a cursor on a non-unique column
169186 // either repeats or skips rows at the page boundary.