@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 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.