accounts 0.18.4 → 0.19.0

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 (71) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/core/AccessKey.d.ts +5 -1
  3. package/dist/core/AccessKey.d.ts.map +1 -1
  4. package/dist/core/AccessKey.js +2 -0
  5. package/dist/core/AccessKey.js.map +1 -1
  6. package/dist/core/Adapter.d.ts +4 -0
  7. package/dist/core/Adapter.d.ts.map +1 -1
  8. package/dist/core/Client.d.ts.map +1 -1
  9. package/dist/core/Client.js +62 -2
  10. package/dist/core/Client.js.map +1 -1
  11. package/dist/core/ExecutionError.d.ts.map +1 -1
  12. package/dist/core/ExecutionError.js +38 -3
  13. package/dist/core/ExecutionError.js.map +1 -1
  14. package/dist/core/Provider.d.ts.map +1 -1
  15. package/dist/core/Provider.js +16 -4
  16. package/dist/core/Provider.js.map +1 -1
  17. package/dist/core/Schema.d.ts +43 -0
  18. package/dist/core/Schema.d.ts.map +1 -1
  19. package/dist/core/adapters/privy.d.ts.map +1 -1
  20. package/dist/core/adapters/privy.js +7 -2
  21. package/dist/core/adapters/privy.js.map +1 -1
  22. package/dist/core/zod/rpc.d.ts +18 -0
  23. package/dist/core/zod/rpc.d.ts.map +1 -1
  24. package/dist/core/zod/rpc.js +13 -2
  25. package/dist/core/zod/rpc.js.map +1 -1
  26. package/dist/react/Remote.d.ts.map +1 -1
  27. package/dist/react/Remote.js +5 -1
  28. package/dist/react/Remote.js.map +1 -1
  29. package/dist/server/CliAuth.d.ts.map +1 -1
  30. package/dist/server/CliAuth.js +6 -4
  31. package/dist/server/CliAuth.js.map +1 -1
  32. package/dist/server/internal/handlers/relay.d.ts +0 -7
  33. package/dist/server/internal/handlers/relay.d.ts.map +1 -1
  34. package/dist/server/internal/handlers/relay.js +0 -46
  35. package/dist/server/internal/handlers/relay.js.map +1 -1
  36. package/dist/server/internal/handlers/sponsorship.d.ts +0 -2
  37. package/dist/server/internal/handlers/sponsorship.d.ts.map +1 -1
  38. package/dist/server/internal/handlers/sponsorship.js +1 -2
  39. package/dist/server/internal/handlers/sponsorship.js.map +1 -1
  40. package/dist/trusted-hosts.json +1 -0
  41. package/package.json +5 -5
  42. package/src/cli/Provider.localnet.test.ts +3 -3
  43. package/src/core/AccessKey.permissions.test.ts +73 -0
  44. package/src/core/AccessKey.test.ts +29 -27
  45. package/src/core/AccessKey.ts +7 -1
  46. package/src/core/Adapter.ts +4 -0
  47. package/src/core/Client.test.ts +153 -1
  48. package/src/core/Client.ts +73 -2
  49. package/src/core/ExecutionError.ts +39 -3
  50. package/src/core/Provider.localnet.test.ts +116 -1
  51. package/src/core/Provider.ts +25 -8
  52. package/src/core/Schema.test-d.ts +14 -0
  53. package/src/core/adapters/deviceCode/deviceCode.localnet.test.ts +4 -1
  54. package/src/core/adapters/privy.test.ts +150 -8
  55. package/src/core/adapters/privy.ts +13 -5
  56. package/src/core/zod/request.test.ts +26 -0
  57. package/src/core/zod/rpc.ts +15 -4
  58. package/src/react/Remote.test.ts +153 -0
  59. package/src/react/Remote.ts +7 -1
  60. package/src/server/CliAuth.test.ts +5 -3
  61. package/src/server/CliAuth.ts +6 -4
  62. package/src/server/internal/handlers/relay.ts +0 -56
  63. package/src/server/internal/handlers/sponsorship.ts +1 -4
  64. package/src/trusted-hosts.json +1 -0
  65. package/dist/server/internal/handlers/multisig.d.ts +0 -214
  66. package/dist/server/internal/handlers/multisig.d.ts.map +0 -1
  67. package/dist/server/internal/handlers/multisig.js +0 -505
  68. package/dist/server/internal/handlers/multisig.js.map +0 -1
  69. package/src/server/internal/handlers/multisig.localnet.test.ts +0 -94
  70. package/src/server/internal/handlers/multisig.test.ts +0 -743
  71. package/src/server/internal/handlers/multisig.ts +0 -774
@@ -570,13 +570,21 @@ export function privy<const client extends privy.Client>(
570
570
  },
571
571
  async revokeAccessKey(parameters) {
572
572
  const account = await getSigningAccount(parameters.address)
573
+ const { feePayer } = parameters
573
574
  try {
574
- await Actions.accessKey.revoke(getClient(), {
575
- account: account as never,
576
- accessKey: parameters.accessKeyAddress,
577
- })
575
+ await Actions.accessKey.revoke(
576
+ getClient({ feePayer: feePayer === true ? undefined : feePayer }),
577
+ {
578
+ account: account as never,
579
+ accessKey: parameters.accessKeyAddress,
580
+ ...(feePayer ? { feePayer: true } : {}),
581
+ ...(parameters.keyAuthorization
582
+ ? { keyAuthorization: parameters.keyAuthorization }
583
+ : {}),
584
+ },
585
+ )
578
586
  } catch (error) {
579
- if (!AccessKey.isUnavailableError(error)) throw error
587
+ if (typeof feePayer === 'string' || !AccessKey.isUnavailableError(error)) throw error
580
588
  }
581
589
  store.accessKeys.remove({
582
590
  accessKey: parameters.accessKeyAddress,
@@ -69,6 +69,32 @@ describe('validate', () => {
69
69
  `)
70
70
  })
71
71
 
72
+ test('behavior: preserves access-key revocation sponsorship', () => {
73
+ const result = RpcRequest.validate(Schema.Request, {
74
+ method: 'wallet_revokeAccessKey',
75
+ params: [
76
+ {
77
+ accessKeyAddress: '0x2222222222222222222222222222222222222222',
78
+ address: '0x1111111111111111111111111111111111111111',
79
+ feePayer: 'https://app.example.com/fee-payer',
80
+ },
81
+ ],
82
+ })
83
+
84
+ expect(result._decoded).toMatchInlineSnapshot(`
85
+ {
86
+ "method": "wallet_revokeAccessKey",
87
+ "params": [
88
+ {
89
+ "accessKeyAddress": "0x2222222222222222222222222222222222222222",
90
+ "address": "0x1111111111111111111111111111111111111111",
91
+ "feePayer": "https://app.example.com/fee-payer",
92
+ },
93
+ ],
94
+ }
95
+ `)
96
+ })
97
+
72
98
  test('default: validates wallet_switchEthereumChain with hex chainId', () => {
73
99
  const result = RpcRequest.validate(Schema.Request, {
74
100
  method: 'wallet_switchEthereumChain',
@@ -51,6 +51,7 @@ type KeyAuthorizationRpcDecoded = Omit<KeyAuthorization.Rpc, 'chainId' | 'expiry
51
51
  address?: KeyAuthorization.Rpc['keyId'] | undefined
52
52
  chainId: bigint
53
53
  expiry: number | null | undefined
54
+ keyType: z.output<typeof keyType>
54
55
  limits?:
55
56
  | readonly {
56
57
  token: KeyAuthorization.RpcTokenLimit['token']
@@ -133,7 +134,7 @@ export const keyAuthorization = z.codec(keyAuthorizationRpc, z.custom<KeyAuthori
133
134
  chainId: keyAuthorization.chainId === '0x' ? 0n : Hex.toBigInt(keyAuthorization.chainId),
134
135
  expiry: keyAuthorization.expiry == null ? null : Hex.toNumber(keyAuthorization.expiry),
135
136
  keyId: keyAuthorization.keyId,
136
- keyType: keyAuthorization.keyType,
137
+ keyType: z.parse(keyType, keyAuthorization.keyType),
137
138
  limits: keyAuthorization.limits?.map(({ limit, period, token }) => ({
138
139
  token,
139
140
  limit: Hex.toBigInt(limit),
@@ -540,11 +541,21 @@ export namespace wallet_authorizeAccessKey_strict {
540
541
  }
541
542
 
542
543
  export namespace wallet_revokeAccessKey {
544
+ /** Parameters for revoking an access key. */
545
+ export const parameters = z.object({
546
+ /** Root account address. */
547
+ address: u.address(),
548
+ /** Address of the access key to revoke. */
549
+ accessKeyAddress: u.address(),
550
+ /** App-provided fee sponsorship for the revocation transaction. */
551
+ feePayer: z.optional(z.union([z.boolean(), z.string()])),
552
+ /** Signed authorization for an access key that has not been published on-chain. */
553
+ keyAuthorization: z.optional(keyAuthorization),
554
+ })
555
+
543
556
  export const schema = Schema.defineItem({
544
557
  method: z.literal('wallet_revokeAccessKey'),
545
- params: z.readonly(
546
- z.tuple([z.object({ address: u.address(), accessKeyAddress: u.address() })]),
547
- ),
558
+ params: z.readonly(z.tuple([parameters])),
548
559
  returns: undefined,
549
560
  })
550
561
  export type Encoded = Schema.Encoded<typeof schema>
@@ -0,0 +1,153 @@
1
+ import { vi } from 'vitest'
2
+ import { afterEach, describe, expect, test } from 'vp/test'
3
+
4
+ const hooks = vi.hoisted(() => ({ cleanups: [] as (() => void)[] }))
5
+
6
+ vi.mock('react', () => ({
7
+ useCallback: (fn: unknown) => fn,
8
+ useEffect: (effect: () => void | (() => void)) => {
9
+ const cleanup = effect()
10
+ if (cleanup) hooks.cleanups.push(cleanup)
11
+ },
12
+ useMemo: (fn: () => unknown) => fn(),
13
+ useRef: (current: unknown) => ({ current }),
14
+ useState: (initial: unknown) => [initial, () => {}],
15
+ }))
16
+
17
+ import { useTheme } from './Remote.js'
18
+
19
+ afterEach(() => {
20
+ hooks.cleanups.length = 0
21
+ vi.unstubAllGlobals()
22
+ })
23
+
24
+ describe('useTheme', () => {
25
+ test('behavior: applies URL theme values to the document', () => {
26
+ const root = createRoot()
27
+ stubBrowser(root, '?accent=%23b9a3ff&radius=medium&scheme=dark')
28
+
29
+ useTheme()
30
+
31
+ expect(readRoot(root)).toMatchInlineSnapshot(`
32
+ {
33
+ "attributes": {
34
+ "data-theme": "dark",
35
+ "data-theme-accent": "custom",
36
+ "data-theme-radius": "medium",
37
+ },
38
+ "colorScheme": "dark",
39
+ "properties": {
40
+ "--theme-accent": "#b9a3ff",
41
+ },
42
+ }
43
+ `)
44
+ })
45
+
46
+ test('behavior: replaces URL overrides with live theme updates', () => {
47
+ const root = createRoot()
48
+ stubBrowser(root, '?accent=purple&radius=full&scheme=dark')
49
+ let update: ((theme: { scheme?: string }) => void) | undefined
50
+ const remote = {
51
+ messenger: {
52
+ on(_topic: string, listener: typeof update) {
53
+ update = listener
54
+ return () => {}
55
+ },
56
+ },
57
+ }
58
+ useTheme(remote as never)
59
+
60
+ update?.({ scheme: 'light' })
61
+
62
+ expect(readRoot(root)).toMatchInlineSnapshot(`
63
+ {
64
+ "attributes": {
65
+ "data-theme": "light",
66
+ },
67
+ "colorScheme": "light",
68
+ "properties": {},
69
+ }
70
+ `)
71
+ })
72
+
73
+ test('behavior: restores the document theme when unmounted', () => {
74
+ const root = createRoot(
75
+ {
76
+ 'data-theme': 'light',
77
+ 'data-theme-accent': 'custom',
78
+ 'data-theme-radius': 'small',
79
+ },
80
+ 'light',
81
+ { '--theme-accent': '#ff007a' },
82
+ )
83
+ stubBrowser(root, '?accent=blue&radius=full&scheme=dark')
84
+ useTheme()
85
+
86
+ hooks.cleanups[0]?.()
87
+
88
+ expect(readRoot(root)).toMatchInlineSnapshot(`
89
+ {
90
+ "attributes": {
91
+ "data-theme": "light",
92
+ "data-theme-accent": "custom",
93
+ "data-theme-radius": "small",
94
+ },
95
+ "colorScheme": "light",
96
+ "properties": {
97
+ "--theme-accent": "#ff007a",
98
+ },
99
+ }
100
+ `)
101
+ })
102
+ })
103
+
104
+ // Helpers
105
+
106
+ function stubBrowser(root: ReturnType<typeof createRoot>, search: string) {
107
+ vi.stubGlobal('document', { documentElement: root })
108
+ vi.stubGlobal('window', { location: { search } })
109
+ }
110
+
111
+ function createRoot(
112
+ initialAttributes: Record<string, string> = {},
113
+ colorScheme = '',
114
+ initialProperties: Record<string, string> = {},
115
+ ) {
116
+ const attributes = new Map(Object.entries(initialAttributes))
117
+ const properties = new Map(Object.entries(initialProperties))
118
+ return {
119
+ attributes,
120
+ getAttribute(name: string) {
121
+ return attributes.get(name) ?? null
122
+ },
123
+ removeAttribute(name: string) {
124
+ attributes.delete(name)
125
+ },
126
+ setAttribute(name: string, value: string) {
127
+ attributes.set(name, value)
128
+ },
129
+ style: {
130
+ colorScheme,
131
+ getPropertyValue(name: string) {
132
+ return properties.get(name) ?? ''
133
+ },
134
+ removeProperty(name: string) {
135
+ const value = properties.get(name) ?? ''
136
+ properties.delete(name)
137
+ return value
138
+ },
139
+ setProperty(name: string, value: string) {
140
+ properties.set(name, value)
141
+ },
142
+ },
143
+ properties,
144
+ }
145
+ }
146
+
147
+ function readRoot(root: ReturnType<typeof createRoot>) {
148
+ return {
149
+ attributes: Object.fromEntries(root.attributes),
150
+ colorScheme: root.style.colorScheme,
151
+ properties: Object.fromEntries(root.properties),
152
+ }
153
+ }
@@ -126,7 +126,10 @@ function applyTheme(theme: {
126
126
  root.setAttribute('data-theme-accent', getAccentName(accent))
127
127
  if (!isAccentPreset(accent)) root.style.setProperty('--theme-accent', accent)
128
128
  }
129
- if (scheme) root.style.colorScheme = scheme
129
+ if (scheme) {
130
+ root.style.colorScheme = scheme
131
+ root.setAttribute('data-theme', scheme)
132
+ }
130
133
  if (radius) root.setAttribute('data-theme-radius', radius)
131
134
  }
132
135
 
@@ -137,11 +140,13 @@ function captureTheme(): ThemeSnapshot {
137
140
  accentValue: root.style.getPropertyValue('--theme-accent'),
138
141
  colorScheme: root.style.colorScheme,
139
142
  radius: root.getAttribute('data-theme-radius'),
143
+ scheme: root.getAttribute('data-theme'),
140
144
  }
141
145
  }
142
146
 
143
147
  function restoreTheme(snapshot: ThemeSnapshot) {
144
148
  const root = document.documentElement
149
+ restoreAttribute(root, 'data-theme', snapshot.scheme)
145
150
  restoreAttribute(root, 'data-theme-accent', snapshot.accentPreset)
146
151
  restoreAttribute(root, 'data-theme-radius', snapshot.radius)
147
152
  if (snapshot.accentValue) root.style.setProperty('--theme-accent', snapshot.accentValue)
@@ -175,6 +180,7 @@ type ThemeSnapshot = {
175
180
  accentValue: string
176
181
  colorScheme: string
177
182
  radius: string | null
183
+ scheme: string | null
178
184
  }
179
185
 
180
186
  export declare namespace useEnsureVisibility {
@@ -1,5 +1,5 @@
1
1
  import { Base64, Hex } from 'ox'
2
- import { KeyAuthorization } from 'ox/tempo'
2
+ import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
3
3
  import { createClient, custom, encodeErrorResult, encodeFunctionResult } from 'viem'
4
4
  import { Abis, Account as TempoAccount } from 'viem/tempo'
5
5
  import { describe, expect, test } from 'vp/test'
@@ -89,6 +89,7 @@ async function authorize(
89
89
  code,
90
90
  keyAuthorization: z.decode(CliAuth.keyAuthorization, {
91
91
  ...keyAuthorization,
92
+ keyType: key.keyType,
92
93
  address: keyAuthorization.keyId,
93
94
  }),
94
95
  } satisfies z.output<typeof CliAuth.authorizeRequest>
@@ -126,6 +127,7 @@ async function authorizeWebAuthn(
126
127
  code,
127
128
  keyAuthorization: z.decode(CliAuth.keyAuthorization, {
128
129
  ...keyAuthorization,
130
+ keyType: key.keyType,
129
131
  address: keyAuthorization.keyId,
130
132
  }),
131
133
  } satisfies z.output<typeof CliAuth.authorizeRequest>
@@ -1199,7 +1201,7 @@ describe('poll', () => {
1199
1201
  keyAuthorization: {
1200
1202
  ...first.keyAuthorization,
1201
1203
  signature: {
1202
- type: first.keyAuthorization.signature.type,
1204
+ type: SignatureEnvelope.fromRpc(first.keyAuthorization.signature).type,
1203
1205
  },
1204
1206
  },
1205
1207
  }
@@ -1727,7 +1729,7 @@ describe('authorize', () => {
1727
1729
  ? {
1728
1730
  ...polled.keyAuthorization,
1729
1731
  signature: {
1730
- type: polled.keyAuthorization.signature.type,
1732
+ type: SignatureEnvelope.fromRpc(polled.keyAuthorization.signature).type,
1731
1733
  },
1732
1734
  }
1733
1735
  : undefined
@@ -765,11 +765,12 @@ export function from(options: from.Options = {}): CliAuth {
765
765
  })
766
766
 
767
767
  const client = options.client ?? cache.get(current.chainId)
768
+ const signature = SignatureEnvelope.fromRpc(actual.signature)
768
769
  const valid = await verifyHash(client, {
769
770
  address: options.request.accountAddress,
770
771
  hash: TempoKeyAuthorization.getSignPayload(signed),
771
- signature: SignatureEnvelope.serialize(SignatureEnvelope.fromRpc(actual.signature), {
772
- magic: actual.signature.type === 'webAuthn',
772
+ signature: SignatureEnvelope.serialize(signature, {
773
+ magic: signature.type === 'webAuthn',
773
774
  }),
774
775
  })
775
776
  if (!valid) throw new Error('Key authorization signature is invalid.')
@@ -1258,11 +1259,12 @@ async function verifyKeyAuthorizationSignature(options: {
1258
1259
  ...(actual.limits ? { limits: actual.limits } : {}),
1259
1260
  type: actual.keyType,
1260
1261
  })
1262
+ const signature = SignatureEnvelope.fromRpc(actual.signature)
1261
1263
  const valid = await verifyHash(options.client, {
1262
1264
  address: options.account,
1263
1265
  hash: TempoKeyAuthorization.getSignPayload(unsigned),
1264
- signature: SignatureEnvelope.serialize(SignatureEnvelope.fromRpc(actual.signature), {
1265
- magic: actual.signature.type === 'webAuthn',
1266
+ signature: SignatureEnvelope.serialize(signature, {
1267
+ magic: signature.type === 'webAuthn',
1266
1268
  }),
1267
1269
  })
1268
1270
  if (!valid) throw new Error('Key authorization signature is invalid.')
@@ -26,7 +26,6 @@ import { type Handler, from } from '../../Handler.js'
26
26
  import * as Kv from '../../Kv.js'
27
27
  import { cached } from '../kv.js'
28
28
  import * as Tokenlist from '../tokenlist.js'
29
- import * as Multisig from './multisig.js'
30
29
  import * as Sponsorship from './sponsorship.js'
31
30
  import * as Utils from './utils.js'
32
31
 
@@ -84,7 +83,6 @@ export function relay(options: relay.Options = {}): Handler {
84
83
  ...rest
85
84
  } = options
86
85
  const feePayerOptions = options.feePayer
87
- const multisigOptions = options.multisig
88
86
 
89
87
  // Resolves the verified tokenlist for `chainId`, sharing the KV cache with
90
88
  // `Handler.exchange` so a single `kv: Kv.cloudflare(env.KV)` covers both.
@@ -519,33 +517,6 @@ export function relay(options: relay.Options = {}): Handler {
519
517
  case 'eth_sendRawTransactionSync': {
520
518
  try {
521
519
  const serialized = params[0]
522
- if (
523
- multisigOptions &&
524
- (request.method === 'eth_sendRawTransaction' ||
525
- request.method === 'eth_sendRawTransactionSync') &&
526
- typeof serialized === 'string' &&
527
- Multisig.isMultisigTransaction(serialized as Hex.Hex)
528
- ) {
529
- const result = await Multisig.handleRawTransaction({
530
- claimTtl: multisigOptions.claimTtl,
531
- finalize: multisigOptions.finalize,
532
- getClient,
533
- method: request.method,
534
- request: { params: 'params' in request ? request.params : undefined },
535
- resolveConfig: multisigOptions.resolveConfig,
536
- sponsor: feePayerOptions
537
- ? {
538
- account: feePayerOptions.account,
539
- feeToken: feePayerOptions.feeToken,
540
- resolveFeeToken: async (chainId) => (await getTokens(chainId))[0],
541
- validate: feePayerOptions.validate,
542
- }
543
- : undefined,
544
- store: multisigOptions.store,
545
- })
546
- return RpcResponse.from({ result } as never, { request } as never)
547
- }
548
-
549
520
  if (!feePayerOptions) {
550
521
  // eth_signRawTransaction is a signing method that only the
551
522
  // relay can fulfill — forwarding it to the RPC node returns
@@ -590,27 +561,6 @@ export function relay(options: relay.Options = {}): Handler {
590
561
  }
591
562
  }
592
563
 
593
- case 'eth_getTransactionByHash':
594
- case 'eth_getTransactionReceipt': {
595
- try {
596
- if (multisigOptions) {
597
- const aliased = await Multisig.handleGetTransaction({
598
- getClient,
599
- method: request.method,
600
- request: { params: 'params' in request ? request.params : undefined },
601
- store: multisigOptions.store,
602
- })
603
- if (aliased)
604
- return RpcResponse.from({ result: aliased.result } as never, { request } as never)
605
- }
606
-
607
- const result = await client.request(request as never)
608
- return RpcResponse.from({ result }, { request })
609
- } catch (error) {
610
- return Utils.rpcErrorJson(request, error)
611
- }
612
- }
613
-
614
564
  default: {
615
565
  try {
616
566
  const result = await client.request(request as never)
@@ -696,12 +646,6 @@ export namespace relay {
696
646
  url?: string | undefined
697
647
  }
698
648
  | undefined
699
- /**
700
- * Native multisig configuration. When provided, the relay collects owner
701
- * approvals from multisig raw transaction submissions and broadcasts once
702
- * collected owner weight meets the configured threshold.
703
- */
704
- multisig?: Multisig.Options | undefined
705
649
  /**
706
650
  * Relay features.
707
651
  *
@@ -121,8 +121,7 @@ export async function handleRawTransaction(options: handleRawTransaction.Options
121
121
  })
122
122
 
123
123
  const transaction = Transaction.deserialize(serialized)
124
- // Prefer sender recovered from raw envelope; multisig finalize path supplies fallback sender.
125
- const sender = transaction.from ?? options.sender
124
+ const sender = transaction.from
126
125
 
127
126
  // Sponsorship only applies after sender has signed original transaction.
128
127
  if (!transaction.signature || !sender)
@@ -178,8 +177,6 @@ export declare namespace handleRawTransaction {
178
177
  method: 'eth_signRawTransaction' | 'eth_sendRawTransaction' | 'eth_sendRawTransactionSync'
179
178
  /** Incoming JSON-RPC request. */
180
179
  request: { params?: readonly unknown[] | undefined }
181
- /** Sender address to use if it cannot be recovered from the raw envelope. */
182
- sender?: Address | undefined
183
180
  /** Optional sponsorship approval callback. */
184
181
  validate?: ((request: Transaction.TransactionRequest) => boolean | Promise<boolean>) | undefined
185
182
  }
@@ -3,6 +3,7 @@
3
3
  "localhost",
4
4
  "*.localhost",
5
5
  "*.tempo.xyz",
6
+ "mercator.sh",
6
7
  "paradigm.xyz",
7
8
  "promptgolf.sh",
8
9
  "app.polyhedge.capital",