@headless-lms/core 0.1.0 → 0.3.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 +90 -9
- package/dist/assets/events.js +19 -3
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/file-urls.d.ts +20 -0
- package/dist/assets/file-urls.js +25 -0
- package/dist/assets/file-urls.js.map +1 -0
- package/dist/assets/index.d.ts +5 -4
- package/dist/assets/index.js +3 -2
- package/dist/assets/model.d.ts +2 -2
- package/dist/assets/ports.d.ts +45 -15
- package/dist/assets/service.d.ts +39 -7
- package/dist/assets/service.js +292 -37
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +12 -15
- package/dist/automations/actions.js +188 -30
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +5 -1
- package/dist/automations/catalog.js +514 -197
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.d.ts +14 -0
- package/dist/automations/community-derivations.js +439 -0
- package/dist/automations/community-derivations.js.map +1 -0
- package/dist/automations/community-mail.d.ts +112 -0
- package/dist/automations/community-mail.js +90 -0
- package/dist/automations/community-mail.js.map +1 -0
- package/dist/automations/community-notifications.d.ts +16 -0
- package/dist/automations/community-notifications.js +84 -0
- package/dist/automations/community-notifications.js.map +1 -0
- package/dist/automations/events.d.ts +74 -24
- package/dist/automations/events.js +5 -5
- package/dist/automations/events.js.map +1 -1
- package/dist/automations/index.d.ts +7 -5
- package/dist/automations/index.js +3 -2
- package/dist/automations/model.d.ts +20 -3
- package/dist/automations/model.js +36 -3
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +27 -8
- package/dist/automations/schedule.d.ts +6 -0
- package/dist/automations/schedule.js +41 -0
- package/dist/automations/schedule.js.map +1 -0
- package/dist/automations/seeds.d.ts +17 -0
- package/dist/automations/seeds.js +235 -0
- package/dist/automations/seeds.js.map +1 -0
- package/dist/automations/service.d.ts +41 -6
- package/dist/automations/service.js +542 -59
- 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 +1013 -25
- package/dist/content/events.js +168 -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 +109 -25
- package/dist/content/service.d.ts +68 -23
- package/dist/content/service.js +647 -60
- package/dist/content/service.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +10 -0
- package/dist/discussion/authors.js +23 -0
- package/dist/discussion/authors.js.map +1 -0
- package/dist/discussion/chats.d.ts +104 -0
- package/dist/discussion/chats.js +798 -0
- package/dist/discussion/chats.js.map +1 -0
- package/dist/discussion/events.d.ts +633 -14
- package/dist/discussion/events.js +134 -5
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -3
- package/dist/discussion/index.js +4 -1
- package/dist/discussion/model.d.ts +1 -2
- package/dist/discussion/ports.d.ts +288 -27
- package/dist/discussion/ports.js +6 -1
- package/dist/discussion/ports.js.map +1 -0
- package/dist/discussion/posts.d.ts +103 -0
- package/dist/discussion/posts.js +723 -0
- package/dist/discussion/posts.js.map +1 -0
- package/dist/discussion/service.d.ts +33 -11
- package/dist/discussion/service.js +297 -81
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +264 -13
- package/dist/entitlements/events.js +49 -5
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/model.d.ts +3 -2
- package/dist/entitlements/ports.d.ts +124 -11
- package/dist/entitlements/rules.d.ts +12 -0
- package/dist/entitlements/rules.js +17 -0
- package/dist/entitlements/rules.js.map +1 -0
- package/dist/entitlements/service.d.ts +54 -3
- package/dist/entitlements/service.js +473 -2
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/access.d.ts +6 -0
- package/dist/identity/access.js +7 -0
- package/dist/identity/access.js.map +1 -0
- 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/index.d.ts +6 -4
- package/dist/identity/index.js +3 -1
- package/dist/identity/model.d.ts +2 -1
- package/dist/identity/notifications.d.ts +27 -0
- package/dist/identity/notifications.js +101 -0
- package/dist/identity/notifications.js.map +1 -0
- package/dist/identity/ports.d.ts +76 -3
- package/dist/identity/service.d.ts +17 -3
- package/dist/identity/service.js +73 -5
- 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/model.js +1 -1
- package/dist/integrations/model.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/access-scopes.d.ts +19 -0
- package/dist/organizations/access-scopes.js +89 -0
- package/dist/organizations/access-scopes.js.map +1 -0
- package/dist/organizations/events.d.ts +234 -25
- package/dist/organizations/events.js +58 -9
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +8 -7
- package/dist/organizations/index.js +4 -3
- package/dist/organizations/members.d.ts +8 -5
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/ports.d.ts +20 -21
- package/dist/organizations/roles.d.ts +6 -10
- package/dist/organizations/roles.js +19 -7
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +28 -14
- package/dist/organizations/service.js +274 -105
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +2 -11
- 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/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +5 -3
- package/dist/progress/service.js +9 -3
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +4 -0
- package/dist/reporting/admin/index.js +2 -0
- package/dist/reporting/admin/model.d.ts +108 -0
- package/dist/reporting/admin/ports.d.ts +27 -0
- package/dist/reporting/admin/service.d.ts +22 -0
- package/dist/reporting/admin/service.js +38 -0
- package/dist/reporting/admin/service.js.map +1 -0
- package/dist/reporting/member/index.d.ts +5 -0
- package/dist/reporting/member/index.js +2 -0
- package/dist/reporting/member/model.d.ts +124 -0
- package/dist/reporting/member/ports.d.ts +45 -0
- package/dist/reporting/member/service.d.ts +73 -0
- package/dist/reporting/member/service.js +286 -0
- package/dist/reporting/member/service.js.map +1 -0
- package/dist/shared/actor.d.ts +9 -0
- package/dist/shared/errors.d.ts +7 -1
- package/dist/shared/errors.js +10 -1
- package/dist/shared/errors.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 +12 -0
- package/dist/shared/id.js +13 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +6 -1
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/network.d.ts +10 -0
- package/dist/shared/network.js +211 -0
- package/dist/shared/network.js.map +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +21 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +12 -3
- package/dist/types/email-templates.d.ts +124 -1
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/events.d.ts +3 -3
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +10 -10
- package/dist/types/organizations.d.ts +2 -2
- package/dist/types/ports.d.ts +7 -1
- package/dist/types/schemas/assets.d.ts +67 -36
- package/dist/types/schemas/assets.js +19 -13
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +88 -4
- package/dist/types/schemas/automations.js +46 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +385 -20
- package/dist/types/schemas/content.js +178 -15
- 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 +379 -4
- package/dist/types/schemas/discussion.js +209 -7
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +44 -4
- package/dist/types/schemas/entitlements.js +32 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +115 -5
- package/dist/types/schemas/identity.js +59 -6
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +9 -9
- package/dist/types/schemas/index.js +9 -9
- package/dist/types/schemas/integrations.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +27 -5
- package/dist/types/schemas/organizations.js +14 -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 +4 -1
- package/dist/types/schemas/shared.js +7 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/dist/types/shared.d.ts +1 -1
- package/package.json +2 -2
- package/dist/reporting/dashboard/index.d.ts +0 -4
- package/dist/reporting/dashboard/index.js +0 -2
- package/dist/reporting/dashboard/model.d.ts +0 -14
- package/dist/reporting/dashboard/ports.d.ts +0 -13
- package/dist/reporting/dashboard/service.d.ts +0 -19
- package/dist/reporting/dashboard/service.js +0 -20
- package/dist/reporting/dashboard/service.js.map +0 -1
- package/dist/reporting/learn/index.d.ts +0 -5
- package/dist/reporting/learn/index.js +0 -2
- package/dist/reporting/learn/model.d.ts +0 -23
- package/dist/reporting/learn/ports.d.ts +0 -42
- package/dist/reporting/learn/service.d.ts +0 -48
- package/dist/reporting/learn/service.js +0 -111
- package/dist/reporting/learn/service.js.map +0 -1
- package/dist/reporting/students/index.d.ts +0 -4
- package/dist/reporting/students/index.js +0 -2
- package/dist/reporting/students/model.d.ts +0 -31
- package/dist/reporting/students/ports.d.ts +0 -13
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -19
- package/dist/reporting/students/service.js +0 -20
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{dashboard → admin}/model.js +0 -0
- /package/dist/reporting/{dashboard → admin}/ports.js +0 -0
- /package/dist/reporting/{learn → member}/model.js +0 -0
- /package/dist/reporting/{learn → member}/ports.js +0 -0
- /package/dist/{reporting/students/model.js → shared/actor.js} +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { deliverWebhookInputSchema } from "../types/schemas/automations.js";
|
|
2
|
+
import { ConflictError, NotFoundError } from "../shared/errors.js";
|
|
1
3
|
import { noopLogger } from "../shared/logger.js";
|
|
2
|
-
import {
|
|
4
|
+
import { ActionFailedError, AutomationKindMismatchError, InvalidTriggerError, ReservedActionError } from "./model.js";
|
|
5
|
+
import { DELIVER_WEBHOOK_ACTION, catalogActions, catalogTriggers, publicDataSchema } from "./catalog.js";
|
|
6
|
+
import { generateWebhookSecret } from "./webhook-signing.js";
|
|
3
7
|
import { executeAction } from "./actions.js";
|
|
4
8
|
import { automationEvents } from "./events.js";
|
|
5
|
-
import {
|
|
9
|
+
import { communitySeeds, orgSeeds } from "./seeds.js";
|
|
10
|
+
import { cronSupported } from "./schedule.js";
|
|
6
11
|
//#region src/automations/service.ts
|
|
7
12
|
/** True only when `input` sets `enabled` and nothing else — emits automation.enabled|disabled instead of automation.updated. */
|
|
8
13
|
function isEnabledOnlyUpdate(input) {
|
|
@@ -10,6 +15,107 @@ function isEnabledOnlyUpdate(input) {
|
|
|
10
15
|
const { enabled: _enabled, ...rest } = input;
|
|
11
16
|
return Object.values(rest).every((v) => v === void 0);
|
|
12
17
|
}
|
|
18
|
+
function floorMinute(at) {
|
|
19
|
+
const copy = new Date(at);
|
|
20
|
+
copy.setUTCSeconds(0, 0);
|
|
21
|
+
return copy.getTime();
|
|
22
|
+
}
|
|
23
|
+
function unique(values) {
|
|
24
|
+
return [...new Set(values)];
|
|
25
|
+
}
|
|
26
|
+
function recordString(value, key) {
|
|
27
|
+
if (typeof value !== "object" || value === null || !(key in value)) return null;
|
|
28
|
+
const found = value[key];
|
|
29
|
+
return typeof found === "string" ? found : null;
|
|
30
|
+
}
|
|
31
|
+
function filterMatches(filter, data) {
|
|
32
|
+
if (filter == null) return true;
|
|
33
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
|
|
34
|
+
const record = data;
|
|
35
|
+
return Object.entries(filter).every(([key, expected]) => {
|
|
36
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) return false;
|
|
37
|
+
return JSON.stringify(record[key]) === JSON.stringify(expected);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function assertAuthorableTriggers(triggers) {
|
|
41
|
+
for (const trigger of triggers) {
|
|
42
|
+
if (trigger.startsWith("automation.")) throw new InvalidTriggerError(trigger);
|
|
43
|
+
if (trigger.startsWith("schedule:") && !cronSupported(trigger.slice(9))) throw new InvalidTriggerError(trigger, "schedule");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function assertCatalogTriggers(triggers, schedules) {
|
|
47
|
+
assertAuthorableTriggers(triggers);
|
|
48
|
+
const known = new Set(catalogTriggers().map((t) => t.type));
|
|
49
|
+
for (const trigger of triggers) if (!(schedules && trigger.startsWith("schedule:")) && !known.has(trigger)) throw new InvalidTriggerError(trigger, "unknown");
|
|
50
|
+
}
|
|
51
|
+
function assertAuthorableActions(actions) {
|
|
52
|
+
for (const action of actions) if (action.type === "deliverWebhook") throw new ReservedActionError(action.type);
|
|
53
|
+
}
|
|
54
|
+
const SCHEDULE_FIRED = "automation.schedule.fired";
|
|
55
|
+
function publicEventData(event) {
|
|
56
|
+
const parsed = event.type === SCHEDULE_FIRED ? {
|
|
57
|
+
success: true,
|
|
58
|
+
data: event.data
|
|
59
|
+
} : publicDataSchema(event.type)?.safeParse(event.data);
|
|
60
|
+
return parsed?.success ? JSON.parse(JSON.stringify(parsed.data)) : null;
|
|
61
|
+
}
|
|
62
|
+
/** A run as callers see it: its event carries only what a webhook of that event would. */
|
|
63
|
+
function toRunView(run) {
|
|
64
|
+
const { type, version, id, orgId, occurredAt } = run.event;
|
|
65
|
+
return {
|
|
66
|
+
...run,
|
|
67
|
+
event: {
|
|
68
|
+
type,
|
|
69
|
+
version,
|
|
70
|
+
id,
|
|
71
|
+
orgId,
|
|
72
|
+
occurredAt,
|
|
73
|
+
data: publicEventData(run.event)
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** An automation as callers see it: a webhook's credential reference stays internal. */
|
|
78
|
+
function toAutomationView(automation) {
|
|
79
|
+
return {
|
|
80
|
+
...automation,
|
|
81
|
+
actions: automation.actions.map((action) => {
|
|
82
|
+
if (action.type !== "deliverWebhook") return action;
|
|
83
|
+
const { secretRef: _secretRef, ...input } = action.input;
|
|
84
|
+
return {
|
|
85
|
+
...action,
|
|
86
|
+
input
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function webhookTarget(automation) {
|
|
92
|
+
return deliverWebhookInputSchema.parse(automation.actions[0]?.input);
|
|
93
|
+
}
|
|
94
|
+
function toWebhook(automation) {
|
|
95
|
+
return {
|
|
96
|
+
orgId: automation.orgId,
|
|
97
|
+
id: automation.id,
|
|
98
|
+
url: webhookTarget(automation).url,
|
|
99
|
+
events: automation.triggers,
|
|
100
|
+
description: automation.description,
|
|
101
|
+
enabled: automation.enabled,
|
|
102
|
+
createdAt: automation.createdAt,
|
|
103
|
+
updatedAt: automation.updatedAt
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function updateEvent(orgId, input, updated) {
|
|
107
|
+
if (isEnabledOnlyUpdate(input)) return input.enabled ? automationEvents.automationEnabled.make({
|
|
108
|
+
orgId,
|
|
109
|
+
data: updated
|
|
110
|
+
}) : automationEvents.automationDisabled.make({
|
|
111
|
+
orgId,
|
|
112
|
+
data: updated
|
|
113
|
+
});
|
|
114
|
+
return automationEvents.automationUpdated.make({
|
|
115
|
+
orgId,
|
|
116
|
+
data: updated
|
|
117
|
+
});
|
|
118
|
+
}
|
|
13
119
|
var AutomationsServiceImpl = class {
|
|
14
120
|
repo;
|
|
15
121
|
runsRepo;
|
|
@@ -18,6 +124,12 @@ var AutomationsServiceImpl = class {
|
|
|
18
124
|
mailer;
|
|
19
125
|
lookups;
|
|
20
126
|
integrations;
|
|
127
|
+
credentials;
|
|
128
|
+
webhooks;
|
|
129
|
+
mail;
|
|
130
|
+
grants;
|
|
131
|
+
desk;
|
|
132
|
+
scheduledWork;
|
|
21
133
|
logger;
|
|
22
134
|
constructor(params) {
|
|
23
135
|
this.repo = params.repo;
|
|
@@ -27,12 +139,87 @@ var AutomationsServiceImpl = class {
|
|
|
27
139
|
this.mailer = params.mailer;
|
|
28
140
|
this.lookups = params.lookups;
|
|
29
141
|
this.integrations = params.integrations;
|
|
142
|
+
this.credentials = params.credentials;
|
|
143
|
+
this.webhooks = params.webhooks;
|
|
144
|
+
this.mail = {
|
|
145
|
+
...params.mail,
|
|
146
|
+
recentMessageRecipients: (orgId, automationId, chatId, since) => this.runsRepo.recentRecipients(orgId, automationId, chatId, since)
|
|
147
|
+
};
|
|
148
|
+
this.grants = params.grants;
|
|
149
|
+
this.desk = params.desk;
|
|
150
|
+
this.scheduledWork = params.scheduledWork;
|
|
30
151
|
this.logger = params.logger ?? noopLogger;
|
|
31
152
|
}
|
|
32
153
|
async handle(event) {
|
|
33
154
|
if (event.type.startsWith("automation.")) return;
|
|
155
|
+
await this.seedFor(event);
|
|
34
156
|
const automations = await this.safeListByTrigger(event);
|
|
35
|
-
for (const automation of automations
|
|
157
|
+
for (const automation of automations) {
|
|
158
|
+
const matches = filterMatches(automation.filter, event.data);
|
|
159
|
+
if (!automation.enabled || !matches) {
|
|
160
|
+
if (automation.enabled && !matches) this.logger.debug("automation filter skipped", {
|
|
161
|
+
orgId: event.orgId,
|
|
162
|
+
automationId: automation.id,
|
|
163
|
+
trigger: event.type
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
await this.dispatchOne(automation, event);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
seedsFor(event) {
|
|
171
|
+
if (event.type === "organization.created") return orgSeeds();
|
|
172
|
+
if (event.type !== "content.community.created") return null;
|
|
173
|
+
const communityId = recordString(event.data, "id");
|
|
174
|
+
if (!communityId) {
|
|
175
|
+
this.logger.error("community seed missing id", {
|
|
176
|
+
orgId: event.orgId,
|
|
177
|
+
eventId: event.id
|
|
178
|
+
});
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return communitySeeds(communityId);
|
|
182
|
+
}
|
|
183
|
+
async seedFor(event) {
|
|
184
|
+
const rows = this.seedsFor(event);
|
|
185
|
+
if (!rows) return;
|
|
186
|
+
if (await this.uow.run(async ({ automations, outbox }) => {
|
|
187
|
+
const created = [];
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
const inserted = await automations.insertSeed(event.orgId, {
|
|
190
|
+
name: row.name,
|
|
191
|
+
description: row.description,
|
|
192
|
+
triggers: row.triggers,
|
|
193
|
+
actions: row.actions,
|
|
194
|
+
filter: row.filter,
|
|
195
|
+
kind: "workflow",
|
|
196
|
+
source: "system"
|
|
197
|
+
});
|
|
198
|
+
if (inserted) created.push(automationEvents.automationCreated.make({
|
|
199
|
+
orgId: event.orgId,
|
|
200
|
+
data: inserted
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
if (created.length > 0) {
|
|
204
|
+
await outbox.append(created);
|
|
205
|
+
this.logger.info("system automations seeded", {
|
|
206
|
+
orgId: event.orgId,
|
|
207
|
+
trigger: event.type,
|
|
208
|
+
count: created.length
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return created.length;
|
|
212
|
+
}) > 0) await this.reconcileSchedules(event.orgId);
|
|
213
|
+
}
|
|
214
|
+
async reconcileSchedules(orgId) {
|
|
215
|
+
try {
|
|
216
|
+
await this.engine.syncSchedules?.();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
this.logger.error("automation schedules not reconciled", {
|
|
219
|
+
orgId,
|
|
220
|
+
err
|
|
221
|
+
});
|
|
222
|
+
}
|
|
36
223
|
}
|
|
37
224
|
async safeListByTrigger(event) {
|
|
38
225
|
try {
|
|
@@ -41,55 +228,65 @@ var AutomationsServiceImpl = class {
|
|
|
41
228
|
this.logger.error("automations: failed to load trigger matches", {
|
|
42
229
|
orgId: event.orgId,
|
|
43
230
|
trigger: event.type,
|
|
44
|
-
|
|
231
|
+
err
|
|
45
232
|
});
|
|
46
233
|
return [];
|
|
47
234
|
}
|
|
48
235
|
}
|
|
49
236
|
async dispatchOne(automation, event) {
|
|
50
|
-
let run;
|
|
237
|
+
let run = null;
|
|
51
238
|
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
|
-
});
|
|
239
|
+
run = await this.openRun(automation, event);
|
|
70
240
|
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
|
-
});
|
|
241
|
+
await this.engine.dispatch(this.dispatchFor(automation, run, event));
|
|
78
242
|
} catch (err) {
|
|
79
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
80
243
|
this.logger.error("automation run failed", {
|
|
81
244
|
orgId: event.orgId,
|
|
82
245
|
automationId: automation.id,
|
|
83
246
|
runId: run?.id,
|
|
84
|
-
|
|
247
|
+
trigger: event.type,
|
|
248
|
+
eventId: event.id,
|
|
249
|
+
err
|
|
85
250
|
});
|
|
86
|
-
if (run) await this.recordFailure(run, message);
|
|
251
|
+
if (run) await this.recordFailure(run, err instanceof Error ? err.message : String(err));
|
|
87
252
|
}
|
|
88
253
|
}
|
|
254
|
+
openRun(automation, event) {
|
|
255
|
+
const now = /* @__PURE__ */ new Date();
|
|
256
|
+
return this.uow.run(async ({ runs, outbox }) => {
|
|
257
|
+
const inserted = await runs.insert(event.orgId, {
|
|
258
|
+
orgId: event.orgId,
|
|
259
|
+
automationId: automation.id,
|
|
260
|
+
trigger: event.type,
|
|
261
|
+
event,
|
|
262
|
+
status: "running",
|
|
263
|
+
actionResults: [],
|
|
264
|
+
startedAt: now,
|
|
265
|
+
finishedAt: null,
|
|
266
|
+
createdAt: now,
|
|
267
|
+
updatedAt: now
|
|
268
|
+
});
|
|
269
|
+
if (!inserted) return null;
|
|
270
|
+
await outbox.append([automationEvents.runStarted.make({
|
|
271
|
+
orgId: event.orgId,
|
|
272
|
+
data: inserted
|
|
273
|
+
})]);
|
|
274
|
+
return inserted;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
dispatchFor(automation, run, event) {
|
|
278
|
+
return {
|
|
279
|
+
runId: run.id,
|
|
280
|
+
orgId: event.orgId,
|
|
281
|
+
automationId: automation.id,
|
|
282
|
+
actions: automation.actions,
|
|
283
|
+
event
|
|
284
|
+
};
|
|
285
|
+
}
|
|
89
286
|
/** Best-effort: logs and never rethrows — `handle` must never throw. */
|
|
90
287
|
async recordFailure(run, error) {
|
|
91
288
|
try {
|
|
92
|
-
await this.uow.run(async ({ runs, outbox }) => {
|
|
289
|
+
return await this.uow.run(async ({ runs, outbox }) => {
|
|
93
290
|
const failed = await runs.recordOutcome(run.orgId, run.id, {
|
|
94
291
|
status: "failed",
|
|
95
292
|
actionResults: run.actionResults,
|
|
@@ -99,25 +296,113 @@ var AutomationsServiceImpl = class {
|
|
|
99
296
|
orgId: run.orgId,
|
|
100
297
|
data: failed
|
|
101
298
|
})]);
|
|
299
|
+
return failed;
|
|
102
300
|
});
|
|
103
301
|
} catch (err) {
|
|
104
302
|
this.logger.error("failed to record automation run failure", {
|
|
105
303
|
orgId: run.orgId,
|
|
304
|
+
automationId: run.automationId,
|
|
106
305
|
runId: run.id,
|
|
107
|
-
|
|
306
|
+
err,
|
|
108
307
|
originalError: error
|
|
109
308
|
});
|
|
309
|
+
return null;
|
|
110
310
|
}
|
|
111
311
|
}
|
|
112
312
|
async runAction(d, index) {
|
|
113
313
|
const action = d.actions[index];
|
|
114
314
|
if (!action) throw new Error(`automation ${d.automationId} run ${d.runId}: no action at index ${index}`);
|
|
115
|
-
|
|
315
|
+
let outcome;
|
|
316
|
+
try {
|
|
317
|
+
outcome = await executeAction(action, d.event, {
|
|
318
|
+
mailer: this.mailer,
|
|
319
|
+
lookups: this.lookups,
|
|
320
|
+
credentials: this.credentials,
|
|
321
|
+
webhooks: this.webhooks,
|
|
322
|
+
mail: this.mail,
|
|
323
|
+
grants: this.grants,
|
|
324
|
+
desk: this.desk,
|
|
325
|
+
scheduledWork: this.scheduledWork,
|
|
326
|
+
automationId: d.automationId,
|
|
327
|
+
actionInput: action.input,
|
|
328
|
+
logger: this.logger
|
|
329
|
+
});
|
|
330
|
+
} catch (err) {
|
|
331
|
+
this.logger.error("automation action failed", {
|
|
332
|
+
orgId: d.orgId,
|
|
333
|
+
automationId: d.automationId,
|
|
334
|
+
runId: d.runId,
|
|
335
|
+
trigger: d.event.type,
|
|
336
|
+
index,
|
|
337
|
+
type: action.type,
|
|
338
|
+
err
|
|
339
|
+
});
|
|
340
|
+
throw err instanceof ActionFailedError ? err : new ActionFailedError();
|
|
341
|
+
}
|
|
342
|
+
const { recipients, failed } = outcome;
|
|
343
|
+
this.logger.info("automation action completed", {
|
|
344
|
+
orgId: d.orgId,
|
|
345
|
+
automationId: d.automationId,
|
|
346
|
+
runId: d.runId,
|
|
347
|
+
type: action.type,
|
|
348
|
+
recipients: recipients.length,
|
|
349
|
+
failed: failed.length
|
|
350
|
+
});
|
|
116
351
|
return {
|
|
117
352
|
index,
|
|
118
353
|
type: action.type,
|
|
119
|
-
status: "completed"
|
|
354
|
+
status: "completed",
|
|
355
|
+
...recipients.length > 0 ? { recipients } : {},
|
|
356
|
+
...failed.length > 0 ? { error: `delivery failed for ${failed.length} recipients: ${failed.join(", ")}` } : {}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async listSchedules() {
|
|
360
|
+
return this.repo.listSchedules();
|
|
361
|
+
}
|
|
362
|
+
async fireSchedule(orgId, automationId, at) {
|
|
363
|
+
const firedAt = new Date(floorMinute(at));
|
|
364
|
+
const automation = await this.repo.findById(orgId, automationId);
|
|
365
|
+
if (!automation?.enabled || firedAt.getTime() < floorMinute(automation.createdAt)) {
|
|
366
|
+
this.logger.debug("schedule skipped", {
|
|
367
|
+
orgId,
|
|
368
|
+
automationId
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!await this.scheduleHasWork(orgId, automation, firedAt)) {
|
|
373
|
+
this.logger.debug("schedule idle", {
|
|
374
|
+
orgId,
|
|
375
|
+
automationId
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const event = {
|
|
380
|
+
type: SCHEDULE_FIRED,
|
|
381
|
+
id: `schedule_${automationId}_${firedAt.toISOString()}`,
|
|
382
|
+
orgId,
|
|
383
|
+
version: 1,
|
|
384
|
+
occurredAt: firedAt.toISOString(),
|
|
385
|
+
data: {
|
|
386
|
+
automationId,
|
|
387
|
+
firedAt: firedAt.toISOString()
|
|
388
|
+
}
|
|
120
389
|
};
|
|
390
|
+
await this.dispatchOne(automation, event);
|
|
391
|
+
}
|
|
392
|
+
async scheduleHasWork(orgId, automation, firedAt) {
|
|
393
|
+
const checks = automation.actions.map((action) => this.pendingWork(action.type));
|
|
394
|
+
const known = checks.filter((check) => check !== null);
|
|
395
|
+
if (checks.length === 0 || known.length < checks.length) return true;
|
|
396
|
+
for (const check of known) if (await check(orgId, firedAt)) return true;
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
pendingWork(type) {
|
|
400
|
+
switch (type) {
|
|
401
|
+
case "publishDuePosts": return (orgId, now) => this.scheduledWork.hasDuePosts(orgId, now);
|
|
402
|
+
case "expireDueGrants": return (orgId, now) => this.scheduledWork.hasDueGrants(orgId, now);
|
|
403
|
+
case "removeUnattachedUploads": return (orgId, now) => this.scheduledWork.hasUnattachedUploads(orgId, now);
|
|
404
|
+
default: return null;
|
|
405
|
+
}
|
|
121
406
|
}
|
|
122
407
|
async finalize(d, results) {
|
|
123
408
|
const status = results.length === d.actions.length && results.every((r) => r.status === "completed") ? "completed" : "failed";
|
|
@@ -141,11 +426,46 @@ var AutomationsServiceImpl = class {
|
|
|
141
426
|
}) }));
|
|
142
427
|
await outbox.append([runEvent, ...actionFailedEvents]);
|
|
143
428
|
});
|
|
429
|
+
this.logOutcome(d, status, results);
|
|
430
|
+
}
|
|
431
|
+
logOutcome(d, status, results) {
|
|
432
|
+
const run = {
|
|
433
|
+
orgId: d.orgId,
|
|
434
|
+
automationId: d.automationId,
|
|
435
|
+
runId: d.runId,
|
|
436
|
+
trigger: d.event.type,
|
|
437
|
+
eventId: d.event.id
|
|
438
|
+
};
|
|
439
|
+
if (status === "failed") {
|
|
440
|
+
const failed = results.find((r) => r.status === "failed");
|
|
441
|
+
const index = failed?.index ?? results.length;
|
|
442
|
+
this.logger.error("automation run failed", {
|
|
443
|
+
...run,
|
|
444
|
+
index,
|
|
445
|
+
action: failed?.type ?? d.actions[index]?.type,
|
|
446
|
+
error: failed?.error ?? "not every action ran"
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const partial = results.filter((r) => r.error !== void 0);
|
|
451
|
+
if (partial.length > 0) this.logger.warn("automation run completed with failed deliveries", {
|
|
452
|
+
...run,
|
|
453
|
+
actions: partial.map(({ index, type, error }) => ({
|
|
454
|
+
index,
|
|
455
|
+
type,
|
|
456
|
+
error
|
|
457
|
+
}))
|
|
458
|
+
});
|
|
144
459
|
}
|
|
145
460
|
async create(orgId, input) {
|
|
146
|
-
|
|
461
|
+
assertCatalogTriggers(input.triggers, true);
|
|
462
|
+
assertAuthorableActions(input.actions);
|
|
147
463
|
const automation = await this.uow.run(async ({ automations, outbox }) => {
|
|
148
|
-
const created = await automations.insert(orgId,
|
|
464
|
+
const created = await automations.insert(orgId, {
|
|
465
|
+
...input,
|
|
466
|
+
triggers: unique(input.triggers),
|
|
467
|
+
kind: "workflow"
|
|
468
|
+
});
|
|
149
469
|
await outbox.append([automationEvents.automationCreated.make({
|
|
150
470
|
orgId,
|
|
151
471
|
data: created
|
|
@@ -156,56 +476,219 @@ var AutomationsServiceImpl = class {
|
|
|
156
476
|
orgId,
|
|
157
477
|
automationId: automation.id
|
|
158
478
|
});
|
|
479
|
+
await this.reconcileSchedules(orgId);
|
|
159
480
|
return automation;
|
|
160
481
|
}
|
|
161
482
|
async update(orgId, id, input) {
|
|
162
|
-
if (input.
|
|
483
|
+
if (input.triggers !== void 0) assertCatalogTriggers(input.triggers, true);
|
|
484
|
+
if (input.actions !== void 0) assertAuthorableActions(input.actions);
|
|
485
|
+
const existing = await this.repo.findById(orgId, id);
|
|
486
|
+
if (!existing) return null;
|
|
487
|
+
if (existing.kind !== "workflow") throw new AutomationKindMismatchError(id, existing.kind);
|
|
488
|
+
const patch = input.triggers === void 0 ? input : {
|
|
489
|
+
...input,
|
|
490
|
+
triggers: unique(input.triggers)
|
|
491
|
+
};
|
|
163
492
|
const automation = await this.uow.run(async ({ automations, outbox }) => {
|
|
164
|
-
const updated = await automations.update(orgId, id,
|
|
493
|
+
const updated = await automations.update(orgId, id, patch);
|
|
165
494
|
if (!updated) return null;
|
|
166
|
-
|
|
495
|
+
await outbox.append([updateEvent(orgId, input, updated)]);
|
|
496
|
+
return updated;
|
|
497
|
+
});
|
|
498
|
+
if (automation) {
|
|
499
|
+
this.logger.info("automation updated", {
|
|
167
500
|
orgId,
|
|
168
|
-
|
|
169
|
-
})
|
|
501
|
+
automationId: id
|
|
502
|
+
});
|
|
503
|
+
await this.reconcileSchedules(orgId);
|
|
504
|
+
}
|
|
505
|
+
return automation;
|
|
506
|
+
}
|
|
507
|
+
async delete(orgId, id) {
|
|
508
|
+
const existing = await this.repo.findById(orgId, id);
|
|
509
|
+
if (!existing) return false;
|
|
510
|
+
if (existing.kind !== "workflow") throw new AutomationKindMismatchError(id, existing.kind);
|
|
511
|
+
if (existing.source === "system") throw new ConflictError("A system automation cannot be deleted");
|
|
512
|
+
const deleted = await this.uow.run(async ({ automations, outbox }) => {
|
|
513
|
+
const removed = await automations.delete(orgId, id);
|
|
514
|
+
if (!removed) return null;
|
|
515
|
+
await outbox.append([automationEvents.automationDeleted.make({
|
|
170
516
|
orgId,
|
|
171
|
-
data:
|
|
517
|
+
data: removed
|
|
172
518
|
})]);
|
|
173
|
-
|
|
519
|
+
return removed;
|
|
520
|
+
});
|
|
521
|
+
if (deleted) {
|
|
522
|
+
this.logger.info("automation deleted", {
|
|
174
523
|
orgId,
|
|
175
|
-
|
|
524
|
+
automationId: id
|
|
525
|
+
});
|
|
526
|
+
await this.reconcileSchedules(orgId);
|
|
527
|
+
}
|
|
528
|
+
return deleted !== null;
|
|
529
|
+
}
|
|
530
|
+
async list(orgId, query) {
|
|
531
|
+
return (await this.repo.listByOrg(orgId, query?.kind)).map(toAutomationView);
|
|
532
|
+
}
|
|
533
|
+
async get(orgId, id) {
|
|
534
|
+
const automation = await this.repo.findById(orgId, id);
|
|
535
|
+
return automation ? toAutomationView(automation) : null;
|
|
536
|
+
}
|
|
537
|
+
async listRuns(orgId, automationId, query) {
|
|
538
|
+
const page = await this.runsRepo.list(orgId, automationId, query);
|
|
539
|
+
return {
|
|
540
|
+
...page,
|
|
541
|
+
rows: page.rows.map(toRunView)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
async rerun(orgId, automationId, runId) {
|
|
545
|
+
const existing = await this.runsRepo.findById(orgId, runId);
|
|
546
|
+
if (!existing || existing.automationId !== automationId) throw new NotFoundError("Automation run", runId);
|
|
547
|
+
const automation = await this.repo.findById(orgId, automationId);
|
|
548
|
+
if (!automation) throw new NotFoundError("Automation", automationId);
|
|
549
|
+
const run = await this.uow.run(async ({ runs, outbox }) => {
|
|
550
|
+
const restarted = await runs.restart(orgId, runId, /* @__PURE__ */ new Date());
|
|
551
|
+
if (!restarted) return null;
|
|
552
|
+
await outbox.append([automationEvents.runStarted.make({
|
|
553
|
+
orgId,
|
|
554
|
+
data: restarted
|
|
176
555
|
})]);
|
|
177
|
-
return
|
|
556
|
+
return restarted;
|
|
557
|
+
});
|
|
558
|
+
if (!run) throw new ConflictError(`automation run ${runId} is still running`);
|
|
559
|
+
try {
|
|
560
|
+
await this.engine.dispatch(this.dispatchFor(automation, run, run.event));
|
|
561
|
+
} catch (err) {
|
|
562
|
+
this.logger.error("automation rerun failed", {
|
|
563
|
+
orgId,
|
|
564
|
+
automationId,
|
|
565
|
+
runId: run.id,
|
|
566
|
+
trigger: run.trigger,
|
|
567
|
+
err
|
|
568
|
+
});
|
|
569
|
+
return toRunView(await this.recordFailure(run, err instanceof Error ? err.message : String(err)) ?? run);
|
|
570
|
+
}
|
|
571
|
+
this.logger.info("automation rerun started", {
|
|
572
|
+
orgId,
|
|
573
|
+
automationId,
|
|
574
|
+
runId: run.id
|
|
575
|
+
});
|
|
576
|
+
return toRunView(run);
|
|
577
|
+
}
|
|
578
|
+
async listWebhooks(orgId) {
|
|
579
|
+
return (await this.repo.listByOrg(orgId, "webhook")).map(toWebhook);
|
|
580
|
+
}
|
|
581
|
+
async getWebhook(orgId, id) {
|
|
582
|
+
const automation = await this.findWebhook(orgId, id);
|
|
583
|
+
return automation ? toWebhook(automation) : null;
|
|
584
|
+
}
|
|
585
|
+
async createWebhook(orgId, input) {
|
|
586
|
+
const events = unique(input.events);
|
|
587
|
+
assertCatalogTriggers(events, false);
|
|
588
|
+
const secret = generateWebhookSecret();
|
|
589
|
+
const automation = await this.uow.run(async ({ automations, credentials, outbox }) => {
|
|
590
|
+
const secretRef = await credentials.store(orgId, { secret });
|
|
591
|
+
const created = await automations.insert(orgId, {
|
|
592
|
+
kind: "webhook",
|
|
593
|
+
name: input.url,
|
|
594
|
+
description: input.description,
|
|
595
|
+
triggers: events,
|
|
596
|
+
actions: [{
|
|
597
|
+
type: DELIVER_WEBHOOK_ACTION,
|
|
598
|
+
input: {
|
|
599
|
+
url: input.url,
|
|
600
|
+
secretRef
|
|
601
|
+
}
|
|
602
|
+
}]
|
|
603
|
+
});
|
|
604
|
+
await outbox.append([automationEvents.automationCreated.make({
|
|
605
|
+
orgId,
|
|
606
|
+
data: created
|
|
607
|
+
})]);
|
|
608
|
+
return created;
|
|
609
|
+
});
|
|
610
|
+
this.logger.info("webhook created", {
|
|
611
|
+
orgId,
|
|
612
|
+
automationId: automation.id
|
|
613
|
+
});
|
|
614
|
+
return {
|
|
615
|
+
...toWebhook(automation),
|
|
616
|
+
secret
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
async updateWebhook(orgId, id, input) {
|
|
620
|
+
const existing = await this.findWebhook(orgId, id);
|
|
621
|
+
if (!existing) return null;
|
|
622
|
+
const events = input.events === void 0 ? void 0 : unique(input.events);
|
|
623
|
+
if (events) assertCatalogTriggers(events, false);
|
|
624
|
+
const { secretRef } = webhookTarget(existing);
|
|
625
|
+
const patch = {
|
|
626
|
+
name: input.url,
|
|
627
|
+
description: input.description,
|
|
628
|
+
triggers: events,
|
|
629
|
+
actions: input.url === void 0 ? void 0 : [{
|
|
630
|
+
type: DELIVER_WEBHOOK_ACTION,
|
|
631
|
+
input: {
|
|
632
|
+
url: input.url,
|
|
633
|
+
secretRef
|
|
634
|
+
}
|
|
635
|
+
}],
|
|
636
|
+
enabled: input.enabled
|
|
637
|
+
};
|
|
638
|
+
const updated = await this.uow.run(async ({ automations, outbox }) => {
|
|
639
|
+
const row = await automations.update(orgId, id, patch);
|
|
640
|
+
if (!row) return null;
|
|
641
|
+
await outbox.append([updateEvent(orgId, input, row)]);
|
|
642
|
+
return row;
|
|
178
643
|
});
|
|
179
|
-
if (
|
|
644
|
+
if (!updated) return null;
|
|
645
|
+
this.logger.info("webhook updated", {
|
|
180
646
|
orgId,
|
|
181
647
|
automationId: id
|
|
182
648
|
});
|
|
183
|
-
return
|
|
649
|
+
return toWebhook(updated);
|
|
184
650
|
}
|
|
185
|
-
async
|
|
186
|
-
const
|
|
651
|
+
async deleteWebhook(orgId, id) {
|
|
652
|
+
const existing = await this.findWebhook(orgId, id);
|
|
653
|
+
if (!existing) return false;
|
|
654
|
+
const { secretRef } = webhookTarget(existing);
|
|
655
|
+
const deleted = await this.uow.run(async ({ automations, credentials, outbox }) => {
|
|
187
656
|
const removed = await automations.delete(orgId, id);
|
|
188
657
|
if (!removed) return null;
|
|
658
|
+
await credentials.destroy(orgId, secretRef);
|
|
189
659
|
await outbox.append([automationEvents.automationDeleted.make({
|
|
190
660
|
orgId,
|
|
191
661
|
data: removed
|
|
192
662
|
})]);
|
|
193
663
|
return removed;
|
|
194
664
|
});
|
|
195
|
-
if (deleted) this.logger.info("
|
|
665
|
+
if (deleted) this.logger.info("webhook deleted", {
|
|
196
666
|
orgId,
|
|
197
667
|
automationId: id
|
|
198
668
|
});
|
|
199
669
|
return deleted !== null;
|
|
200
670
|
}
|
|
201
|
-
|
|
202
|
-
|
|
671
|
+
async revealWebhookSecret(orgId, id) {
|
|
672
|
+
const existing = await this.findWebhook(orgId, id);
|
|
673
|
+
if (!existing) return null;
|
|
674
|
+
const secret = (await this.credentials.reveal(orgId, webhookTarget(existing).secretRef))?.["secret"];
|
|
675
|
+
if (typeof secret !== "string") throw new Error(`webhook ${id}: signing secret is missing`);
|
|
676
|
+
return { secret };
|
|
203
677
|
}
|
|
204
|
-
|
|
205
|
-
|
|
678
|
+
async rotateWebhookSecret(orgId, id) {
|
|
679
|
+
const existing = await this.findWebhook(orgId, id);
|
|
680
|
+
if (!existing) return null;
|
|
681
|
+
const secret = generateWebhookSecret();
|
|
682
|
+
await this.credentials.update(orgId, webhookTarget(existing).secretRef, { secret });
|
|
683
|
+
this.logger.info("webhook secret rotated", {
|
|
684
|
+
orgId,
|
|
685
|
+
automationId: id
|
|
686
|
+
});
|
|
687
|
+
return { secret };
|
|
206
688
|
}
|
|
207
|
-
|
|
208
|
-
|
|
689
|
+
async findWebhook(orgId, id) {
|
|
690
|
+
const automation = await this.repo.findById(orgId, id);
|
|
691
|
+
return automation?.kind === "webhook" ? automation : null;
|
|
209
692
|
}
|
|
210
693
|
availableActions() {
|
|
211
694
|
return [...catalogActions(), ...this.integrations.available().flatMap((integration) => integration.actions.map((action) => ({
|