@preventive/triage 1.0.0-alpha.2 → 1.0.0-alpha.21
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/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +111 -0
- package/common/bundle-metadata.d.ts +10 -0
- package/common/bundle-metadata.js +177 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-sources.d.ts +3 -0
- package/common/bundle-sources.js +284 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +36 -0
- package/common/default-scan-models.ts +30 -0
- package/common/finding-id.js +47 -0
- package/common/github-pr.ts +56 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/permissions.ts +35 -0
- package/common/managed/report-content.ts +42 -0
- package/common/managed/report-filter.ts +108 -0
- package/common/managed/roles.ts +28 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +121 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/triage.ts +83 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/server-info.ts +33 -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-import.js +81 -0
- package/out/client-managed.js +110 -0
- package/out/client-sync.js +16 -13
- package/out/graph.js +30 -4
- package/out/index.html +55 -8
- package/out/prism.js +2 -2
- package/out/stasis.svg +45 -0
- package/out/terminal.js +273 -39
- package/out/view.css +1 -1
- package/out/view.js +198 -62
- package/package.json +179 -55
- package/report/index.js +254 -0
- package/report/src/finding-id.js +80 -0
- package/report/src/finding.js +312 -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/security.js +63 -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/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-log.ts +34 -0
- package/server-common/vercel-blob.ts +110 -0
- package/server-e2e/app.ts +485 -0
- package/{server → server-e2e}/auth.ts +5 -1
- package/{server → server-e2e}/bus-receiver.ts +9 -8
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +54 -39
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +7 -10
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +96 -135
- package/{server → server-e2e}/http.ts +110 -12
- package/{server → server-e2e}/hub.ts +44 -14
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +11 -144
- package/{server → server-e2e}/objstore/blob-fs.ts +6 -8
- package/{server → server-e2e}/objstore/blob-vercel.ts +49 -141
- package/{server → server-e2e}/objstore/blob.ts +24 -9
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +19 -20
- package/{server → server-e2e}/objstore/init.ts +53 -27
- package/{server → server-e2e}/objstore/reaper.ts +31 -11
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +110 -93
- package/{server → server-e2e}/objstore/sign.ts +105 -0
- package/{server → server-e2e}/objstore/store-neon.ts +10 -14
- package/{server → server-e2e}/objstore/store.ts +98 -118
- package/{server → server-e2e}/objstore/tokens.ts +9 -12
- package/{server → server-e2e}/peer.ts +7 -9
- package/{server → server-e2e}/pubsub.ts +29 -36
- package/{server → server-e2e}/sign.ts +12 -14
- package/{server → server-e2e}/sse-server.ts +105 -73
- package/{server → server-e2e}/sse-session.ts +30 -16
- package/{server → server-e2e}/static.ts +36 -29
- package/server-e2e/sync-handlers.ts +408 -0
- package/{server → server-e2e}/util.ts +9 -0
- package/{server → server-e2e}/ws-server.ts +29 -23
- package/server-managed/activity.ts +231 -0
- package/server-managed/avatar-store.ts +51 -0
- package/server-managed/blob-store.ts +66 -0
- package/server-managed/blob-vercel.ts +125 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-cache.ts +185 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-store.ts +28 -0
- package/server-managed/bundle-summary-cache.ts +97 -0
- package/server-managed/bundle.ts +39 -0
- package/server-managed/cache-storage.ts +40 -0
- package/server-managed/cli.js +13 -0
- package/server-managed/combined.ts +46 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/config.ts +144 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1336 -0
- package/server-managed/db-neon.ts +171 -0
- package/server-managed/db-schema.ts +203 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +109 -0
- package/server-managed/github-app.ts +332 -0
- package/server-managed/github-metadata.ts +65 -0
- package/server-managed/github-oauth.ts +215 -0
- package/server-managed/github-pulls.ts +115 -0
- package/server-managed/http-response.ts +18 -0
- package/server-managed/http.ts +2043 -0
- package/server-managed/import-triage.ts +48 -0
- package/server-managed/index.ts +135 -0
- package/server-managed/public-workspace.ts +150 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +4 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +154 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/session.ts +78 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/sql-postgres.ts +30 -0
- package/server-managed/sql.ts +61 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage.ts +34 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +128 -0
- package/server-managed/team-reports.ts +156 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/uploads.ts +47 -0
- package/server-managed/workspace-shares.ts +150 -0
- package/server.ts +50 -0
- package/server/index.ts +0 -481
- package/server/lifecycle.ts +0 -204
- package/server/sync-handlers.ts +0 -327
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Staging is shared storage, never process memory. Each part is bound to the
|
|
2
|
+
// authenticated session, destination and random upload ID. No bearer URL is
|
|
3
|
+
// exposed; all reads/writes still pass the managed role/origin/CSRF gates.
|
|
4
|
+
import { Buffer } from 'node:buffer'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import type { IncomingMessage } from 'node:http'
|
|
7
|
+
import type { BlobStore } from './blob-store.ts'
|
|
8
|
+
|
|
9
|
+
export const UPLOAD_CHUNK_BYTES = 3 * 1024 * 1024
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u
|
|
11
|
+
export type UploadKind = 'reports' | 'bundles'
|
|
12
|
+
|
|
13
|
+
function partId(session: string, kind: UploadKind, id: string, index: number): string {
|
|
14
|
+
const hash = createHash('sha256').update(JSON.stringify([session, kind, id, index])).digest('hex').slice(0, 32)
|
|
15
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20)}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validUploadPart(id: string, index: number, maxBytes: number): boolean {
|
|
19
|
+
return UUID.test(id) && Number.isSafeInteger(index) && index >= 0 && index < Math.ceil(maxBytes / UPLOAD_CHUNK_BYTES)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function putUploadPart(store: BlobStore, session: string, kind: UploadKind, id: string, index: number, bytes: Buffer): Promise<void> {
|
|
23
|
+
return store.put(partId(session, kind, id, index), bytes)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function readUpload(store: BlobStore, req: IncomingMessage, session: string, kind: UploadKind, maxBytes: number): Promise<Buffer> {
|
|
27
|
+
const id = req.headers['x-upload-id']
|
|
28
|
+
const count = Number(req.headers['x-upload-parts']), size = Number(req.headers['x-upload-size'])
|
|
29
|
+
if (typeof id !== 'string' || !UUID.test(id) || !Number.isSafeInteger(size) || size <= 0
|
|
30
|
+
|| !Number.isSafeInteger(count) || count !== Math.ceil(size / UPLOAD_CHUNK_BYTES)) throw new Error('bad-upload')
|
|
31
|
+
if (size > maxBytes) throw new Error('too-large')
|
|
32
|
+
const ids = Array.from({ length: count }, (_, index) => partId(session, kind, id, index))
|
|
33
|
+
const parts: Buffer[] = []
|
|
34
|
+
try {
|
|
35
|
+
for (const [index, key] of ids.entries()) {
|
|
36
|
+
const bytes = await store.get(key)
|
|
37
|
+
const expected = Math.min(UPLOAD_CHUNK_BYTES, size - index * UPLOAD_CHUNK_BYTES)
|
|
38
|
+
if (!bytes || bytes.length !== expected) throw new Error('bad-upload')
|
|
39
|
+
parts.push(bytes)
|
|
40
|
+
}
|
|
41
|
+
return Buffer.concat(parts, size)
|
|
42
|
+
} finally {
|
|
43
|
+
// These bytes can no longer be finalized. Failed/abandoned chunk uploads
|
|
44
|
+
// are also covered by the daily staging sweep.
|
|
45
|
+
for (const key of ids) await store.delete(key).catch(err => console.warn('managed: upload cleanup failed:', err))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ManagedSql } from './sql.ts'
|
|
2
|
+
import type { ManagedBundle, TeamReportAccessSnapshot, UserTeam } from './db.ts'
|
|
3
|
+
import { type TeamUserPermissions, parseTeamUserPermissions } from '../common/managed/permissions.ts'
|
|
4
|
+
|
|
5
|
+
export const WORKSPACE_SHARE_SCHEMA = `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS managed_workspace_share (
|
|
7
|
+
token_hash TEXT PRIMARY KEY,
|
|
8
|
+
team_id TEXT NOT NULL REFERENCES managed_team(id) ON DELETE CASCADE,
|
|
9
|
+
created_by TEXT NOT NULL REFERENCES managed_user(id) ON DELETE CASCADE,
|
|
10
|
+
created_at INTEGER NOT NULL,
|
|
11
|
+
dependencies INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
security INTEGER NOT NULL DEFAULT 0
|
|
13
|
+
) STRICT;
|
|
14
|
+
CREATE INDEX IF NOT EXISTS managed_workspace_share_team_idx ON managed_workspace_share(team_id);
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
export interface WorkspaceShareSnapshot extends TeamReportAccessSnapshot {
|
|
18
|
+
team: UserTeam
|
|
19
|
+
permissions: TeamUserPermissions
|
|
20
|
+
repositories: { repoId: number; github: string; path: string }[]
|
|
21
|
+
bundles: ManagedBundle[]
|
|
22
|
+
}
|
|
23
|
+
export interface WorkspaceShareInfo {
|
|
24
|
+
id: string
|
|
25
|
+
createdAt: number
|
|
26
|
+
createdBy: string
|
|
27
|
+
permissions: TeamUserPermissions
|
|
28
|
+
}
|
|
29
|
+
export interface ManagedWorkspaceShare extends WorkspaceShareInfo {
|
|
30
|
+
teamId: string
|
|
31
|
+
teamName: string
|
|
32
|
+
teamSlug: string
|
|
33
|
+
}
|
|
34
|
+
export interface WorkspaceShareStore {
|
|
35
|
+
createWorkspaceShare(sessionId: string, now: number, teamId: string, tokenHash: string, permissions?: TeamUserPermissions): Promise<boolean>
|
|
36
|
+
listWorkspaceShares(sessionId: string, now: number, teamId: string): Promise<WorkspaceShareInfo[] | null>
|
|
37
|
+
listManagedWorkspaceShares(sessionId: string, now: number): Promise<ManagedWorkspaceShare[] | null>
|
|
38
|
+
updateWorkspaceShare(sessionId: string, now: number, teamId: string, id: string, permissions: TeamUserPermissions): Promise<boolean>
|
|
39
|
+
revokeWorkspaceShares(sessionId: string, now: number, teamId: string, id?: string): Promise<boolean>
|
|
40
|
+
getWorkspaceShare(tokenHash: string): Promise<WorkspaceShareSnapshot | null>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A share is a capability, never a user session. Its issuer must still manage
|
|
44
|
+
// this team on every read. No union of the issuer's other memberships applies.
|
|
45
|
+
function shareQueries(db: ManagedSql) {
|
|
46
|
+
const manager = db.prepare(`SELECT 1 FROM managed_session s
|
|
47
|
+
JOIN managed_user u ON u.id = s.user_id
|
|
48
|
+
JOIN managed_team t ON t.id = ?
|
|
49
|
+
WHERE s.id = ? AND s.expires_at > ? AND (u.role = 'admin' OR (u.role = 'manage'
|
|
50
|
+
AND EXISTS (SELECT 1 FROM managed_team_user tu WHERE tu.user_id = u.id AND tu.team_id = t.id)))`)
|
|
51
|
+
const insert = db.prepare(`INSERT INTO managed_workspace_share (token_hash, team_id, created_by, created_at, dependencies, security)
|
|
52
|
+
SELECT ?, ?, user_id, ?, ?, ? FROM managed_session WHERE id = ?`)
|
|
53
|
+
const remove = db.prepare('DELETE FROM managed_workspace_share WHERE team_id = ? AND (CAST(? AS TEXT) IS NULL OR token_hash = ?)')
|
|
54
|
+
const update = db.prepare('UPDATE managed_workspace_share SET dependencies = ?, security = ? WHERE team_id = ? AND token_hash = ?')
|
|
55
|
+
// The management ID is a hash, never a bearer token. Existing URLs remain
|
|
56
|
+
// unchanged when their permissions are edited; plaintext tokens aren't kept.
|
|
57
|
+
const list = db.prepare(`SELECT s.token_hash AS id, s.created_at AS createdAt, u.login AS createdBy, s.dependencies, s.security
|
|
58
|
+
FROM managed_workspace_share s JOIN managed_user u ON u.id = s.created_by WHERE s.team_id = ? ORDER BY s.created_at DESC, s.token_hash`)
|
|
59
|
+
const session = db.prepare(`SELECT u.id, u.role FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
60
|
+
WHERE s.id = ? AND s.expires_at > ? AND u.role IN ('manage', 'admin')`)
|
|
61
|
+
const all = db.prepare(`SELECT s.token_hash AS id, s.created_at AS createdAt, u.login AS createdBy, s.dependencies, s.security,
|
|
62
|
+
t.id AS teamId, t.slug AS teamSlug, t.name AS teamName FROM managed_workspace_share s
|
|
63
|
+
JOIN managed_user u ON u.id = s.created_by JOIN managed_team t ON t.id = s.team_id
|
|
64
|
+
WHERE ? = 'admin' OR EXISTS (SELECT 1 FROM managed_team_user tu WHERE tu.user_id = ? AND tu.team_id = s.team_id)
|
|
65
|
+
ORDER BY t.name, t.id, s.created_at DESC, s.token_hash`)
|
|
66
|
+
const share = db.prepare(`SELECT t.id, t.slug, t.name, s.dependencies, s.security FROM managed_workspace_share s
|
|
67
|
+
JOIN managed_user u ON u.id = s.created_by
|
|
68
|
+
JOIN managed_team t ON t.id = s.team_id
|
|
69
|
+
WHERE s.token_hash = ? AND (u.role = 'admin' OR (u.role = 'manage'
|
|
70
|
+
AND EXISTS (SELECT 1 FROM managed_team_user tu WHERE tu.user_id = u.id AND tu.team_id = s.team_id)))`)
|
|
71
|
+
const repositories = db.prepare(`SELECT tr.repo_id AS repoId, sr.full_name AS github, tr.path
|
|
72
|
+
FROM managed_team_repo tr JOIN managed_selected_repo sr ON sr.repo_id = tr.repo_id
|
|
73
|
+
WHERE tr.team_id = ? ORDER BY tr.repo_id, tr.path`)
|
|
74
|
+
const reports = db.prepare(`SELECT DISTINCT r.id, r.slug, r.filename, r.byte_size AS byteSize,
|
|
75
|
+
r.sha256, r.repo_directory AS directory, sr.full_name AS github
|
|
76
|
+
FROM managed_team_repo tr JOIN managed_report r ON r.repo_id = tr.repo_id
|
|
77
|
+
JOIN managed_selected_repo sr ON sr.repo_id = r.repo_id
|
|
78
|
+
WHERE tr.team_id = ? AND r.visible = 1 AND (tr.path = '' OR r.repo_directory = tr.path
|
|
79
|
+
OR substr(r.repo_directory, 1, length(tr.path) + 1) = tr.path || '/') ORDER BY r.id`)
|
|
80
|
+
// A raw bundle is visible only when its declared root is inside the team scope.
|
|
81
|
+
// Root bundles remain hidden from directory-only grants.
|
|
82
|
+
const bundles = db.prepare(`SELECT DISTINCT b.id, b.slug, b.integrity, b.filename, b.kind, b.byte_size AS byteSize,
|
|
83
|
+
b.uploaded_by AS uploadedBy, b.repo_id AS repoId, b.repo_directory AS repoDirectory, b.uploaded_at AS uploadedAt, sr.full_name AS repoFullName
|
|
84
|
+
FROM managed_team_repo tr JOIN managed_bundle b ON b.repo_id = tr.repo_id
|
|
85
|
+
JOIN managed_selected_repo sr ON sr.repo_id = b.repo_id
|
|
86
|
+
WHERE tr.team_id = ? AND (tr.path = '' OR b.repo_directory = tr.path
|
|
87
|
+
OR substr(b.repo_directory, 1, length(tr.path) + 1) = tr.path || '/') ORDER BY b.id`)
|
|
88
|
+
return { manager, insert, remove, update, list, session, all, share, repositories, reports, bundles }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function shareInfo<T extends { dependencies: number; security: number }>(row: T) {
|
|
92
|
+
const { dependencies, security, ...rest } = row
|
|
93
|
+
return { ...rest, permissions: { dependencies: dependencies === 1, security: security === 1 } }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function workspaceShareMethods(db: ManagedSql): WorkspaceShareStore {
|
|
97
|
+
const q = shareQueries(db)
|
|
98
|
+
return {
|
|
99
|
+
async createWorkspaceShare(sessionId, now, teamId, tokenHash, permissions) {
|
|
100
|
+
if (!await q.manager.get(teamId, sessionId, now)) return false
|
|
101
|
+
const p = parseTeamUserPermissions(permissions)
|
|
102
|
+
await q.insert.run(tokenHash, teamId, now, +p.dependencies, +p.security, sessionId)
|
|
103
|
+
return true
|
|
104
|
+
},
|
|
105
|
+
async listWorkspaceShares(sessionId, now, teamId) {
|
|
106
|
+
if (!await q.manager.get(teamId, sessionId, now)) return null
|
|
107
|
+
const rows = await q.list.all(teamId) as { id: string; createdAt: number; createdBy: string; dependencies: number; security: number }[]
|
|
108
|
+
return rows.map(shareInfo)
|
|
109
|
+
},
|
|
110
|
+
async listManagedWorkspaceShares(sessionId, now) {
|
|
111
|
+
const viewer = await q.session.get(sessionId, now) as { id: string; role: string } | undefined
|
|
112
|
+
if (!viewer) return null
|
|
113
|
+
const rows = await q.all.all(viewer.role, viewer.id) as (Omit<ManagedWorkspaceShare, 'permissions'> & { dependencies: number; security: number })[]
|
|
114
|
+
return rows.map(shareInfo)
|
|
115
|
+
},
|
|
116
|
+
async updateWorkspaceShare(sessionId, now, teamId, id, permissions) {
|
|
117
|
+
if (!await q.manager.get(teamId, sessionId, now)) return false
|
|
118
|
+
const p = parseTeamUserPermissions(permissions)
|
|
119
|
+
return (await q.update.run(+p.dependencies, +p.security, teamId, id)).changes > 0
|
|
120
|
+
},
|
|
121
|
+
async revokeWorkspaceShares(sessionId, now, teamId, id) {
|
|
122
|
+
if (!await q.manager.get(teamId, sessionId, now)) return false
|
|
123
|
+
const result = await q.remove.run(teamId, id ?? null, id ?? null)
|
|
124
|
+
return id === undefined || result.changes > 0
|
|
125
|
+
},
|
|
126
|
+
async getWorkspaceShare(tokenHash) {
|
|
127
|
+
const grant = await q.share.get(tokenHash) as { id: string; slug: string; name: string; dependencies: number; security: number } | undefined
|
|
128
|
+
if (!grant) return null
|
|
129
|
+
const { dependencies, security, ...team } = grant
|
|
130
|
+
const permissions = { dependencies: dependencies === 1, security: security === 1 }
|
|
131
|
+
const rows = await q.reports.all(team.id) as { id: string; slug: string; filename: string; byteSize: number; sha256: string; directory: string; github: string }[]
|
|
132
|
+
const bundleRows = await q.bundles.all(team.id) as (ManagedBundle & { repoFullName: string })[]
|
|
133
|
+
return {
|
|
134
|
+
user: { id: `share:${tokenHash}`, login: 'public', name: 'Public workspace', avatarUrl: null, role: 'view' },
|
|
135
|
+
teamId: team.id,
|
|
136
|
+
permissions,
|
|
137
|
+
repositories: await q.repositories.all(team.id) as WorkspaceShareSnapshot['repositories'],
|
|
138
|
+
reports: rows.map(row => ({ id: row.id, filename: row.filename, byteSize: row.byteSize, sha256: row.sha256,
|
|
139
|
+
repo: { github: row.github, directory: row.directory }, permissions })),
|
|
140
|
+
team: { ...team,
|
|
141
|
+
reports: rows.map(row => ({ id: row.id, slug: row.slug, filename: row.filename, repoFullName: row.github, repoDirectory: row.directory,
|
|
142
|
+
cacheKey: JSON.stringify([row.sha256, row.github, row.directory, row.filename, permissions]) })),
|
|
143
|
+
bundles: bundleRows.map(row => ({ id: row.id, slug: row.slug, integrity: row.integrity,
|
|
144
|
+
filename: row.filename, kind: row.kind, byteSize: row.byteSize, repoId: row.repoId!, repoDirectory: row.repoDirectory, repoFullName: row.repoFullName })),
|
|
145
|
+
},
|
|
146
|
+
bundles: bundleRows,
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
package/server.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
|
+
import type { Server } from 'node:http'
|
|
3
|
+
import { startServer } from './server-common/standalone.ts'
|
|
4
|
+
|
|
5
|
+
const modes = ['e2e', 'managed', 'managed-e2e', 'e2e-managed'] as const
|
|
6
|
+
export type ServerMode = typeof modes[number]
|
|
7
|
+
|
|
8
|
+
function parseMode(mode: string): ServerMode {
|
|
9
|
+
for (const candidate of modes) if (mode === candidate) return candidate
|
|
10
|
+
throw new Error(`Unknown mode: ${mode}. Expected ${modes.join(', ')}.`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Assemble the selected server without binding a port. Hosts can reuse its
|
|
14
|
+
// request/upgrade listeners or call listen() themselves.
|
|
15
|
+
export async function init(mode: ServerMode = 'e2e'): Promise<Server> {
|
|
16
|
+
const selected = parseMode(mode)
|
|
17
|
+
if (selected === 'e2e') {
|
|
18
|
+
const { createE2eApp } = await import('./server-e2e/app.ts')
|
|
19
|
+
return (await createE2eApp()).httpServer
|
|
20
|
+
}
|
|
21
|
+
if (selected === 'managed') {
|
|
22
|
+
const managed = await import('./server-managed/index.ts')
|
|
23
|
+
return managed.init()
|
|
24
|
+
}
|
|
25
|
+
const combined = await import('./server-managed/combined.ts')
|
|
26
|
+
return combined.init(selected === 'managed-e2e' ? 'managed+e2e' : 'e2e+managed')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function start(): Promise<void> {
|
|
30
|
+
const { values } = parseArgs({ options: {
|
|
31
|
+
mode: { type: 'string', default: 'e2e' },
|
|
32
|
+
help: { type: 'boolean', short: 'h' },
|
|
33
|
+
} })
|
|
34
|
+
const mode = parseMode(values.mode)
|
|
35
|
+
if (values.help) {
|
|
36
|
+
console.log(`Usage: node cli.js --mode <${modes.join('|')}>
|
|
37
|
+
Defaults to e2e. Combined modes share HOST/PORT; the first mode is the client default.
|
|
38
|
+
Managed modes require GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET and OAUTH_CALLBACK_URL.
|
|
39
|
+
Combined storage: DATABASE_URL for a shared Neon DB, or E2E_DATABASE_URL and
|
|
40
|
+
MANAGED_DATABASE_URL for separate Neon DBs. Do not mix shared and per-mode URLs.
|
|
41
|
+
Without URLs: SQLite at DB_PATH for e2e and MANAGED_DB_PATH for managed.
|
|
42
|
+
Combined mode does not support mixing Neon and SQLite.`)
|
|
43
|
+
} else {
|
|
44
|
+
const config = mode === 'managed'
|
|
45
|
+
? (await import('./server-managed/config.ts')).loadManagedConfig()
|
|
46
|
+
: (await import('./server-e2e/config.ts')).loadConfig()
|
|
47
|
+
const server = await init(mode)
|
|
48
|
+
startServer(server, config)
|
|
49
|
+
}
|
|
50
|
+
}
|