@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.
Files changed (154) hide show
  1. package/dist/assets/events.d.ts +66 -3
  2. package/dist/assets/events.js +19 -3
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/service.js +5 -3
  5. package/dist/assets/service.js.map +1 -1
  6. package/dist/automations/actions.d.ts +11 -3
  7. package/dist/automations/actions.js +38 -5
  8. package/dist/automations/actions.js.map +1 -1
  9. package/dist/automations/catalog.d.ts +4 -1
  10. package/dist/automations/catalog.js +289 -188
  11. package/dist/automations/catalog.js.map +1 -1
  12. package/dist/automations/events.d.ts +46 -24
  13. package/dist/automations/events.js +5 -5
  14. package/dist/automations/events.js.map +1 -1
  15. package/dist/automations/index.d.ts +5 -5
  16. package/dist/automations/index.js +2 -2
  17. package/dist/automations/model.d.ts +13 -3
  18. package/dist/automations/model.js +23 -3
  19. package/dist/automations/model.js.map +1 -1
  20. package/dist/automations/ports.d.ts +23 -8
  21. package/dist/automations/service.d.ts +21 -5
  22. package/dist/automations/service.js +267 -47
  23. package/dist/automations/service.js.map +1 -1
  24. package/dist/automations/types.d.ts +2 -2
  25. package/dist/automations/webhook-signing.d.ts +7 -0
  26. package/dist/automations/webhook-signing.js +22 -0
  27. package/dist/automations/webhook-signing.js.map +1 -0
  28. package/dist/content/events.d.ts +401 -25
  29. package/dist/content/events.js +125 -19
  30. package/dist/content/events.js.map +1 -1
  31. package/dist/content/index.d.ts +5 -5
  32. package/dist/content/index.js +2 -2
  33. package/dist/content/model.d.ts +2 -2
  34. package/dist/content/ports.d.ts +42 -11
  35. package/dist/content/service.d.ts +15 -9
  36. package/dist/content/service.js +153 -45
  37. package/dist/content/service.js.map +1 -1
  38. package/dist/content/types.d.ts +2 -2
  39. package/dist/discussion/events.d.ts +60 -4
  40. package/dist/discussion/events.js +30 -4
  41. package/dist/discussion/events.js.map +1 -1
  42. package/dist/discussion/model.d.ts +1 -2
  43. package/dist/discussion/ports.d.ts +1 -1
  44. package/dist/discussion/service.d.ts +1 -1
  45. package/dist/discussion/service.js +11 -10
  46. package/dist/discussion/service.js.map +1 -1
  47. package/dist/entitlements/events.d.ts +96 -12
  48. package/dist/entitlements/events.js +21 -4
  49. package/dist/entitlements/events.js.map +1 -1
  50. package/dist/entitlements/service.js +1 -0
  51. package/dist/entitlements/service.js.map +1 -1
  52. package/dist/identity/events.d.ts +17 -3
  53. package/dist/identity/events.js +12 -2
  54. package/dist/identity/events.js.map +1 -1
  55. package/dist/identity/ports.d.ts +3 -0
  56. package/dist/identity/service.d.ts +3 -0
  57. package/dist/identity/service.js +12 -1
  58. package/dist/identity/service.js.map +1 -1
  59. package/dist/integrations/events.d.ts +27 -3
  60. package/dist/integrations/events.js +15 -3
  61. package/dist/integrations/events.js.map +1 -1
  62. package/dist/integrations/service.js +1 -1
  63. package/dist/integrations/service.js.map +1 -1
  64. package/dist/organizations/events.d.ts +178 -11
  65. package/dist/organizations/events.js +52 -9
  66. package/dist/organizations/events.js.map +1 -1
  67. package/dist/organizations/index.d.ts +3 -3
  68. package/dist/organizations/members.js.map +1 -1
  69. package/dist/organizations/ports.d.ts +6 -10
  70. package/dist/organizations/roles.js.map +1 -1
  71. package/dist/organizations/service.d.ts +9 -4
  72. package/dist/organizations/service.js +131 -72
  73. package/dist/organizations/service.js.map +1 -1
  74. package/dist/organizations/types.d.ts +2 -2
  75. package/dist/progress/events.d.ts +34 -2
  76. package/dist/progress/events.js +15 -2
  77. package/dist/progress/events.js.map +1 -1
  78. package/dist/progress/service.d.ts +2 -2
  79. package/dist/progress/service.js +4 -2
  80. package/dist/progress/service.js.map +1 -1
  81. package/dist/reporting/courses/index.d.ts +4 -0
  82. package/dist/reporting/courses/index.js +2 -0
  83. package/dist/reporting/courses/model.d.ts +24 -0
  84. package/dist/reporting/courses/model.js +1 -0
  85. package/dist/reporting/courses/ports.d.ts +13 -0
  86. package/dist/reporting/courses/ports.js +1 -0
  87. package/dist/reporting/courses/service.d.ts +19 -0
  88. package/dist/reporting/courses/service.js +20 -0
  89. package/dist/reporting/courses/service.js.map +1 -0
  90. package/dist/reporting/learn/model.d.ts +2 -2
  91. package/dist/reporting/learn/ports.d.ts +2 -1
  92. package/dist/reporting/learn/service.d.ts +2 -1
  93. package/dist/reporting/learn/service.js +5 -0
  94. package/dist/reporting/learn/service.js.map +1 -1
  95. package/dist/reporting/students/index.d.ts +2 -2
  96. package/dist/reporting/students/model.d.ts +21 -1
  97. package/dist/reporting/students/ports.d.ts +3 -1
  98. package/dist/reporting/students/service.d.ts +2 -1
  99. package/dist/reporting/students/service.js +3 -0
  100. package/dist/reporting/students/service.js.map +1 -1
  101. package/dist/shared/events.d.ts +5 -3
  102. package/dist/shared/events.js +1 -0
  103. package/dist/shared/events.js.map +1 -1
  104. package/dist/shared/id.d.ts +1 -0
  105. package/dist/shared/id.js +1 -0
  106. package/dist/shared/id.js.map +1 -1
  107. package/dist/shared/mailer.d.ts +2 -0
  108. package/dist/shared/mailer.js.map +1 -1
  109. package/dist/shared/settings.js.map +1 -1
  110. package/dist/types/automations.d.ts +13 -2
  111. package/dist/types/content.d.ts +2 -2
  112. package/dist/types/discussion.d.ts +3 -2
  113. package/dist/types/email-templates.d.ts +3 -1
  114. package/dist/types/events.d.ts +3 -3
  115. package/dist/types/index.d.ts +6 -6
  116. package/dist/types/organizations.d.ts +2 -2
  117. package/dist/types/ports.d.ts +1 -1
  118. package/dist/types/schemas/assets.d.ts +3 -0
  119. package/dist/types/schemas/assets.js +2 -1
  120. package/dist/types/schemas/assets.js.map +1 -1
  121. package/dist/types/schemas/automations.d.ts +71 -4
  122. package/dist/types/schemas/automations.js +35 -4
  123. package/dist/types/schemas/automations.js.map +1 -1
  124. package/dist/types/schemas/content.d.ts +51 -18
  125. package/dist/types/schemas/content.js +40 -13
  126. package/dist/types/schemas/content.js.map +1 -1
  127. package/dist/types/schemas/credentials.js.map +1 -1
  128. package/dist/types/schemas/discussion.d.ts +10 -2
  129. package/dist/types/schemas/discussion.js +12 -5
  130. package/dist/types/schemas/discussion.js.map +1 -1
  131. package/dist/types/schemas/entitlements.d.ts +8 -2
  132. package/dist/types/schemas/entitlements.js +10 -4
  133. package/dist/types/schemas/entitlements.js.map +1 -1
  134. package/dist/types/schemas/identity.d.ts +4 -4
  135. package/dist/types/schemas/identity.js +5 -5
  136. package/dist/types/schemas/identity.js.map +1 -1
  137. package/dist/types/schemas/index.d.ts +6 -6
  138. package/dist/types/schemas/index.js +6 -6
  139. package/dist/types/schemas/integrations.js.map +1 -1
  140. package/dist/types/schemas/organizations.d.ts +19 -5
  141. package/dist/types/schemas/organizations.js +12 -7
  142. package/dist/types/schemas/organizations.js.map +1 -1
  143. package/dist/types/schemas/outbox.d.ts +1 -0
  144. package/dist/types/schemas/outbox.js +1 -0
  145. package/dist/types/schemas/outbox.js.map +1 -1
  146. package/dist/types/schemas/progress.d.ts +1 -0
  147. package/dist/types/schemas/progress.js +1 -0
  148. package/dist/types/schemas/progress.js.map +1 -1
  149. package/dist/types/schemas/settings.js.map +1 -1
  150. package/dist/types/schemas/shared.d.ts +2 -1
  151. package/dist/types/schemas/shared.js +3 -1
  152. package/dist/types/schemas/shared.js.map +1 -1
  153. package/dist/types/shared.d.ts +1 -1
  154. 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 { catalogActions, catalogTriggers } from "./catalog.js";
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.uow.run(async ({ runs, outbox }) => {
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, this.mailer, this.lookups);
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
- if (input.trigger.startsWith("automation.")) throw new InvalidTriggerError(input.trigger);
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, input);
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.trigger !== void 0 && input.trigger.startsWith("automation.")) throw new InvalidTriggerError(input.trigger);
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, input);
237
+ const updated = await automations.update(orgId, id, patch);
165
238
  if (!updated) return null;
166
- if (isEnabledOnlyUpdate(input)) await outbox.append([input.enabled ? automationEvents.automationEnabled.make({
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"}