@siglo/notifications 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +478 -2
- package/dist/approvals.d.ts +38 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +111 -0
- package/dist/approvals.js.map +1 -0
- package/dist/callbacks.d.ts +14 -0
- package/dist/callbacks.d.ts.map +1 -0
- package/dist/callbacks.js +144 -0
- package/dist/callbacks.js.map +1 -0
- package/dist/dispatch.d.ts +26 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +42 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/egress.d.ts +8 -0
- package/dist/egress.d.ts.map +1 -0
- package/dist/egress.js +96 -0
- package/dist/egress.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/monitor.d.ts +10 -0
- package/dist/monitor.d.ts.map +1 -0
- package/dist/monitor.js +20 -0
- package/dist/monitor.js.map +1 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +15 -0
- package/dist/registry.js.map +1 -0
- package/dist/sms.d.ts +28 -0
- package/dist/sms.d.ts.map +1 -0
- package/dist/sms.js +43 -0
- package/dist/sms.js.map +1 -0
- package/dist/steps.d.ts +7 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +131 -0
- package/dist/steps.js.map +1 -0
- package/dist/testing/index.d.ts +18 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing.d.ts +18 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +19 -0
- package/dist/testing.js.map +1 -0
- package/dist/tokens.d.ts +24 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +48 -0
- package/dist/tokens.js.map +1 -0
- package/dist/transports.d.ts +120 -0
- package/dist/transports.d.ts.map +1 -0
- package/dist/transports.js +172 -0
- package/dist/transports.js.map +1 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Jerico de guzman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,479 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @siglo/notifications
|
|
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
|
+
|
|
5
|
+
|
|
6
|
+
Enterprise multi-channel notifications, tamper-proof cryptographic approval tokens, and durable human-in-the-loop callback resumption for Siglo workflows.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@siglo/notifications)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://github.com/jsoftgem/siglo)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Overview & Architecture
|
|
15
|
+
|
|
16
|
+
In enterprise backend workflows, human intervention (e.g. invoice approvals, production deployment gates, manual fraud reviews) and notifications across Slack, Email, Discord, and SMS are critical. `@siglo/notifications` integrates these patterns directly into Siglo's 5-phase step lifecycle:
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart TD
|
|
20
|
+
subgraph Execution["1. Workflow Dispatch & Suspension"]
|
|
21
|
+
Step["sendInteractiveApprovalStep('managerApproval', ...)"]
|
|
22
|
+
EgressCheck["Assert Transport Host in ASL Network Policy"]
|
|
23
|
+
IdempCheck["Claim Idempotency Key (stackId:stepName:recipientHash)"]
|
|
24
|
+
TokenGen["Generate HMAC-SHA256 Signed Action URLs (KeyRing)"]
|
|
25
|
+
Dispatch["Dispatch via Provider (Slack Block Kit / HTML Email)"]
|
|
26
|
+
Wait["waitForEvent('APPROVAL_RESOLVED', { correlationKey, timeout })"]
|
|
27
|
+
Suspend["Checkpoint to PostgreSQL / Redis (Zero Memory Footprint)"]
|
|
28
|
+
|
|
29
|
+
Step --> EgressCheck --> IdempCheck --> TokenGen --> Dispatch --> Wait --> Suspend
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subgraph Resumption["2. Webhook Callback & Distributed Resumption"]
|
|
33
|
+
User(["Human Approver clicks 'Approve' or 'Reject'"])
|
|
34
|
+
Router["Callback Router (@siglo/express, fastify, hono)"]
|
|
35
|
+
Verify["verifyActionToken() (timingSafeCompare, tolerance window)"]
|
|
36
|
+
RevokeCheck{"isRevoked(correlationKey)?"}
|
|
37
|
+
ClaimWait["Atomic Storage Claim: claimWaitRecord(correlationKey)"]
|
|
38
|
+
ResumeFlow["resumeWorkflow(flow, correlationKey, { approved: true })"]
|
|
39
|
+
Duplicate["Reject with 409 Conflict (WorkflowAlreadyResumedException)"]
|
|
40
|
+
|
|
41
|
+
User --> Router --> Verify --> RevokeCheck
|
|
42
|
+
RevokeCheck -- Yes (Cancelled) --> Duplicate
|
|
43
|
+
RevokeCheck -- No --> ClaimWait
|
|
44
|
+
ClaimWait -- Claim Won (200 OK) --> ResumeFlow
|
|
45
|
+
ClaimWait -- Already Claimed (409) --> Duplicate
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
subgraph Scheduler["3. Timeout Awakening (Dormant Resumption)"]
|
|
49
|
+
ExpiredWait["Approval Timeout Elapsed ('2h', '48h')"]
|
|
50
|
+
Poller["@siglo/scheduler (PostgresWaitPoller / session locks)"]
|
|
51
|
+
TimeoutResume["resumeWorkflow(..., { event: 'timer:expired', allowExpired: true })"]
|
|
52
|
+
|
|
53
|
+
ExpiredWait --> Poller --> TimeoutResume
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2. Installation & Requirements
|
|
60
|
+
|
|
61
|
+
Install `@siglo/notifications` alongside `@siglo/core`, `@siglo/crypto`, and `@siglo/http`:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install @siglo/notifications @siglo/crypto @siglo/http @siglo/core
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Peer dependencies:
|
|
68
|
+
- `@siglo/core`: `^1.0.0`
|
|
69
|
+
- `@siglo/crypto`: `^1.0.0`
|
|
70
|
+
- `@siglo/http`: `^1.0.0`
|
|
71
|
+
- `@siglo/scheduler` (optional, for background timeout polling): `^1.0.0`
|
|
72
|
+
- `@siglo/postgres` (optional, for persistent checkpoints and shared revocation): `^1.0.0`
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. Quick Start: Interactive Human Approval Flow
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import express from 'express';
|
|
80
|
+
import { flow, waitForEvent, Step, useStorage } from '@siglo/core';
|
|
81
|
+
import { PostgresStorageAdapter } from '@siglo/postgres';
|
|
82
|
+
import { StaticKeyRing } from '@siglo/crypto';
|
|
83
|
+
import {
|
|
84
|
+
registerNotificationTransport,
|
|
85
|
+
SlackTransport,
|
|
86
|
+
sendInteractiveApprovalStep,
|
|
87
|
+
createNotificationExpressMiddleware,
|
|
88
|
+
configureApprovalRevocationStore,
|
|
89
|
+
PostgresApprovalRevocationStore,
|
|
90
|
+
PostgresNotificationDispatchStore,
|
|
91
|
+
configureNotificationDispatchStore
|
|
92
|
+
} from '@siglo/notifications';
|
|
93
|
+
import { Pool } from 'pg';
|
|
94
|
+
|
|
95
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
96
|
+
const storage = new PostgresStorageAdapter({ client: pool });
|
|
97
|
+
await storage.ensureTables();
|
|
98
|
+
useStorage(storage);
|
|
99
|
+
|
|
100
|
+
// 1. Shared revocation store across workers
|
|
101
|
+
const revocationStore = new PostgresApprovalRevocationStore(pool);
|
|
102
|
+
await revocationStore.initialize();
|
|
103
|
+
configureApprovalRevocationStore(revocationStore);
|
|
104
|
+
const dispatchStore = new PostgresNotificationDispatchStore(pool);
|
|
105
|
+
await dispatchStore.initialize();
|
|
106
|
+
configureNotificationDispatchStore(dispatchStore);
|
|
107
|
+
|
|
108
|
+
// 2. Register transports globally (or pass per-step)
|
|
109
|
+
registerNotificationTransport(new SlackTransport({
|
|
110
|
+
apiKey: process.env.SLACK_BOT_TOKEN!
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
// 3. Define KeyRing for HMAC signing and key rotation
|
|
114
|
+
const approvalKeyRing = new StaticKeyRing('primary', {
|
|
115
|
+
primary: process.env.APPROVAL_SECRET_CURRENT!,
|
|
116
|
+
previous: process.env.APPROVAL_SECRET_PREVIOUS!
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 4. Define the approval workflow
|
|
120
|
+
export const deploymentWorkflow = flow(
|
|
121
|
+
// Step 1: Send Slack Block Kit card with signed Approve/Reject buttons
|
|
122
|
+
sendInteractiveApprovalStep('requestApproval', {
|
|
123
|
+
channel: 'slack',
|
|
124
|
+
recipient: (ctx: { leadDevSlackId: string }) => ({ slackUserId: ctx.leadDevSlackId }),
|
|
125
|
+
title: (ctx: { service: string; version: string }) => `Deploy ${ctx.service}@${ctx.version}?`,
|
|
126
|
+
description: (ctx: { deploymentId: string }) => `Deployment ID: ${ctx.deploymentId}`,
|
|
127
|
+
correlationKey: (ctx: { deploymentId: string }) => `deploy:${ctx.deploymentId}`,
|
|
128
|
+
timeout: '2h',
|
|
129
|
+
onApproveEvent: 'APPROVAL_RESOLVED',
|
|
130
|
+
onRejectEvent: 'APPROVAL_RESOLVED',
|
|
131
|
+
callbackBaseUrl: 'https://orchestrator.acme.com/api/siglo/callback',
|
|
132
|
+
signingSecret: approvalKeyRing,
|
|
133
|
+
autoCompensate: true // Updates Slack card to 'Cancelled' on failure or rollback
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
// Step 2: Suspends workflow into PostgreSQL durable checkpoint with a 2h timer
|
|
137
|
+
waitForEvent('APPROVAL_RESOLVED', {
|
|
138
|
+
correlationKey: (ctx: { deploymentId: string }) => `deploy:${ctx.deploymentId}`,
|
|
139
|
+
timeout: '2h'
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
// Step 3: Guard on approved decision before executing side effects
|
|
143
|
+
new Step('verifyDecision', async (ctx: { approved?: boolean; event?: { type: string } }) => {
|
|
144
|
+
if (ctx.approved !== true || ctx.event?.type === 'timer:expired') {
|
|
145
|
+
throw new Error('Deployment approval was rejected or expired');
|
|
146
|
+
}
|
|
147
|
+
return { verified: true };
|
|
148
|
+
}),
|
|
149
|
+
|
|
150
|
+
deployServiceStep
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// 5. Expose callback endpoint in Express
|
|
154
|
+
const app = express();
|
|
155
|
+
app.use(express.urlencoded({ extended: false }));
|
|
156
|
+
app.all(
|
|
157
|
+
'/api/siglo/callback',
|
|
158
|
+
createNotificationExpressMiddleware({
|
|
159
|
+
workflow: deploymentWorkflow,
|
|
160
|
+
signingSecret: approvalKeyRing,
|
|
161
|
+
authorize: authorizeApproval, // Application identity and CSRF check; see migration guide.
|
|
162
|
+
responseFormat: 'html',
|
|
163
|
+
allowedOrigins: ['https://orchestrator.acme.com', 'https://dashboard.acme.com']
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
app.listen(3000);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 4. Multi-Channel Transports
|
|
172
|
+
|
|
173
|
+
`@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
|
+
|
|
175
|
+
| Transport | Channel | Allowed Hosts | Key Features |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `SlackTransport` | `slack` | `slack.com` | Block Kit action buttons, message cancellation via `chat.update`, modal forms via `openModal` |
|
|
178
|
+
| `ResendTransport` | `email` | `api.resend.com` | Responsive HTML email action templates |
|
|
179
|
+
| `SendGridTransport` | `email` | `api.sendgrid.com` | HTML email dispatch with `x-message-id` tracking |
|
|
180
|
+
| `PostmarkTransport` | `email` | `api.postmarkapp.com` | HTML + plain-text email with server token auth |
|
|
181
|
+
| `SmtpTransport` | `email` | User-defined host | Injected SMTP client (`sendMail`) without bundled dependencies |
|
|
182
|
+
| `DiscordTransport` | `discord` | `discord.com` | Webhook embeds with clickable action links |
|
|
183
|
+
| `TwilioTransport` | `sms` | `api.twilio.com` | Basic SMS dispatch and two-way signed webhook parsing |
|
|
184
|
+
| `PushWebhookTransport` | `push` | Webhook hostname | Provider-neutral JSON push payloads over HTTPS |
|
|
185
|
+
|
|
186
|
+
### Transport Configuration Examples
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import {
|
|
190
|
+
SlackTransport, ResendTransport, SendGridTransport, PostmarkTransport,
|
|
191
|
+
DiscordTransport, TwilioTransport, SmtpTransport, PushWebhookTransport,
|
|
192
|
+
registerNotificationTransport
|
|
193
|
+
} from '@siglo/notifications';
|
|
194
|
+
|
|
195
|
+
// Slack
|
|
196
|
+
registerNotificationTransport(new SlackTransport({
|
|
197
|
+
apiKey: process.env.SLACK_BOT_TOKEN!
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
// Resend Email
|
|
201
|
+
registerNotificationTransport(new ResendTransport({
|
|
202
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
203
|
+
from: 'ops@acme.com'
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
// SendGrid Email
|
|
207
|
+
registerNotificationTransport(new SendGridTransport({
|
|
208
|
+
apiKey: process.env.SENDGRID_API_KEY!,
|
|
209
|
+
from: 'ops@acme.com'
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
// Postmark Email
|
|
213
|
+
registerNotificationTransport(new PostmarkTransport({
|
|
214
|
+
apiKey: process.env.POSTMARK_SERVER_TOKEN!,
|
|
215
|
+
from: 'ops@acme.com'
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
// Discord
|
|
219
|
+
registerNotificationTransport(new DiscordTransport());
|
|
220
|
+
|
|
221
|
+
// Twilio SMS
|
|
222
|
+
registerNotificationTransport(new TwilioTransport({
|
|
223
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID!,
|
|
224
|
+
authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
225
|
+
from: '+15551234567'
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
// Generic SMTP (e.g. Nodemailer)
|
|
229
|
+
registerNotificationTransport(new SmtpTransport({
|
|
230
|
+
host: 'smtp.mailgun.org',
|
|
231
|
+
from: 'no-reply@acme.com',
|
|
232
|
+
sendMail: async (mail) => nodemailerTransport.sendMail(mail)
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
// Generic HTTPS Push Webhook
|
|
236
|
+
registerNotificationTransport(new PushWebhookTransport({
|
|
237
|
+
url: 'https://push.acme.com/v1/notify',
|
|
238
|
+
headers: { 'X-Service-Token': process.env.PUSH_SERVICE_TOKEN! }
|
|
239
|
+
}));
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 5. Workflow Step API
|
|
245
|
+
|
|
246
|
+
### `sendNotificationStep(name, options)`
|
|
247
|
+
|
|
248
|
+
Dispatches one-way alerts (email, chat, SMS, push) with automatic idempotency deduplication and context redaction:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { sendNotificationStep } from '@siglo/notifications';
|
|
252
|
+
|
|
253
|
+
const alertOps = sendNotificationStep('alertOps', {
|
|
254
|
+
channel: 'slack',
|
|
255
|
+
recipient: { slackChannelId: 'C_OPS_ALERTS' },
|
|
256
|
+
template: (ctx) => `High latency detected on ${ctx.serviceName}: ${ctx.p99}ms`,
|
|
257
|
+
deduplicationTtl: 3600 // Suppresses duplicate alerts within 1 hour
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `sendInteractiveApprovalStep(name, options)`
|
|
262
|
+
|
|
263
|
+
Generates signed action tokens, formats interactive cards/emails, registers compensation rollbacks, and attaches approval metadata:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { sendInteractiveApprovalStep } from '@siglo/notifications';
|
|
267
|
+
|
|
268
|
+
const approvalStep = sendInteractiveApprovalStep('legalApproval', {
|
|
269
|
+
channel: 'email',
|
|
270
|
+
recipient: (ctx) => ({ email: ctx.legalReviewerEmail }),
|
|
271
|
+
title: (ctx) => `Contract Review Required: ${ctx.contractTitle}`,
|
|
272
|
+
description: (ctx) => `Value: $${ctx.contractValue}`,
|
|
273
|
+
correlationKey: (ctx) => `contract:${ctx.contractId}`,
|
|
274
|
+
timeout: '72h',
|
|
275
|
+
onApproveEvent: 'CONTRACT_APPROVED',
|
|
276
|
+
onRejectEvent: 'CONTRACT_REJECTED',
|
|
277
|
+
callbackBaseUrl: 'https://api.acme.com/siglo/callback',
|
|
278
|
+
signingSecret: process.env.APPROVAL_SECRET!,
|
|
279
|
+
autoCompensate: true
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 6. Cryptographic Action Tokens & Secret Rotation
|
|
286
|
+
|
|
287
|
+
Action tokens embed callback metadata in a compact URL-safe format:
|
|
288
|
+
|
|
289
|
+
$$\text{Token} = \text{base64url}(\text{Payload}) \,\|\, \text{base64url}(\text{HMAC-SHA256}(\text{Payload}, \text{Secret}))$$
|
|
290
|
+
|
|
291
|
+
### Features:
|
|
292
|
+
- **No Database Lookup on Verification**: Validates authenticity and expiration instantly before accessing storage.
|
|
293
|
+
- **Constant-Time Verification**: Uses `@siglo/crypto`'s `timingSafeCompare` to defeat timing attacks.
|
|
294
|
+
- **Replay handling**: Durable claims reject completed requests. `toleranceSeconds` checks future issuance only; nonces are not independently consumed.
|
|
295
|
+
- **Key Rotation**: Compatible with `@siglo/crypto`'s `StaticKeyRing` to verify tokens signed with previous keys during rotation windows.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { StaticKeyRing } from '@siglo/crypto';
|
|
299
|
+
import { signActionToken, verifyActionToken } from '@siglo/notifications';
|
|
300
|
+
|
|
301
|
+
const keyRing = new StaticKeyRing('v2', {
|
|
302
|
+
v2: 'current-secret-key-2026',
|
|
303
|
+
v1: 'legacy-secret-key-2025'
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Verify token
|
|
307
|
+
const payload = await verifyActionToken(tokenString, keyRing, {
|
|
308
|
+
toleranceSeconds: 60 // Allow 60s future clock skew
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 7. Web Framework Resumption Handlers
|
|
315
|
+
|
|
316
|
+
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
|
+
|
|
318
|
+
### Supported Security Headers
|
|
319
|
+
- `Cache-Control: no-store`
|
|
320
|
+
- `Referrer-Policy: no-referrer`
|
|
321
|
+
- `X-Content-Type-Options: nosniff`
|
|
322
|
+
- `Content-Security-Policy: default-src 'none'; frame-ancestors 'none'`
|
|
323
|
+
|
|
324
|
+
### Express Example
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import express from 'express';
|
|
328
|
+
import { createNotificationExpressMiddleware } from '@siglo/notifications';
|
|
329
|
+
|
|
330
|
+
const app = express();
|
|
331
|
+
app.use(express.urlencoded({ extended: false }));
|
|
332
|
+
|
|
333
|
+
app.all(
|
|
334
|
+
'/api/siglo/callback',
|
|
335
|
+
createNotificationExpressMiddleware({
|
|
336
|
+
workflow: orderFlow,
|
|
337
|
+
signingSecret: process.env.APPROVAL_SECRET!,
|
|
338
|
+
authorize: authorizeApproval, // Application identity and CSRF check; see migration guide.
|
|
339
|
+
responseFormat: 'html', // Renders accessible confirmation page
|
|
340
|
+
allowedOrigins: ['https://orchestrator.acme.com', 'https://dashboard.acme.com'],
|
|
341
|
+
onSuccessRedirect: 'https://dashboard.acme.com/approvals/success',
|
|
342
|
+
onExpiredRedirect: 'https://dashboard.acme.com/approvals/expired',
|
|
343
|
+
onErrorRedirect: 'https://dashboard.acme.com/approvals/error'
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Fastify Example
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import Fastify from 'fastify';
|
|
352
|
+
import { createNotificationFastifyHandler } from '@siglo/notifications';
|
|
353
|
+
|
|
354
|
+
const fastify = Fastify();
|
|
355
|
+
const handler = createNotificationFastifyHandler({
|
|
356
|
+
workflow: orderFlow,
|
|
357
|
+
signingSecret: process.env.APPROVAL_SECRET!,
|
|
358
|
+
authorize: authorizeApproval // Application authentication and CSRF check.
|
|
359
|
+
});
|
|
360
|
+
fastify.route({ method: ['GET', 'POST'], url: '/api/siglo/callback', handler });
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Hono Example (Node)
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { Hono } from 'hono';
|
|
367
|
+
import { createNotificationHonoHandler } from '@siglo/notifications';
|
|
368
|
+
|
|
369
|
+
const app = new Hono();
|
|
370
|
+
app.all('/api/siglo/callback', createNotificationHonoHandler({
|
|
371
|
+
workflow: orderFlow,
|
|
372
|
+
signingSecret: process.env.APPROVAL_SECRET!,
|
|
373
|
+
authorize: authorizeApproval
|
|
374
|
+
}));
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## 8. Inbound Twilio SMS Two-Way Approvals
|
|
380
|
+
|
|
381
|
+
Parse and cryptographically verify inbound SMS approval webhooks (`APPROVE <ref>` or `REJECT <ref>`) adhering to Twilio's signed form specification:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { parseTwilioSmsReply } from '@siglo/notifications';
|
|
385
|
+
|
|
386
|
+
app.post('/api/twilio/sms', express.urlencoded({ extended: false }), async (req, res) => {
|
|
387
|
+
const reply = parseTwilioSmsReply(req.body, {
|
|
388
|
+
publicUrl: 'https://api.acme.com/api/twilio/sms',
|
|
389
|
+
authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
390
|
+
signature: req.headers['x-twilio-signature'] as string
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// reply: { approved: true, reference: 'deploy:123', from: '+15550001', messageId: 'SM...' }
|
|
394
|
+
// Validate a persisted, unpredictable challenge and expected sender first.
|
|
395
|
+
// Atomically consume the pending request and deduplicate MessageSid in your
|
|
396
|
+
// application approval service. See the security migration guide.
|
|
397
|
+
await approvalService.acceptVerifiedSmsReply(reply);
|
|
398
|
+
|
|
399
|
+
res.type('text/xml').send('<Response></Response>');
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## 9. Saga Compensation & Shared Revocations
|
|
406
|
+
|
|
407
|
+
When a downstream step fails or a workflow times out, Siglo triggers Named Saga Compensation (`cancelNotification`):
|
|
408
|
+
1. **Chat Channels (Slack)**: Calls `chat.update` to replace interactive buttons with an alert (e.g. `"Workflow Cancelled"`).
|
|
409
|
+
2. **Email & SMS**: Records the `correlationKey` into the revocation store so any future clicks on existing email links return `409 Conflict`.
|
|
410
|
+
|
|
411
|
+
### Configuring Shared PostgreSQL Revocation Store
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { configureApprovalRevocationStore, PostgresApprovalRevocationStore } from '@siglo/notifications';
|
|
415
|
+
|
|
416
|
+
const revocationStore = new PostgresApprovalRevocationStore(pgPool);
|
|
417
|
+
await revocationStore.initialize();
|
|
418
|
+
configureApprovalRevocationStore(revocationStore);
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 10. Observability & PII Redaction
|
|
424
|
+
|
|
425
|
+
Standard recipient identifiers, credentials, and tokens are automatically redacted by Siglo core before telemetry dispatch. For fine-grained domain PII masking, attach a `NotificationMonitor`:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { NotificationMonitor } from '@siglo/notifications';
|
|
429
|
+
|
|
430
|
+
const monitor = new NotificationMonitor([
|
|
431
|
+
'outputSummary.customers.*.contact',
|
|
432
|
+
'order.shippingAddress.phone'
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
workflow.execute(input, { monitor });
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## 11. Testing & Verification
|
|
441
|
+
|
|
442
|
+
`@siglo/notifications` ships with a zero-network mock transport in `@siglo/notifications/testing`:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import { MockNotificationTransport } from '@siglo/notifications/testing';
|
|
446
|
+
import { sendNotificationStep } from '@siglo/notifications';
|
|
447
|
+
|
|
448
|
+
const mock = new MockNotificationTransport();
|
|
449
|
+
const step = sendNotificationStep('notify', {
|
|
450
|
+
channel: 'email',
|
|
451
|
+
recipient: 'user@example.com',
|
|
452
|
+
template: 'Hello world',
|
|
453
|
+
transport: mock
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
await step.execute({});
|
|
457
|
+
console.log(mock.messages.length); // 1
|
|
458
|
+
console.log(mock.messages[0].to); // 'user@example.com'
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Running Test Suites
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
# Run unit suite (zero network egress)
|
|
465
|
+
npm test --workspace=@siglo/notifications
|
|
466
|
+
|
|
467
|
+
# Run 100% coverage verification
|
|
468
|
+
npm run test:coverage --workspace=@siglo/notifications
|
|
469
|
+
|
|
470
|
+
# Run multi-worker PostgreSQL integration suite
|
|
471
|
+
SIGLO_TEST_POSTGRES_URL=postgres://siglo:siglo_test@localhost:5432/siglo_notifications_test \
|
|
472
|
+
npm run test:postgres --workspace=@siglo/notifications
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 12. License
|
|
478
|
+
|
|
479
|
+
MIT © [Jerico de Guzman](https://github.com/jsoftgem/siglo)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Acceptance and revocation must use the same lock implementation on every worker. */
|
|
2
|
+
export interface ApprovalRevocationStore {
|
|
3
|
+
revoke(correlationKey: string): Promise<void>;
|
|
4
|
+
isRevoked(correlationKey: string): Promise<boolean>;
|
|
5
|
+
withApprovalLock<T>(correlationKey: string, action: () => Promise<T>): Promise<T>;
|
|
6
|
+
}
|
|
7
|
+
export declare class InMemoryApprovalRevocationStore implements ApprovalRevocationStore {
|
|
8
|
+
private revoked;
|
|
9
|
+
private locks;
|
|
10
|
+
private scope;
|
|
11
|
+
withApprovalLock<T>(key: string, action: () => Promise<T>): Promise<T>;
|
|
12
|
+
revoke(key: string): Promise<void>;
|
|
13
|
+
isRevoked(key: string): Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
export declare function configureApprovalRevocationStore(value: ApprovalRevocationStore): void;
|
|
16
|
+
export declare function getApprovalRevocationStore(): ApprovalRevocationStore;
|
|
17
|
+
interface Connection {
|
|
18
|
+
query(sql: string, values?: any[]): Promise<{
|
|
19
|
+
rows: any[];
|
|
20
|
+
}>;
|
|
21
|
+
release?(error?: Error): void;
|
|
22
|
+
}
|
|
23
|
+
interface Client extends Connection {
|
|
24
|
+
connect?(): Promise<Connection | void>;
|
|
25
|
+
}
|
|
26
|
+
/** Use a Pool in multi-worker deployments; locks hold a dedicated connection until completion. */
|
|
27
|
+
export declare class PostgresApprovalRevocationStore implements ApprovalRevocationStore {
|
|
28
|
+
private client;
|
|
29
|
+
private scope;
|
|
30
|
+
private directClientBusy;
|
|
31
|
+
constructor(client: Client);
|
|
32
|
+
initialize(): Promise<void>;
|
|
33
|
+
withApprovalLock<T>(key: string, action: () => Promise<T>): Promise<T>;
|
|
34
|
+
revoke(correlationKey: string): Promise<void>;
|
|
35
|
+
isRevoked(correlationKey: string): Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=approvals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"AAEA,uFAAuF;AACvF,MAAM,WAAW,uBAAuB;IACtC,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;CACnF;AACD,qBAAa,+BAAgC,YAAW,uBAAuB;IAC7E,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;IAQtE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAClC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAC/C;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAmB;AACzG,wBAAgB,0BAA0B,IAAI,uBAAuB,CAAkB;AACvF,UAAU,UAAU;IAClB,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;CAC/B;AACD,UAAU,MAAO,SAAQ,UAAU;IAAG,OAAO,CAAC,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;CAAE;AAC9E,kGAAkG;AAClG,qBAAa,+BAAgC,YAAW,uBAAuB;IAGjE,OAAO,CAAC,MAAM;IAF1B,OAAO,CAAC,KAAK,CAAiF;IAC9F,OAAO,CAAC,gBAAgB,CAAS;gBACb,MAAM,EAAE,MAAM;IAC5B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAG3B,gBAAgB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAsBtE,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa7C,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAI1D"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresApprovalRevocationStore = exports.InMemoryApprovalRevocationStore = void 0;
|
|
4
|
+
exports.configureApprovalRevocationStore = configureApprovalRevocationStore;
|
|
5
|
+
exports.getApprovalRevocationStore = getApprovalRevocationStore;
|
|
6
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
7
|
+
const core_1 = require("@siglo/core");
|
|
8
|
+
class InMemoryApprovalRevocationStore {
|
|
9
|
+
revoked = new Set();
|
|
10
|
+
locks = new Map();
|
|
11
|
+
scope = new node_async_hooks_1.AsyncLocalStorage();
|
|
12
|
+
async withApprovalLock(key, action) {
|
|
13
|
+
if (this.locks.has(key) && this.scope.getStore()?.get(key) === this.locks.get(key))
|
|
14
|
+
return action();
|
|
15
|
+
if (this.locks.has(key))
|
|
16
|
+
throw new core_1.DuplicateEventException(key);
|
|
17
|
+
const owner = Symbol(key);
|
|
18
|
+
this.locks.set(key, owner);
|
|
19
|
+
try {
|
|
20
|
+
return await this.scope.run(new Map([...(this.scope.getStore() ?? []), [key, owner]]), action);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
this.locks.delete(key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async revoke(key) { await this.withApprovalLock(key, async () => { this.revoked.add(key); const waiting = core_1.defaultDurableManager.getWaiting(key); if (waiting)
|
|
27
|
+
waiting.status = 'failed'; }); }
|
|
28
|
+
async isRevoked(key) { return this.revoked.has(key); }
|
|
29
|
+
}
|
|
30
|
+
exports.InMemoryApprovalRevocationStore = InMemoryApprovalRevocationStore;
|
|
31
|
+
let store = new InMemoryApprovalRevocationStore();
|
|
32
|
+
function configureApprovalRevocationStore(value) { store = value; }
|
|
33
|
+
function getApprovalRevocationStore() { return store; }
|
|
34
|
+
/** Use a Pool in multi-worker deployments; locks hold a dedicated connection until completion. */
|
|
35
|
+
class PostgresApprovalRevocationStore {
|
|
36
|
+
client;
|
|
37
|
+
scope = new node_async_hooks_1.AsyncLocalStorage();
|
|
38
|
+
directClientBusy = false;
|
|
39
|
+
constructor(client) {
|
|
40
|
+
this.client = client;
|
|
41
|
+
}
|
|
42
|
+
async initialize() {
|
|
43
|
+
await this.client.query('CREATE TABLE IF NOT EXISTS siglo_approval_revocations (correlation_key TEXT PRIMARY KEY, revoked_at TIMESTAMPTZ NOT NULL DEFAULT NOW())');
|
|
44
|
+
}
|
|
45
|
+
async withApprovalLock(key, action) {
|
|
46
|
+
if (this.scope.getStore()?.active && this.scope.getStore()?.key === key)
|
|
47
|
+
return action();
|
|
48
|
+
// pg.Client.connect() is not a pool checkout. Pools expose totalCount.
|
|
49
|
+
const pooled = 'totalCount' in this.client;
|
|
50
|
+
if (!pooled && this.directClientBusy)
|
|
51
|
+
throw new core_1.DuplicateEventException(key);
|
|
52
|
+
if (!pooled)
|
|
53
|
+
this.directClientBusy = true;
|
|
54
|
+
let connection;
|
|
55
|
+
let locked = false;
|
|
56
|
+
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', [`siglo:approval:${key}`]);
|
|
59
|
+
locked = result.rows[0]?.locked === true;
|
|
60
|
+
if (!locked)
|
|
61
|
+
throw new core_1.DuplicateEventException(key);
|
|
62
|
+
const scope = { key, client: connection, active: true };
|
|
63
|
+
try {
|
|
64
|
+
return await this.scope.run(scope, action);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
scope.active = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
try {
|
|
72
|
+
if (locked)
|
|
73
|
+
await connection.query('SELECT pg_advisory_unlock(hashtextextended($1, 0))', [`siglo:approval:${key}`]);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (pooled) {
|
|
77
|
+
connection?.release?.(error);
|
|
78
|
+
connection = undefined;
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (pooled)
|
|
84
|
+
connection?.release?.();
|
|
85
|
+
else
|
|
86
|
+
this.directClientBusy = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async revoke(correlationKey) {
|
|
91
|
+
await this.withApprovalLock(correlationKey, async () => {
|
|
92
|
+
const adapter = (0, core_1.getStorageAdapter)();
|
|
93
|
+
const record = await adapter?.getWaitRecord?.(correlationKey);
|
|
94
|
+
const cancel = async () => {
|
|
95
|
+
// Cancel recovery first: a crash before the revocation INSERT must fail closed.
|
|
96
|
+
await adapter?.cancelWaitRecord?.(correlationKey);
|
|
97
|
+
await this.scope.getStore().client.query('INSERT INTO siglo_approval_revocations (correlation_key) VALUES ($1) ON CONFLICT (correlation_key) DO NOTHING', [correlationKey]);
|
|
98
|
+
};
|
|
99
|
+
if (record && adapter?.withWorkflowLock)
|
|
100
|
+
await adapter.withWorkflowLock(record.stackId, cancel);
|
|
101
|
+
else
|
|
102
|
+
await cancel();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async isRevoked(correlationKey) {
|
|
106
|
+
const result = await (this.scope.getStore()?.active ? this.scope.getStore().client : this.client).query('SELECT correlation_key FROM siglo_approval_revocations WHERE correlation_key = $1', [correlationKey]);
|
|
107
|
+
return result.rows.length > 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.PostgresApprovalRevocationStore = PostgresApprovalRevocationStore;
|
|
111
|
+
//# sourceMappingURL=approvals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.js","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CallbackHandlerOptions } from './types';
|
|
2
|
+
export interface NotificationCallbackResponse {
|
|
3
|
+
status: number;
|
|
4
|
+
body: {
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
redirect?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function handleNotificationCallback(token: string, options: CallbackHandlerOptions): Promise<NotificationCallbackResponse>;
|
|
10
|
+
export declare function renderApprovalConfirmation(result: NotificationCallbackResponse): string;
|
|
11
|
+
export declare function createNotificationExpressMiddleware(options: CallbackHandlerOptions): (req: any, res: any) => Promise<void>;
|
|
12
|
+
export declare function createNotificationFastifyHandler(options: CallbackHandlerOptions): (req: any, reply: any) => Promise<void>;
|
|
13
|
+
export declare function createNotificationHonoHandler(options: CallbackHandlerOptions): (c: any) => Promise<Response>;
|
|
14
|
+
//# sourceMappingURL=callbacks.d.ts.map
|