@headless-lms/core 0.1.0 → 0.2.0
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/dist/assets/events.d.ts +66 -3
- package/dist/assets/events.js +19 -3
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/service.js +5 -3
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +11 -3
- package/dist/automations/actions.js +38 -5
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +4 -1
- package/dist/automations/catalog.js +289 -188
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/events.d.ts +46 -24
- package/dist/automations/events.js +5 -5
- package/dist/automations/events.js.map +1 -1
- package/dist/automations/index.d.ts +5 -5
- package/dist/automations/index.js +2 -2
- package/dist/automations/model.d.ts +13 -3
- package/dist/automations/model.js +23 -3
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +23 -8
- package/dist/automations/service.d.ts +21 -5
- package/dist/automations/service.js +267 -47
- package/dist/automations/service.js.map +1 -1
- package/dist/automations/types.d.ts +2 -2
- package/dist/automations/webhook-signing.d.ts +7 -0
- package/dist/automations/webhook-signing.js +22 -0
- package/dist/automations/webhook-signing.js.map +1 -0
- package/dist/content/events.d.ts +401 -25
- package/dist/content/events.js +125 -19
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +5 -5
- package/dist/content/index.js +2 -2
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +42 -11
- package/dist/content/service.d.ts +15 -9
- package/dist/content/service.js +153 -45
- package/dist/content/service.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/events.d.ts +60 -4
- package/dist/discussion/events.js +30 -4
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/model.d.ts +1 -2
- package/dist/discussion/ports.d.ts +1 -1
- package/dist/discussion/service.d.ts +1 -1
- package/dist/discussion/service.js +11 -10
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +96 -12
- package/dist/entitlements/events.js +21 -4
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/service.js +1 -0
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/events.d.ts +17 -3
- package/dist/identity/events.js +12 -2
- package/dist/identity/events.js.map +1 -1
- package/dist/identity/ports.d.ts +3 -0
- package/dist/identity/service.d.ts +3 -0
- package/dist/identity/service.js +12 -1
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +27 -3
- package/dist/integrations/events.js +15 -3
- package/dist/integrations/events.js.map +1 -1
- package/dist/integrations/service.js +1 -1
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/events.d.ts +178 -11
- package/dist/organizations/events.js +52 -9
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +3 -3
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/ports.d.ts +6 -10
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +9 -4
- package/dist/organizations/service.js +131 -72
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +2 -2
- package/dist/progress/events.d.ts +34 -2
- package/dist/progress/events.js +15 -2
- package/dist/progress/events.js.map +1 -1
- package/dist/progress/service.d.ts +2 -2
- package/dist/progress/service.js +4 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/courses/index.d.ts +4 -0
- package/dist/reporting/courses/index.js +2 -0
- package/dist/reporting/courses/model.d.ts +24 -0
- package/dist/reporting/courses/model.js +1 -0
- package/dist/reporting/courses/ports.d.ts +13 -0
- package/dist/reporting/courses/ports.js +1 -0
- package/dist/reporting/courses/service.d.ts +19 -0
- package/dist/reporting/courses/service.js +20 -0
- package/dist/reporting/courses/service.js.map +1 -0
- package/dist/reporting/learn/model.d.ts +2 -2
- package/dist/reporting/learn/ports.d.ts +2 -1
- package/dist/reporting/learn/service.d.ts +2 -1
- package/dist/reporting/learn/service.js +5 -0
- package/dist/reporting/learn/service.js.map +1 -1
- package/dist/reporting/students/index.d.ts +2 -2
- package/dist/reporting/students/model.d.ts +21 -1
- package/dist/reporting/students/ports.d.ts +3 -1
- package/dist/reporting/students/service.d.ts +2 -1
- package/dist/reporting/students/service.js +3 -0
- package/dist/reporting/students/service.js.map +1 -1
- package/dist/shared/events.d.ts +5 -3
- package/dist/shared/events.js +1 -0
- package/dist/shared/events.js.map +1 -1
- package/dist/shared/id.d.ts +1 -0
- package/dist/shared/id.js +1 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +2 -0
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +13 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +3 -2
- package/dist/types/email-templates.d.ts +3 -1
- package/dist/types/events.d.ts +3 -3
- package/dist/types/index.d.ts +6 -6
- package/dist/types/organizations.d.ts +2 -2
- package/dist/types/ports.d.ts +1 -1
- package/dist/types/schemas/assets.d.ts +3 -0
- package/dist/types/schemas/assets.js +2 -1
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +71 -4
- package/dist/types/schemas/automations.js +35 -4
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +51 -18
- package/dist/types/schemas/content.js +40 -13
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/credentials.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +10 -2
- package/dist/types/schemas/discussion.js +12 -5
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +8 -2
- package/dist/types/schemas/entitlements.js +10 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +4 -4
- package/dist/types/schemas/identity.js +5 -5
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +6 -6
- package/dist/types/schemas/index.js +6 -6
- package/dist/types/schemas/integrations.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +19 -5
- package/dist/types/schemas/organizations.js +12 -7
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/outbox.d.ts +1 -0
- package/dist/types/schemas/outbox.js +1 -0
- package/dist/types/schemas/outbox.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +1 -0
- package/dist/types/schemas/progress.js +1 -0
- package/dist/types/schemas/progress.js.map +1 -1
- package/dist/types/schemas/settings.js.map +1 -1
- package/dist/types/schemas/shared.d.ts +2 -1
- package/dist/types/schemas/shared.js +3 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/dist/types/shared.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { deliverWebhookInputSchema } from "../types/schemas/automations.js";
|
|
1
2
|
import { noopLogger } from "../shared/logger.js";
|
|
2
|
-
import {
|
|
3
|
+
import { ConflictError, NotFoundError } from "../shared/errors.js";
|
|
4
|
+
import { DELIVER_WEBHOOK_ACTION, catalogActions, catalogTriggers } from "./catalog.js";
|
|
5
|
+
import { generateWebhookSecret } from "./webhook-signing.js";
|
|
3
6
|
import { executeAction } from "./actions.js";
|
|
4
7
|
import { automationEvents } from "./events.js";
|
|
5
|
-
import { InvalidTriggerError } from "./model.js";
|
|
8
|
+
import { AutomationKindMismatchError, InvalidTriggerError, ReservedActionError } from "./model.js";
|
|
6
9
|
//#region src/automations/service.ts
|
|
7
10
|
/** True only when `input` sets `enabled` and nothing else — emits automation.enabled|disabled instead of automation.updated. */
|
|
8
11
|
function isEnabledOnlyUpdate(input) {
|
|
@@ -10,6 +13,43 @@ function isEnabledOnlyUpdate(input) {
|
|
|
10
13
|
const { enabled: _enabled, ...rest } = input;
|
|
11
14
|
return Object.values(rest).every((v) => v === void 0);
|
|
12
15
|
}
|
|
16
|
+
function unique(values) {
|
|
17
|
+
return [...new Set(values)];
|
|
18
|
+
}
|
|
19
|
+
function assertAuthorableTriggers(triggers) {
|
|
20
|
+
for (const trigger of triggers) if (trigger.startsWith("automation.")) throw new InvalidTriggerError(trigger);
|
|
21
|
+
}
|
|
22
|
+
function assertAuthorableActions(actions) {
|
|
23
|
+
for (const action of actions) if (action.type === "deliverWebhook") throw new ReservedActionError(action.type);
|
|
24
|
+
}
|
|
25
|
+
function webhookTarget(automation) {
|
|
26
|
+
return deliverWebhookInputSchema.parse(automation.actions[0]?.input);
|
|
27
|
+
}
|
|
28
|
+
function toWebhook(automation) {
|
|
29
|
+
return {
|
|
30
|
+
orgId: automation.orgId,
|
|
31
|
+
id: automation.id,
|
|
32
|
+
url: webhookTarget(automation).url,
|
|
33
|
+
events: automation.triggers,
|
|
34
|
+
description: automation.description,
|
|
35
|
+
enabled: automation.enabled,
|
|
36
|
+
createdAt: automation.createdAt,
|
|
37
|
+
updatedAt: automation.updatedAt
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function updateEvent(orgId, input, updated) {
|
|
41
|
+
if (isEnabledOnlyUpdate(input)) return input.enabled ? automationEvents.automationEnabled.make({
|
|
42
|
+
orgId,
|
|
43
|
+
data: updated
|
|
44
|
+
}) : automationEvents.automationDisabled.make({
|
|
45
|
+
orgId,
|
|
46
|
+
data: updated
|
|
47
|
+
});
|
|
48
|
+
return automationEvents.automationUpdated.make({
|
|
49
|
+
orgId,
|
|
50
|
+
data: updated
|
|
51
|
+
});
|
|
52
|
+
}
|
|
13
53
|
var AutomationsServiceImpl = class {
|
|
14
54
|
repo;
|
|
15
55
|
runsRepo;
|
|
@@ -18,6 +58,8 @@ var AutomationsServiceImpl = class {
|
|
|
18
58
|
mailer;
|
|
19
59
|
lookups;
|
|
20
60
|
integrations;
|
|
61
|
+
credentials;
|
|
62
|
+
webhooks;
|
|
21
63
|
logger;
|
|
22
64
|
constructor(params) {
|
|
23
65
|
this.repo = params.repo;
|
|
@@ -27,6 +69,8 @@ var AutomationsServiceImpl = class {
|
|
|
27
69
|
this.mailer = params.mailer;
|
|
28
70
|
this.lookups = params.lookups;
|
|
29
71
|
this.integrations = params.integrations;
|
|
72
|
+
this.credentials = params.credentials;
|
|
73
|
+
this.webhooks = params.webhooks;
|
|
30
74
|
this.logger = params.logger ?? noopLogger;
|
|
31
75
|
}
|
|
32
76
|
async handle(event) {
|
|
@@ -47,34 +91,11 @@ var AutomationsServiceImpl = class {
|
|
|
47
91
|
}
|
|
48
92
|
}
|
|
49
93
|
async dispatchOne(automation, event) {
|
|
50
|
-
let run;
|
|
94
|
+
let run = null;
|
|
51
95
|
try {
|
|
52
|
-
run = await this.
|
|
53
|
-
const inserted = await runs.insert(event.orgId, {
|
|
54
|
-
orgId: event.orgId,
|
|
55
|
-
automationId: automation.id,
|
|
56
|
-
trigger: automation.trigger,
|
|
57
|
-
event,
|
|
58
|
-
status: "running",
|
|
59
|
-
actionResults: [],
|
|
60
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
61
|
-
finishedAt: null
|
|
62
|
-
});
|
|
63
|
-
if (!inserted) return null;
|
|
64
|
-
await outbox.append([automationEvents.runStarted.make({
|
|
65
|
-
orgId: event.orgId,
|
|
66
|
-
data: inserted
|
|
67
|
-
})]);
|
|
68
|
-
return inserted;
|
|
69
|
-
});
|
|
96
|
+
run = await this.openRun(automation, event);
|
|
70
97
|
if (!run) return;
|
|
71
|
-
await this.engine.dispatch(
|
|
72
|
-
runId: run.id,
|
|
73
|
-
orgId: event.orgId,
|
|
74
|
-
automationId: automation.id,
|
|
75
|
-
actions: automation.actions,
|
|
76
|
-
event
|
|
77
|
-
});
|
|
98
|
+
await this.engine.dispatch(this.dispatchFor(automation, run, event));
|
|
78
99
|
} catch (err) {
|
|
79
100
|
const message = err instanceof Error ? err.message : String(err);
|
|
80
101
|
this.logger.error("automation run failed", {
|
|
@@ -86,10 +107,42 @@ var AutomationsServiceImpl = class {
|
|
|
86
107
|
if (run) await this.recordFailure(run, message);
|
|
87
108
|
}
|
|
88
109
|
}
|
|
110
|
+
openRun(automation, event) {
|
|
111
|
+
const now = /* @__PURE__ */ new Date();
|
|
112
|
+
return this.uow.run(async ({ runs, outbox }) => {
|
|
113
|
+
const inserted = await runs.insert(event.orgId, {
|
|
114
|
+
orgId: event.orgId,
|
|
115
|
+
automationId: automation.id,
|
|
116
|
+
trigger: event.type,
|
|
117
|
+
event,
|
|
118
|
+
status: "running",
|
|
119
|
+
actionResults: [],
|
|
120
|
+
startedAt: now,
|
|
121
|
+
finishedAt: null,
|
|
122
|
+
createdAt: now,
|
|
123
|
+
updatedAt: now
|
|
124
|
+
});
|
|
125
|
+
if (!inserted) return null;
|
|
126
|
+
await outbox.append([automationEvents.runStarted.make({
|
|
127
|
+
orgId: event.orgId,
|
|
128
|
+
data: inserted
|
|
129
|
+
})]);
|
|
130
|
+
return inserted;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
dispatchFor(automation, run, event) {
|
|
134
|
+
return {
|
|
135
|
+
runId: run.id,
|
|
136
|
+
orgId: event.orgId,
|
|
137
|
+
automationId: automation.id,
|
|
138
|
+
actions: automation.actions,
|
|
139
|
+
event
|
|
140
|
+
};
|
|
141
|
+
}
|
|
89
142
|
/** Best-effort: logs and never rethrows — `handle` must never throw. */
|
|
90
143
|
async recordFailure(run, error) {
|
|
91
144
|
try {
|
|
92
|
-
await this.uow.run(async ({ runs, outbox }) => {
|
|
145
|
+
return await this.uow.run(async ({ runs, outbox }) => {
|
|
93
146
|
const failed = await runs.recordOutcome(run.orgId, run.id, {
|
|
94
147
|
status: "failed",
|
|
95
148
|
actionResults: run.actionResults,
|
|
@@ -99,6 +152,7 @@ var AutomationsServiceImpl = class {
|
|
|
99
152
|
orgId: run.orgId,
|
|
100
153
|
data: failed
|
|
101
154
|
})]);
|
|
155
|
+
return failed;
|
|
102
156
|
});
|
|
103
157
|
} catch (err) {
|
|
104
158
|
this.logger.error("failed to record automation run failure", {
|
|
@@ -107,12 +161,18 @@ var AutomationsServiceImpl = class {
|
|
|
107
161
|
error: err instanceof Error ? err.message : String(err),
|
|
108
162
|
originalError: error
|
|
109
163
|
});
|
|
164
|
+
return null;
|
|
110
165
|
}
|
|
111
166
|
}
|
|
112
167
|
async runAction(d, index) {
|
|
113
168
|
const action = d.actions[index];
|
|
114
169
|
if (!action) throw new Error(`automation ${d.automationId} run ${d.runId}: no action at index ${index}`);
|
|
115
|
-
await executeAction(action, d.event,
|
|
170
|
+
await executeAction(action, d.event, {
|
|
171
|
+
mailer: this.mailer,
|
|
172
|
+
lookups: this.lookups,
|
|
173
|
+
credentials: this.credentials,
|
|
174
|
+
webhooks: this.webhooks
|
|
175
|
+
});
|
|
116
176
|
return {
|
|
117
177
|
index,
|
|
118
178
|
type: action.type,
|
|
@@ -143,9 +203,14 @@ var AutomationsServiceImpl = class {
|
|
|
143
203
|
});
|
|
144
204
|
}
|
|
145
205
|
async create(orgId, input) {
|
|
146
|
-
|
|
206
|
+
assertAuthorableTriggers(input.triggers);
|
|
207
|
+
assertAuthorableActions(input.actions);
|
|
147
208
|
const automation = await this.uow.run(async ({ automations, outbox }) => {
|
|
148
|
-
const created = await automations.insert(orgId,
|
|
209
|
+
const created = await automations.insert(orgId, {
|
|
210
|
+
...input,
|
|
211
|
+
triggers: unique(input.triggers),
|
|
212
|
+
kind: "workflow"
|
|
213
|
+
});
|
|
149
214
|
await outbox.append([automationEvents.automationCreated.make({
|
|
150
215
|
orgId,
|
|
151
216
|
data: created
|
|
@@ -159,21 +224,19 @@ var AutomationsServiceImpl = class {
|
|
|
159
224
|
return automation;
|
|
160
225
|
}
|
|
161
226
|
async update(orgId, id, input) {
|
|
162
|
-
if (input.
|
|
227
|
+
if (input.triggers !== void 0) assertAuthorableTriggers(input.triggers);
|
|
228
|
+
if (input.actions !== void 0) assertAuthorableActions(input.actions);
|
|
229
|
+
const existing = await this.repo.findById(orgId, id);
|
|
230
|
+
if (!existing) return null;
|
|
231
|
+
if (existing.kind !== "workflow") throw new AutomationKindMismatchError(id, existing.kind);
|
|
232
|
+
const patch = input.triggers === void 0 ? input : {
|
|
233
|
+
...input,
|
|
234
|
+
triggers: unique(input.triggers)
|
|
235
|
+
};
|
|
163
236
|
const automation = await this.uow.run(async ({ automations, outbox }) => {
|
|
164
|
-
const updated = await automations.update(orgId, id,
|
|
237
|
+
const updated = await automations.update(orgId, id, patch);
|
|
165
238
|
if (!updated) return null;
|
|
166
|
-
|
|
167
|
-
orgId,
|
|
168
|
-
data: updated
|
|
169
|
-
}) : automationEvents.automationDisabled.make({
|
|
170
|
-
orgId,
|
|
171
|
-
data: updated
|
|
172
|
-
})]);
|
|
173
|
-
else await outbox.append([automationEvents.automationUpdated.make({
|
|
174
|
-
orgId,
|
|
175
|
-
data: updated
|
|
176
|
-
})]);
|
|
239
|
+
await outbox.append([updateEvent(orgId, input, updated)]);
|
|
177
240
|
return updated;
|
|
178
241
|
});
|
|
179
242
|
if (automation) this.logger.info("automation updated", {
|
|
@@ -183,6 +246,9 @@ var AutomationsServiceImpl = class {
|
|
|
183
246
|
return automation;
|
|
184
247
|
}
|
|
185
248
|
async delete(orgId, id) {
|
|
249
|
+
const existing = await this.repo.findById(orgId, id);
|
|
250
|
+
if (!existing) return false;
|
|
251
|
+
if (existing.kind !== "workflow") throw new AutomationKindMismatchError(id, existing.kind);
|
|
186
252
|
const deleted = await this.uow.run(async ({ automations, outbox }) => {
|
|
187
253
|
const removed = await automations.delete(orgId, id);
|
|
188
254
|
if (!removed) return null;
|
|
@@ -198,8 +264,8 @@ var AutomationsServiceImpl = class {
|
|
|
198
264
|
});
|
|
199
265
|
return deleted !== null;
|
|
200
266
|
}
|
|
201
|
-
list(orgId) {
|
|
202
|
-
return this.repo.listByOrg(orgId);
|
|
267
|
+
list(orgId, query) {
|
|
268
|
+
return this.repo.listByOrg(orgId, query?.kind);
|
|
203
269
|
}
|
|
204
270
|
get(orgId, id) {
|
|
205
271
|
return this.repo.findById(orgId, id);
|
|
@@ -207,6 +273,160 @@ var AutomationsServiceImpl = class {
|
|
|
207
273
|
listRuns(orgId, automationId, query) {
|
|
208
274
|
return this.runsRepo.list(orgId, automationId, query);
|
|
209
275
|
}
|
|
276
|
+
async rerun(orgId, automationId, runId) {
|
|
277
|
+
const existing = await this.runsRepo.findById(orgId, runId);
|
|
278
|
+
if (!existing || existing.automationId !== automationId) throw new NotFoundError("Automation run", runId);
|
|
279
|
+
const automation = await this.repo.findById(orgId, automationId);
|
|
280
|
+
if (!automation) throw new NotFoundError("Automation", automationId);
|
|
281
|
+
const run = await this.uow.run(async ({ runs, outbox }) => {
|
|
282
|
+
const restarted = await runs.restart(orgId, runId, /* @__PURE__ */ new Date());
|
|
283
|
+
if (!restarted) return null;
|
|
284
|
+
await outbox.append([automationEvents.runStarted.make({
|
|
285
|
+
orgId,
|
|
286
|
+
data: restarted
|
|
287
|
+
})]);
|
|
288
|
+
return restarted;
|
|
289
|
+
});
|
|
290
|
+
if (!run) throw new ConflictError(`automation run ${runId} is still running`);
|
|
291
|
+
try {
|
|
292
|
+
await this.engine.dispatch(this.dispatchFor(automation, run, run.event));
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
295
|
+
this.logger.error("automation rerun failed", {
|
|
296
|
+
orgId,
|
|
297
|
+
automationId,
|
|
298
|
+
runId: run.id,
|
|
299
|
+
error: message
|
|
300
|
+
});
|
|
301
|
+
return await this.recordFailure(run, message) ?? run;
|
|
302
|
+
}
|
|
303
|
+
this.logger.info("automation rerun started", {
|
|
304
|
+
orgId,
|
|
305
|
+
automationId,
|
|
306
|
+
runId: run.id
|
|
307
|
+
});
|
|
308
|
+
return run;
|
|
309
|
+
}
|
|
310
|
+
async listWebhooks(orgId) {
|
|
311
|
+
return (await this.repo.listByOrg(orgId, "webhook")).map(toWebhook);
|
|
312
|
+
}
|
|
313
|
+
async getWebhook(orgId, id) {
|
|
314
|
+
const automation = await this.findWebhook(orgId, id);
|
|
315
|
+
return automation ? toWebhook(automation) : null;
|
|
316
|
+
}
|
|
317
|
+
async createWebhook(orgId, input) {
|
|
318
|
+
const events = unique(input.events);
|
|
319
|
+
this.assertSubscribableEvents(events);
|
|
320
|
+
const secret = generateWebhookSecret();
|
|
321
|
+
const automation = await this.uow.run(async ({ automations, credentials, outbox }) => {
|
|
322
|
+
const secretRef = await credentials.store(orgId, { secret });
|
|
323
|
+
const created = await automations.insert(orgId, {
|
|
324
|
+
kind: "webhook",
|
|
325
|
+
name: input.url,
|
|
326
|
+
description: input.description,
|
|
327
|
+
triggers: events,
|
|
328
|
+
actions: [{
|
|
329
|
+
type: DELIVER_WEBHOOK_ACTION,
|
|
330
|
+
input: {
|
|
331
|
+
url: input.url,
|
|
332
|
+
secretRef
|
|
333
|
+
}
|
|
334
|
+
}]
|
|
335
|
+
});
|
|
336
|
+
await outbox.append([automationEvents.automationCreated.make({
|
|
337
|
+
orgId,
|
|
338
|
+
data: created
|
|
339
|
+
})]);
|
|
340
|
+
return created;
|
|
341
|
+
});
|
|
342
|
+
this.logger.info("webhook created", {
|
|
343
|
+
orgId,
|
|
344
|
+
automationId: automation.id
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
...toWebhook(automation),
|
|
348
|
+
secret
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async updateWebhook(orgId, id, input) {
|
|
352
|
+
const existing = await this.findWebhook(orgId, id);
|
|
353
|
+
if (!existing) return null;
|
|
354
|
+
const events = input.events === void 0 ? void 0 : unique(input.events);
|
|
355
|
+
if (events) this.assertSubscribableEvents(events);
|
|
356
|
+
const { secretRef } = webhookTarget(existing);
|
|
357
|
+
const patch = {
|
|
358
|
+
name: input.url,
|
|
359
|
+
description: input.description,
|
|
360
|
+
triggers: events,
|
|
361
|
+
actions: input.url === void 0 ? void 0 : [{
|
|
362
|
+
type: DELIVER_WEBHOOK_ACTION,
|
|
363
|
+
input: {
|
|
364
|
+
url: input.url,
|
|
365
|
+
secretRef
|
|
366
|
+
}
|
|
367
|
+
}],
|
|
368
|
+
enabled: input.enabled
|
|
369
|
+
};
|
|
370
|
+
const updated = await this.uow.run(async ({ automations, outbox }) => {
|
|
371
|
+
const row = await automations.update(orgId, id, patch);
|
|
372
|
+
if (!row) return null;
|
|
373
|
+
await outbox.append([updateEvent(orgId, input, row)]);
|
|
374
|
+
return row;
|
|
375
|
+
});
|
|
376
|
+
if (!updated) return null;
|
|
377
|
+
this.logger.info("webhook updated", {
|
|
378
|
+
orgId,
|
|
379
|
+
automationId: id
|
|
380
|
+
});
|
|
381
|
+
return toWebhook(updated);
|
|
382
|
+
}
|
|
383
|
+
async deleteWebhook(orgId, id) {
|
|
384
|
+
const existing = await this.findWebhook(orgId, id);
|
|
385
|
+
if (!existing) return false;
|
|
386
|
+
const { secretRef } = webhookTarget(existing);
|
|
387
|
+
const deleted = await this.uow.run(async ({ automations, credentials, outbox }) => {
|
|
388
|
+
const removed = await automations.delete(orgId, id);
|
|
389
|
+
if (!removed) return null;
|
|
390
|
+
await credentials.destroy(orgId, secretRef);
|
|
391
|
+
await outbox.append([automationEvents.automationDeleted.make({
|
|
392
|
+
orgId,
|
|
393
|
+
data: removed
|
|
394
|
+
})]);
|
|
395
|
+
return removed;
|
|
396
|
+
});
|
|
397
|
+
if (deleted) this.logger.info("webhook deleted", {
|
|
398
|
+
orgId,
|
|
399
|
+
automationId: id
|
|
400
|
+
});
|
|
401
|
+
return deleted !== null;
|
|
402
|
+
}
|
|
403
|
+
async revealWebhookSecret(orgId, id) {
|
|
404
|
+
const existing = await this.findWebhook(orgId, id);
|
|
405
|
+
if (!existing) return null;
|
|
406
|
+
const secret = (await this.credentials.reveal(orgId, webhookTarget(existing).secretRef))?.["secret"];
|
|
407
|
+
if (typeof secret !== "string") throw new Error(`webhook ${id}: signing secret is missing`);
|
|
408
|
+
return { secret };
|
|
409
|
+
}
|
|
410
|
+
async rotateWebhookSecret(orgId, id) {
|
|
411
|
+
const existing = await this.findWebhook(orgId, id);
|
|
412
|
+
if (!existing) return null;
|
|
413
|
+
const secret = generateWebhookSecret();
|
|
414
|
+
await this.credentials.update(orgId, webhookTarget(existing).secretRef, { secret });
|
|
415
|
+
this.logger.info("webhook secret rotated", {
|
|
416
|
+
orgId,
|
|
417
|
+
automationId: id
|
|
418
|
+
});
|
|
419
|
+
return { secret };
|
|
420
|
+
}
|
|
421
|
+
async findWebhook(orgId, id) {
|
|
422
|
+
const automation = await this.repo.findById(orgId, id);
|
|
423
|
+
return automation?.kind === "webhook" ? automation : null;
|
|
424
|
+
}
|
|
425
|
+
assertSubscribableEvents(events) {
|
|
426
|
+
assertAuthorableTriggers(events);
|
|
427
|
+
const known = new Set(catalogTriggers().map((t) => t.type));
|
|
428
|
+
for (const event of events) if (!known.has(event)) throw new InvalidTriggerError(event, "unknown");
|
|
429
|
+
}
|
|
210
430
|
availableActions() {
|
|
211
431
|
return [...catalogActions(), ...this.integrations.available().flatMap((integration) => integration.actions.map((action) => ({
|
|
212
432
|
type: `${integration.id}.${action.id}`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/automations/service.ts"],"sourcesContent":["// automations context — service implementation (inbound port) and the\n// AutomationExecutor the engine drives.\n//\n// `handle` is subscribed to all events on the EventBus: it matches an event\n// against enabled automations for its trigger, opens one run per match, and\n// hands it to the injected AutomationEngine, which calls back into\n// `runAction`/`finalize`.\nimport { catalogActions, catalogTriggers } from './catalog.js';\nimport { executeAction } from './actions.js';\nimport {\n InvalidTriggerError,\n type Automation,\n type AutomationActionResult,\n type AutomationRun,\n type Page,\n} from './model.js';\nimport type {\n AutomationRunsQuery,\n AvailableActions,\n AvailableTriggers,\n CreateAutomationInput,\n UpdateAutomationInput,\n} from './types.js';\nimport type {\n AutomationDispatch,\n AutomationEngine,\n AutomationExecutor,\n AutomationRunsRepository,\n AutomationsRepository,\n AutomationsService,\n AutomationsUnitOfWork,\n} from './ports.js';\nimport type { DomainEvent, Logger } from '../shared/ports.js';\nimport type { JsonValue } from '../types/index.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Mailer, MailerLookups } from '../shared/mailer.js';\nimport type { IntegrationsService } from '../integrations/index.js';\nimport { automationEvents } from './events.js';\n\n/** True only when `input` sets `enabled` and nothing else — emits automation.enabled|disabled instead of automation.updated. */\nfunction isEnabledOnlyUpdate(input: UpdateAutomationInput): input is { enabled: boolean } {\n if (input.enabled === undefined) {\n return false;\n }\n const { enabled: _enabled, ...rest } = input;\n return Object.values(rest).every((v) => v === undefined);\n}\n\nexport type AutomationsServiceParams = {\n /** Read-only access (list/get/available) — runs outside any transaction. */\n repo: AutomationsRepository;\n runsRepo: AutomationRunsRepository;\n /** Atomic write scope: tx-bound repos + outbox appender. */\n uow: AutomationsUnitOfWork;\n engine: AutomationEngine;\n mailer: Pick<Mailer, 'send'>;\n lookups: MailerLookups;\n integrations: Pick<IntegrationsService, 'available'>;\n logger?: Logger;\n};\n\nexport class AutomationsServiceImpl implements AutomationsService, AutomationExecutor {\n private readonly repo: AutomationsRepository;\n private readonly runsRepo: AutomationRunsRepository;\n private readonly uow: AutomationsUnitOfWork;\n private readonly engine: AutomationEngine;\n private readonly mailer: Pick<Mailer, 'send'>;\n private readonly lookups: MailerLookups;\n private readonly integrations: Pick<IntegrationsService, 'available'>;\n private readonly logger: Logger;\n\n constructor(params: AutomationsServiceParams) {\n this.repo = params.repo;\n this.runsRepo = params.runsRepo;\n this.uow = params.uow;\n this.engine = params.engine;\n this.mailer = params.mailer;\n this.lookups = params.lookups;\n this.integrations = params.integrations;\n this.logger = params.logger ?? noopLogger;\n }\n\n async handle(event: DomainEvent): Promise<void> {\n // automation.* events are this service's own output — matching one as a trigger would self-loop.\n if (event.type.startsWith('automation.')) {\n return;\n }\n const automations = await this.safeListByTrigger(event);\n for (const automation of automations.filter((a) => a.enabled)) {\n await this.dispatchOne(automation, event);\n }\n }\n\n private async safeListByTrigger(event: DomainEvent): Promise<Automation[]> {\n try {\n return await this.repo.listByTrigger(event.orgId, event.type);\n } catch (err) {\n this.logger.error('automations: failed to load trigger matches', {\n orgId: event.orgId,\n trigger: event.type,\n error: err instanceof Error ? err.message : String(err),\n });\n return [];\n }\n }\n\n private async dispatchOne(automation: Automation, event: DomainEvent): Promise<void> {\n let run: AutomationRun | null | undefined;\n try {\n run = await this.uow.run(async ({ runs, outbox }) => {\n const inserted = await runs.insert(event.orgId, {\n orgId: event.orgId,\n automationId: automation.id,\n trigger: automation.trigger,\n event,\n status: 'running',\n actionResults: [],\n startedAt: new Date(),\n finishedAt: null,\n });\n if (!inserted) {\n // At-least-once redelivery of a trigger event already run for this automation — no-op.\n return null;\n }\n await outbox.append([\n automationEvents.runStarted.make({\n orgId: event.orgId,\n data: inserted,\n }),\n ]);\n return inserted;\n });\n if (!run) {\n return;\n }\n await this.engine.dispatch({\n runId: run.id,\n orgId: event.orgId,\n automationId: automation.id,\n actions: automation.actions,\n event,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.logger.error('automation run failed', {\n orgId: event.orgId,\n automationId: automation.id,\n runId: run?.id,\n error: message,\n });\n if (run) {\n await this.recordFailure(run, message);\n }\n }\n }\n\n /** Best-effort: logs and never rethrows — `handle` must never throw. */\n private async recordFailure(run: AutomationRun, error: string): Promise<void> {\n try {\n await this.uow.run(async ({ runs, outbox }) => {\n const failed = await runs.recordOutcome(run.orgId, run.id, {\n status: 'failed',\n actionResults: run.actionResults,\n finishedAt: new Date(),\n });\n if (failed) {\n await outbox.append([\n automationEvents.runFailed.make({\n orgId: run.orgId,\n data: failed,\n }),\n ]);\n }\n });\n } catch (err) {\n this.logger.error('failed to record automation run failure', {\n orgId: run.orgId,\n runId: run.id,\n error: err instanceof Error ? err.message : String(err),\n originalError: error,\n });\n }\n }\n\n async runAction(d: AutomationDispatch, index: number): Promise<AutomationActionResult> {\n const action = d.actions[index];\n if (!action) {\n throw new Error(`automation ${d.automationId} run ${d.runId}: no action at index ${index}`);\n }\n await executeAction(action, d.event, this.mailer, this.lookups);\n return { index, type: action.type, status: 'completed' };\n }\n\n async finalize(d: AutomationDispatch, results: AutomationActionResult[]): Promise<void> {\n const status: 'completed' | 'failed' =\n results.length === d.actions.length && results.every((r) => r.status === 'completed')\n ? 'completed'\n : 'failed';\n await this.uow.run(async ({ runs, outbox }) => {\n const run = await runs.recordOutcome(d.orgId, d.runId, {\n status,\n actionResults: results,\n finishedAt: new Date(),\n });\n if (!run) {\n return;\n }\n const runEvent =\n status === 'completed'\n ? automationEvents.runCompleted.make({ orgId: d.orgId, data: run })\n : automationEvents.runFailed.make({ orgId: d.orgId, data: run });\n const actionFailedEvents = results\n .filter((r) => r.status === 'failed')\n .map((result) => ({\n ...automationEvents.actionFailed.make({\n orgId: d.orgId,\n data: result,\n }),\n }));\n await outbox.append([runEvent, ...actionFailedEvents]);\n });\n }\n\n async create(orgId: string, input: CreateAutomationInput): Promise<Automation> {\n if (input.trigger.startsWith('automation.')) {\n throw new InvalidTriggerError(input.trigger);\n }\n const automation = await this.uow.run(async ({ automations, outbox }) => {\n const created = await automations.insert(orgId, input);\n await outbox.append([\n automationEvents.automationCreated.make({ orgId, data: created }),\n ]);\n return created;\n });\n this.logger.info('automation created', { orgId, automationId: automation.id });\n return automation;\n }\n\n async update(\n orgId: string,\n id: string,\n input: UpdateAutomationInput,\n ): Promise<Automation | null> {\n if (input.trigger !== undefined && input.trigger.startsWith('automation.')) {\n throw new InvalidTriggerError(input.trigger);\n }\n const automation = await this.uow.run(async ({ automations, outbox }) => {\n const updated = await automations.update(orgId, id, input);\n if (!updated) {\n return null;\n }\n if (isEnabledOnlyUpdate(input)) {\n await outbox.append([\n input.enabled\n ? automationEvents.automationEnabled.make({\n orgId,\n data: updated,\n })\n : automationEvents.automationDisabled.make({\n orgId,\n data: updated,\n }),\n ]);\n } else {\n await outbox.append([\n automationEvents.automationUpdated.make({ orgId, data: updated }),\n ]);\n }\n return updated;\n });\n if (automation) {\n this.logger.info('automation updated', { orgId, automationId: id });\n }\n return automation;\n }\n\n async delete(orgId: string, id: string): Promise<boolean> {\n const deleted = await this.uow.run(async ({ automations, outbox }) => {\n const removed = await automations.delete(orgId, id);\n if (!removed) {\n return null;\n }\n await outbox.append([\n automationEvents.automationDeleted.make({ orgId, data: removed }),\n ]);\n return removed;\n });\n if (deleted) {\n this.logger.info('automation deleted', { orgId, automationId: id });\n }\n return deleted !== null;\n }\n\n list(orgId: string): Promise<Automation[]> {\n return this.repo.listByOrg(orgId);\n }\n\n get(orgId: string, id: string): Promise<Automation | null> {\n return this.repo.findById(orgId, id);\n }\n\n listRuns(\n orgId: string,\n automationId: string,\n query: AutomationRunsQuery,\n ): Promise<Page<AutomationRun>> {\n return this.runsRepo.list(orgId, automationId, query);\n }\n\n availableActions(): AvailableActions {\n return [\n ...catalogActions(),\n ...this.integrations.available().flatMap((integration) =>\n integration.actions.map((action) => ({\n type: `${integration.id}.${action.id}`,\n description: action.description,\n inputSchema: action.inputSchema as Record<string, JsonValue>,\n source: integration.id,\n })),\n ),\n ];\n }\n\n availableTriggers(): AvailableTriggers {\n return { triggers: catalogTriggers() };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/automations/service.ts"],"sourcesContent":["// automations context — service implementation (inbound port) and the\n// AutomationExecutor the engine drives.\n//\n// `handle` is subscribed to all events on the EventBus: it matches an event\n// against enabled automations for its trigger, opens one run per match, and\n// hands it to the injected AutomationEngine, which calls back into\n// `runAction`/`finalize`.\nimport { catalogActions, catalogTriggers, DELIVER_WEBHOOK_ACTION } from './catalog.js';\nimport { executeAction } from './actions.js';\nimport { generateWebhookSecret } from './webhook-signing.js';\nimport {\n AutomationKindMismatchError,\n InvalidTriggerError,\n ReservedActionError,\n type Automation,\n type AutomationAction,\n type AutomationActionResult,\n type AutomationRun,\n type CreatedWebhook,\n type DeliverWebhookInput,\n type Page,\n type Webhook,\n type WebhookSecret,\n} from './model.js';\nimport type {\n AutomationRunsQuery,\n AutomationsQuery,\n AvailableActions,\n AvailableTriggers,\n CreateAutomationInput,\n CreateWebhookInput,\n UpdateAutomationInput,\n UpdateWebhookInput,\n} from './types.js';\nimport type {\n AutomationDispatch,\n AutomationEngine,\n AutomationExecutor,\n AutomationRunsRepository,\n AutomationsRepository,\n AutomationsService,\n AutomationsUnitOfWork,\n WebhookSender,\n} from './ports.js';\nimport type { CredentialStore, DomainEvent, Logger } from '../shared/ports.js';\nimport type { JsonValue } from '../types/index.js';\nimport { deliverWebhookInputSchema } from '../types/schemas/index.js';\nimport { ConflictError, NotFoundError } from '../shared/errors.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type { Mailer, MailerLookups } from '../shared/mailer.js';\nimport type { IntegrationsService } from '../integrations/index.js';\nimport { automationEvents } from './events.js';\n\n/** True only when `input` sets `enabled` and nothing else — emits automation.enabled|disabled instead of automation.updated. */\nfunction isEnabledOnlyUpdate<T extends { enabled?: boolean }>(\n input: T,\n): input is T & { enabled: boolean } {\n if (input.enabled === undefined) {\n return false;\n }\n const { enabled: _enabled, ...rest } = input;\n return Object.values(rest).every((v) => v === undefined);\n}\n\nfunction unique(values: string[]): string[] {\n return [...new Set(values)];\n}\n\nfunction assertAuthorableTriggers(triggers: string[]): void {\n for (const trigger of triggers) {\n if (trigger.startsWith('automation.')) {\n throw new InvalidTriggerError(trigger);\n }\n }\n}\n\nfunction assertAuthorableActions(actions: AutomationAction[]): void {\n for (const action of actions) {\n if (action.type === DELIVER_WEBHOOK_ACTION) {\n throw new ReservedActionError(action.type);\n }\n }\n}\n\nfunction webhookTarget(automation: Automation): DeliverWebhookInput {\n return deliverWebhookInputSchema.parse(automation.actions[0]?.input);\n}\n\nfunction toWebhook(automation: Automation): Webhook {\n return {\n orgId: automation.orgId,\n id: automation.id,\n url: webhookTarget(automation).url,\n events: automation.triggers,\n description: automation.description,\n enabled: automation.enabled,\n createdAt: automation.createdAt,\n updatedAt: automation.updatedAt,\n };\n}\n\nfunction updateEvent(orgId: string, input: { enabled?: boolean }, updated: Automation) {\n if (isEnabledOnlyUpdate(input)) {\n return input.enabled\n ? automationEvents.automationEnabled.make({ orgId, data: updated })\n : automationEvents.automationDisabled.make({ orgId, data: updated });\n }\n return automationEvents.automationUpdated.make({ orgId, data: updated });\n}\n\nexport type AutomationsServiceParams = {\n /** Read-only access (list/get/available) — runs outside any transaction. */\n repo: AutomationsRepository;\n runsRepo: AutomationRunsRepository;\n /** Atomic write scope: tx-bound repos + credential store + outbox appender. */\n uow: AutomationsUnitOfWork;\n engine: AutomationEngine;\n mailer: Pick<Mailer, 'send'>;\n lookups: MailerLookups;\n integrations: Pick<IntegrationsService, 'available'>;\n credentials: CredentialStore;\n webhooks: WebhookSender;\n logger?: Logger;\n};\n\nexport class AutomationsServiceImpl implements AutomationsService, AutomationExecutor {\n private readonly repo: AutomationsRepository;\n private readonly runsRepo: AutomationRunsRepository;\n private readonly uow: AutomationsUnitOfWork;\n private readonly engine: AutomationEngine;\n private readonly mailer: Pick<Mailer, 'send'>;\n private readonly lookups: MailerLookups;\n private readonly integrations: Pick<IntegrationsService, 'available'>;\n private readonly credentials: CredentialStore;\n private readonly webhooks: WebhookSender;\n private readonly logger: Logger;\n\n constructor(params: AutomationsServiceParams) {\n this.repo = params.repo;\n this.runsRepo = params.runsRepo;\n this.uow = params.uow;\n this.engine = params.engine;\n this.mailer = params.mailer;\n this.lookups = params.lookups;\n this.integrations = params.integrations;\n this.credentials = params.credentials;\n this.webhooks = params.webhooks;\n this.logger = params.logger ?? noopLogger;\n }\n\n async handle(event: DomainEvent): Promise<void> {\n // automation.* events are this service's own output — matching one as a trigger would self-loop.\n if (event.type.startsWith('automation.')) {\n return;\n }\n const automations = await this.safeListByTrigger(event);\n for (const automation of automations.filter((a) => a.enabled)) {\n await this.dispatchOne(automation, event);\n }\n }\n\n private async safeListByTrigger(event: DomainEvent): Promise<Automation[]> {\n try {\n return await this.repo.listByTrigger(event.orgId, event.type);\n } catch (err) {\n this.logger.error('automations: failed to load trigger matches', {\n orgId: event.orgId,\n trigger: event.type,\n error: err instanceof Error ? err.message : String(err),\n });\n return [];\n }\n }\n\n private async dispatchOne(automation: Automation, event: DomainEvent): Promise<void> {\n let run: AutomationRun | null = null;\n try {\n run = await this.openRun(automation, event);\n if (!run) {\n // At-least-once redelivery of a trigger event already run for this automation — no-op.\n return;\n }\n await this.engine.dispatch(this.dispatchFor(automation, run, event));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.logger.error('automation run failed', {\n orgId: event.orgId,\n automationId: automation.id,\n runId: run?.id,\n error: message,\n });\n if (run) {\n await this.recordFailure(run, message);\n }\n }\n }\n\n private openRun(automation: Automation, event: DomainEvent): Promise<AutomationRun | null> {\n const now = new Date();\n return this.uow.run(async ({ runs, outbox }) => {\n const inserted = await runs.insert(event.orgId, {\n orgId: event.orgId,\n automationId: automation.id,\n trigger: event.type,\n event,\n status: 'running',\n actionResults: [],\n startedAt: now,\n finishedAt: null,\n createdAt: now,\n updatedAt: now,\n });\n if (!inserted) {\n return null;\n }\n await outbox.append([\n automationEvents.runStarted.make({\n orgId: event.orgId,\n data: inserted,\n }),\n ]);\n return inserted;\n });\n }\n\n private dispatchFor(\n automation: Automation,\n run: AutomationRun,\n event: DomainEvent,\n ): AutomationDispatch {\n return {\n runId: run.id,\n orgId: event.orgId,\n automationId: automation.id,\n actions: automation.actions,\n event,\n };\n }\n\n /** Best-effort: logs and never rethrows — `handle` must never throw. */\n private async recordFailure(run: AutomationRun, error: string): Promise<AutomationRun | null> {\n try {\n return await this.uow.run(async ({ runs, outbox }) => {\n const failed = await runs.recordOutcome(run.orgId, run.id, {\n status: 'failed',\n actionResults: run.actionResults,\n finishedAt: new Date(),\n });\n if (failed) {\n await outbox.append([\n automationEvents.runFailed.make({\n orgId: run.orgId,\n data: failed,\n }),\n ]);\n }\n return failed;\n });\n } catch (err) {\n this.logger.error('failed to record automation run failure', {\n orgId: run.orgId,\n runId: run.id,\n error: err instanceof Error ? err.message : String(err),\n originalError: error,\n });\n return null;\n }\n }\n\n async runAction(d: AutomationDispatch, index: number): Promise<AutomationActionResult> {\n const action = d.actions[index];\n if (!action) {\n throw new Error(`automation ${d.automationId} run ${d.runId}: no action at index ${index}`);\n }\n await executeAction(action, d.event, {\n mailer: this.mailer,\n lookups: this.lookups,\n credentials: this.credentials,\n webhooks: this.webhooks,\n });\n return { index, type: action.type, status: 'completed' };\n }\n\n async finalize(d: AutomationDispatch, results: AutomationActionResult[]): Promise<void> {\n const status: 'completed' | 'failed' =\n results.length === d.actions.length && results.every((r) => r.status === 'completed')\n ? 'completed'\n : 'failed';\n await this.uow.run(async ({ runs, outbox }) => {\n const run = await runs.recordOutcome(d.orgId, d.runId, {\n status,\n actionResults: results,\n finishedAt: new Date(),\n });\n if (!run) {\n return;\n }\n const runEvent =\n status === 'completed'\n ? automationEvents.runCompleted.make({ orgId: d.orgId, data: run })\n : automationEvents.runFailed.make({ orgId: d.orgId, data: run });\n const actionFailedEvents = results\n .filter((r) => r.status === 'failed')\n .map((result) => ({\n ...automationEvents.actionFailed.make({\n orgId: d.orgId,\n data: result,\n }),\n }));\n await outbox.append([runEvent, ...actionFailedEvents]);\n });\n }\n\n async create(orgId: string, input: CreateAutomationInput): Promise<Automation> {\n assertAuthorableTriggers(input.triggers);\n assertAuthorableActions(input.actions);\n const automation = await this.uow.run(async ({ automations, outbox }) => {\n const created = await automations.insert(orgId, {\n ...input,\n triggers: unique(input.triggers),\n kind: 'workflow',\n });\n await outbox.append([automationEvents.automationCreated.make({ orgId, data: created })]);\n return created;\n });\n this.logger.info('automation created', { orgId, automationId: automation.id });\n return automation;\n }\n\n async update(\n orgId: string,\n id: string,\n input: UpdateAutomationInput,\n ): Promise<Automation | null> {\n if (input.triggers !== undefined) {\n assertAuthorableTriggers(input.triggers);\n }\n if (input.actions !== undefined) {\n assertAuthorableActions(input.actions);\n }\n const existing = await this.repo.findById(orgId, id);\n if (!existing) {\n return null;\n }\n if (existing.kind !== 'workflow') {\n throw new AutomationKindMismatchError(id, existing.kind);\n }\n const patch =\n input.triggers === undefined ? input : { ...input, triggers: unique(input.triggers) };\n const automation = await this.uow.run(async ({ automations, outbox }) => {\n const updated = await automations.update(orgId, id, patch);\n if (!updated) {\n return null;\n }\n await outbox.append([updateEvent(orgId, input, updated)]);\n return updated;\n });\n if (automation) {\n this.logger.info('automation updated', { orgId, automationId: id });\n }\n return automation;\n }\n\n async delete(orgId: string, id: string): Promise<boolean> {\n const existing = await this.repo.findById(orgId, id);\n if (!existing) {\n return false;\n }\n if (existing.kind !== 'workflow') {\n throw new AutomationKindMismatchError(id, existing.kind);\n }\n const deleted = await this.uow.run(async ({ automations, outbox }) => {\n const removed = await automations.delete(orgId, id);\n if (!removed) {\n return null;\n }\n await outbox.append([automationEvents.automationDeleted.make({ orgId, data: removed })]);\n return removed;\n });\n if (deleted) {\n this.logger.info('automation deleted', { orgId, automationId: id });\n }\n return deleted !== null;\n }\n\n list(orgId: string, query?: AutomationsQuery): Promise<Automation[]> {\n return this.repo.listByOrg(orgId, query?.kind);\n }\n\n get(orgId: string, id: string): Promise<Automation | null> {\n return this.repo.findById(orgId, id);\n }\n\n listRuns(\n orgId: string,\n automationId: string,\n query: AutomationRunsQuery,\n ): Promise<Page<AutomationRun>> {\n return this.runsRepo.list(orgId, automationId, query);\n }\n\n async rerun(orgId: string, automationId: string, runId: string): Promise<AutomationRun> {\n const existing = await this.runsRepo.findById(orgId, runId);\n if (!existing || existing.automationId !== automationId) {\n throw new NotFoundError('Automation run', runId);\n }\n const automation = await this.repo.findById(orgId, automationId);\n if (!automation) {\n throw new NotFoundError('Automation', automationId);\n }\n const run = await this.uow.run(async ({ runs, outbox }) => {\n const restarted = await runs.restart(orgId, runId, new Date());\n if (!restarted) {\n return null;\n }\n await outbox.append([automationEvents.runStarted.make({ orgId, data: restarted })]);\n return restarted;\n });\n if (!run) {\n throw new ConflictError(`automation run ${runId} is still running`);\n }\n try {\n await this.engine.dispatch(this.dispatchFor(automation, run, run.event));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.logger.error('automation rerun failed', {\n orgId,\n automationId,\n runId: run.id,\n error: message,\n });\n return (await this.recordFailure(run, message)) ?? run;\n }\n this.logger.info('automation rerun started', { orgId, automationId, runId: run.id });\n return run;\n }\n\n async listWebhooks(orgId: string): Promise<Webhook[]> {\n const automations = await this.repo.listByOrg(orgId, 'webhook');\n return automations.map(toWebhook);\n }\n\n async getWebhook(orgId: string, id: string): Promise<Webhook | null> {\n const automation = await this.findWebhook(orgId, id);\n return automation ? toWebhook(automation) : null;\n }\n\n async createWebhook(orgId: string, input: CreateWebhookInput): Promise<CreatedWebhook> {\n const events = unique(input.events);\n this.assertSubscribableEvents(events);\n const secret = generateWebhookSecret();\n const automation = await this.uow.run(async ({ automations, credentials, outbox }) => {\n const secretRef = await credentials.store(orgId, { secret });\n const created = await automations.insert(orgId, {\n kind: 'webhook',\n name: input.url,\n description: input.description,\n triggers: events,\n actions: [{ type: DELIVER_WEBHOOK_ACTION, input: { url: input.url, secretRef } }],\n });\n await outbox.append([automationEvents.automationCreated.make({ orgId, data: created })]);\n return created;\n });\n this.logger.info('webhook created', { orgId, automationId: automation.id });\n return { ...toWebhook(automation), secret };\n }\n\n async updateWebhook(\n orgId: string,\n id: string,\n input: UpdateWebhookInput,\n ): Promise<Webhook | null> {\n const existing = await this.findWebhook(orgId, id);\n if (!existing) {\n return null;\n }\n const events = input.events === undefined ? undefined : unique(input.events);\n if (events) {\n this.assertSubscribableEvents(events);\n }\n const { secretRef } = webhookTarget(existing);\n const patch: UpdateAutomationInput = {\n name: input.url,\n description: input.description,\n triggers: events,\n actions:\n input.url === undefined\n ? undefined\n : [{ type: DELIVER_WEBHOOK_ACTION, input: { url: input.url, secretRef } }],\n enabled: input.enabled,\n };\n const updated = await this.uow.run(async ({ automations, outbox }) => {\n const row = await automations.update(orgId, id, patch);\n if (!row) {\n return null;\n }\n await outbox.append([updateEvent(orgId, input, row)]);\n return row;\n });\n if (!updated) {\n return null;\n }\n this.logger.info('webhook updated', { orgId, automationId: id });\n return toWebhook(updated);\n }\n\n async deleteWebhook(orgId: string, id: string): Promise<boolean> {\n const existing = await this.findWebhook(orgId, id);\n if (!existing) {\n return false;\n }\n const { secretRef } = webhookTarget(existing);\n const deleted = await this.uow.run(async ({ automations, credentials, outbox }) => {\n const removed = await automations.delete(orgId, id);\n if (!removed) {\n return null;\n }\n await credentials.destroy(orgId, secretRef);\n await outbox.append([automationEvents.automationDeleted.make({ orgId, data: removed })]);\n return removed;\n });\n if (deleted) {\n this.logger.info('webhook deleted', { orgId, automationId: id });\n }\n return deleted !== null;\n }\n\n async revealWebhookSecret(orgId: string, id: string): Promise<WebhookSecret | null> {\n const existing = await this.findWebhook(orgId, id);\n if (!existing) {\n return null;\n }\n const secrets = await this.credentials.reveal(orgId, webhookTarget(existing).secretRef);\n const secret = secrets?.['secret'];\n if (typeof secret !== 'string') {\n throw new Error(`webhook ${id}: signing secret is missing`);\n }\n return { secret };\n }\n\n async rotateWebhookSecret(orgId: string, id: string): Promise<WebhookSecret | null> {\n const existing = await this.findWebhook(orgId, id);\n if (!existing) {\n return null;\n }\n const secret = generateWebhookSecret();\n await this.credentials.update(orgId, webhookTarget(existing).secretRef, { secret });\n this.logger.info('webhook secret rotated', { orgId, automationId: id });\n return { secret };\n }\n\n private async findWebhook(orgId: string, id: string): Promise<Automation | null> {\n const automation = await this.repo.findById(orgId, id);\n return automation?.kind === 'webhook' ? automation : null;\n }\n\n private assertSubscribableEvents(events: string[]): void {\n assertAuthorableTriggers(events);\n const known = new Set(catalogTriggers().map((t) => t.type));\n for (const event of events) {\n if (!known.has(event)) {\n throw new InvalidTriggerError(event, 'unknown');\n }\n }\n }\n\n availableActions(): AvailableActions {\n return [\n ...catalogActions(),\n ...this.integrations.available().flatMap((integration) =>\n integration.actions.map((action) => ({\n type: `${integration.id}.${action.id}`,\n description: action.description,\n inputSchema: action.inputSchema as Record<string, JsonValue>,\n source: integration.id,\n })),\n ),\n ];\n }\n\n availableTriggers(): AvailableTriggers {\n return { triggers: catalogTriggers() };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { AutomationRunsQuery, AvailableAction, AvailableActions, AvailableTriggers, CreateAutomationInput, UpdateAutomationInput } from "../types/schemas/automations.js";
|
|
1
|
+
import { AutomationRunsQuery, AutomationsQuery, AvailableAction, AvailableActions, AvailableTriggers, CreateAutomationInput, CreateWebhookInput, UpdateAutomationInput, UpdateWebhookInput } from "../types/schemas/automations.js";
|
|
2
2
|
import "../types/index.js";
|
|
3
|
-
export type { AutomationRunsQuery, AvailableAction, AvailableActions, AvailableTriggers, CreateAutomationInput, UpdateAutomationInput };
|
|
3
|
+
export type { AutomationRunsQuery, AutomationsQuery, AvailableAction, AvailableActions, AvailableTriggers, CreateAutomationInput, CreateWebhookInput, UpdateAutomationInput, UpdateWebhookInput };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/automations/webhook-signing.d.ts
|
|
2
|
+
declare function generateWebhookSecret(): string;
|
|
3
|
+
declare function signWebhook(secret: string, id: string, timestamp: number, body: string): string;
|
|
4
|
+
declare function webhookHeaders(secret: string, id: string, timestamp: number, body: string): Record<string, string>;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { generateWebhookSecret, signWebhook, webhookHeaders };
|
|
7
|
+
//# sourceMappingURL=webhook-signing.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
//#region src/automations/webhook-signing.ts
|
|
3
|
+
const SECRET_PREFIX = "whsec_";
|
|
4
|
+
const SECRET_BYTES = 32;
|
|
5
|
+
function generateWebhookSecret() {
|
|
6
|
+
return `${SECRET_PREFIX}${randomBytes(SECRET_BYTES).toString("base64")}`;
|
|
7
|
+
}
|
|
8
|
+
function signWebhook(secret, id, timestamp, body) {
|
|
9
|
+
return `v1,${createHmac("sha256", Buffer.from(secret.slice(6), "base64")).update(`${id}.${timestamp}.${body}`).digest("base64")}`;
|
|
10
|
+
}
|
|
11
|
+
function webhookHeaders(secret, id, timestamp, body) {
|
|
12
|
+
return {
|
|
13
|
+
"content-type": "application/json",
|
|
14
|
+
"webhook-id": id,
|
|
15
|
+
"webhook-timestamp": String(timestamp),
|
|
16
|
+
"webhook-signature": signWebhook(secret, id, timestamp, body)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { generateWebhookSecret, signWebhook, webhookHeaders };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=webhook-signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-signing.js","names":[],"sources":["../../src/automations/webhook-signing.ts"],"sourcesContent":["import { createHmac, randomBytes } from 'node:crypto';\n\nconst SECRET_PREFIX = 'whsec_';\nconst SECRET_BYTES = 32;\n\nexport function generateWebhookSecret(): string {\n return `${SECRET_PREFIX}${randomBytes(SECRET_BYTES).toString('base64')}`;\n}\n\nexport function signWebhook(secret: string, id: string, timestamp: number, body: string): string {\n const key = Buffer.from(secret.slice(SECRET_PREFIX.length), 'base64');\n const signature = createHmac('sha256', key).update(`${id}.${timestamp}.${body}`).digest('base64');\n return `v1,${signature}`;\n}\n\nexport function webhookHeaders(\n secret: string,\n id: string,\n timestamp: number,\n body: string,\n): Record<string, string> {\n return {\n 'content-type': 'application/json',\n 'webhook-id': id,\n 'webhook-timestamp': String(timestamp),\n 'webhook-signature': signWebhook(secret, id, timestamp, body),\n };\n}\n"],"mappings":";;AAEA,MAAM,gBAAgB;AACtB,MAAM,eAAe;AAErB,SAAgB,wBAAgC;CAC9C,OAAO,GAAG,gBAAgB,YAAY,YAAY,CAAC,CAAC,SAAS,QAAQ;AACvE;AAEA,SAAgB,YAAY,QAAgB,IAAY,WAAmB,MAAsB;CAG/F,OAAO,MADW,WAAW,UADjB,OAAO,KAAK,OAAO,MAAM,CAAoB,GAAG,QACnB,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,GAAG,UAAU,GAAG,MAAM,CAAC,CAAC,OAAO,QACnE;AACvB;AAEA,SAAgB,eACd,QACA,IACA,WACA,MACwB;CACxB,OAAO;EACL,gBAAgB;EAChB,cAAc;EACd,qBAAqB,OAAO,SAAS;EACrC,qBAAqB,YAAY,QAAQ,IAAI,WAAW,IAAI;CAC9D;AACF"}
|