@powerhousedao/switchboard 6.2.3-dev.34 → 6.2.3-dev.36
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/attachments/reserve-metadata-parity.test.d.ts +2 -0
- package/.tsbuild/test/attachments/reserve-metadata-parity.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-privacy.test.d.ts +2 -0
- package/.tsbuild/test/boot-privacy.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +56 -0
- package/dist/index.mjs +1 -1
- package/dist/{server-sQoWp21G.mjs → server-D3CYuEka.mjs} +154 -52
- package/dist/server-D3CYuEka.mjs.map +1 -0
- package/dist/server.d.mts +18 -1
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/package.json +16 -15
- package/test/attachment-reference-read-model.test.ts +6 -1
- package/test/attachments/byte-grants.test.ts +4 -1
- package/test/attachments/download-target.test.ts +7 -1
- package/test/attachments/reserve-metadata-parity.test.ts +96 -0
- package/test/attachments/routes-integration.test.ts +1 -0
- package/test/attachments/routes.test.ts +1 -0
- package/test/boot-privacy.test.ts +243 -0
- package/test/boot-unsupported-stored-documents.test.ts +9 -6
- package/test/fatal-shutdown.test.ts +31 -1
- package/test/workflow-runtime.test.ts +14 -1
- package/tsconfig.json +3 -0
- package/dist/server-sQoWp21G.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
## 6.2.3-dev.36 (2026-10-01)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- **switchboard:** pass the attachment provider to host-built subgraphs ([9264687cf6](https://github.com/powerhouse-inc/powerhouse/commit/9264687cf6))
|
|
6
|
+
- **reactor-api:** declare caller-bound attachment types ([d6748866bc](https://github.com/powerhouse-inc/powerhouse/commit/d6748866bc))
|
|
7
|
+
|
|
8
|
+
### 🩹 Fixes
|
|
9
|
+
|
|
10
|
+
- **reactor-attachments:** reject mimeTypes a Content-Type header cannot carry ([2301dbe466](https://github.com/powerhouse-inc/powerhouse/commit/2301dbe466))
|
|
11
|
+
|
|
12
|
+
### ❤️ Thank You
|
|
13
|
+
|
|
14
|
+
- Benjamin Jordan
|
|
15
|
+
- Claude Opus 5.5
|
|
16
|
+
|
|
17
|
+
## 6.2.3-dev.35 (2026-10-01)
|
|
18
|
+
|
|
19
|
+
### 🚀 Features
|
|
20
|
+
|
|
21
|
+
- ⚠️ **registry:** serve every replica from Postgres and S3, processing publishes in a worker ([0d25d5fe35](https://github.com/powerhouse-inc/powerhouse/commit/0d25d5fe35))
|
|
22
|
+
- **switchboard:** configure the erasure purge timeout ([354375ea61](https://github.com/powerhouse-inc/powerhouse/commit/354375ea61))
|
|
23
|
+
- **switchboard:** run the privacy add-on behind PH_PRIVACY_ENABLED ([38c9a43856](https://github.com/powerhouse-inc/powerhouse/commit/38c9a43856))
|
|
24
|
+
- **reactor-privacy:** erase requested documents on a serial schedule ([f4f31289a1](https://github.com/powerhouse-inc/powerhouse/commit/f4f31289a1))
|
|
25
|
+
- **reactor-privacy:** serve disclosure and erasure to supreme admins only ([7a6b5232cb](https://github.com/powerhouse-inc/powerhouse/commit/7a6b5232cb))
|
|
26
|
+
- **reactor-privacy:** scaffold the package and its migration ledger ([152ecff77b](https://github.com/powerhouse-inc/powerhouse/commit/152ecff77b))
|
|
27
|
+
- **reactor-drive:** erase a purged document's nodes in NodeProcessor ([5a608e73bf](https://github.com/powerhouse-inc/powerhouse/commit/5a608e73bf))
|
|
28
|
+
- **reactor-attachments:** delete a purged document's attachment references ([4121c83347](https://github.com/powerhouse-inc/powerhouse/commit/4121c83347))
|
|
29
|
+
|
|
30
|
+
### 🩹 Fixes
|
|
31
|
+
|
|
32
|
+
- **switchboard:** let workflow steps read attachments as they read documents ([b8c861360b](https://github.com/powerhouse-inc/powerhouse/commit/b8c861360b))
|
|
33
|
+
- **switchboard:** shut down when a PGlite store cannot flush ([42ea6fd893](https://github.com/powerhouse-inc/powerhouse/commit/42ea6fd893))
|
|
34
|
+
- **reactor:** a drive's purge does not require a live former member ([e8266b1035](https://github.com/powerhouse-inc/powerhouse/commit/e8266b1035))
|
|
35
|
+
- **reactor-privacy:** warn and record lastError while a leaked purge blocks dispatch ([cd6fe4f022](https://github.com/powerhouse-inc/powerhouse/commit/cd6fe4f022))
|
|
36
|
+
- **reactor-privacy:** reopen every failed request with no failed item each tick ([d9c19c3837](https://github.com/powerhouse-inc/powerhouse/commit/d9c19c3837))
|
|
37
|
+
- **reactor-api:** ignore polled refusals of an unknown kind ([aac4a3e752](https://github.com/powerhouse-inc/powerhouse/commit/aac4a3e752))
|
|
38
|
+
- **reactor-api:** fail a poll with a recoverable code when a refusal is not recorded ([3ab1c92f7b](https://github.com/powerhouse-inc/powerhouse/commit/3ab1c92f7b))
|
|
39
|
+
- **reactor:** keep a polled marker refusal only when the marker was owed ([07cae595da](https://github.com/powerhouse-inc/powerhouse/commit/07cae595da))
|
|
40
|
+
- **reactor:** persist a purge refusal only from a MARKER_REFUSED dead letter ([ef2c3f4729](https://github.com/powerhouse-inc/powerhouse/commit/ef2c3f4729))
|
|
41
|
+
|
|
42
|
+
### 🔥 Performance
|
|
43
|
+
|
|
44
|
+
- **reactor-privacy:** dispatch the next purge without rescanning the backlog ([5de3703ebf](https://github.com/powerhouse-inc/powerhouse/commit/5de3703ebf))
|
|
45
|
+
|
|
46
|
+
### ⚠️ Breaking Changes
|
|
47
|
+
|
|
48
|
+
- **registry:** serve every replica from Postgres and S3, processing publishes in a worker ([0d25d5fe35](https://github.com/powerhouse-inc/powerhouse/commit/0d25d5fe35))
|
|
49
|
+
/packages and /pieces return pages ({items,total,limit,offset,hasMore}) instead of full arrays, and piece bundles move to /-/pieces/bundled/<name>/<version>.tgz.
|
|
50
|
+
|
|
51
|
+
### ❤️ Thank You
|
|
52
|
+
|
|
53
|
+
- acaldas
|
|
54
|
+
- Benjamin Jordan
|
|
55
|
+
- Claude Opus 5.5
|
|
56
|
+
|
|
1
57
|
## 6.2.3-dev.34 (2026-09-30)
|
|
2
58
|
|
|
3
59
|
### 🚀 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]="691165f8-a729-5658-bf22-e7b2440e6156")}catch(e){}}();
|
|
4
|
-
import { i as StoredDocumentsRefusedError, r as startSwitchboard, s as parseForcePgVersion } from "./server-
|
|
4
|
+
import { i as StoredDocumentsRefusedError, r as startSwitchboard, s as parseForcePgVersion } from "./server-D3CYuEka.mjs";
|
|
5
5
|
import { metrics } from "@opentelemetry/api";
|
|
6
6
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
7
7
|
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
|
|
@@ -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]="98e9dab8-b145-5eb1-8457-7a9d38dda98e")}catch(e){}}();
|
|
3
3
|
import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-uaBlzyjS.mjs";
|
|
4
4
|
import { createRequire, register } from "node:module";
|
|
5
5
|
import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
|
|
@@ -11,9 +11,9 @@ import { existsSync, promises, realpathSync } from "node:fs";
|
|
|
11
11
|
import path, { resolve } from "node:path";
|
|
12
12
|
import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
|
|
13
13
|
import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, UnsupportedStoredProtocolError, createHybridProjectionCoordinatorFactory, instrumentPgPool, parseDriveUrl, supportsLiveReadModelRegistration } from "@powerhousedao/reactor";
|
|
14
|
-
import { AuthorizationPolicy, BaseSubgraph, ForbiddenError, HttpPackageLoader, ImportPackageLoader, PGLITE_UTC_PARSERS, PackageManagementService, PackagesSubgraph, callerSubject, createCanonicalDocumentIdResolver, getUniqueDocumentModels, initializeAndStartAPI, resolveRenownConfig } from "@powerhousedao/reactor-api";
|
|
14
|
+
import { AuthorizationPolicy, BaseSubgraph, ForbiddenError, HttpPackageLoader, ImportPackageLoader, PGLITE_UTC_PARSERS, PackageManagementService, PackagesSubgraph, PrivacyPermissionAdapter, callerSubject, createCanonicalDocumentIdResolver, getUniqueDocumentModels, initializeAndStartAPI, resolveRenownConfig } from "@powerhousedao/reactor-api";
|
|
15
15
|
import { httpsHooksPath } from "@powerhousedao/reactor-api/https-hooks";
|
|
16
|
-
import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, AttachmentReferenceReadModel, AttachmentSchemaCompiler, HashMismatch, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRef, createRemoteAttachmentService, parseAttachmentDownloadTarget, parseRef } from "@powerhousedao/reactor-attachments";
|
|
16
|
+
import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, AttachmentReferenceReadModel, AttachmentSchemaCompiler, HashMismatch, InvalidAttachmentMetadata, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRef, createRemoteAttachmentService, parseAttachmentDownloadTarget, parseRef, validateReserveMetadata } from "@powerhousedao/reactor-attachments";
|
|
17
17
|
import { createAttachmentClient } from "@powerhousedao/reactor-attachments/client";
|
|
18
18
|
import { DriveNodeView, NodeProcessor, ReactorDriveClient, createReactorDriveResolvers, reactorDriveDocumentModelModule, reactorDriveSubgraphTypeDefs } from "@powerhousedao/reactor-drive";
|
|
19
19
|
import { DEFAULT_KEYPAIR_PATH, DEFAULT_RENOWN_URL, NodeKeyStorage, RENOWN_READ_MODEL_SUBGRAPH, RenownBuilder, RenownCryptoBuilder, createLocalCredentialVerifier, createRenownTrustPolicy, resolveSwitchboardEndpoint } from "@renown/sdk/node";
|
|
@@ -23,6 +23,7 @@ import path$1 from "path";
|
|
|
23
23
|
import { Pool } from "pg";
|
|
24
24
|
import { Readable } from "node:stream";
|
|
25
25
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
26
|
+
import { DisclosureService, MIN_DEPLOYMENT_SECRET_BYTES, createModuleErasure, createPrivacySubgraph, registerSubjectDocumentsReadModel } from "@powerhousedao/reactor-privacy";
|
|
26
27
|
import { ReactorGroupV1 } from "@powerhousedao/reactor-group";
|
|
27
28
|
import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
|
|
28
29
|
import { DEFAULT_SIGNATURE_POLICY, isSignaturePolicy } from "@powerhousedao/shared/document-model";
|
|
@@ -151,10 +152,6 @@ function mountAuthenticatedNodeRoute(api, method, path, handler, options) {
|
|
|
151
152
|
const logger$2 = childLogger(["switchboard", "attachments"]);
|
|
152
153
|
const RETRY_AFTER_SECONDS = 5;
|
|
153
154
|
const HASH_PATTERN = /^[a-f0-9]{64}$/i;
|
|
154
|
-
const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
|
|
155
|
-
const MIME_TYPE_PATTERN = /^[!#$%&'*+\-.^_`|~\w]+\/[!#$%&'*+\-.^_`|~\w]+(?:\s*;\s*[!#$%&'*+\-.^_`|~\w]+=(?:[!#$%&'*+\-.^_`|~\w]+|"(?:[^"\\\r\n]|\\[^\r\n])*"))*$/;
|
|
156
|
-
const MAX_FILENAME_LEN = 255;
|
|
157
|
-
const MAX_MIMETYPE_LEN = 255;
|
|
158
155
|
function sendJson(res, status, body) {
|
|
159
156
|
res.statusCode = status;
|
|
160
157
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -191,27 +188,34 @@ async function readJsonBody(req, body) {
|
|
|
191
188
|
function parseReserveOptions(input) {
|
|
192
189
|
if (input === null || typeof input !== "object") return null;
|
|
193
190
|
const obj = input;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
191
|
+
const { mimeType, fileName } = obj;
|
|
192
|
+
const extension = obj.extension ?? null;
|
|
193
|
+
try {
|
|
194
|
+
validateReserveMetadata({
|
|
195
|
+
mimeType,
|
|
196
|
+
fileName,
|
|
197
|
+
extension
|
|
198
|
+
});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (err instanceof InvalidAttachmentMetadata) return null;
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
if (typeof mimeType !== "string" || typeof fileName !== "string") return null;
|
|
204
|
+
if (extension !== null && typeof extension !== "string") return null;
|
|
201
205
|
if (obj.clientHash !== void 0) {
|
|
202
206
|
if (typeof obj.clientHash !== "string" || !HASH_PATTERN.test(obj.clientHash)) return null;
|
|
203
207
|
if (typeof obj.sizeBytes !== "number" || !Number.isInteger(obj.sizeBytes) || obj.sizeBytes <= 0 || !Number.isSafeInteger(obj.sizeBytes)) return null;
|
|
204
208
|
return {
|
|
205
|
-
mimeType
|
|
206
|
-
fileName
|
|
209
|
+
mimeType,
|
|
210
|
+
fileName,
|
|
207
211
|
extension,
|
|
208
212
|
clientHash: obj.clientHash.toLowerCase(),
|
|
209
213
|
sizeBytes: obj.sizeBytes
|
|
210
214
|
};
|
|
211
215
|
}
|
|
212
216
|
return {
|
|
213
|
-
mimeType
|
|
214
|
-
fileName
|
|
217
|
+
mimeType,
|
|
218
|
+
fileName,
|
|
215
219
|
extension
|
|
216
220
|
};
|
|
217
221
|
}
|
|
@@ -790,7 +794,7 @@ async function registerAttachmentReferenceReadModelOnModule(clientModule, attach
|
|
|
790
794
|
//#endregion
|
|
791
795
|
//#region src/fatal-shutdown.mts
|
|
792
796
|
const FORCED_EXIT_MS = 15e3;
|
|
793
|
-
const
|
|
797
|
+
const fatalHandlers = /* @__PURE__ */ new WeakMap();
|
|
794
798
|
/**
|
|
795
799
|
* Sends an uncaught exception or unhandled rejection through the SIGTERM
|
|
796
800
|
* shutdown that `withSignalHandlers()` installs, so the reactor and read-model
|
|
@@ -803,8 +807,7 @@ const installed = /* @__PURE__ */ new WeakSet();
|
|
|
803
807
|
* listener deliberately keeps the process running.
|
|
804
808
|
*/
|
|
805
809
|
function installFatalErrorShutdown(logger, proc = process) {
|
|
806
|
-
if (
|
|
807
|
-
installed.add(proc);
|
|
810
|
+
if (fatalHandlers.has(proc)) return;
|
|
808
811
|
const realExit = proc.exit.bind(proc);
|
|
809
812
|
let shuttingDown = false;
|
|
810
813
|
const onFatal = (kind, err) => {
|
|
@@ -818,12 +821,97 @@ function installFatalErrorShutdown(logger, proc = process) {
|
|
|
818
821
|
}, FORCED_EXIT_MS).unref();
|
|
819
822
|
proc.kill(proc.pid, "SIGTERM");
|
|
820
823
|
};
|
|
824
|
+
fatalHandlers.set(proc, onFatal);
|
|
821
825
|
proc.on("uncaughtException", (err) => onFatal("Uncaught exception", err));
|
|
822
826
|
proc.on("unhandledRejection", (reason) => {
|
|
823
827
|
if (proc.listenerCount("unhandledRejection") > 1) return;
|
|
824
828
|
onFatal("Unhandled rejection", reason);
|
|
825
829
|
});
|
|
826
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* Routes a fatal condition the caller detected through the same shutdown as an
|
|
833
|
+
* uncaught error. Returns false, and does nothing, when
|
|
834
|
+
* `installFatalErrorShutdown` has not run for `proc`.
|
|
835
|
+
*/
|
|
836
|
+
function triggerFatalShutdown(kind, err, proc = process) {
|
|
837
|
+
const onFatal = fatalHandlers.get(proc);
|
|
838
|
+
if (!onFatal) return false;
|
|
839
|
+
onFatal(kind, err);
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
//#endregion
|
|
843
|
+
//#region src/privacy.mts
|
|
844
|
+
const DAY_MS = 864e5;
|
|
845
|
+
/** Enabling privacy on a switchboard that cannot run it safely. */
|
|
846
|
+
var PrivacyBootError = class extends Error {
|
|
847
|
+
constructor(message) {
|
|
848
|
+
super(message);
|
|
849
|
+
this.name = "PrivacyBootError";
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
function positive(env, name, unitMs) {
|
|
853
|
+
const raw = env[name];
|
|
854
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
855
|
+
const value = Number(raw);
|
|
856
|
+
if (!Number.isFinite(value) || value <= 0) throw new PrivacyBootError(`${name} must be a positive number, got "${raw}"`);
|
|
857
|
+
return value * unitMs;
|
|
858
|
+
}
|
|
859
|
+
function resolvePrivacy(options, env) {
|
|
860
|
+
if (!(options?.enabled ?? env.PH_PRIVACY_ENABLED === "true")) return { enabled: false };
|
|
861
|
+
const deploymentSecret = options?.deploymentSecret ?? env.PH_PRIVACY_DEPLOYMENT_SECRET;
|
|
862
|
+
if (!deploymentSecret || Buffer.byteLength(deploymentSecret, "utf8") < MIN_DEPLOYMENT_SECRET_BYTES) throw new PrivacyBootError(`PH_PRIVACY_ENABLED needs PH_PRIVACY_DEPLOYMENT_SECRET of at least ${MIN_DEPLOYMENT_SECRET_BYTES} bytes`);
|
|
863
|
+
return {
|
|
864
|
+
enabled: true,
|
|
865
|
+
deploymentSecret,
|
|
866
|
+
intervalMs: positive(env, "PH_PRIVACY_INTERVAL_MS", 1),
|
|
867
|
+
deadlineMs: positive(env, "PH_PRIVACY_DEADLINE_DAYS", DAY_MS),
|
|
868
|
+
markerGraceMs: positive(env, "PH_PRIVACY_MARKER_GRACE_DAYS", DAY_MS),
|
|
869
|
+
purgeTimeoutMs: positive(env, "PH_PRIVACY_PURGE_TIMEOUT_MINUTES", 6e4)
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/** Subject index, erasure scheduler and admin subgraph on a built reactor. */
|
|
873
|
+
async function startPrivacy(deps) {
|
|
874
|
+
const { config, reactorModule, authorizationService } = deps;
|
|
875
|
+
if (authorizationService.config.policy === AuthorizationPolicy.OPEN) throw new PrivacyBootError("PH_PRIVACY_ENABLED is refused under the OPEN authorization policy: every caller, anonymous included, would administer erasure. Enable authentication (AUTH_ENABLED=true with ADMINS) first.");
|
|
876
|
+
if (!reactorModule) throw new PrivacyBootError("PH_PRIVACY_ENABLED needs an in-process reactor module");
|
|
877
|
+
if (!deps.signer?.app?.key) throw new PrivacyBootError("PH_PRIVACY_ENABLED needs a reactor signer (a Renown identity): every receiver refuses an unsigned purge marker");
|
|
878
|
+
const permissions = deps.documentPermissionService ? new PrivacyPermissionAdapter(deps.documentPermissionService) : void 0;
|
|
879
|
+
const { service, scheduler } = createModuleErasure(reactorModule, {
|
|
880
|
+
deploymentSecret: config.deploymentSecret,
|
|
881
|
+
signer: deps.signer,
|
|
882
|
+
permissions,
|
|
883
|
+
intervalMs: config.intervalMs,
|
|
884
|
+
deadlineMs: config.deadlineMs,
|
|
885
|
+
markerGraceMs: config.markerGraceMs,
|
|
886
|
+
purgeTimeoutMs: config.purgeTimeoutMs,
|
|
887
|
+
logger: deps.logger
|
|
888
|
+
});
|
|
889
|
+
const disclosure = new DisclosureService(reactorModule.database, config.deploymentSecret, permissions);
|
|
890
|
+
let subgraph;
|
|
891
|
+
try {
|
|
892
|
+
subgraph = createPrivacySubgraph({
|
|
893
|
+
authorizationService,
|
|
894
|
+
erasure: service,
|
|
895
|
+
disclosure
|
|
896
|
+
});
|
|
897
|
+
} catch (error) {
|
|
898
|
+
throw new PrivacyBootError(error instanceof Error ? error.message : String(error));
|
|
899
|
+
}
|
|
900
|
+
await registerSubjectDocumentsReadModel(reactorModule, { deploymentSecret: config.deploymentSecret });
|
|
901
|
+
await deps.graphqlManager.registerSubgraphInstance({
|
|
902
|
+
...subgraph,
|
|
903
|
+
path: deps.graphqlManager.getBasePath(),
|
|
904
|
+
reactorClient: deps.reactorClient,
|
|
905
|
+
relationalDb: void 0
|
|
906
|
+
}, "graphql", false);
|
|
907
|
+
scheduler.start();
|
|
908
|
+
deps.logger.info("Privacy add-on started: subject index, erasure scheduler");
|
|
909
|
+
return {
|
|
910
|
+
erasure: service,
|
|
911
|
+
scheduler,
|
|
912
|
+
stop: () => scheduler.stop()
|
|
913
|
+
};
|
|
914
|
+
}
|
|
827
915
|
//#endregion
|
|
828
916
|
//#region src/builder-defaults.mts
|
|
829
917
|
/**
|
|
@@ -1930,28 +2018,14 @@ function writeAssertion(authorizationService, reactorClient) {
|
|
|
1930
2018
|
if (!await authorizationService.canWrite(documentId, ctx.user?.address)) throw new ForbiddenError("to write this document");
|
|
1931
2019
|
};
|
|
1932
2020
|
}
|
|
1933
|
-
/**
|
|
1934
|
-
* carries no caller
|
|
1935
|
-
function
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
let parsed;
|
|
1942
|
-
try {
|
|
1943
|
-
parsed = parseRef(ref);
|
|
1944
|
-
} catch {
|
|
1945
|
-
return false;
|
|
1946
|
-
}
|
|
1947
|
-
if (parsed.version !== 1) return false;
|
|
1948
|
-
const canonicalRef = createRef(parsed.hash.toLowerCase());
|
|
1949
|
-
try {
|
|
1950
|
-
return await references.hasReference(await resolveCanonical(documentId), canonicalRef);
|
|
1951
|
-
} catch {
|
|
1952
|
-
return false;
|
|
1953
|
-
}
|
|
1954
|
-
};
|
|
2021
|
+
/** A step reads any well-formed attachment, as it reads any document: a run
|
|
2022
|
+
* carries no caller to check against. */
|
|
2023
|
+
function canReadAttachmentRef(_documentId, ref) {
|
|
2024
|
+
try {
|
|
2025
|
+
return Promise.resolve(parseRef(ref).version === 1);
|
|
2026
|
+
} catch {
|
|
2027
|
+
return Promise.resolve(false);
|
|
2028
|
+
}
|
|
1955
2029
|
}
|
|
1956
2030
|
async function registerWorkflowTriggersReadModel(engine, runtime, clientModule) {
|
|
1957
2031
|
const reactorModule = clientModule?.reactorModule;
|
|
@@ -1995,7 +2069,7 @@ async function composeWorkflowRuntime(deps) {
|
|
|
1995
2069
|
assertCanWrite: writeAssertion(deps.authorizationService, deps.reactorClient),
|
|
1996
2070
|
webhooks: deps.webhooks,
|
|
1997
2071
|
attachments: deps.attachments,
|
|
1998
|
-
canReadAttachmentRef
|
|
2072
|
+
canReadAttachmentRef,
|
|
1999
2073
|
logger: deps.logger
|
|
2000
2074
|
});
|
|
2001
2075
|
const triggers = await registerWorkflowTriggersReadModel(engine, runtime, deps.clientModule);
|
|
@@ -2332,6 +2406,9 @@ const PGLITE_FLUSH_INTERVAL_MS = (() => {
|
|
|
2332
2406
|
const parsed = Number(raw);
|
|
2333
2407
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 100;
|
|
2334
2408
|
})();
|
|
2409
|
+
function onPgliteFlushError(err) {
|
|
2410
|
+
triggerFatalShutdown("PGlite snapshot flush failed", err);
|
|
2411
|
+
}
|
|
2335
2412
|
const PGLITE_IN_MEMORY = process.env.PH_PGLITE_IN_MEMORY === "1";
|
|
2336
2413
|
/**
|
|
2337
2414
|
* Attempt to bind a throwaway TCP server to the given port. Resolves true if
|
|
@@ -2396,7 +2473,8 @@ async function createReactorKysely(opts) {
|
|
|
2396
2473
|
const { PGlite } = await loadPGliteModule(reactorPgliteMajor);
|
|
2397
2474
|
const pglite = inMemory ? new PGlite() : new PGlite({ fs: new AtomicNodeFs(reactorPgliteDir, {
|
|
2398
2475
|
logger,
|
|
2399
|
-
flushIntervalMs
|
|
2476
|
+
flushIntervalMs,
|
|
2477
|
+
onFlushError: onPgliteFlushError
|
|
2400
2478
|
}) });
|
|
2401
2479
|
logger.info(inMemory ? `Using in-memory PGlite (PG${reactorPgliteMajor}) for reactor storage [PH_PGLITE_IN_MEMORY=1]` : `Using PGlite (PG${reactorPgliteMajor}) for reactor storage at ${reactorPgliteDir}`);
|
|
2402
2480
|
return {
|
|
@@ -2519,6 +2597,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
|
|
|
2519
2597
|
process.kill(process.pid, "SIGTERM");
|
|
2520
2598
|
};
|
|
2521
2599
|
const workflowsEnabled = options.workflows?.enabled === true;
|
|
2600
|
+
const privacyConfig = resolvePrivacy(options.privacy, process.env);
|
|
2522
2601
|
if (workflowsEnabled) {
|
|
2523
2602
|
await assertWorkflowPackageLoadable();
|
|
2524
2603
|
if (!packages.includes("@powerhousedao/workflow")) packages.push(WORKFLOW_PACKAGE_NAME);
|
|
@@ -2698,7 +2777,8 @@ async function initServer(serverPort, options, renown, renownConfig) {
|
|
|
2698
2777
|
pgliteFactory = PGLITE_IN_MEMORY ? () => new ReadModelPGlite({ parsers: PGLITE_UTC_PARSERS }) : (connectionString) => new ReadModelPGlite({
|
|
2699
2778
|
fs: new AtomicNodeFs(connectionString ?? readModelPgliteDir, {
|
|
2700
2779
|
logger,
|
|
2701
|
-
flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS
|
|
2780
|
+
flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS,
|
|
2781
|
+
onFlushError: onPgliteFlushError
|
|
2702
2782
|
}),
|
|
2703
2783
|
parsers: PGLITE_UTC_PARSERS
|
|
2704
2784
|
});
|
|
@@ -2743,8 +2823,6 @@ async function initServer(serverPort, options, renown, renownConfig) {
|
|
|
2743
2823
|
relationalDb: api.relationalDb,
|
|
2744
2824
|
secretsKeyFile: readModelPgliteDir === null ? false : void 0,
|
|
2745
2825
|
attachments: createAttachmentClient(api.attachments.service),
|
|
2746
|
-
attachmentReferences: api.attachmentReferenceIndex.store,
|
|
2747
|
-
attachmentReferenceProjection: api.attachmentReferenceProjection,
|
|
2748
2826
|
webhooks: api.httpRoutes.scopeFor(WORKFLOW_PACKAGE_NAME).webhooks,
|
|
2749
2827
|
http: api.httpRoutes.scopeFor(WORKFLOW_PACKAGE_NAME),
|
|
2750
2828
|
authorizationService: api.authorizationService,
|
|
@@ -2761,7 +2839,8 @@ async function initServer(serverPort, options, renown, renownConfig) {
|
|
|
2761
2839
|
graphqlManager,
|
|
2762
2840
|
syncManager: api.syncManager,
|
|
2763
2841
|
authorizationService: graphqlManager.getAuthorizationService(),
|
|
2764
|
-
path: graphqlManager.getBasePath()
|
|
2842
|
+
path: graphqlManager.getBasePath(),
|
|
2843
|
+
attachments: api.attachmentClientProvider
|
|
2765
2844
|
});
|
|
2766
2845
|
lateSubgraphs.push(graphqlManager.registerSubgraphInstance(workflowSubgraph, "graphql", false).catch((error) => {
|
|
2767
2846
|
logger.error("Failed to register workflow-runtime subgraph: @error", error);
|
|
@@ -2769,7 +2848,28 @@ async function initServer(serverPort, options, renown, renownConfig) {
|
|
|
2769
2848
|
await workflows.start();
|
|
2770
2849
|
logger.info("Workflow runtime started");
|
|
2771
2850
|
}
|
|
2851
|
+
let privacy;
|
|
2852
|
+
if (privacyConfig.enabled) {
|
|
2853
|
+
const signer = options.reactor ? options.privacy?.signer : options.privacy?.signer ?? renown?.signer;
|
|
2854
|
+
try {
|
|
2855
|
+
privacy = await startPrivacy({
|
|
2856
|
+
config: privacyConfig,
|
|
2857
|
+
reactorModule: (options.reactor ?? ownedReactorModule)?.reactorModule,
|
|
2858
|
+
signer,
|
|
2859
|
+
authorizationService: api.authorizationService,
|
|
2860
|
+
documentPermissionService: api.documentPermissionService,
|
|
2861
|
+
graphqlManager,
|
|
2862
|
+
reactorClient: client,
|
|
2863
|
+
logger: logger.child(["privacy"])
|
|
2864
|
+
});
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
await workflows?.stop();
|
|
2867
|
+
await abortBoot(api);
|
|
2868
|
+
throw error;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2772
2871
|
const shutdown = async () => {
|
|
2872
|
+
await privacy?.stop();
|
|
2773
2873
|
await workflows?.stop();
|
|
2774
2874
|
await api.dispose();
|
|
2775
2875
|
};
|
|
@@ -2795,7 +2895,8 @@ async function initServer(serverPort, options, renown, renownConfig) {
|
|
|
2795
2895
|
path: graphqlManager.getBasePath(),
|
|
2796
2896
|
authorizationService: graphqlManager.getAuthorizationService(),
|
|
2797
2897
|
packageManagementService,
|
|
2798
|
-
http: graphqlManager.scopeForPackage("@powerhousedao/switchboard")
|
|
2898
|
+
http: graphqlManager.scopeForPackage("@powerhousedao/switchboard"),
|
|
2899
|
+
attachments: api.attachmentClientProvider
|
|
2799
2900
|
});
|
|
2800
2901
|
lateSubgraphs.push(graphqlManager.registerSubgraphInstance(packagesSubgraph, "graphql", false).catch((error) => {
|
|
2801
2902
|
logger.error("Failed to register packages subgraph: @error", error);
|
|
@@ -2864,6 +2965,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
|
|
|
2864
2965
|
attachmentReferenceProjection: api.attachmentReferenceProjection,
|
|
2865
2966
|
workflowTriggers: workflows?.triggers,
|
|
2866
2967
|
workflowsEnabled,
|
|
2968
|
+
privacy: privacy ? { erasure: privacy.erasure } : void 0,
|
|
2867
2969
|
mcpEnabled: options.mcp !== false,
|
|
2868
2970
|
renown,
|
|
2869
2971
|
port: serverPort,
|
|
@@ -2935,5 +3037,5 @@ if (import.meta.main) await startSwitchboard({ fatalErrorShutdown: true });
|
|
|
2935
3037
|
//#endregion
|
|
2936
3038
|
export { resolveUnsupportedStoredDocuments as a, StoredDocumentsRefusedError as i, isPortAvailable as n, applySwitchboardReactorDefaults as o, startSwitchboard as r, parseForcePgVersion as s, deriveAttachmentServiceConfig as t };
|
|
2937
3039
|
|
|
2938
|
-
//# sourceMappingURL=server-
|
|
2939
|
-
//# debugId=
|
|
3040
|
+
//# sourceMappingURL=server-D3CYuEka.mjs.map
|
|
3041
|
+
//# debugId=98e9dab8-b145-5eb1-8457-7a9d38dda98e
|