@hatk/hatk 0.0.1-alpha.41 → 0.0.1-alpha.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +16 -553
- package/dist/database/adapters/sqlite.d.ts.map +1 -1
- package/dist/database/adapters/sqlite.js +2 -1
- package/dist/database/db.d.ts +23 -0
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +81 -4
- package/dist/labels.d.ts +2 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +5 -0
- package/dist/lexicon-resolve.d.ts.map +1 -1
- package/dist/lexicon-resolve.js +27 -112
- package/dist/lexicons/com/atproto/label/defs.json +75 -0
- package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
- package/dist/lexicons/com/atproto/repo/strongRef.json +24 -0
- package/dist/lexicons/dev/hatk/createRecord.json +40 -0
- package/dist/lexicons/dev/hatk/createReport.json +48 -0
- package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
- package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
- package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
- package/dist/lexicons/dev/hatk/describeLabels.json +31 -0
- package/dist/lexicons/dev/hatk/getFeed.json +30 -0
- package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
- package/dist/lexicons/dev/hatk/getRecord.json +26 -0
- package/dist/lexicons/dev/hatk/getRecords.json +32 -0
- package/dist/lexicons/dev/hatk/putPreference.json +28 -0
- package/dist/lexicons/dev/hatk/putRecord.json +41 -0
- package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
- package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +2 -1
- package/dist/pds-proxy.d.ts.map +1 -1
- package/dist/pds-proxy.js +15 -0
- package/dist/server-init.d.ts.map +1 -1
- package/dist/server-init.js +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +91 -13
- package/dist/templates/feed.tpl +14 -0
- package/dist/templates/hook.tpl +5 -0
- package/dist/templates/label.tpl +15 -0
- package/dist/templates/og.tpl +17 -0
- package/dist/templates/seed.tpl +11 -0
- package/dist/templates/setup.tpl +5 -0
- package/dist/templates/test-feed.tpl +19 -0
- package/dist/templates/test-xrpc.tpl +19 -0
- package/dist/templates/xrpc.tpl +41 -0
- package/package.json +3 -2
- package/public/admin.html +133 -0
- package/dist/cloudflare/container.d.ts +0 -73
- package/dist/cloudflare/container.d.ts.map +0 -1
- package/dist/cloudflare/container.js +0 -232
- package/dist/cloudflare/hooks.d.ts +0 -33
- package/dist/cloudflare/hooks.d.ts.map +0 -1
- package/dist/cloudflare/hooks.js +0 -40
- package/dist/cloudflare/init.d.ts +0 -27
- package/dist/cloudflare/init.d.ts.map +0 -1
- package/dist/cloudflare/init.js +0 -103
- package/dist/cloudflare/worker.d.ts +0 -27
- package/dist/cloudflare/worker.d.ts.map +0 -1
- package/dist/cloudflare/worker.js +0 -54
- package/dist/database/adapters/d1.d.ts +0 -56
- package/dist/database/adapters/d1.d.ts.map +0 -1
- package/dist/database/adapters/d1.js +0 -108
- package/dist/db.d.ts +0 -134
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -1327
- package/dist/fts.d.ts +0 -20
- package/dist/fts.d.ts.map +0 -1
- package/dist/fts.js +0 -767
- package/dist/oauth/hooks.d.ts +0 -10
- package/dist/oauth/hooks.d.ts.map +0 -1
- package/dist/oauth/hooks.js +0 -40
- package/dist/schema.d.ts +0 -59
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js +0 -387
- package/dist/test-browser.d.ts +0 -14
- package/dist/test-browser.d.ts.map +0 -1
- package/dist/test-browser.js +0 -26
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
import { createTestContext } from '@hatk/hatk/test'
|
|
3
|
+
|
|
4
|
+
let ctx: Awaited<ReturnType<typeof createTestContext>>
|
|
5
|
+
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
ctx = await createTestContext()
|
|
8
|
+
await ctx.loadFixtures()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
afterAll(async () => ctx?.close())
|
|
12
|
+
|
|
13
|
+
describe('{{name}} feed', () => {
|
|
14
|
+
test('returns results', async () => {
|
|
15
|
+
const feed = ctx.loadFeed('{{name}}')
|
|
16
|
+
const result = await feed.generate(ctx.feedContext({ limit: 10 }))
|
|
17
|
+
expect(result).toBeDefined()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
import { createTestContext } from '@hatk/hatk/test'
|
|
3
|
+
|
|
4
|
+
let ctx: Awaited<ReturnType<typeof createTestContext>>
|
|
5
|
+
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
ctx = await createTestContext()
|
|
8
|
+
await ctx.loadFixtures()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
afterAll(async () => ctx?.close())
|
|
12
|
+
|
|
13
|
+
describe('{{name}}', () => {
|
|
14
|
+
test('returns response', async () => {
|
|
15
|
+
const handler = ctx.loadXrpc('{{name}}')
|
|
16
|
+
const result = await handler.handler({ params: {} })
|
|
17
|
+
expect(result).toBeDefined()
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineQuery } from '$hatk'
|
|
2
|
+
|
|
3
|
+
export default defineQuery('{{name}}', async (ctx) => {
|
|
4
|
+
const { ok, db, params, packCursor, unpackCursor } = ctx
|
|
5
|
+
const limit = params.limit ?? 30
|
|
6
|
+
const cursor = params.cursor
|
|
7
|
+
|
|
8
|
+
const conditions: string[] = []
|
|
9
|
+
const sqlParams: (string | number)[] = []
|
|
10
|
+
let paramIdx = 1
|
|
11
|
+
|
|
12
|
+
if (cursor) {
|
|
13
|
+
const parsed = unpackCursor(cursor)
|
|
14
|
+
if (parsed) {
|
|
15
|
+
conditions.push(`(s.indexed_at < $${paramIdx} OR (s.indexed_at = $${paramIdx + 1} AND s.cid < $${paramIdx + 2}))`)
|
|
16
|
+
sqlParams.push(parsed.primary, parsed.primary, parsed.cid)
|
|
17
|
+
paramIdx += 3
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const where = conditions.length ? 'WHERE ' + conditions.join(' AND ') : ''
|
|
22
|
+
|
|
23
|
+
const rows = (await db.query(
|
|
24
|
+
`SELECT s.* FROM "your.collection.here" s ${where} ORDER BY s.indexed_at DESC, s.cid DESC LIMIT $${paramIdx}`,
|
|
25
|
+
sqlParams.concat([limit + 1]),
|
|
26
|
+
)) as {
|
|
27
|
+
uri: string
|
|
28
|
+
cid: string
|
|
29
|
+
did: string
|
|
30
|
+
indexed_at: string
|
|
31
|
+
}[]
|
|
32
|
+
|
|
33
|
+
const hasMore = rows.length > limit
|
|
34
|
+
if (hasMore) rows.pop()
|
|
35
|
+
const lastRow = rows[rows.length - 1]
|
|
36
|
+
|
|
37
|
+
return ok({
|
|
38
|
+
items: rows,
|
|
39
|
+
cursor: hasMore && lastRow ? packCursor(lastRow.indexed_at, lastRow.cid) : undefined,
|
|
40
|
+
})
|
|
41
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hatk/hatk",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.42",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hatk": "dist/cli.js"
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"./renderer": "./dist/renderer.js"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"
|
|
31
|
+
"clean": "rm -rf dist",
|
|
32
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && cp -r src/templates dist/templates && cp -r src/lexicons dist/lexicons",
|
|
32
33
|
"prepublishOnly": "npm run build"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
package/public/admin.html
CHANGED
|
@@ -1203,6 +1203,7 @@
|
|
|
1203
1203
|
<button class="tab active" data-tab="overview">Overview</button>
|
|
1204
1204
|
<button class="tab" data-tab="repos">Repos</button>
|
|
1205
1205
|
<button class="tab" data-tab="content">Content</button>
|
|
1206
|
+
<button class="tab" data-tab="reports">Reports</button>
|
|
1206
1207
|
</nav>
|
|
1207
1208
|
|
|
1208
1209
|
<!-- Overview -->
|
|
@@ -1277,6 +1278,21 @@
|
|
|
1277
1278
|
<div class="loading">Loading</div>
|
|
1278
1279
|
</div>
|
|
1279
1280
|
</div>
|
|
1281
|
+
|
|
1282
|
+
<!-- Reports -->
|
|
1283
|
+
<div class="tab-panel" id="panel-reports">
|
|
1284
|
+
<div class="search-bar">
|
|
1285
|
+
<select class="search-input" id="reports-status" style="max-width: 200px">
|
|
1286
|
+
<option value="open">Open</option>
|
|
1287
|
+
<option value="resolved">Resolved</option>
|
|
1288
|
+
<option value="dismissed">Dismissed</option>
|
|
1289
|
+
</select>
|
|
1290
|
+
<select class="search-input" id="reports-label-filter" style="max-width: 200px">
|
|
1291
|
+
<option value="">All labels</option>
|
|
1292
|
+
</select>
|
|
1293
|
+
</div>
|
|
1294
|
+
<div id="reports-results"></div>
|
|
1295
|
+
</div>
|
|
1280
1296
|
</div>
|
|
1281
1297
|
|
|
1282
1298
|
<!-- Bottom nav (mobile) -->
|
|
@@ -1285,6 +1301,7 @@
|
|
|
1285
1301
|
<button class="bnav-btn active" data-tab="overview">Overview</button>
|
|
1286
1302
|
<button class="bnav-btn" data-tab="repos">Repos</button>
|
|
1287
1303
|
<button class="bnav-btn" data-tab="content">Content</button>
|
|
1304
|
+
<button class="bnav-btn" data-tab="reports">Reports</button>
|
|
1288
1305
|
</div>
|
|
1289
1306
|
</div>
|
|
1290
1307
|
</div>
|
|
@@ -1468,6 +1485,7 @@
|
|
|
1468
1485
|
if (tab === 'overview') loadOverview()
|
|
1469
1486
|
if (tab === 'repos') loadRepos()
|
|
1470
1487
|
if (tab === 'content') loadContent()
|
|
1488
|
+
if (tab === 'reports') loadReports()
|
|
1471
1489
|
if (push) pushURL({ tab, status: '', q: '', offset: 0, cq: '' })
|
|
1472
1490
|
}
|
|
1473
1491
|
|
|
@@ -1494,6 +1512,7 @@
|
|
|
1494
1512
|
<div class="stat-card"><div class="stat-label">Pending</div><div class="stat-value yellow">${fmt(repoStatuses.pending)}</div></div>
|
|
1495
1513
|
<div class="stat-card"><div class="stat-label">Failed</div><div class="stat-value red">${fmt(repoStatuses.failed)}</div></div>
|
|
1496
1514
|
<div class="stat-card"><div class="stat-label">Taken Down</div><div class="stat-value red">${fmt(repoStatuses.takendown)}</div></div>
|
|
1515
|
+
${info.openReports > 0 ? `<div class="stat-card" style="cursor:pointer" onclick="activateTab('reports')"><div class="stat-label">Open Reports</div><div class="stat-value yellow">${fmt(info.openReports)}</div></div>` : ''}
|
|
1497
1516
|
`
|
|
1498
1517
|
|
|
1499
1518
|
const collectionCards = document.getElementById('collection-cards')
|
|
@@ -2107,6 +2126,120 @@
|
|
|
2107
2126
|
})
|
|
2108
2127
|
})
|
|
2109
2128
|
}
|
|
2129
|
+
|
|
2130
|
+
// ── Reports ──
|
|
2131
|
+
|
|
2132
|
+
const reportsPage = { limit: 50, offset: 0 }
|
|
2133
|
+
|
|
2134
|
+
function populateReportsLabelFilter() {
|
|
2135
|
+
const select = document.getElementById('reports-label-filter')
|
|
2136
|
+
const current = select.value
|
|
2137
|
+
select.innerHTML = '<option value="">All labels</option>' +
|
|
2138
|
+
labelDefinitions.map(d => `<option value="${d.identifier}">${d.identifier}</option>`).join('')
|
|
2139
|
+
select.value = current
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
document.getElementById('reports-status').addEventListener('change', () => {
|
|
2143
|
+
reportsPage.offset = 0
|
|
2144
|
+
loadReports()
|
|
2145
|
+
})
|
|
2146
|
+
document.getElementById('reports-label-filter').addEventListener('change', () => {
|
|
2147
|
+
reportsPage.offset = 0
|
|
2148
|
+
loadReports()
|
|
2149
|
+
})
|
|
2150
|
+
|
|
2151
|
+
async function loadReports() {
|
|
2152
|
+
populateReportsLabelFilter()
|
|
2153
|
+
const status = document.getElementById('reports-status').value
|
|
2154
|
+
const label = document.getElementById('reports-label-filter').value
|
|
2155
|
+
const container = document.getElementById('reports-results')
|
|
2156
|
+
container.innerHTML = '<div class="loading">Loading</div>'
|
|
2157
|
+
try {
|
|
2158
|
+
let url = `/admin/reports?status=${status}&limit=${reportsPage.limit}&offset=${reportsPage.offset}`
|
|
2159
|
+
if (label) url += `&label=${encodeURIComponent(label)}`
|
|
2160
|
+
const result = await api(url)
|
|
2161
|
+
renderReports(result.reports || [], result.total)
|
|
2162
|
+
} catch (e) {
|
|
2163
|
+
container.innerHTML = `<div class="empty-state">${escapeHtml(e.message)}</div>`
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function renderReports(reports, total) {
|
|
2168
|
+
const container = document.getElementById('reports-results')
|
|
2169
|
+
if (!reports.length) {
|
|
2170
|
+
container.innerHTML = '<div class="empty-state">No reports found</div>'
|
|
2171
|
+
return
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
const showPagination = total != null && total > reportsPage.limit
|
|
2175
|
+
const paginationHtml = showPagination ? `
|
|
2176
|
+
<div class="pagination">
|
|
2177
|
+
<span>${reportsPage.offset + 1}\u2013${Math.min(reportsPage.offset + reportsPage.limit, total)} of ${total.toLocaleString()}</span>
|
|
2178
|
+
<div class="pagination-buttons">
|
|
2179
|
+
<button class="btn btn-sm" data-reports-page="prev" ${reportsPage.offset === 0 ? 'disabled' : ''}>Prev</button>
|
|
2180
|
+
<button class="btn btn-sm" data-reports-page="next" ${reportsPage.offset + reportsPage.limit >= total ? 'disabled' : ''}>Next</button>
|
|
2181
|
+
</div>
|
|
2182
|
+
</div>
|
|
2183
|
+
` : ''
|
|
2184
|
+
|
|
2185
|
+
const countLabel = total != null
|
|
2186
|
+
? `${total.toLocaleString()} report${total !== 1 ? 's' : ''}`
|
|
2187
|
+
: `${reports.length} result${reports.length !== 1 ? 's' : ''}`
|
|
2188
|
+
|
|
2189
|
+
const isOpen = document.getElementById('reports-status').value === 'open'
|
|
2190
|
+
|
|
2191
|
+
container.innerHTML = `
|
|
2192
|
+
<div class="card">
|
|
2193
|
+
<div class="result-count">${countLabel}</div>
|
|
2194
|
+
${reports.map(r => {
|
|
2195
|
+
const reporterDisplay = r.reported_by_handle ? `@${escapeHtml(r.reported_by_handle)}` : escapeHtml(r.reported_by)
|
|
2196
|
+
const date = new Date(r.created_at).toLocaleString()
|
|
2197
|
+
return `<div class="record-card">
|
|
2198
|
+
<div class="record-header">
|
|
2199
|
+
<div class="record-meta">
|
|
2200
|
+
<div class="record-uri" title="${escapeHtml(r.subject_uri)}">${escapeHtml(r.subject_uri)}</div>
|
|
2201
|
+
<div class="record-summary">
|
|
2202
|
+
<span class="label-tag">${escapeHtml(r.label)}</span>
|
|
2203
|
+
reported by ${reporterDisplay} · ${date}
|
|
2204
|
+
</div>
|
|
2205
|
+
${r.reason ? `<div class="record-summary" style="margin-top:0.25rem">${escapeHtml(r.reason)}</div>` : ''}
|
|
2206
|
+
${!isOpen ? `<div class="record-summary" style="margin-top:0.25rem;opacity:0.6">${escapeHtml(r.status)}${r.resolved_by ? ` by ${escapeHtml(r.resolved_by)}` : ''}</div>` : ''}
|
|
2207
|
+
</div>
|
|
2208
|
+
${isOpen ? `<div class="record-actions">
|
|
2209
|
+
<button class="btn btn-sm" data-action="resolve-report" data-id="${r.id}" data-resolve="resolve" style="background:var(--accent);color:white">Apply Label</button>
|
|
2210
|
+
<button class="btn btn-sm" data-action="resolve-report" data-id="${r.id}" data-resolve="dismiss">Dismiss</button>
|
|
2211
|
+
</div>` : ''}
|
|
2212
|
+
</div>
|
|
2213
|
+
</div>`
|
|
2214
|
+
}).join('')}
|
|
2215
|
+
${paginationHtml}
|
|
2216
|
+
</div>
|
|
2217
|
+
`
|
|
2218
|
+
|
|
2219
|
+
container.querySelectorAll('[data-reports-page="prev"]').forEach(b => {
|
|
2220
|
+
b.addEventListener('click', () => { reportsPage.offset = Math.max(0, reportsPage.offset - reportsPage.limit); loadReports() })
|
|
2221
|
+
})
|
|
2222
|
+
container.querySelectorAll('[data-reports-page="next"]').forEach(b => {
|
|
2223
|
+
b.addEventListener('click', () => { reportsPage.offset += reportsPage.limit; loadReports() })
|
|
2224
|
+
})
|
|
2225
|
+
|
|
2226
|
+
container.querySelectorAll('[data-action="resolve-report"]').forEach(btn => {
|
|
2227
|
+
btn.addEventListener('click', async () => {
|
|
2228
|
+
const action = btn.dataset.resolve
|
|
2229
|
+
try {
|
|
2230
|
+
await api('/admin/reports/resolve', {
|
|
2231
|
+
method: 'POST',
|
|
2232
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2233
|
+
body: JSON.stringify({ id: parseInt(btn.dataset.id), action }),
|
|
2234
|
+
})
|
|
2235
|
+
toast(action === 'resolve' ? 'Label applied & report resolved' : 'Report dismissed', 'success')
|
|
2236
|
+
loadReports()
|
|
2237
|
+
} catch (e) {
|
|
2238
|
+
toast(e.message, 'error')
|
|
2239
|
+
}
|
|
2240
|
+
})
|
|
2241
|
+
})
|
|
2242
|
+
}
|
|
2110
2243
|
</script>
|
|
2111
2244
|
</body>
|
|
2112
2245
|
</html>
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloudflare Container entry point for hatk.
|
|
3
|
-
*
|
|
4
|
-
* Runs as a long-lived Node.js process alongside the Worker. Handles the
|
|
5
|
-
* firehose indexer and backfill loop — the Worker delegates resync requests
|
|
6
|
-
* here via RPC (Cloudflare Container service bindings).
|
|
7
|
-
*
|
|
8
|
-
* No HTTP server — all communication is via the Container RPC interface.
|
|
9
|
-
*/
|
|
10
|
-
export interface Env {
|
|
11
|
-
/** Cloudflare D1 database binding */
|
|
12
|
-
DB: D1Database;
|
|
13
|
-
HATK_RELAY: string;
|
|
14
|
-
HATK_PLC: string;
|
|
15
|
-
HATK_OAUTH_ISSUER?: string;
|
|
16
|
-
HATK_OAUTH_SCOPES?: string;
|
|
17
|
-
HATK_ADMINS?: string;
|
|
18
|
-
HATK_COLLECTIONS?: string;
|
|
19
|
-
HATK_BACKFILL_PARALLELISM?: string;
|
|
20
|
-
HATK_BACKFILL_FETCH_TIMEOUT?: string;
|
|
21
|
-
HATK_BACKFILL_MAX_RETRIES?: string;
|
|
22
|
-
HATK_BACKFILL_FULL_NETWORK?: string;
|
|
23
|
-
HATK_BACKFILL_REPOS?: string;
|
|
24
|
-
HATK_BACKFILL_SIGNAL_COLLECTIONS?: string;
|
|
25
|
-
}
|
|
26
|
-
interface D1Database {
|
|
27
|
-
prepare(sql: string): any;
|
|
28
|
-
batch<T = unknown>(statements: any[]): Promise<any[]>;
|
|
29
|
-
exec(sql: string): Promise<any>;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Resync a single DID by triggering auto-backfill.
|
|
33
|
-
* Called by the Worker via Container service binding RPC.
|
|
34
|
-
*/
|
|
35
|
-
declare function resync(did: string): Promise<void>;
|
|
36
|
-
/**
|
|
37
|
-
* Trigger a full re-enumeration backfill of all repos.
|
|
38
|
-
* Called by the Worker via Container service binding RPC.
|
|
39
|
-
*/
|
|
40
|
-
declare function resyncAll(): Promise<void>;
|
|
41
|
-
/**
|
|
42
|
-
* Return basic status info about the Container.
|
|
43
|
-
* Called by the Worker for health checks / admin UI.
|
|
44
|
-
*/
|
|
45
|
-
declare function getStatus(): {
|
|
46
|
-
initialized: boolean;
|
|
47
|
-
collections: string[];
|
|
48
|
-
uptimeMs: number;
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Cloudflare Container entry point.
|
|
52
|
-
*
|
|
53
|
-
* Containers expose RPC methods that the Worker can call via the service binding.
|
|
54
|
-
* The Container also handles fetch requests routed from the Worker, but for hatk
|
|
55
|
-
* all Worker-to-Container communication uses the RPC methods above.
|
|
56
|
-
*/
|
|
57
|
-
declare const _default: {
|
|
58
|
-
/**
|
|
59
|
-
* Container startup — called when the Container is first instantiated.
|
|
60
|
-
* Initializes the database, starts the firehose, and begins backfill.
|
|
61
|
-
*/
|
|
62
|
-
start(env: Env): Promise<void>;
|
|
63
|
-
/**
|
|
64
|
-
* Handle fetch requests forwarded from the Worker.
|
|
65
|
-
* The Container doesn't serve HTTP — return 404 for any direct requests.
|
|
66
|
-
*/
|
|
67
|
-
fetch(request: Request, env: Env): Promise<Response>;
|
|
68
|
-
resync: typeof resync;
|
|
69
|
-
resyncAll: typeof resyncAll;
|
|
70
|
-
getStatus: typeof getStatus;
|
|
71
|
-
};
|
|
72
|
-
export default _default;
|
|
73
|
-
//# sourceMappingURL=container.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../../src/cloudflare/container.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0BH,MAAM,WAAW,GAAG;IAClB,qCAAqC;IACrC,EAAE,EAAE,UAAU,CAAA;IAGd,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAGzB,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,0BAA0B,CAAC,EAAE,MAAM,CAAA;IACnC,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,gCAAgC,CAAC,EAAE,MAAM,CAAA;CAC1C;AAGD,UAAU,UAAU;IAClB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;IACzB,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IACrD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;CAChC;AAoLD;;;GAGG;AACH,iBAAe,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhD;AAED;;;GAGG;AACH,iBAAe,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAExC;AAED;;;GAGG;AACH,iBAAS,SAAS,IAAI;IAAE,WAAW,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAMtF;AAID;;;;;;GAMG;;IAED;;;OAGG;eACc,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpC;;;OAGG;mBACkB,OAAO,OAAO,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC;;;;;AAb5D,wBA0BC"}
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloudflare Container entry point for hatk.
|
|
3
|
-
*
|
|
4
|
-
* Runs as a long-lived Node.js process alongside the Worker. Handles the
|
|
5
|
-
* firehose indexer and backfill loop — the Worker delegates resync requests
|
|
6
|
-
* here via RPC (Cloudflare Container service bindings).
|
|
7
|
-
*
|
|
8
|
-
* No HTTP server — all communication is via the Container RPC interface.
|
|
9
|
-
*/
|
|
10
|
-
import { D1Adapter } from "../database/adapters/d1.js";
|
|
11
|
-
import { initDatabase, getCursor, migrateSchema } from "../database/db.js";
|
|
12
|
-
import { storeLexicons, discoverCollections, buildSchemas } from "../database/schema.js";
|
|
13
|
-
import { discoverViews } from "../views.js";
|
|
14
|
-
import { getDialect } from "../database/dialect.js";
|
|
15
|
-
import { setSearchPort } from "../database/fts.js";
|
|
16
|
-
import { rebuildAllIndexes } from "../database/fts.js";
|
|
17
|
-
import { registerCoreHandlers } from "../server.js";
|
|
18
|
-
import { configureRelay } from "../xrpc.js";
|
|
19
|
-
import { startIndexer, triggerAutoBackfill } from "../indexer.js";
|
|
20
|
-
import { runBackfill } from "../backfill.js";
|
|
21
|
-
import { relayHttpUrl } from "../config.js";
|
|
22
|
-
import { validateLexicons } from '@bigmoves/lexicon';
|
|
23
|
-
import { log } from "../logger.js";
|
|
24
|
-
// ---------- Container state ----------
|
|
25
|
-
let initialized = false;
|
|
26
|
-
let initPromise = null;
|
|
27
|
-
let collections = [];
|
|
28
|
-
let collectionSet = new Set();
|
|
29
|
-
let backfillOpts = null;
|
|
30
|
-
let startedAt = 0;
|
|
31
|
-
/**
|
|
32
|
-
* One-time initialization. Mirrors main.ts startup minus the HTTP server.
|
|
33
|
-
*/
|
|
34
|
-
async function initialize(env) {
|
|
35
|
-
startedAt = Date.now();
|
|
36
|
-
// 1. Parse config from env vars
|
|
37
|
-
const relay = env.HATK_RELAY || 'wss://bsky.network';
|
|
38
|
-
const plc = env.HATK_PLC || 'https://plc.directory';
|
|
39
|
-
configureRelay(relay);
|
|
40
|
-
const admins = env.HATK_ADMINS ? env.HATK_ADMINS.split(',').map((s) => s.trim()) : [];
|
|
41
|
-
// 2. Load lexicons — injected at build time via virtual module
|
|
42
|
-
let lexicons;
|
|
43
|
-
try {
|
|
44
|
-
// @ts-expect-error — virtual module generated at build time
|
|
45
|
-
const lexiconModule = await import('virtual:hatk-lexicons');
|
|
46
|
-
lexicons = new Map(Object.entries(lexiconModule.default));
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
lexicons = new Map();
|
|
50
|
-
}
|
|
51
|
-
const lexiconErrors = validateLexicons([...lexicons.values()]);
|
|
52
|
-
if (lexiconErrors) {
|
|
53
|
-
for (const [nsid, errors] of Object.entries(lexiconErrors)) {
|
|
54
|
-
for (const err of errors) {
|
|
55
|
-
console.error(`[container] Invalid lexicon ${nsid}: ${err}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
throw new Error('Invalid lexicons — check build output');
|
|
59
|
-
}
|
|
60
|
-
storeLexicons(lexicons);
|
|
61
|
-
// 3. Determine collections
|
|
62
|
-
collections = env.HATK_COLLECTIONS
|
|
63
|
-
? env.HATK_COLLECTIONS.split(',').map((s) => s.trim())
|
|
64
|
-
: discoverCollections(lexicons);
|
|
65
|
-
collectionSet = new Set(collections);
|
|
66
|
-
if (collections.length === 0) {
|
|
67
|
-
log('[container] No record collections found — running in indexer-only mode');
|
|
68
|
-
}
|
|
69
|
-
log(`[container] Loaded config: ${collections.length} collections`);
|
|
70
|
-
// 4. Build schemas and init D1
|
|
71
|
-
discoverViews();
|
|
72
|
-
const engineDialect = getDialect('d1');
|
|
73
|
-
const { schemas, ddlStatements } = buildSchemas(lexicons, collections, engineDialect);
|
|
74
|
-
const adapter = new D1Adapter();
|
|
75
|
-
adapter.initWithBinding(env.DB);
|
|
76
|
-
setSearchPort(null); // D1 uses SQLite FTS natively
|
|
77
|
-
await initDatabase(adapter, ':memory:', schemas, ddlStatements);
|
|
78
|
-
// Auto-migrate schema if lexicons changed
|
|
79
|
-
const migrationChanges = await migrateSchema(schemas);
|
|
80
|
-
if (migrationChanges.length > 0) {
|
|
81
|
-
log(`[container] Applied ${migrationChanges.length} schema migration(s)`);
|
|
82
|
-
}
|
|
83
|
-
// 5. Init server directory handlers (feeds, labels, hooks, xrpc, setup)
|
|
84
|
-
// In Containers, we load these via the bundled virtual module like the Worker.
|
|
85
|
-
// The server/ directory scanning won't work in a Container since there's no filesystem
|
|
86
|
-
// layout matching the dev project. For now, register core handlers only.
|
|
87
|
-
// When build tooling (Task 7) bundles server handlers, they'll be imported here.
|
|
88
|
-
const oauthConfig = env.HATK_OAUTH_ISSUER
|
|
89
|
-
? {
|
|
90
|
-
issuer: env.HATK_OAUTH_ISSUER,
|
|
91
|
-
scopes: env.HATK_OAUTH_SCOPES ? env.HATK_OAUTH_SCOPES.split(',').map((s) => s.trim()) : ['read', 'write'],
|
|
92
|
-
clients: [],
|
|
93
|
-
}
|
|
94
|
-
: null;
|
|
95
|
-
registerCoreHandlers(collections, oauthConfig);
|
|
96
|
-
// 6. Parse backfill config
|
|
97
|
-
const backfillConfig = {
|
|
98
|
-
fullNetwork: env.HATK_BACKFILL_FULL_NETWORK === 'true',
|
|
99
|
-
parallelism: env.HATK_BACKFILL_PARALLELISM ? parseInt(env.HATK_BACKFILL_PARALLELISM, 10) : 10,
|
|
100
|
-
fetchTimeout: env.HATK_BACKFILL_FETCH_TIMEOUT ? parseInt(env.HATK_BACKFILL_FETCH_TIMEOUT, 10) : 30,
|
|
101
|
-
maxRetries: env.HATK_BACKFILL_MAX_RETRIES ? parseInt(env.HATK_BACKFILL_MAX_RETRIES, 10) : 5,
|
|
102
|
-
repos: env.HATK_BACKFILL_REPOS ? env.HATK_BACKFILL_REPOS.split(',').map((s) => s.trim()) : undefined,
|
|
103
|
-
signalCollections: env.HATK_BACKFILL_SIGNAL_COLLECTIONS
|
|
104
|
-
? env.HATK_BACKFILL_SIGNAL_COLLECTIONS.split(',').map((s) => s.trim())
|
|
105
|
-
: undefined,
|
|
106
|
-
};
|
|
107
|
-
backfillOpts = {
|
|
108
|
-
pdsUrl: relayHttpUrl(relay),
|
|
109
|
-
plcUrl: plc,
|
|
110
|
-
collections: collectionSet,
|
|
111
|
-
config: backfillConfig,
|
|
112
|
-
};
|
|
113
|
-
// 7. Start firehose indexer
|
|
114
|
-
const cursor = await getCursor('relay');
|
|
115
|
-
startIndexer({
|
|
116
|
-
relayUrl: relay,
|
|
117
|
-
collections: collectionSet,
|
|
118
|
-
signalCollections: backfillConfig.signalCollections ? new Set(backfillConfig.signalCollections) : undefined,
|
|
119
|
-
pinnedRepos: backfillConfig.repos ? new Set(backfillConfig.repos) : undefined,
|
|
120
|
-
cursor,
|
|
121
|
-
fetchTimeout: backfillConfig.fetchTimeout,
|
|
122
|
-
maxRetries: backfillConfig.maxRetries,
|
|
123
|
-
parallelism: backfillConfig.parallelism,
|
|
124
|
-
});
|
|
125
|
-
log('[container] Firehose indexer started');
|
|
126
|
-
// 8. Run backfill in background
|
|
127
|
-
runBackfillAndRestart();
|
|
128
|
-
initialized = true;
|
|
129
|
-
log('[container] Initialization complete');
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Run backfill, rebuild FTS indexes, and restart the process if records
|
|
133
|
-
* were imported (to reclaim memory from CAR parsing). Mirrors main.ts behavior.
|
|
134
|
-
*/
|
|
135
|
-
function runBackfillAndRestart() {
|
|
136
|
-
if (!backfillOpts)
|
|
137
|
-
return;
|
|
138
|
-
runBackfill(backfillOpts)
|
|
139
|
-
.then(async (recordCount) => {
|
|
140
|
-
log('[container] Backfill complete, building FTS indexes...');
|
|
141
|
-
await rebuildAllIndexes(collections);
|
|
142
|
-
log('[container] FTS indexes ready');
|
|
143
|
-
return recordCount;
|
|
144
|
-
})
|
|
145
|
-
.then((recordCount) => {
|
|
146
|
-
if (recordCount > 0) {
|
|
147
|
-
log('[container] Restarting to reclaim memory...');
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
.catch((err) => {
|
|
152
|
-
console.error('[container] Backfill error:', err.message);
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Ensure initialization has completed. Uses a shared promise so concurrent
|
|
157
|
-
* RPC calls don't trigger multiple inits.
|
|
158
|
-
*/
|
|
159
|
-
function ensureInit(env) {
|
|
160
|
-
if (initialized)
|
|
161
|
-
return Promise.resolve();
|
|
162
|
-
if (!initPromise) {
|
|
163
|
-
initPromise = initialize(env).catch((err) => {
|
|
164
|
-
initPromise = null;
|
|
165
|
-
throw err;
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return initPromise;
|
|
169
|
-
}
|
|
170
|
-
// ---------- RPC methods ----------
|
|
171
|
-
/**
|
|
172
|
-
* Resync a single DID by triggering auto-backfill.
|
|
173
|
-
* Called by the Worker via Container service binding RPC.
|
|
174
|
-
*/
|
|
175
|
-
async function resync(did) {
|
|
176
|
-
await triggerAutoBackfill(did);
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Trigger a full re-enumeration backfill of all repos.
|
|
180
|
-
* Called by the Worker via Container service binding RPC.
|
|
181
|
-
*/
|
|
182
|
-
async function resyncAll() {
|
|
183
|
-
runBackfillAndRestart();
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Return basic status info about the Container.
|
|
187
|
-
* Called by the Worker for health checks / admin UI.
|
|
188
|
-
*/
|
|
189
|
-
function getStatus() {
|
|
190
|
-
return {
|
|
191
|
-
initialized,
|
|
192
|
-
collections,
|
|
193
|
-
uptimeMs: startedAt > 0 ? Date.now() - startedAt : 0,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
// ---------- Container export ----------
|
|
197
|
-
/**
|
|
198
|
-
* Cloudflare Container entry point.
|
|
199
|
-
*
|
|
200
|
-
* Containers expose RPC methods that the Worker can call via the service binding.
|
|
201
|
-
* The Container also handles fetch requests routed from the Worker, but for hatk
|
|
202
|
-
* all Worker-to-Container communication uses the RPC methods above.
|
|
203
|
-
*/
|
|
204
|
-
export default {
|
|
205
|
-
/**
|
|
206
|
-
* Container startup — called when the Container is first instantiated.
|
|
207
|
-
* Initializes the database, starts the firehose, and begins backfill.
|
|
208
|
-
*/
|
|
209
|
-
async start(env) {
|
|
210
|
-
await ensureInit(env);
|
|
211
|
-
},
|
|
212
|
-
/**
|
|
213
|
-
* Handle fetch requests forwarded from the Worker.
|
|
214
|
-
* The Container doesn't serve HTTP — return 404 for any direct requests.
|
|
215
|
-
*/
|
|
216
|
-
async fetch(request, env) {
|
|
217
|
-
await ensureInit(env);
|
|
218
|
-
return new Response(JSON.stringify({ error: 'Container does not serve HTTP requests' }), {
|
|
219
|
-
status: 404,
|
|
220
|
-
headers: { 'Content-Type': 'application/json' },
|
|
221
|
-
});
|
|
222
|
-
},
|
|
223
|
-
// RPC methods exposed to the Worker via service binding
|
|
224
|
-
resync,
|
|
225
|
-
resyncAll,
|
|
226
|
-
getStatus,
|
|
227
|
-
};
|
|
228
|
-
// Graceful shutdown
|
|
229
|
-
process.on('SIGTERM', () => {
|
|
230
|
-
log('[container] Received SIGTERM, shutting down...');
|
|
231
|
-
process.exit(0);
|
|
232
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SvelteKit handle hook for Cloudflare Workers.
|
|
3
|
-
*
|
|
4
|
-
* Use with @sveltejs/adapter-cloudflare. Lazily initializes hatk on the first
|
|
5
|
-
* request using the D1 binding from platform.env, then intercepts hatk API
|
|
6
|
-
* routes before SvelteKit processes them.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* // app/hooks.server.ts
|
|
11
|
-
* import { createHandle } from '@hatk/hatk/cloudflare/hooks'
|
|
12
|
-
* export const handle = createHandle()
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
type MaybePromise<T> = T | Promise<T>;
|
|
16
|
-
/** Minimal SvelteKit Handle type to avoid depending on @sveltejs/kit. */
|
|
17
|
-
type Handle = (input: {
|
|
18
|
-
event: {
|
|
19
|
-
request: Request;
|
|
20
|
-
url: URL;
|
|
21
|
-
platform?: {
|
|
22
|
-
env?: Record<string, unknown>;
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
resolve: (event: any) => MaybePromise<Response>;
|
|
26
|
-
}) => MaybePromise<Response>;
|
|
27
|
-
/**
|
|
28
|
-
* Create a SvelteKit `handle` function that initializes hatk with D1
|
|
29
|
-
* and intercepts API routes.
|
|
30
|
-
*/
|
|
31
|
-
export declare function createHandle(): Handle;
|
|
32
|
-
export {};
|
|
33
|
-
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/cloudflare/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;AAErC,yEAAyE;AACzE,KAAK,MAAM,GAAG,CAAC,KAAK,EAAE;IACpB,KAAK,EAAE;QACL,OAAO,EAAE,OAAO,CAAA;QAChB,GAAG,EAAE,GAAG,CAAA;QACR,QAAQ,CAAC,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAA;KAC7C,CAAA;IACD,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,YAAY,CAAC,QAAQ,CAAC,CAAA;CAChD,KAAK,YAAY,CAAC,QAAQ,CAAC,CAAA;AAE5B;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAsBrC"}
|
package/dist/cloudflare/hooks.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SvelteKit handle hook for Cloudflare Workers.
|
|
3
|
-
*
|
|
4
|
-
* Use with @sveltejs/adapter-cloudflare. Lazily initializes hatk on the first
|
|
5
|
-
* request using the D1 binding from platform.env, then intercepts hatk API
|
|
6
|
-
* routes before SvelteKit processes them.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* // app/hooks.server.ts
|
|
11
|
-
* import { createHandle } from '@hatk/hatk/cloudflare/hooks'
|
|
12
|
-
* export const handle = createHandle()
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
import { ensureInit, getHandler } from "./init.js";
|
|
16
|
-
import { isHatkRoute } from "../adapter.js";
|
|
17
|
-
/**
|
|
18
|
-
* Create a SvelteKit `handle` function that initializes hatk with D1
|
|
19
|
-
* and intercepts API routes.
|
|
20
|
-
*/
|
|
21
|
-
export function createHandle() {
|
|
22
|
-
return async ({ event, resolve }) => {
|
|
23
|
-
const env = event.platform?.env;
|
|
24
|
-
if (!env || !env.DB) {
|
|
25
|
-
// Not running on Cloudflare (e.g. dev mode) — pass through
|
|
26
|
-
return resolve(event);
|
|
27
|
-
}
|
|
28
|
-
// Lazy init hatk with the D1 binding
|
|
29
|
-
await ensureInit(env);
|
|
30
|
-
// hatk API routes
|
|
31
|
-
if (isHatkRoute(event.url.pathname)) {
|
|
32
|
-
const handler = getHandler();
|
|
33
|
-
if (handler) {
|
|
34
|
-
return handler(event.request);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
// Everything else → SvelteKit
|
|
38
|
-
return resolve(event);
|
|
39
|
-
};
|
|
40
|
-
}
|