@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.
@@ -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
+ }
@@ -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
- const jitter = Math.random() * this.initialBackoffMs
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 { error?: StrimzErrorBody } | StrimzErrorBody
187
- if (typeof json === 'object' && json !== null && 'error' in json && json.error) {
188
- return json.error
189
- }
190
- if (typeof json === 'object' && json !== null && 'code' in json && 'message' in json) {
191
- return json as StrimzErrorBody
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
  }