@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/.tsbuild/test/workflow/harness.d.ts +45 -0
- package/.tsbuild/test/workflow/harness.d.ts.map +1 -0
- package/.tsbuild/test/workflow/resolvers.test.d.ts +2 -0
- package/.tsbuild/test/workflow/resolvers.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/webhook-dedupe.test.d.ts +2 -0
- package/.tsbuild/test/workflow/webhook-dedupe.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/webhook-signed.test.d.ts +2 -0
- package/.tsbuild/test/workflow/webhook-signed.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow/webhook-unsigned.test.d.ts +2 -0
- package/.tsbuild/test/workflow/webhook-unsigned.test.d.ts.map +1 -0
- package/.tsbuild/test/workflow-runtime.test.d.ts +2 -0
- package/.tsbuild/test/workflow-runtime.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +17 -0
- package/dist/index.mjs +1 -1
- package/dist/{server-CiKkVE6g.mjs → server-DndPNlPT.mjs} +548 -8
- package/dist/server-DndPNlPT.mjs.map +1 -0
- package/dist/server.d.mts +21 -3
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/package.json +17 -13
- package/test/workflow/harness.ts +237 -0
- package/test/workflow/resolvers.test.ts +105 -0
- package/test/workflow/webhook-dedupe.test.ts +295 -0
- package/test/workflow/webhook-signed.test.ts +509 -0
- package/test/workflow/webhook-unsigned.test.ts +332 -0
- package/test/workflow-runtime.test.ts +412 -0
- package/tsconfig.json +6 -0
- package/dist/server-CiKkVE6g.mjs.map +0 -1
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-
|
|
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]="
|
|
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: [
|
|
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
|
|
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-
|
|
1555
|
-
//# debugId=
|
|
2094
|
+
//# sourceMappingURL=server-DndPNlPT.mjs.map
|
|
2095
|
+
//# debugId=bf9f4a7e-71d0-5c0a-b467-a07a4938dc34
|