lua-cli 3.42.0-beta.1 → 3.42.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-exports.d.ts +2458 -112
- package/dist/api-exports.js +18145 -11424
- package/dist/api-exports.js.map +1 -1
- package/dist/index.js +9696 -990
- package/dist/index.js.map +1 -1
- package/dist/voice/test/index.d.ts +54 -54
- package/dist/workflow-builder.d.ts +16 -2
- package/dist/workflow-builder.js +5788 -317
- package/dist/workflow-builder.js.map +1 -1
- package/docs/API_REFERENCE.md +7 -1
- package/docs/api/AI.md +3 -1
- package/docs/api/Agents.md +275 -12
- package/docs/api/Data.md +192 -61
- package/docs/api/Jobs.md +2 -0
- package/docs/api/LuaWebhook.md +168 -87
- package/docs/api/LuaWorkflow.md +15 -15
- package/docs/api/Orders.md +7 -5
- package/docs/api/Products.md +1 -1
- package/docs/api/Progress.md +57 -0
- package/docs/api/Threads.md +122 -0
- package/docs/api/User.md +2 -2
- package/docs/api/Workflows.md +22 -1
- package/docs/workflows/approvals.md +4 -0
- package/docs/workflows/coding-harness.md +27 -1
- package/docs/workflows/workspaces-and-long-steps.md +3 -1
- package/package.json +3 -3
- package/scripts/generate-catalog.lib.ts +813 -0
- package/scripts/generate-catalog.ts +94 -0
- package/template/package.json +1 -1
- package/template-web-app/web/src/lua/client.ts +11 -1
package/docs/api/LuaWebhook.md
CHANGED
|
@@ -12,6 +12,7 @@ import { z } from 'zod';
|
|
|
12
12
|
## Overview
|
|
13
13
|
|
|
14
14
|
Webhooks enable:
|
|
15
|
+
|
|
15
16
|
- Receive HTTP requests from external services
|
|
16
17
|
- Validate query parameters, headers, and body
|
|
17
18
|
- Process events from third-party platforms
|
|
@@ -51,11 +52,49 @@ interface LuaWebhookConfig {
|
|
|
51
52
|
/** Optional HMAC-SHA256 signing key (see Request Verification) */
|
|
52
53
|
secret?: string;
|
|
53
54
|
|
|
55
|
+
/** Reject schema-violating requests with HTTP 400 (default: log a warning only) */
|
|
56
|
+
enforceSchemas?: boolean;
|
|
57
|
+
|
|
54
58
|
/** Function that executes the webhook logic */
|
|
55
59
|
execute: (event: LuaWebhookEvent) => Promise<any>;
|
|
56
60
|
}
|
|
57
61
|
```
|
|
58
62
|
|
|
63
|
+
## Schema Enforcement
|
|
64
|
+
|
|
65
|
+
`querySchema`, `headerSchema` and `bodySchema` are checked before `execute` runs, for webhooks
|
|
66
|
+
compiled with this CLI version or later. Only the schemas you declare are applied. An absent input
|
|
67
|
+
is validated as `{}` (query/headers) or `undefined` (body), so a required `bodySchema` flags a
|
|
68
|
+
request with no body.
|
|
69
|
+
|
|
70
|
+
**Default: log only.** A violating request still reaches `execute` with the original `event`, and a
|
|
71
|
+
single warning is written to the webhook's logs (also printed by `lua test`):
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
[lua:webhook-schema] would-reject webhook=<name> parts=query,body issues=3 first=body:n:invalid_type (request still processed; ...)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The line names the webhook, the failing parts, the total issue count and the first issue as
|
|
78
|
+
`<part>:<path>:<code>`. It never includes input values or issue messages. Filter the logs on the
|
|
79
|
+
`[lua:webhook-schema] would-reject` prefix to count would-rejects.
|
|
80
|
+
|
|
81
|
+
**Opt in to rejection** with `enforceSchemas: true`. A violating request is then rejected before
|
|
82
|
+
`execute` (query is checked first, then headers, then body) with HTTP 400 when deployed, and fails
|
|
83
|
+
`lua test` with the same message:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{ "statusCode": 400, "code": "VALIDATION", "message": "Invalid body", "part": "body", "issues": [] }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Recommended rollout: deploy, watch the would-reject warnings, fix the schemas or the senders, then
|
|
90
|
+
set `enforceSchemas: true`. Only a strict boolean `true` enforces.
|
|
91
|
+
|
|
92
|
+
- Validation only: `execute` receives the original `event`. No coercion, defaults or key stripping are applied to `event.query`, `event.headers` or `event.body`.
|
|
93
|
+
- Header names are lowercase, so keys in a `headerSchema` must be lowercase (`'x-probe'`, not `'X-Probe'`). Transport headers are present too, so avoid `z.strictObject` for headers.
|
|
94
|
+
- Webhooks that also subscribe to events receive `body: { eventType, ...payload }`; make sure `bodySchema` allows `eventType`.
|
|
95
|
+
- Event deliveries are checked too, and they arrive with EMPTY `query` and `headers`. A `querySchema` or `headerSchema` with required keys, or a `bodySchema` that rejects `eventType`, logs a warning on every event delivery. Only with `enforceSchemas: true` do those deliveries fail, get logged at ERROR and not retried. Keep these schemas permissive, or declare them only on webhooks that are never event-subscribed.
|
|
96
|
+
- Webhooks deployed from older CLI versions keep the old, unvalidated behaviour until they are re-compiled and re-pushed.
|
|
97
|
+
|
|
59
98
|
## Request Verification
|
|
60
99
|
|
|
61
100
|
A webhook URL is public. Set `secret` and Lua requires every request to carry
|
|
@@ -94,11 +133,22 @@ interface LuaWebhookEvent {
|
|
|
94
133
|
/** Request body (JSON or raw data) */
|
|
95
134
|
body?: any;
|
|
96
135
|
|
|
136
|
+
/** Exact unparsed request bytes (utf8) — for verifying vendor signatures */
|
|
137
|
+
rawBody?: string;
|
|
138
|
+
|
|
97
139
|
/** ISO timestamp when the webhook was received */
|
|
98
140
|
timestamp: string;
|
|
99
141
|
}
|
|
100
142
|
```
|
|
101
143
|
|
|
144
|
+
`rawBody` is the request exactly as it arrived on the wire, as a utf8 string.
|
|
145
|
+
Vendor HMAC schemes (Stripe `t=…,v1=…`, GitHub `sha256=…`, Slack `v0:…`) are
|
|
146
|
+
computed over those bytes, and `JSON.stringify(event.body)` does not reproduce
|
|
147
|
+
them (whitespace, key order and escaping differ), so verify against `rawBody`.
|
|
148
|
+
It is present for JSON requests (`''` for an empty body) and absent for
|
|
149
|
+
form-encoded or plain-text bodies and for webhooks fired by platform events.
|
|
150
|
+
Header keys arrive lowercased. See [Verify Signatures](#verify-signatures).
|
|
151
|
+
|
|
102
152
|
## Complete Examples
|
|
103
153
|
|
|
104
154
|
### GitHub Webhook
|
|
@@ -113,22 +163,24 @@ export const githubWebhook = new LuaWebhook({
|
|
|
113
163
|
|
|
114
164
|
headerSchema: z.object({
|
|
115
165
|
'x-github-event': z.string(),
|
|
116
|
-
'x-hub-signature': z.string()
|
|
166
|
+
'x-hub-signature': z.string(),
|
|
117
167
|
}),
|
|
118
168
|
|
|
119
169
|
bodySchema: z.object({
|
|
120
170
|
repository: z.object({
|
|
121
171
|
name: z.string(),
|
|
122
|
-
full_name: z.string()
|
|
172
|
+
full_name: z.string(),
|
|
123
173
|
}),
|
|
124
174
|
pusher: z.object({
|
|
125
175
|
name: z.string(),
|
|
126
|
-
email: z.string()
|
|
176
|
+
email: z.string(),
|
|
127
177
|
}),
|
|
128
|
-
commits: z.array(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
178
|
+
commits: z.array(
|
|
179
|
+
z.object({
|
|
180
|
+
message: z.string(),
|
|
181
|
+
url: z.string(),
|
|
182
|
+
})
|
|
183
|
+
),
|
|
132
184
|
}),
|
|
133
185
|
|
|
134
186
|
execute: async (event) => {
|
|
@@ -138,7 +190,7 @@ export const githubWebhook = new LuaWebhook({
|
|
|
138
190
|
if (headers['x-github-event'] !== 'push') {
|
|
139
191
|
return {
|
|
140
192
|
success: false,
|
|
141
|
-
message: 'Only push events are supported'
|
|
193
|
+
message: 'Only push events are supported',
|
|
142
194
|
};
|
|
143
195
|
}
|
|
144
196
|
|
|
@@ -149,25 +201,27 @@ export const githubWebhook = new LuaWebhook({
|
|
|
149
201
|
author: body.pusher.name,
|
|
150
202
|
message: commit.message,
|
|
151
203
|
url: commit.url,
|
|
152
|
-
timestamp: event.timestamp
|
|
204
|
+
timestamp: event.timestamp,
|
|
153
205
|
});
|
|
154
206
|
}
|
|
155
207
|
|
|
156
208
|
// Notify relevant user
|
|
157
209
|
const user = await User.get({ email: body.pusher.email });
|
|
158
210
|
if (user) {
|
|
159
|
-
await user.send([
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
211
|
+
await user.send([
|
|
212
|
+
{
|
|
213
|
+
type: 'text',
|
|
214
|
+
text: `✅ ${body.commits.length} commits pushed to ${body.repository.name}`,
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
163
217
|
}
|
|
164
218
|
|
|
165
219
|
return {
|
|
166
220
|
success: true,
|
|
167
221
|
commits: body.commits.length,
|
|
168
|
-
repository: body.repository.name
|
|
222
|
+
repository: body.repository.name,
|
|
169
223
|
};
|
|
170
|
-
}
|
|
224
|
+
},
|
|
171
225
|
});
|
|
172
226
|
```
|
|
173
227
|
|
|
@@ -182,7 +236,7 @@ export const stripeWebhook = new LuaWebhook({
|
|
|
182
236
|
description: 'Handles Stripe payment events',
|
|
183
237
|
|
|
184
238
|
headerSchema: z.object({
|
|
185
|
-
'stripe-signature': z.string()
|
|
239
|
+
'stripe-signature': z.string(),
|
|
186
240
|
}),
|
|
187
241
|
|
|
188
242
|
bodySchema: z.object({
|
|
@@ -193,9 +247,9 @@ export const stripeWebhook = new LuaWebhook({
|
|
|
193
247
|
amount: z.number(),
|
|
194
248
|
currency: z.string(),
|
|
195
249
|
customer: z.string().optional(),
|
|
196
|
-
metadata: z.record(z.any()).optional()
|
|
197
|
-
})
|
|
198
|
-
})
|
|
250
|
+
metadata: z.record(z.any()).optional(),
|
|
251
|
+
}),
|
|
252
|
+
}),
|
|
199
253
|
}),
|
|
200
254
|
|
|
201
255
|
execute: async (event) => {
|
|
@@ -214,23 +268,25 @@ export const stripeWebhook = new LuaWebhook({
|
|
|
214
268
|
await order.update({
|
|
215
269
|
paymentId: payment.id,
|
|
216
270
|
paidAmount: payment.amount / 100,
|
|
217
|
-
paidAt: event.timestamp
|
|
271
|
+
paidAt: event.timestamp,
|
|
218
272
|
});
|
|
219
273
|
|
|
220
274
|
// Notify customer
|
|
221
275
|
if (payment.customer) {
|
|
222
276
|
const user = await User.get(payment.customer);
|
|
223
|
-
await user.send([
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
277
|
+
await user.send([
|
|
278
|
+
{
|
|
279
|
+
type: 'text',
|
|
280
|
+
text: `✅ Payment received! Your order #${orderId} is being processed.`,
|
|
281
|
+
},
|
|
282
|
+
]);
|
|
227
283
|
}
|
|
228
284
|
}
|
|
229
285
|
|
|
230
286
|
return {
|
|
231
287
|
success: true,
|
|
232
288
|
event: 'payment_succeeded',
|
|
233
|
-
orderId
|
|
289
|
+
orderId,
|
|
234
290
|
};
|
|
235
291
|
}
|
|
236
292
|
|
|
@@ -241,16 +297,18 @@ export const stripeWebhook = new LuaWebhook({
|
|
|
241
297
|
// Notify customer of failure
|
|
242
298
|
if (payment.customer) {
|
|
243
299
|
const user = await User.get(payment.customer);
|
|
244
|
-
await user.send([
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
300
|
+
await user.send([
|
|
301
|
+
{
|
|
302
|
+
type: 'text',
|
|
303
|
+
text: `❌ Payment failed for order #${orderId}. Please try again.`,
|
|
304
|
+
},
|
|
305
|
+
]);
|
|
248
306
|
}
|
|
249
307
|
|
|
250
308
|
return {
|
|
251
309
|
success: true,
|
|
252
310
|
event: 'payment_failed',
|
|
253
|
-
orderId
|
|
311
|
+
orderId,
|
|
254
312
|
};
|
|
255
313
|
}
|
|
256
314
|
|
|
@@ -258,10 +316,10 @@ export const stripeWebhook = new LuaWebhook({
|
|
|
258
316
|
return {
|
|
259
317
|
success: true,
|
|
260
318
|
event: body.type,
|
|
261
|
-
message: 'Event received but not handled'
|
|
319
|
+
message: 'Event received but not handled',
|
|
262
320
|
};
|
|
263
321
|
}
|
|
264
|
-
}
|
|
322
|
+
},
|
|
265
323
|
});
|
|
266
324
|
```
|
|
267
325
|
|
|
@@ -278,12 +336,14 @@ export const slackWebhook = new LuaWebhook({
|
|
|
278
336
|
bodySchema: z.object({
|
|
279
337
|
type: z.string(),
|
|
280
338
|
challenge: z.string().optional(),
|
|
281
|
-
event: z
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
339
|
+
event: z
|
|
340
|
+
.object({
|
|
341
|
+
type: z.string(),
|
|
342
|
+
user: z.string().optional(),
|
|
343
|
+
text: z.string().optional(),
|
|
344
|
+
channel: z.string(),
|
|
345
|
+
})
|
|
346
|
+
.optional(),
|
|
287
347
|
}),
|
|
288
348
|
|
|
289
349
|
execute: async (event) => {
|
|
@@ -306,10 +366,9 @@ export const slackWebhook = new LuaWebhook({
|
|
|
306
366
|
// Process user message
|
|
307
367
|
if (slackEvent.type === 'message' && slackEvent.text) {
|
|
308
368
|
// Generate AI response
|
|
309
|
-
const response = await AI.generate(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
);
|
|
369
|
+
const response = await AI.generate('You are a helpful Slack bot assistant.', [
|
|
370
|
+
{ type: 'text', text: slackEvent.text },
|
|
371
|
+
]);
|
|
313
372
|
|
|
314
373
|
// Store interaction
|
|
315
374
|
await Data.create('slack_messages', {
|
|
@@ -317,20 +376,20 @@ export const slackWebhook = new LuaWebhook({
|
|
|
317
376
|
channel: slackEvent.channel,
|
|
318
377
|
message: slackEvent.text,
|
|
319
378
|
response,
|
|
320
|
-
timestamp: event.timestamp
|
|
379
|
+
timestamp: event.timestamp,
|
|
321
380
|
});
|
|
322
381
|
|
|
323
382
|
// TODO: Send response back to Slack using Slack API
|
|
324
383
|
|
|
325
384
|
return {
|
|
326
385
|
success: true,
|
|
327
|
-
response
|
|
386
|
+
response,
|
|
328
387
|
};
|
|
329
388
|
}
|
|
330
389
|
}
|
|
331
390
|
|
|
332
391
|
return { success: true, message: 'Event received' };
|
|
333
|
-
}
|
|
392
|
+
},
|
|
334
393
|
});
|
|
335
394
|
```
|
|
336
395
|
|
|
@@ -345,7 +404,7 @@ export const contactFormWebhook = new LuaWebhook({
|
|
|
345
404
|
description: 'Handles contact form submissions',
|
|
346
405
|
|
|
347
406
|
querySchema: z.object({
|
|
348
|
-
source: z.enum(['website', 'mobile_app']).optional()
|
|
407
|
+
source: z.enum(['website', 'mobile_app']).optional(),
|
|
349
408
|
}),
|
|
350
409
|
|
|
351
410
|
bodySchema: z.object({
|
|
@@ -354,7 +413,7 @@ export const contactFormWebhook = new LuaWebhook({
|
|
|
354
413
|
phone: z.string().optional(),
|
|
355
414
|
subject: z.string(),
|
|
356
415
|
message: z.string().min(10),
|
|
357
|
-
consent: z.boolean()
|
|
416
|
+
consent: z.boolean(),
|
|
358
417
|
}),
|
|
359
418
|
|
|
360
419
|
execute: async (event) => {
|
|
@@ -364,7 +423,7 @@ export const contactFormWebhook = new LuaWebhook({
|
|
|
364
423
|
if (!body.consent) {
|
|
365
424
|
return {
|
|
366
425
|
success: false,
|
|
367
|
-
error: 'Consent is required'
|
|
426
|
+
error: 'Consent is required',
|
|
368
427
|
};
|
|
369
428
|
}
|
|
370
429
|
|
|
@@ -377,7 +436,7 @@ export const contactFormWebhook = new LuaWebhook({
|
|
|
377
436
|
message: body.message,
|
|
378
437
|
source: query?.source || 'unknown',
|
|
379
438
|
submittedAt: event.timestamp,
|
|
380
|
-
status: 'new'
|
|
439
|
+
status: 'new',
|
|
381
440
|
});
|
|
382
441
|
|
|
383
442
|
// Create or update user
|
|
@@ -387,22 +446,24 @@ export const contactFormWebhook = new LuaWebhook({
|
|
|
387
446
|
await user.update({
|
|
388
447
|
name: body.name,
|
|
389
448
|
email: body.email,
|
|
390
|
-
phone: body.phone
|
|
449
|
+
phone: body.phone,
|
|
391
450
|
});
|
|
392
451
|
}
|
|
393
452
|
|
|
394
453
|
// Send confirmation
|
|
395
|
-
await user.send([
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
454
|
+
await user.send([
|
|
455
|
+
{
|
|
456
|
+
type: 'text',
|
|
457
|
+
text: `Thank you for contacting us, ${body.name}! We've received your message about "${body.subject}" and will get back to you soon.`,
|
|
458
|
+
},
|
|
459
|
+
]);
|
|
399
460
|
|
|
400
461
|
return {
|
|
401
462
|
success: true,
|
|
402
463
|
submissionId: entry.id,
|
|
403
|
-
message: 'Form submitted successfully'
|
|
464
|
+
message: 'Form submitted successfully',
|
|
404
465
|
};
|
|
405
|
-
}
|
|
466
|
+
},
|
|
406
467
|
});
|
|
407
468
|
```
|
|
408
469
|
|
|
@@ -418,17 +479,17 @@ export const apiWebhook = new LuaWebhook({
|
|
|
418
479
|
|
|
419
480
|
headerSchema: z.object({
|
|
420
481
|
'x-api-key': z.string(),
|
|
421
|
-
'content-type': z.literal('application/json')
|
|
482
|
+
'content-type': z.literal('application/json'),
|
|
422
483
|
}),
|
|
423
484
|
|
|
424
485
|
querySchema: z.object({
|
|
425
486
|
action: z.enum(['create', 'update', 'delete']),
|
|
426
|
-
entity: z.string()
|
|
487
|
+
entity: z.string(),
|
|
427
488
|
}),
|
|
428
489
|
|
|
429
490
|
bodySchema: z.object({
|
|
430
491
|
id: z.string(),
|
|
431
|
-
data: z.record(z.any())
|
|
492
|
+
data: z.record(z.any()),
|
|
432
493
|
}),
|
|
433
494
|
|
|
434
495
|
execute: async (event) => {
|
|
@@ -439,7 +500,7 @@ export const apiWebhook = new LuaWebhook({
|
|
|
439
500
|
if (headers['x-api-key'] !== validApiKey) {
|
|
440
501
|
return {
|
|
441
502
|
success: false,
|
|
442
|
-
error: 'Invalid API key'
|
|
503
|
+
error: 'Invalid API key',
|
|
443
504
|
};
|
|
444
505
|
}
|
|
445
506
|
|
|
@@ -449,7 +510,7 @@ export const apiWebhook = new LuaWebhook({
|
|
|
449
510
|
await Data.create(query.entity, {
|
|
450
511
|
externalId: body.id,
|
|
451
512
|
...body.data,
|
|
452
|
-
syncedAt: event.timestamp
|
|
513
|
+
syncedAt: event.timestamp,
|
|
453
514
|
});
|
|
454
515
|
break;
|
|
455
516
|
|
|
@@ -458,7 +519,7 @@ export const apiWebhook = new LuaWebhook({
|
|
|
458
519
|
if (entries.data.length > 0) {
|
|
459
520
|
await Data.update(query.entity, entries.data[0].id, {
|
|
460
521
|
...body.data,
|
|
461
|
-
syncedAt: event.timestamp
|
|
522
|
+
syncedAt: event.timestamp,
|
|
462
523
|
});
|
|
463
524
|
}
|
|
464
525
|
break;
|
|
@@ -475,9 +536,9 @@ export const apiWebhook = new LuaWebhook({
|
|
|
475
536
|
success: true,
|
|
476
537
|
action: query.action,
|
|
477
538
|
entity: query.entity,
|
|
478
|
-
id: body.id
|
|
539
|
+
id: body.id,
|
|
479
540
|
};
|
|
480
|
-
}
|
|
541
|
+
},
|
|
481
542
|
});
|
|
482
543
|
```
|
|
483
544
|
|
|
@@ -490,6 +551,7 @@ https://api.heylua.ai/webhook/{agentId}/{webhookName}
|
|
|
490
551
|
```
|
|
491
552
|
|
|
492
553
|
Example:
|
|
554
|
+
|
|
493
555
|
```
|
|
494
556
|
https://api.heylua.ai/webhook/agent_123/github-push
|
|
495
557
|
```
|
|
@@ -525,7 +587,7 @@ curl -X POST http://localhost:3002/webhook/contact-form \
|
|
|
525
587
|
|
|
526
588
|
### ✅ DO
|
|
527
589
|
|
|
528
|
-
- Always
|
|
590
|
+
- Always declare schemas (query, headers, body), watch the would-reject warnings, then opt in with `enforceSchemas: true`
|
|
529
591
|
- Verify webhook signatures when available (GitHub, Stripe, etc.)
|
|
530
592
|
- Return meaningful response objects
|
|
531
593
|
- Store webhook events for auditing
|
|
@@ -536,7 +598,7 @@ curl -X POST http://localhost:3002/webhook/contact-form \
|
|
|
536
598
|
|
|
537
599
|
### ❌ DON'T
|
|
538
600
|
|
|
539
|
-
- Don't skip validation -
|
|
601
|
+
- Don't skip validation - declare schemas and enable `enforceSchemas` once the warnings are clean
|
|
540
602
|
- Don't expose sensitive data in responses
|
|
541
603
|
- Don't perform long-running operations synchronously
|
|
542
604
|
- Don't trust incoming data without verification
|
|
@@ -551,7 +613,7 @@ export const webhook = new LuaWebhook({
|
|
|
551
613
|
description: 'Webhook with error handling',
|
|
552
614
|
|
|
553
615
|
bodySchema: z.object({
|
|
554
|
-
data: z.string()
|
|
616
|
+
data: z.string(),
|
|
555
617
|
}),
|
|
556
618
|
|
|
557
619
|
execute: async (event) => {
|
|
@@ -561,7 +623,7 @@ export const webhook = new LuaWebhook({
|
|
|
561
623
|
|
|
562
624
|
return {
|
|
563
625
|
success: true,
|
|
564
|
-
result
|
|
626
|
+
result,
|
|
565
627
|
};
|
|
566
628
|
} catch (error) {
|
|
567
629
|
// Log error
|
|
@@ -569,17 +631,17 @@ export const webhook = new LuaWebhook({
|
|
|
569
631
|
webhookName: 'robust-webhook',
|
|
570
632
|
error: error.message,
|
|
571
633
|
event: event.body,
|
|
572
|
-
timestamp: event.timestamp
|
|
634
|
+
timestamp: event.timestamp,
|
|
573
635
|
});
|
|
574
636
|
|
|
575
637
|
// Return error response
|
|
576
638
|
return {
|
|
577
639
|
success: false,
|
|
578
640
|
error: error.message,
|
|
579
|
-
retry: true // Indicate that the sender should retry
|
|
641
|
+
retry: true, // Indicate that the sender should retry
|
|
580
642
|
};
|
|
581
643
|
}
|
|
582
|
-
}
|
|
644
|
+
},
|
|
583
645
|
});
|
|
584
646
|
```
|
|
585
647
|
|
|
@@ -587,32 +649,51 @@ export const webhook = new LuaWebhook({
|
|
|
587
649
|
|
|
588
650
|
### Verify Signatures
|
|
589
651
|
|
|
652
|
+
Verify a vendor's signature in `execute` against `event.rawBody` — the exact
|
|
653
|
+
bytes the vendor signed — never against a re-serialized `event.body`:
|
|
654
|
+
|
|
590
655
|
```typescript
|
|
591
656
|
import crypto from 'crypto';
|
|
657
|
+
import { LuaWebhook, env } from 'lua-cli';
|
|
592
658
|
|
|
593
|
-
function verifyGitHubSignature(
|
|
594
|
-
|
|
595
|
-
signature
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const hmac = crypto.createHmac('sha256', secret);
|
|
599
|
-
const digest = 'sha256=' + hmac.update(body).digest('hex');
|
|
600
|
-
return crypto.timingSafeEqual(
|
|
601
|
-
Buffer.from(signature),
|
|
602
|
-
Buffer.from(digest)
|
|
603
|
-
);
|
|
659
|
+
function verifyGitHubSignature(rawBody: string, signature: string, secret: string): boolean {
|
|
660
|
+
const digest = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
|
|
661
|
+
const a = Buffer.from(signature);
|
|
662
|
+
const b = Buffer.from(digest);
|
|
663
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
604
664
|
}
|
|
665
|
+
|
|
666
|
+
export const githubPush = new LuaWebhook({
|
|
667
|
+
name: 'github-push-verified',
|
|
668
|
+
description: 'GitHub push events, signature-checked',
|
|
669
|
+
execute: async (event) => {
|
|
670
|
+
const signature = event.headers?.['x-hub-signature-256'];
|
|
671
|
+
const secret = env('GITHUB_WEBHOOK_SECRET');
|
|
672
|
+
if (!event.rawBody || !signature || !secret || !verifyGitHubSignature(event.rawBody, signature, secret)) {
|
|
673
|
+
return { success: false, error: 'invalid signature' };
|
|
674
|
+
}
|
|
675
|
+
// Signature OK — event.body is the parsed form of the same bytes.
|
|
676
|
+
return { success: true, ref: event.body.ref };
|
|
677
|
+
},
|
|
678
|
+
});
|
|
605
679
|
```
|
|
606
680
|
|
|
681
|
+
`lua test webhook` has no wire bytes, so it sets `rawBody` to the compact
|
|
682
|
+
`JSON.stringify(body)` of the test input; pass an explicit one with
|
|
683
|
+
`--input '{"body": {...}, "headers": {...}, "rawBody": "<exact bytes>"}'` to
|
|
684
|
+
exercise a signature check against a captured delivery.
|
|
685
|
+
|
|
686
|
+
To have Lua reject unsigned calls before `execute` runs, use the built-in
|
|
687
|
+
`secret` instead (see [Request Verification](#request-verification)).
|
|
688
|
+
|
|
607
689
|
### API Key Validation
|
|
608
690
|
|
|
609
691
|
```typescript
|
|
610
692
|
headerSchema: z.object({
|
|
611
|
-
'x-api-key': z.string()
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
})
|
|
693
|
+
'x-api-key': z.string().refine((key) => key === process.env.WEBHOOK_API_KEY, {
|
|
694
|
+
message: 'Invalid API key',
|
|
695
|
+
}),
|
|
696
|
+
});
|
|
616
697
|
```
|
|
617
698
|
|
|
618
699
|
## Related APIs
|
package/docs/api/LuaWorkflow.md
CHANGED
|
@@ -54,21 +54,21 @@ Config: `name`, `description?`, `inputSchema`, `outputSchema?`, `budget?` (`maxC
|
|
|
54
54
|
|
|
55
55
|
**The call site places.** Every builder call — `then`, `agentStep`, `specialistStep`, `toolStep`, `map`, `approval`, `waitForSignal`, `sleep`, `sleepUntil`, `foreach`, `parallel`, `branch`, `switch`, `dowhile`, `dountil`, `workflow` — appends **exactly one** entry to the chain where it is called. Containers take `StepRef`s: an inline step object, or a **string** naming an `agentStep`/`specialistStep`/`toolStep` declared elsewhere in the same chain — a string ref inside a container means "declare here, inside me" and does not append a second top-level entry. An id declared but never placed is `WORKFLOW_UNPLACED_STEP` (warning locally, error at push); referenced but never declared is `unknown-step-ref` at `.commit()`.
|
|
56
56
|
|
|
57
|
-
| Verb
|
|
58
|
-
|
|
|
59
|
-
| `.then(step, input?)`
|
|
60
|
-
| `.agentStep(id, { agentId, prompt, outputSchema?, toolScope?, tier?, … })`
|
|
61
|
-
| `.specialistStep(id, { role, prompt, … })`
|
|
62
|
-
| `.toolStep(id, { toolId, input? })`
|
|
63
|
-
| `.map(descriptors, { id })`
|
|
64
|
-
| `.parallel([...arms], { merge? })`
|
|
65
|
-
| `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)`
|
|
66
|
-
| `.foreach(step, { items?, concurrency?, maxItems?, chunk?, rateLimit? })`
|
|
67
|
-
| `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)`
|
|
68
|
-
| `.sleep(ms)` / `.sleepUntil(template)`
|
|
69
|
-
| `.approval(id, { title, approver, timeoutHours, onTimeout, editable?, editablePaths?, itemsPath?, … })` | Human gate — see [Approvals](../workflows/approvals.md) |
|
|
70
|
-
| `.waitForSignal(id, { signal, schema?, timeoutHours, acceptedSources? })`
|
|
71
|
-
| `.workflow(id, ref, input?, { workspace? })`
|
|
57
|
+
| Verb | Entry |
|
|
58
|
+
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `.then(step, input?)` | Sequential step |
|
|
60
|
+
| `.agentStep(id, { agentId, prompt, outputSchema?, toolScope?, tier?, … })` | Agent turn (`prompt` is a `template(...)`) |
|
|
61
|
+
| `.specialistStep(id, { role, prompt, … })` | Ephemeral role on the owning agent (D25) |
|
|
62
|
+
| `.toolStep(id, { toolId, input? })` | One tool call — `toolId` must name a `LuaTool` the compiler can see as a tool primitive (declared in its own module and exported, or registered on the agent); a tool object defined inline in the workflow file is not detected and fails `WORKFLOW_TOOL_REF_UNRESOLVED` at compile |
|
|
63
|
+
| `.map(descriptors, { id })` | Data reshaping — `id` is **required** once the workflow has ≥ 2 maps (`map-id-required`) |
|
|
64
|
+
| `.parallel([...arms], { merge? })` | Concurrent arms; a `[map, step]` pair is a legal arm — the step runs with the map as its own `input` (the arm node carries `input`, like a `toolStep` with one). Not a `foreach` / loop body: those receive their item / the previous output (`mapping-placement` at `.commit()`) — map the items before the container instead |
|
|
65
|
+
| `.switch([[predicate, armRef]...], otherwise?)` / `.branch(...)` | Conditional (exclusive / inclusive) |
|
|
66
|
+
| `.foreach(step, { items?, concurrency?, maxItems?, chunk?, rateLimit? })` | Fan-out over an upstream array. `items` lowers to a `<bodyId>_items` mapping ahead of the entry and takes `fromInit(path)` / `fromStep(id, path)` or an `init(...)` / `step(...).path(...)` ref (LUA-864) — the node is byte-identical whichever form you write; any other descriptor (`value`, `template`, `fromRequest`, `rows`, `fromKnowledge`, a fan-in `fromStep([...])`) is `invalid-envelope` at build: `.map({ '': ... }, { id })` before the foreach instead |
|
|
67
|
+
| `.dowhile(ref, predicate, { maxIterations, intervalSeconds? })` / `.dountil(...)` | Loop |
|
|
68
|
+
| `.sleep(ms)` / `.sleepUntil(template)` | Engine-side waits — `sleepUntil` lowers to an `<id>_at` mapping + `sleepUntil{dateFrom}` (D6-r1) |
|
|
69
|
+
| `.approval(id, { title, approver, timeoutHours, onTimeout, remindAfterHours?, editable?, editablePaths?, itemsPath?, … })` | Human gate — see [Approvals](../workflows/approvals.md) |
|
|
70
|
+
| `.waitForSignal(id, { signal, schema?, timeoutHours, acceptedSources? })` | External event via `Workflows.signal` — completes with `{ payload, source: { kind, id, principalId? }, signalId, receivedAt }`; payload keys named like a credential (`token`, `secret`, `apiKey`, `accessToken`, …) are persisted `[REDACTED]` (the `signal-reserved-key` warning says so at push / compose) — see [Reserved payload keys](Workflows.md#reserved-payload-keys) |
|
|
71
|
+
| `.workflow(id, ref, input?, { workspace? })` | Child run the parent waits for |
|
|
72
72
|
|
|
73
73
|
### Typed predicates
|
|
74
74
|
|
package/docs/api/Orders.md
CHANGED
|
@@ -30,14 +30,16 @@ The Orders API enables:
|
|
|
30
30
|
```typescript
|
|
31
31
|
enum OrderStatus {
|
|
32
32
|
PENDING = 'pending',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
CANCELLED = 'cancelled',
|
|
37
|
-
REFUNDED = 'refunded'
|
|
33
|
+
CONFIRMED = 'confirmed',
|
|
34
|
+
FULFILLED = 'fulfilled',
|
|
35
|
+
CANCELLED = 'cancelled'
|
|
38
36
|
}
|
|
39
37
|
```
|
|
40
38
|
|
|
39
|
+
> **Note:** the platform spells the fulfilled status `fullfilled`. `OrderStatus.FULFILLED` is sent to the platform as `fullfilled` (for both `updateStatus` and `get(status)`), and `order.common.status` reads back as `fullfilled`. Any other status value is rejected with HTTP 400, and an unknown order id answers HTTP 404; both are thrown as an `Error` carrying the server message.
|
|
40
|
+
>
|
|
41
|
+
> `Orders.getById`, `updateStatus` and `updateData` take the order `id` (the 24-character hex returned by `create`/`get`), not the `orderId` display string; an id that is not 24-hex answers HTTP 404. An unexpected platform failure on `Orders.create` answers HTTP 424 and a failure on the other calls answers HTTP 500; both are thrown as an `Error` with the generic message `Order request failed`.
|
|
42
|
+
|
|
41
43
|
## OrderInstance Methods
|
|
42
44
|
|
|
43
45
|
Once you have an order instance, you can call these methods:
|
package/docs/api/Products.md
CHANGED
|
@@ -101,7 +101,7 @@ Searches products by name or description.
|
|
|
101
101
|
|
|
102
102
|
**Parameters:**
|
|
103
103
|
- `query` (string) - Search term
|
|
104
|
-
- `limit` (number, optional) - Max results (default:
|
|
104
|
+
- `limit` (number, optional) - Max results (default: 5)
|
|
105
105
|
|
|
106
106
|
**Returns:** `Promise<ProductSearchInstance>`
|
|
107
107
|
|