@preventive/triage 1.0.0-alpha.14 → 1.0.0-alpha.16
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/common/scan-server.ts +13 -0
- package/common/server-info.ts +1 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/out/brotli-fallback.js +3 -3
- package/out/client-managed.js +83 -1
- package/out/client-sync.js +14 -14
- package/out/graph.js +30 -5
- package/out/index.html +49 -8
- package/out/prism.js +2 -2
- package/out/provider-icons/claude.svg +1 -0
- package/out/provider-icons/deepseek.svg +1 -0
- package/out/provider-icons/google.svg +1 -0
- package/out/provider-icons/grok.svg +1 -0
- package/out/provider-icons/moonshot.svg +1 -0
- package/out/provider-icons/nvidia.svg +1 -0
- package/out/provider-icons/openai.svg +1 -0
- package/out/provider-icons/openrouter.svg +1 -0
- package/out/provider-icons/qwen.svg +1 -0
- package/out/provider-icons/zai.svg +1 -0
- package/out/stasis.svg +45 -0
- package/out/terminal.js +266 -45
- package/out/view.css +1 -1
- package/out/view.js +157 -119
- package/package.json +31 -5
- package/report/index.js +253 -0
- package/report/src/finding-id.js +80 -0
- package/report/src/finding.js +300 -0
- package/report/src/labels.js +33 -0
- package/report/src/md-structure.js +471 -0
- package/report/src/md-text.js +167 -0
- package/report/src/meta.js +76 -0
- package/report/src/parse-codex.js +147 -0
- package/report/src/parse-deepsec.js +197 -0
- package/report/src/parse-deepview-fields.js +375 -0
- package/report/src/parse-deepview-md.js +185 -0
- package/report/src/parse-md-id.js +137 -0
- package/report/src/parse-md.js +322 -0
- package/report/src/parse-piolium-id.js +79 -0
- package/report/src/parse-piolium-rows.js +131 -0
- package/report/src/parse-piolium-tokens.js +175 -0
- package/report/src/parse-piolium.js +400 -0
- package/report/src/utf8.js +21 -0
- package/report/src/write-md-finding.js +273 -0
- package/report/src/write-md.js +291 -0
- package/server-common/scan-config.ts +19 -0
- package/server-e2e/bus-receiver.ts +1 -0
- package/server-e2e/config.ts +6 -0
- package/server-e2e/http.ts +1 -1
- package/server-e2e/hub.ts +37 -6
- package/server-e2e/index.ts +4 -4
- package/server-e2e/objstore/handlers.ts +6 -5
- package/server-e2e/objstore/init.ts +1 -1
- package/server-e2e/objstore/rest-mint.ts +2 -2
- package/server-e2e/objstore/rest.ts +2 -2
- package/server-e2e/pubsub.ts +8 -5
- package/server-e2e/static.ts +19 -11
- package/server-e2e/sync-handlers.ts +54 -28
- package/out/client-admin.js +0 -28
package/server-e2e/hub.ts
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
// testable and reasoned about on its own.
|
|
8
8
|
|
|
9
9
|
import type { WebSocket } from 'ws'
|
|
10
|
+
import { Buffer } from 'node:buffer'
|
|
10
11
|
import type { PeerRegistry } from './peer.ts'
|
|
11
12
|
|
|
12
13
|
export type Hub = {
|
|
14
|
+
// Buffer broadcasts during async subscription snapshots. The returned
|
|
15
|
+
// release flushes them after the snapshot/chain replies, preserving order.
|
|
16
|
+
pauseBroadcasts(socket: WebSocket): () => void
|
|
13
17
|
subscribe(socket: WebSocket, tag: string): void
|
|
14
18
|
unsubscribeAll(socket: WebSocket): void
|
|
15
19
|
send(socket: WebSocket, msg: object): void
|
|
@@ -38,18 +42,34 @@ export function createHub(deps: { peers: PeerRegistry; maxBufferedBytes: number;
|
|
|
38
42
|
// workspaceTag → Set<WebSocket>. The per-socket reverse index lives on
|
|
39
43
|
// `Peer.tags` (see ./peer.ts) and is read by `unsubscribeAll` on close.
|
|
40
44
|
const subscribers = new Map<string, Set<WebSocket>>()
|
|
45
|
+
const paused = new WeakMap<WebSocket, { depth: number; payloads: string[]; bytes: number }>()
|
|
46
|
+
|
|
47
|
+
function pauseBroadcasts(socket: WebSocket): () => void {
|
|
48
|
+
const pending = paused.get(socket) ?? { depth: 0, payloads: [], bytes: 0 }
|
|
49
|
+
pending.depth++
|
|
50
|
+
paused.set(socket, pending)
|
|
51
|
+
let released = false
|
|
52
|
+
return () => {
|
|
53
|
+
if (released) return
|
|
54
|
+
released = true
|
|
55
|
+
if (--pending.depth > 0 || paused.get(socket) !== pending) return
|
|
56
|
+
paused.delete(socket)
|
|
57
|
+
for (const payload of pending.payloads) sendRaw(socket, payload)
|
|
58
|
+
pending.payloads.length = 0
|
|
59
|
+
}
|
|
60
|
+
}
|
|
41
61
|
|
|
42
62
|
function subscribe(socket: WebSocket, tag: string): void {
|
|
43
63
|
let set = subscribers.get(tag)
|
|
44
|
-
if (!set) {
|
|
45
|
-
set = new Set()
|
|
46
|
-
subscribers.set(tag, set)
|
|
47
|
-
}
|
|
64
|
+
if (!set) { set = new Set(); subscribers.set(tag, set) }
|
|
48
65
|
set.add(socket)
|
|
49
66
|
peers.get(socket)?.tags.add(tag)
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
function unsubscribeAll(socket: WebSocket): void {
|
|
70
|
+
const pending = paused.get(socket)
|
|
71
|
+
if (pending) pending.payloads.length = 0
|
|
72
|
+
paused.delete(socket)
|
|
53
73
|
const tags = peers.get(socket)?.tags
|
|
54
74
|
if (!tags) return
|
|
55
75
|
for (const tag of tags) {
|
|
@@ -102,6 +122,7 @@ export function createHub(deps: { peers: PeerRegistry; maxBufferedBytes: number;
|
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
function fanOut(set: Set<WebSocket>, payload: string, except: WebSocket | null): void {
|
|
125
|
+
const payloadBytes = Buffer.byteLength(payload)
|
|
105
126
|
// Snapshot before iterating — a socket transitioning to CLOSED
|
|
106
127
|
// mid-broadcast triggers `unsubscribeAll` from the 'close' handler,
|
|
107
128
|
// mutating `set` while we walk it. The snapshot also keeps a future
|
|
@@ -109,9 +130,19 @@ export function createHub(deps: { peers: PeerRegistry; maxBufferedBytes: number;
|
|
|
109
130
|
// subscribers. Audit M4 round-3.
|
|
110
131
|
for (const s of [...set]) {
|
|
111
132
|
if (s === except) continue
|
|
112
|
-
|
|
133
|
+
const pending = paused.get(s)
|
|
134
|
+
if (!pending) { sendRaw(s, payload); continue }
|
|
135
|
+
pending.bytes += payloadBytes
|
|
136
|
+
if (pending.bytes > maxBufferedBytes) {
|
|
137
|
+
// Snapshot waits must obey the same memory bound as socket writes.
|
|
138
|
+
pending.payloads.length = 0
|
|
139
|
+
paused.delete(s)
|
|
140
|
+
try { s.terminate() } catch {}
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
pending.payloads.push(payload)
|
|
113
144
|
}
|
|
114
145
|
}
|
|
115
146
|
|
|
116
|
-
return { subscribe, unsubscribeAll, send, sendRaw, broadcast, broadcastLocalRaw }
|
|
147
|
+
return { pauseBroadcasts, subscribe, unsubscribeAll, send, sendRaw, broadcast, broadcastLocalRaw }
|
|
117
148
|
}
|
package/server-e2e/index.ts
CHANGED
|
@@ -289,12 +289,13 @@ const publishRevision = (tag: string, revisionId: string): void => {
|
|
|
289
289
|
const publishObjPut = (tag: string, resourceTag: string): void => {
|
|
290
290
|
pubsub.publish({ kind: 'objput', tag, res: resourceTag })
|
|
291
291
|
}
|
|
292
|
-
const publishObjDeleted = (tag: string, resourceTag: string, version: number): void => {
|
|
293
|
-
pubsub.publish({ kind: 'objdel', tag, res: resourceTag, ver: version })
|
|
292
|
+
const publishObjDeleted = (tag: string, resourceTag: string, version: number, incarnation: string): void => {
|
|
293
|
+
pubsub.publish({ kind: 'objdel', tag, res: resourceTag, ver: version, incarnation })
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
const { handleSave, handleSaveRest, handleSubscribe, sendSaveError } = createSyncHandlers({
|
|
297
297
|
handle, send, broadcast, publishRevision, subscribe, getNonce,
|
|
298
|
+
pauseBroadcasts: hub.pauseBroadcasts,
|
|
298
299
|
requiresAuth, passwordConfigured, sendUnauthorized, workspaceExists,
|
|
299
300
|
// Folds the objstore inventory into the `workspace-subscribed` ack.
|
|
300
301
|
// The objstore store keeps its own richer `Handle`, so we wire the
|
|
@@ -345,7 +346,7 @@ const { track, isShuttingDown, install: installLifecycle } = createLifecycle()
|
|
|
345
346
|
// The sync protocol this build advertises — emitted as a `server-info` frame
|
|
346
347
|
// right after the challenge on every connection. This is the e2e boot, so it
|
|
347
348
|
// always advertises e2e.
|
|
348
|
-
const SERVER_INFO: ServerInfo = { mode: 'e2e', managed: null }
|
|
349
|
+
const SERVER_INFO: ServerInfo = { mode: 'e2e', managed: null, ...(config.deepviewScanServer ? { deepviewScanServer: config.deepviewScanServer } : {}) }
|
|
349
350
|
|
|
350
351
|
// Shared per-connection dispatch surface. Both the WS plane
|
|
351
352
|
// (installWsServer below) and the SSE+POST fallback (installSseServer
|
|
@@ -483,4 +484,3 @@ export { httpServer, wss }
|
|
|
483
484
|
if (import.meta.main) {
|
|
484
485
|
start()
|
|
485
486
|
}
|
|
486
|
-
|
|
@@ -48,9 +48,9 @@ export type ObjstoreDeps = {
|
|
|
48
48
|
// Cross-instance pub/sub for objstore-deleted. Fired alongside the
|
|
49
49
|
// local `broadcast` after a successful delete so peers on OTHER
|
|
50
50
|
// server instances see the version drop in real time. Carries the
|
|
51
|
-
// full (tag, resourceTag, version) tuple inline — the workspace_object
|
|
51
|
+
// full (tag, resourceTag, version, incarnation) tuple inline — the workspace_object
|
|
52
52
|
// row is gone post-delete, so the bus payload IS the wire data.
|
|
53
|
-
publishObjDeleted: (tag: string, resourceTag: string, version: number) => void
|
|
53
|
+
publishObjDeleted: (tag: string, resourceTag: string, version: number, incarnation: string) => void
|
|
54
54
|
getNonce: (socket: WebSocket) => string | undefined
|
|
55
55
|
debug: boolean
|
|
56
56
|
// Auth gate for the FIRST put-begin against a never-before-seen
|
|
@@ -191,13 +191,14 @@ async function handleDelete(deps: ObjstoreDeps, socket: WebSocket, msg: Objstore
|
|
|
191
191
|
// observes its own PUTs as echo events. The same symmetry on
|
|
192
192
|
// `onDeleted` lets `session.onDeleted` fire for the session's
|
|
193
193
|
// own deletes — pinned by `tests/objstore-client-races.test.js`.
|
|
194
|
-
|
|
194
|
+
// A successful nonempty delete matched this authenticated incarnation.
|
|
195
|
+
deps.broadcast(tag, { type: 'objstore-deleted', workspaceTag: tag, resourceTag, version: result.deletedVersion, incarnation: prevIncarnation! }, null)
|
|
195
196
|
// Cross-instance fan-out (Neon mode). The workspace_object row is
|
|
196
197
|
// gone post-delete so the bus payload carries (tag, resourceTag,
|
|
197
|
-
// version) inline; the receiver builds its `objstore-deleted`
|
|
198
|
+
// version, incarnation) inline; the receiver builds its `objstore-deleted`
|
|
198
199
|
// broadcast directly from the bus envelope. SQLite mode publishes
|
|
199
200
|
// to a no-op.
|
|
200
|
-
deps.publishObjDeleted(tag, resourceTag, result.deletedVersion)
|
|
201
|
+
deps.publishObjDeleted(tag, resourceTag, result.deletedVersion, prevIncarnation!)
|
|
201
202
|
if (deps.debug) console.log(`objstore delete → ${debugTag(tag)}/${resourceTag.slice(0, 8)}…`)
|
|
202
203
|
}
|
|
203
204
|
|
|
@@ -30,7 +30,7 @@ export type ObjstoreInitDeps = {
|
|
|
30
30
|
// mode passes Postgres LISTEN/NOTIFY-backed implementations. See
|
|
31
31
|
// server-e2e/pubsub.ts for the bus design.
|
|
32
32
|
publishObjPut: (tag: string, resourceTag: string) => void
|
|
33
|
-
publishObjDeleted: (tag: string, resourceTag: string, version: number) => void
|
|
33
|
+
publishObjDeleted: (tag: string, resourceTag: string, version: number, incarnation: string) => void
|
|
34
34
|
getNonce: (socket: WebSocket) => string | undefined
|
|
35
35
|
debug: boolean
|
|
36
36
|
// Auth gate for the FIRST objstore-put-begin against a workspace
|
|
@@ -219,6 +219,6 @@ async function handleRestDelete(
|
|
|
219
219
|
// fans out to subscribers (including the originator, matching the WS path's
|
|
220
220
|
// `except: null`) so peers' `onDeleted` fire.
|
|
221
221
|
if (result.deletedVersion === 0) return
|
|
222
|
-
deps.broadcast(route.tag, { type: 'objstore-deleted', workspaceTag: route.tag, resourceTag: route.resourceTag, version: result.deletedVersion }, null)
|
|
223
|
-
deps.publishObjDeleted(route.tag, route.resourceTag, result.deletedVersion)
|
|
222
|
+
deps.broadcast(route.tag, { type: 'objstore-deleted', workspaceTag: route.tag, resourceTag: route.resourceTag, version: result.deletedVersion, incarnation: prevIncarnation! }, null)
|
|
223
|
+
deps.publishObjDeleted(route.tag, route.resourceTag, result.deletedVersion, prevIncarnation!)
|
|
224
224
|
}
|
|
@@ -71,8 +71,8 @@ export type ObjstoreRestDeps = {
|
|
|
71
71
|
// Cross-instance pub/sub for objstore-deleted — fired alongside the local
|
|
72
72
|
// `broadcast` after a successful REST delete mint, so peers on OTHER
|
|
73
73
|
// instances drop the resource in real time. Carries (tag, resourceTag,
|
|
74
|
-
// version) inline (the row is gone post-delete). SQLite mode passes a no-op.
|
|
75
|
-
publishObjDeleted: (tag: string, resourceTag: string, version: number) => void
|
|
74
|
+
// version, incarnation) inline (the row is gone post-delete). SQLite mode passes a no-op.
|
|
75
|
+
publishObjDeleted: (tag: string, resourceTag: string, version: number, incarnation: string) => void
|
|
76
76
|
// New-workspace operator gate for the REST put-begin mint — the
|
|
77
77
|
// connection-independent analog of the WS path's `authGate`. Returns
|
|
78
78
|
// `true` to DENY (a password is configured AND the workspace is
|
package/server-e2e/pubsub.ts
CHANGED
|
@@ -57,13 +57,13 @@ function newSenderId(): string {
|
|
|
57
57
|
// - 'objput': objstore-put broadcast. `res` is the resource tag; the
|
|
58
58
|
// receiver SELECTs the live row by (tag, res) for the rest of the
|
|
59
59
|
// metadata fields.
|
|
60
|
-
// - 'objdel': objstore-deleted broadcast. `ver`
|
|
61
|
-
//
|
|
62
|
-
//
|
|
60
|
+
// - 'objdel': objstore-deleted broadcast. `ver` and `incarnation` identify
|
|
61
|
+
// the deleted row, which is gone from workspace_object after commit.
|
|
62
|
+
// Incarnation is optional only for messages from older relay instances.
|
|
63
63
|
export type BusMessage =
|
|
64
64
|
| { kind: 'rev'; tag: string; id: string }
|
|
65
65
|
| { kind: 'objput'; tag: string; res: string }
|
|
66
|
-
| { kind: 'objdel'; tag: string; res: string; ver: number }
|
|
66
|
+
| { kind: 'objdel'; tag: string; res: string; ver: number; incarnation?: string }
|
|
67
67
|
|
|
68
68
|
// Receiver wired up by the hub layer (see server-e2e/index.ts). Each handler
|
|
69
69
|
// runs once per remote message; failures are logged but don't crash the
|
|
@@ -388,7 +388,10 @@ function parseBusMessage(raw: Record<string, unknown>): BusMessage | null {
|
|
|
388
388
|
const ver = raw['ver']
|
|
389
389
|
if (typeof res !== 'string') return null
|
|
390
390
|
if (typeof ver !== 'number' || !Number.isSafeInteger(ver)) return null
|
|
391
|
-
|
|
391
|
+
const incarnation = raw['incarnation']
|
|
392
|
+
if (incarnation === undefined) return { kind: 'objdel', tag, res, ver }
|
|
393
|
+
if (typeof incarnation !== 'string' || incarnation.length === 0) return null
|
|
394
|
+
return { kind: 'objdel', tag, res, ver, incarnation }
|
|
392
395
|
}
|
|
393
396
|
return null
|
|
394
397
|
}
|
package/server-e2e/static.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// `join(staticDir, dirent.name)` from `readdirSync` at boot — the
|
|
7
7
|
// request URL is only used as a Map key, never joined with the
|
|
8
8
|
// filesystem — so the handler has no path-traversal surface at all:
|
|
9
|
-
// `..`, percent-encoded slashes
|
|
10
|
-
//
|
|
9
|
+
// `..`, percent-encoded slashes and absolute-form URIs all just
|
|
10
|
+
// produce a key that isn't in the map and 404. Provider SVGs are
|
|
11
|
+
// the one explicitly allowed asset subdirectory.
|
|
11
12
|
//
|
|
12
13
|
// Compression: every entry whose extension is in COMPRESSIBLE gets
|
|
13
14
|
// pre-computed brotli + gzip at boot. The handler picks brotli over
|
|
@@ -38,6 +39,7 @@ import { readFileSync, readdirSync } from 'node:fs'
|
|
|
38
39
|
import { createHash } from 'node:crypto'
|
|
39
40
|
import { brotliCompressSync, gzipSync, constants as zlibConstants } from 'node:zlib'
|
|
40
41
|
import { extname, join } from 'node:path'
|
|
42
|
+
import { scanServerHtml } from '../server-common/scan-config.ts'
|
|
41
43
|
|
|
42
44
|
// MIME types for extensions the production UI bundle currently
|
|
43
45
|
// emits. Anything else falls through to `application/octet-stream`
|
|
@@ -102,8 +104,8 @@ export type StaticHandler = (req: HttpRequest, res: ServerResponse) => boolean
|
|
|
102
104
|
// caller falls through to its next route. Missing `staticDir`
|
|
103
105
|
// (pre-build case) logs a warning and returns a handler that always
|
|
104
106
|
// answers false; the API/WS planes are unaffected.
|
|
105
|
-
export function loadStatic(staticDir: string): StaticHandler {
|
|
106
|
-
const files = readStaticFiles(staticDir)
|
|
107
|
+
export function loadStatic(staticDir: string, deepviewScanServer: string | null = null): StaticHandler {
|
|
108
|
+
const files = readStaticFiles(staticDir, deepviewScanServer)
|
|
107
109
|
return function handleStatic(req: HttpRequest, res: ServerResponse): boolean {
|
|
108
110
|
if (req.method !== 'GET' && req.method !== 'HEAD') return false
|
|
109
111
|
if (typeof req.url !== 'string') return false
|
|
@@ -151,7 +153,7 @@ export function loadStatic(staticDir: string): StaticHandler {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
function readStaticFiles(staticDir: string): ReadonlyMap<string, StaticEntry> {
|
|
156
|
+
function readStaticFiles(staticDir: string, deepviewScanServer: string | null): ReadonlyMap<string, StaticEntry> {
|
|
155
157
|
const files = new Map<string, StaticEntry>()
|
|
156
158
|
let entries
|
|
157
159
|
try { entries = readdirSync(staticDir, { withFileTypes: true }) } catch (err) {
|
|
@@ -165,18 +167,24 @@ function readStaticFiles(staticDir: string): ReadonlyMap<string, StaticEntry> {
|
|
|
165
167
|
throw err
|
|
166
168
|
}
|
|
167
169
|
for (const entry of entries) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
if (entry.isDirectory() && entry.name === 'provider-icons') {
|
|
171
|
+
for (const icon of readdirSync(join(staticDir, entry.name), { withFileTypes: true })) {
|
|
172
|
+
if (!icon.isFile() || extname(icon.name) !== '.svg') continue
|
|
173
|
+
const name = `${entry.name}/${icon.name}`
|
|
174
|
+
files.set(name, buildEntry(staticDir, name))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Exclude other subdirectories, symlinks and non-file entries.
|
|
171
178
|
if (!entry.isFile()) continue
|
|
172
|
-
files.set(entry.name, buildEntry(staticDir, entry.name))
|
|
179
|
+
files.set(entry.name, buildEntry(staticDir, entry.name, deepviewScanServer))
|
|
173
180
|
}
|
|
174
181
|
return files
|
|
175
182
|
}
|
|
176
183
|
|
|
177
|
-
function buildEntry(staticDir: string, name: string): StaticEntry {
|
|
184
|
+
function buildEntry(staticDir: string, name: string, deepviewScanServer: string | null = null): StaticEntry {
|
|
178
185
|
const ext = extname(name)
|
|
179
|
-
const
|
|
186
|
+
const source = readFileSync(join(staticDir, name))
|
|
187
|
+
const raw = name === 'index.html' ? Buffer.from(scanServerHtml(source.toString('utf8'), deepviewScanServer)) : source
|
|
180
188
|
const type = CONTENT_TYPE[ext] ?? 'application/octet-stream'
|
|
181
189
|
// HTML: lift `<link rel="(module)preload" …>` into a Link header and
|
|
182
190
|
// drop the tags from the served body. ETag + compression run against
|
|
@@ -26,6 +26,8 @@ type WireRevision = {
|
|
|
26
26
|
signature: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
type SubscriptionLookup = { resume: () => void }
|
|
30
|
+
|
|
29
31
|
// Structured result of the shared save pipeline (`commitSave`), rendered
|
|
30
32
|
// per transport: WS → protocol frames; REST → JSON + HTTP status.
|
|
31
33
|
type SaveOutcome =
|
|
@@ -79,6 +81,7 @@ export type SyncHandlersDeps = {
|
|
|
79
81
|
// payload budget. Optional: a SQLite deployment passes a no-op.
|
|
80
82
|
publishRevision: (tag: string, revisionId: string) => void
|
|
81
83
|
subscribe: (socket: WebSocket, tag: string) => void
|
|
84
|
+
pauseBroadcasts: (socket: WebSocket) => () => void
|
|
82
85
|
getNonce: (socket: WebSocket) => string | undefined
|
|
83
86
|
requiresAuth: (socket: WebSocket) => boolean
|
|
84
87
|
// Whether an operator password is configured. The REST save plane's
|
|
@@ -112,6 +115,9 @@ export type SyncHandlers = {
|
|
|
112
115
|
|
|
113
116
|
export function createSyncHandlers(deps: SyncHandlersDeps): SyncHandlers {
|
|
114
117
|
const { handle, send, broadcast, publishRevision, subscribe, getNonce, requiresAuth, passwordConfigured, sendUnauthorized, workspaceExists, objstoreResources, debug } = deps
|
|
118
|
+
// Only the newest lookup for a socket/tag may publish a snapshot.
|
|
119
|
+
// Versions cannot order different object incarnations (or deletions).
|
|
120
|
+
const subscriptionLookups = new WeakMap<WebSocket, Map<string, SubscriptionLookup>>()
|
|
115
121
|
|
|
116
122
|
// Typed wrapper for the three `workspace-save-error` emit sites
|
|
117
123
|
// (too-large at handleSave, stale-base after the catch-up, busy at
|
|
@@ -348,34 +354,54 @@ export function createSyncHandlers(deps: SyncHandlersDeps): SyncHandlers {
|
|
|
348
354
|
return
|
|
349
355
|
}
|
|
350
356
|
const tag = msg.workspaceTag
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
357
|
+
const lookups = subscriptionLookups.get(socket) ?? new Map<string, SubscriptionLookup>()
|
|
358
|
+
subscriptionLookups.set(socket, lookups)
|
|
359
|
+
const previous = lookups.get(tag)
|
|
360
|
+
const resume = deps.pauseBroadcasts(socket)
|
|
361
|
+
const lookup = { resume }
|
|
362
|
+
lookups.set(tag, lookup)
|
|
363
|
+
// Acquire the replacement's pause before releasing its predecessor so
|
|
364
|
+
// buffered broadcasts cannot overtake the replacement snapshot. A hung
|
|
365
|
+
// obsolete read must not keep the whole socket paused. The hub's release
|
|
366
|
+
// is idempotent, so the old finally can still call it when the read ends.
|
|
367
|
+
previous?.resume()
|
|
368
|
+
try {
|
|
369
|
+
subscribe(socket, tag)
|
|
370
|
+
// Explicit ack — distinguishes "the server processed my subscribe
|
|
371
|
+
// and registered me as a peer" from "the WebSocket is open". A
|
|
372
|
+
// client that sent a malformed / bad-sig subscribe never gets this;
|
|
373
|
+
// a client that did gets one before the chain arrives. Lets the UI
|
|
374
|
+
// surface a `connecting → online` transition based on real handshake
|
|
375
|
+
// completion, not just socket state.
|
|
376
|
+
//
|
|
377
|
+
// The ack also carries the objstore inventory snapshot: the same
|
|
378
|
+
// subscribe that registers this socket for objstore-put / -deleted
|
|
379
|
+
// broadcasts seeds the client's initial inventory in one handshake.
|
|
380
|
+
// The client keeps it live thereafter from those broadcasts.
|
|
381
|
+
// Returns [] for a triage-only workspace. A lookup failure is NOT an
|
|
382
|
+
// empty inventory: clients interpret absent resources as deletions.
|
|
383
|
+
// Reconnect to retry the complete handshake on either transport.
|
|
384
|
+
const resources = await objstoreResources(tag)
|
|
385
|
+
if (socket.readyState !== socket.OPEN || lookups.get(tag) !== lookup) return
|
|
386
|
+
// `from` is the last revision id the client claims to have applied —
|
|
387
|
+
// now a base64url string, not an integer. We send only revisions
|
|
388
|
+
// after that. Client lying about `from` just means they get a
|
|
389
|
+
// smaller catch-up — their subsequent saves will reveal stale state
|
|
390
|
+
// on the usual base-mismatch path. Null / missing → full chain.
|
|
391
|
+
const fromId = typeof msg.from === 'string' ? msg.from : null
|
|
392
|
+
const revisions = chainForWire(await chainFrom(handle, tag, fromId))
|
|
393
|
+
if (socket.readyState !== socket.OPEN || lookups.get(tag) !== lookup) return
|
|
394
|
+
if (debug) console.log(`subscribe ${debugTag(tag)} from=${fromId?.slice(0, 8) ?? 'null'} → chain ${revisions.length}`)
|
|
395
|
+
send(socket, { type: 'workspace-subscribed', workspaceTag: tag, resources })
|
|
396
|
+
send(socket, { type: 'workspace-state', workspaceTag: tag, revisions })
|
|
397
|
+
} catch (err) {
|
|
398
|
+
if (lookups.get(tag) !== lookup) return
|
|
399
|
+
if (debug) console.warn('subscribe: snapshot lookup failed', debugTag(tag), err)
|
|
400
|
+
socket.close(1011, 'subscription unavailable')
|
|
401
|
+
} finally {
|
|
402
|
+
if (lookups.get(tag) === lookup) lookups.delete(tag)
|
|
403
|
+
resume()
|
|
404
|
+
}
|
|
379
405
|
}
|
|
380
406
|
|
|
381
407
|
return { handleSave, handleSaveRest, handleSubscribe, sendSaveError }
|
package/out/client-admin.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
var N=globalThis,L=N.ShadowRoot&&(N.ShadyCSS===void 0||N.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,D=Symbol(),ie=new WeakMap,E=class{constructor(e,t,r){if(this._$cssResult$=!0,r!==D)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(L&&e===void 0){let r=t!==void 0&&t.length===1;r&&(e=ie.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&ie.set(t,e))}return e}toString(){return this.cssText}},oe=i=>new E(typeof i=="string"?i:i+"",void 0,D),y=(i,...e)=>{let t=i.length===1?i[0]:e.reduce((r,s,o)=>r+(a=>{if(a._$cssResult$===!0)return a.cssText;if(typeof a=="number")return a;throw Error("Value passed to 'css' function must be a 'css' function result: "+a+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+i[o+1],i[0]);return new E(t,i,D)},ne=(i,e)=>{if(L)i.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let r=document.createElement("style"),s=N.litNonce;s!==void 0&&r.setAttribute("nonce",s),r.textContent=t.cssText,i.appendChild(r)}},j=L?i=>i:i=>i instanceof CSSStyleSheet?(e=>{let t="";for(let r of e.cssRules)t+=r.cssText;return oe(t)})(i):i;var{is:Re,defineProperty:Te,getOwnPropertyDescriptor:Ie,getOwnPropertyNames:Ue,getOwnPropertySymbols:Oe,getPrototypeOf:ze}=Object,H=globalThis,ae=H.trustedTypes,Ne=ae?ae.emptyScript:"",Le=H.reactiveElementPolyfillSupport,C=(i,e)=>i,B={toAttribute(i,e){switch(e){case Boolean:i=i?Ne:null;break;case Object:case Array:i=i==null?i:JSON.stringify(i)}return i},fromAttribute(i,e){let t=i;switch(e){case Boolean:t=i!==null;break;case Number:t=i===null?null:Number(i);break;case Object:case Array:try{t=JSON.parse(i)}catch{t=null}}return t}},de=(i,e)=>!Re(i,e),le={attribute:!0,type:String,converter:B,reflect:!1,useDefault:!1,hasChanged:de};Symbol.metadata??=Symbol("metadata"),H.litPropertyMetadata??=new WeakMap;var g=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=le){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let r=Symbol(),s=this.getPropertyDescriptor(e,r,t);s!==void 0&&Te(this.prototype,e,s)}}static getPropertyDescriptor(e,t,r){let{get:s,set:o}=Ie(this.prototype,e)??{get(){return this[t]},set(a){this[t]=a}};return{get:s,set(a){let h=s?.call(this);o?.call(this,a),this.requestUpdate(e,h,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??le}static _$Ei(){if(this.hasOwnProperty(C("elementProperties")))return;let e=ze(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(C("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(C("properties"))){let t=this.properties,r=[...Ue(t),...Oe(t)];for(let s of r)this.createProperty(s,t[s])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[r,s]of t)this.elementProperties.set(r,s)}this._$Eh=new Map;for(let[t,r]of this.elementProperties){let s=this._$Eu(t,r);s!==void 0&&this._$Eh.set(s,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let r=new Set(e.flat(1/0).reverse());for(let s of r)t.unshift(j(s))}else e!==void 0&&t.push(j(e));return t}static _$Eu(e,t){let r=t.attribute;return r===!1?void 0:typeof r=="string"?r:typeof e=="string"?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let r of t.keys())this.hasOwnProperty(r)&&(e.set(r,this[r]),delete this[r]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return ne(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$ET(e,t){let r=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,r);if(s!==void 0&&r.reflect===!0){let o=(r.converter?.toAttribute!==void 0?r.converter:B).toAttribute(t,r.type);this._$Em=e,o==null?this.removeAttribute(s):this.setAttribute(s,o),this._$Em=null}}_$AK(e,t){let r=this.constructor,s=r._$Eh.get(e);if(s!==void 0&&this._$Em!==s){let o=r.getPropertyOptions(s),a=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:B;this._$Em=s;let h=a.fromAttribute(t,o.type);this[s]=h??this._$Ej?.get(s)??h,this._$Em=null}}requestUpdate(e,t,r,s=!1,o){if(e!==void 0){let a=this.constructor;if(s===!1&&(o=this[e]),r??=a.getPropertyOptions(e),!((r.hasChanged??de)(o,t)||r.useDefault&&r.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(a._$Eu(e,r))))return;this.C(e,t,r)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(e,t,{useDefault:r,reflect:s,wrapped:o},a){r&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,a??t??this[e]),o!==!0||a!==void 0)||(this._$AL.has(e)||(this.hasUpdated||r||(t=void 0),this._$AL.set(e,t)),s===!0&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[s,o]of this._$Ep)this[s]=o;this._$Ep=void 0}let r=this.constructor.elementProperties;if(r.size>0)for(let[s,o]of r){let{wrapped:a}=o,h=this[s];a!==!0||this._$AL.has(s)||h===void 0||this.C(s,void 0,o,h)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(r=>r.hostUpdate?.()),this.update(t)):this._$EM()}catch(r){throw e=!1,this._$EM(),r}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(e){}firstUpdated(e){}};g.elementStyles=[],g.shadowRootOptions={mode:"open"},g[C("elementProperties")]=new Map,g[C("finalized")]=new Map,Le?.({ReactiveElement:g}),(H.reactiveElementVersions??=[]).push("2.1.2");var K=globalThis,ce=i=>i,M=K.trustedTypes,pe=M?M.createPolicy("lit-html",{createHTML:i=>i}):void 0,_e="$lit$",b=`lit$${Math.random().toFixed(9).slice(2)}$`,be="?"+b,He=`<${be}>`,x=document,R=()=>x.createComment(""),T=i=>i===null||typeof i!="object"&&typeof i!="function",G=Array.isArray,Me=i=>G(i)||typeof i?.[Symbol.iterator]=="function",V=`[
|
|
2
|
-
\f\r]`,P=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,he=/-->/g,ue=/>/g,$=RegExp(`>|${V}(?:([^\\s"'>=/]+)(${V}*=${V}*(?:[^
|
|
3
|
-
\f\r"'\`<>=]|("|')|))|$)`,"g"),me=/'/g,fe=/"/g,ve=/^(?:script|style|textarea|title)$/i,Y=i=>(e,...t)=>({_$litType$:i,strings:e,values:t}),n=Y(1),dt=Y(2),ct=Y(3),A=Symbol.for("lit-noChange"),c=Symbol.for("lit-nothing"),ge=new WeakMap,w=x.createTreeWalker(x,129);function ye(i,e){if(!G(i)||!i.hasOwnProperty("raw"))throw Error("invalid template strings array");return pe!==void 0?pe.createHTML(e):e}var De=(i,e)=>{let t=i.length-1,r=[],s,o=e===2?"<svg>":e===3?"<math>":"",a=P;for(let h=0;h<t;h++){let l=i[h],d,u,p=-1,f=0;for(;f<l.length&&(a.lastIndex=f,u=a.exec(l),u!==null);)f=a.lastIndex,a===P?u[1]==="!--"?a=he:u[1]!==void 0?a=ue:u[2]!==void 0?(ve.test(u[2])&&(s=RegExp("</"+u[2],"g")),a=$):u[3]!==void 0&&(a=$):a===$?u[0]===">"?(a=s??P,p=-1):u[1]===void 0?p=-2:(p=a.lastIndex-u[2].length,d=u[1],a=u[3]===void 0?$:u[3]==='"'?fe:me):a===fe||a===me?a=$:a===he||a===ue?a=P:(a=$,s=void 0);let _=a===$&&i[h+1].startsWith("/>")?" ":"";o+=a===P?l+He:p>=0?(r.push(d),l.slice(0,p)+_e+l.slice(p)+b+_):l+b+(p===-2?h:_)}return[ye(i,o+(i[t]||"<?>")+(e===2?"</svg>":e===3?"</math>":"")),r]},I=class i{constructor({strings:e,_$litType$:t},r){let s;this.parts=[];let o=0,a=0,h=e.length-1,l=this.parts,[d,u]=De(e,t);if(this.el=i.createElement(d,r),w.currentNode=this.el.content,t===2||t===3){let p=this.el.content.firstChild;p.replaceWith(...p.childNodes)}for(;(s=w.nextNode())!==null&&l.length<h;){if(s.nodeType===1){if(s.hasAttributes())for(let p of s.getAttributeNames())if(p.endsWith(_e)){let f=u[a++],_=s.getAttribute(p).split(b),z=/([.?@])?(.*)/.exec(f);l.push({type:1,index:o,name:z[2],strings:_,ctor:z[1]==="."?F:z[1]==="?"?Z:z[1]==="@"?W:S}),s.removeAttribute(p)}else p.startsWith(b)&&(l.push({type:6,index:o}),s.removeAttribute(p));if(ve.test(s.tagName)){let p=s.textContent.split(b),f=p.length-1;if(f>0){s.textContent=M?M.emptyScript:"";for(let _=0;_<f;_++)s.append(p[_],R()),w.nextNode(),l.push({type:2,index:++o});s.append(p[f],R())}}}else if(s.nodeType===8)if(s.data===be)l.push({type:2,index:o});else{let p=-1;for(;(p=s.data.indexOf(b,p+1))!==-1;)l.push({type:7,index:o}),p+=b.length-1}o++}}static createElement(e,t){let r=x.createElement("template");return r.innerHTML=e,r}};function k(i,e,t=i,r){if(e===A)return e;let s=r!==void 0?t._$Co?.[r]:t._$Cl,o=T(e)?void 0:e._$litDirective$;return s?.constructor!==o&&(s?._$AO?.(!1),o===void 0?s=void 0:(s=new o(i),s._$AT(i,t,r)),r!==void 0?(t._$Co??=[])[r]=s:t._$Cl=s),s!==void 0&&(e=k(i,s._$AS(i,e.values),s,r)),e}var q=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:r}=this._$AD,s=(e?.creationScope??x).importNode(t,!0);w.currentNode=s;let o=w.nextNode(),a=0,h=0,l=r[0];for(;l!==void 0;){if(a===l.index){let d;l.type===2?d=new U(o,o.nextSibling,this,e):l.type===1?d=new l.ctor(o,l.name,l.strings,this,e):l.type===6&&(d=new J(o,this,e)),this._$AV.push(d),l=r[++h]}a!==l?.index&&(o=w.nextNode(),a++)}return w.currentNode=x,s}p(e){let t=0;for(let r of this._$AV)r!==void 0&&(r.strings!==void 0?(r._$AI(e,r,t),t+=r.strings.length-2):r._$AI(e[t])),t++}},U=class i{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,r,s){this.type=2,this._$AH=c,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=r,this.options=s,this._$Cv=s?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=k(this,e,t),T(e)?e===c||e==null||e===""?(this._$AH!==c&&this._$AR(),this._$AH=c):e!==this._$AH&&e!==A&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):Me(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==c&&T(this._$AH)?this._$AA.nextSibling.data=e:this.T(x.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:r}=e,s=typeof r=="number"?this._$AC(e):(r.el===void 0&&(r.el=I.createElement(ye(r.h,r.h[0]),this.options)),r);if(this._$AH?._$AD===s)this._$AH.p(t);else{let o=new q(s,this),a=o.u(this.options);o.p(t),this.T(a),this._$AH=o}}_$AC(e){let t=ge.get(e.strings);return t===void 0&&ge.set(e.strings,t=new I(e)),t}k(e){G(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,r,s=0;for(let o of e)s===t.length?t.push(r=new i(this.O(R()),this.O(R()),this,this.options)):r=t[s],r._$AI(o),s++;s<t.length&&(this._$AR(r&&r._$AB.nextSibling,s),t.length=s)}_$AR(e=this._$AA.nextSibling,t){for(this._$AP?.(!1,!0,t);e!==this._$AB;){let r=ce(e).nextSibling;ce(e).remove(),e=r}}setConnected(e){this._$AM===void 0&&(this._$Cv=e,this._$AP?.(e))}},S=class{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(e,t,r,s,o){this.type=1,this._$AH=c,this._$AN=void 0,this.element=e,this.name=t,this._$AM=s,this.options=o,r.length>2||r[0]!==""||r[1]!==""?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=c}_$AI(e,t=this,r,s){let o=this.strings,a=!1;if(o===void 0)e=k(this,e,t,0),a=!T(e)||e!==this._$AH&&e!==A,a&&(this._$AH=e);else{let h=e,l,d;for(e=o[0],l=0;l<o.length-1;l++)d=k(this,h[r+l],t,l),d===A&&(d=this._$AH[l]),a||=!T(d)||d!==this._$AH[l],d===c?e=c:e!==c&&(e+=(d??"")+o[l+1]),this._$AH[l]=d}a&&!s&&this.j(e)}j(e){e===c?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,e??"")}},F=class extends S{constructor(){super(...arguments),this.type=3}j(e){this.element[this.name]=e===c?void 0:e}},Z=class extends S{constructor(){super(...arguments),this.type=4}j(e){this.element.toggleAttribute(this.name,!!e&&e!==c)}},W=class extends S{constructor(e,t,r,s,o){super(e,t,r,s,o),this.type=5}_$AI(e,t=this){if((e=k(this,e,t,0)??c)===A)return;let r=this._$AH,s=e===c&&r!==c||e.capture!==r.capture||e.once!==r.once||e.passive!==r.passive,o=e!==c&&(r===c||s);s&&this.element.removeEventListener(this.name,this,r),o&&this.element.addEventListener(this.name,this,e),this._$AH=e}handleEvent(e){typeof this._$AH=="function"?this._$AH.call(this.options?.host??this.element,e):this._$AH.handleEvent(e)}},J=class{constructor(e,t,r){this.element=e,this.type=6,this._$AN=void 0,this._$AM=t,this.options=r}get _$AU(){return this._$AM._$AU}_$AI(e){k(this,e)}};var je=K.litHtmlPolyfillSupport;je?.(I,U),(K.litHtmlVersions??=[]).push("3.3.3");var $e=(i,e,t)=>{let r=t?.renderBefore??e,s=r._$litPart$;if(s===void 0){let o=t?.renderBefore??null;r._$litPart$=s=new U(e.insertBefore(R(),o),o,void 0,t??{})}return s._$AI(i),s};var Q=globalThis,m=class extends g{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=$e(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return A}};m._$litElement$=!0,m.finalized=!0,Q.litElementHydrateSupport?.({LitElement:m});var Be=Q.litElementPolyfillSupport;Be?.({LitElement:m});(Q.litElementVersions??=[]).push("4.2.2");var we=["admin","manage","triage","view","none"];var xe={dependencies:"Dependencies",security:"Security"};async function O(){let i=await fetch("/api/auth/session",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)return null;let e=await i.json();return{id:e?.user?.id??null,csrfToken:typeof e?.csrfToken=="string"?e.csrfToken:null}}async function Ve(){let i=await fetch("/api/admin/users",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);let e=await i.json();return Array.isArray(e?.users)?e.users:[]}async function qe(i,e,t){let r={"content-type":"application/json"};t&&(r["x-csrf-token"]=t);let s=await fetch("/api/admin/set-role",{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify({userId:i,role:e})});if(!s.ok)throw new Error(`HTTP ${s.status}`)}var X=class extends m{static properties={_users:{state:!0},_error:{state:!0}};static styles=y`:host{display:block;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}h1{font-size:1.15rem;font-weight:600;margin:0 0 1rem;user-select:none}.users{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.users li{display:flex;align-items:center;gap:.7rem;padding:.55rem .85rem}.users li+li{border-top:1px solid var(--border)}.avatar{position:relative;flex-shrink:0;width:28px;height:28px;border-radius:50%;overflow:hidden;background:var(--accent);display:inline-grid;place-items:center;user-select:none}.avatar>span{font-size:.8rem;font-weight:600;color:var(--bg);text-transform:uppercase}.avatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.avatar img.broken{display:none}.who{display:flex;flex-direction:column;min-width:0}.login{font-weight:600;font-size:.9rem}.name{color:var(--muted);font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.role{margin-left:auto;flex-shrink:0;font:inherit;font-size:.82rem;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.2rem .4rem}.role:disabled{opacity:.55}.msg{color:var(--muted);font-size:.9rem}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._users=null,this._error=null,this._me=null,this._csrf=null}connectedCallback(){super.connectedCallback(),this._load()}async _load(){this._error=null,this._users=null;try{let[e,t]=await Promise.all([O(),Ve()]);this._me=e?.id??null,this._csrf=e?.csrfToken??null,this._users=t}catch(e){this._error=String(e?.message??e)}}render(){return n`<div class="wrap"><h1>Users</h1>${this._error==null?this._users==null?n`<p class="msg">Loading…</p>`:this._list():n`<p class="msg error">Couldn't load users: ${this._error}</p>`}</div>`}_list(){return this._users.length===0?n`<p class="msg">No users yet.</p>`:n`<ul class="users">${this._users.map(e=>this._row(e))}</ul>`}_row(e){let t=(e.login?.[0]??"?").toUpperCase(),r=e.id===this._me;return n`<li><span class="avatar"><span>${t}</span> <img alt="" src="${`/api/avatar/${encodeURIComponent(e.id)}`}" @error="${s=>s.currentTarget.classList.add("broken")}"> </span><span class="who"><span class="login">${e.login}</span> ${e.name?n`<span class="name">${e.name}</span>`:c} </span><select class="role" ?disabled="${r}" title="${r?"You can\u2019t change your own role":"Change role"}" @change="${s=>this._changeRole(e,s.target.value,s.target)}">${we.map(s=>n`<option value="${s}" ?selected="${s===e.role}">${s}</option>`)}</select></li>`}async _changeRole(e,t,r){let s=e.role;if(t!==s){try{await qe(e.id,t,this._csrf),e.role=t}catch(o){console.warn("admin: set role failed:",o),r.value=s}this.requestUpdate()}}};customElements.define("managed-admin-users",X);async function Fe(){let i=await fetch("/api/admin/repositories",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);return i.json()}async function Ze(i,e,t){let r={"content-type":"application/json"};t&&(r["x-csrf-token"]=t);let s=await fetch("/api/admin/repositories/select",{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify({repoId:i,selected:e})});if(!s.ok)throw new Error(`HTTP ${s.status}`)}var We=n`<svg class="repo-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M2 2.75A2.75 2.75 0 0 1 4.75 0h7.5a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75H4.5a1 1 0 0 0 0 2h8a.75.75 0 0 1 0 1.5h-8A2.5 2.5 0 0 1 2 13V2.75Zm2.75-.25a1.25 1.25 0 0 0-1.25 1.25v7.32c.317-.114.66-.07 1 .18V2.5h-.5a.25.25 0 0 0 .75 0Zm6.75 0H5.5v8.5h6V2.5Z"/></svg>`,ee=class extends m{static properties={_data:{state:!0},_error:{state:!0},_tab:{state:!0}};static styles=y`:host{display:block;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}.head{display:flex;align-items:center;gap:1rem;margin:0 0 .75rem}h1{font-size:1.15rem;font-weight:600;margin:0;user-select:none}.connect{margin-left:auto;flex-shrink:0;font-size:.85rem;font-weight:600;color:var(--bg);background:var(--accent);text-decoration:none;border-radius:6px;padding:.35rem .7rem;white-space:nowrap}.connect:hover{opacity:.9}.tabs{display:flex;gap:.25rem;margin:0 0 1rem;border-bottom:1px solid var(--border)}.tab{font:inherit;font-size:.85rem;font-weight:600;color:var(--muted);background:0 0;border:none;border-bottom:2px solid transparent;margin-bottom:-1px;padding:.4rem .55rem;cursor:pointer;user-select:none}.tab:hover{color:var(--text)}.tab.active{color:var(--text);border-bottom-color:var(--accent)}.hint{color:var(--muted);font-size:.82rem;margin:0 0 .8rem}.repos{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.repos li{display:flex;align-items:center;gap:.6rem;padding:.55rem .85rem}.repos li+li{border-top:1px solid var(--border)}.select{flex-shrink:0;width:16px;height:16px;accent-color:var(--accent);cursor:pointer}.repo-icon{flex-shrink:0;color:var(--muted)}.full-name{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge{flex-shrink:0;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:.05rem .4rem}.open{margin-left:auto;flex-shrink:0;font-size:.82rem;color:var(--accent);text-decoration:none}.open:hover{text-decoration:underline}.msg{color:var(--muted);font-size:.9rem;line-height:1.5}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._data=null,this._error=null,this._csrf=null,this._tab="manage"}connectedCallback(){super.connectedCallback(),this._load()}async _load(){this._error=null,this._data=null;try{let[e,t]=await Promise.all([O(),Fe()]);this._csrf=e?.csrfToken??null,this._data=t}catch(e){this._error=String(e?.message??e)}}render(){let e=this._data?.installUrl;return n`<div class="wrap"><div class="head"><h1>Repositories</h1>${e?n`<a class="connect" href="${e}" target="_blank" rel="noopener noreferrer">Connect a repository</a>`:c}</div><div class="tabs" role="tablist"><button type="button" role="tab" class="tab ${this._tab==="manage"?"active":""}" aria-selected="${this._tab==="manage"}" @click="${()=>{this._tab="manage"}}">Manage repositories</button> <button type="button" role="tab" class="tab ${this._tab==="select"?"active":""}" aria-selected="${this._tab==="select"}" @click="${()=>{this._tab="select"}}">Select repositories</button></div>${this._body()}</div>`}_body(){if(this._error!=null)return n`<p class="msg error">Couldn't load repositories: ${this._error}</p>`;if(this._data==null)return n`<p class="msg">Loading…</p>`;let e=Array.isArray(this._data.repositories)?this._data.repositories:[];return e.length===0&&this._data.tokenMissing?n`<p class="msg">We don't have current GitHub access for your account. Please <strong>log out and log back in</strong> to grant repository access.</p>`:this._tab==="select"?this._selectView(e):this._manageView(e)}_manageView(e){let t=e.filter(r=>r.installed);return t.length===0?n`<p class="msg">No repositories connected yet. ${this._data.installUrl?n`Use “Connect a repository” to install the GitHub App on the repositories you want to read.`:n`Install the GitHub App on the repositories you want to read.`}</p>`:n`<p class="hint">Connected through the GitHub App — we can read these repositories.</p><ul class="repos">${t.map(r=>this._row(r,!1))}</ul>`}_selectView(e){return e.length===0?n`<p class="msg">No repositories found. ${this._data.installUrl?n`Use “Connect a repository” to install the GitHub App on the private repositories you want.`:n`Public repositories you own appear here.`}</p>`:n`${this._data.tokenMissing?n`<p class="hint">Some public repositories may be hidden — log out and back in to refresh GitHub access.</p>`:c}<p class="hint">Check a repository to operate on it — we'll read its contents.</p><ul class="repos">${e.map(t=>this._row(t,!0))}</ul>`}_row(e,t){return n`<li>${t?n`<input type="checkbox" class="select" .checked="${e.selected===!0}" title="${e.selected?"Selected \u2014 remove from the operate-on set":"Select this repository to operate on"}" @change="${r=>this._toggle(e,r.target)}">`:c} ${We} <span class="full-name">${e.fullName}</span> ${e.private?n`<span class="badge">private</span>`:c} ${e.htmlUrl?n`<a class="open" href="${e.htmlUrl}" target="_blank" rel="noopener noreferrer">open</a>`:c}</li>`}async _toggle(e,t){let r=t.checked;try{await Ze(e.id,r,this._csrf),e.selected=r}catch(s){console.warn("admin: repo selection failed:",s),t.checked=!r}this.requestUpdate()}};customElements.define("managed-admin-repos",ee);async function Je(){let i=await fetch("/api/admin/reports",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);return i.json()}async function Ke(i,e,t){let r={"content-type":i.type||"application/json","x-report-filename":encodeURIComponent(i.name)};e&&(r["x-csrf-token"]=e),t!=null&&(r["x-repo-id"]=String(t));let s=await fetch("/api/admin/reports",{method:"POST",credentials:"same-origin",headers:r,body:i});if(!s.ok)throw new Error(s.status===413?"too large":`HTTP ${s.status}`);return s.json()}async function Ge(i,e){let t=e?{"x-csrf-token":e}:{},r=await fetch(`/api/admin/reports/${encodeURIComponent(i)}`,{method:"DELETE",credentials:"same-origin",headers:t});if(!r.ok)throw new Error(`HTTP ${r.status}`)}async function Ye(i,e,t){let r={"content-type":"application/json"};t&&(r["x-csrf-token"]=t);let s=await fetch("/api/admin/reports/set-repo",{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify({reportId:i,repoId:e})});if(!s.ok)throw new Error(`HTTP ${s.status}`)}function Ae(i){return typeof i!="number"||!Number.isFinite(i)?"":i<1024?`${i} B`:i<1048576?`${(i/1024).toFixed(1)} KB`:`${(i/1048576).toFixed(1)} MB`}function ke(i,e){return n`<option value="" ?selected="${e==null||e===""}">No repository</option>${i.map(t=>n`<option value="${t.repoId}" ?selected="${Number(e)===t.repoId}">${t.fullName}</option>`)}`}function Se(i,e,t){return!Array.isArray(i)||i.length===0?c:n`<label class="repo-picker"><span class="repo-picker-label">Attach uploads to</span> <select class="repo-select" @change="${r=>t(r.target.value===""?null:Number(r.target.value))}">${ke(i,e)}</select></label>`}function Ee(i,e,t){return!Array.isArray(i)||i.length===0?c:n`<select class="repo-attach" title="Attach to a repository" @change="${r=>t(r.target.value===""?null:Number(r.target.value))}">${ke(i,e)}</select>`}function Ce(i,e=!0){let t=document.createElement("input");t.type="file",t.multiple=e,t.addEventListener("change",()=>{i([...t.files])},{once:!0}),t.click()}function Pe(i,e,t){let r=0,s=d=>Array.from(d.dataTransfer?.types??[]).includes("Files"),o=d=>{s(d)&&(d.preventDefault(),r+=1,t(!0))},a=d=>{s(d)&&d.preventDefault()},h=d=>{s(d)&&(r=Math.max(0,r-1),r===0&&t(!1))},l=d=>{s(d)&&(d.preventDefault(),d.stopPropagation(),r=0,t(!1),e([...d.dataTransfer.files]))};return i.addEventListener("dragenter",o),i.addEventListener("dragover",a),i.addEventListener("dragleave",h),i.addEventListener("drop",l),()=>{i.removeEventListener("dragenter",o),i.removeEventListener("dragover",a),i.removeEventListener("dragleave",h),i.removeEventListener("drop",l)}}var Qe=n`<svg class="report-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 10 4.25V1.5Zm7.75.689V4.25c0 .138.112.25.25.25h2.061ZM4.5 8.75A.75.75 0 0 1 5.25 8h5.5a.75.75 0 0 1 0 1.5h-5.5A.75.75 0 0 1 4.5 8.75Zm0 2.5a.75.75 0 0 1 .75-.75h5.5a.75.75 0 0 1 0 1.5h-5.5a.75.75 0 0 1-.75-.75Z"/></svg>`,te=class extends m{static properties={_data:{state:!0},_error:{state:!0},_busy:{state:!0},_dragOver:{state:!0}};static styles=y`:host{display:block;position:relative;flex:1 1 auto;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}.dropzone{position:absolute;inset:.6rem;z-index:5;display:grid;place-items:center;border:2px dashed var(--accent);border-radius:10px;background:rgb(from var(--bg) r g b / .9);color:var(--accent);font-size:1rem;font-weight:600;pointer-events:none}.head{display:flex;align-items:center;gap:1rem;margin:0 0 .75rem}h1{font-size:1.15rem;font-weight:600;margin:0;user-select:none}.head-actions{margin-left:auto;flex-shrink:0;display:flex;align-items:center;gap:.5rem}.repo-picker{display:inline-flex;align-items:center;gap:.4rem}.repo-picker-label{font-size:.8rem;color:var(--muted);white-space:nowrap}.repo-select{font:inherit;font-size:.82rem;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.2rem .4rem;max-width:14rem}.repo-attach{flex-shrink:0;font:inherit;font-size:.78rem;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.1rem .3rem;max-width:11rem}.upload{flex-shrink:0;font:inherit;font-size:.85rem;font-weight:600;color:var(--bg);background:var(--accent);border:none;cursor:pointer;border-radius:6px;padding:.35rem .7rem;white-space:nowrap}.upload:hover{opacity:.9}.upload:disabled{opacity:.55;cursor:default}.hint{color:var(--muted);font-size:.82rem;margin:0 0 .8rem}.reports{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.reports li{display:flex;align-items:center;gap:.6rem;padding:.55rem .85rem}.reports li+li{border-top:1px solid var(--border)}.report-icon{flex-shrink:0;color:var(--muted)}.who{display:flex;flex-direction:column;min-width:0}.filename{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.meta{color:var(--muted);font-size:.78rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions{margin-left:auto;flex-shrink:0;display:flex;gap:.7rem;align-items:center}.download{font-size:.82rem;color:var(--accent);text-decoration:none}.download:hover{text-decoration:underline}.delete{font:inherit;font-size:.82rem;color:var(--critical,#c00);background:0 0;border:none;cursor:pointer;padding:0}.delete:hover{text-decoration:underline}.msg{color:var(--muted);font-size:.9rem;line-height:1.5}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._data=null,this._error=null,this._csrf=null,this._busy=!1,this._repoId=null,this._dragOver=!1,this._teardownDrop=null,this._queue=[]}connectedCallback(){super.connectedCallback(),this._load(),this._teardownDrop=Pe(this,e=>{this._upload(e)},e=>{this._dragOver=e})}disconnectedCallback(){super.disconnectedCallback(),this._teardownDrop?.()}async _load(){this._error=null,this._data=null;try{let[e,t]=await Promise.all([O(),Je()]);this._csrf=e?.csrfToken??null,this._data=t}catch(e){this._error=String(e?.message??e)}}render(){return n`${this._dragOver?n`<div class="dropzone">Drop reports to upload</div>`:c}<div class="wrap"><div class="head"><h1>Reports</h1><div class="head-actions">${Se(this._data?.repos,this._repoId,e=>{this._repoId=e})} <button type="button" class="upload" ?disabled="${this._busy}" @click="${()=>Ce(e=>{this._upload(e)})}">${this._busy?"Uploading\u2026":"Upload report"}</button></div></div>${this._body()}</div>`}_body(){if(this._error!=null)return n`<p class="msg error">Couldn't load reports: ${this._error}</p>`;if(this._data==null)return n`<p class="msg">Loading…</p>`;let e=Array.isArray(this._data.reports)?this._data.reports:[];return e.length===0?n`<p class="msg">No reports uploaded yet. Drag & drop files here, or use “Upload report”, to add a findings report (JSON, markdown, or CSV) for the server to operate on.</p>`:n`<p class="hint">Reports stored on the server.</p><ul class="reports">${e.map(t=>this._row(t))}</ul>`}_row(e){let t=e.uploadedByLogin?`by ${e.uploadedByLogin}`:"uploader removed",r=Number.isFinite(e.uploadedAt)?new Date(e.uploadedAt).toLocaleString():"",s=e.bundleFilename?`bundle: ${e.bundleFilename}`:e.bundleIntegrity?"bundle: (not uploaded)":null,o=[t,r,Ae(e.byteSize),s].filter(Boolean).join(" \xB7 ");return n`<li>${Qe} <span class="who"><span class="filename">${e.filename}</span> <span class="meta">${o}</span> </span><span class="actions">${Ee(this._data?.repos,e.repoId,a=>this._setRepo(e,a))} <a class="download" href="${`/api/admin/reports/${encodeURIComponent(e.id)}`}">download</a> <button type="button" class="delete" @click="${()=>this._delete(e)}">delete</button></span></li>`}async _setRepo(e,t){try{await Ye(e.id,t,this._csrf)}catch(r){this._error=`Couldn't change repo: ${String(r?.message??r)}`}await this._load()}async _upload(e){if(e.length!==0&&(this._queue.push(...e),!this._busy)){this._busy=!0,this._error=null;try{for(;this._queue.length>0;){let t=this._queue.shift();await Ke(t,this._csrf,this._repoId)}}catch(t){this._queue=[],this._error=`Upload failed: ${String(t?.message??t)}`}finally{this._busy=!1,await this._load()}}}async _delete(e){if(globalThis.confirm?.(`Delete \u201C${e.filename}\u201D? This can't be undone.`)){try{await Ge(e.id,this._csrf)}catch(t){this._error=`Delete failed: ${String(t?.message??t)}`}await this._load()}}};customElements.define("managed-admin-reports",te);async function Xe(){let i=await fetch("/api/admin/bundles",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);return i.json()}async function et(i,e,t){let r={"content-type":"application/octet-stream","x-bundle-filename":encodeURIComponent(i.name)};e&&(r["x-csrf-token"]=e),t!=null&&(r["x-repo-id"]=String(t));let s=await fetch("/api/admin/bundles",{method:"POST",credentials:"same-origin",headers:r,body:i});if(!s.ok)throw new Error(s.status===413?"too large":`HTTP ${s.status}`);return s.json()}async function tt(i,e){let t=e?{"x-csrf-token":e}:{},r=await fetch(`/api/admin/bundles/${encodeURIComponent(i)}`,{method:"DELETE",credentials:"same-origin",headers:t});if(!r.ok)throw new Error(`HTTP ${r.status}`)}async function rt(i,e,t){let r={"content-type":"application/json"};t&&(r["x-csrf-token"]=t);let s=await fetch("/api/admin/bundles/set-repo",{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify({bundleId:i,repoId:e})});if(!s.ok)throw new Error(`HTTP ${s.status}`)}var st=n`<svg class="report-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M8.878.392a1.75 1.75 0 0 0-1.756 0l-5.25 3.045A1.75 1.75 0 0 0 1 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 0 0 1.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514ZM7.875 1.69a.25.25 0 0 1 .25 0l4.63 2.685L8 7.133 3.245 4.375Zm-5.125 4.4 4.5 2.61v5.317l-4.375-2.537a.25.25 0 0 1-.125-.216Zm6 7.927V8.7l4.5-2.61v3.96a.25.25 0 0 1-.125.216Z"/></svg>`,re=class extends m{static properties={_data:{state:!0},_error:{state:!0},_busy:{state:!0},_dragOver:{state:!0}};static styles=y`:host{display:block;position:relative;flex:1 1 auto;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}.dropzone{position:absolute;inset:.6rem;z-index:5;display:grid;place-items:center;border:2px dashed var(--accent);border-radius:10px;background:rgb(from var(--bg) r g b / .9);color:var(--accent);font-size:1rem;font-weight:600;pointer-events:none}.head{display:flex;align-items:center;gap:1rem;margin:0 0 .75rem}h1{font-size:1.15rem;font-weight:600;margin:0;user-select:none}.head-actions{margin-left:auto;flex-shrink:0;display:flex;align-items:center;gap:.5rem}.repo-picker{display:inline-flex;align-items:center;gap:.4rem}.repo-picker-label{font-size:.8rem;color:var(--muted);white-space:nowrap}.repo-select{font:inherit;font-size:.82rem;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.2rem .4rem;max-width:14rem}.repo-attach{flex-shrink:0;font:inherit;font-size:.78rem;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.1rem .3rem;max-width:11rem}.upload{flex-shrink:0;font:inherit;font-size:.85rem;font-weight:600;color:var(--bg);background:var(--accent);border:none;cursor:pointer;border-radius:6px;padding:.35rem .7rem;white-space:nowrap}.upload:hover{opacity:.9}.upload:disabled{opacity:.55;cursor:default}.hint{color:var(--muted);font-size:.82rem;margin:0 0 .8rem}.reports{list-style:none;margin:0;padding:0;border:1px solid var(--border);border-radius:8px;overflow:hidden}.reports li{display:flex;align-items:center;gap:.6rem;padding:.55rem .85rem}.reports li+li{border-top:1px solid var(--border)}.report-icon{flex-shrink:0;color:var(--muted)}.who{display:flex;flex-direction:column;min-width:0}.filename{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.meta{color:var(--muted);font-size:.78rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.badge{flex-shrink:0;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:.05rem .4rem}.actions{margin-left:auto;flex-shrink:0;display:flex;gap:.7rem;align-items:center}.download{font-size:.82rem;color:var(--accent);text-decoration:none}.download:hover{text-decoration:underline}.delete{font:inherit;font-size:.82rem;color:var(--critical,#c00);background:0 0;border:none;cursor:pointer;padding:0}.delete:hover{text-decoration:underline}.msg{color:var(--muted);font-size:.9rem;line-height:1.5}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._data=null,this._error=null,this._csrf=null,this._busy=!1,this._repoId=null,this._dragOver=!1,this._teardownDrop=null,this._queue=[]}connectedCallback(){super.connectedCallback(),this._load(),this._teardownDrop=Pe(this,e=>{this._upload(e)},e=>{this._dragOver=e})}disconnectedCallback(){super.disconnectedCallback(),this._teardownDrop?.()}async _load(){this._error=null,this._data=null;try{let[e,t]=await Promise.all([O(),Xe()]);this._csrf=e?.csrfToken??null,this._data=t}catch(e){this._error=String(e?.message??e)}}render(){return n`${this._dragOver?n`<div class="dropzone">Drop bundles to upload</div>`:c}<div class="wrap"><div class="head"><h1>Bundles</h1><div class="head-actions">${Se(this._data?.repos,this._repoId,e=>{this._repoId=e})} <button type="button" class="upload" ?disabled="${this._busy}" @click="${()=>Ce(e=>{this._upload(e)})}">${this._busy?"Uploading\u2026":"Upload bundle"}</button></div></div>${this._body()}</div>`}_body(){if(this._error!=null)return n`<p class="msg error">Couldn't load bundles: ${this._error}</p>`;if(this._data==null)return n`<p class="msg">Loading…</p>`;let e=Array.isArray(this._data.bundles)?this._data.bundles:[];return e.length===0?n`<p class="msg">No bundles uploaded yet. Drag & drop files here, or use “Upload bundle”, to add a sourcemap or stasis archive for the server to operate on; reports auto-link by content hash.</p>`:n`<p class="hint">Bundles stored on the server.</p><ul class="reports">${e.map(t=>this._row(t))}</ul>`}_row(e){let t=e.uploadedByLogin?`by ${e.uploadedByLogin}`:"uploader removed",r=Number.isFinite(e.uploadedAt)?new Date(e.uploadedAt).toLocaleString():"",s=[t,r,Ae(e.byteSize)].filter(Boolean).join(" \xB7 ");return n`<li title="${e.integrity??""}">${st} <span class="who"><span class="filename">${e.filename}</span> <span class="meta">${s}</span> </span>${e.kind?n`<span class="badge">${e.kind}</span>`:c} <span class="actions">${Ee(this._data?.repos,e.repoId,o=>this._setRepo(e,o))} <a class="download" href="${`/api/admin/bundles/${encodeURIComponent(e.id)}`}">download</a> <button type="button" class="delete" @click="${()=>this._delete(e)}">delete</button></span></li>`}async _setRepo(e,t){try{await rt(e.id,t,this._csrf)}catch(r){this._error=`Couldn't change repo: ${String(r?.message??r)}`}await this._load()}async _upload(e){if(e.length!==0&&(this._queue.push(...e),!this._busy)){this._busy=!0,this._error=null;try{for(;this._queue.length>0;){let t=this._queue.shift();await et(t,this._csrf,this._repoId)}}catch(t){this._queue=[],this._error=`Upload failed: ${String(t?.message??t)}`}finally{this._busy=!1,await this._load()}}}async _delete(e){if(globalThis.confirm?.(`Delete \u201C${e.filename}\u201D? Linked reports will keep their pending link.`)){try{await tt(e.id,this._csrf)}catch(t){this._error=`Delete failed: ${String(t?.message??t)}`}await this._load()}}};customElements.define("managed-admin-bundles",re);async function it(){let i=await fetch("/api/admin/teams",{credentials:"same-origin",headers:{accept:"application/json"}});if(!i.ok)throw new Error(`HTTP ${i.status}`);return i.json()}async function v(i,e,t){let r={"content-type":"application/json"};e&&(r["x-csrf-token"]=e);let s=await fetch(i,{method:"POST",credentials:"same-origin",headers:r,body:JSON.stringify(t)});if(!s.ok)throw new Error(s.status===409?"name already taken":`HTTP ${s.status}`)}var se=class extends m{static properties={_data:{state:!0},_error:{state:!0},_busy:{state:!0},_renamingId:{state:!0}};static styles=y`:host{display:block;padding:1.5rem clamp(1rem,4vw,2.5rem);color:var(--text)}.wrap{max-width:48rem;margin:0 auto}.head{display:flex;align-items:center;gap:.5rem;margin:0 0 1rem}h1{font-size:1.15rem;font-weight:600;margin:0 auto 0 0;user-select:none}input,select{font:inherit;font-size:.82rem;color:var(--text);background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:.25rem .4rem}.new-name{width:12rem}.btn{font:inherit;font-size:.82rem;font-weight:600;color:var(--bg);background:var(--accent);border:none;border-radius:6px;padding:.3rem .6rem;cursor:pointer;white-space:nowrap}.btn:hover{opacity:.9}.btn:disabled{opacity:.55;cursor:default}.team{border:1px solid var(--border);border-radius:8px;padding:.85rem;margin:0 0 1rem}.team-head{display:flex;align-items:center;gap:.6rem;margin:0 0 .6rem}.team-name{font-weight:600;font-size:1rem}.rename-input{width:12rem}.sub{margin:.5rem 0 0}.sub-title{font-size:.72rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:0 0 .3rem}.links{list-style:none;margin:0 0 .4rem;padding:0}.links li{display:flex;align-items:center;gap:.6rem;padding:.25rem 0}.ln{font-size:.88rem}.path{color:var(--muted);font-size:.82rem}.perms{margin-left:auto;display:flex;gap:.8rem}.perm{display:inline-flex;align-items:center;gap:.25rem;font-size:.8rem;color:var(--muted);user-select:none}.add-row{display:flex;align-items:center;gap:.4rem;margin:.3rem 0 0;flex-wrap:wrap}.add-row select{max-width:16rem}.x{font:inherit;font-size:.8rem;color:var(--critical,#c00);background:0 0;border:none;cursor:pointer;padding:0}.x:hover{text-decoration:underline}.delete{margin-left:auto}.muted{color:var(--muted);font-size:.82rem;margin:0 0 .3rem}.msg{color:var(--muted);font-size:.9rem}.msg.error{color:var(--critical,#c00)}`;constructor(){super(),this._data=null,this._error=null,this._csrf=null,this._busy=!1,this._renamingId=null}connectedCallback(){super.connectedCallback(),this._load()}async _load(){this._error=null;try{let[e,t]=await Promise.all([O(),it()]);this._csrf=e?.csrfToken??null,this._data=t}catch(e){this._error=String(e?.message??e)}}async _do(e){if(!this._busy){this._busy=!0,this._error=null;try{await e()}catch(t){this._error=String(t?.message??t)}finally{this._busy=!1,await this._load()}}}render(){return n`<div class="wrap"><div class="head"><h1>Teams</h1><input class="new-name" type="text" placeholder="New team name" maxlength="100" @keydown="${e=>{e.key==="Enter"&&this._create()}}"> <button class="btn" ?disabled="${this._busy}" @click="${()=>this._create()}">Create</button></div>${this._body()}</div>`}_body(){if(this._error!=null&&this._data==null)return n`<p class="msg error">Couldn't load teams: ${this._error}</p>`;if(this._data==null)return n`<p class="msg">Loading…</p>`;let e=Array.isArray(this._data.teams)?this._data.teams:[];return n`${this._error==null?c:n`<p class="msg error">${this._error}</p>`} ${e.length===0?n`<p class="msg">No teams yet. Create one above.</p>`:e.map(t=>this._team(t))}`}_team(e){return n`<div class="team"><div class="team-head">${this._renamingId===e.id?n`<input class="rename-input" type="text" .value="${e.name}" maxlength="100" @keydown="${t=>this._renameKey(t,e)}"> <button class="btn" ?disabled="${this._busy}" @click="${t=>this._saveRename(e,t)}">save</button> <button class="x" @click="${()=>{this._renamingId=null}}">cancel</button>`:n`<span class="team-name">${e.name}</span> <button class="x" @click="${()=>{this._renamingId=e.id}}">rename</button> <button class="x delete" @click="${()=>this._deleteTeam(e)}">delete team</button>`}</div><div class="sub"><div class="sub-title">Repositories</div>${e.repos.length===0?n`<p class="muted">No repositories linked.</p>`:n`<ul class="links">${e.repos.map(t=>this._repoRow(e,t))}</ul>`} ${this._addRepoRow(e)}</div><div class="sub"><div class="sub-title">Members</div>${e.members.length===0?n`<p class="muted">No members.</p>`:n`<ul class="links">${e.members.map(t=>this._memberRow(e,t))}</ul>`} ${this._addMemberRow(e)}</div></div>`}_repoRow(e,t){return n`<li><span class="ln">${t.fullName}${t.path?n`<span class="path">/${t.path}</span>`:c}</span> <button class="x" @click="${()=>this._do(()=>v("/api/admin/teams/remove-repo",this._csrf,{teamId:e.id,repoId:t.repoId}))}">remove</button></li>`}_addRepoRow(e){let t=Array.isArray(this._data.repos)?this._data.repos:[];if(t.length===0)return n`<p class="muted">No selected repositories to link — pick some on “Manage repositories”.</p>`;let r=new Set(e.repos.map(s=>s.repoId));return n`<div class="add-row"><select class="add-repo-sel"><option value="">Add repository…</option>${t.map(s=>n`<option value="${s.repoId}">${s.fullName}${r.has(s.repoId)?" \u2014 update path":""}</option>`)}</select> <input class="add-repo-path" type="text" placeholder="subpath (optional)" maxlength="500"> <button class="btn" @click="${s=>this._addRepo(e,s)}">add</button></div>`}_memberRow(e,t){let r=Array.isArray(this._data.permissions)?this._data.permissions:[];return n`<li><span class="ln">${t.login}</span> <span class="perms">${r.map(s=>n`<label class="perm"><input type="checkbox" .checked="${t[s]===!0}" @change="${o=>this._togglePerm(e,t,s,o.target.checked)}"> ${xe[s]??s}</label>`)} </span><button class="x" @click="${()=>this._do(()=>v("/api/admin/teams/remove-member",this._csrf,{teamId:e.id,userId:t.userId}))}">remove</button></li>`}_addMemberRow(e){let t=Array.isArray(this._data.users)?this._data.users:[],r=new Set(e.members.map(s=>s.userId));return n`<div class="add-row"><select class="add-member-sel"><option value="">Add member…</option>${t.map(s=>n`<option value="${s.id}" ?disabled="${r.has(s.id)}">${s.login}${r.has(s.id)?" (member)":""}</option>`)}</select> <button class="btn" @click="${s=>this._addMember(e,s)}">add</button></div>`}_create(){let e=this.renderRoot.querySelector(".new-name"),t=e?.value.trim();t&&this._do(async()=>{await v("/api/admin/teams",this._csrf,{name:t}),e&&(e.value="")})}_deleteTeam(e){globalThis.confirm?.(`Delete team \u201C${e.name}\u201D? Its repo + member links are removed.`)&&this._do(()=>v("/api/admin/teams/delete",this._csrf,{teamId:e.id}))}_renameKey(e,t){e.key==="Enter"?this._saveRename(t,e):e.key==="Escape"&&(this._renamingId=null)}_saveRename(e,t){if(this._busy)return;let r=t.target.closest(".team-head")?.querySelector(".rename-input")?.value.trim()??"";this._renamingId=null,!(r===""||r===e.name)&&this._do(()=>v("/api/admin/teams/rename",this._csrf,{teamId:e.id,name:r}))}_addRepo(e,t){let r=t.target.closest(".add-row"),s=Number(r?.querySelector(".add-repo-sel")?.value);if(!Number.isSafeInteger(s)||s<=0)return;let o=r?.querySelector(".add-repo-path")?.value??"";this._do(()=>v("/api/admin/teams/set-repo",this._csrf,{teamId:e.id,repoId:s,path:o}))}_addMember(e,t){let r=t.target.closest(".add-row")?.querySelector(".add-member-sel")?.value;r&&this._do(()=>v("/api/admin/teams/set-member",this._csrf,{teamId:e.id,userId:r}))}_togglePerm(e,t,r,s){let o={};for(let a of this._data.permissions??[])o[a]=t[a]===!0;o[r]=s,this._do(()=>v("/api/admin/teams/set-member",this._csrf,{teamId:e.id,userId:t.userId,...o}))}};customElements.define("managed-admin-teams",se);export{re as ManagedAdminBundles,te as ManagedAdminReports,ee as ManagedAdminRepos,se as ManagedAdminTeams,X as ManagedAdminUsers};
|
|
4
|
-
/*! Bundled license information:
|
|
5
|
-
|
|
6
|
-
@lit/reactive-element/css-tag.js:
|
|
7
|
-
(**
|
|
8
|
-
* @license
|
|
9
|
-
* Copyright 2019 Google LLC
|
|
10
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
11
|
-
*)
|
|
12
|
-
|
|
13
|
-
@lit/reactive-element/reactive-element.js:
|
|
14
|
-
lit-html/lit-html.js:
|
|
15
|
-
lit-element/lit-element.js:
|
|
16
|
-
(**
|
|
17
|
-
* @license
|
|
18
|
-
* Copyright 2017 Google LLC
|
|
19
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
20
|
-
*)
|
|
21
|
-
|
|
22
|
-
lit-html/is-server.js:
|
|
23
|
-
(**
|
|
24
|
-
* @license
|
|
25
|
-
* Copyright 2022 Google LLC
|
|
26
|
-
* SPDX-License-Identifier: BSD-3-Clause
|
|
27
|
-
*)
|
|
28
|
-
*/
|