@strimz/sdk 0.1.1 → 0.2.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/dist/{browser-client-D6VUs85b.d.cts → browser-client-BzLGc50J.d.cts} +3 -1
- package/dist/{browser-client-D6VUs85b.d.ts → browser-client-BzLGc50J.d.ts} +3 -1
- package/dist/browser-client.cjs +33 -8
- 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 +34 -9
- 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 +91 -1
- package/dist/eip712.d.ts +91 -1
- package/dist/eip712.js +67 -1
- package/dist/eip712.js.map +1 -1
- package/dist/index.cjs +34 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +35 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/browser-client.ts +1 -1
- package/src/client.ts +2 -2
- package/src/eip712/index.ts +13 -13
- package/src/eip712/pay-intent.ts +54 -0
- package/src/eip712/subscription-intent.ts +59 -0
- package/src/http/fetcher.ts +43 -8
- package/src/resources/checkout.ts +7 -0
|
@@ -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/http/fetcher.ts
CHANGED
|
@@ -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
|
|
@@ -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'
|
|
@@ -29,4 +31,9 @@ export class CheckoutResource extends BaseResource {
|
|
|
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
|
}
|