@objectstack/trigger-api 17.1.0 → 17.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/CHANGELOG.md +341 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,346 @@
|
|
|
1
1
|
# @objectstack/trigger-api
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [809d417]
|
|
8
|
+
- Updated dependencies [387e231]
|
|
9
|
+
- Updated dependencies [f794e4e]
|
|
10
|
+
- Updated dependencies [cae2169]
|
|
11
|
+
- Updated dependencies [b812a54]
|
|
12
|
+
- Updated dependencies [2d4fa75]
|
|
13
|
+
- Updated dependencies [0e4e51b]
|
|
14
|
+
- Updated dependencies [e84bbf6]
|
|
15
|
+
- Updated dependencies [effae80]
|
|
16
|
+
- Updated dependencies [efb3513]
|
|
17
|
+
- Updated dependencies [d62f990]
|
|
18
|
+
- Updated dependencies [c45d8e6]
|
|
19
|
+
- Updated dependencies [2e3e8c7]
|
|
20
|
+
- Updated dependencies [e621291]
|
|
21
|
+
- Updated dependencies [655b106]
|
|
22
|
+
- Updated dependencies [40a93b5]
|
|
23
|
+
- Updated dependencies [d5b330d]
|
|
24
|
+
- Updated dependencies [dda969c]
|
|
25
|
+
- Updated dependencies [1f45690]
|
|
26
|
+
- Updated dependencies [277948f]
|
|
27
|
+
- Updated dependencies [8bdd955]
|
|
28
|
+
- Updated dependencies [f3bbbef]
|
|
29
|
+
- Updated dependencies [4f24e9d]
|
|
30
|
+
- Updated dependencies [e27583e]
|
|
31
|
+
- Updated dependencies [4bd6faa]
|
|
32
|
+
- Updated dependencies [86cbe37]
|
|
33
|
+
- Updated dependencies [6a180e4]
|
|
34
|
+
- Updated dependencies [474242f]
|
|
35
|
+
- Updated dependencies [63cd487]
|
|
36
|
+
- Updated dependencies [bd4aa4e]
|
|
37
|
+
- Updated dependencies [803eaab]
|
|
38
|
+
- Updated dependencies [f8e8f03]
|
|
39
|
+
- Updated dependencies [983edf1]
|
|
40
|
+
- Updated dependencies [eae824e]
|
|
41
|
+
- Updated dependencies [f6fa22c]
|
|
42
|
+
- Updated dependencies [8a483b3]
|
|
43
|
+
- Updated dependencies [97bcd99]
|
|
44
|
+
- Updated dependencies [df59de0]
|
|
45
|
+
- Updated dependencies [96e25a8]
|
|
46
|
+
- Updated dependencies [f75a38a]
|
|
47
|
+
- Updated dependencies [7a25e7d]
|
|
48
|
+
- Updated dependencies [1fa05a6]
|
|
49
|
+
- Updated dependencies [c85a265]
|
|
50
|
+
- Updated dependencies [dcb10a5]
|
|
51
|
+
- Updated dependencies [773a999]
|
|
52
|
+
- Updated dependencies [35dffea]
|
|
53
|
+
- Updated dependencies [d8024f0]
|
|
54
|
+
- Updated dependencies [8120808]
|
|
55
|
+
- Updated dependencies [776a098]
|
|
56
|
+
- Updated dependencies [5060877]
|
|
57
|
+
- Updated dependencies [4f6325d]
|
|
58
|
+
- Updated dependencies [52954c0]
|
|
59
|
+
- Updated dependencies [2aa8456]
|
|
60
|
+
- Updated dependencies [93809a3]
|
|
61
|
+
- Updated dependencies [7c0d0c3]
|
|
62
|
+
- Updated dependencies [daae7aa]
|
|
63
|
+
- Updated dependencies [8dc22d6]
|
|
64
|
+
- Updated dependencies [279431e]
|
|
65
|
+
- Updated dependencies [948dd6b]
|
|
66
|
+
- Updated dependencies [3b4c56c]
|
|
67
|
+
- Updated dependencies [ae8edd2]
|
|
68
|
+
- Updated dependencies [e25403c]
|
|
69
|
+
- Updated dependencies [64baa68]
|
|
70
|
+
- Updated dependencies [9fa70d7]
|
|
71
|
+
- Updated dependencies [09db64a]
|
|
72
|
+
- Updated dependencies [92916e7]
|
|
73
|
+
- Updated dependencies [a84f3ea]
|
|
74
|
+
- Updated dependencies [f2eaae8]
|
|
75
|
+
- Updated dependencies [c09451b]
|
|
76
|
+
- Updated dependencies [ba64877]
|
|
77
|
+
- Updated dependencies [7345308]
|
|
78
|
+
- Updated dependencies [79b6a22]
|
|
79
|
+
- Updated dependencies [30d96ab]
|
|
80
|
+
- Updated dependencies [f658793]
|
|
81
|
+
- Updated dependencies [c95ad19]
|
|
82
|
+
- Updated dependencies [e58ea8b]
|
|
83
|
+
- Updated dependencies [4a17645]
|
|
84
|
+
- Updated dependencies [3795c5f]
|
|
85
|
+
- Updated dependencies [8ab926b]
|
|
86
|
+
- Updated dependencies [7317cf2]
|
|
87
|
+
- Updated dependencies [e25e839]
|
|
88
|
+
- Updated dependencies [5997207]
|
|
89
|
+
- Updated dependencies [8b13cc8]
|
|
90
|
+
- Updated dependencies [4a4a35d]
|
|
91
|
+
- Updated dependencies [86e765a]
|
|
92
|
+
- Updated dependencies [1d7e76a]
|
|
93
|
+
- Updated dependencies [53dc739]
|
|
94
|
+
- Updated dependencies [fd289be]
|
|
95
|
+
- Updated dependencies [03bf7b1]
|
|
96
|
+
- Updated dependencies [f90e820]
|
|
97
|
+
- Updated dependencies [18d816a]
|
|
98
|
+
- Updated dependencies [e8bd715]
|
|
99
|
+
- Updated dependencies [b91c351]
|
|
100
|
+
- Updated dependencies [a28a3c0]
|
|
101
|
+
- Updated dependencies [daeaaf9]
|
|
102
|
+
- Updated dependencies [c459da6]
|
|
103
|
+
- Updated dependencies [e914733]
|
|
104
|
+
- Updated dependencies [f887e52]
|
|
105
|
+
- Updated dependencies [881f8d8]
|
|
106
|
+
- Updated dependencies [3bfa1e6]
|
|
107
|
+
- Updated dependencies [0a8ebf3]
|
|
108
|
+
- Updated dependencies [901355c]
|
|
109
|
+
- Updated dependencies [34ce8e7]
|
|
110
|
+
- Updated dependencies [33681ea]
|
|
111
|
+
- Updated dependencies [4635f3e]
|
|
112
|
+
- Updated dependencies [fd289be]
|
|
113
|
+
- Updated dependencies [ee3595c]
|
|
114
|
+
- Updated dependencies [b2eab95]
|
|
115
|
+
- Updated dependencies [93940d4]
|
|
116
|
+
- Updated dependencies [3a04b01]
|
|
117
|
+
- Updated dependencies [45b9051]
|
|
118
|
+
- Updated dependencies [b9e9227]
|
|
119
|
+
- Updated dependencies [d395692]
|
|
120
|
+
- Updated dependencies [5894d30]
|
|
121
|
+
- Updated dependencies [a3765f6]
|
|
122
|
+
- Updated dependencies [2d5cee3]
|
|
123
|
+
- Updated dependencies [e22158f]
|
|
124
|
+
- Updated dependencies [7404925]
|
|
125
|
+
- Updated dependencies [0c2334f]
|
|
126
|
+
- Updated dependencies [778c59f]
|
|
127
|
+
- Updated dependencies [d2619fd]
|
|
128
|
+
- Updated dependencies [af56546]
|
|
129
|
+
- Updated dependencies [6acb11a]
|
|
130
|
+
- Updated dependencies [33c5fd3]
|
|
131
|
+
- Updated dependencies [20b0fdb]
|
|
132
|
+
- Updated dependencies [905019b]
|
|
133
|
+
- Updated dependencies [a286411]
|
|
134
|
+
- Updated dependencies [98c0d33]
|
|
135
|
+
- Updated dependencies [368a82e]
|
|
136
|
+
- Updated dependencies [a3d5724]
|
|
137
|
+
- Updated dependencies [93ea19b]
|
|
138
|
+
- Updated dependencies [9ee2dcf]
|
|
139
|
+
- Updated dependencies [8cb96ec]
|
|
140
|
+
- Updated dependencies [8f10a79]
|
|
141
|
+
- Updated dependencies [6269a55]
|
|
142
|
+
- Updated dependencies [a17da05]
|
|
143
|
+
- Updated dependencies [a8c00e2]
|
|
144
|
+
- Updated dependencies [0fb8760]
|
|
145
|
+
- Updated dependencies [e5ce2ed]
|
|
146
|
+
- Updated dependencies [be21955]
|
|
147
|
+
- Updated dependencies [bc56e18]
|
|
148
|
+
- Updated dependencies [be21955]
|
|
149
|
+
- Updated dependencies [a9ee989]
|
|
150
|
+
- Updated dependencies [4d0d944]
|
|
151
|
+
- Updated dependencies [15d58db]
|
|
152
|
+
- Updated dependencies [d63b014]
|
|
153
|
+
- Updated dependencies [9abe4e4]
|
|
154
|
+
- Updated dependencies [2cc7122]
|
|
155
|
+
- Updated dependencies [50d6c92]
|
|
156
|
+
- Updated dependencies [9e0ba21]
|
|
157
|
+
- Updated dependencies [311433f]
|
|
158
|
+
- Updated dependencies [3e5ad08]
|
|
159
|
+
- Updated dependencies [9abe4e4]
|
|
160
|
+
- Updated dependencies [b7131f3]
|
|
161
|
+
- Updated dependencies [e5812fa]
|
|
162
|
+
- Updated dependencies [7085f90]
|
|
163
|
+
- Updated dependencies [dee4dd4]
|
|
164
|
+
- Updated dependencies [ce7e497]
|
|
165
|
+
- Updated dependencies [51ecb2f]
|
|
166
|
+
- Updated dependencies [9086761]
|
|
167
|
+
- Updated dependencies [42a117b]
|
|
168
|
+
- Updated dependencies [1401ae7]
|
|
169
|
+
- Updated dependencies [4297fe7]
|
|
170
|
+
- Updated dependencies [e398863]
|
|
171
|
+
- Updated dependencies [d16df74]
|
|
172
|
+
- Updated dependencies [f11fc61]
|
|
173
|
+
- Updated dependencies [e808890]
|
|
174
|
+
- Updated dependencies [8f79379]
|
|
175
|
+
- Updated dependencies [e6ca40e]
|
|
176
|
+
- Updated dependencies [0c77ea4]
|
|
177
|
+
- Updated dependencies [52954c0]
|
|
178
|
+
- Updated dependencies [89eb997]
|
|
179
|
+
- Updated dependencies [7131f12]
|
|
180
|
+
- Updated dependencies [aa5994e]
|
|
181
|
+
- Updated dependencies [be93457]
|
|
182
|
+
- Updated dependencies [a65db76]
|
|
183
|
+
- Updated dependencies [15eb2c9]
|
|
184
|
+
- Updated dependencies [5691b07]
|
|
185
|
+
- Updated dependencies [2a6122b]
|
|
186
|
+
- Updated dependencies [225e769]
|
|
187
|
+
- Updated dependencies [8af88dd]
|
|
188
|
+
- Updated dependencies [fb5fbb8]
|
|
189
|
+
- Updated dependencies [d7b3963]
|
|
190
|
+
- Updated dependencies [33184fd]
|
|
191
|
+
- Updated dependencies [7c41693]
|
|
192
|
+
- Updated dependencies [b72db01]
|
|
193
|
+
- Updated dependencies [dce5cd4]
|
|
194
|
+
- Updated dependencies [9688f58]
|
|
195
|
+
- Updated dependencies [556ebc1]
|
|
196
|
+
- Updated dependencies [177ebdc]
|
|
197
|
+
- Updated dependencies [8d237b4]
|
|
198
|
+
- Updated dependencies [2d2e6f0]
|
|
199
|
+
- Updated dependencies [2d8dd8d]
|
|
200
|
+
- Updated dependencies [22d573e]
|
|
201
|
+
- Updated dependencies [b5a2398]
|
|
202
|
+
- Updated dependencies [348860c]
|
|
203
|
+
- Updated dependencies [5383fa6]
|
|
204
|
+
- Updated dependencies [5b3ff63]
|
|
205
|
+
- Updated dependencies [1a6a19c]
|
|
206
|
+
- Updated dependencies [527e050]
|
|
207
|
+
- Updated dependencies [dd33bf9]
|
|
208
|
+
- Updated dependencies [4cb2a90]
|
|
209
|
+
- Updated dependencies [74a7804]
|
|
210
|
+
- Updated dependencies [53d3689]
|
|
211
|
+
- Updated dependencies [b3a63d3]
|
|
212
|
+
- Updated dependencies [49f0dcf]
|
|
213
|
+
- Updated dependencies [033a34c]
|
|
214
|
+
- Updated dependencies [4d25d22]
|
|
215
|
+
- Updated dependencies [1ffee51]
|
|
216
|
+
- Updated dependencies [5ae4303]
|
|
217
|
+
- Updated dependencies [ece4dad]
|
|
218
|
+
- Updated dependencies [e9b377e]
|
|
219
|
+
- Updated dependencies [146f448]
|
|
220
|
+
- Updated dependencies [735f5c7]
|
|
221
|
+
- Updated dependencies [a7e18de]
|
|
222
|
+
- Updated dependencies [366f895]
|
|
223
|
+
- Updated dependencies [dc75ba8]
|
|
224
|
+
- Updated dependencies [cce0aa9]
|
|
225
|
+
- Updated dependencies [e764507]
|
|
226
|
+
- Updated dependencies [cff17af]
|
|
227
|
+
- Updated dependencies [39404f3]
|
|
228
|
+
- Updated dependencies [ca1965f]
|
|
229
|
+
- Updated dependencies [8619f95]
|
|
230
|
+
- Updated dependencies [b706af9]
|
|
231
|
+
- Updated dependencies [add4360]
|
|
232
|
+
- Updated dependencies [fc9ba76]
|
|
233
|
+
- Updated dependencies [0f94cc7]
|
|
234
|
+
- Updated dependencies [a11c1a5]
|
|
235
|
+
- Updated dependencies [71f9cd1]
|
|
236
|
+
- Updated dependencies [ee17d86]
|
|
237
|
+
- Updated dependencies [cdbd920]
|
|
238
|
+
- Updated dependencies [18c432e]
|
|
239
|
+
- Updated dependencies [3c418c4]
|
|
240
|
+
- Updated dependencies [fa8715a]
|
|
241
|
+
- Updated dependencies [a933ed7]
|
|
242
|
+
- Updated dependencies [b3ca463]
|
|
243
|
+
- Updated dependencies [a933ed7]
|
|
244
|
+
- Updated dependencies [0d4a6a8]
|
|
245
|
+
- Updated dependencies [518d5e5]
|
|
246
|
+
- Updated dependencies [6643ba1]
|
|
247
|
+
- Updated dependencies [eeba2ef]
|
|
248
|
+
- Updated dependencies [ec4c4d2]
|
|
249
|
+
- Updated dependencies [424f73c]
|
|
250
|
+
- Updated dependencies [cccbe51]
|
|
251
|
+
- Updated dependencies [a8d6b1d]
|
|
252
|
+
- Updated dependencies [e4a7695]
|
|
253
|
+
- Updated dependencies [87075b1]
|
|
254
|
+
- Updated dependencies [fc58a99]
|
|
255
|
+
- Updated dependencies [14cfc00]
|
|
256
|
+
- Updated dependencies [1c6f7b4]
|
|
257
|
+
- Updated dependencies [e854a53]
|
|
258
|
+
- Updated dependencies [dfebfc8]
|
|
259
|
+
- Updated dependencies [d028b37]
|
|
260
|
+
- Updated dependencies [f7b25c5]
|
|
261
|
+
- Updated dependencies [122ef38]
|
|
262
|
+
- Updated dependencies [4a37870]
|
|
263
|
+
- Updated dependencies [428f9b2]
|
|
264
|
+
- Updated dependencies [aa7ff56]
|
|
265
|
+
- Updated dependencies [c41b42e]
|
|
266
|
+
- Updated dependencies [c4db311]
|
|
267
|
+
- Updated dependencies [750fff5]
|
|
268
|
+
- Updated dependencies [c19035e]
|
|
269
|
+
- Updated dependencies [ececf7a]
|
|
270
|
+
- Updated dependencies [d173125]
|
|
271
|
+
- Updated dependencies [8eeca27]
|
|
272
|
+
- Updated dependencies [8425c17]
|
|
273
|
+
- Updated dependencies [a5ef1d8]
|
|
274
|
+
- Updated dependencies [772d5de]
|
|
275
|
+
- Updated dependencies [ce80ec2]
|
|
276
|
+
- Updated dependencies [b372318]
|
|
277
|
+
- Updated dependencies [97a2263]
|
|
278
|
+
- Updated dependencies [29d0676]
|
|
279
|
+
- Updated dependencies [0169d49]
|
|
280
|
+
- Updated dependencies [6bd3231]
|
|
281
|
+
- Updated dependencies [d2b5ba8]
|
|
282
|
+
- Updated dependencies [b799ac5]
|
|
283
|
+
- Updated dependencies [8f74307]
|
|
284
|
+
- Updated dependencies [d23dc08]
|
|
285
|
+
- Updated dependencies [644ad50]
|
|
286
|
+
- Updated dependencies [0da7cd2]
|
|
287
|
+
- Updated dependencies [28a5c3e]
|
|
288
|
+
- Updated dependencies [4bc18e5]
|
|
289
|
+
- @objectstack/spec@17.3.0
|
|
290
|
+
- @objectstack/core@17.3.0
|
|
291
|
+
|
|
292
|
+
## 17.2.0
|
|
293
|
+
|
|
294
|
+
### Patch Changes
|
|
295
|
+
|
|
296
|
+
- Updated dependencies [6936d07]
|
|
297
|
+
- Updated dependencies [59eb04d]
|
|
298
|
+
- Updated dependencies [9f05b7d]
|
|
299
|
+
- Updated dependencies [3b2af5e]
|
|
300
|
+
- Updated dependencies [7d2d112]
|
|
301
|
+
- Updated dependencies [5fa0d72]
|
|
302
|
+
- Updated dependencies [02b3b07]
|
|
303
|
+
- Updated dependencies [914c413]
|
|
304
|
+
- Updated dependencies [55809a0]
|
|
305
|
+
- Updated dependencies [ee2ff45]
|
|
306
|
+
- Updated dependencies [47cd3ec]
|
|
307
|
+
- Updated dependencies [52db1d1]
|
|
308
|
+
- Updated dependencies [5649efb]
|
|
309
|
+
- Updated dependencies [9d7d2de]
|
|
310
|
+
- Updated dependencies [c815c50]
|
|
311
|
+
- Updated dependencies [795ea05]
|
|
312
|
+
- Updated dependencies [2306a76]
|
|
313
|
+
- Updated dependencies [e5ea701]
|
|
314
|
+
- Updated dependencies [a40dcc1]
|
|
315
|
+
- Updated dependencies [def0d3e]
|
|
316
|
+
- Updated dependencies [8d0bb79]
|
|
317
|
+
- Updated dependencies [5acb58d]
|
|
318
|
+
- Updated dependencies [2e3cf95]
|
|
319
|
+
- Updated dependencies [4c93387]
|
|
320
|
+
- Updated dependencies [504c8d5]
|
|
321
|
+
- Updated dependencies [a037f7c]
|
|
322
|
+
- Updated dependencies [3ee8ddf]
|
|
323
|
+
- Updated dependencies [16cef97]
|
|
324
|
+
- Updated dependencies [a79bd35]
|
|
325
|
+
- Updated dependencies [6ceaa4b]
|
|
326
|
+
- Updated dependencies [15ea214]
|
|
327
|
+
- Updated dependencies [de19489]
|
|
328
|
+
- Updated dependencies [c684d00]
|
|
329
|
+
- Updated dependencies [923c424]
|
|
330
|
+
- Updated dependencies [1ec36b7]
|
|
331
|
+
- Updated dependencies [5f2e54c]
|
|
332
|
+
- Updated dependencies [189373b]
|
|
333
|
+
- Updated dependencies [35ad101]
|
|
334
|
+
- Updated dependencies [ceb33a9]
|
|
335
|
+
- Updated dependencies [73d9795]
|
|
336
|
+
- Updated dependencies [8012960]
|
|
337
|
+
- Updated dependencies [f34f56b]
|
|
338
|
+
- Updated dependencies [f399618]
|
|
339
|
+
- Updated dependencies [75e9301]
|
|
340
|
+
- Updated dependencies [2810695]
|
|
341
|
+
- @objectstack/spec@17.2.0
|
|
342
|
+
- @objectstack/core@17.2.0
|
|
343
|
+
|
|
3
344
|
## 17.1.0
|
|
4
345
|
|
|
5
346
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -26,7 +26,7 @@ declare const HOOKS_PATH = "/api/v1/automation/hooks/:flowName/:hookId";
|
|
|
26
26
|
*/
|
|
27
27
|
declare class ApiTriggerPlugin implements Plugin {
|
|
28
28
|
name: string;
|
|
29
|
-
type:
|
|
29
|
+
type: "standard";
|
|
30
30
|
version: string;
|
|
31
31
|
dependencies: string[];
|
|
32
32
|
init(ctx: PluginContext): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ declare const HOOKS_PATH = "/api/v1/automation/hooks/:flowName/:hookId";
|
|
|
26
26
|
*/
|
|
27
27
|
declare class ApiTriggerPlugin implements Plugin {
|
|
28
28
|
name: string;
|
|
29
|
-
type:
|
|
29
|
+
type: "standard";
|
|
30
30
|
version: string;
|
|
31
31
|
dependencies: string[];
|
|
32
32
|
init(ctx: PluginContext): Promise<void>;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/api-trigger.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { ApiTriggerPlugin, HOOKS_PATH } from './plugin.js';\nexport { ApiTrigger, verifySignature } from './api-trigger.js';\nexport type {\n FlowTrigger,\n FlowTriggerBinding,\n QueueServiceSurface,\n TriggerLogger,\n} from './api-trigger.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { AutomationContext } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding` — same\n * decoupling pattern as `trigger-schedule` / `trigger-record-change`: this\n * package never imports `service-automation`.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/** Structural mirror of the engine's `FlowTrigger` extension point. */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IQueueService` this trigger needs. Boundary triggers MUST\n * ingest through the queue (ADR-0041 §5): inbound HTTP is the first event\n * source whose rate we don't control, and a slow flow execution may neither\n * drop nor block inbound events. At-least-once delivery; flows should be\n * authored idempotently (an `x-idempotency-key` header passes through to the\n * queue's dedup window).\n */\nexport interface QueueServiceSurface {\n publish<T = unknown>(queue: string, data: T, options?: { idempotencyKey?: string }): Promise<string>;\n subscribe<T = unknown>(queue: string, handler: (message: { data: T }) => Promise<void> | void): Promise<void>;\n unsubscribe(queue: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n}\n\nconst QUEUE_PREFIX = 'flow-api';\n\n/** One armed inbound hook. */\ninterface ArmedHook {\n flowName: string;\n hookId: string;\n secret?: string;\n queue: string;\n callback: (ctx: AutomationContext) => Promise<void>;\n}\n\n/** Constant-time string compare (length leak only). */\nfunction safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a, 'utf8');\n const bb = Buffer.from(b, 'utf8');\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\n/**\n * GitHub/Stripe-style HMAC verification: the sender computes\n * `sha256=<hex hmac of the raw body>` with the shared per-flow secret and\n * sends it as `x-objectstack-signature`.\n */\nexport function verifySignature(secret: string, rawBody: string, header: string | undefined): boolean {\n if (!header) return false;\n const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');\n return safeEqual(expected, header.trim());\n}\n\n/**\n * `api` flow trigger (ADR-0041 Tier 1) — inbound webhook/HTTP.\n *\n * The engine binds every `type: 'api'` flow to this trigger; `start()` arms a\n * hook (URL path + optional HMAC secret from the start node's `config`) and\n * subscribes a queue consumer that runs the flow. The HTTP side\n * ({@link handleRequest}) validates and **enqueues** — it never executes the\n * flow in-band:\n *\n * POST /api/v1/automation/hooks/:flowName/:hookId\n * → 404 unknown flow / wrong hookId\n * → 401 missing/bad HMAC signature (when the flow declares a `secret`)\n * → 400 non-JSON body\n * → 202 { accepted, messageId } — queued; a consumer executes the flow\n *\n * The JSON payload is exposed to the flow as the trigger record (`$record` /\n * `record.*`, fields flattened to bare references) plus `params` — the same\n * authoring surface record-change flows use.\n *\n * Start-node config keys:\n * - `hookId` — URL path token (default `'default'`); rotate it to revoke\n * old URLs without renaming the flow.\n * - `secret` — HMAC-SHA256 shared secret. Strongly recommended; without it\n * the endpoint accepts unsigned posts (the trigger logs a\n * warning at arm time).\n */\nexport class ApiTrigger implements FlowTrigger {\n readonly type = 'api';\n\n private hooks = new Map<string, ArmedHook>();\n\n constructor(\n private readonly getQueue: () => QueueServiceSurface | null,\n private readonly logger: TriggerLogger,\n ) {}\n\n /** Currently armed hooks (for diagnostics/tests). */\n listHooks(): Array<{ flowName: string; hookId: string; signed: boolean }> {\n return [...this.hooks.values()].map(h => ({\n flowName: h.flowName, hookId: h.hookId, signed: !!h.secret,\n }));\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const cfg = (binding.config ?? {}) as Record<string, unknown>;\n const hookId = typeof cfg.hookId === 'string' && cfg.hookId.trim() ? cfg.hookId.trim() : 'default';\n const secret = typeof cfg.secret === 'string' && cfg.secret.trim() ? cfg.secret.trim() : undefined;\n const queue = `${QUEUE_PREFIX}:${binding.flowName}`;\n\n const hook: ArmedHook = { flowName: binding.flowName, hookId, secret, queue, callback };\n this.hooks.set(binding.flowName, hook);\n\n const q = this.getQueue();\n if (!q) {\n this.logger.warn(\n `[trigger-api] no queue service — inbound posts for flow '${binding.flowName}' will be rejected until one is registered`,\n );\n } else {\n void q.subscribe<{ payload: Record<string, unknown> }>(queue, async (message) => {\n const payload = (message?.data as any)?.payload ?? {};\n await hook.callback({\n // The webhook body IS the trigger record: `$record`, `record.*`,\n // and bare field references all resolve, matching how\n // record-change flows are authored.\n record: payload,\n params: { ...payload },\n event: 'api',\n } as AutomationContext);\n }).catch((err: any) => {\n this.logger.warn(`[trigger-api] subscribe failed for '${queue}': ${err?.message ?? err}`);\n });\n }\n\n if (!secret) {\n this.logger.warn(\n `[trigger-api] flow '${binding.flowName}' armed WITHOUT a secret — endpoint accepts unsigned posts`,\n );\n }\n this.logger.info(\n `[trigger-api] armed: POST .../automation/hooks/${binding.flowName}/${hookId}${secret ? ' (HMAC required)' : ''}`,\n );\n }\n\n stop(flowName: string): void {\n const hook = this.hooks.get(flowName);\n if (!hook) return;\n this.hooks.delete(flowName);\n const q = this.getQueue();\n if (q) void q.unsubscribe(hook.queue).catch(() => { /* already gone */ });\n this.logger.info(`[trigger-api] disarmed: flow '${flowName}'`);\n }\n\n /**\n * Handle one inbound request. Transport-agnostic: the plugin adapts this\n * to the host HTTP server. Returns the response to send.\n */\n async handleRequest(input: {\n flowName: string;\n hookId: string;\n rawBody: string;\n signatureHeader?: string;\n idempotencyKey?: string;\n }): Promise<{ status: number; body: Record<string, unknown> }> {\n const hook = this.hooks.get(input.flowName);\n // Unknown flow and wrong hookId answer identically — no oracle for\n // probing which flows exist.\n if (!hook || !safeEqual(hook.hookId, input.hookId)) {\n return { status: 404, body: { success: false, error: { code: 'RESOURCE_NOT_FOUND', message: 'No such hook.' } } };\n }\n if (hook.secret && !verifySignature(hook.secret, input.rawBody, input.signatureHeader)) {\n return { status: 401, body: { success: false, error: { code: 'INVALID_SIGNATURE', message: 'Signature verification failed.' } } };\n }\n\n let payload: Record<string, unknown>;\n try {\n const parsed: unknown = input.rawBody.trim() ? JSON.parse(input.rawBody) : {};\n if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return { status: 400, body: { success: false, error: { code: 'INVALID_REQUEST', message: 'Body must be a JSON object.' } } };\n }\n payload = parsed as Record<string, unknown>;\n } catch {\n return { status: 400, body: { success: false, error: { code: 'INVALID_REQUEST', message: 'Body must be valid JSON.' } } };\n }\n\n const q = this.getQueue();\n if (!q) {\n return { status: 503, body: { success: false, error: { code: 'SERVICE_UNAVAILABLE', message: 'No queue service is registered.' } } };\n }\n try {\n const messageId = await q.publish(hook.queue, { payload }, {\n idempotencyKey: input.idempotencyKey,\n });\n return { status: 202, body: { accepted: true, messageId } };\n } catch (err: any) {\n this.logger.warn(`[trigger-api] enqueue failed for '${hook.queue}': ${err?.message ?? err}`);\n return { status: 503, body: { success: false, error: { code: 'ENQUEUE_FAILED', message: 'Could not enqueue the trigger message.' } } };\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ApiTrigger } from './api-trigger.js';\nimport type { FlowTrigger, QueueServiceSurface } from './api-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs. Declared structurally\n * so the plugin does not take a build dependency on\n * `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/** The slice of the Hono host server this plugin needs. */\ninterface HttpServerSurface {\n getRawApp(): {\n post(path: string, handler: (c: any) => Promise<unknown> | unknown): void;\n } | null;\n}\n\n/** Mount point for inbound hooks on the host HTTP server. */\nexport const HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId';\n\n/**\n * ApiTriggerPlugin (ADR-0041 Tier 1)\n *\n * Makes `type: 'api'` flows actually fire. The automation engine derives an\n * `api` binding from the flow; this plugin provides the concrete trigger:\n *\n * - mounts `POST /api/v1/automation/hooks/:flowName/:hookId` on the host\n * Hono app (resolved via the `http-server` service);\n * - validates hookId + HMAC signature (`x-objectstack-signature`,\n * GitHub/Stripe style, constant-time) against the flow's start-node\n * config;\n * - **enqueues** the payload (`queue` service) and ACKs 202 — flow\n * execution happens on the queue consumer, never in-band with the\n * inbound request (ADR-0041 §5: boundary triggers must not let a slow\n * flow drop or block events). `x-idempotency-key` passes through to the\n * queue's dedup window.\n *\n * Webhook payloads surface to the flow as the trigger record (`$record` /\n * `record.*` / bare field references) — the same authoring surface\n * record-change flows use.\n */\nexport class ApiTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.api';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.service.queue'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('API trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Resolve at kernel:ready — the automation engine, queue service, and\n // HTTP server may all start after this plugin.\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn('ApiTriggerPlugin: automation service not available — api trigger NOT installed');\n return;\n }\n if (!this.resolveService<QueueServiceSurface>(ctx, 'queue')) {\n ctx.logger.warn('ApiTriggerPlugin: queue service not available — inbound posts will 503 until one is registered');\n }\n\n const trigger = new ApiTrigger(\n () => this.resolveService<QueueServiceSurface>(ctx, 'queue'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n\n const http = this.resolveService<HttpServerSurface>(ctx, 'http-server');\n const rawApp = http && typeof http.getRawApp === 'function' ? http.getRawApp() : null;\n if (!rawApp) {\n ctx.logger.warn('ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted');\n return;\n }\n rawApp.post(HOOKS_PATH, async (c: any) => {\n const rawBody = await c.req.text();\n const out = await trigger.handleRequest({\n flowName: c.req.param('flowName'),\n hookId: c.req.param('hookId'),\n rawBody,\n signatureHeader: c.req.header('x-objectstack-signature'),\n idempotencyKey: c.req.header('x-idempotency-key') || undefined,\n });\n return c.json(out.body, out.status);\n });\n ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/api-trigger.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { ApiTriggerPlugin, HOOKS_PATH } from './plugin.js';\nexport { ApiTrigger, verifySignature } from './api-trigger.js';\nexport type {\n FlowTrigger,\n FlowTriggerBinding,\n QueueServiceSurface,\n TriggerLogger,\n} from './api-trigger.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { AutomationContext } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding` — same\n * decoupling pattern as `trigger-schedule` / `trigger-record-change`: this\n * package never imports `service-automation`.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/** Structural mirror of the engine's `FlowTrigger` extension point. */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IQueueService` this trigger needs. Boundary triggers MUST\n * ingest through the queue (ADR-0041 §5): inbound HTTP is the first event\n * source whose rate we don't control, and a slow flow execution may neither\n * drop nor block inbound events. At-least-once delivery; flows should be\n * authored idempotently (an `x-idempotency-key` header passes through to the\n * queue's dedup window).\n */\nexport interface QueueServiceSurface {\n publish<T = unknown>(queue: string, data: T, options?: { idempotencyKey?: string }): Promise<string>;\n subscribe<T = unknown>(queue: string, handler: (message: { data: T }) => Promise<void> | void): Promise<void>;\n unsubscribe(queue: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n}\n\nconst QUEUE_PREFIX = 'flow-api';\n\n/** One armed inbound hook. */\ninterface ArmedHook {\n flowName: string;\n hookId: string;\n secret?: string;\n queue: string;\n callback: (ctx: AutomationContext) => Promise<void>;\n}\n\n/** Constant-time string compare (length leak only). */\nfunction safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a, 'utf8');\n const bb = Buffer.from(b, 'utf8');\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\n/**\n * GitHub/Stripe-style HMAC verification: the sender computes\n * `sha256=<hex hmac of the raw body>` with the shared per-flow secret and\n * sends it as `x-objectstack-signature`.\n */\nexport function verifySignature(secret: string, rawBody: string, header: string | undefined): boolean {\n if (!header) return false;\n const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');\n return safeEqual(expected, header.trim());\n}\n\n/**\n * `api` flow trigger (ADR-0041 Tier 1) — inbound webhook/HTTP.\n *\n * The engine binds every `type: 'api'` flow to this trigger; `start()` arms a\n * hook (URL path + optional HMAC secret from the start node's `config`) and\n * subscribes a queue consumer that runs the flow. The HTTP side\n * ({@link handleRequest}) validates and **enqueues** — it never executes the\n * flow in-band:\n *\n * POST /api/v1/automation/hooks/:flowName/:hookId\n * → 404 unknown flow / wrong hookId\n * → 401 missing/bad HMAC signature (when the flow declares a `secret`)\n * → 400 non-JSON body\n * → 202 { accepted, messageId } — queued; a consumer executes the flow\n *\n * The JSON payload is exposed to the flow as the trigger record (`$record` /\n * `record.*`, fields flattened to bare references) plus `params` — the same\n * authoring surface record-change flows use.\n *\n * Start-node config keys:\n * - `hookId` — URL path token (default `'default'`); rotate it to revoke\n * old URLs without renaming the flow.\n * - `secret` — HMAC-SHA256 shared secret. Strongly recommended; without it\n * the endpoint accepts unsigned posts (the trigger logs a\n * warning at arm time).\n */\nexport class ApiTrigger implements FlowTrigger {\n readonly type = 'api';\n\n private hooks = new Map<string, ArmedHook>();\n\n constructor(\n private readonly getQueue: () => QueueServiceSurface | null,\n private readonly logger: TriggerLogger,\n ) {}\n\n /** Currently armed hooks (for diagnostics/tests). */\n listHooks(): Array<{ flowName: string; hookId: string; signed: boolean }> {\n return [...this.hooks.values()].map(h => ({\n flowName: h.flowName, hookId: h.hookId, signed: !!h.secret,\n }));\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const cfg = (binding.config ?? {}) as Record<string, unknown>;\n const hookId = typeof cfg.hookId === 'string' && cfg.hookId.trim() ? cfg.hookId.trim() : 'default';\n const secret = typeof cfg.secret === 'string' && cfg.secret.trim() ? cfg.secret.trim() : undefined;\n const queue = `${QUEUE_PREFIX}:${binding.flowName}`;\n\n const hook: ArmedHook = { flowName: binding.flowName, hookId, secret, queue, callback };\n this.hooks.set(binding.flowName, hook);\n\n const q = this.getQueue();\n if (!q) {\n this.logger.warn(\n `[trigger-api] no queue service — inbound posts for flow '${binding.flowName}' will be rejected until one is registered`,\n );\n } else {\n void q.subscribe<{ payload: Record<string, unknown> }>(queue, async (message) => {\n const payload = (message?.data as any)?.payload ?? {};\n await hook.callback({\n // The webhook body IS the trigger record: `$record`, `record.*`,\n // and bare field references all resolve, matching how\n // record-change flows are authored.\n record: payload,\n params: { ...payload },\n event: 'api',\n } as AutomationContext);\n }).catch((err: any) => {\n this.logger.warn(`[trigger-api] subscribe failed for '${queue}': ${err?.message ?? err}`);\n });\n }\n\n if (!secret) {\n this.logger.warn(\n `[trigger-api] flow '${binding.flowName}' armed WITHOUT a secret — endpoint accepts unsigned posts`,\n );\n }\n this.logger.info(\n `[trigger-api] armed: POST .../automation/hooks/${binding.flowName}/${hookId}${secret ? ' (HMAC required)' : ''}`,\n );\n }\n\n stop(flowName: string): void {\n const hook = this.hooks.get(flowName);\n if (!hook) return;\n this.hooks.delete(flowName);\n const q = this.getQueue();\n if (q) void q.unsubscribe(hook.queue).catch(() => { /* already gone */ });\n this.logger.info(`[trigger-api] disarmed: flow '${flowName}'`);\n }\n\n /**\n * Handle one inbound request. Transport-agnostic: the plugin adapts this\n * to the host HTTP server. Returns the response to send.\n */\n async handleRequest(input: {\n flowName: string;\n hookId: string;\n rawBody: string;\n signatureHeader?: string;\n idempotencyKey?: string;\n }): Promise<{ status: number; body: Record<string, unknown> }> {\n const hook = this.hooks.get(input.flowName);\n // Unknown flow and wrong hookId answer identically — no oracle for\n // probing which flows exist.\n if (!hook || !safeEqual(hook.hookId, input.hookId)) {\n return { status: 404, body: { success: false, error: { code: 'RESOURCE_NOT_FOUND', message: 'No such hook.' } } };\n }\n if (hook.secret && !verifySignature(hook.secret, input.rawBody, input.signatureHeader)) {\n return { status: 401, body: { success: false, error: { code: 'INVALID_SIGNATURE', message: 'Signature verification failed.' } } };\n }\n\n let payload: Record<string, unknown>;\n try {\n const parsed: unknown = input.rawBody.trim() ? JSON.parse(input.rawBody) : {};\n if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return { status: 400, body: { success: false, error: { code: 'INVALID_REQUEST', message: 'Body must be a JSON object.' } } };\n }\n payload = parsed as Record<string, unknown>;\n } catch {\n return { status: 400, body: { success: false, error: { code: 'INVALID_REQUEST', message: 'Body must be valid JSON.' } } };\n }\n\n const q = this.getQueue();\n if (!q) {\n return { status: 503, body: { success: false, error: { code: 'SERVICE_UNAVAILABLE', message: 'No queue service is registered.' } } };\n }\n try {\n const messageId = await q.publish(hook.queue, { payload }, {\n idempotencyKey: input.idempotencyKey,\n });\n return { status: 202, body: { accepted: true, messageId } };\n } catch (err: any) {\n this.logger.warn(`[trigger-api] enqueue failed for '${hook.queue}': ${err?.message ?? err}`);\n return { status: 503, body: { success: false, error: { code: 'ENQUEUE_FAILED', message: 'Could not enqueue the trigger message.' } } };\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ApiTrigger } from './api-trigger.js';\nimport type { FlowTrigger, QueueServiceSurface } from './api-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs. Declared structurally\n * so the plugin does not take a build dependency on\n * `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/** The slice of the Hono host server this plugin needs. */\ninterface HttpServerSurface {\n getRawApp(): {\n post(path: string, handler: (c: any) => Promise<unknown> | unknown): void;\n } | null;\n}\n\n/** Mount point for inbound hooks on the host HTTP server. */\nexport const HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId';\n\n/**\n * ApiTriggerPlugin (ADR-0041 Tier 1)\n *\n * Makes `type: 'api'` flows actually fire. The automation engine derives an\n * `api` binding from the flow; this plugin provides the concrete trigger:\n *\n * - mounts `POST /api/v1/automation/hooks/:flowName/:hookId` on the host\n * Hono app (resolved via the `http-server` service);\n * - validates hookId + HMAC signature (`x-objectstack-signature`,\n * GitHub/Stripe style, constant-time) against the flow's start-node\n * config;\n * - **enqueues** the payload (`queue` service) and ACKs 202 — flow\n * execution happens on the queue consumer, never in-band with the\n * inbound request (ADR-0041 §5: boundary triggers must not let a slow\n * flow drop or block events). `x-idempotency-key` passes through to the\n * queue's dedup window.\n *\n * Webhook payloads surface to the flow as the trigger record (`$record` /\n * `record.*` / bare field references) — the same authoring surface\n * record-change flows use.\n */\nexport class ApiTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.api';\n type = 'standard' as const;\n version = '1.0.0';\n dependencies = ['com.objectstack.service.queue'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('API trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Resolve at kernel:ready — the automation engine, queue service, and\n // HTTP server may all start after this plugin.\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn('ApiTriggerPlugin: automation service not available — api trigger NOT installed');\n return;\n }\n if (!this.resolveService<QueueServiceSurface>(ctx, 'queue')) {\n ctx.logger.warn('ApiTriggerPlugin: queue service not available — inbound posts will 503 until one is registered');\n }\n\n const trigger = new ApiTrigger(\n () => this.resolveService<QueueServiceSurface>(ctx, 'queue'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n\n const http = this.resolveService<HttpServerSurface>(ctx, 'http-server');\n const rawApp = http && typeof http.getRawApp === 'function' ? http.getRawApp() : null;\n if (!rawApp) {\n ctx.logger.warn('ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted');\n return;\n }\n rawApp.post(HOOKS_PATH, async (c: any) => {\n const rawBody = await c.req.text();\n const out = await trigger.handleRequest({\n flowName: c.req.param('flowName'),\n hookId: c.req.param('hookId'),\n rawBody,\n signatureHeader: c.req.header('x-objectstack-signature'),\n idempotencyKey: c.req.header('x-idempotency-key') || undefined,\n });\n return c.json(out.body, out.status);\n });\n ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api-trigger.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { AutomationContext } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding` — same\n * decoupling pattern as `trigger-schedule` / `trigger-record-change`: this\n * package never imports `service-automation`.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/** Structural mirror of the engine's `FlowTrigger` extension point. */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IQueueService` this trigger needs. Boundary triggers MUST\n * ingest through the queue (ADR-0041 §5): inbound HTTP is the first event\n * source whose rate we don't control, and a slow flow execution may neither\n * drop nor block inbound events. At-least-once delivery; flows should be\n * authored idempotently (an `x-idempotency-key` header passes through to the\n * queue's dedup window).\n */\nexport interface QueueServiceSurface {\n publish<T = unknown>(queue: string, data: T, options?: { idempotencyKey?: string }): Promise<string>;\n subscribe<T = unknown>(queue: string, handler: (message: { data: T }) => Promise<void> | void): Promise<void>;\n unsubscribe(queue: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n}\n\nconst QUEUE_PREFIX = 'flow-api';\n\n/** One armed inbound hook. */\ninterface ArmedHook {\n flowName: string;\n hookId: string;\n secret?: string;\n queue: string;\n callback: (ctx: AutomationContext) => Promise<void>;\n}\n\n/** Constant-time string compare (length leak only). */\nfunction safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a, 'utf8');\n const bb = Buffer.from(b, 'utf8');\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\n/**\n * GitHub/Stripe-style HMAC verification: the sender computes\n * `sha256=<hex hmac of the raw body>` with the shared per-flow secret and\n * sends it as `x-objectstack-signature`.\n */\nexport function verifySignature(secret: string, rawBody: string, header: string | undefined): boolean {\n if (!header) return false;\n const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');\n return safeEqual(expected, header.trim());\n}\n\n/**\n * `api` flow trigger (ADR-0041 Tier 1) — inbound webhook/HTTP.\n *\n * The engine binds every `type: 'api'` flow to this trigger; `start()` arms a\n * hook (URL path + optional HMAC secret from the start node's `config`) and\n * subscribes a queue consumer that runs the flow. The HTTP side\n * ({@link handleRequest}) validates and **enqueues** — it never executes the\n * flow in-band:\n *\n * POST /api/v1/automation/hooks/:flowName/:hookId\n * → 404 unknown flow / wrong hookId\n * → 401 missing/bad HMAC signature (when the flow declares a `secret`)\n * → 400 non-JSON body\n * → 202 { accepted, messageId } — queued; a consumer executes the flow\n *\n * The JSON payload is exposed to the flow as the trigger record (`$record` /\n * `record.*`, fields flattened to bare references) plus `params` — the same\n * authoring surface record-change flows use.\n *\n * Start-node config keys:\n * - `hookId` — URL path token (default `'default'`); rotate it to revoke\n * old URLs without renaming the flow.\n * - `secret` — HMAC-SHA256 shared secret. Strongly recommended; without it\n * the endpoint accepts unsigned posts (the trigger logs a\n * warning at arm time).\n */\nexport class ApiTrigger implements FlowTrigger {\n readonly type = 'api';\n\n private hooks = new Map<string, ArmedHook>();\n\n constructor(\n private readonly getQueue: () => QueueServiceSurface | null,\n private readonly logger: TriggerLogger,\n ) {}\n\n /** Currently armed hooks (for diagnostics/tests). */\n listHooks(): Array<{ flowName: string; hookId: string; signed: boolean }> {\n return [...this.hooks.values()].map(h => ({\n flowName: h.flowName, hookId: h.hookId, signed: !!h.secret,\n }));\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const cfg = (binding.config ?? {}) as Record<string, unknown>;\n const hookId = typeof cfg.hookId === 'string' && cfg.hookId.trim() ? cfg.hookId.trim() : 'default';\n const secret = typeof cfg.secret === 'string' && cfg.secret.trim() ? cfg.secret.trim() : undefined;\n const queue = `${QUEUE_PREFIX}:${binding.flowName}`;\n\n const hook: ArmedHook = { flowName: binding.flowName, hookId, secret, queue, callback };\n this.hooks.set(binding.flowName, hook);\n\n const q = this.getQueue();\n if (!q) {\n this.logger.warn(\n `[trigger-api] no queue service — inbound posts for flow '${binding.flowName}' will be rejected until one is registered`,\n );\n } else {\n void q.subscribe<{ payload: Record<string, unknown> }>(queue, async (message) => {\n const payload = (message?.data as any)?.payload ?? {};\n await hook.callback({\n // The webhook body IS the trigger record: `$record`, `record.*`,\n // and bare field references all resolve, matching how\n // record-change flows are authored.\n record: payload,\n params: { ...payload },\n event: 'api',\n } as AutomationContext);\n }).catch((err: any) => {\n this.logger.warn(`[trigger-api] subscribe failed for '${queue}': ${err?.message ?? err}`);\n });\n }\n\n if (!secret) {\n this.logger.warn(\n `[trigger-api] flow '${binding.flowName}' armed WITHOUT a secret — endpoint accepts unsigned posts`,\n );\n }\n this.logger.info(\n `[trigger-api] armed: POST .../automation/hooks/${binding.flowName}/${hookId}${secret ? ' (HMAC required)' : ''}`,\n );\n }\n\n stop(flowName: string): void {\n const hook = this.hooks.get(flowName);\n if (!hook) return;\n this.hooks.delete(flowName);\n const q = this.getQueue();\n if (q) void q.unsubscribe(hook.queue).catch(() => { /* already gone */ });\n this.logger.info(`[trigger-api] disarmed: flow '${flowName}'`);\n }\n\n /**\n * Handle one inbound request. Transport-agnostic: the plugin adapts this\n * to the host HTTP server. Returns the response to send.\n */\n async handleRequest(input: {\n flowName: string;\n hookId: string;\n rawBody: string;\n signatureHeader?: string;\n idempotencyKey?: string;\n }): Promise<{ status: number; body: Record<string, unknown> }> {\n const hook = this.hooks.get(input.flowName);\n // Unknown flow and wrong hookId answer identically — no oracle for\n // probing which flows exist.\n if (!hook || !safeEqual(hook.hookId, input.hookId)) {\n return { status: 404, body: { success: false, error: { code: 'RESOURCE_NOT_FOUND', message: 'No such hook.' } } };\n }\n if (hook.secret && !verifySignature(hook.secret, input.rawBody, input.signatureHeader)) {\n return { status: 401, body: { success: false, error: { code: 'INVALID_SIGNATURE', message: 'Signature verification failed.' } } };\n }\n\n let payload: Record<string, unknown>;\n try {\n const parsed: unknown = input.rawBody.trim() ? JSON.parse(input.rawBody) : {};\n if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return { status: 400, body: { success: false, error: { code: 'INVALID_REQUEST', message: 'Body must be a JSON object.' } } };\n }\n payload = parsed as Record<string, unknown>;\n } catch {\n return { status: 400, body: { success: false, error: { code: 'INVALID_REQUEST', message: 'Body must be valid JSON.' } } };\n }\n\n const q = this.getQueue();\n if (!q) {\n return { status: 503, body: { success: false, error: { code: 'SERVICE_UNAVAILABLE', message: 'No queue service is registered.' } } };\n }\n try {\n const messageId = await q.publish(hook.queue, { payload }, {\n idempotencyKey: input.idempotencyKey,\n });\n return { status: 202, body: { accepted: true, messageId } };\n } catch (err: any) {\n this.logger.warn(`[trigger-api] enqueue failed for '${hook.queue}': ${err?.message ?? err}`);\n return { status: 503, body: { success: false, error: { code: 'ENQUEUE_FAILED', message: 'Could not enqueue the trigger message.' } } };\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ApiTrigger } from './api-trigger.js';\nimport type { FlowTrigger, QueueServiceSurface } from './api-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs. Declared structurally\n * so the plugin does not take a build dependency on\n * `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/** The slice of the Hono host server this plugin needs. */\ninterface HttpServerSurface {\n getRawApp(): {\n post(path: string, handler: (c: any) => Promise<unknown> | unknown): void;\n } | null;\n}\n\n/** Mount point for inbound hooks on the host HTTP server. */\nexport const HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId';\n\n/**\n * ApiTriggerPlugin (ADR-0041 Tier 1)\n *\n * Makes `type: 'api'` flows actually fire. The automation engine derives an\n * `api` binding from the flow; this plugin provides the concrete trigger:\n *\n * - mounts `POST /api/v1/automation/hooks/:flowName/:hookId` on the host\n * Hono app (resolved via the `http-server` service);\n * - validates hookId + HMAC signature (`x-objectstack-signature`,\n * GitHub/Stripe style, constant-time) against the flow's start-node\n * config;\n * - **enqueues** the payload (`queue` service) and ACKs 202 — flow\n * execution happens on the queue consumer, never in-band with the\n * inbound request (ADR-0041 §5: boundary triggers must not let a slow\n * flow drop or block events). `x-idempotency-key` passes through to the\n * queue's dedup window.\n *\n * Webhook payloads surface to the flow as the trigger record (`$record` /\n * `record.*` / bare field references) — the same authoring surface\n * record-change flows use.\n */\nexport class ApiTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.api';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.service.queue'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('API trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Resolve at kernel:ready — the automation engine, queue service, and\n // HTTP server may all start after this plugin.\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn('ApiTriggerPlugin: automation service not available — api trigger NOT installed');\n return;\n }\n if (!this.resolveService<QueueServiceSurface>(ctx, 'queue')) {\n ctx.logger.warn('ApiTriggerPlugin: queue service not available — inbound posts will 503 until one is registered');\n }\n\n const trigger = new ApiTrigger(\n () => this.resolveService<QueueServiceSurface>(ctx, 'queue'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n\n const http = this.resolveService<HttpServerSurface>(ctx, 'http-server');\n const rawApp = http && typeof http.getRawApp === 'function' ? http.getRawApp() : null;\n if (!rawApp) {\n ctx.logger.warn('ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted');\n return;\n }\n rawApp.post(HOOKS_PATH, async (c: any) => {\n const rawBody = await c.req.text();\n const out = await trigger.handleRequest({\n flowName: c.req.param('flowName'),\n hookId: c.req.param('hookId'),\n rawBody,\n signatureHeader: c.req.header('x-objectstack-signature'),\n idempotencyKey: c.req.header('x-idempotency-key') || undefined,\n });\n return c.json(out.body, out.status);\n });\n ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/api-trigger.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\nimport type { AutomationContext } from '@objectstack/spec/contracts';\n\n/**\n * Structural mirror of the automation engine's `FlowTriggerBinding` — same\n * decoupling pattern as `trigger-schedule` / `trigger-record-change`: this\n * package never imports `service-automation`.\n */\nexport interface FlowTriggerBinding {\n readonly flowName: string;\n readonly object?: string;\n readonly event?: string;\n readonly condition?: string | { dialect?: string; source?: string; ast?: unknown };\n readonly schedule?: unknown;\n readonly config?: Record<string, unknown>;\n}\n\n/** Structural mirror of the engine's `FlowTrigger` extension point. */\nexport interface FlowTrigger {\n readonly type: string;\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void;\n stop(flowName: string): void;\n}\n\n/**\n * The slice of `IQueueService` this trigger needs. Boundary triggers MUST\n * ingest through the queue (ADR-0041 §5): inbound HTTP is the first event\n * source whose rate we don't control, and a slow flow execution may neither\n * drop nor block inbound events. At-least-once delivery; flows should be\n * authored idempotently (an `x-idempotency-key` header passes through to the\n * queue's dedup window).\n */\nexport interface QueueServiceSurface {\n publish<T = unknown>(queue: string, data: T, options?: { idempotencyKey?: string }): Promise<string>;\n subscribe<T = unknown>(queue: string, handler: (message: { data: T }) => Promise<void> | void): Promise<void>;\n unsubscribe(queue: string): Promise<void>;\n}\n\n/** Minimal logger surface (matches core's `ctx.logger`). */\nexport interface TriggerLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n debug?(msg: string, ...args: unknown[]): void;\n}\n\nconst QUEUE_PREFIX = 'flow-api';\n\n/** One armed inbound hook. */\ninterface ArmedHook {\n flowName: string;\n hookId: string;\n secret?: string;\n queue: string;\n callback: (ctx: AutomationContext) => Promise<void>;\n}\n\n/** Constant-time string compare (length leak only). */\nfunction safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a, 'utf8');\n const bb = Buffer.from(b, 'utf8');\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\n/**\n * GitHub/Stripe-style HMAC verification: the sender computes\n * `sha256=<hex hmac of the raw body>` with the shared per-flow secret and\n * sends it as `x-objectstack-signature`.\n */\nexport function verifySignature(secret: string, rawBody: string, header: string | undefined): boolean {\n if (!header) return false;\n const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');\n return safeEqual(expected, header.trim());\n}\n\n/**\n * `api` flow trigger (ADR-0041 Tier 1) — inbound webhook/HTTP.\n *\n * The engine binds every `type: 'api'` flow to this trigger; `start()` arms a\n * hook (URL path + optional HMAC secret from the start node's `config`) and\n * subscribes a queue consumer that runs the flow. The HTTP side\n * ({@link handleRequest}) validates and **enqueues** — it never executes the\n * flow in-band:\n *\n * POST /api/v1/automation/hooks/:flowName/:hookId\n * → 404 unknown flow / wrong hookId\n * → 401 missing/bad HMAC signature (when the flow declares a `secret`)\n * → 400 non-JSON body\n * → 202 { accepted, messageId } — queued; a consumer executes the flow\n *\n * The JSON payload is exposed to the flow as the trigger record (`$record` /\n * `record.*`, fields flattened to bare references) plus `params` — the same\n * authoring surface record-change flows use.\n *\n * Start-node config keys:\n * - `hookId` — URL path token (default `'default'`); rotate it to revoke\n * old URLs without renaming the flow.\n * - `secret` — HMAC-SHA256 shared secret. Strongly recommended; without it\n * the endpoint accepts unsigned posts (the trigger logs a\n * warning at arm time).\n */\nexport class ApiTrigger implements FlowTrigger {\n readonly type = 'api';\n\n private hooks = new Map<string, ArmedHook>();\n\n constructor(\n private readonly getQueue: () => QueueServiceSurface | null,\n private readonly logger: TriggerLogger,\n ) {}\n\n /** Currently armed hooks (for diagnostics/tests). */\n listHooks(): Array<{ flowName: string; hookId: string; signed: boolean }> {\n return [...this.hooks.values()].map(h => ({\n flowName: h.flowName, hookId: h.hookId, signed: !!h.secret,\n }));\n }\n\n start(binding: FlowTriggerBinding, callback: (ctx: AutomationContext) => Promise<void>): void {\n const cfg = (binding.config ?? {}) as Record<string, unknown>;\n const hookId = typeof cfg.hookId === 'string' && cfg.hookId.trim() ? cfg.hookId.trim() : 'default';\n const secret = typeof cfg.secret === 'string' && cfg.secret.trim() ? cfg.secret.trim() : undefined;\n const queue = `${QUEUE_PREFIX}:${binding.flowName}`;\n\n const hook: ArmedHook = { flowName: binding.flowName, hookId, secret, queue, callback };\n this.hooks.set(binding.flowName, hook);\n\n const q = this.getQueue();\n if (!q) {\n this.logger.warn(\n `[trigger-api] no queue service — inbound posts for flow '${binding.flowName}' will be rejected until one is registered`,\n );\n } else {\n void q.subscribe<{ payload: Record<string, unknown> }>(queue, async (message) => {\n const payload = (message?.data as any)?.payload ?? {};\n await hook.callback({\n // The webhook body IS the trigger record: `$record`, `record.*`,\n // and bare field references all resolve, matching how\n // record-change flows are authored.\n record: payload,\n params: { ...payload },\n event: 'api',\n } as AutomationContext);\n }).catch((err: any) => {\n this.logger.warn(`[trigger-api] subscribe failed for '${queue}': ${err?.message ?? err}`);\n });\n }\n\n if (!secret) {\n this.logger.warn(\n `[trigger-api] flow '${binding.flowName}' armed WITHOUT a secret — endpoint accepts unsigned posts`,\n );\n }\n this.logger.info(\n `[trigger-api] armed: POST .../automation/hooks/${binding.flowName}/${hookId}${secret ? ' (HMAC required)' : ''}`,\n );\n }\n\n stop(flowName: string): void {\n const hook = this.hooks.get(flowName);\n if (!hook) return;\n this.hooks.delete(flowName);\n const q = this.getQueue();\n if (q) void q.unsubscribe(hook.queue).catch(() => { /* already gone */ });\n this.logger.info(`[trigger-api] disarmed: flow '${flowName}'`);\n }\n\n /**\n * Handle one inbound request. Transport-agnostic: the plugin adapts this\n * to the host HTTP server. Returns the response to send.\n */\n async handleRequest(input: {\n flowName: string;\n hookId: string;\n rawBody: string;\n signatureHeader?: string;\n idempotencyKey?: string;\n }): Promise<{ status: number; body: Record<string, unknown> }> {\n const hook = this.hooks.get(input.flowName);\n // Unknown flow and wrong hookId answer identically — no oracle for\n // probing which flows exist.\n if (!hook || !safeEqual(hook.hookId, input.hookId)) {\n return { status: 404, body: { success: false, error: { code: 'RESOURCE_NOT_FOUND', message: 'No such hook.' } } };\n }\n if (hook.secret && !verifySignature(hook.secret, input.rawBody, input.signatureHeader)) {\n return { status: 401, body: { success: false, error: { code: 'INVALID_SIGNATURE', message: 'Signature verification failed.' } } };\n }\n\n let payload: Record<string, unknown>;\n try {\n const parsed: unknown = input.rawBody.trim() ? JSON.parse(input.rawBody) : {};\n if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return { status: 400, body: { success: false, error: { code: 'INVALID_REQUEST', message: 'Body must be a JSON object.' } } };\n }\n payload = parsed as Record<string, unknown>;\n } catch {\n return { status: 400, body: { success: false, error: { code: 'INVALID_REQUEST', message: 'Body must be valid JSON.' } } };\n }\n\n const q = this.getQueue();\n if (!q) {\n return { status: 503, body: { success: false, error: { code: 'SERVICE_UNAVAILABLE', message: 'No queue service is registered.' } } };\n }\n try {\n const messageId = await q.publish(hook.queue, { payload }, {\n idempotencyKey: input.idempotencyKey,\n });\n return { status: 202, body: { accepted: true, messageId } };\n } catch (err: any) {\n this.logger.warn(`[trigger-api] enqueue failed for '${hook.queue}': ${err?.message ?? err}`);\n return { status: 503, body: { success: false, error: { code: 'ENQUEUE_FAILED', message: 'Could not enqueue the trigger message.' } } };\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { ApiTrigger } from './api-trigger.js';\nimport type { FlowTrigger, QueueServiceSurface } from './api-trigger.js';\n\n/**\n * The slice of the automation engine this plugin needs. Declared structurally\n * so the plugin does not take a build dependency on\n * `@objectstack/service-automation`.\n */\ninterface AutomationTriggerRegistry {\n registerTrigger(trigger: FlowTrigger): void;\n unregisterTrigger?(type: string): void;\n}\n\n/** The slice of the Hono host server this plugin needs. */\ninterface HttpServerSurface {\n getRawApp(): {\n post(path: string, handler: (c: any) => Promise<unknown> | unknown): void;\n } | null;\n}\n\n/** Mount point for inbound hooks on the host HTTP server. */\nexport const HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId';\n\n/**\n * ApiTriggerPlugin (ADR-0041 Tier 1)\n *\n * Makes `type: 'api'` flows actually fire. The automation engine derives an\n * `api` binding from the flow; this plugin provides the concrete trigger:\n *\n * - mounts `POST /api/v1/automation/hooks/:flowName/:hookId` on the host\n * Hono app (resolved via the `http-server` service);\n * - validates hookId + HMAC signature (`x-objectstack-signature`,\n * GitHub/Stripe style, constant-time) against the flow's start-node\n * config;\n * - **enqueues** the payload (`queue` service) and ACKs 202 — flow\n * execution happens on the queue consumer, never in-band with the\n * inbound request (ADR-0041 §5: boundary triggers must not let a slow\n * flow drop or block events). `x-idempotency-key` passes through to the\n * queue's dedup window.\n *\n * Webhook payloads surface to the flow as the trigger record (`$record` /\n * `record.*` / bare field references) — the same authoring surface\n * record-change flows use.\n */\nexport class ApiTriggerPlugin implements Plugin {\n name = 'com.objectstack.trigger.api';\n type = 'standard' as const;\n version = '1.0.0';\n dependencies = ['com.objectstack.service.queue'];\n\n async init(ctx: PluginContext): Promise<void> {\n ctx.logger.info('API trigger plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Resolve at kernel:ready — the automation engine, queue service, and\n // HTTP server may all start after this plugin.\n ctx.hook('kernel:ready', async () => {\n const automation = this.resolveService<AutomationTriggerRegistry>(ctx, 'automation');\n if (!automation || typeof automation.registerTrigger !== 'function') {\n ctx.logger.warn('ApiTriggerPlugin: automation service not available — api trigger NOT installed');\n return;\n }\n if (!this.resolveService<QueueServiceSurface>(ctx, 'queue')) {\n ctx.logger.warn('ApiTriggerPlugin: queue service not available — inbound posts will 503 until one is registered');\n }\n\n const trigger = new ApiTrigger(\n () => this.resolveService<QueueServiceSurface>(ctx, 'queue'),\n ctx.logger,\n );\n automation.registerTrigger(trigger);\n\n const http = this.resolveService<HttpServerSurface>(ctx, 'http-server');\n const rawApp = http && typeof http.getRawApp === 'function' ? http.getRawApp() : null;\n if (!rawApp) {\n ctx.logger.warn('ApiTriggerPlugin: HTTP server not available — hooks endpoint not mounted');\n return;\n }\n rawApp.post(HOOKS_PATH, async (c: any) => {\n const rawBody = await c.req.text();\n const out = await trigger.handleRequest({\n flowName: c.req.param('flowName'),\n hookId: c.req.param('hookId'),\n rawBody,\n signatureHeader: c.req.header('x-objectstack-signature'),\n idempotencyKey: c.req.header('x-idempotency-key') || undefined,\n });\n return c.json(out.body, out.status);\n });\n ctx.logger.info(`ApiTriggerPlugin: api trigger registered, hooks mounted at POST ${HOOKS_PATH}`);\n });\n }\n\n private resolveService<T>(ctx: PluginContext, name: string): T | null {\n try {\n return ctx.getService<T>(name) ?? null;\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/trigger-api",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.3.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "17.
|
|
17
|
-
"@objectstack/spec": "17.
|
|
16
|
+
"@objectstack/core": "17.3.0",
|
|
17
|
+
"@objectstack/spec": "17.3.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^26.2.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"CHANGELOG.md"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
-
"build": "tsup --config ../../../tsup.config.ts",
|
|
36
|
+
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
|
|
37
37
|
"test": "vitest run --passWithNoTests",
|
|
38
38
|
"typecheck": "tsc --noEmit"
|
|
39
39
|
}
|