@klappay/types 1.0.0 → 1.0.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.
@@ -0,0 +1,108 @@
1
+ # Tokens and networks
2
+
3
+ Exported from `tokens.ts` and `networks.ts`.
4
+
5
+ ## `Token`
6
+
7
+ `TokenSchema`/`Token` — currently `'USDC' | 'USDT'`. This is today's
8
+ supported list, not a permanent ceiling — more tokens are expected to be
9
+ added over time as new networks and assets come online. Support depends
10
+ on both `network` and `environment`, and not every combination is
11
+ symmetric: `live` has both `USDC` and `USDT` on every operational
12
+ network *except* `bnb` (see the `USDC`/`bnb` note below), and `test`
13
+ coverage varies per network for different reasons — `base`, `optimism`,
14
+ and `ethereum` each have a `test` environment with `USDC` only (none has
15
+ an official Sepolia USDT, since Tether doesn't issue one), while
16
+ `arbitrum`, `polygon`, and `avalanche` have no `test` environment at all
17
+ yet, for any token (0xSplits hasn't deployed its split factory on
18
+ Arbitrum Sepolia, and has no Polygon or Avalanche Fuji testnet support at
19
+ all — a network-level gap, not a token-level one), and `bnb` has none
20
+ either (no BNB testnet support in 0xSplits). An unconfigured combination
21
+ is rejected with `422 token_not_supported`, not silently accepted. See
22
+ `TOKEN_ADDRESSES` below for the exact current matrix rather than
23
+ assuming full coverage.
24
+
25
+ `TOKEN_DECIMALS` (`6`) — both tokens Klap supports use 6 decimals on
26
+ every network they're deployed on. A fact about these specific tokens,
27
+ not a per-network setting, so it's one shared constant rather than
28
+ something you'd look up per-`Network`.
29
+
30
+ `TOKEN_ADDRESSES: Record<Token, Partial<Record<Network, Partial<Record<Environment, `0x${string}`>>>>>`
31
+ — the real on-chain contract address for each token/network/environment
32
+ combination that's actually deployed. Three levels deep because `test`
33
+ and `live` are genuinely different chains for at least one network today
34
+ (`base` → Base mainnet for `live`, Base Sepolia for `test` — see
35
+ [`charges.md`](./charges.md) for `Environment`), so the same token can
36
+ have a different contract address per environment, not just per network.
37
+ `Partial` at every level matters: index it as
38
+ `TOKEN_ADDRESSES[token][network]?.[environment]` and check for
39
+ `undefined`, since a combination that doesn't exist (e.g. no official
40
+ Base Sepolia USDT, or any `arbitrum` `test` entry at all) has no entry
41
+ rather than a placeholder value. One naming quirk on `arbitrum`: the
42
+ `USDT` contract there is real and Tether-backed, but reports its
43
+ on-chain name/`symbol()` as `"USD₮0"` (Tether's "USDT0" cross-chain
44
+ standard, migrated in place in Jan 2025) — this codebase always verifies
45
+ by address, never by `symbol()`, so it doesn't matter functionally, but
46
+ don't be surprised seeing "USD₮0" instead of "USDT" on a block explorer.
47
+
48
+ **`USDC` on `bnb` is Binance-Peg USDC, not a Circle deployment.** Circle
49
+ issues no native USDC on BNB Chain at all — it's absent from both
50
+ Circle's own address list and its CCTP-supported chain list. The address
51
+ in `TOKEN_ADDRESSES` is a Binance-custodied, 1:1-pegged BEP-20 token
52
+ instead (Binance locks real USDC/equivalent collateral in its own wallet
53
+ and mints this against it) — real liquidity and adoption (it's what
54
+ "USDC" means on BNB Chain in practice), but a centralized-custody trust
55
+ model, not the direct-issuer verification every other `USDC`/`USDT`
56
+ entry in this table has. See `docs/payments.md`'s note on it before
57
+ building anything that treats every `TOKEN_ADDRESSES` entry as
58
+ equally trusted. `USDT` on `bnb` is Tether's own official BEP-20
59
+ issuance — same trust model as everywhere else.
60
+
61
+ ## `Network`
62
+
63
+ `NetworkSchema`/`Network` — `'base' | 'optimism' | 'polygon' |
64
+ 'ethereum' | 'arbitrum' | 'avalanche' | 'bnb'`. This is the full type —
65
+ every one of these values is a valid `Network` for reading/filtering
66
+ (e.g. `ListChargesInput.network`). `solana` used to be part of this type
67
+ but was removed entirely (not just left non-operational) once there was
68
+ no near-term plan to wire it — see `docs/security.md`'s finding #13.
69
+
70
+ `OPERATIONAL_NETWORKS` (`readonly ['base', 'arbitrum', 'optimism',
71
+ 'polygon', 'ethereum', 'avalanche', 'bnb']`) and its matching
72
+ `OperationalNetwork` type — the narrower list of networks actually
73
+ wired end-to-end today, which right now is exactly every `Network`
74
+ value. `CreateChargeSchema.network` enforces this at creation: `POST
75
+ /v1/charges` with any `network` outside this list is rejected with
76
+ `400 validation_error` naming the network and what's currently
77
+ supported, not silently accepted and left to strand funds. Not a
78
+ permanent ceiling and not guaranteed to stay in lockstep with
79
+ `Network` — the day a genuinely new chain is added to the type before
80
+ its wiring lands, this list will again be the narrower one. Being in
81
+ `OPERATIONAL_NETWORKS` means `live` works; it says nothing about `test`
82
+ on its own — see `Token` above for why
83
+ `arbitrum`/`polygon`/`avalanche`/`bnb` specifically have no `test`
84
+ environment yet (`optimism`/`ethereum` do).
85
+
86
+ `EVM_NETWORKS` (`readonly ['base', 'optimism', 'polygon', 'ethereum',
87
+ 'arbitrum', 'avalanche', 'bnb']`) and its matching `EvmNetwork` type —
88
+ every current `Network` value (all EVM-based, now that `solana` is
89
+ gone). Use this to guard any logic that assumes an EVM-style
90
+ address/RPC — a future non-EVM network would reintroduce a real gap
91
+ here.
92
+
93
+ `NETWORK_LABELS: Record<Network, string>` — display names (`'Base'`,
94
+ `'Optimism'`, ...) for UI use.
95
+
96
+ `NETWORK_EXPLORERS: Record<Network, string>` — each network's block
97
+ explorer base URL (e.g. `https://basescan.org`), used to build a direct
98
+ transaction link (see `explorerTxUrl` on `VerifyCharge`, in
99
+ [`charges.md`](./charges.md)).
100
+
101
+ ## See also
102
+
103
+ - [`charges.md`](./charges.md) — where `Token`/`Network` are actually
104
+ used (`CreateChargeInput.acceptedPayments`, `Charge.acceptedPayments`/
105
+ `paidWith`, `VerifyCharge`), and `GET /v1/networks`
106
+ (`CapabilitiesSchema`) — the live version of this same matrix, scoped
107
+ to your API key's `environment`, rather than the full type-level
108
+ listing on this page.
@@ -0,0 +1,165 @@
1
+ # Webhooks
2
+
3
+ Everything here is exported from `@klappay/types`'s webhook-related
4
+ modules (`webhook-events.ts`, `webhooks.ts`, `webhook-event-data.ts`) —
5
+ import from the package root either way, this split is an internal
6
+ implementation detail.
7
+
8
+ ## Event types, and the four categories
9
+
10
+ Every event Klap can send is one `WebhookEventTypeSchema` value, built
11
+ from four sub-enums (each independently exported, useful if you only
12
+ ever care about one group):
13
+
14
+ | Sub-schema | Category | Events |
15
+ |---|---|---|
16
+ | `ChargeWebhookEventTypeSchema` | `payments` | `charge.created`, `charge.partially_paid`, `charge.confirmed`, `charge.expired`, `charge.underpaid`, `charge.settled`, `charge.settlement_failed`, `charge.overpaid`, `charge.paused`, `charge.reactivated`, `charge.contribution_received`, `charge.contribution_settled` |
17
+ | `AccountWebhookEventTypeSchema` | `account` | `payout_address.changed`, `api_key.created`, `api_key.revoked`, `webhook.created`, `webhook.deleted`, `webhook.secret_rotated`, `fee_tier.updated`, `member.removed`, `member.role_changed`, `member.invited` |
18
+ | `WebhookDeliveryEventTypeSchema` | `webhooks` | `webhook.delivery_failed`, `webhook.delivery_recovered`, `webhook.endpoint_unhealthy` |
19
+ | `SecurityWebhookEventTypeSchema` | `security` | `auth.login`, `auth.login_failed`, `auth.suspicious_activity`, `auth.email_verified`, `auth.password_reset_requested`, `auth.password_reset_completed` |
20
+
21
+ `EVENT_CATEGORY_MAP` (`Record<WebhookEventType, WebhookCategory>`) is the
22
+ single source of truth for which category an event belongs to —
23
+ generated from the four enums above, never hand-duplicated. Its inverse,
24
+ `WEBHOOK_EVENT_CATEGORIES` (`Record<WebhookCategory, readonly
25
+ WebhookEventType[]>`), gives you every event in a category, e.g. for
26
+ building a subscription UI.
27
+
28
+ Two events worth calling out specifically:
29
+
30
+ - **`charge.confirmed` vs. `charge.settled`** — `confirmed` means the
31
+ payment was *detected on-chain*; `settled` means the merchant's wallet
32
+ *actually received* the funds, a separate, later step (see
33
+ `settlementStatus` in [`charges.md`](./charges.md)). Subscribe to
34
+ `confirmed` if you only need "will I get paid", or `settled` if you
35
+ need "has the money actually arrived".
36
+ - **`charge.overpaid`** fires *alongside* `charge.confirmed`/
37
+ `charge.partially_paid` whenever the cumulative amount received ends
38
+ up above `amount` — it's an additional signal, not a replacement
39
+ status.
40
+ - **`charge.paused`/`charge.reactivated`/`charge.contribution_received`/
41
+ `charge.contribution_settled`** are the only events in this category
42
+ that don't carry the full `Charge` object as `data`. `paused`/
43
+ `reactivated` only ever fire for a charge with no `expiresAt` that's
44
+ gone inactive for longer than its inactivity window, carrying
45
+ `{ chargeId, lastActivityAt, pausedAt }` / `{ chargeId,
46
+ reactivatedAt }`. `contribution_received`/`contribution_settled` are
47
+ exclusive to `mode: 'continuous'` charges (see `mode` in
48
+ [`charges.md`](./charges.md)) — a continuous charge never fires
49
+ `charge.confirmed`/`charge.settled` at all, since `status` never
50
+ leaves `pending`; instead every individual transfer fires
51
+ `contribution_received` on detection and `contribution_settled` once
52
+ its payout completes, carrying `{ chargeId, token, network, amount,
53
+ txHash, payerAddress }` / `{ chargeId, token, network, amount,
54
+ txHash, distributorAddress }` — one pair-scoped event per
55
+ contribution instead of one event for the whole charge.
56
+
57
+ `member.invited` does not exist — there's no invite endpoint in the API
58
+ today.
59
+
60
+ ## Subscribing — `CreateWebhookSchema`
61
+
62
+ `POST /v1/webhooks`'s body. At least one of `events` or `eventCategories`
63
+ is required (enforced by a `.refine()`, not just documentation):
64
+
65
+ ```ts
66
+ import { CreateWebhookSchema } from '@klappay/types'
67
+
68
+ CreateWebhookSchema.parse({
69
+ url: 'https://example.com/webhooks/klap',
70
+ eventCategories: ['payments'],
71
+ })
72
+ ```
73
+
74
+ - `events` — individual event types, or `"*"` (`WEBHOOK_EVENTS_WILDCARD`)
75
+ for every event.
76
+ - `eventCategories` — subscribe to a whole category at once (see the
77
+ table above) — new events added to that category later arrive
78
+ automatically, no subscription update needed.
79
+ - `excludeEvents` — opt back out of specific events even under a `"*"`
80
+ or category subscription.
81
+ - `url` must be HTTPS and resolve to a public address — private/internal
82
+ IPs are rejected server-side.
83
+
84
+ `WebhookSchema` / `Webhook` is the response to creating one — critically,
85
+ `secret` (the HMAC signing secret) is only ever present in **this**
86
+ response. `WebhookListItemSchema` / `WebhookListItem` is what every
87
+ subsequent list/read returns instead: the same shape minus `secret`,
88
+ plus `hint` (a truncated, safe-to-display form, e.g. `whsec_...ab12`).
89
+
90
+ ## The payload envelope
91
+
92
+ `WebhookPayloadSchema` / `WebhookPayload` is the wire shape of every
93
+ delivery: `{ id, event, createdAt, data }`. `data` is typed `unknown`
94
+ here on purpose — its real shape depends on `event`, which a single flat
95
+ schema can't express. `id` is also sent as the `X-Klap-Delivery` header,
96
+ distinct from the delivery-id-per-registered-webhook used internally.
97
+
98
+ For the *typed*, narrowed version, use `TypedWebhookPayload` — a
99
+ discriminated union over `event` (same pattern as Stripe's
100
+ `Event.data.object`):
101
+
102
+ ```ts
103
+ import type { TypedWebhookPayload } from '@klappay/types'
104
+
105
+ function handle(payload: TypedWebhookPayload) {
106
+ if (payload.event === 'charge.confirmed') {
107
+ payload.data.amountReceived // typed as Charge, no cast needed
108
+ } else if (payload.event === 'payout_address.changed') {
109
+ payload.data.to // typed as PayoutAddressChangedData
110
+ }
111
+ }
112
+ ```
113
+
114
+ `WebhookEventDataMap` is the underlying `{ event: dataShape }` mapping
115
+ `TypedWebhookPayload` is built from — every `charge.*` event carries a
116
+ full `Charge`; every other event carries a small, event-specific object
117
+ (all plain TypeScript types, not Zod schemas, since they're never
118
+ validated standalone, only as part of `TypedWebhookPayload`):
119
+
120
+ | Event(s) | `data` shape |
121
+ |---|---|
122
+ | `payout_address.changed` | `PayoutAddressChangedData` — `{ organizationId, from, to }` (`from` nullable) |
123
+ | `api_key.created` / `api_key.revoked` | `ApiKeyEventData` — `{ apiKeyId, name, environment, hint }` |
124
+ | `webhook.created` / `webhook.deleted` / `webhook.secret_rotated` | `WebhookConfigEventData` — `{ webhookId, url }` |
125
+ | `webhook.delivery_failed` / `webhook.delivery_recovered` / `webhook.endpoint_unhealthy` | `WebhookHealthEventData` — `{ webhookId, url, failureRatio? }` (`failureRatio` only on `endpoint_unhealthy`) |
126
+ | `fee_tier.updated` | `FeeTierUpdatedData` — `{ organizationId, previousFeePercent, newFeePercent }` |
127
+ | `member.removed` | `MemberEventData` — `{ userId, email, role }` |
128
+ | `member.role_changed` | `MemberRoleChangedData` — `MemberEventData & { previousRole }` |
129
+ | `member.invited` | `MemberInvitedData` — `{ organizationId, email, role, invitedByUserId }` |
130
+ | `auth.login` | `AuthLoginData` — `{ userId, ipAddress }` |
131
+ | `auth.login_failed` | `AuthLoginFailedData` — `{ email, ipAddress }` |
132
+ | `auth.suspicious_activity` | `AuthSuspiciousActivityData` — `AuthLoginData & { previousIpAddress }` |
133
+ | `auth.email_verified` | `AuthEmailVerifiedData` — `{ userId, email }` |
134
+ | `auth.password_reset_requested` | `AuthPasswordResetRequestedData` — `{ userId, email }` |
135
+ | `auth.password_reset_completed` | `AuthPasswordResetCompletedData` — `{ userId, email }` |
136
+
137
+ ## Delivery health
138
+
139
+ `WebhookDeliveryStatusSchema` / `WebhookDeliveryStatus` —
140
+ `'pending' | 'delivered' | 'failed'` (`failed` means all 5 retry
141
+ attempts over ~24h were exhausted; retry manually via
142
+ `POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry`).
143
+ `WebhookDeliverySchema` / `WebhookDelivery` is one entry from `GET
144
+ /v1/webhooks/{id}/deliveries` — `responseCode: null` means every attempt
145
+ failed to connect at all, not just a non-2xx response.
146
+ `ListWebhookDeliveriesSchema`/`PaginatedWebhookDeliveriesSchema` are the
147
+ query/response shapes for that endpoint — same shared cursor pagination
148
+ as `GET /v1/charges`/`GET /v1/organizations/{id}/api-keys`/
149
+ `GET /v1/organizations/{id}/users`, see
150
+ [`charges.md`](./charges.md#the-shared-cursor-pagination-pattern). `GET
151
+ /v1/webhooks` itself (listing the webhooks, not their deliveries) stays
152
+ unpaginated — it's hard-capped at 20 active webhooks per organization.
153
+
154
+ The `webhook.*` meta-events (`WebhookDeliveryEventTypeSchema`) let you
155
+ monitor this without polling: `webhook.endpoint_unhealthy` fires once
156
+ when a webhook's trailing-24h failure rate crosses 20%,
157
+ `webhook.delivery_recovered` fires once when a delivery next succeeds
158
+ afterward.
159
+
160
+ ## See also
161
+
162
+ - [`charges.md`](./charges.md) — the `Charge` shape carried by every
163
+ `charge.*` event's `data`.
164
+ - [`sandbox.md`](./sandbox.md) — `TriggerableChargeEvent`, the subset of
165
+ charge events you can simulate without a real on-chain transfer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klappay/types",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "license": "MIT",
5
5
  "description": "TypeScript types and Zod schemas for the Klap Core API — the request/response contracts, published separately so integrators get them without needing access to the (closed-source) API implementation.",
6
6
  "main": "./dist/index.js",
@@ -12,19 +12,25 @@
12
12
  "require": "./dist/index.js"
13
13
  }
14
14
  },
15
- "files": ["dist", "LICENSE", "README.md", "logo.png"],
15
+ "files": [
16
+ "dist",
17
+ "docs",
18
+ "LICENSE",
19
+ "README.md",
20
+ "CHANGELOG.md"
21
+ ],
16
22
  "publishConfig": {
17
23
  "access": "public"
18
24
  },
19
- "scripts": {
20
- "build": "tsup",
21
- "dev": "tsup --watch"
22
- },
23
25
  "dependencies": {
24
26
  "zod": "^3.23.0"
25
27
  },
26
28
  "devDependencies": {
27
29
  "tsup": "^8.3.0",
28
30
  "typescript": "^5.6.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "dev": "tsup --watch"
29
35
  }
30
- }
36
+ }
package/logo.png DELETED
Binary file