@powerhousedao/switchboard 6.2.3-dev.11 → 6.2.3-dev.13

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/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## 6.2.3-dev.13 (2026-09-18)
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.3-dev.12 (2026-09-17)
6
+
7
+ ### 🚀 Features
8
+
9
+ - **switchboard:** own the workflow runtime, feed it through a read model, and take workflows out of reactor-api ([#3042](https://github.com/powerhouse-inc/powerhouse/issues/3042))
10
+ - **reactor-workflow:** the workflow engine, composed in reactor-api behind the workflows flag ([cb807bf5d](https://github.com/powerhouse-inc/powerhouse/commit/cb807bf5d))
11
+ - **workflow:** the Connect-loaded workflow package, and the workflows flag in reactor-api ([0953fc254](https://github.com/powerhouse-inc/powerhouse/commit/0953fc254))
12
+ - **pieces-framework:** vendor the Activepieces piece framework as a Powerhouse package ([3da5dbea8](https://github.com/powerhouse-inc/powerhouse/commit/3da5dbea8))
13
+
14
+ ### ❤️ Thank You
15
+
16
+ - acaldas
17
+
1
18
  ## 6.2.3-dev.11 (2026-09-17)
2
19
 
3
20
  ### 🚀 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]="c6c10af1-f4cc-56a5-abf6-fa35cf0de956")}catch(e){}}();
4
- import { a as parseForcePgVersion, r as startSwitchboard } from "./server-CiKkVE6g.mjs";
4
+ import { a as parseForcePgVersion, r as startSwitchboard } from "./server-DndPNlPT.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]="e2c62b7b-68fd-59c2-ab82-7d035e11698b")}catch(e){}}();
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]="bf9f4a7e-71d0-5c0a-b467-a07a4938dc34")}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 { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
@@ -11,9 +11,10 @@ import { existsSync, promises, realpathSync } from "node:fs";
11
11
  import path from "node:path";
12
12
  import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
13
13
  import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, createHybridProjectionCoordinatorFactory, instrumentPgPool, parseDriveUrl, supportsLiveReadModelRegistration } from "@powerhousedao/reactor";
14
- import { HttpPackageLoader, ImportPackageLoader, PGLITE_UTC_PARSERS, PackageManagementService, PackagesSubgraph, getUniqueDocumentModels, initializeAndStartAPI, resolveRenownConfig } from "@powerhousedao/reactor-api";
14
+ import { AuthorizationPolicy, BaseSubgraph, ForbiddenError, HttpPackageLoader, ImportPackageLoader, PGLITE_UTC_PARSERS, PackageManagementService, PackagesSubgraph, 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 } from "@powerhousedao/reactor-attachments";
16
+ import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, AttachmentReferenceReadModel, AttachmentSchemaCompiler, HashMismatch, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRef, createRemoteAttachmentService, parseAttachmentDownloadTarget, parseRef } from "@powerhousedao/reactor-attachments";
17
+ import { createAttachmentClient } from "@powerhousedao/reactor-attachments/client";
17
18
  import { DriveNodeView, NodeProcessor, ReactorDriveClient, createReactorDriveResolvers, reactorDriveDocumentModelModule, reactorDriveSubgraphTypeDefs } from "@powerhousedao/reactor-drive";
18
19
  import { DEFAULT_RENOWN_URL, NodeKeyStorage, RENOWN_READ_MODEL_SUBGRAPH, RenownBuilder, RenownCryptoBuilder, createLocalCredentialVerifier, createSignatureVerifier } from "@renown/sdk/node";
19
20
  import { Kysely, PostgresDialect } from "kysely";
@@ -28,6 +29,8 @@ import os from "node:os";
28
29
  import { fileURLToPath } from "node:url";
29
30
  import { EnvVarProvider } from "@openfeature/env-var-provider";
30
31
  import { OpenFeature } from "@openfeature/server-sdk";
32
+ import { GraphQLError } from "graphql";
33
+ import { gql } from "graphql-tag";
31
34
  import { PGliteDialect } from "kysely-pglite-dialect";
32
35
  //#region src/pglite-version.ts
33
36
  const SUPPORTED_PG_MAJORS = [16, 17];
@@ -865,6 +868,486 @@ async function initFeatureFlags() {
865
868
  return OpenFeature.getClient();
866
869
  }
867
870
  //#endregion
871
+ //#region src/workflow/resolvers.ts
872
+ function requireAdmin(authorizationService, ctx) {
873
+ if (!authorizationService.isSupremeAdmin(ctx.user?.address)) throw new GraphQLError("Admin access required");
874
+ }
875
+ function parseJson(value) {
876
+ if (value === null) return null;
877
+ try {
878
+ return JSON.parse(value);
879
+ } catch {
880
+ return value;
881
+ }
882
+ }
883
+ function toStepRecord(row) {
884
+ return {
885
+ stepId: row.step_id,
886
+ stepKey: row.step_key,
887
+ blockType: row.block_type,
888
+ status: row.status,
889
+ input: parseJson(row.input),
890
+ output: parseJson(row.output),
891
+ port: row.port,
892
+ error: row.error
893
+ };
894
+ }
895
+ function toRunRecord(row, steps) {
896
+ return {
897
+ id: row.id,
898
+ workflowId: row.workflow_id,
899
+ workflowName: row.workflow_name,
900
+ workflowVersion: row.workflow_version,
901
+ triggerKind: row.trigger_kind,
902
+ triggerPayload: parseJson(row.trigger_payload),
903
+ status: row.status,
904
+ error: row.error,
905
+ startedAt: row.started_at,
906
+ endedAt: row.ended_at,
907
+ rerunOf: row.rerun_of,
908
+ steps: steps.map(toStepRecord)
909
+ };
910
+ }
911
+ const getResolvers = (runtime, authorizationService) => {
912
+ return {
913
+ Query: { workflowRuntime: () => ({}) },
914
+ WorkflowRuntimeQueries: {
915
+ health: () => "ok",
916
+ blockDescriptor: (_parent, args) => runtime.blockDescriptor(args.blockType),
917
+ blockOptions: (_parent, args, ctx) => runtime.blockOptions(args.blockType, args.propName, args.input, args.connectionId ?? void 0, ctx),
918
+ pieceCatalog: () => runtime.pieceCatalog(),
919
+ pieceActions: (_parent, args) => runtime.pieceActions(args.packageName),
920
+ pieceTriggers: (_parent, args) => runtime.pieceTriggers(args.packageName),
921
+ blockOutputTree: (_parent, args) => runtime.blockOutputTree(args.blockType, args.config),
922
+ pieceDetail: (_parent, args) => runtime.pieceDetail(args.packageName),
923
+ searchBlocks: (_parent, args) => runtime.searchBlocks(args.query, args.limit ?? void 0),
924
+ connections: (_parent, _args, ctx) => runtime.connections(ctx),
925
+ webhookEndpoint: (_parent, args, ctx) => runtime.webhookEndpoint(args.workflowId, ctx),
926
+ secret: async (_parent, args) => {
927
+ try {
928
+ return await (await runtime.secrets()).stat(args.ref);
929
+ } catch {
930
+ return null;
931
+ }
932
+ },
933
+ secrets: async () => (await runtime.secrets()).list(),
934
+ triggerStates: async (_parent, _args, ctx) => (await runtime.triggerStates(ctx)).map((row) => ({
935
+ workflowId: row.workflow_id,
936
+ blockType: row.block_type,
937
+ status: row.status,
938
+ intervalMs: row.interval_ms,
939
+ nextPollAt: row.next_poll_at,
940
+ lastPollAt: row.last_poll_at,
941
+ lastError: row.last_error,
942
+ consecutiveFailures: row.consecutive_failures
943
+ })),
944
+ runs: async (_parent, args, ctx) => (await runtime.runs(args, ctx)).map((record) => toRunRecord(record.row, record.steps)),
945
+ run: async (_parent, args, ctx) => {
946
+ const record = await runtime.run(args.id, ctx);
947
+ return record ? toRunRecord(record.row, record.steps) : null;
948
+ }
949
+ },
950
+ Mutation: { workflowRuntime: () => ({}) },
951
+ WorkflowRuntimeMutations: {
952
+ fire: (_parent, args, ctx) => runtime.fire(args.workflowId, args.payload, "manual", void 0, ctx),
953
+ testTrigger: (_parent, args, ctx) => runtime.testTrigger(args.workflowId, ctx),
954
+ rerun: (_parent, args, ctx) => runtime.rerun(args.runId, ctx),
955
+ createSecret: async (_parent, args, ctx) => {
956
+ requireAdmin(authorizationService, ctx);
957
+ return (await runtime.secrets()).create({
958
+ value: args.value,
959
+ label: args.label ?? void 0
960
+ });
961
+ },
962
+ rotateSecret: async (_parent, args, ctx) => {
963
+ requireAdmin(authorizationService, ctx);
964
+ return (await runtime.secrets()).rotate(args.ref, args.value);
965
+ },
966
+ deleteSecret: async (_parent, args, ctx) => {
967
+ requireAdmin(authorizationService, ctx);
968
+ await (await runtime.secrets()).delete(args.ref);
969
+ return true;
970
+ },
971
+ checkConnection: (_parent, args, ctx) => runtime.checkConnection(args.connectionId, ctx)
972
+ }
973
+ };
974
+ };
975
+ //#endregion
976
+ //#region src/workflow/schema.ts
977
+ const schema = gql`
978
+ """
979
+ WorkflowRuntime Queries
980
+ """
981
+ type WorkflowRuntimeQueries {
982
+ health: String!
983
+ """
984
+ Persisted runs, newest first. Scope them to one workflow, or to every
985
+ workflow a drive holds; workflowId wins when both are given.
986
+ """
987
+ runs(workflowId: String, driveId: String, limit: Int): [WorkflowRunRecord!]!
988
+ run(id: String!): WorkflowRunRecord
989
+ """
990
+ Action descriptor (props, auth) for a piece block type; null for core blocks.
991
+ """
992
+ blockDescriptor(blockType: String!): Unknown
993
+ """
994
+ Resolves a dynamic prop against the current config values: a DROPDOWN
995
+ yields { options, placeholder, disabled }, a DYNAMIC prop yields the
996
+ resolved sub-property descriptor list.
997
+ """
998
+ blockOptions(
999
+ blockType: String!
1000
+ propName: String!
1001
+ input: Unknown
1002
+ connectionId: String
1003
+ ): Unknown
1004
+ """
1005
+ All published Activepieces pieces with at least one action.
1006
+ """
1007
+ pieceCatalog: Unknown
1008
+ """
1009
+ A piece's actions, each with a ready-to-use blockType.
1010
+ """
1011
+ pieceActions(packageName: String!): Unknown
1012
+ """
1013
+ A piece's triggers, each with a ready-to-use "#trigger:" blockType.
1014
+ """
1015
+ pieceTriggers(packageName: String!): Unknown
1016
+ """
1017
+ Full piece detail (PieceMetadataModel-shaped), verbatim from the cloud API.
1018
+ """
1019
+ pieceDetail(packageName: String!): Unknown
1020
+ """
1021
+ Action/trigger name search across the catalog. The index builds lazily on
1022
+ first use; poll while status is "indexing".
1023
+ """
1024
+ searchBlocks(query: String!, limit: Int): BlockSearchResult!
1025
+ """
1026
+ Health of every registered piece trigger (poll schedule, errors).
1027
+ """
1028
+ triggerStates: [TriggerStateRecord!]!
1029
+ """
1030
+ Authored output shape of a block (SDL / outputSchema / sampleData).
1031
+ """
1032
+ blockOutputTree(blockType: String!, config: Unknown): Unknown
1033
+ """
1034
+ Every powerhouse/connection document, for connection pickers.
1035
+ """
1036
+ connections: [ConnectionRecord!]!
1037
+ """
1038
+ The webhook endpoint for a workflow: the URL to hand the provider. Minted
1039
+ on first ask, whether or not the workflow is armed — an author needs the
1040
+ URL before enabling, and the armed field carries that difference. Null
1041
+ only when the host has no webhook service.
1042
+ """
1043
+ webhookEndpoint(workflowId: String!): WebhookEndpointRecord
1044
+ """
1045
+ Secret metadata (label, version, status). Never the value.
1046
+ """
1047
+ secret(ref: String!): SecretRecord
1048
+ secrets: [SecretRecord!]!
1049
+ }
1050
+
1051
+ type BlockSearchHit {
1052
+ blockType: String!
1053
+ pieceName: String!
1054
+ pieceDisplayName: String!
1055
+ logoUrl: String!
1056
+ displayName: String!
1057
+ description: String!
1058
+ "action | trigger"
1059
+ kind: String!
1060
+ "Triggers only: POLLING | WEBHOOK | APP_WEBHOOK"
1061
+ strategy: String
1062
+ }
1063
+
1064
+ type BlockSearchResult {
1065
+ "ready | indexing | error"
1066
+ status: String!
1067
+ hits: [BlockSearchHit!]!
1068
+ indexedPieces: Int!
1069
+ error: String
1070
+ }
1071
+
1072
+ type SecretRecord {
1073
+ ref: String!
1074
+ label: String
1075
+ version: Int!
1076
+ status: String!
1077
+ createdAt: String!
1078
+ updatedAt: String!
1079
+ }
1080
+
1081
+ type ConnectionRecord {
1082
+ id: String!
1083
+ name: String!
1084
+ connectorId: String!
1085
+ authType: String!
1086
+ status: String!
1087
+ accountLabel: String
1088
+ }
1089
+
1090
+ type ConnectionCheckResult {
1091
+ ok: Boolean!
1092
+ detail: String
1093
+ accountLabel: String
1094
+ }
1095
+
1096
+ type WebhookEndpointRecord {
1097
+ workflowId: String!
1098
+ url: String!
1099
+ """
1100
+ False when the reactor does not know its own public origin, so the url
1101
+ above is a bare path no provider can call: the author supplies the origin.
1102
+ """
1103
+ absoluteUrl: Boolean!
1104
+ "True while the workflow is ENABLED with a valid webhook trigger"
1105
+ armed: Boolean!
1106
+ createdAt: String!
1107
+ }
1108
+
1109
+ type TriggerStateRecord {
1110
+ workflowId: String!
1111
+ blockType: String!
1112
+ status: String!
1113
+ intervalMs: Int!
1114
+ nextPollAt: String
1115
+ lastPollAt: String
1116
+ lastError: String
1117
+ consecutiveFailures: Int!
1118
+ }
1119
+
1120
+ type Query {
1121
+ workflowRuntime: WorkflowRuntimeQueries!
1122
+ }
1123
+
1124
+ type WorkflowStepRunRecord {
1125
+ stepId: String!
1126
+ stepKey: String!
1127
+ blockType: String!
1128
+ status: String!
1129
+ input: Unknown
1130
+ output: Unknown
1131
+ port: String
1132
+ error: String
1133
+ }
1134
+
1135
+ type WorkflowRunRecord {
1136
+ id: String!
1137
+ workflowId: String!
1138
+ workflowName: String!
1139
+ workflowVersion: Int!
1140
+ triggerKind: String!
1141
+ triggerPayload: Unknown
1142
+ status: String!
1143
+ error: String
1144
+ startedAt: String!
1145
+ endedAt: String
1146
+ rerunOf: String
1147
+ steps: [WorkflowStepRunRecord!]!
1148
+ }
1149
+
1150
+ type WorkflowStepRun {
1151
+ stepId: String!
1152
+ key: String!
1153
+ blockType: String!
1154
+ status: String!
1155
+ input: Unknown
1156
+ output: Unknown
1157
+ port: String
1158
+ error: String
1159
+ }
1160
+
1161
+ type WorkflowRunPayload {
1162
+ runId: String
1163
+ status: String!
1164
+ error: String
1165
+ steps: [WorkflowStepRun!]!
1166
+ }
1167
+
1168
+ """
1169
+ WorkflowRuntime Mutations
1170
+ """
1171
+ type WorkflowRuntimeMutations {
1172
+ """
1173
+ Fires a workflow's core#manual trigger and runs it to completion.
1174
+ """
1175
+ fire(workflowId: String!, payload: Unknown): WorkflowRunPayload!
1176
+ """
1177
+ Runs a piece trigger's test hook; sample items, no cursor changes.
1178
+ """
1179
+ testTrigger(workflowId: String!): Unknown
1180
+ """
1181
+ Resumes a FAILED run: succeeded steps replay from the journal,
1182
+ execution restarts at the failure. Produces a new run.
1183
+ """
1184
+ rerun(runId: String!): WorkflowRunPayload!
1185
+ """
1186
+ Mints a managed secret and returns its ref; the value is stored
1187
+ encrypted and is never readable back over any API.
1188
+ """
1189
+ createSecret(value: String!, label: String): SecretRecord!
1190
+ """
1191
+ Replaces the value behind an existing ref; documents stay untouched.
1192
+ """
1193
+ rotateSecret(ref: String!, value: String!): SecretRecord!
1194
+ """
1195
+ Tombstones a secret; its value becomes unrecoverable.
1196
+ """
1197
+ deleteSecret(ref: String!): Boolean!
1198
+ """
1199
+ Runs the piece's app.checkConnection against the connection's
1200
+ credentials and records the outcome on the connection document.
1201
+ """
1202
+ checkConnection(connectionId: String!): ConnectionCheckResult!
1203
+ }
1204
+
1205
+ type Mutation {
1206
+ workflowRuntime: WorkflowRuntimeMutations!
1207
+ }
1208
+ `;
1209
+ //#endregion
1210
+ //#region src/workflow/subgraph.ts
1211
+ /** The runtime's read/write surface. The runtime itself is composed by the
1212
+ * host, so the subgraph only serves what it is handed. */
1213
+ function createWorkflowRuntimeSubgraph(runtime) {
1214
+ return class WorkflowRuntimeSubgraph extends BaseSubgraph {
1215
+ name = "workflow-runtime";
1216
+ typeDefs = schema;
1217
+ resolvers = getResolvers(runtime, this.authorizationService);
1218
+ additionalContextFields = {};
1219
+ };
1220
+ }
1221
+ //#endregion
1222
+ //#region src/workflow-runtime.mts
1223
+ /** The npm name the workflow package owns: its HTTP namespace and its models. */
1224
+ const WORKFLOW_PACKAGE_NAME = "@powerhousedao/workflow";
1225
+ /** The env var and OpenFeature flag key that turns workflows on. */
1226
+ const PH_WORKFLOWS_ENABLED = "PH_WORKFLOWS_ENABLED";
1227
+ /** Precedence, unchanged from the reactor-api resolver this replaces: the
1228
+ * host's option, then PH_WORKFLOWS_ENABLED, then the config file, then off. */
1229
+ async function resolveWorkflowsEnabled({ featureFlags, override, configEnabled = false, env = process.env }) {
1230
+ if (override !== void 0) return override;
1231
+ const raw = env[PH_WORKFLOWS_ENABLED]?.trim();
1232
+ if (raw === "1") return true;
1233
+ if (raw === "0") return false;
1234
+ return featureFlags.getBooleanValue(PH_WORKFLOWS_ENABLED, configEnabled);
1235
+ }
1236
+ async function loadWorkflowDocumentModels(load = () => import("@powerhousedao/workflow/document-models")) {
1237
+ try {
1238
+ return Object.values(await load()).filter((module) => typeof module === "object" && module !== null && "documentModel" in module && "reducer" in module);
1239
+ } catch (error) {
1240
+ throw new Error(`Workflows are enabled but ${WORKFLOW_PACKAGE_NAME} could not be loaded`, { cause: error });
1241
+ }
1242
+ }
1243
+ function readAssertion(authorizationService, reactorClient) {
1244
+ const resolveCanonical = createCanonicalDocumentIdResolver(reactorClient);
1245
+ return async (identifier, caller) => {
1246
+ const ctx = caller;
1247
+ if (authorizationService.isSupremeAdmin(ctx.user?.address)) return;
1248
+ if (authorizationService.config.policy !== AuthorizationPolicy.DOCUMENT_PERMISSIONS) throw new ForbiddenError();
1249
+ let documentId;
1250
+ try {
1251
+ documentId = await resolveCanonical(identifier);
1252
+ } catch {
1253
+ throw new ForbiddenError();
1254
+ }
1255
+ if (!await authorizationService.canRead(documentId, ctx.user?.address)) throw new ForbiddenError("to read this document");
1256
+ };
1257
+ }
1258
+ /** The same, for a design-time call that writes what it names. */
1259
+ function writeAssertion(authorizationService, reactorClient) {
1260
+ const resolveCanonical = createCanonicalDocumentIdResolver(reactorClient);
1261
+ return async (identifier, caller) => {
1262
+ const ctx = caller;
1263
+ if (authorizationService.isSupremeAdmin(ctx.user?.address)) return;
1264
+ if (authorizationService.config.policy !== AuthorizationPolicy.DOCUMENT_PERMISSIONS) throw new ForbiddenError();
1265
+ let documentId;
1266
+ try {
1267
+ documentId = await resolveCanonical(identifier);
1268
+ } catch {
1269
+ throw new ForbiddenError();
1270
+ }
1271
+ if (!await authorizationService.canWrite(documentId, ctx.user?.address)) throw new ForbiddenError("to write this document");
1272
+ };
1273
+ }
1274
+ /** Whether the workflow document really references the attachment. A step
1275
+ * carries no caller, so the relationship is the whole check. */
1276
+ function attachmentRefCheck(deps) {
1277
+ const resolveCanonical = createCanonicalDocumentIdResolver(deps.reactorClient);
1278
+ const references = deps.attachmentReferences;
1279
+ const projection = deps.attachmentReferenceProjection;
1280
+ return async (documentId, ref) => {
1281
+ if (!references || projection?.status !== "available") return false;
1282
+ let parsed;
1283
+ try {
1284
+ parsed = parseRef(ref);
1285
+ } catch {
1286
+ return false;
1287
+ }
1288
+ if (parsed.version !== 1) return false;
1289
+ const canonicalRef = createRef(parsed.hash.toLowerCase());
1290
+ try {
1291
+ return await references.hasReference(await resolveCanonical(documentId), canonicalRef);
1292
+ } catch {
1293
+ return false;
1294
+ }
1295
+ };
1296
+ }
1297
+ async function registerWorkflowTriggersReadModel(engine, runtime, clientModule) {
1298
+ const reactorModule = clientModule?.reactorModule;
1299
+ if (!reactorModule) return {
1300
+ status: "unavailable",
1301
+ reason: "in-process-reactor-module-unavailable"
1302
+ };
1303
+ const coordinator = reactorModule.readModelCoordinator;
1304
+ if (!supportsLiveReadModelRegistration(coordinator)) return {
1305
+ status: "unavailable",
1306
+ reason: "live-read-model-registration-unsupported"
1307
+ };
1308
+ const readModel = new engine.WorkflowTriggersReadModel(reactorModule.database.withSchema(REACTOR_SCHEMA), reactorModule.operationIndex, reactorModule.writeCache, reactorModule.processorManagerConsistencyTracker, runtime);
1309
+ await readModel.init();
1310
+ coordinator.addReadModel(readModel, engine.WORKFLOW_TRIGGERS_READ_MODEL_STAGE);
1311
+ return { status: "available" };
1312
+ }
1313
+ async function composeWorkflowRuntime(deps) {
1314
+ const load = deps.load ?? (() => import("@powerhousedao/reactor-workflow"));
1315
+ let engine;
1316
+ try {
1317
+ engine = await load();
1318
+ } catch (error) {
1319
+ throw new Error("Workflows are enabled but @powerhousedao/reactor-workflow could not be loaded", { cause: error });
1320
+ }
1321
+ const runtime = engine.createWorkflowRuntime({
1322
+ relationalDb: deps.relationalDb,
1323
+ reactorClient: deps.reactorClient,
1324
+ assertCanRead: readAssertion(deps.authorizationService, deps.reactorClient),
1325
+ assertCanWrite: writeAssertion(deps.authorizationService, deps.reactorClient),
1326
+ webhooks: deps.webhooks,
1327
+ attachments: deps.attachments,
1328
+ canReadAttachmentRef: attachmentRefCheck(deps),
1329
+ logger: deps.logger
1330
+ });
1331
+ const triggers = await registerWorkflowTriggersReadModel(engine, runtime, deps.clientModule);
1332
+ if (triggers.status === "available") deps.logger.info(`Workflow trigger read model registered (${engine.WORKFLOW_TRIGGERS_READ_MODEL}, ${engine.WORKFLOW_TRIGGERS_READ_MODEL_STAGE})`);
1333
+ else deps.logger.error("Workflow document triggers are NOT armed (@reason): this reactor's read-model coordinator takes no live registration, so no document operation reaches the runtime. Webhook and schedule triggers are unaffected.", triggers.reason);
1334
+ let stopped = false;
1335
+ return {
1336
+ subgraph: createWorkflowRuntimeSubgraph(runtime),
1337
+ triggers,
1338
+ async start() {
1339
+ await runtime.registerWebhookEndpoint();
1340
+ runtime.startTriggerSupervisor();
1341
+ },
1342
+ stop() {
1343
+ if (stopped) return Promise.resolve();
1344
+ stopped = true;
1345
+ runtime.shutdown();
1346
+ return Promise.resolve();
1347
+ }
1348
+ };
1349
+ }
1350
+ //#endregion
868
1351
  //#region src/pglite-dialect.ts
869
1352
  var ClosablePGliteDialect = class extends PGliteDialect {
870
1353
  #pglite;
@@ -1077,6 +1560,13 @@ function isPortAvailable(port) {
1077
1560
  function resolveConfigPath(configFile) {
1078
1561
  return configFile ?? path$1.join(process.cwd(), "powerhouse.config.json");
1079
1562
  }
1563
+ function readConfigWorkflowsEnabled(configPath) {
1564
+ try {
1565
+ return getConfig(configPath).workflows?.enabled ?? false;
1566
+ } catch {
1567
+ return false;
1568
+ }
1569
+ }
1080
1570
  async function resolveServerPort(requested, strictPort, logger) {
1081
1571
  if (strictPort) return requested;
1082
1572
  for (let i = 0; i < PORT_FALLBACK_ATTEMPTS; i++) {
@@ -1224,6 +1714,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
1224
1714
  reactorLogger.error(`Projection worker ${shardId} died; shutting down so the supervisor restarts a healthy process`, reason);
1225
1715
  process.kill(process.pid, "SIGTERM");
1226
1716
  };
1717
+ const workflowsEnabled = options.workflows?.enabled === true;
1227
1718
  let ownedReactorModule;
1228
1719
  const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
1229
1720
  if (options.reactor) {
@@ -1255,8 +1746,13 @@ async function initServer(serverPort, options, renown, renownConfig) {
1255
1746
  if (poolInstrumentation) reactorBuilder.withInstrumentedPool(poolInstrumentation);
1256
1747
  const clientBuilder = new ReactorClientBuilder().withReactorBuilder(reactorBuilder);
1257
1748
  const vetraDocumentModels = dev ? Object.values(await import("@powerhousedao/vetra/document-models")).filter((m) => typeof m === "object" && m !== null && "documentModel" in m && "reducer" in m) : [];
1749
+ const workflowDocumentModels = workflowsEnabled ? await loadWorkflowDocumentModels() : [];
1258
1750
  applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, {
1259
- documentModels: [...documentModels, ...vetraDocumentModels],
1751
+ documentModels: [
1752
+ ...documentModels,
1753
+ ...vetraDocumentModels,
1754
+ ...workflowDocumentModels
1755
+ ],
1260
1756
  upgradeManifests,
1261
1757
  executorConfig: hasSkipThreshold || enabledFeatureFlags.length > 0 ? {
1262
1758
  ...hasSkipThreshold ? { maxSkipThreshold } : {},
@@ -1409,6 +1905,41 @@ async function initServer(serverPort, options, renown, renownConfig) {
1409
1905
  if (process.env.SENTRY_DSN) api.httpAdapter.setupSentryErrorHandler(Sentry);
1410
1906
  const { client, graphqlManager, documentModelRegistry } = api;
1411
1907
  const lateSubgraphs = [];
1908
+ let workflows;
1909
+ if (workflowsEnabled) {
1910
+ workflows = await composeWorkflowRuntime({
1911
+ reactorClient: client,
1912
+ clientModule: options.reactor ?? ownedReactorModule,
1913
+ relationalDb: api.relationalDb,
1914
+ attachments: createAttachmentClient(api.attachments.service),
1915
+ attachmentReferences: api.attachmentReferenceIndex.store,
1916
+ attachmentReferenceProjection: api.attachmentReferenceProjection,
1917
+ webhooks: api.httpRoutes.scopeFor(WORKFLOW_PACKAGE_NAME).webhooks,
1918
+ authorizationService: api.authorizationService,
1919
+ logger: logger.child(["workflow-runtime"])
1920
+ });
1921
+ const WorkflowRuntimeSubgraph = workflows.subgraph;
1922
+ const workflowSubgraph = new WorkflowRuntimeSubgraph({
1923
+ reactorClient: client,
1924
+ http: graphqlManager.scopeForPackage(WORKFLOW_PACKAGE_NAME),
1925
+ relationalDb: api.relationalDb,
1926
+ analyticsStore: void 0,
1927
+ graphqlManager,
1928
+ syncManager: api.syncManager,
1929
+ authorizationService: graphqlManager.getAuthorizationService(),
1930
+ path: graphqlManager.getBasePath()
1931
+ });
1932
+ lateSubgraphs.push(graphqlManager.registerSubgraphInstance(workflowSubgraph, "graphql", false).catch((error) => {
1933
+ logger.error("Failed to register workflow-runtime subgraph: @error", error);
1934
+ }));
1935
+ await workflows.start();
1936
+ logger.info("Workflow runtime started");
1937
+ }
1938
+ const shutdown = async () => {
1939
+ await workflows?.stop();
1940
+ await api.dispose();
1941
+ };
1942
+ apiRef.current = { dispose: shutdown };
1412
1943
  if (httpLoader) {
1413
1944
  const packageManagementService = new PackageManagementService({
1414
1945
  defaultRegistryUrl: registryUrl,
@@ -1492,9 +2023,10 @@ async function initServer(serverPort, options, renown, renownConfig) {
1492
2023
  reactor: client,
1493
2024
  attachmentService,
1494
2025
  attachmentReferenceProjection: api.attachmentReferenceProjection,
2026
+ workflowTriggers: workflows?.triggers,
1495
2027
  renown,
1496
2028
  port: serverPort,
1497
- shutdown: () => api.dispose()
2029
+ shutdown
1498
2030
  };
1499
2031
  }
1500
2032
  /**
@@ -1522,6 +2054,13 @@ const startSwitchboard = async (options = {}) => {
1522
2054
  const enableDocumentModelSubgraphs = await featureFlags.getBooleanValue(DOCUMENT_MODEL_SUBGRAPHS_ENABLED, options.enableDocumentModelSubgraphs ?? DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT);
1523
2055
  options.enableDocumentModelSubgraphs = enableDocumentModelSubgraphs;
1524
2056
  const requireSignatures = options.identity?.requireSignatures ?? await featureFlags.getBooleanValue(REQUIRE_SIGNATURES, REQUIRE_SIGNATURES_DEFAULT);
2057
+ const configPathForFlags = resolveConfigPath(options.configFile);
2058
+ const workflowsEnabled = await resolveWorkflowsEnabled({
2059
+ featureFlags,
2060
+ override: options.workflows?.enabled,
2061
+ configEnabled: readConfigWorkflowsEnabled(configPathForFlags)
2062
+ });
2063
+ options.workflows = { enabled: workflowsEnabled };
1525
2064
  const renownConfig = resolveRenownConfig(getConfig(resolveConfigPath(options.configFile)).auth?.renown, process.env, logger);
1526
2065
  options.identity = {
1527
2066
  ...options.identity,
@@ -1530,7 +2069,8 @@ const startSwitchboard = async (options = {}) => {
1530
2069
  };
1531
2070
  logger.info("Feature flags: @flags", JSON.stringify({
1532
2071
  DOCUMENT_MODEL_SUBGRAPHS_ENABLED: enableDocumentModelSubgraphs,
1533
- REQUIRE_SIGNATURES: requireSignatures
2072
+ REQUIRE_SIGNATURES: requireSignatures,
2073
+ PH_WORKFLOWS_ENABLED: workflowsEnabled
1534
2074
  }, null, 2));
1535
2075
  let renown = null;
1536
2076
  try {
@@ -1551,5 +2091,5 @@ if (import.meta.main) await startSwitchboard();
1551
2091
  //#endregion
1552
2092
  export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
1553
2093
 
1554
- //# sourceMappingURL=server-CiKkVE6g.mjs.map
1555
- //# debugId=e2c62b7b-68fd-59c2-ab82-7d035e11698b
2094
+ //# sourceMappingURL=server-DndPNlPT.mjs.map
2095
+ //# debugId=bf9f4a7e-71d0-5c0a-b467-a07a4938dc34