@powerhousedao/switchboard 6.2.2-dev.4 → 6.2.2-dev.40
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/.tsbuild/test/attachment-reference-read-model.test.d.ts +2 -0
- package/.tsbuild/test/attachment-reference-read-model.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/download-target.test.d.ts +2 -0
- package/.tsbuild/test/attachments/download-target.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +225 -0
- package/dist/index.mjs +1 -1
- package/dist/{server-Cgud_ONM.mjs → server-BLXBKOSF.mjs} +243 -23
- package/dist/server-BLXBKOSF.mjs.map +1 -0
- package/dist/server.d.mts +5 -2
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/package.json +12 -12
- package/test/attachment-reference-read-model.test.ts +425 -0
- package/test/attachments/auth.test.ts +138 -8
- package/test/attachments/download-target.test.ts +445 -0
- package/test/attachments/index.test.ts +37 -2
- package/test/attachments/routes-integration.test.ts +98 -1
- package/dist/server-Cgud_ONM.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,228 @@
|
|
|
1
|
+
## 6.2.2-dev.40 (2026-08-08)
|
|
2
|
+
|
|
3
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
4
|
+
|
|
5
|
+
## 6.2.2-dev.39 (2026-08-07)
|
|
6
|
+
|
|
7
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
8
|
+
|
|
9
|
+
## 6.2.2-dev.38 (2026-08-06)
|
|
10
|
+
|
|
11
|
+
### 🚀 Features
|
|
12
|
+
|
|
13
|
+
- added recipe e2e tests to the suite ([88cd1a55c](https://github.com/powerhouse-inc/powerhouse/commit/88cd1a55c))
|
|
14
|
+
- auth phase 4 ([28aa9ad6a](https://github.com/powerhouse-inc/powerhouse/commit/28aa9ad6a))
|
|
15
|
+
|
|
16
|
+
### 🩹 Fixes
|
|
17
|
+
|
|
18
|
+
- stage llm files from pre-commit hook ([0af2ab705](https://github.com/powerhouse-inc/powerhouse/commit/0af2ab705))
|
|
19
|
+
- **reactor:** point preflight:auth at a real store ([c1315a9f9](https://github.com/powerhouse-inc/powerhouse/commit/c1315a9f9))
|
|
20
|
+
- **reactor:** filter initialState and keep auth in a scoped subscription read ([77fee70b4](https://github.com/powerhouse-inc/powerhouse/commit/77fee70b4))
|
|
21
|
+
- **reactor:** degrade the poll query against a remote on the older schema ([0c3aefe65](https://github.com/powerhouse-inc/powerhouse/commit/0c3aefe65))
|
|
22
|
+
- **shared:** stop a state snapshot from installing or replacing a policy ([f947e562e](https://github.com/powerhouse-inc/powerhouse/commit/f947e562e))
|
|
23
|
+
- **shared:** make auth-administration retention a reachability rule ([3db42daac](https://github.com/powerhouse-inc/powerhouse/commit/3db42daac))
|
|
24
|
+
|
|
25
|
+
### ❤️ Thank You
|
|
26
|
+
|
|
27
|
+
- Benjamin Jordan
|
|
28
|
+
- Claude Fable 5
|
|
29
|
+
|
|
30
|
+
## 6.2.2-dev.37 (2026-08-05)
|
|
31
|
+
|
|
32
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
33
|
+
|
|
34
|
+
## 6.2.2-dev.36 (2026-08-04)
|
|
35
|
+
|
|
36
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
37
|
+
|
|
38
|
+
## 6.2.2-dev.35 (2026-08-03)
|
|
39
|
+
|
|
40
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
41
|
+
|
|
42
|
+
## 6.2.2-dev.34 (2026-08-02)
|
|
43
|
+
|
|
44
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
45
|
+
|
|
46
|
+
## 6.2.2-dev.33 (2026-08-01)
|
|
47
|
+
|
|
48
|
+
### 🩹 Fixes
|
|
49
|
+
|
|
50
|
+
- **reactor:** fixing a few defects with document scope reshuffle specifics ([7546b9ff2](https://github.com/powerhouse-inc/powerhouse/commit/7546b9ff2))
|
|
51
|
+
|
|
52
|
+
### ❤️ Thank You
|
|
53
|
+
|
|
54
|
+
- Benjamin Jordan
|
|
55
|
+
|
|
56
|
+
## 6.2.2-dev.32 (2026-07-31)
|
|
57
|
+
|
|
58
|
+
### 🚀 Features
|
|
59
|
+
|
|
60
|
+
- **reactor:** feature flag guards and spec updates to make plan for guarded reshuffle edge cases ([20b0fd6be](https://github.com/powerhouse-inc/powerhouse/commit/20b0fd6be))
|
|
61
|
+
|
|
62
|
+
### 🩹 Fixes
|
|
63
|
+
|
|
64
|
+
- **reactor:** fix convergence in the case we deny an operation that has a skip ([21cbb3ecb](https://github.com/powerhouse-inc/powerhouse/commit/21cbb3ecb))
|
|
65
|
+
- auth-scope spec updates, assert ordered streams, and fix some busted default protocol versions ([97c5a37b5](https://github.com/powerhouse-inc/powerhouse/commit/97c5a37b5))
|
|
66
|
+
|
|
67
|
+
### ❤️ Thank You
|
|
68
|
+
|
|
69
|
+
- Benjamin Jordan
|
|
70
|
+
|
|
71
|
+
## 6.2.2-dev.31 (2026-07-31)
|
|
72
|
+
|
|
73
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
74
|
+
|
|
75
|
+
## 6.2.2-dev.30 (2026-07-31)
|
|
76
|
+
|
|
77
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
78
|
+
|
|
79
|
+
## 6.2.2-dev.29 (2026-07-31)
|
|
80
|
+
|
|
81
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
82
|
+
|
|
83
|
+
## 6.2.2-dev.28 (2026-07-30)
|
|
84
|
+
|
|
85
|
+
### 🚀 Features
|
|
86
|
+
|
|
87
|
+
- **reactor-browser:** add IReactorBrowserClient and a light GraphQL client ([#2893](https://github.com/powerhouse-inc/powerhouse/pull/2893))
|
|
88
|
+
- **reactor:** plugging in the first decision model ([27e9b2772](https://github.com/powerhouse-inc/powerhouse/commit/27e9b2772))
|
|
89
|
+
|
|
90
|
+
### ❤️ Thank You
|
|
91
|
+
|
|
92
|
+
- Benjamin Jordan
|
|
93
|
+
- Guillermo Puente Sandoval @gpuente
|
|
94
|
+
|
|
95
|
+
## 6.2.2-dev.27 (2026-07-30)
|
|
96
|
+
|
|
97
|
+
### 🩹 Fixes
|
|
98
|
+
|
|
99
|
+
- **connect:** pin @privy-io/react-auth to 3.35.0 ([909352ce3](https://github.com/powerhouse-inc/powerhouse/commit/909352ce3))
|
|
100
|
+
|
|
101
|
+
### ❤️ Thank You
|
|
102
|
+
|
|
103
|
+
- acaldas
|
|
104
|
+
|
|
105
|
+
## 6.2.2-dev.26 (2026-07-30)
|
|
106
|
+
|
|
107
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
108
|
+
|
|
109
|
+
## 6.2.2-dev.25 (2026-07-29)
|
|
110
|
+
|
|
111
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
112
|
+
|
|
113
|
+
## 6.2.2-dev.24 (2026-07-29)
|
|
114
|
+
|
|
115
|
+
### 🩹 Fixes
|
|
116
|
+
|
|
117
|
+
- **reactor:** fix an issue exacerbated by auth where snapshots were ordered by insertion not revision ([4aa32fe1b](https://github.com/powerhouse-inc/powerhouse/commit/4aa32fe1b))
|
|
118
|
+
|
|
119
|
+
### ❤️ Thank You
|
|
120
|
+
|
|
121
|
+
- Benjamin Jordan
|
|
122
|
+
|
|
123
|
+
## 6.2.2-dev.23 (2026-07-28)
|
|
124
|
+
|
|
125
|
+
### 🩹 Fixes
|
|
126
|
+
|
|
127
|
+
- **deps:** pin qr to 0.5.5 so RainbowKit can render the WalletConnect QR ([5343f9178](https://github.com/powerhouse-inc/powerhouse/commit/5343f9178))
|
|
128
|
+
|
|
129
|
+
### ❤️ Thank You
|
|
130
|
+
|
|
131
|
+
- acaldas
|
|
132
|
+
|
|
133
|
+
## 6.2.2-dev.22 (2026-07-28)
|
|
134
|
+
|
|
135
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
136
|
+
|
|
137
|
+
## 6.2.2-dev.21 (2026-07-27)
|
|
138
|
+
|
|
139
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
140
|
+
|
|
141
|
+
## 6.2.2-dev.20 (2026-07-27)
|
|
142
|
+
|
|
143
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
144
|
+
|
|
145
|
+
## 6.2.2-dev.19 (2026-07-27)
|
|
146
|
+
|
|
147
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
148
|
+
|
|
149
|
+
## 6.2.2-dev.18 (2026-07-27)
|
|
150
|
+
|
|
151
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
152
|
+
|
|
153
|
+
## 6.2.2-dev.17 (2026-07-26)
|
|
154
|
+
|
|
155
|
+
### 🚀 Features
|
|
156
|
+
|
|
157
|
+
- **attachments:** S3-compatible storage, direct transfers, and document-derived authorization ([#2884](https://github.com/powerhouse-inc/powerhouse/pull/2884))
|
|
158
|
+
|
|
159
|
+
### ❤️ Thank You
|
|
160
|
+
|
|
161
|
+
- Guillermo Puente Sandoval @gpuente
|
|
162
|
+
|
|
163
|
+
## 6.2.2-dev.16 (2026-07-25)
|
|
164
|
+
|
|
165
|
+
### 🩹 Fixes
|
|
166
|
+
|
|
167
|
+
- code review fixes ([318cbefa9](https://github.com/powerhouse-inc/powerhouse/commit/318cbefa9))
|
|
168
|
+
- version 0 is reserved and only 1 and on is available for setting ([193ac4c56](https://github.com/powerhouse-inc/powerhouse/commit/193ac4c56))
|
|
169
|
+
- resolve a contradiction in group membership ([39845a65c](https://github.com/powerhouse-inc/powerhouse/commit/39845a65c))
|
|
170
|
+
- grant evaluation needs to ignore features that do not exist yet ([0c1d06396](https://github.com/powerhouse-inc/powerhouse/commit/0c1d06396))
|
|
171
|
+
|
|
172
|
+
### ❤️ Thank You
|
|
173
|
+
|
|
174
|
+
- Benjamin Jordan
|
|
175
|
+
|
|
176
|
+
## 6.2.2-dev.15 (2026-07-24)
|
|
177
|
+
|
|
178
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
179
|
+
|
|
180
|
+
## 6.2.2-dev.14 (2026-07-23)
|
|
181
|
+
|
|
182
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
183
|
+
|
|
184
|
+
## 6.2.2-dev.13 (2026-07-23)
|
|
185
|
+
|
|
186
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
187
|
+
|
|
188
|
+
## 6.2.2-dev.12 (2026-07-23)
|
|
189
|
+
|
|
190
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
191
|
+
|
|
192
|
+
## 6.2.2-dev.11 (2026-07-22)
|
|
193
|
+
|
|
194
|
+
### 🚀 Features
|
|
195
|
+
|
|
196
|
+
- **connect:** dynamic packages can contribute to the PWA configuration ([da867e21d](https://github.com/powerhouse-inc/powerhouse/commit/da867e21d))
|
|
197
|
+
|
|
198
|
+
### ❤️ Thank You
|
|
199
|
+
|
|
200
|
+
- Yasiel Cabrera @YasielCabrera
|
|
201
|
+
|
|
202
|
+
## 6.2.2-dev.10 (2026-07-21)
|
|
203
|
+
|
|
204
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
205
|
+
|
|
206
|
+
## 6.2.2-dev.9 (2026-07-20)
|
|
207
|
+
|
|
208
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
209
|
+
|
|
210
|
+
## 6.2.2-dev.8 (2026-07-19)
|
|
211
|
+
|
|
212
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
213
|
+
|
|
214
|
+
## 6.2.2-dev.7 (2026-07-18)
|
|
215
|
+
|
|
216
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
217
|
+
|
|
218
|
+
## 6.2.2-dev.6 (2026-07-17)
|
|
219
|
+
|
|
220
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
221
|
+
|
|
222
|
+
## 6.2.2-dev.5 (2026-07-17)
|
|
223
|
+
|
|
224
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
225
|
+
|
|
1
226
|
## 6.2.2-dev.4 (2026-07-17)
|
|
2
227
|
|
|
3
228
|
### 🚀 Features
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5a65c537-fadb-5705-95c0-4cec44d7b4d2")}catch(e){}}();
|
|
4
|
-
import { a as parseForcePgVersion, r as startSwitchboard } from "./server-
|
|
4
|
+
import { a as parseForcePgVersion, r as startSwitchboard } from "./server-BLXBKOSF.mjs";
|
|
5
5
|
import "./utils-Baw7rThP.mjs";
|
|
6
6
|
import { metrics } from "@opentelemetry/api";
|
|
7
7
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ca6cb919-a51c-59fe-9716-c135c0b04465")}catch(e){}}();
|
|
3
3
|
import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-Baw7rThP.mjs";
|
|
4
4
|
import { register } from "node:module";
|
|
5
5
|
import * as Sentry from "@sentry/node";
|
|
@@ -10,10 +10,10 @@ import { existsSync, promises, realpathSync } from "node:fs";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
|
|
12
12
|
import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
|
|
13
|
-
import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, parseDriveUrl } from "@powerhousedao/reactor";
|
|
13
|
+
import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, parseDriveUrl, supportsLiveReadModelRegistration } from "@powerhousedao/reactor";
|
|
14
14
|
import { HttpPackageLoader, ImportPackageLoader, PackageManagementService, PackagesSubgraph, getUniqueDocumentModels, initializeAndStartAPI } from "@powerhousedao/reactor-api";
|
|
15
15
|
import { httpsHooksPath } from "@powerhousedao/reactor-api/https-hooks";
|
|
16
|
-
import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, HashMismatch, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRemoteAttachmentService } from "@powerhousedao/reactor-attachments";
|
|
16
|
+
import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, AttachmentReferenceReadModel, AttachmentSchemaCompiler, HashMismatch, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRef, createRemoteAttachmentService, parseAttachmentDownloadTarget } from "@powerhousedao/reactor-attachments";
|
|
17
17
|
import { DriveNodeView, NodeProcessor, ReactorDriveClient, createReactorDriveResolvers, reactorDriveDocumentModelModule, reactorDriveSubgraphTypeDefs } from "@powerhousedao/reactor-drive";
|
|
18
18
|
import { Kysely, PostgresDialect } from "kysely";
|
|
19
19
|
import net from "node:net";
|
|
@@ -64,13 +64,20 @@ async function loadPgDump(major) {
|
|
|
64
64
|
}
|
|
65
65
|
//#endregion
|
|
66
66
|
//#region src/attachments/auth.ts
|
|
67
|
+
const ANONYMOUS_ACTOR = {
|
|
68
|
+
user: void 0,
|
|
69
|
+
authEnabled: false
|
|
70
|
+
};
|
|
67
71
|
/**
|
|
68
72
|
* Wrap a Node-style handler so that, when `authService` is provided and auth is
|
|
69
|
-
* enabled, the request must carry a verifiable Bearer token.
|
|
73
|
+
* enabled, the request must carry a verifiable Bearer token. The handler always
|
|
74
|
+
* receives an actor context: the verified bearer user when auth is enabled, or
|
|
75
|
+
* the anonymous context when it is disabled. With `allowAnonymous`, a missing
|
|
76
|
+
* bearer yields an anonymous actor with `authEnabled: true` instead of a 401.
|
|
70
77
|
*/
|
|
71
|
-
function requireAuth(authService, handler) {
|
|
72
|
-
if (!authService) return handler;
|
|
73
|
-
return async (req, res) => {
|
|
78
|
+
function requireAuth(authService, handler, options) {
|
|
79
|
+
if (!authService) return (req, res, body) => handler(req, res, body, ANONYMOUS_ACTOR);
|
|
80
|
+
return async (req, res, body) => {
|
|
74
81
|
let result;
|
|
75
82
|
try {
|
|
76
83
|
result = await authService.verifyBearer(req.headers.authorization);
|
|
@@ -88,25 +95,32 @@ function requireAuth(authService, handler) {
|
|
|
88
95
|
res.end(body);
|
|
89
96
|
return;
|
|
90
97
|
}
|
|
91
|
-
if (result.auth_enabled && !result.user) {
|
|
98
|
+
if (result.auth_enabled && !result.user && !options?.allowAnonymous) {
|
|
92
99
|
res.statusCode = 401;
|
|
93
100
|
res.setHeader("Content-Type", "application/json");
|
|
94
101
|
res.end(JSON.stringify({ error: "Authentication required" }));
|
|
95
102
|
return;
|
|
96
103
|
}
|
|
97
|
-
await handler(req, res
|
|
104
|
+
await handler(req, res, body, {
|
|
105
|
+
user: result.user,
|
|
106
|
+
authEnabled: result.auth_enabled
|
|
107
|
+
});
|
|
98
108
|
};
|
|
99
109
|
}
|
|
100
110
|
//#endregion
|
|
101
111
|
//#region src/attachments/mount-auth.ts
|
|
102
112
|
/**
|
|
103
113
|
* Mount a Node-style attachment route with `requireAuth` applied unconditionally.
|
|
104
|
-
* When `api.authService` is undefined (auth disabled),
|
|
105
|
-
*
|
|
106
|
-
*
|
|
114
|
+
* When `api.authService` is undefined (auth disabled), the handler still runs
|
|
115
|
+
* through `requireAuth` and receives the anonymous actor context — that is the
|
|
116
|
+
* only way to opt out of bearer verification. To register a route without auth
|
|
117
|
+
* wrapping you must call `api.httpAdapter.mountNodeRoute` directly.
|
|
118
|
+
*
|
|
119
|
+
* `allowAnonymous` is reserved for routes whose handlers make a per-document
|
|
120
|
+
* authorization decision themselves; identity-only routes must not use it.
|
|
107
121
|
*/
|
|
108
|
-
function mountAuthenticatedNodeRoute(api, method, path, handler) {
|
|
109
|
-
api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler));
|
|
122
|
+
function mountAuthenticatedNodeRoute(api, method, path, handler, options) {
|
|
123
|
+
api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler, options));
|
|
110
124
|
}
|
|
111
125
|
//#endregion
|
|
112
126
|
//#region src/attachments/routes.ts
|
|
@@ -216,7 +230,8 @@ function makeReserveHandler(attachments) {
|
|
|
216
230
|
sendJson(res, 201, {
|
|
217
231
|
reservationId: upload.reservationId,
|
|
218
232
|
ref: upload.ref,
|
|
219
|
-
expiresAtUtc: upload.expiresAtUtc
|
|
233
|
+
expiresAtUtc: upload.expiresAtUtc,
|
|
234
|
+
...upload.uploadTarget ? { uploadTarget: upload.uploadTarget } : {}
|
|
220
235
|
});
|
|
221
236
|
};
|
|
222
237
|
}
|
|
@@ -227,6 +242,10 @@ function makeUploadHandler(attachments) {
|
|
|
227
242
|
sendError(res, 400, "Missing reservationId");
|
|
228
243
|
return;
|
|
229
244
|
}
|
|
245
|
+
if (attachments.backend?.kind === "s3") {
|
|
246
|
+
sendError(res, 405, "Use the reservation uploadTarget for S3 uploads");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
230
249
|
let reservation;
|
|
231
250
|
try {
|
|
232
251
|
reservation = await attachments.reservations.get(reservationId);
|
|
@@ -373,6 +392,144 @@ function makeDeleteReservationHandler(attachments) {
|
|
|
373
392
|
function extractParam(req, name) {
|
|
374
393
|
return req.params?.[name];
|
|
375
394
|
}
|
|
395
|
+
const MAX_DOCUMENT_ID_LEN = 512;
|
|
396
|
+
const ATTACHMENT_NOT_FOUND_BODY = { error: "Attachment not found" };
|
|
397
|
+
const MAX_DOWNLOAD_TARGET_TTL_SECONDS = 10080 * 60;
|
|
398
|
+
/**
|
|
399
|
+
* Returns the single `documentId` query value, or null when it is missing,
|
|
400
|
+
* duplicated, blank, or oversized. Validation happens before authorization so
|
|
401
|
+
* malformed requests never reach the access service.
|
|
402
|
+
*/
|
|
403
|
+
function extractSingleDocumentId(req) {
|
|
404
|
+
if (!req.url) return null;
|
|
405
|
+
let url;
|
|
406
|
+
try {
|
|
407
|
+
url = new URL(req.url, "http://switchboard.invalid");
|
|
408
|
+
} catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const values = url.searchParams.getAll("documentId");
|
|
412
|
+
if (values.length !== 1) return null;
|
|
413
|
+
const value = values[0];
|
|
414
|
+
if (value.trim().length === 0 || value.length > MAX_DOCUMENT_ID_LEN) return null;
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Returns the requested target TTL in seconds: undefined when absent,
|
|
419
|
+
* "invalid" when malformed (duplicated, non-integer, or non-positive), and
|
|
420
|
+
* otherwise the value clamped to the presigning ceiling.
|
|
421
|
+
*/
|
|
422
|
+
function extractExpiresIn(req) {
|
|
423
|
+
if (!req.url) return void 0;
|
|
424
|
+
let url;
|
|
425
|
+
try {
|
|
426
|
+
url = new URL(req.url, "http://switchboard.invalid");
|
|
427
|
+
} catch {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const values = url.searchParams.getAll("expiresIn");
|
|
431
|
+
if (values.length === 0) return void 0;
|
|
432
|
+
if (values.length > 1) return "invalid";
|
|
433
|
+
const parsed = Number(values[0]);
|
|
434
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return "invalid";
|
|
435
|
+
return Math.min(parsed, MAX_DOWNLOAD_TARGET_TTL_SECONDS);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Base URL of this Switchboard as seen by the caller, used to build
|
|
439
|
+
* filesystem `switchboard` download targets that point back at the existing
|
|
440
|
+
* authenticated byte route.
|
|
441
|
+
*/
|
|
442
|
+
function requestBaseUrl(req) {
|
|
443
|
+
const forwardedProto = req.headers["x-forwarded-proto"];
|
|
444
|
+
const proto = (Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto)?.split(",")[0]?.trim() || (req.socket.encrypted ? "https" : "http");
|
|
445
|
+
const forwardedHost = req.headers["x-forwarded-host"];
|
|
446
|
+
const host = (Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost) ?? req.headers.host;
|
|
447
|
+
if (!host) return null;
|
|
448
|
+
return `${proto}://${host}`;
|
|
449
|
+
}
|
|
450
|
+
function makeDownloadTargetHandler(attachments, attachmentAccess) {
|
|
451
|
+
return async (req, res, _body, actor) => {
|
|
452
|
+
res.setHeader("Cache-Control", "no-store");
|
|
453
|
+
const hash = extractParam(req, "hash");
|
|
454
|
+
if (!hash || !HASH_PATTERN.test(hash)) {
|
|
455
|
+
sendError(res, 400, "Invalid attachment hash");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const documentId = extractSingleDocumentId(req);
|
|
459
|
+
if (documentId === null) {
|
|
460
|
+
sendError(res, 400, "documentId is required exactly once as a non-empty query parameter");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const expiresIn = extractExpiresIn(req);
|
|
464
|
+
if (expiresIn === "invalid") {
|
|
465
|
+
sendError(res, 400, "expiresIn must be a single positive integer number of seconds");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const canonicalHash = hash.toLowerCase();
|
|
469
|
+
let decision;
|
|
470
|
+
try {
|
|
471
|
+
decision = await attachmentAccess.canReadAttachment({
|
|
472
|
+
documentId,
|
|
473
|
+
attachmentRef: createRef(canonicalHash),
|
|
474
|
+
userAddress: actor?.user?.address
|
|
475
|
+
});
|
|
476
|
+
} catch (err) {
|
|
477
|
+
logger$1.error("Attachment access decision failed: @error", err);
|
|
478
|
+
sendError(res, 500, "Internal error");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (decision.kind === "projection-unavailable") {
|
|
482
|
+
sendError(res, 503, "Attachment downloads are temporarily unavailable");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (decision.kind === "denied") {
|
|
486
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
let header;
|
|
490
|
+
try {
|
|
491
|
+
header = await attachments.store.stat(canonicalHash);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
if (err instanceof AttachmentNotFound) {
|
|
494
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
logger$1.error("Attachment metadata lookup failed: @error", err);
|
|
498
|
+
sendError(res, 500, "Internal error");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (header.status !== "available") {
|
|
502
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
let target;
|
|
506
|
+
if (attachments.backend && attachments.backend.kind !== "filesystem") try {
|
|
507
|
+
target = await attachments.backend.prepareDownloadTarget(canonicalHash, expiresIn);
|
|
508
|
+
} catch {
|
|
509
|
+
sendError(res, 502, "Attachment download target unavailable");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const base = requestBaseUrl(req);
|
|
514
|
+
if (!base) {
|
|
515
|
+
sendError(res, 500, "Internal error");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
target = parseAttachmentDownloadTarget({
|
|
520
|
+
kind: "switchboard",
|
|
521
|
+
method: "GET",
|
|
522
|
+
url: `${base}/attachments/${canonicalHash}`,
|
|
523
|
+
headers: {}
|
|
524
|
+
});
|
|
525
|
+
} catch {
|
|
526
|
+
sendError(res, 500, "Internal error");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
sendJson(res, 200, target);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
376
533
|
//#endregion
|
|
377
534
|
//#region src/attachments/index.ts
|
|
378
535
|
function registerAttachmentRoutes(api) {
|
|
@@ -382,9 +539,50 @@ function registerAttachmentRoutes(api) {
|
|
|
382
539
|
mountAuthenticatedNodeRoute(api, "DELETE", "/attachments/reservations/:reservationId", makeDeleteReservationHandler(attachments));
|
|
383
540
|
mountAuthenticatedNodeRoute(api, "PUT", "/attachments/reservations/:reservationId", makeUploadHandler(attachments));
|
|
384
541
|
mountAuthenticatedNodeRoute(api, "HEAD", "/attachments/:hash", makeStatHandler(attachments));
|
|
542
|
+
mountAuthenticatedNodeRoute(api, "GET", "/attachments/:hash/download-target", makeDownloadTargetHandler(attachments, api.attachmentAccess), { allowAnonymous: true });
|
|
385
543
|
mountAuthenticatedNodeRoute(api, "GET", "/attachments/:hash", makeDownloadHandler(attachments));
|
|
386
544
|
}
|
|
387
545
|
//#endregion
|
|
546
|
+
//#region src/attachment-reference-read-model.mts
|
|
547
|
+
const attachmentSchemaCompiler = new AttachmentSchemaCompiler();
|
|
548
|
+
function createAttachmentReferenceReadModel(baseKysely, dependencies, attachmentReferenceWriter) {
|
|
549
|
+
return new AttachmentReferenceReadModel(baseKysely.withSchema(REACTOR_SCHEMA), dependencies.operationIndex, dependencies.writeCache, dependencies.processorManagerConsistencyTracker, dependencies.documentModelRegistry, attachmentSchemaCompiler, attachmentReferenceWriter);
|
|
550
|
+
}
|
|
551
|
+
function registerAttachmentReferenceReadModel(reactorBuilder, registration) {
|
|
552
|
+
reactorBuilder.withReadModelFactory(async ({ documentModelRegistry, operationIndex, writeCache, processorManagerConsistencyTracker }) => {
|
|
553
|
+
const readModel = createAttachmentReferenceReadModel(registration.baseKysely, {
|
|
554
|
+
operationIndex,
|
|
555
|
+
writeCache,
|
|
556
|
+
processorManagerConsistencyTracker,
|
|
557
|
+
documentModelRegistry
|
|
558
|
+
}, registration.attachmentReferenceWriter);
|
|
559
|
+
await readModel.init();
|
|
560
|
+
return readModel;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async function registerAttachmentReferenceReadModelOnModule(clientModule, attachmentReferenceWriter) {
|
|
564
|
+
const reactorModule = clientModule.reactorModule;
|
|
565
|
+
if (!reactorModule) return {
|
|
566
|
+
status: "unavailable",
|
|
567
|
+
reason: "in-process-reactor-module-unavailable"
|
|
568
|
+
};
|
|
569
|
+
const coordinator = reactorModule.readModelCoordinator;
|
|
570
|
+
if (!supportsLiveReadModelRegistration(coordinator)) return {
|
|
571
|
+
status: "unavailable",
|
|
572
|
+
reason: "live-read-model-registration-unsupported"
|
|
573
|
+
};
|
|
574
|
+
const readModel = createAttachmentReferenceReadModel(reactorModule.database, {
|
|
575
|
+
operationIndex: reactorModule.operationIndex,
|
|
576
|
+
writeCache: reactorModule.writeCache,
|
|
577
|
+
processorManagerConsistencyTracker: reactorModule.processorManagerConsistencyTracker,
|
|
578
|
+
documentModelRegistry: reactorModule.documentModelRegistry
|
|
579
|
+
}, attachmentReferenceWriter);
|
|
580
|
+
await readModel.init();
|
|
581
|
+
coordinator.addReadModel(readModel, "pre_ready");
|
|
582
|
+
await readModel.init();
|
|
583
|
+
return { status: "available" };
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
388
586
|
//#region src/builder-defaults.mts
|
|
389
587
|
/**
|
|
390
588
|
* Apply switchboard's standard configuration to a reactor + client builder
|
|
@@ -410,7 +608,10 @@ function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options
|
|
|
410
608
|
const scheme = options.channelScheme === void 0 ? ChannelScheme.SWITCHBOARD : options.channelScheme;
|
|
411
609
|
if (scheme !== false) reactorBuilder.withChannelScheme(scheme);
|
|
412
610
|
if (options.signalHandlers !== false) reactorBuilder.withSignalHandlers();
|
|
413
|
-
if (options.executorConfig?.maxSkipThreshold !== void 0) reactorBuilder.withExecutorConfig({
|
|
611
|
+
if (options.executorConfig?.maxSkipThreshold !== void 0 || options.executorConfig?.featureFlags !== void 0) reactorBuilder.withExecutorConfig({
|
|
612
|
+
...options.executorConfig.maxSkipThreshold !== void 0 ? { maxSkipThreshold: options.executorConfig.maxSkipThreshold } : {},
|
|
613
|
+
...options.executorConfig.featureFlags !== void 0 ? { featureFlags: options.executorConfig.featureFlags } : {}
|
|
614
|
+
});
|
|
414
615
|
if (options.documentModelLoader) reactorBuilder.withDocumentModelLoader(options.documentModelLoader);
|
|
415
616
|
if (options.logger) reactorBuilder.withLogger(options.logger);
|
|
416
617
|
if (options.signer) clientBuilder.withSigner(options.signer);
|
|
@@ -911,13 +1112,17 @@ async function initServer(serverPort, options, renown) {
|
|
|
911
1112
|
});
|
|
912
1113
|
}
|
|
913
1114
|
const reactorLogger = logger.child(["reactor"]);
|
|
914
|
-
const initializeClient = async (documentModels) => {
|
|
1115
|
+
const initializeClient = async (documentModels, { attachmentReferenceWriter }) => {
|
|
915
1116
|
if (options.reactor) {
|
|
1117
|
+
const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
|
|
916
1118
|
if (options.reactor.reactorModule) {
|
|
917
1119
|
new ReactorInstrumentation(options.reactor.reactorModule).start();
|
|
918
1120
|
reactorLogger.info("Reactor metrics instrumentation started (using caller-provided reactor)");
|
|
919
1121
|
}
|
|
920
|
-
return {
|
|
1122
|
+
return {
|
|
1123
|
+
module: options.reactor,
|
|
1124
|
+
attachmentReferenceProjection
|
|
1125
|
+
};
|
|
921
1126
|
}
|
|
922
1127
|
const baseKysely = await createReactorKysely({
|
|
923
1128
|
reactorDbUrl,
|
|
@@ -930,13 +1135,22 @@ async function initServer(serverPort, options, renown) {
|
|
|
930
1135
|
const maxSkipThreshold = parseInt(process.env.MAX_SKIP_THRESHOLD ?? "", 10);
|
|
931
1136
|
const hasSkipThreshold = !isNaN(maxSkipThreshold) && maxSkipThreshold > 0;
|
|
932
1137
|
if (hasSkipThreshold) logger.info(`Reactor maxSkipThreshold set to ${maxSkipThreshold}`);
|
|
933
|
-
|
|
1138
|
+
const documentDecisions = process.env.REACTOR_DOCUMENT_DECISIONS === "true";
|
|
1139
|
+
if (documentDecisions) logger.info("Reactor document decisions enabled");
|
|
1140
|
+
const authEnforcement = process.env.REACTOR_AUTH_ENFORCEMENT === "true";
|
|
1141
|
+
if (authEnforcement) logger.info("Reactor auth enforcement enabled");
|
|
934
1142
|
const reactorBuilder = new ReactorBuilder().withEventBus(new EventBus()).withKysely(baseKysely);
|
|
935
1143
|
const clientBuilder = new ReactorClientBuilder().withReactorBuilder(reactorBuilder);
|
|
936
1144
|
const vetraDocumentModels = dev ? Object.values(await import("@powerhousedao/vetra/document-models")).filter((m) => typeof m === "object" && m !== null && "documentModel" in m && "reducer" in m) : [];
|
|
937
1145
|
applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, {
|
|
938
1146
|
documentModels: [...documentModels, ...vetraDocumentModels],
|
|
939
|
-
executorConfig: hasSkipThreshold
|
|
1147
|
+
executorConfig: hasSkipThreshold || documentDecisions || authEnforcement ? {
|
|
1148
|
+
...hasSkipThreshold ? { maxSkipThreshold } : {},
|
|
1149
|
+
...documentDecisions || authEnforcement ? { featureFlags: {
|
|
1150
|
+
...documentDecisions ? { documentDecisions: true } : {},
|
|
1151
|
+
...authEnforcement ? { authEnforcement: true } : {}
|
|
1152
|
+
} } : {}
|
|
1153
|
+
} : void 0,
|
|
940
1154
|
documentModelLoader: httpLoader && dynamicModelLoading ? httpLoader.documentModelLoader : void 0,
|
|
941
1155
|
logger: reactorLogger,
|
|
942
1156
|
signer: renown ? getRenownSignerConfig(renown, options.identity?.requireSignatures) : void 0
|
|
@@ -955,6 +1169,10 @@ async function initServer(serverPort, options, renown) {
|
|
|
955
1169
|
await nodeProcessor.init();
|
|
956
1170
|
return nodeProcessor;
|
|
957
1171
|
});
|
|
1172
|
+
registerAttachmentReferenceReadModel(reactorBuilder, {
|
|
1173
|
+
baseKysely,
|
|
1174
|
+
attachmentReferenceWriter
|
|
1175
|
+
});
|
|
958
1176
|
reactorBuilder.withShutdownHook(async () => {
|
|
959
1177
|
if (apiRef.current) await apiRef.current.dispose();
|
|
960
1178
|
});
|
|
@@ -969,7 +1187,8 @@ async function initServer(serverPort, options, renown) {
|
|
|
969
1187
|
reactorDriveClient: new ReactorDriveClient({
|
|
970
1188
|
reactor: module.client,
|
|
971
1189
|
readModel: driveNodeView
|
|
972
|
-
})
|
|
1190
|
+
}),
|
|
1191
|
+
attachmentReferenceProjection: { status: "available" }
|
|
973
1192
|
};
|
|
974
1193
|
};
|
|
975
1194
|
let defaultDriveUrl = void 0;
|
|
@@ -1100,6 +1319,7 @@ async function initServer(serverPort, options, renown) {
|
|
|
1100
1319
|
api,
|
|
1101
1320
|
reactor: client,
|
|
1102
1321
|
attachmentService,
|
|
1322
|
+
attachmentReferenceProjection: api.attachmentReferenceProjection,
|
|
1103
1323
|
renown,
|
|
1104
1324
|
port: serverPort,
|
|
1105
1325
|
shutdown: () => api.dispose()
|
|
@@ -1157,5 +1377,5 @@ if (import.meta.main) await startSwitchboard();
|
|
|
1157
1377
|
//#endregion
|
|
1158
1378
|
export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
|
|
1159
1379
|
|
|
1160
|
-
//# sourceMappingURL=server-
|
|
1161
|
-
//# debugId=
|
|
1380
|
+
//# sourceMappingURL=server-BLXBKOSF.mjs.map
|
|
1381
|
+
//# debugId=ca6cb919-a51c-59fe-9716-c135c0b04465
|