@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.51
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/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +3 -0
- package/common/managed/finding-detail.js +66 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +7 -0
- package/common/managed/npm-packages.js +51 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +359 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +6 -0
- package/common/managed/team-app.js +47 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2K6UN3PC.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7JBSSRD7.js +3 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FMXWXDR4.js +36 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/chunk-IPAH4LKF.js +232 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -0
- package/out/chunk-KOGNQDSJ.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-VDRXHIHN.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WRAJVD35.js +1 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +364 -59
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -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-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- 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 +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- 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 +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +23 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +76 -0
- package/server-managed/comparison.ts +52 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +468 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +143 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +98 -0
- package/server-managed/dependency-reviews.ts +364 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +376 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +136 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5503 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +152 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +225 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-patch.ts +325 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +351 -0
- package/server-managed/pull-dependency-reviews.ts +334 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +448 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +166 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +212 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +319 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
//
|
|
9
9
|
// One route, POSTs only:
|
|
10
10
|
//
|
|
11
|
-
// POST /api/sync/sse
|
|
11
|
+
// POST /api/sync/sse
|
|
12
12
|
// Request body:
|
|
13
|
-
// {
|
|
13
|
+
// { id?: string, — session continuation token
|
|
14
|
+
// password?: string, — cached client password
|
|
14
15
|
// frames?: Array<protocol-frame> — WS-style JSON frames
|
|
15
16
|
// }
|
|
16
17
|
// Response:
|
|
@@ -25,14 +26,23 @@
|
|
|
25
26
|
// carrying the WS protocol's JSON envelopes.
|
|
26
27
|
//
|
|
27
28
|
// Continuation: each POST replaces the previous POST's response as the
|
|
28
|
-
// session's downstream channel. If the
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
29
|
+
// session's downstream channel. If the body's `id` matches a session
|
|
30
|
+
// this replica knows, the session continues (new outbound stream
|
|
31
|
+
// attached, old one end()ed). If the id is unknown (different replica
|
|
32
|
+
// picked up the POST, or session expired), a fresh session with a new
|
|
33
|
+
// id is created and announced via the first `session` event; the
|
|
34
|
+
// client uses the new id on all subsequent POSTs and re-sends its
|
|
35
|
+
// subscribe frames on the next POST (its `frames` carry the signed
|
|
36
|
+
// subscribes — they always do, see client/sync/sse-transport.ts).
|
|
37
|
+
//
|
|
38
|
+
// The id rides the JSON body, NOT the URL: the sid is a live bearer
|
|
39
|
+
// capability (whoever presents it attaches to the session's downstream
|
|
40
|
+
// and inherits its operator-auth flag), and a query-string token leaks
|
|
41
|
+
// into proxy / LB access logs — the same reason the objstore bearer
|
|
42
|
+
// tokens ride the Authorization header, never the URL. A legacy
|
|
43
|
+
// `?id=<sid>` query form is still ACCEPTED (older client bundles sent
|
|
44
|
+
// it; rejecting would churn them through a fresh session per POST),
|
|
45
|
+
// but current clients never emit it.
|
|
36
46
|
//
|
|
37
47
|
// Why POSTs only: a long-lived GET pins the client to one replica via
|
|
38
48
|
// TCP affinity, which would mean POSTs from the same client must be
|
|
@@ -47,7 +57,7 @@ import { type PeerConnectionDeps, setupPeerConnection } from './ws-server.ts'
|
|
|
47
57
|
import { SseSession } from './sse-session.ts'
|
|
48
58
|
import { errMsg, randomId } from './util.ts'
|
|
49
59
|
|
|
50
|
-
// Same prefix the WS upgrade lives under (server/http.ts WS_UPGRADE_PATH
|
|
60
|
+
// Same prefix the WS upgrade lives under (server-e2e/http.ts WS_UPGRADE_PATH
|
|
51
61
|
// = '/api/sync'); subroute keeps the SSE plane sibling to the upgrade
|
|
52
62
|
// path so the same `location` block routes both.
|
|
53
63
|
export const SSE_OPEN_PATH = '/api/sync/sse'
|
|
@@ -75,7 +85,7 @@ export type SseServerDeps = {
|
|
|
75
85
|
// transport into it. Closure over the same handler / hub / objstore
|
|
76
86
|
// / track / debug surface the WS path uses.
|
|
77
87
|
peerDeps: PeerConnectionDeps
|
|
78
|
-
// Shutdown gate. Mirror of the REST + WS branches in server/http.ts:
|
|
88
|
+
// Shutdown gate. Mirror of the REST + WS branches in server-e2e/http.ts:
|
|
79
89
|
// an SSE POST arriving on an existing keep-alive socket after
|
|
80
90
|
// SIGTERM should be rejected, not dispatched against a draining DB.
|
|
81
91
|
isShuttingDown: () => boolean
|
|
@@ -83,7 +93,7 @@ export type SseServerDeps = {
|
|
|
83
93
|
// 503 the open request. Caps the SSE-side equivalent of `wss.clients`.
|
|
84
94
|
maxSessions: number
|
|
85
95
|
// Max body size for one POST. Matches the WS `maxPayload` in
|
|
86
|
-
// server/index.ts so the SSE plane can't accept frames the WS plane
|
|
96
|
+
// server-e2e/index.ts so the SSE plane can't accept frames the WS plane
|
|
87
97
|
// would reject. The POST body envelope can hold multiple frames so
|
|
88
98
|
// the per-frame budget is the same as the WS plane after the
|
|
89
99
|
// dispatcher splits them.
|
|
@@ -106,8 +116,10 @@ export type SseServer = {
|
|
|
106
116
|
// Inbound POST body. Every field optional — an empty-body POST is a
|
|
107
117
|
// valid "wake the session" probe (the response stream rides on every
|
|
108
118
|
// POST), and a body that carries only `password` or only `frames` is
|
|
109
|
-
// a normal partial update.
|
|
119
|
+
// a normal partial update. `id` is the session continuation token
|
|
120
|
+
// (see the header's "Continuation" note for why it rides the body).
|
|
110
121
|
type SseBody = {
|
|
122
|
+
id?: unknown
|
|
111
123
|
password?: unknown
|
|
112
124
|
frames?: unknown
|
|
113
125
|
}
|
|
@@ -116,12 +128,13 @@ export function installSseServer(deps: SseServerDeps): SseServer {
|
|
|
116
128
|
const { peerDeps, isShuttingDown, maxSessions, maxBodyBytes, debug } = deps
|
|
117
129
|
|
|
118
130
|
// Active SSE sessions, keyed by the random session id `createSession`
|
|
119
|
-
// mints on the first POST that
|
|
120
|
-
// replica doesn't recognise) and that subsequent POSTs echo
|
|
121
|
-
// continue the session. Bounded by
|
|
122
|
-
// POSTs get a 503. POSTs against an
|
|
123
|
-
// mint a fresh session instead, so a
|
|
124
|
-
// require sticky LB routing to
|
|
131
|
+
// mints on the first POST that carries no continuation id (or whose
|
|
132
|
+
// id this replica doesn't recognise) and that subsequent POSTs echo
|
|
133
|
+
// back (body `id` field) to continue the session. Bounded by
|
|
134
|
+
// `maxSessions` — over the cap, new POSTs get a 503. POSTs against an
|
|
135
|
+
// unknown id are NOT 404'd — they mint a fresh session instead, so a
|
|
136
|
+
// multi-replica deployment doesn't require sticky LB routing to
|
|
137
|
+
// recover.
|
|
125
138
|
const sessions = new Map<string, SseSession>()
|
|
126
139
|
|
|
127
140
|
function dropSession(sid: string): void {
|
|
@@ -251,6 +264,12 @@ export function installSseServer(deps: SseServerDeps): SseServer {
|
|
|
251
264
|
return
|
|
252
265
|
}
|
|
253
266
|
}
|
|
267
|
+
// Resolve the continuation token: the JSON body's `id` is the
|
|
268
|
+
// canonical carrier; the `?id=` query form is the legacy
|
|
269
|
+
// fallback for older client bundles (see the header note — a
|
|
270
|
+
// query sid leaks into access logs). Body wins when both are
|
|
271
|
+
// present (current clients only ever send one).
|
|
272
|
+
const sid = parseSid(body.id) ?? sidFromUrl
|
|
254
273
|
// Look up the session by id (if the client sent one). If found
|
|
255
274
|
// and alive, attach the new response and dispatch. If not (or
|
|
256
275
|
// session is closed), mint a fresh one — the response stream
|
|
@@ -265,8 +284,8 @@ export function installSseServer(deps: SseServerDeps): SseServer {
|
|
|
265
284
|
// through to createSession with `res` still header-virgin, so
|
|
266
285
|
// the new session can writeSseHeaders without ERR_HTTP_HEADERS_SENT.
|
|
267
286
|
let session: SseSession | null = null
|
|
268
|
-
if (
|
|
269
|
-
const existing = sessions.get(
|
|
287
|
+
if (sid) {
|
|
288
|
+
const existing = sessions.get(sid)
|
|
270
289
|
if (existing && existing.readyState === existing.OPEN && existing.attachResponse(res)) {
|
|
271
290
|
writeSseHeaders(res)
|
|
272
291
|
session = existing
|
|
@@ -333,25 +352,33 @@ export function installSseServer(deps: SseServerDeps): SseServer {
|
|
|
333
352
|
return { handle, sessions: () => sessions.values(), keepaliveTimer }
|
|
334
353
|
}
|
|
335
354
|
|
|
336
|
-
//
|
|
355
|
+
// Shape gate for a continuation token from either carrier (body `id`
|
|
356
|
+
// field or the legacy `?id=` query). The {1,64} bound is a
|
|
357
|
+
// deliberately lenient sanity gate: anything outside the base64url
|
|
358
|
+
// alphabet is rejected; an unrecognised sid just gets a fresh session
|
|
359
|
+
// from createSession (no failure mode), and the wide length window
|
|
360
|
+
// keeps a future randomId-length change from silently breaking old
|
|
361
|
+
// clients that round-trip a longer/shorter token. Returns null on
|
|
362
|
+
// missing / non-string / malformed.
|
|
363
|
+
function parseSid(v: unknown): string | null {
|
|
364
|
+
if (typeof v !== 'string') return null
|
|
365
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/u.test(v)) return null
|
|
366
|
+
return v
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Bare-bones query parse for the LEGACY `id=<base64url>` carrier
|
|
370
|
+
// (older client bundles; current clients send the sid in the POST
|
|
371
|
+
// body — see the header's "Continuation" note). Avoids URLSearchParams
|
|
337
372
|
// (which decodes percent-escapes) — `randomId()` mints a 22-char
|
|
338
373
|
// base64url string echoed back unchanged, so no escapes are possible
|
|
339
|
-
// on the legitimate path.
|
|
340
|
-
// sanity gate: anything outside the base64url alphabet is rejected; an
|
|
341
|
-
// unrecognised sid just gets a fresh session from createSession (no
|
|
342
|
-
// failure mode), and the wide length window keeps a future
|
|
343
|
-
// randomId-length change from silently breaking old clients that
|
|
344
|
-
// round-trip a longer/shorter token. Returns null on missing /
|
|
345
|
-
// malformed.
|
|
374
|
+
// on the legitimate path.
|
|
346
375
|
function parseSidQuery(query: string | undefined): string | null {
|
|
347
376
|
if (typeof query !== 'string') return null
|
|
348
377
|
for (const part of query.split('&')) {
|
|
349
378
|
const eq = part.indexOf('=')
|
|
350
379
|
if (eq <= 0) continue
|
|
351
380
|
if (part.slice(0, eq) !== 'id') continue
|
|
352
|
-
|
|
353
|
-
if (!/^[A-Za-z0-9_-]{1,64}$/u.test(v)) return null
|
|
354
|
-
return v
|
|
381
|
+
return parseSid(part.slice(eq + 1))
|
|
355
382
|
}
|
|
356
383
|
return null
|
|
357
384
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
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.
|
|
11
11
|
//
|
|
12
12
|
// Compression: every entry whose extension is in COMPRESSIBLE gets
|
|
13
13
|
// pre-computed brotli + gzip at boot. The handler picks brotli over
|
|
@@ -38,6 +38,7 @@ import { readFileSync, readdirSync } from 'node:fs'
|
|
|
38
38
|
import { createHash } from 'node:crypto'
|
|
39
39
|
import { brotliCompressSync, gzipSync, constants as zlibConstants } from 'node:zlib'
|
|
40
40
|
import { extname, join } from 'node:path'
|
|
41
|
+
import { scanServerHtml } from '../server-common/scan-config.ts'
|
|
41
42
|
|
|
42
43
|
// MIME types for extensions the production UI bundle currently
|
|
43
44
|
// emits. Anything else falls through to `application/octet-stream`
|
|
@@ -64,15 +65,24 @@ const COMPRESSIBLE = new Set(['.html', '.css', '.js', '.svg', '.webmanifest'])
|
|
|
64
65
|
// navigated `image/svg+xml` icon into something script-executable.
|
|
65
66
|
// `no-referrer` keeps URLs of this (potentially sensitive) viewer out
|
|
66
67
|
// of outbound Referer headers, and `same-origin` CORP stops other
|
|
67
|
-
// origins embedding these bytes.
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
68
|
+
// origins embedding these bytes. COOP `same-origin` severs the
|
|
69
|
+
// window.opener link to any cross-origin opener/popup (so an external
|
|
70
|
+
// `target=_blank` GitHub/Claude link can't reach back into this context),
|
|
71
|
+
// and COEP `require-corp` bars the document from loading any cross-origin
|
|
72
|
+
// subresource that doesn't opt in via CORP/CORS. Together they also make
|
|
73
|
+
// the page cross-origin-isolated. Safe here because the bundle is fully
|
|
74
|
+
// same-origin (no external scripts/images/fonts/iframes) — switch COEP to
|
|
75
|
+
// `credentialless` if a cross-origin resource is ever added. CSP is
|
|
76
|
+
// deliberately NOT here: the HTML documents carry their own per-page
|
|
77
|
+
// `<meta http-equiv>` policy (and the three pages differ), so a blanket
|
|
78
|
+
// header CSP would just AND against the meta one and risk breaking a page
|
|
79
|
+
// — non-document assets get their own flat CSP via `StaticEntry.csp`.
|
|
72
80
|
const SECURITY_HEADERS = {
|
|
73
81
|
'x-content-type-options': 'nosniff',
|
|
74
82
|
'referrer-policy': 'no-referrer',
|
|
75
83
|
'cross-origin-resource-policy': 'same-origin',
|
|
84
|
+
'cross-origin-opener-policy': 'same-origin',
|
|
85
|
+
'cross-origin-embedder-policy': 'require-corp',
|
|
76
86
|
} as const
|
|
77
87
|
|
|
78
88
|
type StaticEntry = {
|
|
@@ -93,8 +103,10 @@ export type StaticHandler = (req: HttpRequest, res: ServerResponse) => boolean
|
|
|
93
103
|
// caller falls through to its next route. Missing `staticDir`
|
|
94
104
|
// (pre-build case) logs a warning and returns a handler that always
|
|
95
105
|
// answers false; the API/WS planes are unaffected.
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
type StaticOptions = { transformIndex?: (html: string) => string, indexOnly?: boolean }
|
|
107
|
+
|
|
108
|
+
export function loadStatic(staticDir: string, deepviewScanServer: string | null = null, options: StaticOptions = {}): StaticHandler {
|
|
109
|
+
const files = readStaticFiles(staticDir, deepviewScanServer, options)
|
|
98
110
|
return function handleStatic(req: HttpRequest, res: ServerResponse): boolean {
|
|
99
111
|
if (req.method !== 'GET' && req.method !== 'HEAD') return false
|
|
100
112
|
if (typeof req.url !== 'string') return false
|
|
@@ -142,7 +154,7 @@ export function loadStatic(staticDir: string): StaticHandler {
|
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
function readStaticFiles(staticDir: string): ReadonlyMap<string, StaticEntry> {
|
|
157
|
+
function readStaticFiles(staticDir: string, deepviewScanServer: string | null, options: StaticOptions): ReadonlyMap<string, StaticEntry> {
|
|
146
158
|
const files = new Map<string, StaticEntry>()
|
|
147
159
|
let entries
|
|
148
160
|
try { entries = readdirSync(staticDir, { withFileTypes: true }) } catch (err) {
|
|
@@ -156,18 +168,17 @@ function readStaticFiles(staticDir: string): ReadonlyMap<string, StaticEntry> {
|
|
|
156
168
|
throw err
|
|
157
169
|
}
|
|
158
170
|
for (const entry of entries) {
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!entry.isFile()) continue
|
|
163
|
-
files.set(entry.name, buildEntry(staticDir, entry.name))
|
|
171
|
+
// Exclude subdirectories, symlinks and non-file entries.
|
|
172
|
+
if (!entry.isFile() || (options.indexOnly && entry.name !== 'index.html')) continue
|
|
173
|
+
files.set(entry.name, buildEntry(staticDir, entry.name, deepviewScanServer, options.transformIndex))
|
|
164
174
|
}
|
|
165
175
|
return files
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
function buildEntry(staticDir: string, name: string): StaticEntry {
|
|
178
|
+
function buildEntry(staticDir: string, name: string, deepviewScanServer: string | null = null, transformIndex: (html: string) => string = html => html): StaticEntry {
|
|
169
179
|
const ext = extname(name)
|
|
170
|
-
const
|
|
180
|
+
const source = readFileSync(join(staticDir, name))
|
|
181
|
+
const raw = name === 'index.html' ? Buffer.from(transformIndex(scanServerHtml(source.toString('utf8'), deepviewScanServer))) : source
|
|
171
182
|
const type = CONTENT_TYPE[ext] ?? 'application/octet-stream'
|
|
172
183
|
// HTML: lift `<link rel="(module)preload" …>` into a Link header and
|
|
173
184
|
// drop the tags from the served body. ETag + compression run against
|
|
@@ -183,13 +194,12 @@ function buildEntry(staticDir: string, name: string): StaticEntry {
|
|
|
183
194
|
// collision-resistance against the ~tens of files we load.
|
|
184
195
|
const etag = `"${createHash('sha256').update(identity).digest('hex').slice(0, 32)}"`
|
|
185
196
|
const isCompressible = COMPRESSIBLE.has(ext)
|
|
186
|
-
// Brotli quality
|
|
187
|
-
//
|
|
188
|
-
// request thereafter. text mode tells the encoder we're working
|
|
197
|
+
// Brotli quality 9 limits boot-time compression work, including on
|
|
198
|
+
// serverless cold starts. Text mode tells the encoder we're working
|
|
189
199
|
// on UTF-8 text (every COMPRESSIBLE entry is text-shaped).
|
|
190
200
|
const br = isCompressible ? brotliCompressSync(identity, {
|
|
191
201
|
params: {
|
|
192
|
-
[zlibConstants.BROTLI_PARAM_QUALITY]:
|
|
202
|
+
[zlibConstants.BROTLI_PARAM_QUALITY]: 9,
|
|
193
203
|
[zlibConstants.BROTLI_PARAM_MODE]: zlibConstants.BROTLI_MODE_TEXT,
|
|
194
204
|
},
|
|
195
205
|
}) : null
|
|
@@ -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
|
|
@@ -101,7 +104,7 @@ export type SyncHandlersDeps = {
|
|
|
101
104
|
export type SyncHandlers = {
|
|
102
105
|
handleSave: (socket: WebSocket, msg: SaveMsg) => Promise<void>
|
|
103
106
|
// Session-independent REST save plane (`POST /api/sync/save`), wired into
|
|
104
|
-
// the HTTP dispatcher in server/http.ts. Runs the same pipeline as
|
|
107
|
+
// the HTTP dispatcher in server-e2e/http.ts. Runs the same pipeline as
|
|
105
108
|
// `handleSave` and renders the outcome as a JSON response.
|
|
106
109
|
handleSaveRest: (req: IncomingMessage, res: ServerResponse) => Promise<void>
|
|
107
110
|
handleSubscribe: (socket: WebSocket, msg: SubscribeMsg) => Promise<void>
|
|
@@ -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
|
|
@@ -238,6 +244,14 @@ export function createSyncHandlers(deps: SyncHandlersDeps): SyncHandlers {
|
|
|
238
244
|
// chain in the 409 body. Either way the catch-up clears the client's
|
|
239
245
|
// pending and the error is a no-op on the now-missing pending (a
|
|
240
246
|
// recoverable race — client rebases + re-saves).
|
|
247
|
+
//
|
|
248
|
+
// The catch-up CAN be empty: a client holding a base from a chain
|
|
249
|
+
// this deployment no longer has (wiped / moved DB, SQLite→Neon
|
|
250
|
+
// migration — head=null, no keyframe, no rows) gets `revisions: []`.
|
|
251
|
+
// The client detects that shape (pending survives the catch-up) and
|
|
252
|
+
// answers with a full-state push re-anchored at base=null, which
|
|
253
|
+
// commits as the new chain root — see the client's
|
|
254
|
+
// `handleSaveError` stale-base branch.
|
|
241
255
|
const revisions = chainForWire(await chainFrom(handle, tag, baseNorm))
|
|
242
256
|
if (debug) console.log(`save (stale base ${baseNorm} vs head ${commit.head}) → chain ${revisions.length}`)
|
|
243
257
|
return { kind: 'stale-base', base: baseNorm, revisions }
|
|
@@ -291,7 +305,7 @@ export function createSyncHandlers(deps: SyncHandlersDeps): SyncHandlers {
|
|
|
291
305
|
// maps the outcome to a JSON + HTTP status the client switches on. SSE-mode
|
|
292
306
|
// clients POST here so a save doesn't take over their event-stream; a 401
|
|
293
307
|
// routes them to the in-band frame (which runs the operator auth flow).
|
|
294
|
-
// Mounted + gated (same-origin, shutdown, idle-timeout) in server/http.ts.
|
|
308
|
+
// Mounted + gated (same-origin, shutdown, idle-timeout) in server-e2e/http.ts.
|
|
295
309
|
async function handleSaveRest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
296
310
|
const body = await readSaveBody(req)
|
|
297
311
|
if (!body || typeof body !== 'object') { respondJson(res, 400, { reason: 'bad-request' }); return }
|
|
@@ -340,34 +354,54 @@ export function createSyncHandlers(deps: SyncHandlersDeps): SyncHandlers {
|
|
|
340
354
|
return
|
|
341
355
|
}
|
|
342
356
|
const tag = msg.workspaceTag
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
}
|
|
371
405
|
}
|
|
372
406
|
|
|
373
407
|
return { handleSave, handleSaveRest, handleSubscribe, sendSaveError }
|
|
@@ -16,6 +16,7 @@ import { Buffer } from 'node:buffer'
|
|
|
16
16
|
import type { IncomingMessage as HttpRequest } from 'node:http'
|
|
17
17
|
import { decodeUtf8 } from '../common/utf8.js'
|
|
18
18
|
import type { SaveErrorReason } from '../common/save-error-reason.ts'
|
|
19
|
+
import type { ServerInfo } from '../common/server-info.ts'
|
|
19
20
|
import { Peer, type PeerRegistry } from './peer.ts'
|
|
20
21
|
import { errMsg, errStack, randomId } from './util.ts'
|
|
21
22
|
import type { SaveMsg, SubscribeMsg } from './sign.ts'
|
|
@@ -36,6 +37,9 @@ type IncomingMessage = {
|
|
|
36
37
|
// SSE path doesn't need them.
|
|
37
38
|
export type PeerConnectionDeps = {
|
|
38
39
|
peers: PeerRegistry
|
|
40
|
+
// The mode this server advertises, emitted as the first `server-info`
|
|
41
|
+
// frame on every connection (see setupPeerConnection).
|
|
42
|
+
serverInfo: ServerInfo
|
|
39
43
|
send: (socket: WebSocket, msg: object) => void
|
|
40
44
|
unsubscribeAll: (socket: WebSocket) => void
|
|
41
45
|
handleSave: (socket: WebSocket, msg: SaveMsg) => Promise<void>
|
|
@@ -60,7 +64,7 @@ export type WsServerDeps = PeerConnectionDeps & {
|
|
|
60
64
|
// `as unknown as WebSocket`; only the subset both expose is used.
|
|
61
65
|
export function setupPeerConnection(socket: WebSocket, req: HttpRequest, deps: PeerConnectionDeps): void {
|
|
62
66
|
const {
|
|
63
|
-
peers, send, unsubscribeAll, handleSave, handleSubscribe, handleAuthenticate,
|
|
67
|
+
peers, serverInfo, send, unsubscribeAll, handleSave, handleSubscribe, handleAuthenticate,
|
|
64
68
|
sendSaveError, objstore, track, isShuttingDown, maxInflightPerSocket, debug,
|
|
65
69
|
} = deps
|
|
66
70
|
if (debug) console.log(`connect from ${req.socket.remoteAddress}`)
|
|
@@ -71,13 +75,20 @@ export function setupPeerConnection(socket: WebSocket, req: HttpRequest, deps: P
|
|
|
71
75
|
const peer = new Peer(randomId())
|
|
72
76
|
peers.set(socket, peer)
|
|
73
77
|
socket.on('pong', () => { peer.alive = true })
|
|
74
|
-
// Issue the per-connection challenge nonce
|
|
75
|
-
// send anything that needs it
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
78
|
+
// Issue the per-connection challenge nonce FIRST — before the client can
|
|
79
|
+
// send anything that needs it, AND as frame #0 so clients/tests that
|
|
80
|
+
// positionally read the first frame as the challenge keep working. The
|
|
81
|
+
// client signs it into every `workspace-subscribe` (see canonicalSubscribe
|
|
82
|
+
// in server-e2e/sign.ts); a captured subscribe frame can't be replayed from
|
|
83
|
+
// a different connection because that connection's nonce differs and the
|
|
79
84
|
// signature won't verify against the new canonical bytes. Round-9 H2.
|
|
80
85
|
send(socket, { type: 'challenge', nonce: peer.challenge })
|
|
86
|
+
// Then advertise the sync protocol (the mode probe), right after the
|
|
87
|
+
// challenge — the WS plane and the SSE fallback share this path. A client
|
|
88
|
+
// uses it to detect the mode, cache it, and refuse a cross-mode switch.
|
|
89
|
+
// Unauthenticated + mode-agnostic; an older client ignores it (predicate
|
|
90
|
+
// readers skip it; the workspaceTag demux drops it).
|
|
91
|
+
send(socket, { type: 'server-info', ...serverInfo })
|
|
81
92
|
// Per-socket handlers are DELIBERATELY NOT serialized (vs the
|
|
82
93
|
// client-side `messageQueue = messageQueue.then(...)` Promise
|
|
83
94
|
// chain inside `client/triage-sync.ts:onTransportMessage`).
|
|
@@ -86,10 +97,10 @@ export function setupPeerConnection(socket: WebSocket, req: HttpRequest, deps: P
|
|
|
86
97
|
// boundaries inside the handlers. Per-resource correctness needs
|
|
87
98
|
// no in-process lock: `commitRevision` resolves concurrent saves
|
|
88
99
|
// via its single gated INSERT (one snapshot + the
|
|
89
|
-
// `UNIQUE(workspace_tag, seq)` PK — see `server/db.ts`), and the
|
|
100
|
+
// `UNIQUE(workspace_tag, seq)` PK — see `server-e2e/db.ts`), and the
|
|
90
101
|
// objstore handlers (`commitPut` / `beginPut` / `deleteObject`)
|
|
91
102
|
// via the version compare-and-set + content-addressing (see
|
|
92
|
-
// `server/objstore/store.ts`), backed by post-await
|
|
103
|
+
// `server-e2e/objstore/store.ts`), backed by post-await
|
|
93
104
|
// `readyState === OPEN` rechecks in every objstore handler. The
|
|
94
105
|
// unbounded fan-out is capped by `maxInflightPerSocket` (see
|
|
95
106
|
// also the `'busy'` NACK at the cap below). Concurrent dispatch
|
|
@@ -129,7 +140,7 @@ export function setupPeerConnection(socket: WebSocket, req: HttpRequest, deps: P
|
|
|
129
140
|
// shedding load. Pings go through a fast inline `send(pong)`
|
|
130
141
|
// path BELOW that doesn't bump the per-socket inflight counter,
|
|
131
142
|
// so a ping-spam at the cap can't outrun the gate. Transport
|
|
132
|
-
// audit `server/index.ts:590` + post-#58 audit follow-up.
|
|
143
|
+
// audit `server-e2e/index.ts:590` + post-#58 audit follow-up.
|
|
133
144
|
if (parsed.type === 'ping') {
|
|
134
145
|
send(socket, { type: 'pong' })
|
|
135
146
|
return
|