@doeza/sms-service 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/AGENTS.md +1616 -0
- package/README.md +139 -0
- package/examples/README.md +4 -0
- package/examples/nextjs-supabase/.env.example +10 -0
- package/examples/nextjs-supabase/README.md +211 -0
- package/examples/nextjs-supabase/src/app/api/sms/callbacks/reply/route.ts +12 -0
- package/examples/nextjs-supabase/src/app/api/sms/callbacks/status/route.ts +12 -0
- package/examples/nextjs-supabase/src/app/api/sms/send/route.ts +133 -0
- package/examples/nextjs-supabase/src/lib/sms/repository.ts +250 -0
- package/examples/nextjs-supabase/src/lib/sms/send-request.ts +56 -0
- package/examples/nextjs-supabase/src/lib/sms/service.ts +43 -0
- package/examples/nextjs-supabase/src/lib/sms/webhook-auth.ts +19 -0
- package/examples/nextjs-supabase/src/lib/supabase/admin.ts +25 -0
- package/examples/nextjs-supabase/src/lib/supabase/server.ts +28 -0
- package/examples/nextjs-supabase/supabase/schema.sql +399 -0
- package/examples/send.mjs +18 -0
- package/http.d.ts +9 -0
- package/http.js +75 -0
- package/index.d.ts +70 -0
- package/index.js +176 -0
- package/package.json +1 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,1616 @@
|
|
|
1
|
+
# @doeza/sms-service — AI Agent Development and Integration Guide
|
|
2
|
+
|
|
3
|
+
This document is the authoritative implementation guide for LLM-based coding agents, autonomous software agents, AI pair programmers, and human developers using or modifying `@doeza/sms-service`.
|
|
4
|
+
|
|
5
|
+
It is intentionally stricter than the package README. The README explains how to use the package. This document defines how an implementation agent must reason about the package, what it may change, what it must preserve, and which production-safety rules are non-negotiable.
|
|
6
|
+
|
|
7
|
+
The package source and tests remain the ultimate source of truth. When this document and executable behavior differ, inspect the current source and tests before making any change. Do not silently reinterpret existing behavior.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Agent operating mandate
|
|
12
|
+
|
|
13
|
+
When integrating or modifying this package, act as a senior backend/platform engineer responsible for correctness, security, delivery semantics, observability, and operational safety.
|
|
14
|
+
|
|
15
|
+
Your priorities, in order, are:
|
|
16
|
+
|
|
17
|
+
1. Prevent duplicate SMS sends.
|
|
18
|
+
2. Protect credentials, message content, recipient data, and provider URLs.
|
|
19
|
+
3. Preserve explicit delivery-state semantics.
|
|
20
|
+
4. Fail closed on malformed or ambiguous provider responses.
|
|
21
|
+
5. Keep provider-specific behavior inside the SMS package or a thin application adapter.
|
|
22
|
+
6. Keep authentication, authorization, persistence, rate limiting, product policy, and business rules in the host application.
|
|
23
|
+
7. Preserve the package's zero-runtime-dependency, server-only design unless a deliberate package-level architectural change is explicitly requested.
|
|
24
|
+
8. Prefer small, auditable changes over framework-specific abstractions.
|
|
25
|
+
9. Never claim delivery when the provider only confirmed acceptance.
|
|
26
|
+
10. Never add automatic retries to the send path without an explicit, externally verified idempotency design.
|
|
27
|
+
|
|
28
|
+
Do not optimize for convenience at the expense of these rules.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Mandatory preflight for every agent task
|
|
33
|
+
|
|
34
|
+
Before changing code that uses or modifies this package, inspect at least:
|
|
35
|
+
|
|
36
|
+
- `packages/sms-service/package.json`
|
|
37
|
+
- `packages/sms-service/index.js`
|
|
38
|
+
- `packages/sms-service/index.d.ts`
|
|
39
|
+
- `packages/sms-service/http.js` when using Web `Request` / `Response` handlers
|
|
40
|
+
- `packages/sms-service/http.d.ts` when using the HTTP entry point
|
|
41
|
+
- `packages/sms-service/test/service.test.js`
|
|
42
|
+
- `packages/sms-service/README.md`
|
|
43
|
+
|
|
44
|
+
If integrating in this repository, also inspect:
|
|
45
|
+
|
|
46
|
+
- `lib/sms.ts`
|
|
47
|
+
- the relevant `app/api/**/route.ts` files
|
|
48
|
+
- the host application's authentication, authorization, rate-limiting, and persistence layers
|
|
49
|
+
|
|
50
|
+
Do not assume the public API from memory. Do not invent methods, options, callback fields, or error codes.
|
|
51
|
+
|
|
52
|
+
Before coding, determine which of these tasks you are performing:
|
|
53
|
+
|
|
54
|
+
- direct server-side send integration
|
|
55
|
+
- HTTP send endpoint integration
|
|
56
|
+
- delivery-status callback integration
|
|
57
|
+
- incoming-reply callback integration
|
|
58
|
+
- package modification
|
|
59
|
+
- provider migration
|
|
60
|
+
- test-only integration
|
|
61
|
+
|
|
62
|
+
Use the smallest public surface needed for the task.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. Package contract at a glance
|
|
67
|
+
|
|
68
|
+
Package name:
|
|
69
|
+
|
|
70
|
+
```txt
|
|
71
|
+
@doeza/sms-service
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Runtime characteristics:
|
|
75
|
+
|
|
76
|
+
- Node.js 22+
|
|
77
|
+
- ESM
|
|
78
|
+
- no runtime dependencies
|
|
79
|
+
- no build step
|
|
80
|
+
- no environment loader
|
|
81
|
+
- no database
|
|
82
|
+
- no framework dependency
|
|
83
|
+
- no implicit logging
|
|
84
|
+
- no automatic retries
|
|
85
|
+
- explicit credential injection
|
|
86
|
+
- Fetch API transport
|
|
87
|
+
- TypeScript declarations included
|
|
88
|
+
|
|
89
|
+
Public entry points:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import {
|
|
93
|
+
createSmsService,
|
|
94
|
+
SmsError,
|
|
95
|
+
normalizeRecipients,
|
|
96
|
+
parseStatusCallback,
|
|
97
|
+
parseReplyCallback,
|
|
98
|
+
} from "@doeza/sms-service";
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Optional HTTP adapter:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import {
|
|
105
|
+
createSendSmsHandler,
|
|
106
|
+
createStatusCallbackHandler,
|
|
107
|
+
createReplyCallbackHandler,
|
|
108
|
+
} from "@doeza/sms-service/http";
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Do not import implementation files such as `index.js` or `http.js` through deep package-relative paths from a consuming application. Use the documented package exports.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 4. Core architectural boundary
|
|
116
|
+
|
|
117
|
+
The package owns:
|
|
118
|
+
|
|
119
|
+
- recipient normalization
|
|
120
|
+
- send-input validation
|
|
121
|
+
- Clickatell request construction
|
|
122
|
+
- provider transport
|
|
123
|
+
- timeout handling
|
|
124
|
+
- cancellation handling
|
|
125
|
+
- provider response validation
|
|
126
|
+
- typed result normalization
|
|
127
|
+
- SMS-specific error normalization
|
|
128
|
+
- callback payload parsing
|
|
129
|
+
- framework-neutral Web `Request` / `Response` adapters
|
|
130
|
+
|
|
131
|
+
The host application owns:
|
|
132
|
+
|
|
133
|
+
- environment-variable loading
|
|
134
|
+
- secret storage
|
|
135
|
+
- authentication
|
|
136
|
+
- authorization
|
|
137
|
+
- permissions
|
|
138
|
+
- rate limits
|
|
139
|
+
- abuse prevention
|
|
140
|
+
- CAPTCHA / bot protection when applicable
|
|
141
|
+
- persistence
|
|
142
|
+
- message templates
|
|
143
|
+
- consent and communication policy
|
|
144
|
+
- recipient ownership checks
|
|
145
|
+
- business rules
|
|
146
|
+
- scheduling
|
|
147
|
+
- queues
|
|
148
|
+
- auditing
|
|
149
|
+
- application logging
|
|
150
|
+
- metrics and tracing
|
|
151
|
+
- webhook ingress protection
|
|
152
|
+
- callback deduplication
|
|
153
|
+
- callback ordering policy
|
|
154
|
+
- provider-status reconciliation
|
|
155
|
+
- user-facing error text
|
|
156
|
+
|
|
157
|
+
Do not move host-application responsibilities into the package merely because it is convenient for one application.
|
|
158
|
+
|
|
159
|
+
Do not make the package read `process.env` directly. Pass configuration explicitly.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 5. Required service construction pattern
|
|
164
|
+
|
|
165
|
+
Create one service instance per credential/configuration set and reuse it.
|
|
166
|
+
|
|
167
|
+
Preferred host adapter:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import {
|
|
171
|
+
createSmsService,
|
|
172
|
+
type SmsService,
|
|
173
|
+
} from "@doeza/sms-service";
|
|
174
|
+
|
|
175
|
+
let smsService: SmsService | undefined;
|
|
176
|
+
|
|
177
|
+
export function getSmsService(): SmsService {
|
|
178
|
+
if (smsService) return smsService;
|
|
179
|
+
|
|
180
|
+
smsService = createSmsService({
|
|
181
|
+
apiKey: process.env.CLICKATELL_API_KEY ?? "",
|
|
182
|
+
timeoutMs: 10_000,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return smsService;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Why:
|
|
190
|
+
|
|
191
|
+
- configuration stays in the host application
|
|
192
|
+
- package initialization is centralized
|
|
193
|
+
- credentials are not duplicated across routes
|
|
194
|
+
- tests can replace the adapter
|
|
195
|
+
- lazy construction prevents missing runtime secrets from unnecessarily breaking build-time evaluation in frameworks such as Next.js
|
|
196
|
+
|
|
197
|
+
Do not create a new instance for every recipient unless a specific credential/configuration boundary requires it.
|
|
198
|
+
|
|
199
|
+
Do not construct the service in browser code.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## 6. Server-only rule
|
|
204
|
+
|
|
205
|
+
`@doeza/sms-service` is server-side infrastructure.
|
|
206
|
+
|
|
207
|
+
Never:
|
|
208
|
+
|
|
209
|
+
- import it into a React client component
|
|
210
|
+
- expose `CLICKATELL_API_KEY` through `NEXT_PUBLIC_*`
|
|
211
|
+
- pass the API key to a browser
|
|
212
|
+
- send directly from browser JavaScript to Clickatell
|
|
213
|
+
- place provider credentials in serialized page props
|
|
214
|
+
- store the API key in localStorage, sessionStorage, IndexedDB, cookies, HTML, or client bundles
|
|
215
|
+
|
|
216
|
+
A browser action must call an authenticated and authorized server endpoint controlled by the host application.
|
|
217
|
+
|
|
218
|
+
For Next.js App Router, make the Node runtime explicit when appropriate:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
export const runtime = "nodejs";
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 7. Recipient handling rules
|
|
227
|
+
|
|
228
|
+
The package accepts:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
string | readonly string[]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
A string may be comma-separated.
|
|
235
|
+
|
|
236
|
+
Accepted number format after normalization:
|
|
237
|
+
|
|
238
|
+
- optional surrounding whitespace
|
|
239
|
+
- optional leading `+`
|
|
240
|
+
- 5–15 digits total after removing the leading `+`
|
|
241
|
+
- first digit must be non-zero
|
|
242
|
+
- country code must already be present
|
|
243
|
+
|
|
244
|
+
The package intentionally does not guess country codes.
|
|
245
|
+
|
|
246
|
+
Correct:
|
|
247
|
+
|
|
248
|
+
```txt
|
|
249
|
+
+46701234567
|
|
250
|
+
46701234567
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Incorrect:
|
|
254
|
+
|
|
255
|
+
```txt
|
|
256
|
+
0701234567
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Do not silently prepend `46`, `1`, `44`, or any other country code in a generic integration layer.
|
|
260
|
+
|
|
261
|
+
If the product accepts local-format numbers, normalize them in a country-aware host-domain layer before calling this package. That layer must have explicit locale/country context and tests.
|
|
262
|
+
|
|
263
|
+
The package deduplicates identical normalized recipients. Do not assume duplicate inputs produce duplicate sends.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 8. Message-content rules
|
|
268
|
+
|
|
269
|
+
`content` must be a non-empty string after trimming for validation.
|
|
270
|
+
|
|
271
|
+
The package sends the original content string. Do not assume it trims, rewrites, transliterates, shortens, or sanitizes the message.
|
|
272
|
+
|
|
273
|
+
The host application must own:
|
|
274
|
+
|
|
275
|
+
- template rendering
|
|
276
|
+
- localization
|
|
277
|
+
- length policy
|
|
278
|
+
- segmentation/cost policy
|
|
279
|
+
- prohibited-content policy
|
|
280
|
+
- personalization escaping
|
|
281
|
+
- opt-out wording
|
|
282
|
+
- consent requirements
|
|
283
|
+
|
|
284
|
+
Do not add HTML escaping and assume it is SMS sanitization. SMS is plain text; product-specific template safety must be handled where templates are created.
|
|
285
|
+
|
|
286
|
+
Do not log full message bodies by default.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 9. `clientMessageId` semantics
|
|
291
|
+
|
|
292
|
+
`clientMessageId` is a correlation identifier.
|
|
293
|
+
|
|
294
|
+
It is not an idempotency guarantee.
|
|
295
|
+
|
|
296
|
+
If omitted, the package generates a UUID.
|
|
297
|
+
|
|
298
|
+
Use an application-provided `clientMessageId` when you need stable correlation across:
|
|
299
|
+
|
|
300
|
+
- your database record
|
|
301
|
+
- the initial send request
|
|
302
|
+
- logs
|
|
303
|
+
- provider callbacks
|
|
304
|
+
- operational reconciliation
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
const result = await sms.send({
|
|
310
|
+
to: recipient,
|
|
311
|
+
content,
|
|
312
|
+
clientMessageId: notification.id,
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Do not interpret reusing the same `clientMessageId` as proof that the provider will suppress duplicate sends.
|
|
317
|
+
|
|
318
|
+
Never implement retry safety solely by reusing `clientMessageId`.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 10. The most important rule: do not automatically retry sends
|
|
323
|
+
|
|
324
|
+
The package intentionally performs one provider request and does not retry automatically.
|
|
325
|
+
|
|
326
|
+
This is a correctness feature.
|
|
327
|
+
|
|
328
|
+
A timeout, socket failure, connection reset, response-body failure, cancellation, malformed provider response, or intermediary failure can occur after the provider has already accepted the SMS.
|
|
329
|
+
|
|
330
|
+
Therefore:
|
|
331
|
+
|
|
332
|
+
```txt
|
|
333
|
+
request failed locally != provider definitely did not accept the message
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Never add generic retry middleware around `sms.send()`.
|
|
337
|
+
|
|
338
|
+
Never configure a job runner, queue, HTTP client, serverless platform, or orchestration system to blindly rerun failed SMS sends.
|
|
339
|
+
|
|
340
|
+
Never use patterns such as:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
await retry(() => sms.send(input), { retries: 3 });
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Do not retry `NETWORK`, `TIMEOUT`, `ABORTED`, or `INVALID_RESPONSE` merely because they look transient.
|
|
347
|
+
|
|
348
|
+
Before any resend after an ambiguous failure:
|
|
349
|
+
|
|
350
|
+
1. persist or recover the correlation identifier
|
|
351
|
+
2. query/reconcile provider state when possible
|
|
352
|
+
3. determine whether the provider accepted the prior request
|
|
353
|
+
4. resend only if policy explicitly says it is safe
|
|
354
|
+
5. create a new application audit event for the resend decision
|
|
355
|
+
|
|
356
|
+
If the provider gains a documented, verified idempotency facility in the future, design and test idempotency before introducing retries.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 11. Distinguish acceptance from delivery
|
|
361
|
+
|
|
362
|
+
`SmsResult.success === true` means every requested recipient was accepted by Clickatell's send endpoint.
|
|
363
|
+
|
|
364
|
+
It does not mean:
|
|
365
|
+
|
|
366
|
+
- delivered to handset
|
|
367
|
+
- displayed to recipient
|
|
368
|
+
- read by recipient
|
|
369
|
+
- permanently successful
|
|
370
|
+
|
|
371
|
+
Delivery must be derived from later delivery-status callbacks or provider reconciliation.
|
|
372
|
+
|
|
373
|
+
UI copy, database field names, and logs must reflect this distinction.
|
|
374
|
+
|
|
375
|
+
Prefer terms such as:
|
|
376
|
+
|
|
377
|
+
- `accepted`
|
|
378
|
+
- `providerAcceptedAt`
|
|
379
|
+
- `deliveryStatus`
|
|
380
|
+
- `deliveredAt`
|
|
381
|
+
|
|
382
|
+
Avoid setting a final status named `sent` if that status is later interpreted as delivered.
|
|
383
|
+
|
|
384
|
+
If legacy schema uses `sent`, document its exact meaning.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 12. Batch-send result handling
|
|
389
|
+
|
|
390
|
+
A provider request can contain multiple recipients.
|
|
391
|
+
|
|
392
|
+
Always inspect `result.messages`.
|
|
393
|
+
|
|
394
|
+
Result shape:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
interface SmsResult {
|
|
398
|
+
success: boolean;
|
|
399
|
+
partial: boolean;
|
|
400
|
+
clientMessageId: string;
|
|
401
|
+
to: string[];
|
|
402
|
+
responseCode?: number;
|
|
403
|
+
messages: Array<{
|
|
404
|
+
to: string;
|
|
405
|
+
accepted: boolean;
|
|
406
|
+
apiMessageId?: string;
|
|
407
|
+
errorCode?: string | number;
|
|
408
|
+
}>;
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Semantics:
|
|
413
|
+
|
|
414
|
+
- `success: true` => all recipients accepted
|
|
415
|
+
- `partial: true` => some accepted and some rejected
|
|
416
|
+
- `success: false, partial: false` => none accepted
|
|
417
|
+
|
|
418
|
+
Never retry the entire recipient batch after partial acceptance.
|
|
419
|
+
|
|
420
|
+
If product policy permits retries, isolate only recipients known to have been rejected and make the retry decision explicitly.
|
|
421
|
+
|
|
422
|
+
Persist per-recipient provider IDs and error codes when delivery tracking or reconciliation matters.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## 13. Error model
|
|
427
|
+
|
|
428
|
+
All expected SMS-package failures are normalized to `SmsError`.
|
|
429
|
+
|
|
430
|
+
Codes:
|
|
431
|
+
|
|
432
|
+
```txt
|
|
433
|
+
CONFIGURATION
|
|
434
|
+
VALIDATION
|
|
435
|
+
PROVIDER
|
|
436
|
+
INVALID_RESPONSE
|
|
437
|
+
NETWORK
|
|
438
|
+
TIMEOUT
|
|
439
|
+
ABORTED
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Use typed branching:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
import { SmsError } from "@doeza/sms-service";
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
return await sms.send(input);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (!(error instanceof SmsError)) throw error;
|
|
451
|
+
|
|
452
|
+
switch (error.code) {
|
|
453
|
+
case "VALIDATION":
|
|
454
|
+
// Caller/input problem.
|
|
455
|
+
break;
|
|
456
|
+
case "CONFIGURATION":
|
|
457
|
+
// Deployment/configuration problem.
|
|
458
|
+
break;
|
|
459
|
+
case "PROVIDER":
|
|
460
|
+
case "INVALID_RESPONSE":
|
|
461
|
+
case "NETWORK":
|
|
462
|
+
case "TIMEOUT":
|
|
463
|
+
case "ABORTED":
|
|
464
|
+
// Operational failure. Do not blindly resend.
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Do not parse error message strings to determine behavior.
|
|
471
|
+
|
|
472
|
+
Do not expose internal stack traces to end users.
|
|
473
|
+
|
|
474
|
+
Do not replace `SmsError` with an untyped generic application error before recording the error code and correlation identifier needed for operations.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 14. Secret-redaction rules
|
|
479
|
+
|
|
480
|
+
The Clickatell HTTP endpoint used by this package sends credentials and message data as URL query parameters.
|
|
481
|
+
|
|
482
|
+
Therefore, the full outbound URL is secret-bearing.
|
|
483
|
+
|
|
484
|
+
Never log:
|
|
485
|
+
|
|
486
|
+
- `url.href` from the provider request
|
|
487
|
+
- raw fetch request URLs
|
|
488
|
+
- raw native fetch exceptions that may contain the URL
|
|
489
|
+
- Clickatell API keys
|
|
490
|
+
- complete query strings
|
|
491
|
+
- full provider response bodies unless separately reviewed and redacted
|
|
492
|
+
- message content by default
|
|
493
|
+
|
|
494
|
+
This applies to:
|
|
495
|
+
|
|
496
|
+
- application logs
|
|
497
|
+
- reverse proxies
|
|
498
|
+
- APM tools
|
|
499
|
+
- OpenTelemetry attributes
|
|
500
|
+
- tracing spans
|
|
501
|
+
- Sentry breadcrumbs
|
|
502
|
+
- HTTP instrumentation
|
|
503
|
+
- test snapshots
|
|
504
|
+
- debug output
|
|
505
|
+
- CI logs
|
|
506
|
+
|
|
507
|
+
When instrumenting `fetch`, explicitly redact the URL.
|
|
508
|
+
|
|
509
|
+
Prefer metadata such as:
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
{
|
|
513
|
+
operation: "sms.send",
|
|
514
|
+
provider: "clickatell",
|
|
515
|
+
clientMessageId,
|
|
516
|
+
recipientCount,
|
|
517
|
+
outcome,
|
|
518
|
+
errorCode,
|
|
519
|
+
durationMs,
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
If recipient-level observability is required, prefer an internal recipient/customer identifier or a deliberately masked/hardened representation rather than full phone numbers.
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## 15. Logging and observability policy
|
|
528
|
+
|
|
529
|
+
The package intentionally does not log.
|
|
530
|
+
|
|
531
|
+
The host application should add structured observability at the boundary around `sms.send()`.
|
|
532
|
+
|
|
533
|
+
Recommended events:
|
|
534
|
+
|
|
535
|
+
- `sms_send_attempted`
|
|
536
|
+
- `sms_send_accepted`
|
|
537
|
+
- `sms_send_partially_accepted`
|
|
538
|
+
- `sms_send_rejected`
|
|
539
|
+
- `sms_send_failed`
|
|
540
|
+
- `sms_delivery_status_received`
|
|
541
|
+
- `sms_reply_received`
|
|
542
|
+
|
|
543
|
+
Recommended dimensions:
|
|
544
|
+
|
|
545
|
+
- `clientMessageId`
|
|
546
|
+
- application notification/message ID
|
|
547
|
+
- provider
|
|
548
|
+
- recipient count
|
|
549
|
+
- result category
|
|
550
|
+
- `SmsError.code`
|
|
551
|
+
- HTTP status if safely available
|
|
552
|
+
- latency
|
|
553
|
+
|
|
554
|
+
Avoid high-cardinality dimensions unless your telemetry system is designed for them.
|
|
555
|
+
|
|
556
|
+
Never attach API keys or full message bodies to traces.
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## 16. Timeout policy
|
|
561
|
+
|
|
562
|
+
Default package timeout is 10 seconds unless overridden.
|
|
563
|
+
|
|
564
|
+
`timeoutMs` must be a positive integer.
|
|
565
|
+
|
|
566
|
+
The timeout covers both:
|
|
567
|
+
|
|
568
|
+
- obtaining the HTTP response
|
|
569
|
+
- reading the response body
|
|
570
|
+
|
|
571
|
+
Do not assume a timeout proves non-delivery.
|
|
572
|
+
|
|
573
|
+
Choose a host-specific timeout based on the containing request/job budget, but preserve enough time for the package to cleanly classify its own timeout.
|
|
574
|
+
|
|
575
|
+
Avoid stacking multiple timeout layers with nearly identical deadlines because the outer layer can obscure the SMS-specific error type.
|
|
576
|
+
|
|
577
|
+
When cancellation from the caller is meaningful, pass an `AbortSignal`:
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
await sms.send(input, { signal });
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Cancellation is also an ambiguous send outcome if the provider request may already have left the process.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## 17. Callback URL configuration
|
|
588
|
+
|
|
589
|
+
Optional service options:
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
statusCallbackUrl?: string;
|
|
593
|
+
replyCallbackUrl?: string;
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
They must be absolute `http:` or `https:` URLs without embedded credentials or URL fragments.
|
|
597
|
+
|
|
598
|
+
Prefer building callback URLs from an explicit trusted application origin:
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
const appUrl = process.env.APP_URL;
|
|
602
|
+
const statusCallbackUrl = new URL("/api/callbacks/sms-status", appUrl).href;
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Do not derive externally visible callback destinations from an untrusted `Host`, `X-Forwarded-Host`, or request-origin header unless the deployment has a carefully configured trusted-proxy model.
|
|
606
|
+
|
|
607
|
+
Do not assume supplying callback URLs in the send request automatically completes all provider-side callback or two-way messaging configuration.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## 18. Callback authentication and ingress protection
|
|
612
|
+
|
|
613
|
+
`parseStatusCallback` and `parseReplyCallback` validate shape. They do not authenticate the sender.
|
|
614
|
+
|
|
615
|
+
The callback HTTP handlers also do not add authentication unless `authorize` is supplied.
|
|
616
|
+
|
|
617
|
+
This distinction is mandatory:
|
|
618
|
+
|
|
619
|
+
```txt
|
|
620
|
+
valid payload shape != authentic provider callback
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Before production exposure, determine how the deployment verifies callback authenticity. Depending on provider capabilities and infrastructure this may include:
|
|
624
|
+
|
|
625
|
+
- provider-supported signatures
|
|
626
|
+
- a secret callback token
|
|
627
|
+
- authenticated ingress
|
|
628
|
+
- provider IP allowlisting as a defense-in-depth control
|
|
629
|
+
- private networking or gateway verification
|
|
630
|
+
|
|
631
|
+
Do not invent a signature scheme that Clickatell does not document.
|
|
632
|
+
|
|
633
|
+
Do not claim callbacks are secure merely because the payload parses.
|
|
634
|
+
|
|
635
|
+
Use `authorize(request)` when the verification can be expressed at the handler boundary:
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
const handler = createStatusCallbackHandler(saveStatus, {
|
|
639
|
+
authorize: verifySmsWebhookRequest,
|
|
640
|
+
});
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
For more complex verification, place a trusted ingress wrapper in front of the package handler/parser.
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## 19. Callback persistence rules
|
|
648
|
+
|
|
649
|
+
Callback consumers may be called more than once and callbacks can arrive out of order.
|
|
650
|
+
|
|
651
|
+
Persistence must therefore be idempotent and ordering-aware.
|
|
652
|
+
|
|
653
|
+
For delivery status:
|
|
654
|
+
|
|
655
|
+
- key records by provider message ID and/or stable application correlation data
|
|
656
|
+
- store raw normalized status fields needed for auditing
|
|
657
|
+
- preserve the latest accepted business state according to an explicit state-transition policy
|
|
658
|
+
- do not assume arrival order equals lifecycle order
|
|
659
|
+
- make duplicate processing safe
|
|
660
|
+
|
|
661
|
+
For replies:
|
|
662
|
+
|
|
663
|
+
- deduplicate by a stable provider reply/message identifier when available
|
|
664
|
+
- avoid creating duplicate chat messages from repeated provider callbacks
|
|
665
|
+
- retain enough linkage to associate the reply with the correct conversation/account
|
|
666
|
+
|
|
667
|
+
The package handlers acknowledge success only after your consumer resolves successfully. Preserve that behavior.
|
|
668
|
+
|
|
669
|
+
Do not return success before durable storage when provider retry behavior is required for reliability.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## 20. Status callback shape
|
|
674
|
+
|
|
675
|
+
Required normalized fields:
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
{
|
|
679
|
+
messageId: string;
|
|
680
|
+
to: string;
|
|
681
|
+
statusCode: number;
|
|
682
|
+
status: string;
|
|
683
|
+
timestamp: string;
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Optional normalized fields include:
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
integrationName
|
|
691
|
+
requestId
|
|
692
|
+
clientMessageId
|
|
693
|
+
from
|
|
694
|
+
statusDescription
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Numeric query/form values are normalized where applicable.
|
|
698
|
+
|
|
699
|
+
Do not accept malformed callback values merely to keep an endpoint returning `200`.
|
|
700
|
+
|
|
701
|
+
Failing closed allows invalid integrations to surface during testing and operations.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## 21. Reply callback shape
|
|
706
|
+
|
|
707
|
+
Required normalized fields:
|
|
708
|
+
|
|
709
|
+
```ts
|
|
710
|
+
{
|
|
711
|
+
fromNumber: string;
|
|
712
|
+
toNumber: string;
|
|
713
|
+
timestamp: number;
|
|
714
|
+
text: string;
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
Optional fields include:
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
integrationName
|
|
722
|
+
replyMessageId
|
|
723
|
+
messageId
|
|
724
|
+
charset
|
|
725
|
+
udh
|
|
726
|
+
network
|
|
727
|
+
keyword
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
Do not treat incoming text as trusted command input.
|
|
731
|
+
|
|
732
|
+
If replies trigger business actions, run normal authentication-by-context, authorization, parsing, anti-abuse, and domain validation before acting.
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## 22. HTTP adapter rules
|
|
737
|
+
|
|
738
|
+
The `/http` entry point uses standard Web `Request` and `Response` types.
|
|
739
|
+
|
|
740
|
+
`createSendSmsHandler`:
|
|
741
|
+
|
|
742
|
+
- accepts POST only
|
|
743
|
+
- reads JSON
|
|
744
|
+
- can run `authorize(request)` before processing
|
|
745
|
+
- returns structured SMS results
|
|
746
|
+
- returns HTTP 200 for provider-level partial/all-recipient rejection represented as a valid result
|
|
747
|
+
- maps validation errors to 400
|
|
748
|
+
- maps configuration errors to 500
|
|
749
|
+
- maps provider/invalid-response errors to 502
|
|
750
|
+
- maps network/timeout errors to 504
|
|
751
|
+
- maps cancellation to 408
|
|
752
|
+
- adds `Cache-Control: no-store`
|
|
753
|
+
|
|
754
|
+
Do not interpret every HTTP 200 as `result.success === true`.
|
|
755
|
+
|
|
756
|
+
The response body is authoritative for recipient acceptance.
|
|
757
|
+
|
|
758
|
+
`createStatusCallbackHandler` and `createReplyCallbackHandler`:
|
|
759
|
+
|
|
760
|
+
- accept GET and POST
|
|
761
|
+
- accept JSON
|
|
762
|
+
- accept `application/x-www-form-urlencoded`
|
|
763
|
+
- accept query parameters on GET
|
|
764
|
+
- await the consumer before acknowledging success
|
|
765
|
+
- support optional authorization
|
|
766
|
+
|
|
767
|
+
Do not bypass host authentication/rate limiting when exposing the send handler publicly.
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
## 23. Next.js integration pattern
|
|
772
|
+
|
|
773
|
+
Recommended App Router structure:
|
|
774
|
+
|
|
775
|
+
```txt
|
|
776
|
+
lib/
|
|
777
|
+
sms.ts
|
|
778
|
+
app/
|
|
779
|
+
api/
|
|
780
|
+
send-sms/
|
|
781
|
+
route.ts
|
|
782
|
+
callbacks/
|
|
783
|
+
sms-status/
|
|
784
|
+
route.ts
|
|
785
|
+
sms-reply/
|
|
786
|
+
route.ts
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Service adapter:
|
|
790
|
+
|
|
791
|
+
```ts
|
|
792
|
+
// lib/sms.ts
|
|
793
|
+
import { createSmsService, type SmsService } from "@doeza/sms-service";
|
|
794
|
+
|
|
795
|
+
let service: SmsService | undefined;
|
|
796
|
+
|
|
797
|
+
export function getSmsService() {
|
|
798
|
+
if (service) return service;
|
|
799
|
+
|
|
800
|
+
service = createSmsService({
|
|
801
|
+
apiKey: process.env.CLICKATELL_API_KEY ?? "",
|
|
802
|
+
timeoutMs: Number(process.env.SMS_TIMEOUT_MS ?? 10_000),
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
return service;
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Route:
|
|
810
|
+
|
|
811
|
+
```ts
|
|
812
|
+
// app/api/send-sms/route.ts
|
|
813
|
+
import { createSendSmsHandler } from "@doeza/sms-service/http";
|
|
814
|
+
import { getSmsService } from "@/lib/sms";
|
|
815
|
+
|
|
816
|
+
export const runtime = "nodejs";
|
|
817
|
+
|
|
818
|
+
export const POST = createSendSmsHandler(getSmsService, {
|
|
819
|
+
authorize: async (request) => {
|
|
820
|
+
// Call the application's real authentication/authorization layer.
|
|
821
|
+
return false;
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
The `authorize` example deliberately denies by default. Replace it with the host application's real authorization logic.
|
|
827
|
+
|
|
828
|
+
Never ship a production send route with a placeholder `return true` authorization callback.
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## 24. Express / Fastify / job-worker integration
|
|
833
|
+
|
|
834
|
+
For server frameworks that do not use Web `Request` / `Response`, prefer calling the core service directly.
|
|
835
|
+
|
|
836
|
+
Example service-layer function:
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
export async function sendBookingConfirmation(input: {
|
|
840
|
+
phoneNumber: string;
|
|
841
|
+
bookingId: string;
|
|
842
|
+
}) {
|
|
843
|
+
const sms = getSmsService();
|
|
844
|
+
const content = renderBookingConfirmation(input.bookingId);
|
|
845
|
+
|
|
846
|
+
return sms.send({
|
|
847
|
+
to: input.phoneNumber,
|
|
848
|
+
content,
|
|
849
|
+
clientMessageId: `booking:${input.bookingId}`,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
Keep framework adaptation outside the package.
|
|
855
|
+
|
|
856
|
+
Do not wrap the package in a second generic "SMS SDK" unless that layer provides real application-domain value such as templates, policy, persistence, or provider abstraction.
|
|
857
|
+
|
|
858
|
+
Avoid abstraction layers that only rename methods and make failure semantics harder to see.
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
## 25. Authentication, authorization, and abuse prevention
|
|
863
|
+
|
|
864
|
+
Sending SMS has direct financial and abuse impact.
|
|
865
|
+
|
|
866
|
+
A production send endpoint should normally enforce:
|
|
867
|
+
|
|
868
|
+
- authenticated caller identity
|
|
869
|
+
- authorization for the specific operation
|
|
870
|
+
- recipient ownership or business relationship
|
|
871
|
+
- server-side recipient selection when possible
|
|
872
|
+
- rate limits
|
|
873
|
+
- per-account quotas
|
|
874
|
+
- anti-automation/abuse controls where applicable
|
|
875
|
+
- audit logging
|
|
876
|
+
|
|
877
|
+
Do not let a normal end user submit arbitrary `to` and `content` values to a generic public relay unless the product explicitly requires that behavior and has strong abuse controls.
|
|
878
|
+
|
|
879
|
+
Preferred product API:
|
|
880
|
+
|
|
881
|
+
```json
|
|
882
|
+
{
|
|
883
|
+
"bookingId": "..."
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
Less safe generic API:
|
|
888
|
+
|
|
889
|
+
```json
|
|
890
|
+
{
|
|
891
|
+
"to": "+46701234567",
|
|
892
|
+
"content": "anything"
|
|
893
|
+
}
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
When possible, resolve recipients and templates server-side from trusted application data.
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
## 26. Persistence and transaction boundaries
|
|
901
|
+
|
|
902
|
+
Do not combine SMS transmission and database transactions in a way that causes database retries to resend SMS.
|
|
903
|
+
|
|
904
|
+
Bad pattern:
|
|
905
|
+
|
|
906
|
+
```ts
|
|
907
|
+
await db.transaction(async (tx) => {
|
|
908
|
+
await tx.insert(...);
|
|
909
|
+
await sms.send(...);
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
If the transaction callback is retried by the database/client, the SMS may be duplicated.
|
|
914
|
+
|
|
915
|
+
Prefer an outbox/job model when durable workflow matters:
|
|
916
|
+
|
|
917
|
+
1. transactionally store the domain change and an SMS intent/outbox record
|
|
918
|
+
2. commit
|
|
919
|
+
3. a worker claims the SMS intent exactly once according to application policy
|
|
920
|
+
4. worker sends once
|
|
921
|
+
5. worker records the provider result
|
|
922
|
+
6. ambiguous failures enter reconciliation, not blind resend
|
|
923
|
+
|
|
924
|
+
If an outbox is unnecessary, at minimum separate durable application writes from the network send so retry behavior is explicit.
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## 27. Queue-worker rules
|
|
929
|
+
|
|
930
|
+
Queues often retry failed jobs automatically. This is dangerous for SMS.
|
|
931
|
+
|
|
932
|
+
If `sms.send()` is called inside a queue job:
|
|
933
|
+
|
|
934
|
+
- do not use the queue's default retry policy without analysis
|
|
935
|
+
- classify failures
|
|
936
|
+
- prevent automatic resend after ambiguous transport failures
|
|
937
|
+
- persist the attempt before or with a stable attempt identifier
|
|
938
|
+
- route uncertain outcomes to reconciliation/manual policy
|
|
939
|
+
- make job ownership/locking robust against concurrent workers
|
|
940
|
+
|
|
941
|
+
A job-level crash after provider acceptance but before success persistence is also an ambiguous state.
|
|
942
|
+
|
|
943
|
+
Design for that case explicitly.
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
947
|
+
## 28. Rate limiting
|
|
948
|
+
|
|
949
|
+
The package does not implement rate limits.
|
|
950
|
+
|
|
951
|
+
Apply rate limits at the host boundary based on product risk, for example:
|
|
952
|
+
|
|
953
|
+
- caller/account
|
|
954
|
+
- recipient
|
|
955
|
+
- operation type
|
|
956
|
+
- tenant
|
|
957
|
+
- IP address as a secondary signal
|
|
958
|
+
|
|
959
|
+
Do not rate-limit only by IP in authenticated multi-user systems.
|
|
960
|
+
|
|
961
|
+
Do not rely on provider throttling as your abuse-control layer.
|
|
962
|
+
|
|
963
|
+
Provider HTTP 429 is a provider failure, not permission to automatically resend the same SMS later without duplicate-safety analysis.
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
## 29. Configuration validation
|
|
968
|
+
|
|
969
|
+
Allow package construction to validate SMS-specific configuration.
|
|
970
|
+
|
|
971
|
+
Also validate deployment configuration at application startup or health-check time when operationally appropriate.
|
|
972
|
+
|
|
973
|
+
Important variables may include:
|
|
974
|
+
|
|
975
|
+
```txt
|
|
976
|
+
CLICKATELL_API_KEY
|
|
977
|
+
SMS_TIMEOUT_MS
|
|
978
|
+
APP_URL
|
|
979
|
+
ENABLE_SMS_CALLBACKS
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
Do not coerce invalid numeric values into apparently valid defaults without intention.
|
|
983
|
+
|
|
984
|
+
Do not expose secret values in configuration-error messages.
|
|
985
|
+
|
|
986
|
+
---
|
|
987
|
+
|
|
988
|
+
## 30. Testing strategy
|
|
989
|
+
|
|
990
|
+
Every integration should test behavior, not only the happy path.
|
|
991
|
+
|
|
992
|
+
The package supports injected `fetch`:
|
|
993
|
+
|
|
994
|
+
```ts
|
|
995
|
+
const sms = createSmsService({
|
|
996
|
+
apiKey: "test-key",
|
|
997
|
+
fetch: mockFetch,
|
|
998
|
+
});
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
Use this instead of real provider calls in automated tests.
|
|
1002
|
+
|
|
1003
|
+
Required test categories for changes affecting sends:
|
|
1004
|
+
|
|
1005
|
+
1. successful acceptance
|
|
1006
|
+
2. partial acceptance
|
|
1007
|
+
3. all-recipient rejection
|
|
1008
|
+
4. input validation
|
|
1009
|
+
5. provider HTTP error
|
|
1010
|
+
6. body-level provider error
|
|
1011
|
+
7. malformed JSON
|
|
1012
|
+
8. malformed provider response shape
|
|
1013
|
+
9. timeout
|
|
1014
|
+
10. cancellation
|
|
1015
|
+
11. native network failure
|
|
1016
|
+
12. secret redaction
|
|
1017
|
+
13. no implicit retry
|
|
1018
|
+
14. duplicate recipient normalization
|
|
1019
|
+
15. Unicode/special-character content encoding
|
|
1020
|
+
|
|
1021
|
+
Required callback test categories:
|
|
1022
|
+
|
|
1023
|
+
1. valid callback
|
|
1024
|
+
2. malformed required field
|
|
1025
|
+
3. numeric query/form normalization
|
|
1026
|
+
4. duplicate callback handling in the host persistence layer
|
|
1027
|
+
5. consumer failure does not acknowledge success
|
|
1028
|
+
6. authorization failure
|
|
1029
|
+
7. GET query payload when supported
|
|
1030
|
+
8. POST JSON
|
|
1031
|
+
9. POST form-encoded payload
|
|
1032
|
+
|
|
1033
|
+
Never require real credentials or network access for unit tests.
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
## 31. Package-change requirements
|
|
1038
|
+
|
|
1039
|
+
When modifying `packages/sms-service` itself, preserve the public contract unless the task explicitly authorizes a breaking change.
|
|
1040
|
+
|
|
1041
|
+
For every public API modification:
|
|
1042
|
+
|
|
1043
|
+
- update JavaScript implementation
|
|
1044
|
+
- update `.d.ts` declarations
|
|
1045
|
+
- update tests
|
|
1046
|
+
- update README
|
|
1047
|
+
- update this `AGENTS.md` if agent guidance changes
|
|
1048
|
+
- consider package versioning implications
|
|
1049
|
+
|
|
1050
|
+
Do not add a dependency for functionality that can be implemented safely with Node/Web platform APIs unless there is a strong maintenance reason.
|
|
1051
|
+
|
|
1052
|
+
Do not add framework imports to the core package.
|
|
1053
|
+
|
|
1054
|
+
Do not add hidden environment-variable reads.
|
|
1055
|
+
|
|
1056
|
+
Do not add global mutable state inside the package for application-specific concerns.
|
|
1057
|
+
|
|
1058
|
+
Do not expose raw provider responses or native fetch errors simply to aid debugging; add safe normalized fields instead.
|
|
1059
|
+
|
|
1060
|
+
---
|
|
1061
|
+
|
|
1062
|
+
## 32. Provider-response validation principle
|
|
1063
|
+
|
|
1064
|
+
Fail closed.
|
|
1065
|
+
|
|
1066
|
+
The package deliberately verifies that provider responses correspond to the requested recipients and contain expected fields.
|
|
1067
|
+
|
|
1068
|
+
Do not weaken response validation to "make an integration work" without first verifying the current Clickatell response contract.
|
|
1069
|
+
|
|
1070
|
+
If the provider changes its response format:
|
|
1071
|
+
|
|
1072
|
+
1. capture a sanitized example
|
|
1073
|
+
2. consult current provider documentation
|
|
1074
|
+
3. determine compatibility requirements
|
|
1075
|
+
4. update parser behavior deliberately
|
|
1076
|
+
5. add regression tests
|
|
1077
|
+
6. preserve secret redaction
|
|
1078
|
+
7. document the change
|
|
1079
|
+
|
|
1080
|
+
Never mark an SMS accepted from an unrecognized or incomplete response structure.
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
## 33. Provider API changes
|
|
1085
|
+
|
|
1086
|
+
The current transport targets:
|
|
1087
|
+
|
|
1088
|
+
```txt
|
|
1089
|
+
https://platform.clickatell.com/messages/http/send
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
It uses a GET request with URL-encoded parameters and refuses redirects.
|
|
1093
|
+
|
|
1094
|
+
Do not migrate to another Clickatell API, SDK, HTTP method, or endpoint as an incidental refactor.
|
|
1095
|
+
|
|
1096
|
+
A provider-API migration affects:
|
|
1097
|
+
|
|
1098
|
+
- authentication
|
|
1099
|
+
- secret exposure model
|
|
1100
|
+
- request encoding
|
|
1101
|
+
- response schema
|
|
1102
|
+
- retry/idempotency semantics
|
|
1103
|
+
- callback semantics
|
|
1104
|
+
- errors
|
|
1105
|
+
- rate limits
|
|
1106
|
+
- delivery receipts
|
|
1107
|
+
- tests
|
|
1108
|
+
- documentation
|
|
1109
|
+
|
|
1110
|
+
Treat it as an explicit migration project.
|
|
1111
|
+
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
## 34. Do not accidentally leak data through instrumentation
|
|
1115
|
+
|
|
1116
|
+
Many HTTP/APM libraries automatically capture request URLs.
|
|
1117
|
+
|
|
1118
|
+
Because this provider uses query parameters for secrets and message content, review instrumentation around custom/injected `fetch` implementations.
|
|
1119
|
+
|
|
1120
|
+
If adding tracing, either:
|
|
1121
|
+
|
|
1122
|
+
- disable URL capture for this operation, or
|
|
1123
|
+
- replace the URL attribute with a safe origin/path-only representation
|
|
1124
|
+
|
|
1125
|
+
Safe example:
|
|
1126
|
+
|
|
1127
|
+
```txt
|
|
1128
|
+
https://platform.clickatell.com/messages/http/send
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
Unsafe example:
|
|
1132
|
+
|
|
1133
|
+
```txt
|
|
1134
|
+
https://platform.clickatell.com/messages/http/send?apiKey=...&to=...&content=...
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
Do not attach `URLSearchParams` objects to telemetry.
|
|
1138
|
+
|
|
1139
|
+
---
|
|
1140
|
+
|
|
1141
|
+
## 35. Data minimization
|
|
1142
|
+
|
|
1143
|
+
Phone numbers and message bodies may contain personal or sensitive information.
|
|
1144
|
+
|
|
1145
|
+
Default engineering policy:
|
|
1146
|
+
|
|
1147
|
+
- process only what is required
|
|
1148
|
+
- avoid duplicating message bodies into logs
|
|
1149
|
+
- retain provider IDs and correlation IDs when they are sufficient
|
|
1150
|
+
- mask phone numbers in operational logs where practical
|
|
1151
|
+
- follow the host application's retention policy
|
|
1152
|
+
- do not place SMS bodies in error text
|
|
1153
|
+
|
|
1154
|
+
Do not make broad claims about regulatory compliance solely because these engineering practices are followed. Compliance depends on jurisdiction and product context.
|
|
1155
|
+
|
|
1156
|
+
---
|
|
1157
|
+
|
|
1158
|
+
## 36. Safe user-facing status mapping
|
|
1159
|
+
|
|
1160
|
+
Application UI should not directly display low-level provider/network errors.
|
|
1161
|
+
|
|
1162
|
+
Example policy:
|
|
1163
|
+
|
|
1164
|
+
```ts
|
|
1165
|
+
function toUserMessage(error: unknown) {
|
|
1166
|
+
if (!(error instanceof SmsError)) {
|
|
1167
|
+
return "We could not send the message right now.";
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (error.code === "VALIDATION") {
|
|
1171
|
+
return "The phone number or message is invalid.";
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
return "We could not confirm the SMS send. Please check the message status before trying again.";
|
|
1175
|
+
}
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
The final text is product-specific, but it must not encourage automatic/manual duplicate sends after ambiguous outcomes without checking status.
|
|
1179
|
+
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
## 37. Code-review invariants
|
|
1183
|
+
|
|
1184
|
+
Reject a change if it introduces any of the following without an explicit and verified design:
|
|
1185
|
+
|
|
1186
|
+
- automatic retries around `sms.send()`
|
|
1187
|
+
- provider URLs in logs
|
|
1188
|
+
- API keys in client code
|
|
1189
|
+
- browser-side provider calls
|
|
1190
|
+
- direct `process.env` reads inside the package
|
|
1191
|
+
- local phone-number guessing in the generic package
|
|
1192
|
+
- callbacks without any production ingress-authentication plan
|
|
1193
|
+
- success being equated with handset delivery
|
|
1194
|
+
- whole-batch retry after partial acceptance
|
|
1195
|
+
- swallowed callback persistence failures
|
|
1196
|
+
- untyped string matching on error messages
|
|
1197
|
+
- raw provider-body exposure
|
|
1198
|
+
- native fetch error exposure
|
|
1199
|
+
- framework dependency in the core package
|
|
1200
|
+
- database dependency in the package
|
|
1201
|
+
- queue retry policies that can duplicate sends
|
|
1202
|
+
- database transaction callbacks that may resend SMS on transaction retry
|
|
1203
|
+
- tests that require real provider credentials
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
## 38. Agent anti-patterns
|
|
1208
|
+
|
|
1209
|
+
### Anti-pattern: "Improve reliability with exponential backoff"
|
|
1210
|
+
|
|
1211
|
+
Do not add automatic resend logic to ambiguous failures.
|
|
1212
|
+
|
|
1213
|
+
### Anti-pattern: "Store API key in a public env var so the frontend can send"
|
|
1214
|
+
|
|
1215
|
+
Never expose the provider key to the client.
|
|
1216
|
+
|
|
1217
|
+
### Anti-pattern: "Normalize Swedish numbers by adding +46"
|
|
1218
|
+
|
|
1219
|
+
Not in this generic package. Use an explicit country-aware application layer.
|
|
1220
|
+
|
|
1221
|
+
### Anti-pattern: "The provider returned HTTP 200, so sending succeeded"
|
|
1222
|
+
|
|
1223
|
+
Wrong. Inspect normalized `messages` and `success`.
|
|
1224
|
+
|
|
1225
|
+
### Anti-pattern: "The API returned success, mark delivered"
|
|
1226
|
+
|
|
1227
|
+
Wrong. Acceptance and delivery are separate states.
|
|
1228
|
+
|
|
1229
|
+
### Anti-pattern: "Log the request URL to debug the provider"
|
|
1230
|
+
|
|
1231
|
+
Wrong. The URL contains secrets and content.
|
|
1232
|
+
|
|
1233
|
+
### Anti-pattern: "A callback parsed, so it came from Clickatell"
|
|
1234
|
+
|
|
1235
|
+
Wrong. Parsing validates shape, not authenticity.
|
|
1236
|
+
|
|
1237
|
+
### Anti-pattern: "Database write failed after send, rerun the function"
|
|
1238
|
+
|
|
1239
|
+
Potential duplicate. Separate persistence/reconciliation from resend behavior.
|
|
1240
|
+
|
|
1241
|
+
### Anti-pattern: "Queue failed, let BullMQ/SQS/etc. retry automatically"
|
|
1242
|
+
|
|
1243
|
+
Potential duplicate. Define safe retry semantics first.
|
|
1244
|
+
|
|
1245
|
+
---
|
|
1246
|
+
|
|
1247
|
+
## 39. Recommended production domain model
|
|
1248
|
+
|
|
1249
|
+
For systems that need reliable tracking, model an SMS as more than a boolean.
|
|
1250
|
+
|
|
1251
|
+
Example states:
|
|
1252
|
+
|
|
1253
|
+
```txt
|
|
1254
|
+
created
|
|
1255
|
+
send_attempt_started
|
|
1256
|
+
provider_accepted
|
|
1257
|
+
partially_accepted
|
|
1258
|
+
provider_rejected
|
|
1259
|
+
send_outcome_unknown
|
|
1260
|
+
delivery_pending
|
|
1261
|
+
delivered
|
|
1262
|
+
delivery_failed
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
This is an example, not a package API.
|
|
1266
|
+
|
|
1267
|
+
The key principle is to preserve uncertainty rather than converting every exception into `failed` and every acceptance into `delivered`.
|
|
1268
|
+
|
|
1269
|
+
A useful attempt record may contain:
|
|
1270
|
+
|
|
1271
|
+
```ts
|
|
1272
|
+
{
|
|
1273
|
+
id: string;
|
|
1274
|
+
clientMessageId: string;
|
|
1275
|
+
applicationMessageId: string;
|
|
1276
|
+
provider: "clickatell";
|
|
1277
|
+
startedAt: Date;
|
|
1278
|
+
completedAt?: Date;
|
|
1279
|
+
outcome: string;
|
|
1280
|
+
errorCode?: string;
|
|
1281
|
+
}
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
Store recipient/provider-message details in child records when sending to multiple recipients.
|
|
1285
|
+
|
|
1286
|
+
---
|
|
1287
|
+
|
|
1288
|
+
## 40. Recommended implementation workflow for AI agents
|
|
1289
|
+
|
|
1290
|
+
When asked to implement SMS in a new project, follow this sequence.
|
|
1291
|
+
|
|
1292
|
+
### Phase 1 — inspect
|
|
1293
|
+
|
|
1294
|
+
- identify runtime/framework
|
|
1295
|
+
- identify authentication layer
|
|
1296
|
+
- identify persistence layer
|
|
1297
|
+
- identify queue/retry behavior
|
|
1298
|
+
- identify where secrets live
|
|
1299
|
+
- identify how phone numbers are normalized
|
|
1300
|
+
- identify whether status/reply callbacks are needed
|
|
1301
|
+
|
|
1302
|
+
### Phase 2 — integrate core service
|
|
1303
|
+
|
|
1304
|
+
- install `@doeza/sms-service`
|
|
1305
|
+
- add a server-only service adapter
|
|
1306
|
+
- pass credentials explicitly
|
|
1307
|
+
- reuse one instance per credential set
|
|
1308
|
+
- choose a timeout
|
|
1309
|
+
|
|
1310
|
+
### Phase 3 — add product boundary
|
|
1311
|
+
|
|
1312
|
+
- enforce auth/authz
|
|
1313
|
+
- resolve recipient server-side when possible
|
|
1314
|
+
- render approved templates server-side
|
|
1315
|
+
- apply rate limits/quotas
|
|
1316
|
+
- assign stable correlation IDs
|
|
1317
|
+
|
|
1318
|
+
### Phase 4 — persistence
|
|
1319
|
+
|
|
1320
|
+
- persist send intent when required
|
|
1321
|
+
- preserve per-recipient acceptance
|
|
1322
|
+
- model uncertain outcomes
|
|
1323
|
+
- avoid transaction/queue retry duplication
|
|
1324
|
+
|
|
1325
|
+
### Phase 5 — callbacks
|
|
1326
|
+
|
|
1327
|
+
- configure trusted callback URLs
|
|
1328
|
+
- implement ingress verification
|
|
1329
|
+
- use idempotent persistence
|
|
1330
|
+
- handle out-of-order status events
|
|
1331
|
+
|
|
1332
|
+
### Phase 6 — observability
|
|
1333
|
+
|
|
1334
|
+
- add structured safe metadata
|
|
1335
|
+
- redact URLs, keys, bodies, and sensitive recipient data
|
|
1336
|
+
- add operational metrics
|
|
1337
|
+
|
|
1338
|
+
### Phase 7 — tests
|
|
1339
|
+
|
|
1340
|
+
- test happy path
|
|
1341
|
+
- test partial result
|
|
1342
|
+
- test malformed provider response
|
|
1343
|
+
- test network/timeout ambiguity
|
|
1344
|
+
- assert no automatic retry
|
|
1345
|
+
- test callback duplicates and persistence failures
|
|
1346
|
+
|
|
1347
|
+
### Phase 8 — review
|
|
1348
|
+
|
|
1349
|
+
Run the checklist in this document before completion.
|
|
1350
|
+
|
|
1351
|
+
---
|
|
1352
|
+
|
|
1353
|
+
## 41. Master prompt for an implementation agent
|
|
1354
|
+
|
|
1355
|
+
Copy this prompt when assigning an AI agent to integrate the package into another project.
|
|
1356
|
+
|
|
1357
|
+
```text
|
|
1358
|
+
You are the senior backend engineer responsible for integrating @doeza/sms-service into this application.
|
|
1359
|
+
|
|
1360
|
+
Treat SMS sending as a side effect with financial cost and ambiguous network-failure semantics.
|
|
1361
|
+
|
|
1362
|
+
Before changing code, inspect the application's runtime, authentication, authorization, persistence, queue/retry configuration, phone-number model, logging/APM setup, and existing notification architecture. Inspect the installed @doeza/sms-service README, type declarations, implementation, and tests. Do not invent package APIs.
|
|
1363
|
+
|
|
1364
|
+
Non-negotiable rules:
|
|
1365
|
+
1. Keep the package server-only. Never expose provider credentials to client code.
|
|
1366
|
+
2. Pass the Clickatell API key explicitly from the host application's secret/config layer. Do not modify the package to read environment variables.
|
|
1367
|
+
3. Reuse one service instance per credential/configuration set.
|
|
1368
|
+
4. Do not automatically retry sms.send() after timeout, network error, cancellation, malformed response, or other ambiguous transport failure. The provider may already have accepted the SMS.
|
|
1369
|
+
5. clientMessageId is correlation data, not an idempotency guarantee.
|
|
1370
|
+
6. Treat result.success as provider acceptance, not handset delivery.
|
|
1371
|
+
7. Inspect per-recipient result.messages and preserve partial acceptance. Never resend an entire partially accepted batch.
|
|
1372
|
+
8. Never log full Clickatell request URLs, query strings, API keys, raw native fetch errors, or message bodies. The provider request URL contains sensitive query parameters.
|
|
1373
|
+
9. Do not guess country codes. Supply international-format numbers to the package from a country-aware application layer.
|
|
1374
|
+
10. Authentication, authorization, rate limiting, quotas, consent/business rules, templates, persistence, and audit logging belong to the host application.
|
|
1375
|
+
11. Callback parsers validate payload shape; they do not authenticate webhook origin. Add a real ingress-verification strategy before production exposure.
|
|
1376
|
+
12. Make callback persistence idempotent and able to tolerate duplicate/out-of-order callbacks.
|
|
1377
|
+
13. Await durable callback processing before acknowledging success when provider retries are needed.
|
|
1378
|
+
14. If a database transaction or queue can retry application code, ensure it cannot duplicate an SMS send.
|
|
1379
|
+
15. Use SmsError.code for control flow. Do not parse error strings.
|
|
1380
|
+
16. Use injected fetch/mocks for tests; automated tests must not require real provider credentials or network access.
|
|
1381
|
+
17. Preserve the package's Node 22+, ESM, zero-runtime-dependency design unless explicitly asked to redesign the package itself.
|
|
1382
|
+
|
|
1383
|
+
Implementation expectations:
|
|
1384
|
+
- Create a thin server-side application adapter around createSmsService.
|
|
1385
|
+
- Use createSendSmsHandler only when standard Web Request/Response integration is appropriate; otherwise call sms.send() directly from the framework's server layer.
|
|
1386
|
+
- Add explicit auth/authz and rate limits around any send endpoint.
|
|
1387
|
+
- Prefer server-side recipient/template selection over accepting arbitrary public to/content values.
|
|
1388
|
+
- Add structured, redacted observability using correlation IDs and outcome categories.
|
|
1389
|
+
- Model acceptance, delivery, rejection, and unknown outcomes distinctly if persisted.
|
|
1390
|
+
- Implement callback storage with deduplication and ordering rules when delivery/reply tracking is required.
|
|
1391
|
+
- Cover success, partial acceptance, validation, provider rejection, malformed provider responses, timeout, network failure, cancellation, secret redaction, and no-retry behavior in tests.
|
|
1392
|
+
|
|
1393
|
+
Before completing the task, review every changed file for credential exposure, accidental retries, incorrect delivery semantics, weak callback authentication, unsafe logging, and framework/client-boundary violations. Report any assumptions that could not be verified from the codebase.
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
---
|
|
1397
|
+
|
|
1398
|
+
## 42. Master prompt for an agent modifying the package itself
|
|
1399
|
+
|
|
1400
|
+
```text
|
|
1401
|
+
You are maintaining @doeza/sms-service, a standalone server-side Clickatell SMS package.
|
|
1402
|
+
|
|
1403
|
+
Before editing, read package.json, index.js, index.d.ts, http.js, http.d.ts, README.md, AGENTS.md, and the complete test suite.
|
|
1404
|
+
|
|
1405
|
+
Preserve these architectural invariants unless the task explicitly authorizes changing them:
|
|
1406
|
+
- Node.js 22+
|
|
1407
|
+
- ESM
|
|
1408
|
+
- zero runtime dependencies
|
|
1409
|
+
- no framework dependency
|
|
1410
|
+
- no database dependency
|
|
1411
|
+
- no environment loader
|
|
1412
|
+
- explicit configuration injection
|
|
1413
|
+
- server-side use only
|
|
1414
|
+
- normalized SmsError failures
|
|
1415
|
+
- no automatic send retries
|
|
1416
|
+
- provider acceptance kept distinct from delivery
|
|
1417
|
+
- strict provider-response validation
|
|
1418
|
+
- no raw provider/native-fetch error exposure
|
|
1419
|
+
- no logging inside the package
|
|
1420
|
+
- Web Request/Response HTTP adapter kept optional under the ./http export
|
|
1421
|
+
|
|
1422
|
+
For every public behavior change, update implementation, TypeScript declarations, tests, README, and AGENTS.md where applicable.
|
|
1423
|
+
|
|
1424
|
+
Security requirements:
|
|
1425
|
+
- never expose API keys or message content in errors
|
|
1426
|
+
- never expose native fetch errors containing credential-bearing URLs
|
|
1427
|
+
- preserve redirect refusal unless a verified provider change requires otherwise
|
|
1428
|
+
- preserve explicit callback URL validation
|
|
1429
|
+
- do not claim webhook authenticity from parser validation
|
|
1430
|
+
|
|
1431
|
+
Reliability requirements:
|
|
1432
|
+
- do not introduce automatic retries unless the provider's current API supplies a documented idempotency mechanism and the change includes a complete idempotency design and regression tests
|
|
1433
|
+
- preserve per-recipient result handling and partial acceptance
|
|
1434
|
+
- fail closed on malformed/incomplete/mismatched provider responses
|
|
1435
|
+
- keep timeout and cancellation behavior explicit
|
|
1436
|
+
|
|
1437
|
+
Testing requirements:
|
|
1438
|
+
- no real credentials
|
|
1439
|
+
- no required network access
|
|
1440
|
+
- use injected Fetch implementations
|
|
1441
|
+
- test all new branches
|
|
1442
|
+
- include secret-redaction regression tests for error paths
|
|
1443
|
+
- include no-retry assertions for ambiguous send failures
|
|
1444
|
+
|
|
1445
|
+
Do not add abstraction or dependencies unless they materially improve maintainability and preserve the package's standalone nature.
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
---
|
|
1449
|
+
|
|
1450
|
+
## 43. Review prompt for AI code reviewers
|
|
1451
|
+
|
|
1452
|
+
```text
|
|
1453
|
+
Review this SMS integration as a production backend/security reviewer.
|
|
1454
|
+
|
|
1455
|
+
Do not focus only on style. Find correctness and operational risks.
|
|
1456
|
+
|
|
1457
|
+
Check specifically for:
|
|
1458
|
+
- browser/client exposure of @doeza/sms-service or CLICKATELL_API_KEY
|
|
1459
|
+
- provider URLs, API keys, message bodies, or raw fetch errors entering logs/traces
|
|
1460
|
+
- automatic retries around sms.send()
|
|
1461
|
+
- queue retries that can resend SMS
|
|
1462
|
+
- database transaction retries that can resend SMS
|
|
1463
|
+
- misuse of clientMessageId as idempotency
|
|
1464
|
+
- result.success incorrectly treated as handset delivery
|
|
1465
|
+
- partial recipient acceptance being ignored
|
|
1466
|
+
- entire-batch resend after partial acceptance
|
|
1467
|
+
- local-number country-code guessing in generic infrastructure
|
|
1468
|
+
- missing authentication/authorization on send routes
|
|
1469
|
+
- accepting arbitrary recipient/content from untrusted callers without product policy
|
|
1470
|
+
- missing rate limits/quotas
|
|
1471
|
+
- missing callback sender verification
|
|
1472
|
+
- non-idempotent callback persistence
|
|
1473
|
+
- callback consumers acknowledged before durable write
|
|
1474
|
+
- out-of-order delivery events overwriting newer state incorrectly
|
|
1475
|
+
- string matching on SmsError.message instead of SmsError.code
|
|
1476
|
+
- raw provider response exposure
|
|
1477
|
+
- weakened malformed-response validation
|
|
1478
|
+
- real network/provider calls in unit tests
|
|
1479
|
+
- package API/type declaration drift
|
|
1480
|
+
|
|
1481
|
+
Classify findings by severity and explain the duplicate-send, credential-exposure, data-integrity, or delivery-state failure mode for each finding. Suggest the smallest safe correction.
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1484
|
+
---
|
|
1485
|
+
|
|
1486
|
+
## 44. Migration prompt for existing SMS code
|
|
1487
|
+
|
|
1488
|
+
```text
|
|
1489
|
+
Migrate the existing SMS implementation to @doeza/sms-service without changing product behavior unnecessarily.
|
|
1490
|
+
|
|
1491
|
+
First inventory:
|
|
1492
|
+
- existing provider endpoint and parameters
|
|
1493
|
+
- credential source
|
|
1494
|
+
- number normalization
|
|
1495
|
+
- templates/content generation
|
|
1496
|
+
- retries/backoff
|
|
1497
|
+
- queue behavior
|
|
1498
|
+
- persistence schema
|
|
1499
|
+
- delivery callbacks
|
|
1500
|
+
- incoming replies
|
|
1501
|
+
- auth/authz
|
|
1502
|
+
- rate limits
|
|
1503
|
+
- logs/APM
|
|
1504
|
+
- user-facing statuses
|
|
1505
|
+
|
|
1506
|
+
Then map each responsibility either to @doeza/sms-service or to the host application. Do not move product-specific concerns into the package.
|
|
1507
|
+
|
|
1508
|
+
Pay special attention to legacy retry logic. Remove or redesign any automatic retry that could duplicate sends after ambiguous failures. Preserve existing correlation IDs where possible, but do not treat them as provider idempotency keys.
|
|
1509
|
+
|
|
1510
|
+
Preserve per-recipient acceptance and distinguish provider acceptance from delivery.
|
|
1511
|
+
|
|
1512
|
+
Add regression tests before deleting the old transport. Ensure no provider URL/API key/message content is logged by the new integration.
|
|
1513
|
+
|
|
1514
|
+
Complete the migration with a clear list of behavior intentionally preserved, behavior intentionally changed, and unresolved provider assumptions.
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
---
|
|
1518
|
+
|
|
1519
|
+
## 45. Incident-debugging prompt
|
|
1520
|
+
|
|
1521
|
+
```text
|
|
1522
|
+
Investigate this SMS incident without causing duplicate sends.
|
|
1523
|
+
|
|
1524
|
+
Do not resend anything as an investigative step.
|
|
1525
|
+
|
|
1526
|
+
Collect and correlate only safe metadata:
|
|
1527
|
+
- application message/notification ID
|
|
1528
|
+
- clientMessageId
|
|
1529
|
+
- provider apiMessageId when available
|
|
1530
|
+
- timestamps
|
|
1531
|
+
- recipient-safe identifier/masked number
|
|
1532
|
+
- SmsError.code
|
|
1533
|
+
- HTTP status when available
|
|
1534
|
+
- delivery callback history
|
|
1535
|
+
- queue/job attempt history
|
|
1536
|
+
|
|
1537
|
+
Do not log or paste API keys, full Clickatell request URLs, query strings, or message bodies.
|
|
1538
|
+
|
|
1539
|
+
Determine whether the state is:
|
|
1540
|
+
- provider accepted
|
|
1541
|
+
- provider rejected
|
|
1542
|
+
- partially accepted
|
|
1543
|
+
- delivery pending
|
|
1544
|
+
- delivered
|
|
1545
|
+
- delivery failed
|
|
1546
|
+
- unknown/ambiguous
|
|
1547
|
+
|
|
1548
|
+
For timeout/network/cancellation/malformed-response incidents, assume the send outcome may be unknown until provider state is reconciled. Do not infer non-acceptance from a local exception.
|
|
1549
|
+
|
|
1550
|
+
Identify whether queue/database retry behavior could have produced duplicates. Provide remediation that prevents recurrence before recommending any resend policy.
|
|
1551
|
+
```
|
|
1552
|
+
|
|
1553
|
+
---
|
|
1554
|
+
|
|
1555
|
+
## 46. Completion checklist
|
|
1556
|
+
|
|
1557
|
+
An AI agent must not mark an SMS integration complete until it can answer yes to all applicable items.
|
|
1558
|
+
|
|
1559
|
+
### Architecture
|
|
1560
|
+
|
|
1561
|
+
- Is the package used only on the server?
|
|
1562
|
+
- Is configuration injected explicitly?
|
|
1563
|
+
- Is the service instance reused appropriately?
|
|
1564
|
+
- Are host-specific concerns outside the package?
|
|
1565
|
+
|
|
1566
|
+
### Security
|
|
1567
|
+
|
|
1568
|
+
- Is the provider API key absent from client code?
|
|
1569
|
+
- Are full provider URLs/query strings excluded from logs and traces?
|
|
1570
|
+
- Are raw native fetch errors excluded from user/log output?
|
|
1571
|
+
- Are message bodies excluded from routine logs?
|
|
1572
|
+
- Is any send endpoint authenticated and authorized?
|
|
1573
|
+
- Are abuse/rate-limit controls present where required?
|
|
1574
|
+
- Is callback-origin verification handled or explicitly blocked from production until handled?
|
|
1575
|
+
|
|
1576
|
+
### Correctness
|
|
1577
|
+
|
|
1578
|
+
- Are phone numbers international before reaching the package?
|
|
1579
|
+
- Is no generic country-code guessing performed?
|
|
1580
|
+
- Is `clientMessageId` treated only as correlation?
|
|
1581
|
+
- Is acceptance distinguished from delivery?
|
|
1582
|
+
- Are per-recipient results processed?
|
|
1583
|
+
- Is partial acceptance handled without whole-batch resend?
|
|
1584
|
+
- Are malformed provider responses treated as failures?
|
|
1585
|
+
|
|
1586
|
+
### Reliability
|
|
1587
|
+
|
|
1588
|
+
- Are automatic send retries absent unless a verified idempotency design exists?
|
|
1589
|
+
- Are queue retries safe?
|
|
1590
|
+
- Are database transaction retries safe?
|
|
1591
|
+
- Are ambiguous failures represented as unknown/reconciliation states where persistence exists?
|
|
1592
|
+
- Are callback writes idempotent?
|
|
1593
|
+
- Are out-of-order callback events handled?
|
|
1594
|
+
|
|
1595
|
+
### Testing
|
|
1596
|
+
|
|
1597
|
+
- Are unit tests network-independent?
|
|
1598
|
+
- Is `fetch` mocked/injected?
|
|
1599
|
+
- Are success, partial, rejection, timeout, cancellation, network, malformed response, and validation paths covered?
|
|
1600
|
+
- Is no-retry behavior asserted?
|
|
1601
|
+
- Is secret redaction asserted?
|
|
1602
|
+
- Are callback duplicate/failure cases covered?
|
|
1603
|
+
|
|
1604
|
+
### Documentation
|
|
1605
|
+
|
|
1606
|
+
- Are new public APIs reflected in `.d.ts`?
|
|
1607
|
+
- Is README documentation current?
|
|
1608
|
+
- Is this agent guide still accurate?
|
|
1609
|
+
|
|
1610
|
+
---
|
|
1611
|
+
|
|
1612
|
+
## 47. Final agent rule
|
|
1613
|
+
|
|
1614
|
+
When uncertain whether an operation is safe to repeat, treat it as non-repeatable until you have evidence otherwise.
|
|
1615
|
+
|
|
1616
|
+
For SMS, duplicate prevention and secret protection are more important than hiding an operational failure with a retry.
|