@distyra/sdk 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,8 +36,9 @@ if (error) {
36
36
  throw new Error(error.detail ?? error.error);
37
37
  }
38
38
 
39
- // `data` is fully typed against the EnrichResponse schema
40
- console.log(data.merchant.name, data.category.primary);
39
+ // `data` is fully typed against the EnrichResponse schema. `merchant` is null
40
+ // for non-merchant transactions (bank fees, transfers), so guard it.
41
+ console.log(data.merchant?.name ?? '(no merchant)', data.category.primary);
41
42
  ```
42
43
 
43
44
  ## Options
@@ -52,6 +53,67 @@ console.log(data.merchant.name, data.category.primary);
52
53
 
53
54
  The returned client is a typed [`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/) client — use `.GET`, `.POST`, etc. against any path in the spec, with full request/response typing.
54
55
 
56
+ ## Handling unknown merchants (eventual consistency)
57
+
58
+ The first time anyone enriches a brand-new merchant, the response may be unresolved
59
+ (`resolution.status` other than `resolved`). The system then discovers and confirms that
60
+ merchant in the background, so the same descriptor resolves shortly after, for every
61
+ caller, with no model call.
62
+
63
+ Read `resolution.retry_after`:
64
+
65
+ - A number (seconds) means a confirmed answer is likely coming. Retry the same descriptor
66
+ once after that delay, or just let your next natural re-sync pick it up.
67
+ - `null` means the result is definitive; no retry will help.
68
+
69
+ ```ts
70
+ const { data } = await distyra.POST('/v1/enrich', { body: { descriptor, country_hint } });
71
+ if (data.resolution.status !== 'resolved' && data.resolution.retry_after != null) {
72
+ // retry once after data.resolution.retry_after seconds, or wait for your next sync
73
+ }
74
+ ```
75
+
76
+ Note: `resolution.retryable` is a different signal. It is `true` only for a transient
77
+ backend error (`timed_out` / `transient_error`), which you retry within seconds. See the
78
+ full resolution-and-retries guide at <https://api.distyra.com/docs>.
79
+
80
+ ## Verifying webhooks
81
+
82
+ Distyra signs every webhook delivery with a Stripe-shape header:
83
+
84
+ ```
85
+ X-Distyra-Signature: t=<unix>,v1=<hmac_sha256_hex>
86
+ ```
87
+
88
+ `verifyWebhook` checks it (HMAC-SHA256 over `"<t>.<rawBody>"`, keyed by the
89
+ endpoint's signing secret, with replay protection). Always verify against the
90
+ **raw request body**, before JSON parsing — re-serializing changes the bytes and
91
+ breaks the signature.
92
+
93
+ ```ts
94
+ import { verifyWebhook } from '@distyra/sdk';
95
+
96
+ // Express — capture the raw body, e.g. app.use(express.raw({ type: 'application/json' }))
97
+ app.post('/webhooks/distyra', (req, res) => {
98
+ const raw = req.body.toString('utf8');
99
+ const result = verifyWebhook(
100
+ process.env.DISTYRA_WEBHOOK_SECRET!,
101
+ raw,
102
+ req.header('X-Distyra-Signature'),
103
+ );
104
+ if (!result.ok) return res.status(400).send(result.reason);
105
+
106
+ const event = JSON.parse(raw);
107
+ // ... handle event ...
108
+ res.sendStatus(200);
109
+ });
110
+ ```
111
+
112
+ It returns `{ ok: true, timestamp }` or `{ ok: false, reason }` where `reason` is
113
+ `'malformed'`, `'stale'`, or `'no_match'`. The default replay window is 5 minutes
114
+ (`DEFAULT_TOLERANCE_SECONDS`); override it with `{ toleranceSeconds }`. Uses
115
+ `node:crypto`, so it runs server-side (Node 18+).
116
+
55
117
  ## Lower-level access
56
118
 
57
119
  The generated `paths` and `operations` types are re-exported if you want to build your own `openapi-fetch` client or reference response shapes directly: