n8n-nodes-base 3.0.1 → 3.1.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/cjs/build.tsbuildinfo +1 -1
- package/dist/credentials/OpenWeatherMapApi.credentials.d.ts.map +1 -1
- package/dist/credentials/OpenWeatherMapApi.credentials.js +8 -2
- package/dist/credentials/OpenWeatherMapApi.credentials.js.map +1 -1
- package/dist/credentials/PerplexityApi.credentials.d.ts.map +1 -1
- package/dist/credentials/PerplexityApi.credentials.js +2 -6
- package/dist/credentials/PerplexityApi.credentials.js.map +1 -1
- package/dist/node-definitions/.nodes-hash +1 -1
- package/dist/node-definitions/nodes/n8n-nodes-base/googleAnalytics/v2/resource_report/operation_get.ts +2 -2
- package/dist/node-definitions/nodes/n8n-nodes-base/microsoftOutlook/v2/resource_event/operation_create.ts +2 -2
- package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/index.schema.js +2 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/index.ts +3 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/resource_user/index.schema.js +18 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/resource_user/index.ts +10 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/resource_user/operation_get.schema.js +24 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/resource_user/operation_get.ts +59 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/openWeatherMap/index.ts +3 -1
- package/dist/node-definitions/nodes/n8n-nodes-base/openWeatherMap/v11.schema.js +32 -0
- package/dist/node-definitions/nodes/n8n-nodes-base/openWeatherMap/v11.ts +68 -0
- package/dist/nodes/EmailReadImap/v1/EmailReadImapV1.node.d.ts.map +1 -1
- package/dist/nodes/EmailReadImap/v1/EmailReadImapV1.node.js +2 -1
- package/dist/nodes/EmailReadImap/v1/EmailReadImapV1.node.js.map +1 -1
- package/dist/nodes/EmailReadImap/v2/EmailReadImapV2.node.d.ts.map +1 -1
- package/dist/nodes/EmailReadImap/v2/EmailReadImapV2.node.js +3 -2
- package/dist/nodes/EmailReadImap/v2/EmailReadImapV2.node.js.map +1 -1
- package/dist/nodes/Google/Calendar/GoogleCalendar.node.js +1 -1
- package/dist/nodes/Google/Calendar/GoogleCalendar.node.js.map +1 -1
- package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.d.ts.map +1 -1
- package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.js +39 -11
- package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.js.map +1 -1
- package/dist/nodes/Google/Gmail/Gmail.node.d.ts.map +1 -1
- package/dist/nodes/Google/Gmail/Gmail.node.js +1 -0
- package/dist/nodes/Google/Gmail/Gmail.node.js.map +1 -1
- package/dist/nodes/Google/Gmail/GmailTrigger.node.js +1 -1
- package/dist/nodes/Google/Gmail/GmailTrigger.node.js.map +1 -1
- package/dist/nodes/Jenkins/GenericFunctions.d.ts.map +1 -1
- package/dist/nodes/Jenkins/GenericFunctions.js +99 -9
- package/dist/nodes/Jenkins/GenericFunctions.js.map +1 -1
- package/dist/nodes/Linear/GenericFunctions.d.ts.map +1 -1
- package/dist/nodes/Linear/GenericFunctions.js +3 -2
- package/dist/nodes/Linear/GenericFunctions.js.map +1 -1
- package/dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js +3 -3
- package/dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js.map +1 -1
- package/dist/nodes/Microsoft/Teams/MicrosoftTeamsTrigger.node.d.ts.map +1 -1
- package/dist/nodes/Microsoft/Teams/MicrosoftTeamsTrigger.node.js +106 -112
- package/dist/nodes/Microsoft/Teams/MicrosoftTeamsTrigger.node.js.map +1 -1
- package/dist/nodes/Microsoft/Teams/v2/helpers/utils-trigger.d.ts +74 -6
- package/dist/nodes/Microsoft/Teams/v2/helpers/utils-trigger.d.ts.map +1 -1
- package/dist/nodes/Microsoft/Teams/v2/helpers/utils-trigger.js +367 -109
- package/dist/nodes/Microsoft/Teams/v2/helpers/utils-trigger.js.map +1 -1
- package/dist/nodes/Microsoft/Teams/v2/transport/forbiddenHints.d.ts +10 -0
- package/dist/nodes/Microsoft/Teams/v2/transport/forbiddenHints.d.ts.map +1 -1
- package/dist/nodes/Microsoft/Teams/v2/transport/forbiddenHints.js +24 -4
- package/dist/nodes/Microsoft/Teams/v2/transport/forbiddenHints.js.map +1 -1
- package/dist/nodes/N8n/N8n.node.d.ts.map +1 -1
- package/dist/nodes/N8n/N8n.node.js +7 -0
- package/dist/nodes/N8n/N8n.node.js.map +1 -1
- package/dist/nodes/N8n/UserDescription.d.ts +4 -0
- package/dist/nodes/N8n/UserDescription.d.ts.map +1 -0
- package/dist/nodes/N8n/UserDescription.js +86 -0
- package/dist/nodes/N8n/UserDescription.js.map +1 -0
- package/dist/nodes/Notion/shared/GenericFunctions.d.ts +3 -3
- package/dist/nodes/Notion/shared/GenericFunctions.d.ts.map +1 -1
- package/dist/nodes/OpenWeatherMap/OpenWeatherMap.node.d.ts.map +1 -1
- package/dist/nodes/OpenWeatherMap/OpenWeatherMap.node.js +12 -9
- package/dist/nodes/OpenWeatherMap/OpenWeatherMap.node.js.map +1 -1
- package/dist/nodes/Snowflake/GenericFunctions.d.ts +3 -3
- package/dist/nodes/Snowflake/GenericFunctions.d.ts.map +1 -1
- package/dist/nodes/Snowflake/GenericFunctions.js +12 -6
- package/dist/nodes/Snowflake/GenericFunctions.js.map +1 -1
- package/dist/nodes/Snowflake/Snowflake.node.d.ts.map +1 -1
- package/dist/nodes/Snowflake/Snowflake.node.js +4 -6
- package/dist/nodes/Snowflake/Snowflake.node.js.map +1 -1
- package/dist/nodes/Splunk/v1/GenericFunctions.d.ts +2 -2
- package/dist/nodes/Splunk/v1/GenericFunctions.d.ts.map +1 -1
- package/dist/nodes/Splunk/v1/GenericFunctions.js +10 -3
- package/dist/nodes/Splunk/v1/GenericFunctions.js.map +1 -1
- package/dist/nodes/Splunk/v1/SplunkV1.node.js +4 -4
- package/dist/nodes/Splunk/v1/SplunkV1.node.js.map +1 -1
- package/dist/nodes/Splunk/v2/actions/report/getAll.operation.js +1 -1
- package/dist/nodes/Splunk/v2/actions/report/getAll.operation.js.map +1 -1
- package/dist/nodes/Splunk/v2/actions/search/getAll.operation.js +1 -1
- package/dist/nodes/Splunk/v2/actions/search/getAll.operation.js.map +1 -1
- package/dist/nodes/Splunk/v2/actions/search/getResult.operation.js +1 -1
- package/dist/nodes/Splunk/v2/actions/search/getResult.operation.js.map +1 -1
- package/dist/nodes/Splunk/v2/actions/user/getAll.operation.d.ts +1 -1
- package/dist/nodes/Splunk/v2/actions/user/getAll.operation.d.ts.map +1 -1
- package/dist/nodes/Splunk/v2/actions/user/getAll.operation.js +2 -2
- package/dist/nodes/Splunk/v2/actions/user/getAll.operation.js.map +1 -1
- package/dist/nodes/Splunk/v2/helpers/utils.d.ts +2 -2
- package/dist/nodes/Splunk/v2/helpers/utils.d.ts.map +1 -1
- package/dist/nodes/Splunk/v2/helpers/utils.js +10 -3
- package/dist/nodes/Splunk/v2/helpers/utils.js.map +1 -1
- package/dist/types/credentials.json +2 -2
- package/dist/types/nodes.json +8 -8
- package/package.json +22 -17
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateClientState = generateClientState;
|
|
4
|
+
exports.logTrigger = logTrigger;
|
|
4
5
|
exports.isLiveSubscription = isLiveSubscription;
|
|
5
6
|
exports.getStoredSecret = getStoredSecret;
|
|
6
|
-
exports.
|
|
7
|
+
exports.getStoredIds = getStoredIds;
|
|
8
|
+
exports.deleteSubscriptions = deleteSubscriptions;
|
|
7
9
|
exports.fetchAllTeams = fetchAllTeams;
|
|
8
10
|
exports.fetchAllChannels = fetchAllChannels;
|
|
9
11
|
exports.createSubscription = createSubscription;
|
|
12
|
+
exports.createSubscriptions = createSubscriptions;
|
|
10
13
|
exports.listOwnSubscriptions = listOwnSubscriptions;
|
|
14
|
+
exports.markPendingReconcile = markPendingReconcile;
|
|
15
|
+
exports.reconcileSubscriptions = reconcileSubscriptions;
|
|
16
|
+
exports.storedSubscriptionsCover = storedSubscriptionsCover;
|
|
11
17
|
exports.isLifecycleNotification = isLifecycleNotification;
|
|
18
|
+
exports.isSubscriptionResponse = isSubscriptionResponse;
|
|
12
19
|
exports.handleLifecycleNotifications = handleLifecycleNotifications;
|
|
13
20
|
exports.verifyWebhook = verifyWebhook;
|
|
14
21
|
exports.getResourcePath = getResourcePath;
|
|
@@ -22,6 +29,14 @@ const forbiddenHints_1 = require("../transport/forbiddenHints");
|
|
|
22
29
|
function generateClientState() {
|
|
23
30
|
return (0, crypto_1.randomBytes)(32).toString('hex');
|
|
24
31
|
}
|
|
32
|
+
/** One shape for every trigger log line: the prefix, the workflow and node, then the fields. */
|
|
33
|
+
function logTrigger(level, message, fields = {}) {
|
|
34
|
+
this.logger[level](`Microsoft Teams Trigger: ${message}`, {
|
|
35
|
+
workflowId: this.getWorkflow().id,
|
|
36
|
+
nodeName: this.getNode().name,
|
|
37
|
+
...fields,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
25
40
|
// Graph caps Teams subscriptions at 4320 minutes. 4318 keeps a margin for clock skew.
|
|
26
41
|
const SUBSCRIPTION_TTL_MINUTES = 4318;
|
|
27
42
|
// A subscription this close to its expiry counts as gone, so the node replaces it in time.
|
|
@@ -38,25 +53,56 @@ function getStoredSecret(webhookData) {
|
|
|
38
53
|
const secret = webhookData.webhookSecret;
|
|
39
54
|
return typeof secret === 'string' && secret.length > 0 ? secret : undefined;
|
|
40
55
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
/** The stored subscription ids, strings only. */
|
|
57
|
+
function getStoredIds(webhookData) {
|
|
58
|
+
return Array.isArray(webhookData.subscriptionIds)
|
|
59
|
+
? webhookData.subscriptionIds.filter((id) => typeof id === 'string')
|
|
60
|
+
: [];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Deletes every id once, all in parallel, and returns the ids Graph refused to remove, in input
|
|
64
|
+
* order. Never throws: a 404 counts as done, an id that fails validation is warned only, and every
|
|
65
|
+
* other refusal is warned and returned, so the caller decides what it means.
|
|
66
|
+
*/
|
|
67
|
+
async function deleteSubscriptions(ids) {
|
|
68
|
+
const refused = await Promise.all([...new Set(ids)].map(async (id) => {
|
|
69
|
+
let path;
|
|
70
|
+
try {
|
|
71
|
+
path = transport_1.buildTeamsPath.call(this, ['/v1.0/subscriptions/', { id }]);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// No Graph call can ever succeed for this id, so it is not reported.
|
|
75
|
+
logTrigger.call(this, 'warn', 'failed to remove subscription', {
|
|
76
|
+
subscriptionId: id,
|
|
77
|
+
error: (0, ensure_error_1.ensureError)(error).message,
|
|
78
|
+
});
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await transport_1.microsoftApiRequest.call(this, 'DELETE', path);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof n8n_workflow_1.NodeApiError && error.httpCode === '404')
|
|
87
|
+
return [];
|
|
88
|
+
logTrigger.call(this, 'warn', 'failed to remove subscription', {
|
|
89
|
+
subscriptionId: id,
|
|
90
|
+
error: (0, ensure_error_1.ensureError)(error).message,
|
|
91
|
+
});
|
|
92
|
+
return [id];
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
return refused.flat();
|
|
50
96
|
}
|
|
97
|
+
// The reconcile removes an own subscription whose resource is missing from these lists, so
|
|
98
|
+
// they read every page: Graph pages a long list with `@odata.nextLink`.
|
|
51
99
|
async function fetchAllTeams() {
|
|
52
|
-
|
|
53
|
-
return teams;
|
|
100
|
+
return (await transport_1.microsoftApiRequestAllItems.call(this, 'value', 'GET', transport_1.joinedTeamsEndpoint.call(this)));
|
|
54
101
|
}
|
|
55
102
|
async function fetchAllChannels(teamId) {
|
|
56
103
|
// Route through buildTeamsPath so `teamId` is validated before interpolation
|
|
57
104
|
// (watch-all is also guarded in getResourcePath).
|
|
58
|
-
|
|
59
|
-
return channels;
|
|
105
|
+
return (await transport_1.microsoftApiRequestAllItems.call(this, 'value', 'GET', transport_1.buildTeamsPath.call(this, ['/v1.0/teams/', { id: teamId }, '/channels'])));
|
|
60
106
|
}
|
|
61
107
|
async function createSubscription(webhookUrl, resourcePath, clientState) {
|
|
62
108
|
const expirationTime = newSubscriptionExpiry();
|
|
@@ -72,48 +118,273 @@ async function createSubscription(webhookUrl, resourcePath, clientState) {
|
|
|
72
118
|
const response = (await transport_1.microsoftApiRequest.call(this, 'POST', '/v1.0/subscriptions', body));
|
|
73
119
|
return response;
|
|
74
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Creates one subscription per resource in parallel. Every POST settles first, so the
|
|
123
|
+
* ids that landed are known before the cleanup core runs next. An error is returned as
|
|
124
|
+
* thrown (`ensureError` keeps the object), so a Graph 403 keeps its hint copy.
|
|
125
|
+
*/
|
|
126
|
+
async function createSubscriptions(webhookUrl, resources, clientState) {
|
|
127
|
+
const results = await Promise.allSettled(resources.map(async (resource) => await createSubscription.call(this, webhookUrl, resource, clientState)));
|
|
128
|
+
const ids = [];
|
|
129
|
+
const errors = [];
|
|
130
|
+
results.forEach((result, index) => {
|
|
131
|
+
if (result.status === 'fulfilled') {
|
|
132
|
+
ids.push(result.value.id);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const error = (0, ensure_error_1.ensureError)(result.reason);
|
|
136
|
+
logTrigger.call(this, 'warn', 'failed to create subscription', {
|
|
137
|
+
resource: resources[index],
|
|
138
|
+
error: error.message,
|
|
139
|
+
});
|
|
140
|
+
errors.push(error);
|
|
141
|
+
});
|
|
142
|
+
return { ids, errors };
|
|
143
|
+
}
|
|
75
144
|
async function listOwnSubscriptions(webhookUrl) {
|
|
76
145
|
const subscriptions = (await transport_1.microsoftApiRequestAllItems.call(this, 'value', 'GET', '/v1.0/subscriptions'));
|
|
77
146
|
return subscriptions.filter((subscription) => subscription.notificationUrl === webhookUrl);
|
|
78
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Reads each id with GET by id, which returns the clientState that the listing hides, and
|
|
150
|
+
* says whether the subscription carries the stored secret. A 404 counts as another secret:
|
|
151
|
+
* nothing is there to keep. A failed read, or an answer without a clientState or for another
|
|
152
|
+
* id or URL, is unknown. Never throws.
|
|
153
|
+
*/
|
|
154
|
+
async function proveSecrets(ids, webhookUrl, clientState) {
|
|
155
|
+
const proofs = await Promise.all(ids.map(async (id) => {
|
|
156
|
+
try {
|
|
157
|
+
const answer = await transport_1.microsoftApiRequest.call(this, 'GET', transport_1.buildTeamsPath.call(this, ['/v1.0/subscriptions/', { id }]));
|
|
158
|
+
const answeredSecret = (0, is_record_1.isRecord)(answer) ? answer.clientState : undefined;
|
|
159
|
+
if (isSubscriptionResponse(answer) &&
|
|
160
|
+
answer.id === id &&
|
|
161
|
+
answer.notificationUrl === webhookUrl &&
|
|
162
|
+
typeof answeredSecret === 'string') {
|
|
163
|
+
return [id, answeredSecret === clientState ? 'match' : 'other'];
|
|
164
|
+
}
|
|
165
|
+
logTrigger.call(this, 'debug', 'could not prove the secret of a subscription', {
|
|
166
|
+
subscriptionId: id,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof n8n_workflow_1.NodeApiError && error.httpCode === '404')
|
|
171
|
+
return [id, 'other'];
|
|
172
|
+
logTrigger.call(this, 'debug', 'failed to read subscription', {
|
|
173
|
+
subscriptionId: id,
|
|
174
|
+
error: (0, ensure_error_1.ensureError)(error).message,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return [id, 'unknown'];
|
|
178
|
+
}));
|
|
179
|
+
return new Map(proofs);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Marks the workflow so that its next lifecycle batch lists and reconciles instead of only
|
|
183
|
+
* renewing, after a failed create, read or listing. Only a subscription that still delivers
|
|
184
|
+
* sends that batch: when every subscription of the workflow is gone, nothing reads the mark
|
|
185
|
+
* (ENT-537, ENT-545). Writes on change only: the static-data proxy marks every set and delete
|
|
186
|
+
* as a change, and each change is a database write.
|
|
187
|
+
*/
|
|
188
|
+
function markPendingReconcile(webhookData, pending) {
|
|
189
|
+
if (pending && webhookData.pendingReconcile !== true)
|
|
190
|
+
webhookData.pendingReconcile = true;
|
|
191
|
+
if (!pending && webhookData.pendingReconcile !== undefined)
|
|
192
|
+
delete webhookData.pendingReconcile;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Applies the ownership rule to one own listing: one live subscription with the stored secret
|
|
196
|
+
* per required resource stays, the uncovered resources are created with the stored secret, and
|
|
197
|
+
* every other listed own subscription goes, except a stored one beside a keeper, which is
|
|
198
|
+
* forgotten, and everything on a held resource. Reads and writes
|
|
199
|
+
* `subscriptionIds` itself, so the trust decision and the write share one source. Returns the
|
|
200
|
+
* create errors, and whether the next lifecycle batch must reconcile again. Never throws.
|
|
201
|
+
*/
|
|
202
|
+
async function reconcileSubscriptions(input) {
|
|
203
|
+
const { webhookUrl, clientState, subscriptions, renewed = new Set(), gone = new Set(), holdUnproven, } = input;
|
|
204
|
+
const webhookData = this.getWorkflowStaticData('node');
|
|
205
|
+
const storedIds = getStoredIds(webhookData);
|
|
206
|
+
const required = [...new Set(input.requiredResources)];
|
|
207
|
+
// Liveness is decided once: the awaited deletes below must not move the line.
|
|
208
|
+
const live = new Set(subscriptions
|
|
209
|
+
.filter((sub) => !gone.has(sub.id) && (renewed.has(sub.id) || isLiveSubscription(sub)))
|
|
210
|
+
.map((sub) => sub.id));
|
|
211
|
+
const isStored = (sub) => storedIds.includes(sub.id) && !gone.has(sub.id);
|
|
212
|
+
// Renewed first, then stored, then the later expiry, then the id, so requests that see the
|
|
213
|
+
// same subscriptions keep the same one whatever the listing order.
|
|
214
|
+
const byPreference = (a, b) => Number(renewed.has(b.id)) - Number(renewed.has(a.id)) ||
|
|
215
|
+
Number(isStored(b)) - Number(isStored(a)) ||
|
|
216
|
+
(Date.parse(b.expirationDateTime) || 0) - (Date.parse(a.expirationDateTime) || 0) ||
|
|
217
|
+
(a.id < b.id ? -1 : 1);
|
|
218
|
+
// A renewal in this request proves the secret: its lifecycle item passed verification. Earlier
|
|
219
|
+
// versions stored every own subscription, whatever its secret, so storage alone decides only
|
|
220
|
+
// for a stored subscription that is the only live own one and the only stored one on its
|
|
221
|
+
// resource. Elsewhere GET by id proves the live ones, so a live listed subscription with the
|
|
222
|
+
// stored secret goes only beside a keeper, or at activation when it is not stored and its read
|
|
223
|
+
// failed. Stale, unlisted and no longer required ones go without a read.
|
|
224
|
+
const keepers = new Map();
|
|
225
|
+
const unproven = [];
|
|
226
|
+
for (const resource of required) {
|
|
227
|
+
const onResource = subscriptions.filter((sub) => sub.resource === resource);
|
|
228
|
+
const liveHere = onResource.filter((sub) => live.has(sub.id));
|
|
229
|
+
const storedLive = liveHere.filter(isStored);
|
|
230
|
+
const [renewedKeeper] = storedLive.filter((sub) => renewed.has(sub.id)).sort(byPreference);
|
|
231
|
+
if (renewedKeeper)
|
|
232
|
+
keepers.set(resource, renewedKeeper.id);
|
|
233
|
+
else if (storedLive.length === 1 &&
|
|
234
|
+
liveHere.length === 1 &&
|
|
235
|
+
onResource.filter(isStored).length === 1) {
|
|
236
|
+
keepers.set(resource, storedLive[0].id);
|
|
237
|
+
}
|
|
238
|
+
else
|
|
239
|
+
unproven.push(...liveHere);
|
|
240
|
+
}
|
|
241
|
+
const proofs = await proveSecrets.call(this, unproven.map((sub) => sub.id), webhookUrl, clientState);
|
|
242
|
+
// A read can fail for a reason about the transport, a 429 for example, not about the
|
|
243
|
+
// subscription, so a failed read never costs a stored subscription.
|
|
244
|
+
const held = new Set();
|
|
245
|
+
for (const resource of required) {
|
|
246
|
+
if (keepers.has(resource))
|
|
247
|
+
continue;
|
|
248
|
+
const candidates = unproven.filter((sub) => sub.resource === resource);
|
|
249
|
+
if (candidates.some((sub) => proofs.get(sub.id) === 'unknown' && (holdUnproven || isStored(sub)))) {
|
|
250
|
+
held.add(resource);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const [keeper] = candidates.filter((sub) => proofs.get(sub.id) === 'match').sort(byPreference);
|
|
254
|
+
if (keeper)
|
|
255
|
+
keepers.set(resource, keeper.id);
|
|
256
|
+
}
|
|
257
|
+
const keeperIds = new Set(keepers.values());
|
|
258
|
+
// A stored subscription beside a keeper is forgotten, not deleted, whatever its listed
|
|
259
|
+
// expiry: a request that renewed it can run at the same time and keep it, and two requests
|
|
260
|
+
// that share the stored ids must not delete each other's keeper. Unless it expires first, one
|
|
261
|
+
// with the stored secret names an unknown id at its next renewal, and that reconcile removes
|
|
262
|
+
// it. One with another secret is never renewed and expires, or goes now when GET by id
|
|
263
|
+
// proved it.
|
|
264
|
+
const forgotten = new Set(subscriptions
|
|
265
|
+
.filter((sub) => isStored(sub) &&
|
|
266
|
+
keepers.has(sub.resource) &&
|
|
267
|
+
!keeperIds.has(sub.id) &&
|
|
268
|
+
proofs.get(sub.id) !== 'other')
|
|
269
|
+
.map((sub) => sub.id));
|
|
270
|
+
const listedIds = new Set(subscriptions.map((sub) => sub.id));
|
|
271
|
+
const storedAbsent = storedIds.filter((id) => !listedIds.has(id) && !gone.has(id));
|
|
272
|
+
// The deletes finish before the first create: Graph refuses a second
|
|
273
|
+
// `/teams/{id}/channels` subscription.
|
|
274
|
+
const refused = new Set(await deleteSubscriptions.call(this, [
|
|
275
|
+
...subscriptions
|
|
276
|
+
.filter((sub) => !keeperIds.has(sub.id) && !forgotten.has(sub.id) && !held.has(sub.resource))
|
|
277
|
+
.map((sub) => sub.id),
|
|
278
|
+
...storedAbsent,
|
|
279
|
+
]));
|
|
280
|
+
const { ids: created, errors } = await createSubscriptions.call(this, webhookUrl, required.filter((resource) => !keepers.has(resource) && !held.has(resource)), clientState);
|
|
281
|
+
// A stored id that is not listed and that Graph refused to remove stays, so the next
|
|
282
|
+
// reconcile retries the delete; the by-URL listing cannot find it. The stored ids of a held
|
|
283
|
+
// resource stay too, so the next reconcile sees the same subscriptions.
|
|
284
|
+
const retained = storedAbsent.filter((id) => refused.has(id));
|
|
285
|
+
const heldIds = subscriptions
|
|
286
|
+
.filter((sub) => isStored(sub) && held.has(sub.resource))
|
|
287
|
+
.map((sub) => sub.id);
|
|
288
|
+
const ids = [
|
|
289
|
+
...new Set([
|
|
290
|
+
...required.flatMap((resource) => keepers.get(resource) ?? []),
|
|
291
|
+
...heldIds,
|
|
292
|
+
...retained,
|
|
293
|
+
...created,
|
|
294
|
+
]),
|
|
295
|
+
];
|
|
296
|
+
// Every save is a DB write and widens the last-write-wins window, so save on change only.
|
|
297
|
+
const raw = Array.isArray(webhookData.subscriptionIds)
|
|
298
|
+
? webhookData.subscriptionIds
|
|
299
|
+
: undefined;
|
|
300
|
+
const unchanged = raw !== undefined && raw.length === ids.length && ids.every((id) => raw.includes(id));
|
|
301
|
+
if (!unchanged)
|
|
302
|
+
webhookData.subscriptionIds = ids;
|
|
303
|
+
return { errors, retry: errors.length > 0 || held.size > 0 };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Whether the stored ids alone cover every required resource with a live subscription at
|
|
307
|
+
* this URL. Reads each stored id with GET by id, which has its own budget: 2000 requests per
|
|
308
|
+
* 20 s for an app across all tenants, against 40 for the listing. Two stored subscriptions on
|
|
309
|
+
* one resource both count: keeping them is safer than core's cleanup, and the next reconcile
|
|
310
|
+
* proves each secret. An answer with another clientState covers nothing. An answer without
|
|
311
|
+
* one still counts here; the reconcile treats it as unproven. Never throws.
|
|
312
|
+
*/
|
|
313
|
+
async function storedSubscriptionsCover(webhookUrl, clientState, event) {
|
|
314
|
+
let required;
|
|
315
|
+
try {
|
|
316
|
+
required = [await getResourcePath.call(this, event)].flat();
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
const covered = new Set();
|
|
322
|
+
await Promise.all(getStoredIds(this.getWorkflowStaticData('node')).map(async (id) => {
|
|
323
|
+
try {
|
|
324
|
+
const answer = await transport_1.microsoftApiRequest.call(this, 'GET', transport_1.buildTeamsPath.call(this, ['/v1.0/subscriptions/', { id }]));
|
|
325
|
+
const answeredSecret = (0, is_record_1.isRecord)(answer) ? answer.clientState : undefined;
|
|
326
|
+
if (isSubscriptionResponse(answer) &&
|
|
327
|
+
answer.id === id &&
|
|
328
|
+
answer.notificationUrl === webhookUrl &&
|
|
329
|
+
isLiveSubscription(answer) &&
|
|
330
|
+
(typeof answeredSecret !== 'string' || answeredSecret === clientState)) {
|
|
331
|
+
covered.add(answer.resource);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
logTrigger.call(this, 'debug', 'stored subscription does not count', {
|
|
335
|
+
subscriptionId: id,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
logTrigger.call(this, 'debug', 'failed to read stored subscription', {
|
|
340
|
+
subscriptionId: id,
|
|
341
|
+
error: (0, ensure_error_1.ensureError)(error).message,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}));
|
|
345
|
+
return required.every((resource) => covered.has(resource));
|
|
346
|
+
}
|
|
79
347
|
// Anything with a `lifecycleEvent` is a lifecycle item and never a workflow item; the
|
|
80
348
|
// handler checks the id before it uses it.
|
|
81
349
|
function isLifecycleNotification(item) {
|
|
82
350
|
return (0, is_record_1.isRecord)(item) && typeof item.lifecycleEvent === 'string';
|
|
83
351
|
}
|
|
352
|
+
/** A PATCH answer that can stand in for a listing entry. */
|
|
353
|
+
function isSubscriptionResponse(value) {
|
|
354
|
+
return ((0, is_record_1.isRecord)(value) &&
|
|
355
|
+
typeof value.id === 'string' &&
|
|
356
|
+
typeof value.resource === 'string' &&
|
|
357
|
+
typeof value.expirationDateTime === 'string' &&
|
|
358
|
+
typeof value.notificationUrl === 'string');
|
|
359
|
+
}
|
|
84
360
|
/**
|
|
85
361
|
* Runs after `webhook()` sent the 202. Never throws: a throw skips the static-data
|
|
86
|
-
* save and core would answer an ended response.
|
|
87
|
-
*
|
|
362
|
+
* save and core would answer an ended response. Renews the stored ids the batch names;
|
|
363
|
+
* a batch with a removal, an unknown id, a failed renewal or a rejected item lists once
|
|
364
|
+
* and reconciles, and so does a trigger that watches one resource but stores more ids, or
|
|
365
|
+
* one that an earlier batch marked because its reconcile did not finish. A batch arrives only
|
|
366
|
+
* while a subscription still delivers.
|
|
88
367
|
*/
|
|
89
|
-
async function handleLifecycleNotifications(items) {
|
|
368
|
+
async function handleLifecycleNotifications(items, rejectedCount = 0) {
|
|
90
369
|
const webhookData = this.getWorkflowStaticData('node');
|
|
91
|
-
const meta = { workflowId: this.getWorkflow().id, nodeName: this.getNode().name };
|
|
92
|
-
const log = (level, message, fields) => this.logger[level](`Microsoft Teams Trigger: ${message}`, { ...meta, ...fields });
|
|
93
370
|
// Do not make Graph calls if this helper is invoked without a stored secret.
|
|
94
371
|
const clientState = getStoredSecret(webhookData);
|
|
95
372
|
if (clientState === undefined) {
|
|
96
|
-
|
|
373
|
+
logTrigger.call(this, 'warn', 'skipped lifecycle notifications: no stored secret, republish the workflow');
|
|
97
374
|
return;
|
|
98
375
|
}
|
|
99
376
|
const webhookUrl = this.getNodeWebhookUrl('default');
|
|
100
377
|
// Type narrowing only: the subscriptions were created with this URL.
|
|
101
378
|
if (webhookUrl === undefined)
|
|
102
379
|
return;
|
|
103
|
-
const
|
|
104
|
-
? webhookData.subscriptionIds
|
|
105
|
-
: [];
|
|
106
|
-
const storedIds = rawIds.filter((id) => typeof id === 'string');
|
|
107
|
-
// One listing per request; a rejected listing is never fetched again.
|
|
108
|
-
let ownList;
|
|
109
|
-
const ownSubscriptions = async () => await (ownList ??= listOwnSubscriptions.call(this, webhookUrl));
|
|
380
|
+
const storedIds = getStoredIds(webhookData);
|
|
110
381
|
// Classify first, so the order inside the batch does not change the outcome. Logged
|
|
111
382
|
// request values are capped: the sender holds the secret, but the body is still input.
|
|
112
383
|
const gone = new Set();
|
|
113
384
|
const toRenew = new Set();
|
|
114
385
|
for (const { subscriptionId, lifecycleEvent } of items) {
|
|
115
386
|
if (typeof subscriptionId !== 'string') {
|
|
116
|
-
|
|
387
|
+
logTrigger.call(this, 'debug', 'ignored lifecycle event without a subscription id', {
|
|
117
388
|
lifecycleEvent: lifecycleEvent.slice(0, 64),
|
|
118
389
|
});
|
|
119
390
|
}
|
|
@@ -124,121 +395,105 @@ async function handleLifecycleNotifications(items) {
|
|
|
124
395
|
toRenew.add(subscriptionId);
|
|
125
396
|
}
|
|
126
397
|
else {
|
|
127
|
-
|
|
398
|
+
logTrigger.call(this, 'debug', 'ignored lifecycle event', {
|
|
128
399
|
subscriptionId: subscriptionId.slice(0, 64),
|
|
129
400
|
lifecycleEvent: lifecycleEvent.slice(0, 64),
|
|
130
401
|
});
|
|
131
402
|
}
|
|
132
403
|
}
|
|
133
|
-
const unresolved = new Set();
|
|
134
404
|
const renewed = new Set();
|
|
135
|
-
|
|
405
|
+
const renewedEntries = new Map();
|
|
136
406
|
for (const subscriptionId of toRenew) {
|
|
137
407
|
if (gone.has(subscriptionId))
|
|
138
408
|
continue;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
storedIds.includes(subscriptionId) ||
|
|
143
|
-
(await ownSubscriptions()).some((subscription) => subscription.id === subscriptionId);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Route to the reconcile, which awaits the same rejected listing and warns once.
|
|
147
|
-
unresolved.add(subscriptionId);
|
|
148
|
-
needsReconcile = true;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (!isOwn) {
|
|
152
|
-
log('debug', 'skipped renewal: unknown subscription', {
|
|
409
|
+
// An own id that is not stored is an orphan; the reconcile below removes it.
|
|
410
|
+
if (!storedIds.includes(subscriptionId)) {
|
|
411
|
+
logTrigger.call(this, 'debug', 'skipped renewal: unknown subscription', {
|
|
153
412
|
subscriptionId: subscriptionId.slice(0, 64),
|
|
154
413
|
});
|
|
155
|
-
needsReconcile = true;
|
|
156
414
|
continue;
|
|
157
415
|
}
|
|
158
416
|
try {
|
|
159
|
-
await transport_1.microsoftApiRequest.call(this, 'PATCH', transport_1.buildTeamsPath.call(this, ['/v1.0/subscriptions/', { id: subscriptionId }]), { expirationDateTime: newSubscriptionExpiry() });
|
|
417
|
+
const answer = await transport_1.microsoftApiRequest.call(this, 'PATCH', transport_1.buildTeamsPath.call(this, ['/v1.0/subscriptions/', { id: subscriptionId }]), { expirationDateTime: newSubscriptionExpiry() });
|
|
160
418
|
renewed.add(subscriptionId);
|
|
161
|
-
|
|
419
|
+
logTrigger.call(this, 'debug', 'renewed subscription', { subscriptionId });
|
|
420
|
+
// The answer stands in for a listing entry that can lag. A stored id at an old
|
|
421
|
+
// webhook URL must not become a keeper for the current URL through its answer.
|
|
422
|
+
if (isSubscriptionResponse(answer) &&
|
|
423
|
+
answer.id === subscriptionId &&
|
|
424
|
+
answer.notificationUrl === webhookUrl) {
|
|
425
|
+
renewedEntries.set(subscriptionId, answer);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
logTrigger.call(this, 'debug', 'renewed subscription without a usable answer', {
|
|
429
|
+
subscriptionId,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
162
432
|
}
|
|
163
433
|
catch (error) {
|
|
164
434
|
if (error instanceof n8n_workflow_1.NodeApiError && error.httpCode === '404') {
|
|
165
435
|
gone.add(subscriptionId);
|
|
166
|
-
|
|
167
|
-
log('debug', 'subscription gone, will re-create', { subscriptionId });
|
|
436
|
+
logTrigger.call(this, 'debug', 'subscription gone, will re-create', { subscriptionId });
|
|
168
437
|
}
|
|
169
438
|
else {
|
|
170
|
-
|
|
439
|
+
logTrigger.call(this, 'warn', 'failed to renew subscription', {
|
|
171
440
|
subscriptionId,
|
|
172
441
|
error: (0, ensure_error_1.ensureError)(error).message,
|
|
173
442
|
});
|
|
174
443
|
}
|
|
175
444
|
}
|
|
176
445
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
446
|
+
// A plain renewal lists nothing: Graph allows 25 `GET /subscriptions` per 20 s per app
|
|
447
|
+
// per tenant and 40 per 20 s per app across tenants. A trigger that watches one resource
|
|
448
|
+
// but stores more ids lists anyway: earlier versions stored duplicates with the valid
|
|
449
|
+
// secret, and they renew forever unless a listing removes them.
|
|
450
|
+
const event = this.getNodeParameter('event', '');
|
|
451
|
+
const watchesMany = (event === 'newChannel' || event === 'newChannelMessage' || event === 'newTeamMember') &&
|
|
452
|
+
(this.getNodeParameter('watchAllTeams', false) === true ||
|
|
453
|
+
(event === 'newChannelMessage' && this.getNodeParameter('watchAllChannels', false) === true));
|
|
454
|
+
// ponytail: no backoff for a marked workflow; it lists at most once per lifecycle batch, and
|
|
455
|
+
// Graph sets the batch rate and documents no limit. Add a backoff if marked workflows show up
|
|
456
|
+
// in the listing budget.
|
|
457
|
+
const renewOnly = webhookData.pendingReconcile !== true &&
|
|
458
|
+
rejectedCount === 0 &&
|
|
459
|
+
gone.size === 0 &&
|
|
460
|
+
[...toRenew].every((id) => renewed.has(id)) &&
|
|
461
|
+
(watchesMany || storedIds.length <= 1);
|
|
462
|
+
if (renewOnly)
|
|
182
463
|
return;
|
|
183
|
-
}
|
|
184
|
-
const affectedIds = [...gone, ...unresolved].slice(0, 20).map((id) => id.slice(0, 64));
|
|
185
464
|
let subscriptions;
|
|
186
465
|
let requiredResources;
|
|
187
466
|
try {
|
|
188
|
-
subscriptions = await
|
|
189
|
-
|
|
190
|
-
|
|
467
|
+
subscriptions = await listOwnSubscriptions.call(this, webhookUrl);
|
|
468
|
+
requiredResources = [
|
|
469
|
+
await getResourcePath.call(this, this.getNodeParameter('event')),
|
|
470
|
+
].flat();
|
|
191
471
|
}
|
|
192
472
|
catch (error) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
473
|
+
const affectedIds = [...gone, ...toRenew].slice(0, 20).map((id) => id.slice(0, 64));
|
|
474
|
+
logTrigger.call(this, 'warn', 'failed to list subscriptions or resolve resources, skipped re-create', { subscriptionIds: affectedIds, error: (0, ensure_error_1.ensureError)(error).message });
|
|
475
|
+
markPendingReconcile(webhookData, true);
|
|
197
476
|
return;
|
|
198
477
|
}
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
error: (0, ensure_error_1.ensureError)(error).message,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const liveResources = new Set(live.map((subscription) => subscription.resource));
|
|
221
|
-
const created = [];
|
|
222
|
-
for (const resource of requiredResources.filter((path) => !liveResources.has(path))) {
|
|
223
|
-
try {
|
|
224
|
-
const subscription = await createSubscription.call(this, webhookUrl, resource, clientState);
|
|
225
|
-
created.push(subscription.id);
|
|
226
|
-
log('debug', 're-created subscription', { resource, subscriptionId: subscription.id });
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
log('warn', 'failed to re-create subscription', {
|
|
230
|
-
resource,
|
|
231
|
-
subscriptionIds: affectedIds,
|
|
232
|
-
error: (0, ensure_error_1.ensureError)(error).message,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const nextIds = [...live.map((subscription) => subscription.id), ...created];
|
|
237
|
-
// Every save is a DB write and widens the last-write-wins window, so save on change only.
|
|
238
|
-
const unchanged = nextIds.length === rawIds.length && nextIds.every((id) => rawIds.includes(id));
|
|
239
|
-
if (!unchanged)
|
|
240
|
-
webhookData.subscriptionIds = nextIds;
|
|
478
|
+
// An orphan whose own challenge fails verification never reaches this handler, so a
|
|
479
|
+
// batch with an anomaly (a removal, an unknown id, a failed renewal, a rejected item)
|
|
480
|
+
// must prune for it. The helpers log the failures. A failed create or read marks the
|
|
481
|
+
// workflow, so a later batch reconciles again instead of only renewing.
|
|
482
|
+
const { retry } = await reconcileSubscriptions.call(this, {
|
|
483
|
+
webhookUrl,
|
|
484
|
+
clientState,
|
|
485
|
+
requiredResources,
|
|
486
|
+
subscriptions: [
|
|
487
|
+
...subscriptions.filter((subscription) => !renewedEntries.has(subscription.id)),
|
|
488
|
+
...renewedEntries.values(),
|
|
489
|
+
],
|
|
490
|
+
renewed,
|
|
491
|
+
gone,
|
|
492
|
+
holdUnproven: true,
|
|
493
|
+
});
|
|
494
|
+
markPendingReconcile(webhookData, retry);
|
|
241
495
|
}
|
|
496
|
+
/** The accepted items of the request body; `[]` for a missing, empty or non-array body. */
|
|
242
497
|
function verifyWebhook() {
|
|
243
498
|
const req = this.getRequestObject();
|
|
244
499
|
const webhookData = this.getWorkflowStaticData('node');
|
|
@@ -246,9 +501,12 @@ function verifyWebhook() {
|
|
|
246
501
|
const body = req.body;
|
|
247
502
|
const notifications = body?.value;
|
|
248
503
|
if (!Array.isArray(notifications) || notifications.length === 0) {
|
|
249
|
-
return
|
|
504
|
+
return [];
|
|
250
505
|
}
|
|
251
|
-
|
|
506
|
+
// Graph batches several subscriptions into one POST, so an item from another
|
|
507
|
+
// subscription must not drop the valid ones. Without a stored secret no item is
|
|
508
|
+
// accepted.
|
|
509
|
+
return notifications.filter((notification) => {
|
|
252
510
|
const actualClientState = typeof notification === 'object' && notification !== null
|
|
253
511
|
? notification.clientState
|
|
254
512
|
: undefined;
|