@strimz/sdk 0.1.0 → 0.2.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/README.md +109 -23
- package/dist/{browser-client-DlZg0Aae.d.cts → browser-client-BzLGc50J.d.cts} +27 -26
- package/dist/{browser-client-DlZg0Aae.d.ts → browser-client-BzLGc50J.d.ts} +27 -26
- package/dist/browser-client.cjs +38 -13
- package/dist/browser-client.cjs.map +1 -1
- package/dist/browser-client.d.cts +1 -1
- package/dist/browser-client.d.ts +1 -1
- package/dist/browser-client.js +39 -14
- package/dist/browser-client.js.map +1 -1
- package/dist/eip712.cjs +70 -0
- package/dist/eip712.cjs.map +1 -1
- package/dist/eip712.d.cts +110 -19
- package/dist/eip712.d.ts +110 -19
- package/dist/eip712.js +67 -1
- package/dist/eip712.js.map +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +4 -3
- package/dist/errors.d.ts +4 -3
- package/dist/errors.js.map +1 -1
- package/dist/index.cjs +39 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -9
- package/dist/index.d.ts +8 -9
- package/dist/index.js +40 -15
- package/dist/index.js.map +1 -1
- package/dist/webhooks.cjs.map +1 -1
- package/dist/webhooks.d.cts +3 -3
- package/dist/webhooks.d.ts +3 -3
- package/dist/webhooks.js.map +1 -1
- package/package.json +4 -4
- package/src/browser-client.ts +11 -11
- package/src/client.ts +4 -4
- package/src/eip712/index.ts +13 -13
- package/src/eip712/pay-intent.ts +54 -0
- package/src/eip712/permit.ts +8 -8
- package/src/eip712/receive-with-authorization.ts +6 -6
- package/src/eip712/subscription-intent.ts +59 -0
- package/src/eip712/types.ts +5 -4
- package/src/errors.ts +4 -3
- package/src/http/fetcher.ts +46 -11
- package/src/http/headers.ts +4 -4
- package/src/idempotency.ts +2 -3
- package/src/resources/checkout.ts +10 -3
- package/src/resources/tokens.ts +10 -11
- package/src/webhooks.ts +3 -3
package/dist/webhooks.d.ts
CHANGED
|
@@ -4,15 +4,15 @@ export { AgentActionExecutedEvent, AgentJobCompletedEvent, AgentJobDisputedEvent
|
|
|
4
4
|
export { WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_MAX_ATTEMPTS, WEBHOOK_RETRY_SCHEDULE_SECONDS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_TOLERANCE_SECONDS } from '@strimz/shared-config';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Webhook utilities
|
|
8
|
-
*
|
|
7
|
+
* Webhook utilities. Re-exports the verified Web Crypto primitives from
|
|
8
|
+
* `@strimz/shared-crypto` and the typed event union from
|
|
9
9
|
* `@strimz/shared-types`. Consumers should import from here, not directly
|
|
10
10
|
* from the shared packages.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Parse a webhook payload into a typed `StrimzWebhookEvent`.
|
|
15
|
-
* Use after `verifyWebhookSignature` succeeds
|
|
15
|
+
* Use after `verifyWebhookSignature` succeeds. Never trust raw payloads.
|
|
16
16
|
*/
|
|
17
17
|
declare function parseWebhookEvent(rawJson: string): StrimzWebhookEvent;
|
|
18
18
|
|
package/dist/webhooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/webhooks.ts"],"names":["webhookEventSchema"],"mappings":";;;;;;AA6DO,SAAS,kBAAkB,OAAA,EAAqC;AACrE,EAAA,OAAOA,kBAAAA,CAAmB,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,OAAO,CAAC,CAAA;AACrD","file":"webhooks.js","sourcesContent":["/**\n * Webhook utilities
|
|
1
|
+
{"version":3,"sources":["../src/webhooks.ts"],"names":["webhookEventSchema"],"mappings":";;;;;;AA6DO,SAAS,kBAAkB,OAAA,EAAqC;AACrE,EAAA,OAAOA,kBAAAA,CAAmB,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,OAAO,CAAC,CAAA;AACrD","file":"webhooks.js","sourcesContent":["/**\n * Webhook utilities. Re-exports the verified Web Crypto primitives from\n * `@strimz/shared-crypto` and the typed event union from\n * `@strimz/shared-types`. Consumers should import from here, not directly\n * from the shared packages.\n */\n\nexport {\n signWebhookPayload,\n verifyWebhookSignature,\n WEBHOOK_SIGNATURE_VERSION,\n type SignWebhookOptions,\n type VerifyWebhookOptions,\n type VerifyWebhookReason,\n type VerifyWebhookResult,\n} from '@strimz/shared-crypto'\n\nexport {\n webhookEventSchema,\n webhookEventNameSchema,\n type StrimzWebhookEvent,\n type WebhookEventName,\n // Per-event typed payloads.\n type PaymentCreatedEvent,\n type PaymentCompletedEvent,\n type PaymentFailedEvent,\n type SubscriptionCreatedEvent,\n type SubscriptionChargedEvent,\n type SubscriptionChargeFailedEvent,\n type SubscriptionRecoveryAttemptEvent,\n type SubscriptionRecoveryOutcomeEvent,\n type SubscriptionCancelledEvent,\n type SubscriptionLapsedEvent,\n type RefundCreatedEvent,\n type RefundCompletedEvent,\n type RefundFailedEvent,\n type InvoiceCreatedEvent,\n type InvoicePaidEvent,\n type InvoiceOverdueEvent,\n type AgentActionExecutedEvent,\n type AgentJobProposedEvent,\n type AgentJobCompletedEvent,\n type AgentJobDisputedEvent,\n type ComplianceWalletFlaggedEvent,\n type ComplianceWalletBlockedEvent,\n} from '@strimz/shared-types'\n\nexport {\n WEBHOOK_RETRY_SCHEDULE_SECONDS,\n WEBHOOK_MAX_ATTEMPTS,\n WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,\n WEBHOOK_SIGNATURE_HEADER,\n WEBHOOK_DELIVERY_ID_HEADER,\n} from '@strimz/shared-config'\n\nimport { webhookEventSchema, type StrimzWebhookEvent } from '@strimz/shared-types'\n\n/**\n * Parse a webhook payload into a typed `StrimzWebhookEvent`.\n * Use after `verifyWebhookSignature` succeeds. Never trust raw payloads.\n */\nexport function parseWebhookEvent(rawJson: string): StrimzWebhookEvent {\n return webhookEventSchema.parse(JSON.parse(rawJson))\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strimz/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The Strimz SDK. Typed client for server and edge runtimes. Handles HTTP, retries, idempotency, pagination, EIP-712 typed-data building, and webhook signature verification.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Strimz <hello@strimz.finance>",
|
|
@@ -69,9 +69,9 @@
|
|
|
69
69
|
],
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"zod": "^3.24.1",
|
|
72
|
-
"@strimz/shared-config": "0.1.
|
|
73
|
-
"@strimz/shared-crypto": "0.1.
|
|
74
|
-
"@strimz/shared-types": "0.
|
|
72
|
+
"@strimz/shared-config": "0.1.1",
|
|
73
|
+
"@strimz/shared-crypto": "0.1.1",
|
|
74
|
+
"@strimz/shared-types": "0.2.0"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@types/node": "^22.10.5",
|
package/src/browser-client.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* StrimzBrowserClient
|
|
2
|
+
* StrimzBrowserClient. The publishable-key, browser-safe SDK entry point.
|
|
3
3
|
*
|
|
4
4
|
* Used by `@strimz/sdk-react` and any other browser code that should
|
|
5
|
-
* never see a secret key. Surfaces a strict subset of resource methods
|
|
5
|
+
* never see a secret key. Surfaces a strict subset of resource methods,
|
|
6
6
|
* only those safe to call from a browser with a `pk_test_...` or
|
|
7
7
|
* `pk_live_...` key.
|
|
8
8
|
*/
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from './resources/tokens.js'
|
|
21
21
|
|
|
22
22
|
export interface StrimzBrowserClientOptions {
|
|
23
|
-
/** Publishable key
|
|
23
|
+
/** Publishable key. Either `pk_test_...` or `pk_live_...`. */
|
|
24
24
|
publishableKey: string
|
|
25
25
|
/** Override the API base URL. */
|
|
26
26
|
baseUrl?: string
|
|
@@ -36,21 +36,21 @@ const DEFAULT_BASE_URL = 'https://api.strimz.io'
|
|
|
36
36
|
* Designed for hosted-checkout state polling and the React component
|
|
37
37
|
* family. Read-only on most resources. Refuses a secret key.
|
|
38
38
|
*
|
|
39
|
-
* The `tokens` resource
|
|
40
|
-
* selectors
|
|
41
|
-
* token, the SDK asks the
|
|
42
|
-
* supports, and the selectors map that to a `RelayPath` the
|
|
43
|
-
* UI uses to decide which typed-data to build.
|
|
39
|
+
* The `tokens` resource and the `selectPaymentPath` /
|
|
40
|
+
* `selectSubscriptionPath` selectors form the capability-detection
|
|
41
|
+
* layer. Given a token, the SDK asks the API which meta-tx flows the
|
|
42
|
+
* token supports, and the selectors map that to a `RelayPath` the
|
|
43
|
+
* checkout UI uses to decide which typed-data to build.
|
|
44
44
|
*/
|
|
45
45
|
export class StrimzBrowserClient {
|
|
46
46
|
public readonly mode: ApiKeyMode
|
|
47
47
|
/**
|
|
48
|
-
* Public hosted-checkout reads
|
|
48
|
+
* Public hosted-checkout reads. Payment sessions and subscription
|
|
49
49
|
* plans by id. Backed by the `/v1/checkout/*` namespace on the API,
|
|
50
50
|
* which is intentionally unauthenticated.
|
|
51
51
|
*/
|
|
52
52
|
public readonly checkout: CheckoutResource
|
|
53
|
-
/** Token metadata
|
|
53
|
+
/** Token metadata and EIP-2612 permit nonce lookup. */
|
|
54
54
|
public readonly tokens: TokensResource
|
|
55
55
|
|
|
56
56
|
constructor(options: StrimzBrowserClientOptions) {
|
|
@@ -91,7 +91,7 @@ export class StrimzBrowserClient {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// Re-export the selectors
|
|
94
|
+
// Re-export the selectors and path type so consumers don't need a
|
|
95
95
|
// second import: `import { StrimzBrowserClient, selectPaymentPath }
|
|
96
96
|
// from '@strimz/sdk/browser'`.
|
|
97
97
|
export { selectPaymentPath, selectSubscriptionPath, type RelayPath }
|
package/src/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* StrimzClient
|
|
2
|
+
* StrimzClient. The secret-key, server-side SDK entry point.
|
|
3
3
|
*
|
|
4
4
|
* const strimz = new StrimzClient({ apiKey: process.env.STRIMZ_SECRET_KEY })
|
|
5
5
|
* const session = await strimz.paymentSessions.create({ amount: '1000000', currency: 'USDC' })
|
|
@@ -24,7 +24,7 @@ import { StorefrontsResource } from './resources/storefronts.js'
|
|
|
24
24
|
import { AgentsResource } from './resources/agents.js'
|
|
25
25
|
|
|
26
26
|
export interface StrimzClientOptions {
|
|
27
|
-
/** Secret key
|
|
27
|
+
/** Secret key. Either `sk_test_...` or `sk_live_...`. Mode is auto-detected. */
|
|
28
28
|
apiKey: string
|
|
29
29
|
/** Override the API base URL. Defaults to `https://api.strimz.io`. */
|
|
30
30
|
baseUrl?: string
|
|
@@ -35,14 +35,14 @@ export interface StrimzClientOptions {
|
|
|
35
35
|
/** Optional logger callbacks for instrumentation. */
|
|
36
36
|
onRequest?: ConstructorParameters<typeof Fetcher>[0]['onRequest']
|
|
37
37
|
onResponse?: ConstructorParameters<typeof Fetcher>[0]['onResponse']
|
|
38
|
-
/** Override fetch
|
|
38
|
+
/** Override fetch. Use to inject mocks in tests or polyfills in Edge. */
|
|
39
39
|
fetch?: typeof globalThis.fetch
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const DEFAULT_BASE_URL = 'https://api.strimz.io'
|
|
43
43
|
|
|
44
44
|
export class StrimzClient {
|
|
45
|
-
/** Resolved mode
|
|
45
|
+
/** Resolved mode, either `test` or `live`. Derived from the API key prefix. */
|
|
46
46
|
public readonly mode: ApiKeyMode
|
|
47
47
|
|
|
48
48
|
public readonly merchants: MerchantsResource
|
package/src/eip712/index.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EIP-712 typed-data builders.
|
|
2
|
+
* EIP-712 typed-data builders. Pure functions; no wallet dependency.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* const typedData = buildReceiveWithAuthorizationTypedData({ ... })
|
|
10
|
-
* const signature = await walletClient.signTypedData(typedData)
|
|
11
|
-
*
|
|
12
|
-
* The builders are deliberately granular (one per primitive — EIP-3009,
|
|
13
|
-
* EIP-2612) so the SDK can support future schemes (e.g. x402 batched
|
|
14
|
-
* payments) by adding a sibling builder without touching existing
|
|
15
|
-
* call sites.
|
|
4
|
+
* Meta-tx flows need TWO signatures — one for the token
|
|
5
|
+
* (`ReceiveWithAuthorization` or `Permit`) and one Strimz intent
|
|
6
|
+
* (`PayIntent` or `SubscriptionIntent`) that binds the money-routing
|
|
7
|
+
* fields the token never sees.
|
|
16
8
|
*/
|
|
17
9
|
export type { Eip712Domain, Eip712Field, Eip712Types, Eip712TypedData } from './types.js'
|
|
18
10
|
|
|
@@ -23,3 +15,11 @@ export {
|
|
|
23
15
|
} from './receive-with-authorization.js'
|
|
24
16
|
|
|
25
17
|
export { buildPermitTypedData, PERMIT_TYPES, type PermitParams } from './permit.js'
|
|
18
|
+
|
|
19
|
+
export { buildPayIntentTypedData, PAY_INTENT_TYPES, type PayIntentParams } from './pay-intent.js'
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
buildSubscriptionIntentTypedData,
|
|
23
|
+
SUBSCRIPTION_INTENT_TYPES,
|
|
24
|
+
type SubscriptionIntentParams,
|
|
25
|
+
} from './subscription-intent.js'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Eip712TypedData } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inputs for the Strimz PayIntent. Payer signs this alongside the
|
|
5
|
+
* EIP-3009 auth. Without it, a valid auth sig can be redirected to
|
|
6
|
+
* pay a different merchant.
|
|
7
|
+
*
|
|
8
|
+
* Every field must match the corresponding argument the API will
|
|
9
|
+
* submit on-chain — the contract compares them via ecrecover.
|
|
10
|
+
*/
|
|
11
|
+
export interface PayIntentParams {
|
|
12
|
+
chainId: number
|
|
13
|
+
verifyingContract: `0x${string}`
|
|
14
|
+
merchantId: bigint
|
|
15
|
+
token: `0x${string}`
|
|
16
|
+
amount: bigint
|
|
17
|
+
nonce: `0x${string}`
|
|
18
|
+
validBefore: bigint
|
|
19
|
+
ref: `0x${string}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Field order matches `PAY_INTENT_TYPEHASH` in `StrimzPayments.sol`. */
|
|
23
|
+
export const PAY_INTENT_TYPES = {
|
|
24
|
+
PayIntent: [
|
|
25
|
+
{ name: 'merchantId', type: 'uint256' },
|
|
26
|
+
{ name: 'token', type: 'address' },
|
|
27
|
+
{ name: 'amount', type: 'uint256' },
|
|
28
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
29
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
30
|
+
{ name: 'ref', type: 'bytes32' },
|
|
31
|
+
],
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
/** Domain name + version mirror `EIP712("StrimzPayments", "1")`. */
|
|
35
|
+
export function buildPayIntentTypedData(params: PayIntentParams): Eip712TypedData<'PayIntent'> {
|
|
36
|
+
return {
|
|
37
|
+
domain: {
|
|
38
|
+
name: 'StrimzPayments',
|
|
39
|
+
version: '1',
|
|
40
|
+
chainId: params.chainId,
|
|
41
|
+
verifyingContract: params.verifyingContract,
|
|
42
|
+
},
|
|
43
|
+
types: PAY_INTENT_TYPES,
|
|
44
|
+
primaryType: 'PayIntent',
|
|
45
|
+
message: {
|
|
46
|
+
merchantId: params.merchantId,
|
|
47
|
+
token: params.token,
|
|
48
|
+
amount: params.amount,
|
|
49
|
+
nonce: params.nonce,
|
|
50
|
+
validBefore: params.validBefore,
|
|
51
|
+
ref: params.ref,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/eip712/permit.ts
CHANGED
|
@@ -5,22 +5,22 @@ import type { Eip712TypedData } from './types.js'
|
|
|
5
5
|
*
|
|
6
6
|
* - `chainId`, `token`, `tokenName`, `tokenVersion` form the EIP-712
|
|
7
7
|
* domain (same shape as the EIP-3009 builder).
|
|
8
|
-
* - `owner` is the token holder authorising the allowance
|
|
8
|
+
* - `owner` is the token holder authorising the allowance. Must match
|
|
9
9
|
* the signing wallet.
|
|
10
|
-
* - `spender` is the contract that will pull funds
|
|
10
|
+
* - `spender` is the contract that will pull funds. For Strimz
|
|
11
11
|
* subscriptions this is the StrimzSubscriptions contract.
|
|
12
12
|
* - `value` is the allowance to grant in token base units.
|
|
13
13
|
* `type(uint256).max` produces an unlimited allowance and is the
|
|
14
14
|
* common case for subscription enrolment.
|
|
15
15
|
* - `nonce` is the owner's *current* `nonces(owner)` reading from the
|
|
16
|
-
* token contract. The SDK does not fetch this
|
|
16
|
+
* token contract. The SDK does not fetch this. The caller is
|
|
17
17
|
* expected to have just queried it. Race-free: the token rejects
|
|
18
|
-
* stale nonces, so a concurrent permit on the same owner
|
|
19
|
-
*
|
|
18
|
+
* stale nonces, so a concurrent permit on the same owner causes one
|
|
19
|
+
* to fail predictably.
|
|
20
20
|
* - `deadline` is the unix-second timestamp after which the permit
|
|
21
|
-
* expires. Strimz default for subscription enrolment is 24h
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* expires. Strimz default for subscription enrolment is 24h. Enough
|
|
22
|
+
* headroom for a slow merchant submit pipeline without leaving an
|
|
23
|
+
* indefinite signature window.
|
|
24
24
|
*/
|
|
25
25
|
export interface PermitParams {
|
|
26
26
|
chainId: number
|
|
@@ -5,16 +5,16 @@ import type { Eip712TypedData } from './types.js'
|
|
|
5
5
|
*
|
|
6
6
|
* - `chainId` is the EVM chain id (Arc Testnet = 5042002).
|
|
7
7
|
* - `token` is the ERC-20 contract whose `receiveWithAuthorization` is
|
|
8
|
-
* being invoked
|
|
8
|
+
* being invoked. Usually USDC at `0x36...` on Arc.
|
|
9
9
|
* - `tokenName` and `tokenVersion` form the EIP-712 domain. Real USDC
|
|
10
10
|
* on most chains uses `name="USD Coin"` and `version="2"`; tests
|
|
11
11
|
* use whatever the mock token reports. Always read these from the
|
|
12
12
|
* token contract at runtime to avoid drift.
|
|
13
|
-
* - `from` is the payer
|
|
13
|
+
* - `from` is the payer. The wallet that produces the signature must
|
|
14
14
|
* match this address.
|
|
15
|
-
* - `to` is the recipient of the funds
|
|
15
|
+
* - `to` is the recipient of the funds. For Strimz this is the
|
|
16
16
|
* StrimzPayments contract address (the contract pulls funds into
|
|
17
|
-
* itself, then forwards the fee
|
|
17
|
+
* itself, then forwards the fee and net split).
|
|
18
18
|
* - `value` is the gross amount in token base units.
|
|
19
19
|
* - `validAfter` / `validBefore` are unix-second timestamps bounding
|
|
20
20
|
* the signature's validity. Strimz's default checkout window is
|
|
@@ -37,7 +37,7 @@ export interface ReceiveWithAuthorizationParams {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* EIP-712 type definition for `ReceiveWithAuthorization`. Stable
|
|
40
|
+
* EIP-712 type definition for `ReceiveWithAuthorization`. Stable, and
|
|
41
41
|
* shared by every EIP-3009 implementation (USDC, EURC, etc.).
|
|
42
42
|
*
|
|
43
43
|
* Exported so consumers can introspect or extend with sibling
|
|
@@ -61,7 +61,7 @@ export const RECEIVE_WITH_AUTHORIZATION_TYPES = {
|
|
|
61
61
|
* compatible with viem, ethers, Privy, and Reown.
|
|
62
62
|
*
|
|
63
63
|
* Pass the result to `walletClient.signTypedData(typedData)` (viem)
|
|
64
|
-
* or `signer._signTypedData(domain, types, message)` (ethers)
|
|
64
|
+
* or `signer._signTypedData(domain, types, message)` (ethers). The
|
|
65
65
|
* SDK does not bind to a specific signer.
|
|
66
66
|
*/
|
|
67
67
|
export function buildReceiveWithAuthorizationTypedData(
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Eip712TypedData } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inputs for the Strimz SubscriptionIntent. Payer signs this
|
|
5
|
+
* alongside the ERC-2612 permit. Without it, a valid permit can be
|
|
6
|
+
* used to enrol the payer in a plan they never picked.
|
|
7
|
+
*
|
|
8
|
+
* `permitDeadline` ties this intent to the accompanying permit —
|
|
9
|
+
* they expire together.
|
|
10
|
+
*/
|
|
11
|
+
export interface SubscriptionIntentParams {
|
|
12
|
+
chainId: number
|
|
13
|
+
verifyingContract: `0x${string}`
|
|
14
|
+
merchantId: bigint
|
|
15
|
+
token: `0x${string}`
|
|
16
|
+
amount: bigint
|
|
17
|
+
interval: number
|
|
18
|
+
startAt: bigint
|
|
19
|
+
endAt: bigint
|
|
20
|
+
permitDeadline: bigint
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Field order matches `SUBSCRIPTION_INTENT_TYPEHASH` in the contract. */
|
|
24
|
+
export const SUBSCRIPTION_INTENT_TYPES = {
|
|
25
|
+
SubscriptionIntent: [
|
|
26
|
+
{ name: 'merchantId', type: 'uint256' },
|
|
27
|
+
{ name: 'token', type: 'address' },
|
|
28
|
+
{ name: 'amount', type: 'uint256' },
|
|
29
|
+
{ name: 'interval', type: 'uint32' },
|
|
30
|
+
{ name: 'startAt', type: 'uint64' },
|
|
31
|
+
{ name: 'endAt', type: 'uint64' },
|
|
32
|
+
{ name: 'permitDeadline', type: 'uint256' },
|
|
33
|
+
],
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
/** Domain mirrors `EIP712("StrimzSubscriptions", "1")`. */
|
|
37
|
+
export function buildSubscriptionIntentTypedData(
|
|
38
|
+
params: SubscriptionIntentParams,
|
|
39
|
+
): Eip712TypedData<'SubscriptionIntent'> {
|
|
40
|
+
return {
|
|
41
|
+
domain: {
|
|
42
|
+
name: 'StrimzSubscriptions',
|
|
43
|
+
version: '1',
|
|
44
|
+
chainId: params.chainId,
|
|
45
|
+
verifyingContract: params.verifyingContract,
|
|
46
|
+
},
|
|
47
|
+
types: SUBSCRIPTION_INTENT_TYPES,
|
|
48
|
+
primaryType: 'SubscriptionIntent',
|
|
49
|
+
message: {
|
|
50
|
+
merchantId: params.merchantId,
|
|
51
|
+
token: params.token,
|
|
52
|
+
amount: params.amount,
|
|
53
|
+
interval: params.interval,
|
|
54
|
+
startAt: params.startAt,
|
|
55
|
+
endAt: params.endAt,
|
|
56
|
+
permitDeadline: params.permitDeadline,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/eip712/types.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canonical EIP-712 typed-data shape.
|
|
3
3
|
*
|
|
4
|
-
* Matches the structure every major wallet library accepts
|
|
4
|
+
* Matches the structure every major wallet library accepts:
|
|
5
5
|
* viem's `signTypedData`
|
|
6
6
|
* ethers' `signer._signTypedData`
|
|
7
7
|
* Privy's `signTypedData`
|
|
8
8
|
* Reown AppKit's typed-data RPC
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* The SDK can produce typed-data without binding to a specific wallet
|
|
11
|
+
* client. Consumers pipe the result into whichever signer they
|
|
11
12
|
* already use.
|
|
12
13
|
*
|
|
13
14
|
* No viem/ethers dependency on purpose: the SDK stays minimal, and
|
|
@@ -34,7 +35,7 @@ export type Eip712Types = Record<string, readonly Eip712Field[]>
|
|
|
34
35
|
* gets hashed at the top level (the `types` object usually contains
|
|
35
36
|
* additional referenced sub-structs).
|
|
36
37
|
*
|
|
37
|
-
* `bigint` is used for uint values
|
|
38
|
+
* `bigint` is used for uint values. Wallet libs handle the conversion
|
|
38
39
|
* to hex/decimal at signing time. If a consumer needs to JSON-serialise
|
|
39
40
|
* this object, convert bigints to strings at that boundary.
|
|
40
41
|
*/
|
package/src/errors.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error hierarchy for the Strimz SDK.
|
|
3
3
|
*
|
|
4
|
-
* Every error thrown by the SDK extends `StrimzError`. The SDK
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every error thrown by the SDK extends `StrimzError`. The SDK throws
|
|
5
|
+
* on failure rather than resolving to `{ ok: false }` so call sites
|
|
6
|
+
* stay linear and consumers can narrow with `instanceof` on the
|
|
7
|
+
* exported subclasses.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
export type StrimzErrorCode =
|
package/src/http/fetcher.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface FetcherOptions {
|
|
|
38
38
|
ms: number
|
|
39
39
|
attempt: number
|
|
40
40
|
}) => void
|
|
41
|
-
/** Override the global fetch (for tests
|
|
41
|
+
/** Override the global fetch (for tests or Edge polyfills). */
|
|
42
42
|
fetch?: typeof globalThis.fetch
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -110,13 +110,13 @@ export class Fetcher {
|
|
|
110
110
|
const errorBody = await parseErrorBody(res)
|
|
111
111
|
const retryAfterMs = parseRetryAfter(res)
|
|
112
112
|
|
|
113
|
-
// 429
|
|
113
|
+
// 429. Back off and retry up to maxRetries.
|
|
114
114
|
if (res.status === 429 && attempt < this.maxRetries) {
|
|
115
115
|
await delay(retryAfterMs ?? this.backoffFor(attempt))
|
|
116
116
|
attempt++
|
|
117
117
|
continue
|
|
118
118
|
}
|
|
119
|
-
// 5xx
|
|
119
|
+
// 5xx. Retry only if idempotent.
|
|
120
120
|
if (res.status >= 500 && idempotent && attempt < this.maxRetries) {
|
|
121
121
|
await delay(this.backoffFor(attempt))
|
|
122
122
|
attempt++
|
|
@@ -171,11 +171,22 @@ export class Fetcher {
|
|
|
171
171
|
|
|
172
172
|
private backoffFor(attempt: number): number {
|
|
173
173
|
const expo = this.initialBackoffMs * Math.pow(2, attempt)
|
|
174
|
-
|
|
175
|
-
return Math.min(expo + jitter, 8_000)
|
|
174
|
+
return Math.min(expo + cryptoJitter(this.initialBackoffMs), 8_000)
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
177
|
|
|
178
|
+
// Uses crypto so retry timing does not seed off Math.random in
|
|
179
|
+
// environments where that's been monkey-patched.
|
|
180
|
+
function cryptoJitter(maxMs: number): number {
|
|
181
|
+
const g = globalThis as { crypto?: { getRandomValues?: (a: Uint32Array) => Uint32Array } }
|
|
182
|
+
if (g.crypto?.getRandomValues) {
|
|
183
|
+
const buf = new Uint32Array(1)
|
|
184
|
+
g.crypto.getRandomValues(buf)
|
|
185
|
+
return ((buf[0] ?? 0) / 0xffff_ffff) * maxMs
|
|
186
|
+
}
|
|
187
|
+
return Math.random() * maxMs
|
|
188
|
+
}
|
|
189
|
+
|
|
179
190
|
function isIdempotent(spec: RequestSpec): boolean {
|
|
180
191
|
if (spec.method === 'GET' || spec.method === 'DELETE') return true
|
|
181
192
|
return spec.idempotencyKey != null
|
|
@@ -183,12 +194,13 @@ function isIdempotent(spec: RequestSpec): boolean {
|
|
|
183
194
|
|
|
184
195
|
async function parseErrorBody(res: Response): Promise<StrimzErrorBody> {
|
|
185
196
|
try {
|
|
186
|
-
const json = (await res.json()) as
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
const json = (await res.json()) as unknown
|
|
198
|
+
const raw =
|
|
199
|
+
typeof json === 'object' && json !== null && 'error' in json
|
|
200
|
+
? (json as { error: unknown }).error
|
|
201
|
+
: json
|
|
202
|
+
if (typeof raw === 'object' && raw !== null && 'code' in raw && 'message' in raw) {
|
|
203
|
+
return sanitiseErrorBody(raw as Record<string, unknown>)
|
|
192
204
|
}
|
|
193
205
|
} catch {
|
|
194
206
|
// fall through
|
|
@@ -196,6 +208,29 @@ async function parseErrorBody(res: Response): Promise<StrimzErrorBody> {
|
|
|
196
208
|
return { code: 'api_error', message: `HTTP ${res.status}` }
|
|
197
209
|
}
|
|
198
210
|
|
|
211
|
+
// Whitelist server-provided fields. A bugged endpoint that dumps stack
|
|
212
|
+
// traces or internal metadata into the error payload never reaches SDK
|
|
213
|
+
// consumers.
|
|
214
|
+
const ALLOWED_DETAIL_KEYS = new Set(['issues', 'field', 'reason', 'chargeAttemptId'])
|
|
215
|
+
|
|
216
|
+
function sanitiseErrorBody(raw: Record<string, unknown>): StrimzErrorBody {
|
|
217
|
+
const details =
|
|
218
|
+
typeof raw.details === 'object' && raw.details !== null
|
|
219
|
+
? Object.fromEntries(
|
|
220
|
+
Object.entries(raw.details as Record<string, unknown>).filter(([k]) =>
|
|
221
|
+
ALLOWED_DETAIL_KEYS.has(k),
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
: undefined
|
|
225
|
+
return {
|
|
226
|
+
code: typeof raw.code === 'string' ? (raw.code as StrimzErrorBody['code']) : 'api_error',
|
|
227
|
+
message: typeof raw.message === 'string' ? raw.message : 'Request failed',
|
|
228
|
+
param: typeof raw.param === 'string' ? raw.param : undefined,
|
|
229
|
+
requestId: typeof raw.requestId === 'string' ? raw.requestId : undefined,
|
|
230
|
+
details: details && Object.keys(details).length > 0 ? details : undefined,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
199
234
|
function parseRetryAfter(res: Response): number | undefined {
|
|
200
235
|
const v = res.headers.get('Retry-After')
|
|
201
236
|
if (!v) return undefined
|
package/src/http/headers.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Strimz HTTP header conventions.
|
|
3
3
|
*
|
|
4
|
-
* `Authorization: Bearer <secret_key
|
|
5
|
-
* `X-Strimz-Idempotency-Key
|
|
6
|
-
* `X-Strimz-Sdk
|
|
7
|
-
* `X-Strimz-Sdk-Version
|
|
4
|
+
* `Authorization: Bearer <secret_key>`: primary auth.
|
|
5
|
+
* `X-Strimz-Idempotency-Key`: mutating calls.
|
|
6
|
+
* `X-Strimz-Sdk`: telemetry.
|
|
7
|
+
* `X-Strimz-Sdk-Version`: telemetry.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export const HEADERS = {
|
package/src/idempotency.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { uuid } from '@strimz/shared-crypto'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Generate an idempotency key for a mutating SDK call.
|
|
5
|
-
*
|
|
6
|
-
* can grep for in logs.
|
|
4
|
+
* Generate an idempotency key for a mutating SDK call. The key is a
|
|
5
|
+
* UUID with a stable `strimz_` prefix so it stays greppable in logs.
|
|
7
6
|
*/
|
|
8
7
|
export function generateIdempotencyKey(): string {
|
|
9
8
|
return `strimz_${uuid()}`
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
merchantPublicBrandSchema,
|
|
2
3
|
paymentSessionSchema,
|
|
3
4
|
subscriptionPlanSchema,
|
|
5
|
+
type MerchantPublicBrand,
|
|
4
6
|
type PaymentSession,
|
|
5
7
|
type SubscriptionPlan,
|
|
6
8
|
} from '@strimz/shared-types'
|
|
@@ -10,7 +12,7 @@ import { BaseResource } from './base-resource.js'
|
|
|
10
12
|
* Public hosted-checkout reads.
|
|
11
13
|
*
|
|
12
14
|
* Backed by the `/v1/checkout/*` namespace on the Strimz API, which
|
|
13
|
-
* is intentionally unauthenticated
|
|
15
|
+
* is intentionally unauthenticated. Session and plan records are
|
|
14
16
|
* what a merchant offers to payers, so a payer landing on a checkout
|
|
15
17
|
* URL must be able to load the terms without holding any API key.
|
|
16
18
|
*
|
|
@@ -20,13 +22,18 @@ import { BaseResource } from './base-resource.js'
|
|
|
20
22
|
* code with a secret key.
|
|
21
23
|
*/
|
|
22
24
|
export class CheckoutResource extends BaseResource {
|
|
23
|
-
/** Load a payment session by id (public
|
|
25
|
+
/** Load a payment session by id (public, no merchant scoping). */
|
|
24
26
|
session(id: string): Promise<PaymentSession> {
|
|
25
27
|
return this.get(`/v1/checkout/sessions/${encodeURIComponent(id)}`, paymentSessionSchema)
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
/** Load a subscription plan by id (public
|
|
30
|
+
/** Load a subscription plan by id (public, no merchant scoping). */
|
|
29
31
|
plan(id: string): Promise<SubscriptionPlan> {
|
|
30
32
|
return this.get(`/v1/checkout/plans/${encodeURIComponent(id)}`, subscriptionPlanSchema)
|
|
31
33
|
}
|
|
34
|
+
|
|
35
|
+
/** Load the merchant's public brand card by id. */
|
|
36
|
+
merchant(id: string): Promise<MerchantPublicBrand> {
|
|
37
|
+
return this.get(`/v1/checkout/merchants/${encodeURIComponent(id)}`, merchantPublicBrandSchema)
|
|
38
|
+
}
|
|
32
39
|
}
|
package/src/resources/tokens.ts
CHANGED
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
import { BaseResource } from './base-resource.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Token metadata
|
|
10
|
+
* Token metadata and capability lookup.
|
|
11
11
|
*
|
|
12
|
-
* The two endpoints are public on the API (no key required)
|
|
12
|
+
* The two endpoints are public on the API (no key required). Token
|
|
13
13
|
* metadata is purely chain-derived, has no PII, and is what the
|
|
14
14
|
* browser SDK consults to pick which meta-tx path to take for a
|
|
15
15
|
* given token. Bundled here so consumers don't have to write the HTTP
|
|
@@ -18,7 +18,7 @@ import { BaseResource } from './base-resource.js'
|
|
|
18
18
|
export class TokensResource extends BaseResource {
|
|
19
19
|
/**
|
|
20
20
|
* Fetch full metadata for a token. Throws via the underlying fetcher
|
|
21
|
-
* (404
|
|
21
|
+
* (404 maps to `StrimzNotFoundError`) if the token isn't whitelisted.
|
|
22
22
|
*/
|
|
23
23
|
retrieve(address: string): Promise<TokenMetadata> {
|
|
24
24
|
return this.get(`/v1/tokens/${encodeURIComponent(address.toLowerCase())}`, tokenMetadataSchema)
|
|
@@ -26,7 +26,7 @@ export class TokensResource extends BaseResource {
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Read an owner's current EIP-2612 permit nonce. Call this
|
|
29
|
-
* immediately before signing a Permit
|
|
29
|
+
* immediately before signing a Permit. The token contract rejects
|
|
30
30
|
* stale nonces, and the value advances on every successful permit.
|
|
31
31
|
*/
|
|
32
32
|
permitNonce(address: string, owner: string): Promise<TokenPermitNonce> {
|
|
@@ -39,16 +39,15 @@ export class TokensResource extends BaseResource {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Path selector
|
|
42
|
+
* Path selector. Decides which meta-tx flow to take given a token's
|
|
43
43
|
* capabilities and the action a merchant wants to perform.
|
|
44
44
|
*
|
|
45
45
|
* The choice is unambiguous in 99% of cases:
|
|
46
|
-
* - One-shot pay
|
|
47
|
-
*
|
|
48
|
-
* - Subscription enrol
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* use the classic approve+create flow.
|
|
46
|
+
* - One-shot pay. Prefers EIP-3009. Falls back to approve+pay if the
|
|
47
|
+
* token has neither.
|
|
48
|
+
* - Subscription enrol. Prefers EIP-2612 (the only meta-tx path that
|
|
49
|
+
* grants the recurring allowance subscribers need). If unavailable,
|
|
50
|
+
* the merchant must use the classic approve+create flow.
|
|
52
51
|
*
|
|
53
52
|
* Returned `path` values are strings (not enums) so future schemes
|
|
54
53
|
* can extend the union without breaking existing call sites.
|
package/src/webhooks.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Webhook utilities
|
|
3
|
-
*
|
|
2
|
+
* Webhook utilities. Re-exports the verified Web Crypto primitives from
|
|
3
|
+
* `@strimz/shared-crypto` and the typed event union from
|
|
4
4
|
* `@strimz/shared-types`. Consumers should import from here, not directly
|
|
5
5
|
* from the shared packages.
|
|
6
6
|
*/
|
|
@@ -57,7 +57,7 @@ import { webhookEventSchema, type StrimzWebhookEvent } from '@strimz/shared-type
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Parse a webhook payload into a typed `StrimzWebhookEvent`.
|
|
60
|
-
* Use after `verifyWebhookSignature` succeeds
|
|
60
|
+
* Use after `verifyWebhookSignature` succeeds. Never trust raw payloads.
|
|
61
61
|
*/
|
|
62
62
|
export function parseWebhookEvent(rawJson: string): StrimzWebhookEvent {
|
|
63
63
|
return webhookEventSchema.parse(JSON.parse(rawJson))
|