mppx 0.6.27 → 0.6.28

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/Store.d.ts +32 -9
  3. package/dist/Store.d.ts.map +1 -1
  4. package/dist/Store.js +42 -10
  5. package/dist/Store.js.map +1 -1
  6. package/dist/proxy/internal/Headers.d.ts +13 -1
  7. package/dist/proxy/internal/Headers.d.ts.map +1 -1
  8. package/dist/proxy/internal/Headers.js +14 -1
  9. package/dist/proxy/internal/Headers.js.map +1 -1
  10. package/dist/stripe/server/Charge.d.ts +31 -1
  11. package/dist/stripe/server/Charge.d.ts.map +1 -1
  12. package/dist/stripe/server/Charge.js +88 -11
  13. package/dist/stripe/server/Charge.js.map +1 -1
  14. package/dist/tempo/server/Charge.d.ts +6 -0
  15. package/dist/tempo/server/Charge.d.ts.map +1 -1
  16. package/dist/tempo/server/Charge.js +7 -2
  17. package/dist/tempo/server/Charge.js.map +1 -1
  18. package/dist/tempo/server/Session.d.ts +6 -0
  19. package/dist/tempo/server/Session.d.ts.map +1 -1
  20. package/dist/tempo/server/Session.js +1 -1
  21. package/dist/tempo/server/Session.js.map +1 -1
  22. package/dist/tempo/server/Subscription.d.ts +6 -0
  23. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  24. package/dist/tempo/server/Subscription.js +1 -1
  25. package/dist/tempo/server/Subscription.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/Store.test-d.ts +58 -0
  28. package/src/Store.test.ts +77 -0
  29. package/src/Store.ts +155 -74
  30. package/src/proxy/internal/Headers.test.ts +18 -0
  31. package/src/proxy/internal/Headers.ts +14 -1
  32. package/src/stripe/server/Charge.test.ts +215 -1
  33. package/src/stripe/server/Charge.ts +150 -20
  34. package/src/tempo/server/Charge.test.ts +22 -1
  35. package/src/tempo/server/Charge.ts +16 -2
  36. package/src/tempo/server/Session.ts +9 -1
  37. package/src/tempo/server/Subscription.ts +7 -1
  38. package/src/tempo/session/ChannelStore.test.ts +21 -0
  39. package/src/tempo/subscription/Store.test.ts +55 -0
@@ -1,7 +1,8 @@
1
+ import type * as Challenge from '../../Challenge.js'
1
2
  import type * as Credential from '../../Credential.js'
2
3
  import { PaymentActionRequiredError, VerificationFailedError } from '../../Errors.js'
3
4
  import * as Expires from '../../Expires.js'
4
- import type { LooseOmit, OneOf } from '../../internal/types.js'
5
+ import type { LooseOmit, MaybePromise, OneOf } from '../../internal/types.js'
5
6
  import * as Method from '../../Method.js'
6
7
  import type * as Html from '../../server/internal/html/config.ts'
7
8
  import type * as z from '../../zod.js'
@@ -54,6 +55,7 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
54
55
  } = parameters
55
56
 
56
57
  const client = 'client' in parameters ? parameters.client : undefined
58
+ const connect = parameters.connect
57
59
  const secretKey = 'secretKey' in parameters ? parameters.secretKey : undefined
58
60
 
59
61
  type Defaults = charge.DeriveDefaults<parameters>
@@ -92,7 +94,7 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
92
94
  }
93
95
  : undefined,
94
96
 
95
- async verify({ credential, request }) {
97
+ async verify({ credential, envelope, request }) {
96
98
  const { challenge } = credential
97
99
  const resolvedRequest = (() => {
98
100
  const parsed = Methods.charge.schema.request.safeParse(request)
@@ -115,6 +117,13 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
115
117
  | Record<string, string>
116
118
  | undefined
117
119
  const resolvedMetadata = { ...buildAnalytics({ credential }), ...userMetadata }
120
+ const settlement = validateConnectSettlement({
121
+ amount: resolvedRequest.amount,
122
+ settlement:
123
+ typeof connect === 'function'
124
+ ? await connect({ challenge, credential, envelope, request: resolvedRequest })
125
+ : connect,
126
+ })
118
127
 
119
128
  const pi = client
120
129
  ? await createWithClient({
@@ -123,6 +132,7 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
123
132
  request: resolvedRequest,
124
133
  spt,
125
134
  metadata: resolvedMetadata,
135
+ settlement,
126
136
  })
127
137
  : await createWithSecretKey({
128
138
  secretKey: secretKey!,
@@ -130,6 +140,7 @@ export function charge<const parameters extends charge.Parameters>(parameters: p
130
140
  request: resolvedRequest,
131
141
  spt,
132
142
  metadata: resolvedMetadata,
143
+ settlement,
133
144
  })
134
145
 
135
146
  if (pi.replayed)
@@ -171,6 +182,8 @@ export declare namespace charge {
171
182
  | undefined
172
183
  /** Optional metadata to include in SPT creation requests. */
173
184
  metadata?: Record<string, string> | undefined
185
+ /** Optional server-side Stripe Connect settlement policy. Not included in MPP challenges. */
186
+ connect?: ConnectSettlement | ResolveConnectSettlement | undefined
174
187
  } & Defaults &
175
188
  OneOf<
176
189
  | {
@@ -187,6 +200,45 @@ export declare namespace charge {
187
200
  parameters,
188
201
  Extract<keyof parameters, keyof Defaults>
189
202
  > & { decimals: number }
203
+
204
+ /**
205
+ * Server-side Stripe Connect settlement parameters.
206
+ *
207
+ * @see https://docs.stripe.com/connect/destination-charges
208
+ */
209
+ type ConnectSettlement = {
210
+ /** Connected account used as the Stripe account context for the request. */
211
+ stripeAccount?: string | undefined
212
+ /** Platform application fee amount in the smallest currency unit. */
213
+ applicationFeeAmount?: number | undefined
214
+ /** Connected account used as the business of record. */
215
+ onBehalfOf?: string | undefined
216
+ /** Destination transfer created from the PaymentIntent. */
217
+ transferData?: { amount?: number | undefined; destination: string } | undefined
218
+ /** Reconciliation token linking related charges and transfers. */
219
+ transferGroup?: string | undefined
220
+ }
221
+
222
+ type ResolveConnectSettlement = (parameters: {
223
+ challenge: Challenge.Challenge<
224
+ z.output<typeof Methods.charge.schema.request>,
225
+ 'charge',
226
+ 'stripe'
227
+ >
228
+ credential: Credential.Credential<
229
+ z.output<typeof Methods.charge.schema.credential.payload>,
230
+ Challenge.Challenge<z.output<typeof Methods.charge.schema.request>, 'charge', 'stripe'>
231
+ >
232
+ envelope?:
233
+ | Method.VerifiedChallengeEnvelope<
234
+ z.output<typeof Methods.charge.schema.request>,
235
+ z.output<typeof Methods.charge.schema.credential.payload>,
236
+ 'charge',
237
+ 'stripe'
238
+ >
239
+ | undefined
240
+ request: z.output<typeof Methods.charge.schema.request>
241
+ }) => MaybePromise<ConnectSettlement | undefined>
190
242
  }
191
243
 
192
244
  /** Creates a PaymentIntent using the Stripe SDK client. */
@@ -195,21 +247,41 @@ async function createWithClient(parameters: {
195
247
  challenge: { id: string }
196
248
  metadata: Record<string, string>
197
249
  request: { amount: unknown; currency: unknown }
250
+ settlement: charge.ConnectSettlement | undefined
198
251
  spt: string
199
252
  }): Promise<{ id: string; status: string; replayed: boolean }> {
200
- const { client, challenge, metadata, request, spt } = parameters
253
+ const { client, challenge, metadata, request, settlement, spt } = parameters
201
254
  try {
255
+ const paymentIntentParams = {
256
+ amount: Number(request.amount),
257
+ automatic_payment_methods: { allow_redirects: 'never', enabled: true },
258
+ confirm: true,
259
+ currency: request.currency as string,
260
+ metadata,
261
+ ...(settlement?.applicationFeeAmount !== undefined && {
262
+ application_fee_amount: settlement.applicationFeeAmount,
263
+ }),
264
+ ...(settlement?.onBehalfOf !== undefined && { on_behalf_of: settlement.onBehalfOf }),
265
+ ...(settlement?.transferData !== undefined && {
266
+ transfer_data: {
267
+ destination: settlement.transferData.destination,
268
+ ...(settlement.transferData.amount !== undefined && {
269
+ amount: settlement.transferData.amount,
270
+ }),
271
+ },
272
+ }),
273
+ ...(settlement?.transferGroup !== undefined && { transfer_group: settlement.transferGroup }),
274
+ // `shared_payment_granted_token` is not yet in the Stripe SDK types (SPTs are in private preview).
275
+ shared_payment_granted_token: spt,
276
+ }
277
+ const paymentIntentOptions = {
278
+ apiVersion: stripePreviewVersion,
279
+ idempotencyKey: `mppx_${challenge.id}_${spt}`,
280
+ ...(settlement?.stripeAccount !== undefined && { stripeAccount: settlement.stripeAccount }),
281
+ }
202
282
  const result = await client.paymentIntents.create(
203
- {
204
- amount: Number(request.amount),
205
- automatic_payment_methods: { allow_redirects: 'never', enabled: true },
206
- confirm: true,
207
- currency: request.currency as string,
208
- metadata,
209
- // `shared_payment_granted_token` is not yet in the Stripe SDK types (SPTs are in private preview).
210
- shared_payment_granted_token: spt,
211
- } as any,
212
- { idempotencyKey: `mppx_${challenge.id}_${spt}`, apiVersion: stripePreviewVersion },
283
+ paymentIntentParams as any,
284
+ paymentIntentOptions,
213
285
  )
214
286
  // https://docs.stripe.com/error-low-level#idempotency
215
287
  const replayed = result.lastResponse?.headers?.['idempotent-replayed'] === 'true'
@@ -228,9 +300,10 @@ async function createWithSecretKey(parameters: {
228
300
  challenge: { id: string }
229
301
  metadata: Record<string, string>
230
302
  request: { amount: unknown; currency: unknown }
303
+ settlement: charge.ConnectSettlement | undefined
231
304
  spt: string
232
305
  }): Promise<{ id: string; status: string; replayed: boolean }> {
233
- const { secretKey, challenge, metadata, request, spt } = parameters
306
+ const { secretKey, challenge, metadata, request, settlement, spt } = parameters
234
307
 
235
308
  const body = new URLSearchParams({
236
309
  amount: request.amount as string,
@@ -243,15 +316,27 @@ async function createWithSecretKey(parameters: {
243
316
  for (const [key, value] of Object.entries(metadata)) {
244
317
  body.set(`metadata[${key}]`, value)
245
318
  }
319
+ if (settlement?.applicationFeeAmount !== undefined)
320
+ body.set('application_fee_amount', String(settlement.applicationFeeAmount))
321
+ if (settlement?.onBehalfOf !== undefined) body.set('on_behalf_of', settlement.onBehalfOf)
322
+ if (settlement?.transferData !== undefined) {
323
+ body.set('transfer_data[destination]', settlement.transferData.destination)
324
+ if (settlement.transferData.amount !== undefined)
325
+ body.set('transfer_data[amount]', String(settlement.transferData.amount))
326
+ }
327
+ if (settlement?.transferGroup !== undefined) body.set('transfer_group', settlement.transferGroup)
328
+
329
+ const headers = {
330
+ Authorization: `Basic ${btoa(`${secretKey}:`)}`,
331
+ 'Content-Type': 'application/x-www-form-urlencoded',
332
+ 'Idempotency-Key': `mppx_${challenge.id}_${spt}`,
333
+ 'Stripe-Version': stripePreviewVersion,
334
+ ...(settlement?.stripeAccount !== undefined && { 'Stripe-Account': settlement.stripeAccount }),
335
+ }
246
336
 
247
337
  const response = await fetch('https://api.stripe.com/v1/payment_intents', {
248
338
  method: 'POST',
249
- headers: {
250
- Authorization: `Basic ${btoa(`${secretKey}:`)}`,
251
- 'Content-Type': 'application/x-www-form-urlencoded',
252
- 'Idempotency-Key': `mppx_${challenge.id}_${spt}`,
253
- 'Stripe-Version': stripePreviewVersion,
254
- },
339
+ headers,
255
340
  body,
256
341
  })
257
342
 
@@ -288,3 +373,48 @@ function buildAnalytics(parameters: { credential: Credential.Credential }): Reco
288
373
  ...(credential.source ? { mpp_client_id: credential.source } : {}),
289
374
  }
290
375
  }
376
+
377
+ function validateConnectSettlement(parameters: {
378
+ amount: unknown
379
+ settlement: charge.ConnectSettlement | undefined
380
+ }): charge.ConnectSettlement | undefined {
381
+ const { amount, settlement } = parameters
382
+ if (settlement === undefined) return undefined
383
+
384
+ const paymentAmount = Number(amount)
385
+ if (!Number.isSafeInteger(paymentAmount) || paymentAmount < 0)
386
+ throw new VerificationFailedError({ reason: 'Stripe amount must be a non-negative integer.' })
387
+
388
+ validateAccountId(settlement.stripeAccount, 'stripeAccount')
389
+ validateAccountId(settlement.onBehalfOf, 'onBehalfOf')
390
+ validateAmount(settlement.applicationFeeAmount, paymentAmount, 'applicationFeeAmount')
391
+
392
+ if (settlement.transferData !== undefined) {
393
+ validateRequiredAccountId(settlement.transferData.destination, 'transferData.destination')
394
+ validateAmount(settlement.transferData.amount, paymentAmount, 'transferData.amount')
395
+ }
396
+
397
+ return settlement
398
+ }
399
+
400
+ function validateAccountId(value: string | undefined, name: string) {
401
+ if (value !== undefined && value.length === 0)
402
+ throw new VerificationFailedError({ reason: `Stripe Connect ${name} must be non-empty.` })
403
+ }
404
+
405
+ function validateRequiredAccountId(value: string | undefined, name: string) {
406
+ if (value === undefined || value.length === 0)
407
+ throw new VerificationFailedError({ reason: `Stripe Connect ${name} must be non-empty.` })
408
+ }
409
+
410
+ function validateAmount(value: number | undefined, paymentAmount: number, name: string) {
411
+ if (value === undefined) return
412
+ if (!Number.isSafeInteger(value) || value < 0)
413
+ throw new VerificationFailedError({
414
+ reason: `Stripe Connect ${name} must be a non-negative integer.`,
415
+ })
416
+ if (value > paymentAmount)
417
+ throw new VerificationFailedError({
418
+ reason: `Stripe Connect ${name} must be less than or equal to the PaymentIntent amount.`,
419
+ })
420
+ }
@@ -286,6 +286,7 @@ describe('tempo', () => {
286
286
  })
287
287
 
288
288
  test('behavior: rejects replayed transaction hash', async () => {
289
+ const dedupStore = Store.memory()
289
290
  const dedupServer = Mppx_server.create({
290
291
  methods: [
291
292
  tempo_server.charge({
@@ -294,7 +295,8 @@ describe('tempo', () => {
294
295
  },
295
296
  currency: asset,
296
297
  account: accounts[0],
297
- store: Store.memory(),
298
+ store: dedupStore,
299
+ storeKeyPrefix: 'tenant:',
298
300
  }),
299
301
  ],
300
302
  realm,
@@ -336,6 +338,8 @@ describe('tempo', () => {
336
338
  })
337
339
  expect(response.status).toBe(200)
338
340
  }
341
+ expect(await dedupStore.get(`tenant:mppx:charge:${receipt.transactionHash}`)).not.toBeNull()
342
+ expect(await dedupStore.get(`mppx:charge:${receipt.transactionHash}`)).toBeNull()
339
343
 
340
344
  const response2 = await fetch(httpServer.url)
341
345
  expect(response2.status).toBe(402)
@@ -1616,6 +1620,7 @@ describe('tempo', () => {
1616
1620
  currency: asset,
1617
1621
  account: accounts[0],
1618
1622
  store: sponsoredStore,
1623
+ storeKeyPrefix: 'tenant:',
1619
1624
  }),
1620
1625
  ],
1621
1626
  realm,
@@ -1661,6 +1666,16 @@ describe('tempo', () => {
1661
1666
 
1662
1667
  const first = fetch(httpServer.url, { headers: { Authorization: credential1 } })
1663
1668
  await simulationStarted
1669
+ expect(
1670
+ await sponsoredStore.get(
1671
+ `tenant:mppx:charge:sponsor:${chain.id}:${accounts[1].address.toLowerCase()}`,
1672
+ ),
1673
+ ).not.toBeNull()
1674
+ expect(
1675
+ await sponsoredStore.get(
1676
+ `mppx:charge:sponsor:${chain.id}:${accounts[1].address.toLowerCase()}`,
1677
+ ),
1678
+ ).toBeNull()
1664
1679
  const second = fetch(httpServer.url, { headers: { Authorization: credential2 } })
1665
1680
 
1666
1681
  try {
@@ -2870,6 +2885,7 @@ describe('tempo', () => {
2870
2885
 
2871
2886
  test('behavior: store keys proof replay protection by challenge ID', async () => {
2872
2887
  const replayStore = Store.memory()
2888
+ const storeKeyPrefix = 'tenant:'
2873
2889
  const server_ = Mppx_server.create({
2874
2890
  methods: [
2875
2891
  tempo_server.charge({
@@ -2879,6 +2895,7 @@ describe('tempo', () => {
2879
2895
  currency: asset,
2880
2896
  account: accounts[0],
2881
2897
  store: replayStore,
2898
+ storeKeyPrefix,
2882
2899
  }),
2883
2900
  ],
2884
2901
  realm,
@@ -2918,6 +2935,10 @@ describe('tempo', () => {
2918
2935
  headers: { Authorization: Credential.serialize(credential1) },
2919
2936
  })
2920
2937
  expect(response2.status).toBe(200)
2938
+ expect(
2939
+ await replayStore.get(`${storeKeyPrefix}mppx:charge:proof:${challenge1.id}`),
2940
+ ).not.toBeNull()
2941
+ expect(await replayStore.get(`mppx:charge:proof:${challenge1.id}`)).toBeNull()
2921
2942
 
2922
2943
  const response3 = await fetch(httpServer.url)
2923
2944
  expect(response3.status).toBe(402)
@@ -66,8 +66,16 @@ export function charge<const parameters extends charge.Parameters>(
66
66
  validateSender,
67
67
  waitForConfirmation = true,
68
68
  } = parameters
69
- const store = (parameters.store ?? Store.memory()) as Store.AtomicStore<charge.StoreItemMap>
70
- const proofStore = parameters.store as Store.AtomicStore<charge.StoreItemMap> | undefined
69
+ const storeKeyPrefix = parameters.storeKeyPrefix ?? ''
70
+ const store = Store.from(
71
+ (parameters.store ?? Store.memory()) as Store.AtomicStore<charge.StoreItemMap>,
72
+ { keyPrefix: storeKeyPrefix },
73
+ )
74
+ const proofStore = parameters.store
75
+ ? Store.from(parameters.store as Store.AtomicStore<charge.StoreItemMap>, {
76
+ keyPrefix: storeKeyPrefix,
77
+ })
78
+ : undefined
71
79
 
72
80
  const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
73
81
 
@@ -539,6 +547,12 @@ export declare namespace charge {
539
547
  * memo binding, transaction success, and replay protection.
540
548
  */
541
549
  validateSender?: ValidateSender | undefined
550
+ /**
551
+ * Prefix prepended to charge replay-protection store keys.
552
+ *
553
+ * By default, no prefix is applied.
554
+ */
555
+ storeKeyPrefix?: string | undefined
542
556
  /**
543
557
  * Whether to wait for the charge transaction to confirm on-chain before
544
558
  * responding. @default true
@@ -104,7 +104,9 @@ export function session<const parameters extends session.Parameters>(
104
104
 
105
105
  const lastOnChainVerified = new Map<Hex, number>()
106
106
 
107
- const store = ChannelStore.fromStore(rawStore)
107
+ const store = ChannelStore.fromStore(
108
+ Store.from(rawStore, { keyPrefix: parameters.storeKeyPrefix }),
109
+ )
108
110
 
109
111
  const { account, recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
110
112
 
@@ -357,6 +359,12 @@ export declare namespace session {
357
359
  * local single-process usage.
358
360
  */
359
361
  store?: Store.AtomicStore | undefined
362
+ /**
363
+ * Prefix prepended to channel state store keys.
364
+ *
365
+ * By default, no prefix is applied.
366
+ */
367
+ storeKeyPrefix?: string | undefined
360
368
  /**
361
369
  * Enable SSE streaming.
362
370
  *
@@ -72,7 +72,7 @@ export function subscription<const parameters extends subscription.Parameters>(
72
72
 
73
73
  const { recipient } = Account.resolve(parameters)
74
74
  const context = createContext(parameters, {
75
- store: rawStore,
75
+ store: Store.from(rawStore, { keyPrefix: parameters.storeKeyPrefix }),
76
76
  options: {
77
77
  activationTimeoutMs: parameters.activationTimeoutMs,
78
78
  renewalTimeoutMs: parameters.renewalTimeoutMs,
@@ -1110,6 +1110,12 @@ export declare namespace subscription {
1110
1110
  subscription: SubscriptionRecord
1111
1111
  }) => Promise<RenewalResult>
1112
1112
  store?: Store.AtomicStore<Record<string, unknown>> | undefined
1113
+ /**
1114
+ * Prefix prepended to all subscription store keys.
1115
+ *
1116
+ * By default, no prefix is applied.
1117
+ */
1118
+ storeKeyPrefix?: string | undefined
1113
1119
  testnet?: boolean | undefined
1114
1120
  waitForConfirmation?: boolean | undefined
1115
1121
  } & Defaults
@@ -117,6 +117,27 @@ describe('channelStore', () => {
117
117
  expect(typeof loaded!.createdAt).toBe('string')
118
118
  })
119
119
 
120
+ test('prefixes backing store keys when configured', async () => {
121
+ const rawStore = Store.memory()
122
+ const cs = ChannelStore.fromStore(Store.from(rawStore, { keyPrefix: 'tenant:' }))
123
+ await cs.updateChannel(channelId, () => makeChannel())
124
+
125
+ expect(await rawStore.get(`tenant:${channelId}`)).not.toBeNull()
126
+ expect(await rawStore.get(channelId)).toBeNull()
127
+ expect((await cs.getChannel(channelId))?.channelId).toBe(channelId)
128
+ })
129
+
130
+ test('isolates prefixed store wrappers', async () => {
131
+ const rawStore = Store.memory()
132
+ const first = ChannelStore.fromStore(Store.from(rawStore, { keyPrefix: 'tenant-a:' }))
133
+ const second = ChannelStore.fromStore(Store.from(rawStore, { keyPrefix: 'tenant-b:' }))
134
+ await first.updateChannel(channelId, () => makeChannel())
135
+
136
+ expect(await second.getChannel(channelId)).toBeNull()
137
+ expect(await rawStore.get(`tenant-a:${channelId}`)).not.toBeNull()
138
+ expect(await rawStore.get(`tenant-b:${channelId}`)).toBeNull()
139
+ })
140
+
120
141
  test('treats case-variant channelIds as the same record', async () => {
121
142
  const cs = ChannelStore.fromStore(Store.memory())
122
143
  await cs.updateChannel(mixedCaseAliasChannelId, () =>
@@ -26,6 +26,61 @@ function createRecord(overrides: Partial<SubscriptionRecord> = {}): Subscription
26
26
  }
27
27
 
28
28
  describe('tempo subscription store', () => {
29
+ test('prefixes all backing store keys when configured', async () => {
30
+ const rawStore = Store.memory()
31
+ const store = fromStore(Store.from(rawStore, { keyPrefix: 'tenant:' }))
32
+ let finishActivation!: () => void
33
+ const pendingActivation = new Promise<void>((resolve) => {
34
+ finishActivation = resolve
35
+ })
36
+ let activationStarted!: () => void
37
+ const activationStartedPromise = new Promise<void>((resolve) => {
38
+ activationStarted = resolve
39
+ })
40
+
41
+ const activation = store.activate({
42
+ challengeId: 'challenge-1',
43
+ create: async () => {
44
+ activationStarted()
45
+ await pendingActivation
46
+ return { subscription: createRecord() }
47
+ },
48
+ lookupKey: 'user-1:plan:pro',
49
+ })
50
+ await activationStartedPromise
51
+
52
+ expect(await rawStore.get('tenant:tempo:subscription:credential:challenge-1')).not.toBeNull()
53
+ expect(
54
+ await rawStore.get('tenant:tempo:subscription:activation:user-1:plan:pro'),
55
+ ).not.toBeNull()
56
+ expect(await rawStore.get('tempo:subscription:credential:challenge-1')).toBeNull()
57
+
58
+ finishActivation()
59
+ expect((await activation).status).toBe('activated')
60
+ await store.getOrCreateAccessKey('user-1:plan:pro')
61
+
62
+ expect(await rawStore.get(`tenant:tempo:subscription:record:${subscriptionId}`)).not.toBeNull()
63
+ expect(await rawStore.get('tenant:tempo:subscription:key:user-1:plan:pro')).toBe(subscriptionId)
64
+ expect(
65
+ await rawStore.get('tenant:tempo:subscription:access-key:user-1:plan:pro'),
66
+ ).not.toBeNull()
67
+ expect(await rawStore.get(`tempo:subscription:record:${subscriptionId}`)).toBeNull()
68
+ })
69
+
70
+ test('combines store prefix with custom key family prefixes', async () => {
71
+ const rawStore = Store.memory()
72
+ const store = fromStore(Store.from(rawStore, { keyPrefix: 'tenant:' }), {
73
+ keyPrefix: 'lookup:',
74
+ recordPrefix: 'record:',
75
+ })
76
+
77
+ await store.put(createRecord())
78
+
79
+ expect(await rawStore.get(`tenant:record:${subscriptionId}`)).not.toBeNull()
80
+ expect(await rawStore.get('tenant:lookup:user-1:plan:pro')).toBe(subscriptionId)
81
+ expect(await rawStore.get(`record:${subscriptionId}`)).toBeNull()
82
+ })
83
+
29
84
  test('rejects a replayed activation challenge', async () => {
30
85
  const store = fromStore(Store.memory())
31
86