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.
- package/CHANGELOG.md +8 -0
- package/dist/Store.d.ts +32 -9
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +42 -10
- package/dist/Store.js.map +1 -1
- package/dist/proxy/internal/Headers.d.ts +13 -1
- package/dist/proxy/internal/Headers.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.js +14 -1
- package/dist/proxy/internal/Headers.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts +31 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +88 -11
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +6 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +7 -2
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +6 -0
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +1 -1
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +6 -0
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +1 -1
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/package.json +1 -1
- package/src/Store.test-d.ts +58 -0
- package/src/Store.test.ts +77 -0
- package/src/Store.ts +155 -74
- package/src/proxy/internal/Headers.test.ts +18 -0
- package/src/proxy/internal/Headers.ts +14 -1
- package/src/stripe/server/Charge.test.ts +215 -1
- package/src/stripe/server/Charge.ts +150 -20
- package/src/tempo/server/Charge.test.ts +22 -1
- package/src/tempo/server/Charge.ts +16 -2
- package/src/tempo/server/Session.ts +9 -1
- package/src/tempo/server/Subscription.ts +7 -1
- package/src/tempo/session/ChannelStore.test.ts +21 -0
- 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
|
-
|
|
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:
|
|
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
|
|
70
|
-
const
|
|
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(
|
|
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
|
|