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.
Files changed (96) hide show
  1. package/dist/cjs/build.tsbuildinfo +1 -1
  2. package/dist/credentials/OpenWeatherMapApi.credentials.d.ts.map +1 -1
  3. package/dist/credentials/OpenWeatherMapApi.credentials.js +8 -2
  4. package/dist/credentials/OpenWeatherMapApi.credentials.js.map +1 -1
  5. package/dist/credentials/PerplexityApi.credentials.d.ts.map +1 -1
  6. package/dist/credentials/PerplexityApi.credentials.js +2 -6
  7. package/dist/credentials/PerplexityApi.credentials.js.map +1 -1
  8. package/dist/node-definitions/.nodes-hash +1 -1
  9. package/dist/node-definitions/nodes/n8n-nodes-base/googleAnalytics/v2/resource_report/operation_get.ts +2 -2
  10. package/dist/node-definitions/nodes/n8n-nodes-base/microsoftOutlook/v2/resource_event/operation_create.ts +2 -2
  11. package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/index.schema.js +2 -0
  12. package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/index.ts +3 -0
  13. package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/resource_user/index.schema.js +18 -0
  14. package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/resource_user/index.ts +10 -0
  15. package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/resource_user/operation_get.schema.js +24 -0
  16. package/dist/node-definitions/nodes/n8n-nodes-base/n8n/v1/resource_user/operation_get.ts +59 -0
  17. package/dist/node-definitions/nodes/n8n-nodes-base/openWeatherMap/index.ts +3 -1
  18. package/dist/node-definitions/nodes/n8n-nodes-base/openWeatherMap/v11.schema.js +32 -0
  19. package/dist/node-definitions/nodes/n8n-nodes-base/openWeatherMap/v11.ts +68 -0
  20. package/dist/nodes/EmailReadImap/v1/EmailReadImapV1.node.d.ts.map +1 -1
  21. package/dist/nodes/EmailReadImap/v1/EmailReadImapV1.node.js +2 -1
  22. package/dist/nodes/EmailReadImap/v1/EmailReadImapV1.node.js.map +1 -1
  23. package/dist/nodes/EmailReadImap/v2/EmailReadImapV2.node.d.ts.map +1 -1
  24. package/dist/nodes/EmailReadImap/v2/EmailReadImapV2.node.js +3 -2
  25. package/dist/nodes/EmailReadImap/v2/EmailReadImapV2.node.js.map +1 -1
  26. package/dist/nodes/Google/Calendar/GoogleCalendar.node.js +1 -1
  27. package/dist/nodes/Google/Calendar/GoogleCalendar.node.js.map +1 -1
  28. package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.d.ts.map +1 -1
  29. package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.js +39 -11
  30. package/dist/nodes/Google/Drive/GoogleDriveTrigger.node.js.map +1 -1
  31. package/dist/nodes/Google/Gmail/Gmail.node.d.ts.map +1 -1
  32. package/dist/nodes/Google/Gmail/Gmail.node.js +1 -0
  33. package/dist/nodes/Google/Gmail/Gmail.node.js.map +1 -1
  34. package/dist/nodes/Google/Gmail/GmailTrigger.node.js +1 -1
  35. package/dist/nodes/Google/Gmail/GmailTrigger.node.js.map +1 -1
  36. package/dist/nodes/Jenkins/GenericFunctions.d.ts.map +1 -1
  37. package/dist/nodes/Jenkins/GenericFunctions.js +99 -9
  38. package/dist/nodes/Jenkins/GenericFunctions.js.map +1 -1
  39. package/dist/nodes/Linear/GenericFunctions.d.ts.map +1 -1
  40. package/dist/nodes/Linear/GenericFunctions.js +3 -2
  41. package/dist/nodes/Linear/GenericFunctions.js.map +1 -1
  42. package/dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js +3 -3
  43. package/dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js.map +1 -1
  44. package/dist/nodes/Microsoft/Teams/MicrosoftTeamsTrigger.node.d.ts.map +1 -1
  45. package/dist/nodes/Microsoft/Teams/MicrosoftTeamsTrigger.node.js +106 -112
  46. package/dist/nodes/Microsoft/Teams/MicrosoftTeamsTrigger.node.js.map +1 -1
  47. package/dist/nodes/Microsoft/Teams/v2/helpers/utils-trigger.d.ts +74 -6
  48. package/dist/nodes/Microsoft/Teams/v2/helpers/utils-trigger.d.ts.map +1 -1
  49. package/dist/nodes/Microsoft/Teams/v2/helpers/utils-trigger.js +367 -109
  50. package/dist/nodes/Microsoft/Teams/v2/helpers/utils-trigger.js.map +1 -1
  51. package/dist/nodes/Microsoft/Teams/v2/transport/forbiddenHints.d.ts +10 -0
  52. package/dist/nodes/Microsoft/Teams/v2/transport/forbiddenHints.d.ts.map +1 -1
  53. package/dist/nodes/Microsoft/Teams/v2/transport/forbiddenHints.js +24 -4
  54. package/dist/nodes/Microsoft/Teams/v2/transport/forbiddenHints.js.map +1 -1
  55. package/dist/nodes/N8n/N8n.node.d.ts.map +1 -1
  56. package/dist/nodes/N8n/N8n.node.js +7 -0
  57. package/dist/nodes/N8n/N8n.node.js.map +1 -1
  58. package/dist/nodes/N8n/UserDescription.d.ts +4 -0
  59. package/dist/nodes/N8n/UserDescription.d.ts.map +1 -0
  60. package/dist/nodes/N8n/UserDescription.js +86 -0
  61. package/dist/nodes/N8n/UserDescription.js.map +1 -0
  62. package/dist/nodes/Notion/shared/GenericFunctions.d.ts +3 -3
  63. package/dist/nodes/Notion/shared/GenericFunctions.d.ts.map +1 -1
  64. package/dist/nodes/OpenWeatherMap/OpenWeatherMap.node.d.ts.map +1 -1
  65. package/dist/nodes/OpenWeatherMap/OpenWeatherMap.node.js +12 -9
  66. package/dist/nodes/OpenWeatherMap/OpenWeatherMap.node.js.map +1 -1
  67. package/dist/nodes/Snowflake/GenericFunctions.d.ts +3 -3
  68. package/dist/nodes/Snowflake/GenericFunctions.d.ts.map +1 -1
  69. package/dist/nodes/Snowflake/GenericFunctions.js +12 -6
  70. package/dist/nodes/Snowflake/GenericFunctions.js.map +1 -1
  71. package/dist/nodes/Snowflake/Snowflake.node.d.ts.map +1 -1
  72. package/dist/nodes/Snowflake/Snowflake.node.js +4 -6
  73. package/dist/nodes/Snowflake/Snowflake.node.js.map +1 -1
  74. package/dist/nodes/Splunk/v1/GenericFunctions.d.ts +2 -2
  75. package/dist/nodes/Splunk/v1/GenericFunctions.d.ts.map +1 -1
  76. package/dist/nodes/Splunk/v1/GenericFunctions.js +10 -3
  77. package/dist/nodes/Splunk/v1/GenericFunctions.js.map +1 -1
  78. package/dist/nodes/Splunk/v1/SplunkV1.node.js +4 -4
  79. package/dist/nodes/Splunk/v1/SplunkV1.node.js.map +1 -1
  80. package/dist/nodes/Splunk/v2/actions/report/getAll.operation.js +1 -1
  81. package/dist/nodes/Splunk/v2/actions/report/getAll.operation.js.map +1 -1
  82. package/dist/nodes/Splunk/v2/actions/search/getAll.operation.js +1 -1
  83. package/dist/nodes/Splunk/v2/actions/search/getAll.operation.js.map +1 -1
  84. package/dist/nodes/Splunk/v2/actions/search/getResult.operation.js +1 -1
  85. package/dist/nodes/Splunk/v2/actions/search/getResult.operation.js.map +1 -1
  86. package/dist/nodes/Splunk/v2/actions/user/getAll.operation.d.ts +1 -1
  87. package/dist/nodes/Splunk/v2/actions/user/getAll.operation.d.ts.map +1 -1
  88. package/dist/nodes/Splunk/v2/actions/user/getAll.operation.js +2 -2
  89. package/dist/nodes/Splunk/v2/actions/user/getAll.operation.js.map +1 -1
  90. package/dist/nodes/Splunk/v2/helpers/utils.d.ts +2 -2
  91. package/dist/nodes/Splunk/v2/helpers/utils.d.ts.map +1 -1
  92. package/dist/nodes/Splunk/v2/helpers/utils.js +10 -3
  93. package/dist/nodes/Splunk/v2/helpers/utils.js.map +1 -1
  94. package/dist/types/credentials.json +2 -2
  95. package/dist/types/nodes.json +8 -8
  96. 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.deleteSubscription = deleteSubscription;
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
- /** Deletes one subscription. A `404` counts as done, for example after it expired. */
42
- async function deleteSubscription(subscriptionId) {
43
- try {
44
- await transport_1.microsoftApiRequest.call(this, 'DELETE', transport_1.buildTeamsPath.call(this, ['/v1.0/subscriptions/', { id: subscriptionId }]));
45
- }
46
- catch (error) {
47
- if (!(error instanceof n8n_workflow_1.NodeApiError && error.httpCode === '404'))
48
- throw error;
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
- const { value: teams } = (await transport_1.microsoftApiRequest.call(this, 'GET', transport_1.joinedTeamsEndpoint.call(this)));
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
- const { value: channels } = (await transport_1.microsoftApiRequest.call(this, 'GET', transport_1.buildTeamsPath.call(this, ['/v1.0/teams/', { id: teamId }, '/channels'])));
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. Lists the subscriptions at most once
87
- * per request.
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
- log('warn', 'skipped lifecycle notifications: no stored secret, republish the workflow', {});
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 rawIds = Array.isArray(webhookData.subscriptionIds)
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
- log('debug', 'ignored lifecycle event without a subscription id', {
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
- log('debug', 'ignored lifecycle event', {
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
- let needsReconcile = gone.size > 0;
405
+ const renewedEntries = new Map();
136
406
  for (const subscriptionId of toRenew) {
137
407
  if (gone.has(subscriptionId))
138
408
  continue;
139
- let isOwn;
140
- try {
141
- isOwn =
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
- log('debug', 'renewed subscription', { subscriptionId });
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
- needsReconcile = true;
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
- log('warn', 'failed to renew subscription', {
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
- if (!needsReconcile) {
178
- // Store the ids that the listing proved to be own, so `delete()` removes them on unpublish.
179
- const discovered = [...renewed].filter((id) => !storedIds.includes(id));
180
- if (discovered.length > 0)
181
- webhookData.subscriptionIds = [...storedIds, ...discovered];
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 ownSubscriptions();
189
- const resourcePaths = await getResourcePath.call(this, this.getNodeParameter('event'));
190
- requiredResources = Array.isArray(resourcePaths) ? resourcePaths : [resourcePaths];
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
- log('warn', 'failed to list subscriptions or resolve resources, skipped re-create', {
194
- subscriptionIds: affectedIds,
195
- error: (0, ensure_error_1.ensureError)(error).message,
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
- // Partition once: an id renewed in this request stays live even when its listed expiry
200
- // passed meanwhile, and the awaited deletes below must not move the line.
201
- const live = subscriptions.filter((subscription) => !gone.has(subscription.id) &&
202
- (renewed.has(subscription.id) || isLiveSubscription(subscription)));
203
- const liveIds = new Set(live.map((subscription) => subscription.id));
204
- // Every other own subscription is removed before its replacement is created, so the two
205
- // never deliver together and Graph does not reject the replacement as a duplicate. One
206
- // that Graph already removed answers 404, which counts as done.
207
- for (const { id } of subscriptions) {
208
- if (liveIds.has(id))
209
- continue;
210
- try {
211
- await deleteSubscription.call(this, id);
212
- }
213
- catch (error) {
214
- log('debug', 'failed to remove a stale subscription', {
215
- subscriptionId: id,
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 false;
504
+ return [];
250
505
  }
251
- return notifications.every((notification) => {
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;