accounts 0.6.1 → 0.6.2

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 (50) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/core/Schema.d.ts +12 -12
  3. package/dist/core/adapters/dialog.d.ts.map +1 -1
  4. package/dist/core/adapters/dialog.js +3 -1
  5. package/dist/core/adapters/dialog.js.map +1 -1
  6. package/dist/core/zod/rpc.d.ts +9 -9
  7. package/dist/core/zod/rpc.js +1 -1
  8. package/dist/core/zod/rpc.js.map +1 -1
  9. package/dist/server/CliAuth.d.ts +11 -11
  10. package/dist/server/CliAuth.js +1 -1
  11. package/dist/server/CliAuth.js.map +1 -1
  12. package/dist/server/Handler.d.ts +4 -252
  13. package/dist/server/Handler.d.ts.map +1 -1
  14. package/dist/server/Handler.js +4 -573
  15. package/dist/server/Handler.js.map +1 -1
  16. package/dist/server/internal/handlers/codeAuth.d.ts +41 -0
  17. package/dist/server/internal/handlers/codeAuth.d.ts.map +1 -0
  18. package/dist/server/internal/handlers/codeAuth.js +104 -0
  19. package/dist/server/internal/handlers/codeAuth.js.map +1 -0
  20. package/dist/server/internal/handlers/feePayer.d.ts +73 -0
  21. package/dist/server/internal/handlers/feePayer.d.ts.map +1 -0
  22. package/dist/server/internal/handlers/feePayer.js +184 -0
  23. package/dist/server/internal/handlers/feePayer.js.map +1 -0
  24. package/dist/server/internal/handlers/relay.d.ts +148 -0
  25. package/dist/server/internal/handlers/relay.d.ts.map +1 -0
  26. package/dist/server/internal/handlers/relay.js +600 -0
  27. package/dist/server/internal/handlers/relay.js.map +1 -0
  28. package/dist/server/internal/handlers/utils.d.ts +12 -0
  29. package/dist/server/internal/handlers/utils.d.ts.map +1 -0
  30. package/dist/server/internal/handlers/utils.js +80 -0
  31. package/dist/server/internal/handlers/utils.js.map +1 -0
  32. package/dist/server/internal/handlers/webAuthn.d.ts +57 -0
  33. package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -0
  34. package/dist/server/internal/handlers/webAuthn.js +143 -0
  35. package/dist/server/internal/handlers/webAuthn.js.map +1 -0
  36. package/package.json +2 -2
  37. package/src/core/Provider.connect.browser.test.ts +23 -2
  38. package/src/core/adapters/dialog.ts +6 -1
  39. package/src/core/zod/rpc.ts +1 -1
  40. package/src/server/CliAuth.ts +1 -1
  41. package/src/server/Handler.test.ts +3 -418
  42. package/src/server/Handler.ts +5 -766
  43. package/src/server/internal/handlers/codeAuth.ts +148 -0
  44. package/src/server/internal/handlers/feePayer.test.ts +335 -0
  45. package/src/server/internal/handlers/feePayer.ts +271 -0
  46. package/src/server/internal/handlers/relay.test.ts +767 -0
  47. package/src/server/internal/handlers/relay.ts +817 -0
  48. package/src/server/internal/handlers/utils.ts +96 -0
  49. package/src/server/internal/handlers/webAuthn.test.ts +170 -0
  50. package/src/server/internal/handlers/webAuthn.ts +213 -0
@@ -1,17 +1,10 @@
1
1
  import { Elysia } from 'elysia'
2
2
  import express from 'express'
3
3
  import { Hono } from 'hono'
4
- import { Json, type RpcRequest } from 'ox'
5
- import { SignatureEnvelope, Transaction as core_Transaction, TxEnvelopeTempo } from 'ox/tempo'
6
- import { sendTransactionSync } from 'viem/actions'
7
- import { Transaction, withFeePayer } from 'viem/tempo'
8
- import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vp/test'
9
-
10
- import { accounts, chain, getClient, http } from '../../test/config.js'
11
- import { createServer, type Server } from '../../test/utils.js'
12
- import * as WebAuthnCeremony from '../core/WebAuthnCeremony.js'
4
+ import { describe, expect, test } from 'vp/test'
5
+
6
+ import { createServer } from '../../test/utils.js'
13
7
  import * as Handler from './Handler.js'
14
- import * as Kv from './Kv.js'
15
8
 
16
9
  describe('from', () => {
17
10
  describe('cors', () => {
@@ -696,411 +689,3 @@ describe('from', () => {
696
689
  })
697
690
  })
698
691
  })
699
-
700
- describe('feePayer', () => {
701
- const userAccount = accounts[9]!
702
- const feePayerAccount = accounts[0]!
703
-
704
- let server: Server
705
- let requests: RpcRequest.RpcRequest[] = []
706
-
707
- beforeAll(async () => {
708
- server = await createServer(
709
- Handler.feePayer({
710
- account: feePayerAccount,
711
- chains: [chain],
712
- transports: { [chain.id]: http() },
713
- onRequest: async (request) => {
714
- requests.push(request)
715
- },
716
- }).listener,
717
- )
718
- })
719
-
720
- afterAll(() => {
721
- server.close()
722
- process.on('SIGINT', () => {
723
- server.close()
724
- process.exit(0)
725
- })
726
- process.on('SIGTERM', () => {
727
- server.close()
728
- process.exit(0)
729
- })
730
- })
731
-
732
- afterEach(() => {
733
- requests = []
734
- })
735
-
736
- async function rpc(request: Record<string, unknown>) {
737
- return await fetch(server.url, {
738
- body: Json.stringify(request),
739
- headers: { 'content-type': 'application/json' },
740
- method: 'POST',
741
- }).then((response) => response.json())
742
- }
743
-
744
- /** Signs a sponsor-bound Tempo transaction, preserving the feePayerSignature. */
745
- async function signSponsoredTx(account: (typeof accounts)[number], transaction: object) {
746
- const serialized = (await Transaction.serialize(transaction as never)) as `0x76${string}`
747
- const envelope = TxEnvelopeTempo.deserialize(serialized)
748
- const signature = await account.sign({
749
- hash: TxEnvelopeTempo.getSignPayload(envelope),
750
- })
751
- return TxEnvelopeTempo.serialize(envelope, {
752
- signature: SignatureEnvelope.from(signature),
753
- })
754
- }
755
-
756
- describe('POST /', () => {
757
- test('default: eth_fillTransaction returns a sponsor-bound transaction the sender can broadcast', async () => {
758
- const response = (await rpc({
759
- id: 1,
760
- jsonrpc: '2.0',
761
- method: 'eth_fillTransaction',
762
- params: [
763
- {
764
- chainId: chain.id,
765
- feePayer: true,
766
- from: userAccount.address,
767
- to: '0x0000000000000000000000000000000000000000',
768
- },
769
- ],
770
- })) as {
771
- result: {
772
- sponsor: { address: string }
773
- tx: Record<string, unknown>
774
- }
775
- }
776
- const prepared = core_Transaction.fromRpc(response.result.tx as never) as {
777
- feePayerSignature?: unknown
778
- }
779
- const signed = await signSponsoredTx(userAccount, prepared)
780
- const receipt = (await getClient().request({
781
- method: 'eth_sendRawTransactionSync',
782
- params: [signed],
783
- })) as { feePayer?: string | undefined }
784
-
785
- expect(response.result.sponsor.address).toBe(feePayerAccount.address)
786
- expect(prepared?.feePayerSignature).toBeDefined()
787
- expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
788
- expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
789
- [
790
- "eth_fillTransaction",
791
- ]
792
- `)
793
- })
794
-
795
- test('behavior: mutating a sponsor-bound transaction invalidates the fee payer binding', async () => {
796
- const response = (await rpc({
797
- id: 1,
798
- jsonrpc: '2.0',
799
- method: 'eth_fillTransaction',
800
- params: [
801
- {
802
- chainId: chain.id,
803
- feePayer: true,
804
- from: userAccount.address,
805
- to: '0x0000000000000000000000000000000000000000',
806
- },
807
- ],
808
- })) as {
809
- result: { tx: Record<string, unknown> }
810
- }
811
- const prepared = core_Transaction.fromRpc(response.result.tx as never) as {
812
- gas?: bigint | undefined
813
- feePayerSignature?: unknown
814
- }
815
- const signed = await signSponsoredTx(userAccount, {
816
- ...prepared,
817
- gas: (prepared?.gas ?? 0n) + 1n,
818
- })
819
-
820
- await expect(
821
- getClient().request({
822
- method: 'eth_sendRawTransactionSync',
823
- params: [signed],
824
- }),
825
- ).rejects.toThrowError()
826
- })
827
-
828
- test('behavior: eth_signRawTransaction', async () => {
829
- const client = getClient({
830
- account: userAccount,
831
- transport: withFeePayer(http(), http(server.url)),
832
- })
833
-
834
- const receipt = await sendTransactionSync(client, {
835
- feePayer: true,
836
- to: '0x0000000000000000000000000000000000000000',
837
- })
838
-
839
- expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
840
-
841
- expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
842
- [
843
- "eth_signRawTransaction",
844
- ]
845
- `)
846
- })
847
-
848
- test('behavior: eth_sendRawTransaction', async () => {
849
- const client = getClient({
850
- account: userAccount,
851
- transport: withFeePayer(http(), http(server.url), {
852
- policy: 'sign-and-broadcast',
853
- }),
854
- })
855
-
856
- const receipt = await sendTransactionSync(client, {
857
- feePayer: true,
858
- to: '0x0000000000000000000000000000000000000000',
859
- })
860
-
861
- expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
862
-
863
- expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
864
- [
865
- "eth_sendRawTransactionSync",
866
- ]
867
- `)
868
- })
869
-
870
- test('behavior: eth_sendRawTransactionSync', async () => {
871
- const client = getClient({
872
- account: userAccount,
873
- transport: withFeePayer(http(), http(server.url), {
874
- policy: 'sign-and-broadcast',
875
- }),
876
- })
877
-
878
- const receipt = await sendTransactionSync(client, {
879
- feePayer: true,
880
- to: '0x0000000000000000000000000000000000000000',
881
- })
882
-
883
- expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
884
-
885
- expect(requests.map(({ method }) => method)).toMatchInlineSnapshot(`
886
- [
887
- "eth_sendRawTransactionSync",
888
- ]
889
- `)
890
- })
891
-
892
- test('behavior: unsupported method', async () => {
893
- await expect(
894
- fetch(server.url, {
895
- method: 'POST',
896
- body: JSON.stringify({
897
- jsonrpc: '2.0',
898
- id: 1,
899
- method: 'eth_chainId',
900
- }),
901
- }).then((response) => response.json()),
902
- ).resolves.toMatchInlineSnapshot(`
903
- {
904
- "error": {
905
- "code": -32004,
906
- "name": "RpcResponse.MethodNotSupportedError",
907
- "stack": "",
908
- },
909
- "id": 1,
910
- "jsonrpc": "2.0",
911
- }
912
- `)
913
- })
914
-
915
- test('behavior: internal error', async () => {
916
- const response = await fetch(server.url, {
917
- method: 'POST',
918
- body: JSON.stringify({
919
- jsonrpc: '2.0',
920
- id: 1,
921
- method: 'eth_signRawTransaction',
922
- params: ['0xinvalid'],
923
- }),
924
- })
925
-
926
- const data = await response.json()
927
- expect(data).toMatchInlineSnapshot(`
928
- {
929
- "error": {
930
- "code": -32602,
931
- "name": "RpcResponse.InvalidParamsError",
932
- "stack": "",
933
- },
934
- "id": 1,
935
- "jsonrpc": "2.0",
936
- }
937
- `)
938
- })
939
- })
940
- })
941
-
942
- describe('webauthn', () => {
943
- let server: Server
944
- let ceremony: WebAuthnCeremony.WebAuthnCeremony
945
-
946
- beforeAll(async () => {
947
- server = await createServer(
948
- Handler.webAuthn({
949
- kv: Kv.memory(),
950
- origin: 'http://localhost',
951
- rpId: 'localhost',
952
- }).listener,
953
- )
954
- ceremony = WebAuthnCeremony.server({ url: server.url })
955
- })
956
-
957
- afterAll(async () => {
958
- await server.closeAsync()
959
- })
960
-
961
- describe('POST /register/options', () => {
962
- test('default: returns registration options', async () => {
963
- const { options } = await ceremony.getRegistrationOptions({ name: 'Test' })
964
- expect(options.publicKey).toBeDefined()
965
- expect(options.publicKey!.rp.id).toMatchInlineSnapshot(`"localhost"`)
966
- expect(options.publicKey!.rp.name).toMatchInlineSnapshot(`"localhost"`)
967
- expect(typeof options.publicKey!.challenge).toMatchInlineSnapshot(`"string"`)
968
- })
969
-
970
- test('behavior: each call generates a unique challenge', async () => {
971
- const { options: a } = await ceremony.getRegistrationOptions({ name: 'Test' })
972
- const { options: b } = await ceremony.getRegistrationOptions({ name: 'Test' })
973
- expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
974
- })
975
- })
976
-
977
- describe('POST /login/options', () => {
978
- test('default: returns authentication options', async () => {
979
- const { options } = await ceremony.getAuthenticationOptions()
980
- expect(options.publicKey).toBeDefined()
981
- expect(options.publicKey!.rpId).toMatchInlineSnapshot(`"localhost"`)
982
- expect(typeof options.publicKey!.challenge).toMatchInlineSnapshot(`"string"`)
983
- })
984
-
985
- test('behavior: each call generates a unique challenge', async () => {
986
- const { options: a } = await ceremony.getAuthenticationOptions()
987
- const { options: b } = await ceremony.getAuthenticationOptions()
988
- expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
989
- })
990
- })
991
-
992
- describe('POST /register', () => {
993
- test('error: invalid credential → 400', async () => {
994
- const response = await fetch(`${server.url}/register`, {
995
- method: 'POST',
996
- headers: { 'Content-Type': 'application/json' },
997
- body: JSON.stringify({ id: 'fake', clientDataJSON: 'bad', attestationObject: 'bad' }),
998
- })
999
- expect(response.status).toBe(400)
1000
- const body = await response.json()
1001
- expect(body.error).toBeTypeOf('string')
1002
- })
1003
- })
1004
-
1005
- describe('POST /login', () => {
1006
- test('error: unknown credential → 400', async () => {
1007
- const response = await fetch(`${server.url}/login`, {
1008
- method: 'POST',
1009
- headers: { 'Content-Type': 'application/json' },
1010
- body: JSON.stringify({
1011
- id: 'unknown',
1012
- metadata: { authenticatorData: '0x00', clientDataJSON: '{"challenge":"0xdead"}' },
1013
- raw: {
1014
- id: 'unknown',
1015
- type: 'public-key',
1016
- authenticatorAttachment: null,
1017
- rawId: 'unknown',
1018
- response: { clientDataJSON: 'e30' },
1019
- },
1020
- signature: '0x00',
1021
- }),
1022
- })
1023
- expect(response.status).toBe(400)
1024
- const body = await response.json()
1025
- expect(body.error).toMatchInlineSnapshot(`"Missing or expired challenge"`)
1026
- })
1027
- })
1028
-
1029
- describe('challenge replay', () => {
1030
- test('behavior: challenge consumed after register/options → re-fetching is required', async () => {
1031
- // Get options twice — each should have a unique challenge stored in KV
1032
- const { options: a } = await ceremony.getRegistrationOptions({ name: 'Replay' })
1033
- const { options: b } = await ceremony.getRegistrationOptions({ name: 'Replay' })
1034
- expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
1035
- })
1036
-
1037
- test('behavior: challenge consumed after login/options → re-fetching is required', async () => {
1038
- const { options: a } = await ceremony.getAuthenticationOptions()
1039
- const { options: b } = await ceremony.getAuthenticationOptions()
1040
- expect(a.publicKey!.challenge).not.toBe(b.publicKey!.challenge)
1041
- })
1042
- })
1043
-
1044
- describe('hooks', () => {
1045
- test('behavior: onRegister error does not call hook', async () => {
1046
- let called = false
1047
- const hookServer = await createServer(
1048
- Handler.webAuthn({
1049
- kv: Kv.memory(),
1050
- origin: 'http://localhost',
1051
- rpId: 'localhost',
1052
- onRegister() {
1053
- called = true
1054
- return Response.json({ extra: true })
1055
- },
1056
- }).listener,
1057
- )
1058
-
1059
- const response = await fetch(`${hookServer.url}/register`, {
1060
- method: 'POST',
1061
- headers: { 'Content-Type': 'application/json' },
1062
- body: JSON.stringify({ id: 'fake', clientDataJSON: 'bad', attestationObject: 'bad' }),
1063
- })
1064
- expect(response.status).toBe(400)
1065
- expect(called).toBe(false)
1066
-
1067
- await hookServer.closeAsync()
1068
- })
1069
-
1070
- test('behavior: onAuthenticate error does not call hook', async () => {
1071
- let called = false
1072
- const hookServer = await createServer(
1073
- Handler.webAuthn({
1074
- kv: Kv.memory(),
1075
- origin: 'http://localhost',
1076
- rpId: 'localhost',
1077
- onAuthenticate() {
1078
- called = true
1079
- return Response.json({ extra: true })
1080
- },
1081
- }).listener,
1082
- )
1083
-
1084
- const response = await fetch(`${hookServer.url}/login`, {
1085
- method: 'POST',
1086
- headers: { 'Content-Type': 'application/json' },
1087
- body: JSON.stringify({
1088
- id: 'unknown',
1089
- metadata: { authenticatorData: '0x00', clientDataJSON: '{"challenge":"0xdead"}' },
1090
- raw: {
1091
- id: 'unknown',
1092
- type: 'public-key',
1093
- authenticatorAttachment: null,
1094
- rawId: 'unknown',
1095
- response: { clientDataJSON: 'e30' },
1096
- },
1097
- signature: '0x00',
1098
- }),
1099
- })
1100
- expect(response.status).toBe(400)
1101
- expect(called).toBe(false)
1102
-
1103
- await hookServer.closeAsync()
1104
- })
1105
- })
1106
- })