@siglo/notifications 1.0.3 → 1.2.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/README.md +179 -154
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +27 -9
- package/dist/approvals.js.map +1 -1
- package/dist/callbacks.d.ts.map +1 -1
- package/dist/callbacks.js +68 -13
- package/dist/callbacks.js.map +1 -1
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +10 -4
- package/dist/dispatch.js.map +1 -1
- package/dist/egress.d.ts.map +1 -1
- package/dist/egress.js +23 -8
- package/dist/egress.js.map +1 -1
- package/dist/monitor.d.ts.map +1 -1
- package/dist/monitor.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +6 -2
- package/dist/registry.js.map +1 -1
- package/dist/sms.d.ts.map +1 -1
- package/dist/sms.js +18 -5
- package/dist/sms.js.map +1 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +143 -27
- package/dist/steps.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +7 -2
- package/dist/testing/index.js.map +1 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +15 -6
- package/dist/tokens.js.map +1 -1
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +116 -14
- package/dist/transports.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -4
package/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
> Security migration: read [approval authorization, POST confirmation, durable recovery and delivery guarantees](../../docs/adapters/notification-hardening.md). GET is read-only; configure `authorize` or explicitly opt into `allowBearerApproval`.
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
Enterprise multi-channel notifications, tamper-proof cryptographic approval tokens, and durable human-in-the-loop callback resumption for Siglo workflows.
|
|
7
6
|
|
|
8
7
|
[](https://www.npmjs.com/package/@siglo/notifications)
|
|
@@ -65,6 +64,7 @@ npm install @siglo/notifications @siglo/crypto @siglo/http @siglo/core
|
|
|
65
64
|
```
|
|
66
65
|
|
|
67
66
|
Peer dependencies:
|
|
67
|
+
|
|
68
68
|
- `@siglo/core`: `^1.0.0`
|
|
69
69
|
- `@siglo/crypto`: `^1.0.0`
|
|
70
70
|
- `@siglo/http`: `^1.0.0`
|
|
@@ -81,14 +81,14 @@ import { flow, waitForEvent, Step, useStorage } from '@siglo/core';
|
|
|
81
81
|
import { PostgresStorageAdapter } from '@siglo/postgres';
|
|
82
82
|
import { StaticKeyRing } from '@siglo/crypto';
|
|
83
83
|
import {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
registerNotificationTransport,
|
|
85
|
+
SlackTransport,
|
|
86
|
+
sendInteractiveApprovalStep,
|
|
87
|
+
createNotificationExpressMiddleware,
|
|
88
|
+
configureApprovalRevocationStore,
|
|
89
|
+
PostgresApprovalRevocationStore,
|
|
90
|
+
PostgresNotificationDispatchStore,
|
|
91
|
+
configureNotificationDispatchStore,
|
|
92
92
|
} from '@siglo/notifications';
|
|
93
93
|
import { Pool } from 'pg';
|
|
94
94
|
|
|
@@ -106,62 +106,64 @@ await dispatchStore.initialize();
|
|
|
106
106
|
configureNotificationDispatchStore(dispatchStore);
|
|
107
107
|
|
|
108
108
|
// 2. Register transports globally (or pass per-step)
|
|
109
|
-
registerNotificationTransport(
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
registerNotificationTransport(
|
|
110
|
+
new SlackTransport({
|
|
111
|
+
apiKey: process.env.SLACK_BOT_TOKEN!,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
112
114
|
|
|
113
115
|
// 3. Define KeyRing for HMAC signing and key rotation
|
|
114
116
|
const approvalKeyRing = new StaticKeyRing('primary', {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
primary: process.env.APPROVAL_SECRET_CURRENT!,
|
|
118
|
+
previous: process.env.APPROVAL_SECRET_PREVIOUS!,
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
// 4. Define the approval workflow
|
|
120
122
|
export const deploymentWorkflow = flow(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
123
|
+
// Step 1: Send Slack Block Kit card with signed Approve/Reject buttons
|
|
124
|
+
sendInteractiveApprovalStep('requestApproval', {
|
|
125
|
+
channel: 'slack',
|
|
126
|
+
recipient: (ctx: { leadDevSlackId: string }) => ({ slackUserId: ctx.leadDevSlackId }),
|
|
127
|
+
title: (ctx: { service: string; version: string }) => `Deploy ${ctx.service}@${ctx.version}?`,
|
|
128
|
+
description: (ctx: { deploymentId: string }) => `Deployment ID: ${ctx.deploymentId}`,
|
|
129
|
+
correlationKey: (ctx: { deploymentId: string }) => `deploy:${ctx.deploymentId}`,
|
|
130
|
+
timeout: '2h',
|
|
131
|
+
onApproveEvent: 'APPROVAL_RESOLVED',
|
|
132
|
+
onRejectEvent: 'APPROVAL_RESOLVED',
|
|
133
|
+
callbackBaseUrl: 'https://orchestrator.acme.com/api/siglo/callback',
|
|
134
|
+
signingSecret: approvalKeyRing,
|
|
135
|
+
autoCompensate: true, // Updates Slack card to 'Cancelled' on failure or rollback
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
// Step 2: Suspends workflow into PostgreSQL durable checkpoint with a 2h timer
|
|
139
|
+
waitForEvent('APPROVAL_RESOLVED', {
|
|
140
|
+
correlationKey: (ctx: { deploymentId: string }) => `deploy:${ctx.deploymentId}`,
|
|
141
|
+
timeout: '2h',
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
// Step 3: Guard on approved decision before executing side effects
|
|
145
|
+
new Step('verifyDecision', async (ctx: { approved?: boolean; event?: { type: string } }) => {
|
|
146
|
+
if (ctx.approved !== true || ctx.event?.type === 'timer:expired') {
|
|
147
|
+
throw new Error('Deployment approval was rejected or expired');
|
|
148
|
+
}
|
|
149
|
+
return { verified: true };
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
deployServiceStep,
|
|
151
153
|
);
|
|
152
154
|
|
|
153
155
|
// 5. Expose callback endpoint in Express
|
|
154
156
|
const app = express();
|
|
155
157
|
app.use(express.urlencoded({ extended: false }));
|
|
156
158
|
app.all(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
'/api/siglo/callback',
|
|
160
|
+
createNotificationExpressMiddleware({
|
|
161
|
+
workflow: deploymentWorkflow,
|
|
162
|
+
signingSecret: approvalKeyRing,
|
|
163
|
+
authorize: authorizeApproval, // Application identity and CSRF check; see migration guide.
|
|
164
|
+
responseFormat: 'html',
|
|
165
|
+
allowedOrigins: ['https://orchestrator.acme.com', 'https://dashboard.acme.com'],
|
|
166
|
+
}),
|
|
165
167
|
);
|
|
166
168
|
app.listen(3000);
|
|
167
169
|
```
|
|
@@ -172,71 +174,91 @@ app.listen(3000);
|
|
|
172
174
|
|
|
173
175
|
`@siglo/notifications` ships with built-in adapters that adhere to Siglo's AsyncLocalStorage (ASL) network egress perimeter and require zero third-party SDK dependencies (using native HTTPS with validated socket DNS resolution):
|
|
174
176
|
|
|
175
|
-
| Transport
|
|
176
|
-
|
|
177
|
-
| `SlackTransport`
|
|
178
|
-
| `ResendTransport`
|
|
179
|
-
| `SendGridTransport`
|
|
180
|
-
| `PostmarkTransport`
|
|
181
|
-
| `SmtpTransport`
|
|
182
|
-
| `DiscordTransport`
|
|
183
|
-
| `TwilioTransport`
|
|
184
|
-
| `PushWebhookTransport` | `push`
|
|
177
|
+
| Transport | Channel | Allowed Hosts | Key Features |
|
|
178
|
+
| ---------------------- | --------- | --------------------- | --------------------------------------------------------------------------------------------- |
|
|
179
|
+
| `SlackTransport` | `slack` | `slack.com` | Block Kit action buttons, message cancellation via `chat.update`, modal forms via `openModal` |
|
|
180
|
+
| `ResendTransport` | `email` | `api.resend.com` | Responsive HTML email action templates |
|
|
181
|
+
| `SendGridTransport` | `email` | `api.sendgrid.com` | HTML email dispatch with `x-message-id` tracking |
|
|
182
|
+
| `PostmarkTransport` | `email` | `api.postmarkapp.com` | HTML + plain-text email with server token auth |
|
|
183
|
+
| `SmtpTransport` | `email` | User-defined host | Injected SMTP client (`sendMail`) without bundled dependencies |
|
|
184
|
+
| `DiscordTransport` | `discord` | `discord.com` | Webhook embeds with clickable action links |
|
|
185
|
+
| `TwilioTransport` | `sms` | `api.twilio.com` | Basic SMS dispatch and two-way signed webhook parsing |
|
|
186
|
+
| `PushWebhookTransport` | `push` | Webhook hostname | Provider-neutral JSON push payloads over HTTPS |
|
|
185
187
|
|
|
186
188
|
### Transport Configuration Examples
|
|
187
189
|
|
|
188
190
|
```typescript
|
|
189
191
|
import {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
SlackTransport,
|
|
193
|
+
ResendTransport,
|
|
194
|
+
SendGridTransport,
|
|
195
|
+
PostmarkTransport,
|
|
196
|
+
DiscordTransport,
|
|
197
|
+
TwilioTransport,
|
|
198
|
+
SmtpTransport,
|
|
199
|
+
PushWebhookTransport,
|
|
200
|
+
registerNotificationTransport,
|
|
193
201
|
} from '@siglo/notifications';
|
|
194
202
|
|
|
195
203
|
// Slack
|
|
196
|
-
registerNotificationTransport(
|
|
197
|
-
|
|
198
|
-
|
|
204
|
+
registerNotificationTransport(
|
|
205
|
+
new SlackTransport({
|
|
206
|
+
apiKey: process.env.SLACK_BOT_TOKEN!,
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
199
209
|
|
|
200
210
|
// Resend Email
|
|
201
|
-
registerNotificationTransport(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
registerNotificationTransport(
|
|
212
|
+
new ResendTransport({
|
|
213
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
214
|
+
from: 'ops@acme.com',
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
205
217
|
|
|
206
218
|
// SendGrid Email
|
|
207
|
-
registerNotificationTransport(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
registerNotificationTransport(
|
|
220
|
+
new SendGridTransport({
|
|
221
|
+
apiKey: process.env.SENDGRID_API_KEY!,
|
|
222
|
+
from: 'ops@acme.com',
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
211
225
|
|
|
212
226
|
// Postmark Email
|
|
213
|
-
registerNotificationTransport(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
registerNotificationTransport(
|
|
228
|
+
new PostmarkTransport({
|
|
229
|
+
apiKey: process.env.POSTMARK_SERVER_TOKEN!,
|
|
230
|
+
from: 'ops@acme.com',
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
217
233
|
|
|
218
234
|
// Discord
|
|
219
235
|
registerNotificationTransport(new DiscordTransport());
|
|
220
236
|
|
|
221
237
|
// Twilio SMS
|
|
222
|
-
registerNotificationTransport(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
registerNotificationTransport(
|
|
239
|
+
new TwilioTransport({
|
|
240
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID!,
|
|
241
|
+
authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
242
|
+
from: '+15551234567',
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
227
245
|
|
|
228
246
|
// Generic SMTP (e.g. Nodemailer)
|
|
229
|
-
registerNotificationTransport(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
247
|
+
registerNotificationTransport(
|
|
248
|
+
new SmtpTransport({
|
|
249
|
+
host: 'smtp.mailgun.org',
|
|
250
|
+
from: 'no-reply@acme.com',
|
|
251
|
+
sendMail: async mail => nodemailerTransport.sendMail(mail),
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
234
254
|
|
|
235
255
|
// Generic HTTPS Push Webhook
|
|
236
|
-
registerNotificationTransport(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
256
|
+
registerNotificationTransport(
|
|
257
|
+
new PushWebhookTransport({
|
|
258
|
+
url: 'https://push.acme.com/v1/notify',
|
|
259
|
+
headers: { 'X-Service-Token': process.env.PUSH_SERVICE_TOKEN! },
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
240
262
|
```
|
|
241
263
|
|
|
242
264
|
---
|
|
@@ -251,10 +273,10 @@ Dispatches one-way alerts (email, chat, SMS, push) with automatic idempotency de
|
|
|
251
273
|
import { sendNotificationStep } from '@siglo/notifications';
|
|
252
274
|
|
|
253
275
|
const alertOps = sendNotificationStep('alertOps', {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
276
|
+
channel: 'slack',
|
|
277
|
+
recipient: { slackChannelId: 'C_OPS_ALERTS' },
|
|
278
|
+
template: ctx => `High latency detected on ${ctx.serviceName}: ${ctx.p99}ms`,
|
|
279
|
+
deduplicationTtl: 3600, // Suppresses duplicate alerts within 1 hour
|
|
258
280
|
});
|
|
259
281
|
```
|
|
260
282
|
|
|
@@ -266,17 +288,17 @@ Generates signed action tokens, formats interactive cards/emails, registers comp
|
|
|
266
288
|
import { sendInteractiveApprovalStep } from '@siglo/notifications';
|
|
267
289
|
|
|
268
290
|
const approvalStep = sendInteractiveApprovalStep('legalApproval', {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
291
|
+
channel: 'email',
|
|
292
|
+
recipient: ctx => ({ email: ctx.legalReviewerEmail }),
|
|
293
|
+
title: ctx => `Contract Review Required: ${ctx.contractTitle}`,
|
|
294
|
+
description: ctx => `Value: $${ctx.contractValue}`,
|
|
295
|
+
correlationKey: ctx => `contract:${ctx.contractId}`,
|
|
296
|
+
timeout: '72h',
|
|
297
|
+
onApproveEvent: 'CONTRACT_APPROVED',
|
|
298
|
+
onRejectEvent: 'CONTRACT_REJECTED',
|
|
299
|
+
callbackBaseUrl: 'https://api.acme.com/siglo/callback',
|
|
300
|
+
signingSecret: process.env.APPROVAL_SECRET!,
|
|
301
|
+
autoCompensate: true,
|
|
280
302
|
});
|
|
281
303
|
```
|
|
282
304
|
|
|
@@ -289,6 +311,7 @@ Action tokens embed callback metadata in a compact URL-safe format:
|
|
|
289
311
|
$$\text{Token} = \text{base64url}(\text{Payload}) \,\|\, \text{base64url}(\text{HMAC-SHA256}(\text{Payload}, \text{Secret}))$$
|
|
290
312
|
|
|
291
313
|
### Features:
|
|
314
|
+
|
|
292
315
|
- **No Database Lookup on Verification**: Validates authenticity and expiration instantly before accessing storage.
|
|
293
316
|
- **Constant-Time Verification**: Uses `@siglo/crypto`'s `timingSafeCompare` to defeat timing attacks.
|
|
294
317
|
- **Replay handling**: Durable claims reject completed requests. `toleranceSeconds` checks future issuance only; nonces are not independently consumed.
|
|
@@ -299,13 +322,13 @@ import { StaticKeyRing } from '@siglo/crypto';
|
|
|
299
322
|
import { signActionToken, verifyActionToken } from '@siglo/notifications';
|
|
300
323
|
|
|
301
324
|
const keyRing = new StaticKeyRing('v2', {
|
|
302
|
-
|
|
303
|
-
|
|
325
|
+
v2: 'current-secret-key-2026',
|
|
326
|
+
v1: 'legacy-secret-key-2025',
|
|
304
327
|
});
|
|
305
328
|
|
|
306
329
|
// Verify token
|
|
307
330
|
const payload = await verifyActionToken(tokenString, keyRing, {
|
|
308
|
-
|
|
331
|
+
toleranceSeconds: 60, // Allow 60s future clock skew
|
|
309
332
|
});
|
|
310
333
|
```
|
|
311
334
|
|
|
@@ -316,6 +339,7 @@ const payload = await verifyActionToken(tokenString, keyRing, {
|
|
|
316
339
|
First-class route handlers are provided for `@siglo/express`, `@siglo/fastify`, and `@siglo/hono` with built-in security headers, CORS preflight handling, and response customization:
|
|
317
340
|
|
|
318
341
|
### Supported Security Headers
|
|
342
|
+
|
|
319
343
|
- `Cache-Control: no-store`
|
|
320
344
|
- `Referrer-Policy: no-referrer`
|
|
321
345
|
- `X-Content-Type-Options: nosniff`
|
|
@@ -331,17 +355,17 @@ const app = express();
|
|
|
331
355
|
app.use(express.urlencoded({ extended: false }));
|
|
332
356
|
|
|
333
357
|
app.all(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
358
|
+
'/api/siglo/callback',
|
|
359
|
+
createNotificationExpressMiddleware({
|
|
360
|
+
workflow: orderFlow,
|
|
361
|
+
signingSecret: process.env.APPROVAL_SECRET!,
|
|
362
|
+
authorize: authorizeApproval, // Application identity and CSRF check; see migration guide.
|
|
363
|
+
responseFormat: 'html', // Renders accessible confirmation page
|
|
364
|
+
allowedOrigins: ['https://orchestrator.acme.com', 'https://dashboard.acme.com'],
|
|
365
|
+
onSuccessRedirect: 'https://dashboard.acme.com/approvals/success',
|
|
366
|
+
onExpiredRedirect: 'https://dashboard.acme.com/approvals/expired',
|
|
367
|
+
onErrorRedirect: 'https://dashboard.acme.com/approvals/error',
|
|
368
|
+
}),
|
|
345
369
|
);
|
|
346
370
|
```
|
|
347
371
|
|
|
@@ -353,9 +377,9 @@ import { createNotificationFastifyHandler } from '@siglo/notifications';
|
|
|
353
377
|
|
|
354
378
|
const fastify = Fastify();
|
|
355
379
|
const handler = createNotificationFastifyHandler({
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
380
|
+
workflow: orderFlow,
|
|
381
|
+
signingSecret: process.env.APPROVAL_SECRET!,
|
|
382
|
+
authorize: authorizeApproval, // Application authentication and CSRF check.
|
|
359
383
|
});
|
|
360
384
|
fastify.route({ method: ['GET', 'POST'], url: '/api/siglo/callback', handler });
|
|
361
385
|
```
|
|
@@ -367,11 +391,14 @@ import { Hono } from 'hono';
|
|
|
367
391
|
import { createNotificationHonoHandler } from '@siglo/notifications';
|
|
368
392
|
|
|
369
393
|
const app = new Hono();
|
|
370
|
-
app.all(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
394
|
+
app.all(
|
|
395
|
+
'/api/siglo/callback',
|
|
396
|
+
createNotificationHonoHandler({
|
|
397
|
+
workflow: orderFlow,
|
|
398
|
+
signingSecret: process.env.APPROVAL_SECRET!,
|
|
399
|
+
authorize: authorizeApproval,
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
375
402
|
```
|
|
376
403
|
|
|
377
404
|
---
|
|
@@ -384,19 +411,19 @@ Parse and cryptographically verify inbound SMS approval webhooks (`APPROVE <ref>
|
|
|
384
411
|
import { parseTwilioSmsReply } from '@siglo/notifications';
|
|
385
412
|
|
|
386
413
|
app.post('/api/twilio/sms', express.urlencoded({ extended: false }), async (req, res) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
414
|
+
const reply = parseTwilioSmsReply(req.body, {
|
|
415
|
+
publicUrl: 'https://api.acme.com/api/twilio/sms',
|
|
416
|
+
authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
417
|
+
signature: req.headers['x-twilio-signature'] as string,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// reply: { approved: true, reference: 'deploy:123', from: '+15550001', messageId: 'SM...' }
|
|
421
|
+
// Validate a persisted, unpredictable challenge and expected sender first.
|
|
422
|
+
// Atomically consume the pending request and deduplicate MessageSid in your
|
|
423
|
+
// application approval service. See the security migration guide.
|
|
424
|
+
await approvalService.acceptVerifiedSmsReply(reply);
|
|
425
|
+
|
|
426
|
+
res.type('text/xml').send('<Response></Response>');
|
|
400
427
|
});
|
|
401
428
|
```
|
|
402
429
|
|
|
@@ -405,6 +432,7 @@ app.post('/api/twilio/sms', express.urlencoded({ extended: false }), async (req,
|
|
|
405
432
|
## 9. Saga Compensation & Shared Revocations
|
|
406
433
|
|
|
407
434
|
When a downstream step fails or a workflow times out, Siglo triggers Named Saga Compensation (`cancelNotification`):
|
|
435
|
+
|
|
408
436
|
1. **Chat Channels (Slack)**: Calls `chat.update` to replace interactive buttons with an alert (e.g. `"Workflow Cancelled"`).
|
|
409
437
|
2. **Email & SMS**: Records the `correlationKey` into the revocation store so any future clicks on existing email links return `409 Conflict`.
|
|
410
438
|
|
|
@@ -427,12 +455,9 @@ Standard recipient identifiers, credentials, and tokens are automatically redact
|
|
|
427
455
|
```typescript
|
|
428
456
|
import { NotificationMonitor } from '@siglo/notifications';
|
|
429
457
|
|
|
430
|
-
const monitor = new NotificationMonitor([
|
|
431
|
-
'outputSummary.customers.*.contact',
|
|
432
|
-
'order.shippingAddress.phone'
|
|
433
|
-
]);
|
|
458
|
+
const monitor = new NotificationMonitor(['outputSummary.customers.*.contact', 'order.shippingAddress.phone']);
|
|
434
459
|
|
|
435
|
-
workflow.
|
|
460
|
+
workflow.run(input, { monitor });
|
|
436
461
|
```
|
|
437
462
|
|
|
438
463
|
---
|
|
@@ -447,15 +472,15 @@ import { sendNotificationStep } from '@siglo/notifications';
|
|
|
447
472
|
|
|
448
473
|
const mock = new MockNotificationTransport();
|
|
449
474
|
const step = sendNotificationStep('notify', {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
475
|
+
channel: 'email',
|
|
476
|
+
recipient: 'user@example.com',
|
|
477
|
+
template: 'Hello world',
|
|
478
|
+
transport: mock,
|
|
454
479
|
});
|
|
455
480
|
|
|
456
481
|
await step.execute({});
|
|
457
482
|
console.log(mock.messages.length); // 1
|
|
458
|
-
console.log(mock.messages[0].to);
|
|
483
|
+
console.log(mock.messages[0].to); // 'user@example.com'
|
|
459
484
|
```
|
|
460
485
|
|
|
461
486
|
### Running Test Suites
|
package/dist/approvals.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"AAEA,uFAAuF;AACvF,MAAM,WAAW,uBAAuB;
|
|
1
|
+
{"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"AAEA,uFAAuF;AACvF,MAAM,WAAW,uBAAuB;IACpC,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,gBAAgB,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACrF;AACD,qBAAa,+BAAgC,YAAW,uBAAuB;IAC3E,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,KAAK,CAAgD;IACvD,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAWtE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGjD;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAErF;AACD,wBAAgB,0BAA0B,IAAI,uBAAuB,CAEpE;AACD,UAAU,UAAU;IAChB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAA;KAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CACjC;AACD,UAAU,MAAO,SAAQ,UAAU;IAC/B,OAAO,CAAC,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;CAC1C;AACD,kGAAkG;AAClG,qBAAa,+BAAgC,YAAW,uBAAuB;IAG/D,OAAO,CAAC,MAAM;IAF1B,OAAO,CAAC,KAAK,CAAiF;IAC9F,OAAO,CAAC,gBAAgB,CAAS;gBACb,MAAM,EAAE,MAAM;IAC5B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAuCtE,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7C,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAO5D"}
|
package/dist/approvals.js
CHANGED
|
@@ -23,14 +23,26 @@ class InMemoryApprovalRevocationStore {
|
|
|
23
23
|
this.locks.delete(key);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
async revoke(key) {
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
async revoke(key) {
|
|
27
|
+
await this.withApprovalLock(key, async () => {
|
|
28
|
+
this.revoked.add(key);
|
|
29
|
+
const waiting = core_1.defaultDurableManager.getWaiting(key);
|
|
30
|
+
if (waiting)
|
|
31
|
+
waiting.status = 'failed';
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async isRevoked(key) {
|
|
35
|
+
return this.revoked.has(key);
|
|
36
|
+
}
|
|
29
37
|
}
|
|
30
38
|
exports.InMemoryApprovalRevocationStore = InMemoryApprovalRevocationStore;
|
|
31
39
|
let store = new InMemoryApprovalRevocationStore();
|
|
32
|
-
function configureApprovalRevocationStore(value) {
|
|
33
|
-
|
|
40
|
+
function configureApprovalRevocationStore(value) {
|
|
41
|
+
store = value;
|
|
42
|
+
}
|
|
43
|
+
function getApprovalRevocationStore() {
|
|
44
|
+
return store;
|
|
45
|
+
}
|
|
34
46
|
/** Use a Pool in multi-worker deployments; locks hold a dedicated connection until completion. */
|
|
35
47
|
class PostgresApprovalRevocationStore {
|
|
36
48
|
client;
|
|
@@ -54,8 +66,10 @@ class PostgresApprovalRevocationStore {
|
|
|
54
66
|
let connection;
|
|
55
67
|
let locked = false;
|
|
56
68
|
try {
|
|
57
|
-
connection = pooled ? await this.client.connect() : this.client;
|
|
58
|
-
const result = await connection.query('SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS locked', [
|
|
69
|
+
connection = pooled ? (await this.client.connect()) : this.client;
|
|
70
|
+
const result = await connection.query('SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS locked', [
|
|
71
|
+
`siglo:approval:${key}`,
|
|
72
|
+
]);
|
|
59
73
|
locked = result.rows[0]?.locked === true;
|
|
60
74
|
if (!locked)
|
|
61
75
|
throw new core_1.DuplicateEventException(key);
|
|
@@ -70,7 +84,9 @@ class PostgresApprovalRevocationStore {
|
|
|
70
84
|
finally {
|
|
71
85
|
try {
|
|
72
86
|
if (locked)
|
|
73
|
-
await connection.query('SELECT pg_advisory_unlock(hashtextextended($1, 0))', [
|
|
87
|
+
await connection.query('SELECT pg_advisory_unlock(hashtextextended($1, 0))', [
|
|
88
|
+
`siglo:approval:${key}`,
|
|
89
|
+
]);
|
|
74
90
|
}
|
|
75
91
|
catch (error) {
|
|
76
92
|
if (pooled) {
|
|
@@ -94,7 +110,9 @@ class PostgresApprovalRevocationStore {
|
|
|
94
110
|
const cancel = async () => {
|
|
95
111
|
// Cancel recovery first: a crash before the revocation INSERT must fail closed.
|
|
96
112
|
await adapter?.cancelWaitRecord?.(correlationKey);
|
|
97
|
-
await this.scope
|
|
113
|
+
await this.scope
|
|
114
|
+
.getStore()
|
|
115
|
+
.client.query('INSERT INTO siglo_approval_revocations (correlation_key) VALUES ($1) ON CONFLICT (correlation_key) DO NOTHING', [correlationKey]);
|
|
98
116
|
};
|
|
99
117
|
if (record && adapter?.withWorkflowLock)
|
|
100
118
|
await adapter.withWorkflowLock(record.stackId, cancel);
|
package/dist/approvals.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approvals.js","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"approvals.js","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"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"}
|
package/dist/callbacks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callbacks.d.ts","sourceRoot":"","sources":["../src/callbacks.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"callbacks.d.ts","sourceRoot":"","sources":["../src/callbacks.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAGjD,MAAM,WAAW,4BAA4B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AACD,wBAAsB,0BAA0B,CAC5C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,sBAAsB,GAChC,OAAO,CAAC,4BAA4B,CAAC,CAoEvC;AACD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,4BAA4B,GAAG,MAAM,CAEvF;AAqDD,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,sBAAsB,SAC5D,GAAG,OAAO,GAAG,KAAG,OAAO,CAAC,IAAI,CAAC,CAmBnD;AACD,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,sBAAsB,SACzD,GAAG,SAAS,GAAG,KAAG,OAAO,CAAC,IAAI,CAAC,CAoBrD;AACD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,sBAAsB,OACxD,GAAG,KAAG,OAAO,CAAC,QAAQ,CAAC,CAkB3C"}
|