{"_id":"@astratra/payments","_rev":"3-e0595d9df1b0bffaac583b3cae8377fb","name":"@astratra/payments","dist-tags":{"latest":"1.0.0"},"versions":{"0.1.0":{"name":"@astratra/payments","version":"0.1.0","keywords":["astratra","payments","webhook","stripe","idempotency"],"license":"MIT","_id":"@astratra/payments@0.1.0","maintainers":[{"name":"emch99","email":"emchkongo@gmail.com"}],"dist":{"shasum":"1b38384bb17054a5f40b8c867e0e995f83d3e863","tarball":"https://registry.npmjs.org/@astratra/payments/-/payments-0.1.0.tgz","fileCount":9,"integrity":"sha512-En4oVojf33QHCcUSOcKLAU88D72dwUrdGbqeyLmdHEBQKCzxyKCuJ90RBTLbT053Sxh49qs1lDweuL6XrbOY7A==","signatures":[{"sig":"MEUCIGXzFI5QAWmQGdpoEP7FlCkvo9OgWt8TDbe+UED9CdZOAiEA6GtzbLfDe6g9eW1X42ededjpBDGORaRBihVAbe+Xz8I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":22936},"jest":{"testMatch":["**/__tests__/**/*.test.js"]},"main":"src/index.js","type":"commonjs","types":"src/index.d.ts","engines":{"node":">=20"},"gitHead":"a3f7c737294561ba8651e14d507a18b41e199999","scripts":{"test":"jest"},"_npmUser":{"name":"emch99","email":"emchkongo@gmail.com"},"_npmVersion":"11.13.0","description":"The payment webhook pipeline done once: raw-body signature checks, replay protection, and the answers that stop a provider retrying for days.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"30.4.2","express":"^4.18.3","supertest":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/payments_0.1.0_1787666104306_0.6462006675621768","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@astratra/payments","version":"0.2.0","keywords":["astratra","payments","webhook","stripe","idempotency"],"license":"MIT","_id":"@astratra/payments@0.2.0","maintainers":[{"name":"emch99","email":"emchkongo@gmail.com"}],"dist":{"shasum":"78b35b6af8d0278c7d27ea296dfe51bfcb9dc151","tarball":"https://registry.npmjs.org/@astratra/payments/-/payments-0.2.0.tgz","fileCount":10,"integrity":"sha512-sp4GDLMKpvZcQiXnE15271cwkTEKVGGdCxtFvYNCbPDv0iiJQZQnT5kypGwXIQYBRWNcUwriWEv2q5CUtEEtJg==","signatures":[{"sig":"MEQCIAkuZ8grWgM7nf7Q6yf5/EdqFFZsxQZ4j/mpxGJO29+MAiAAxmvNHx2kTHo6usukKLJGaPkvUB6raNt+Mr6g9IBRlg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":30523},"jest":{"testMatch":["**/__tests__/**/*.test.js"]},"main":"src/index.js","type":"commonjs","types":"src/index.d.ts","engines":{"node":">=20"},"gitHead":"597c98161c5fc076a7575eaa0a3bd3cdfd5cb132","scripts":{"test":"jest"},"_npmUser":{"name":"emch99","email":"emchkongo@gmail.com"},"_npmVersion":"11.13.0","description":"The payment webhook pipeline done once: raw-body signature checks, replay protection, and the answers that stop a provider retrying for days.","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"30.4.2","express":"^4.18.3","supertest":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/payments_0.2.0_1787768556976_0.7801563447882665","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"_id":"@astratra/payments@1.0.0","dist":{"shasum":"96897408c1e3b6fd242714a8e2dd6a56499e7e9c","tarball":"https://registry.npmjs.org/@astratra/payments/-/payments-1.0.0.tgz","fileCount":10,"integrity":"sha512-QG9+ids0NtfImFMfB7VU5IxNNq5ckX1ZxlN34rrKYpC9V1qWMdohG7VIEYaFcvXQ7qZnvL5hlOY309anae60Jw==","signatures":[{"sig":"MEUCIFUldJZ2z5bkp+gB5u+vuBFf2ha1g9H+bR7oJ3cJTuXJAiEAwRRP5gnPa+dGkkwhzeCophMHjbm3AekH5/Knya7A6ug=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICDn6wkcLGRX4qM11+h7GSTQu4EQj9WI7qzbP8sdunyoAiAIZ5koWY72vlXJOAFG2eq9qLDPZUqdQ/oehVfEZG4Avg=="}],"unpackedSize":30523},"jest":{"testMatch":["**/__tests__/**/*.test.js"]},"main":"src/index.js","name":"@astratra/payments","type":"commonjs","types":"src/index.d.ts","engines":{"node":">=20"},"gitHead":"28e680aaed8dfdf7c5aa00c7dfbde74ab8e377de","license":"MIT","scripts":{"test":"jest"},"version":"1.0.0","_npmUser":{"name":"emch99","email":"emchkongo@gmail.com"},"keywords":["astratra","payments","webhook","stripe","idempotency"],"_npmVersion":"11.13.0","description":"The payment webhook pipeline done once: raw-body signature checks, replay protection, and the answers that stop a provider retrying for days.","directories":{},"maintainers":[{"name":"emch99","email":"emchkongo@gmail.com"}],"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"30.4.2","express":"^4.18.3","supertest":"^7.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/payments_1.0.0_1790882572397_0.29875039541376247"}}},"time":{"created":"2026-08-25T13:55:04.216Z","modified":"2026-10-01T19:22:52.706Z","0.1.0":"2026-08-25T13:55:04.465Z","0.2.0":"2026-08-26T18:22:37.134Z","1.0.0":"2026-10-01T19:22:52.516Z"},"license":"MIT","keywords":["astratra","payments","webhook","stripe","idempotency"],"description":"The payment webhook pipeline done once: raw-body signature checks, replay protection, and the answers that stop a provider retrying for days.","maintainers":[{"name":"emch99","email":"emchkongo@gmail.com"}],"readme":"# @astratra/payments\n\nLe tuyau qu'il faut à tout webhook de paiement, et que tout le monde réécrit mal.\n\nCe package n'importe **aucun SDK de paiement**. Tu lui passes ta fonction de\nvérification de signature ; il s'occupe de tout ce qui l'entoure, c'est-à-dire de\ntout ce qui casse en silence.\n\n## Le côté écran : quel plan, quelle page, quelle confirmation\n\nDeux passerelles nomment les mêmes choses différemment et répondent\ndifféremment. L'écran ne doit pas savoir laquelle est derrière.\n\n```js\nconst flow = createCheckoutFlow({\n  notPurchasable: ['trial'],\n  confirms: {\n    stripe: () => true,\n    cinetpay: (payload) => payload?.success === true\n  }\n});\n\nflow.planAction('pro', 'starter');     // 'choose' | 'current' | 'locked'\nreadCheckoutUrl(payload);              // checkoutUrl ?? paymentUrl ?? url\n```\n\n**Un plan peut être accordé plutôt que vendu.** L'essai est donné à la création\ndu compte ; proposer de l'acheter produit une erreur côté serveur et de\nl'incompréhension côté personne.\n\n**Un compte sans rien à facturer doit se l'entendre dire.** Le bouton de\npaiement vérifiait « pas d'identifiant » puis sortait : aucune requête, aucun\nmessage, rien. Un bouton qui ne fait rien passe pour une panne, alors que c'est\nune situation parfaitement normale.\n\n**« En attente » n'est pas « échoué ».** Quand la boucle de confirmation\nabandonne, le paiement est peut-être passé : le webhook tranchera. Dire à\nquelqu'un que son paiement a échoué pendant que son argent est en route est la\npire réponse possible.\n\n**Une passerelle non déclarée n'est jamais confirmée** — le silence ne doit\njamais valoir un accès.\n\n## Les quatre pièges\n\nChacun tue des paiements pendant que le code a l'air correct.\n\n**1. Le corps brut.** La vérification de signature porte sur les octets exacts\nqu'a signés le prestataire. Un analyseur JSON les réécrit, la vérification\néchoue alors même avec le bon secret — et l'erreur dit « signature invalide »,\nce qui t'envoie chercher le mauvais bug.\n\n**2. La pile de middlewares.** Le CSRF renvoie 403 à un prestataire qui n'a\naucun cookie à envoyer. Un garde d'authentification rejette un appelant qui\nn'est pas un utilisateur. Un limiteur étrangle une rafale de relances. Chaque\ncouche doit exempter le webhook, et en oublier une est invisible jusqu'à ce que\nde l'argent disparaisse.\n\n**3. Répondre 404 à ce qui n'est pas pour soi.** Les webhooks sont à l'échelle\ndu **compte** : un seul point d'entrée reçoit les événements de tous les flux du\ncompte. Un 404 fait relancer le prestataire pendant des jours et finit par\nmarquer la destination défaillante — pour des événements qui n'étaient pas les\ntiens.\n\n**4. Un effet de bord qui fait échouer le webhook.** L'argent est encaissé, la\ncommande confirmée ; si l'e-mail de confirmation lève ensuite, renvoyer 500\ndemande au prestataire de rejouer tout l'événement.\n\nCe package traite les pièges 1, 3 et 4. Le 2 est `createWebhookExemption`, parce\nque ton application est seule à connaître ses propres middlewares.\n\n## Le tuyau\n\n```js\nconst { createWebhookHandler, createMemoryEventLog } = require('@astratra/payments');\n\nconst webhook = createWebhookHandler({\n  // À toi, parce que c'est propre au prestataire. DOIT lever si la signature\n  // ne correspond pas.\n  verify: ({ payload, headers, secret }) =>\n    stripe.webhooks.constructEvent(payload, headers['stripe-signature'], secret),\n\n  // Une fonction, donc relue à chaque appel : le secret se change depuis une\n  // interface sans redémarrage. Se marie avec @astratra/credentials.\n  secret: () => vault.get('STRIPE_WEBHOOK_SECRET'),\n\n  eventLog,   // protection contre les rejeux\n\n  events: {\n    'checkout.session.completed': async (event, { sideEffect, unrelated }) => {\n      const order = await orders.findBySession(event.data.object.id);\n\n      // Piège 3 : cet événement appartient à un autre flux du même compte.\n      if (!order) return unrelated('cette session n\\'est pas une commande');\n\n      await orders.confirm(order.id);\n\n      // Piège 4 : l'argent est déjà pris. Un envoi raté ne doit pas\n      // provoquer une relance.\n      await sideEffect('e-mail de confirmation', () => mail.sendReceipt(order));\n    },\n  },\n});\n\napp.post('/api/payments/webhook', express.raw({ type: 'application/json' }), webhook.middleware);\n```\n\n### Ce que chaque réponse veut dire\n\nLe code HTTP n'est pas décoratif : le prestataire le lit comme une consigne.\n\n| Situation | Réponse | Pourquoi |\n|---|---|---|\n| traité | 200 | terminé |\n| type d'événement inconnu | 200 | acquitté, pas une erreur |\n| `unrelated(...)` | 200 | « reçu, pas mon circuit » |\n| déjà traité | 200 | rejeu reconnu |\n| signature invalide | **400** | relancer ne changerait rien |\n| le gestionnaire a levé | **500** | là, une relance peut aider |\n\nLe 400 sur signature invalide est délibéré. Un 500 ferait relancer un message\nque le prestataire a mal signé, ou que tu ne sais pas vérifier — indéfiniment.\n\n## L'exemption partagée\n\nUn seul prédicat, utilisé par toutes les couches, ne peut pas diverger.\n\n```js\nconst { createWebhookExemption } = require('@astratra/payments');\n\nconst isWebhook = createWebhookExemption({ prefix: '/api/finance/', suffix: '/webhook' });\n\napp.use((req, res, next) => isWebhook(req) ? next() : express.json()(req, res, next));\napp.use(csrf({ skip: isWebhook }));\napp.use(subscriptionGuard({ skip: isWebhook }));\n```\n\nLe préfixe **et** le suffixe ensemble : un préfixe seul exempterait toute une\nsection de l'API — une zone de facturation entière sans CSRF ni\nauthentification.\n\nDans un cas réel, quatre couches exemptaient chacune un webhook et en oubliaient\nun second. Chaque paiement de ce flux mourait avant d'atteindre le code censé\nl'enregistrer, et rien ne journalisait d'erreur.\n\n## Les rejeux\n\nLes prestataires renvoient les événements, volontairement : ils ne peuvent pas\ndistinguer une réponse perdue d'une réponse lente. Sans mémoire, la seconde\nlivraison reconfirme la commande, renvoie le reçu, recrédite le compte.\n\n```\nseen(eventId)          -> boolean\nrecord(eventId, meta)  -> void\n```\n\n`createMemoryEventLog()` sert aux tests et au développement. **Il n'est pas sûr\nen production multi-instances** : chaque processus a sa propre mémoire, donc un\nrejeu qui arrive sur l'autre passe. Utilise un store partagé.\n\nUn événement **en échec n'est pas enregistré** : la relance doit pouvoir\nfonctionner. Un événement déclaré `unrelated` non plus.\n\n## Ce que ce package ne fait pas\n\n- Il n'importe **aucun SDK** — ni Stripe, ni autre.\n- Il ne crée pas de session de paiement et ne rembourse rien : ces appels sont\n  propres au prestataire et tiennent en trois lignes chez toi.\n- Il ne stocke pas tes commandes.\n- Aucune dépendance à l'exécution.\n\n## Tests\n\n```bash\nnpm test --workspace @astratra/payments\n```\n\nUn test traverse une vraie pile Express avec CSRF et analyseur JSON, et vérifie\nque les octets signés arrivent intacts — puis que sans l'exemption, le CSRF\nrépond bien 403.\n","readmeFilename":"README.md"}