mppx 0.6.20 → 0.6.21
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 +7 -0
- package/dist/client/Mppx.d.ts +12 -1
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +127 -10
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +69 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +250 -20
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +1 -1
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +2 -1
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/server/Mppx.d.ts +82 -3
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +557 -83
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/package.json +1 -1
- package/src/client/Mppx.test-d.ts +55 -0
- package/src/client/Mppx.test.ts +181 -0
- package/src/client/Mppx.ts +248 -16
- package/src/client/internal/Fetch.test-d.ts +31 -0
- package/src/client/internal/Fetch.test.ts +261 -0
- package/src/client/internal/Fetch.ts +467 -24
- package/src/middlewares/internal/mppx.ts +5 -6
- package/src/proxy/Proxy.test.ts +69 -0
- package/src/server/Mppx.test-d.ts +50 -0
- package/src/server/Mppx.test.ts +893 -1
- package/src/server/Mppx.ts +862 -97
- package/src/tempo/server/internal/html.gen.ts +1 -1
|
@@ -669,6 +669,267 @@ describe('Fetch.from: 402 retry path', () => {
|
|
|
669
669
|
expect(headers.Authorization).toBe('credential')
|
|
670
670
|
})
|
|
671
671
|
|
|
672
|
+
test('emits client events and allows challenge handler to provide credential', async () => {
|
|
673
|
+
const events: string[] = []
|
|
674
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
675
|
+
let callCount = 0
|
|
676
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
677
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
678
|
+
calls.push({ init })
|
|
679
|
+
callCount++
|
|
680
|
+
if (callCount === 1) return make402()
|
|
681
|
+
return new Response('OK', { status: 200 })
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const method = { ...noopMethod, createCredential }
|
|
685
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
686
|
+
eventDispatcher.on('*', (event) => {
|
|
687
|
+
events.push(`*:${event.name}`)
|
|
688
|
+
})
|
|
689
|
+
eventDispatcher.on('challenge.received', async (payload) => {
|
|
690
|
+
events.push(`challenge:${payload.challenge.id}`)
|
|
691
|
+
return 'event-credential'
|
|
692
|
+
})
|
|
693
|
+
eventDispatcher.on('credential.created', (payload) => {
|
|
694
|
+
events.push(`credential:${payload.credential}`)
|
|
695
|
+
throw new Error('observer failed')
|
|
696
|
+
})
|
|
697
|
+
eventDispatcher.on('payment.response', (payload) => {
|
|
698
|
+
events.push(`response:${payload.response.status}`)
|
|
699
|
+
throw new Error('observer failed')
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
const fetch = Fetch.from({
|
|
703
|
+
eventDispatcher,
|
|
704
|
+
fetch: mockFetch,
|
|
705
|
+
methods: [method],
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
const response = await fetch('https://example.com/api')
|
|
709
|
+
|
|
710
|
+
expect(response.status).toBe(200)
|
|
711
|
+
expect(createCredential).not.toHaveBeenCalled()
|
|
712
|
+
const retryHeaders = new Headers((calls[1]!.init as RequestInit).headers)
|
|
713
|
+
expect(retryHeaders.get('Authorization')).toBe('event-credential')
|
|
714
|
+
expect(events).toEqual([
|
|
715
|
+
'challenge:abc',
|
|
716
|
+
'*:challenge.received',
|
|
717
|
+
'credential:event-credential',
|
|
718
|
+
'*:credential.created',
|
|
719
|
+
'response:200',
|
|
720
|
+
'*:payment.response',
|
|
721
|
+
])
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
test('uses the first challenge event credential', async () => {
|
|
725
|
+
const events: string[] = []
|
|
726
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
727
|
+
const method = { ...noopMethod, createCredential }
|
|
728
|
+
let callCount = 0
|
|
729
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
730
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
731
|
+
calls.push({ init })
|
|
732
|
+
callCount++
|
|
733
|
+
if (callCount === 1) return make402()
|
|
734
|
+
return new Response('OK', { status: 200 })
|
|
735
|
+
}
|
|
736
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
737
|
+
eventDispatcher.on('challenge.received', () => {
|
|
738
|
+
events.push('first')
|
|
739
|
+
return 'first-credential'
|
|
740
|
+
})
|
|
741
|
+
eventDispatcher.on('challenge.received', () => {
|
|
742
|
+
events.push('second')
|
|
743
|
+
return 'second-credential'
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
const fetch = Fetch.from({
|
|
747
|
+
eventDispatcher,
|
|
748
|
+
fetch: mockFetch,
|
|
749
|
+
methods: [method],
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
await fetch('https://example.com/api')
|
|
753
|
+
|
|
754
|
+
expect(createCredential).not.toHaveBeenCalled()
|
|
755
|
+
const retryHeaders = new Headers((calls[1]!.init as RequestInit).headers)
|
|
756
|
+
expect(retryHeaders.get('Authorization')).toBe('first-credential')
|
|
757
|
+
expect(events).toEqual(['first'])
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
test('does not emit payment.response for non-ok retry responses', async () => {
|
|
761
|
+
const events: string[] = []
|
|
762
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
763
|
+
const method = { ...noopMethod, createCredential }
|
|
764
|
+
let callCount = 0
|
|
765
|
+
const mockFetch: typeof globalThis.fetch = async () => {
|
|
766
|
+
callCount++
|
|
767
|
+
if (callCount === 1) return make402()
|
|
768
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
769
|
+
}
|
|
770
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
771
|
+
eventDispatcher.on('payment.response', (payload) => {
|
|
772
|
+
events.push(`response:${payload.response.status}`)
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
const fetch = Fetch.from({
|
|
776
|
+
eventDispatcher,
|
|
777
|
+
fetch: mockFetch,
|
|
778
|
+
methods: [method],
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
const response = await fetch('https://example.com/api')
|
|
782
|
+
|
|
783
|
+
expect(response.status).toBe(500)
|
|
784
|
+
expect(events).toEqual([])
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
test('ignores empty challenge event credentials and continues handlers', async () => {
|
|
788
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
789
|
+
const method = { ...noopMethod, createCredential }
|
|
790
|
+
let callCount = 0
|
|
791
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
792
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
793
|
+
calls.push({ init })
|
|
794
|
+
callCount++
|
|
795
|
+
if (callCount === 1) return make402()
|
|
796
|
+
return new Response('OK', { status: 200 })
|
|
797
|
+
}
|
|
798
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
799
|
+
eventDispatcher.on('challenge.received', () => '')
|
|
800
|
+
eventDispatcher.on('challenge.received', () => 'second-credential')
|
|
801
|
+
|
|
802
|
+
const fetch = Fetch.from({
|
|
803
|
+
eventDispatcher,
|
|
804
|
+
fetch: mockFetch,
|
|
805
|
+
methods: [method],
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
await fetch('https://example.com/api')
|
|
809
|
+
|
|
810
|
+
expect(createCredential).not.toHaveBeenCalled()
|
|
811
|
+
const retryHeaders = new Headers((calls[1]!.init as RequestInit).headers)
|
|
812
|
+
expect(retryHeaders.get('Authorization')).toBe('second-credential')
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
test('memoizes createCredential across wildcard observers and fallback', async () => {
|
|
816
|
+
const createCredential = vi.fn(async () => 'method-credential')
|
|
817
|
+
const method = { ...noopMethod, createCredential }
|
|
818
|
+
let callCount = 0
|
|
819
|
+
const mockFetch: typeof globalThis.fetch = async () => {
|
|
820
|
+
callCount++
|
|
821
|
+
if (callCount === 1) return make402()
|
|
822
|
+
return new Response('OK', { status: 200 })
|
|
823
|
+
}
|
|
824
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
825
|
+
eventDispatcher.on('*', async (event) => {
|
|
826
|
+
if (event.name === 'challenge.received') await event.payload.createCredential()
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
const fetch = Fetch.from({
|
|
830
|
+
eventDispatcher,
|
|
831
|
+
fetch: mockFetch,
|
|
832
|
+
methods: [method],
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
await fetch('https://example.com/api')
|
|
836
|
+
|
|
837
|
+
expect(createCredential).toHaveBeenCalledTimes(1)
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
test('does not expose live challenges or init headers to observers', async () => {
|
|
841
|
+
const createCredential = vi.fn(async ({ challenge }) => `amount:${challenge.request.amount}`)
|
|
842
|
+
const method = { ...noopMethod, createCredential }
|
|
843
|
+
let callCount = 0
|
|
844
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
845
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
846
|
+
calls.push({ init })
|
|
847
|
+
callCount++
|
|
848
|
+
if (callCount === 1) return make402()
|
|
849
|
+
return new Response('OK', { status: 200 })
|
|
850
|
+
}
|
|
851
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
852
|
+
eventDispatcher.on('*', (event) => {
|
|
853
|
+
if (event.name !== 'challenge.received') return
|
|
854
|
+
try {
|
|
855
|
+
;(event.payload.challenge.request as { amount: string }).amount = '999'
|
|
856
|
+
} catch {}
|
|
857
|
+
const headers = new Headers(event.payload.init?.headers)
|
|
858
|
+
headers.set('Authorization', 'attacker')
|
|
859
|
+
if (event.payload.init) event.payload.init.headers = headers
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
const fetch = Fetch.from({
|
|
863
|
+
eventDispatcher,
|
|
864
|
+
fetch: mockFetch,
|
|
865
|
+
methods: [method],
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
await fetch('https://example.com/api', {
|
|
869
|
+
headers: { 'X-Test': '1' },
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
const retryHeaders = new Headers((calls[1]!.init as RequestInit).headers)
|
|
873
|
+
expect(retryHeaders.get('Authorization')).toBe('amount:1')
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
test('continues dispatching observer listeners after one throws', async () => {
|
|
877
|
+
const events: string[] = []
|
|
878
|
+
const method = { ...noopMethod, createCredential: vi.fn(async () => 'credential') }
|
|
879
|
+
let callCount = 0
|
|
880
|
+
const mockFetch: typeof globalThis.fetch = async () => {
|
|
881
|
+
callCount++
|
|
882
|
+
if (callCount === 1) return make402()
|
|
883
|
+
return new Response('OK', { status: 200 })
|
|
884
|
+
}
|
|
885
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
886
|
+
eventDispatcher.on('credential.created', () => {
|
|
887
|
+
events.push('first')
|
|
888
|
+
throw new Error('observer failed')
|
|
889
|
+
})
|
|
890
|
+
eventDispatcher.on('credential.created', () => {
|
|
891
|
+
events.push('second')
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
const fetch = Fetch.from({
|
|
895
|
+
eventDispatcher,
|
|
896
|
+
fetch: mockFetch,
|
|
897
|
+
methods: [method],
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
await fetch('https://example.com/api')
|
|
901
|
+
|
|
902
|
+
expect(events).toEqual(['first', 'second'])
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
test('emits payment.failed when automatic payment handling rejects', async () => {
|
|
906
|
+
const events: string[] = []
|
|
907
|
+
const createCredential = vi.fn(async () => 'credential')
|
|
908
|
+
const mockFetch = vi.fn(async () =>
|
|
909
|
+
make402({ expires: new Date(Date.now() - 60_000).toISOString() }),
|
|
910
|
+
)
|
|
911
|
+
const method = { ...noopMethod, createCredential }
|
|
912
|
+
const eventDispatcher = Fetch.createEventDispatcher<[typeof method]>()
|
|
913
|
+
eventDispatcher.on('*', (event) => {
|
|
914
|
+
events.push(`*:${event.name}`)
|
|
915
|
+
})
|
|
916
|
+
eventDispatcher.on('payment.failed', (payload) => {
|
|
917
|
+
events.push(
|
|
918
|
+
`failed:${payload.error instanceof Errors.PaymentExpiredError}:${payload.challenge?.id}`,
|
|
919
|
+
)
|
|
920
|
+
throw new Error('observer failed')
|
|
921
|
+
})
|
|
922
|
+
const fetch = Fetch.from({
|
|
923
|
+
eventDispatcher,
|
|
924
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
925
|
+
methods: [method],
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
await expect(fetch('https://example.com/api')).rejects.toThrow(Errors.PaymentExpiredError)
|
|
929
|
+
expect(createCredential).not.toHaveBeenCalled()
|
|
930
|
+
expect(events).toEqual(['failed:true:abc', '*:payment.failed'])
|
|
931
|
+
})
|
|
932
|
+
|
|
672
933
|
test('preserves existing headers on retry', async () => {
|
|
673
934
|
let callCount = 0
|
|
674
935
|
const calls: { init: RequestInit | undefined }[] = []
|