accounts 0.4.1 → 0.4.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.
- package/CHANGELOG.md +7 -0
- package/README.md +38 -7
- package/dist/cli/Provider.d.ts +12 -0
- package/dist/cli/Provider.d.ts.map +1 -0
- package/dist/cli/Provider.js +19 -0
- package/dist/cli/Provider.js.map +1 -0
- package/dist/cli/adapter.d.ts +24 -0
- package/dist/cli/adapter.d.ts.map +1 -0
- package/dist/cli/adapter.js +173 -0
- package/dist/cli/adapter.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/Dialog.d.ts.map +1 -1
- package/dist/core/Dialog.js +25 -1
- package/dist/core/Dialog.js.map +1 -1
- package/dist/core/IntersectionObserver.d.ts +3 -0
- package/dist/core/IntersectionObserver.d.ts.map +1 -0
- package/dist/core/IntersectionObserver.js +6 -0
- package/dist/core/IntersectionObserver.js.map +1 -0
- package/dist/core/Messenger.d.ts +14 -3
- package/dist/core/Messenger.d.ts.map +1 -1
- package/dist/core/Messenger.js +4 -4
- package/dist/core/Messenger.js.map +1 -1
- package/dist/core/Remote.d.ts +6 -3
- package/dist/core/Remote.d.ts.map +1 -1
- package/dist/core/Remote.js +3 -6
- package/dist/core/Remote.js.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +2 -2
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Remote.d.ts +21 -0
- package/dist/react/Remote.d.ts.map +1 -0
- package/dist/react/Remote.js +51 -0
- package/dist/react/Remote.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/CliAuth.d.ts +553 -0
- package/dist/server/CliAuth.d.ts.map +1 -0
- package/dist/server/CliAuth.js +446 -0
- package/dist/server/CliAuth.js.map +1 -0
- package/dist/server/Handler.d.ts +36 -2
- package/dist/server/Handler.d.ts.map +1 -1
- package/dist/server/Handler.js +84 -0
- package/dist/server/Handler.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +16 -1
- package/src/cli/Provider.test-d.ts +28 -0
- package/src/cli/Provider.test.ts +235 -0
- package/src/cli/Provider.ts +26 -0
- package/src/cli/adapter.ts +229 -0
- package/src/cli/index.ts +2 -0
- package/src/core/Dialog.ts +31 -1
- package/src/core/IntersectionObserver.ts +6 -0
- package/src/core/Messenger.ts +18 -8
- package/src/core/Provider.test.ts +12 -2
- package/src/core/Remote.ts +9 -10
- package/src/core/adapters/local.ts +7 -2
- package/src/index.ts +1 -0
- package/src/react/Remote.ts +94 -0
- package/src/react/index.ts +1 -0
- package/src/server/CliAuth.test-d.ts +56 -0
- package/src/server/CliAuth.test.ts +800 -0
- package/src/server/CliAuth.ts +634 -0
- package/src/server/Handler.ts +123 -1
- package/src/server/index.ts +1 -0
package/src/core/Dialog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as IO from './IntersectionObserver.js'
|
|
1
2
|
import * as Messenger from './Messenger.js'
|
|
2
3
|
import type * as Store from './Store.js'
|
|
3
4
|
|
|
@@ -207,6 +208,19 @@ export function iframe(): Dialog {
|
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
messenger.on('switch-mode', () => {
|
|
212
|
+
hideDialog()
|
|
213
|
+
activatePage()
|
|
214
|
+
open = false
|
|
215
|
+
|
|
216
|
+
const pending = store
|
|
217
|
+
.getState()
|
|
218
|
+
.requestQueue.filter(
|
|
219
|
+
(x): x is Store.QueuedRequest & { status: 'pending' } => x.status === 'pending',
|
|
220
|
+
)
|
|
221
|
+
if (pending.length > 0) fallback.syncRequests(pending)
|
|
222
|
+
})
|
|
223
|
+
|
|
210
224
|
return {
|
|
211
225
|
close() {
|
|
212
226
|
fallback.close()
|
|
@@ -241,7 +255,23 @@ export function iframe(): Dialog {
|
|
|
241
255
|
isSafari() &&
|
|
242
256
|
requests.some((x) => ['wallet_connect', 'eth_requestAccounts'].includes(x.request.method))
|
|
243
257
|
|
|
244
|
-
|
|
258
|
+
const secure = await (async () => {
|
|
259
|
+
const { trustedHosts } = await messenger.waitForReady()
|
|
260
|
+
const ioSupported = IO.supported()
|
|
261
|
+
const trusted = Boolean(trustedHosts?.includes(window.location.hostname))
|
|
262
|
+
return ioSupported || trusted
|
|
263
|
+
})()
|
|
264
|
+
|
|
265
|
+
if (unsupported || !secure) {
|
|
266
|
+
if (!secure)
|
|
267
|
+
console.warn(
|
|
268
|
+
[
|
|
269
|
+
`[accounts] Browser does not support IntersectionObserver v2 and "${window.location.hostname}" is not a trusted host.`,
|
|
270
|
+
'Falling back to popup dialog.',
|
|
271
|
+
'',
|
|
272
|
+
'To enable the iframe dialog, add your hostname to the trusted hosts list.',
|
|
273
|
+
].join('\n'),
|
|
274
|
+
)
|
|
245
275
|
fallback.syncRequests(requests)
|
|
246
276
|
} else {
|
|
247
277
|
const requiresConfirm = requests.some((x) => x.status === 'pending')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether IntersectionObserver v2 (with `isVisible`) is supported. */
|
|
2
|
+
export const supported = () =>
|
|
3
|
+
'IntersectionObserver' in window &&
|
|
4
|
+
'IntersectionObserverEntry' in window &&
|
|
5
|
+
'intersectionRatio' in IntersectionObserverEntry.prototype &&
|
|
6
|
+
'isVisible' in IntersectionObserverEntry.prototype
|
package/src/core/Messenger.ts
CHANGED
|
@@ -20,19 +20,25 @@ export type Messenger = {
|
|
|
20
20
|
) => Promise<{ id: string; topic: topic; payload: Payload<topic> }>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Options sent with the `ready` signal from the remote frame. */
|
|
24
|
+
export type ReadyOptions = {
|
|
25
|
+
/** Hostnames trusted by the remote embed to render in an iframe. */
|
|
26
|
+
trustedHosts?: string[] | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
/** Bridge messenger that waits for a `ready` signal from the remote frame. */
|
|
24
30
|
export type Bridge = Messenger & {
|
|
25
31
|
/** Signal readiness (called by the remote frame). */
|
|
26
|
-
ready: () => void
|
|
32
|
+
ready: (options?: ReadyOptions | undefined) => void
|
|
27
33
|
/** Promise that resolves when the remote frame signals ready. */
|
|
28
|
-
waitForReady: () => Promise<
|
|
34
|
+
waitForReady: () => Promise<ReadyOptions>
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/** Message schema for cross-frame communication. */
|
|
32
38
|
export type Schema = [
|
|
33
39
|
{
|
|
34
40
|
topic: 'ready'
|
|
35
|
-
payload:
|
|
41
|
+
payload: ReadyOptions
|
|
36
42
|
},
|
|
37
43
|
{
|
|
38
44
|
topic: 'rpc-requests'
|
|
@@ -52,6 +58,10 @@ export type Schema = [
|
|
|
52
58
|
topic: 'close'
|
|
53
59
|
payload: undefined
|
|
54
60
|
},
|
|
61
|
+
{
|
|
62
|
+
topic: 'switch-mode'
|
|
63
|
+
payload: { mode: 'popup' }
|
|
64
|
+
},
|
|
55
65
|
]
|
|
56
66
|
|
|
57
67
|
/** Union of all topic strings. */
|
|
@@ -116,8 +126,8 @@ export function bridge(parameters: bridge.Parameters): Bridge {
|
|
|
116
126
|
|
|
117
127
|
let pending = false
|
|
118
128
|
|
|
119
|
-
const ready = withResolvers<
|
|
120
|
-
from_.on('ready', ready.resolve)
|
|
129
|
+
const ready = withResolvers<ReadyOptions>()
|
|
130
|
+
from_.on('ready', (payload) => ready.resolve(payload ?? {}))
|
|
121
131
|
|
|
122
132
|
const messenger = from({
|
|
123
133
|
destroy() {
|
|
@@ -137,8 +147,8 @@ export function bridge(parameters: bridge.Parameters): Bridge {
|
|
|
137
147
|
|
|
138
148
|
return {
|
|
139
149
|
...messenger,
|
|
140
|
-
ready() {
|
|
141
|
-
void messenger.send('ready',
|
|
150
|
+
ready(options) {
|
|
151
|
+
void messenger.send('ready', options ?? {})
|
|
142
152
|
},
|
|
143
153
|
waitForReady() {
|
|
144
154
|
return ready.promise
|
|
@@ -169,7 +179,7 @@ export function noop(): Bridge {
|
|
|
169
179
|
},
|
|
170
180
|
ready() {},
|
|
171
181
|
waitForReady() {
|
|
172
|
-
return Promise.resolve()
|
|
182
|
+
return Promise.resolve({})
|
|
173
183
|
},
|
|
174
184
|
}
|
|
175
185
|
}
|
|
@@ -1482,10 +1482,15 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
|
|
|
1482
1482
|
|
|
1483
1483
|
const connected = await connect(provider)
|
|
1484
1484
|
await fund(connected)
|
|
1485
|
+
const syncTransferCall = Actions.token.transfer.call({
|
|
1486
|
+
to: '0x0000000000000000000000000000000000000001',
|
|
1487
|
+
token: Addresses.pathUsd,
|
|
1488
|
+
amount: parseUnits('2', 6),
|
|
1489
|
+
})
|
|
1485
1490
|
|
|
1486
1491
|
const receipt = await provider.request({
|
|
1487
1492
|
method: 'eth_sendTransactionSync',
|
|
1488
|
-
params: [{ calls: [
|
|
1493
|
+
params: [{ calls: [syncTransferCall], feePayer: server.url }],
|
|
1489
1494
|
})
|
|
1490
1495
|
|
|
1491
1496
|
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
@@ -1536,10 +1541,15 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
|
|
|
1536
1541
|
|
|
1537
1542
|
const connected = await connect(provider)
|
|
1538
1543
|
await fund(connected)
|
|
1544
|
+
const syncTransferCall = Actions.token.transfer.call({
|
|
1545
|
+
to: '0x0000000000000000000000000000000000000001',
|
|
1546
|
+
token: Addresses.pathUsd,
|
|
1547
|
+
amount: parseUnits('3', 6),
|
|
1548
|
+
})
|
|
1539
1549
|
|
|
1540
1550
|
const receipt = await provider.request({
|
|
1541
1551
|
method: 'eth_sendTransactionSync',
|
|
1542
|
-
params: [{ calls: [
|
|
1552
|
+
params: [{ calls: [syncTransferCall], feePayer: true }],
|
|
1543
1553
|
})
|
|
1544
1554
|
|
|
1545
1555
|
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
package/src/core/Remote.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Hex } from 'ox'
|
|
2
2
|
import * as Provider from 'ox/Provider'
|
|
3
3
|
import * as RpcResponse from 'ox/RpcResponse'
|
|
4
|
-
import { useStore } from 'zustand'
|
|
5
4
|
import type { StoreApi } from 'zustand/vanilla'
|
|
6
5
|
import { createStore } from 'zustand/vanilla'
|
|
7
6
|
|
|
@@ -35,6 +34,10 @@ export type Remote = {
|
|
|
35
34
|
* Remote context store.
|
|
36
35
|
*/
|
|
37
36
|
store: StoreApi<State>
|
|
37
|
+
/**
|
|
38
|
+
* Hostnames trusted to render the embed in an iframe.
|
|
39
|
+
*/
|
|
40
|
+
trustedHosts: readonly string[]
|
|
38
41
|
/**
|
|
39
42
|
* Subscribes to user-facing RPC requests from the parent context.
|
|
40
43
|
*
|
|
@@ -109,7 +112,7 @@ export declare namespace respond {
|
|
|
109
112
|
|
|
110
113
|
/** Creates a remote context for the dialog app. */
|
|
111
114
|
export function create(options: create.Options): Remote {
|
|
112
|
-
const { messenger, provider } = options
|
|
115
|
+
const { messenger, provider, trustedHosts } = options
|
|
113
116
|
const ready =
|
|
114
117
|
typeof window !== 'undefined' && !new URLSearchParams(window.location.search).get('mode')
|
|
115
118
|
const store = createStore<State>(() => ({
|
|
@@ -123,6 +126,7 @@ export function create(options: create.Options): Remote {
|
|
|
123
126
|
messenger,
|
|
124
127
|
provider,
|
|
125
128
|
store,
|
|
129
|
+
trustedHosts: trustedHosts ?? [],
|
|
126
130
|
|
|
127
131
|
onUserRequest(cb) {
|
|
128
132
|
return this.onRequests(async (requests, event, { account }) => {
|
|
@@ -171,7 +175,7 @@ export function create(options: create.Options): Remote {
|
|
|
171
175
|
},
|
|
172
176
|
|
|
173
177
|
ready() {
|
|
174
|
-
messenger.ready()
|
|
178
|
+
messenger.ready({ trustedHosts })
|
|
175
179
|
|
|
176
180
|
if (typeof window !== 'undefined') {
|
|
177
181
|
const params = new URLSearchParams(window.location.search)
|
|
@@ -251,12 +255,7 @@ export declare namespace create {
|
|
|
251
255
|
messenger: Messenger.Bridge
|
|
252
256
|
/** Provider to execute RPC requests against. */
|
|
253
257
|
provider: CoreProvider.Provider
|
|
258
|
+
/** Hostnames trusted to render the embed in an iframe. */
|
|
259
|
+
trustedHosts?: string[] | undefined
|
|
254
260
|
}
|
|
255
261
|
}
|
|
256
|
-
|
|
257
|
-
/** React hook to select state from a remote context's store. */
|
|
258
|
-
export function useState(remote: Remote): State
|
|
259
|
-
export function useState<selected>(remote: Remote, selector: (state: State) => selected): selected
|
|
260
|
-
export function useState(remote: Remote, selector?: (state: State) => unknown) {
|
|
261
|
-
return useStore(remote.store, selector as never)
|
|
262
|
-
}
|
|
@@ -207,8 +207,13 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
207
207
|
},
|
|
208
208
|
async signTypedData({ data, address }) {
|
|
209
209
|
const account = getAccount({ address, signable: true })
|
|
210
|
-
const
|
|
211
|
-
|
|
210
|
+
const parsed = JSON.parse(data) as {
|
|
211
|
+
domain: Record<string, unknown>
|
|
212
|
+
message: Record<string, unknown>
|
|
213
|
+
primaryType: string
|
|
214
|
+
types: Record<string, unknown>
|
|
215
|
+
}
|
|
216
|
+
return await account.signTypedData(parsed)
|
|
212
217
|
},
|
|
213
218
|
async sendTransaction(parameters) {
|
|
214
219
|
const { feePayer, ...rest } = parameters
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState as react_useState } from 'react'
|
|
2
|
+
import { useStore } from 'zustand'
|
|
3
|
+
|
|
4
|
+
import * as IO from '../core/IntersectionObserver.js'
|
|
5
|
+
import type * as CoreRemote from '../core/Remote.js'
|
|
6
|
+
|
|
7
|
+
/** Monitors element visibility using IntersectionObserver v2. */
|
|
8
|
+
export function useEnsureVisibility(
|
|
9
|
+
remote: CoreRemote.Remote,
|
|
10
|
+
options: useEnsureVisibility.Options = {},
|
|
11
|
+
): useEnsureVisibility.ReturnType {
|
|
12
|
+
const { enabled = true } = options
|
|
13
|
+
|
|
14
|
+
const origin = useState(remote, (s) => s.origin)
|
|
15
|
+
|
|
16
|
+
const trusted = useMemo(() => {
|
|
17
|
+
if (!origin) return false
|
|
18
|
+
try {
|
|
19
|
+
const hostname = new URL(origin).hostname
|
|
20
|
+
return remote.trustedHosts.includes(hostname)
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}, [origin, remote.trustedHosts])
|
|
25
|
+
|
|
26
|
+
const active = enabled && !trusted
|
|
27
|
+
|
|
28
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
29
|
+
const [visible, setVisible] = react_useState(true)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!active) return
|
|
33
|
+
if (!ref.current) return
|
|
34
|
+
|
|
35
|
+
if (!IO.supported()) {
|
|
36
|
+
setVisible(false)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const observer = new IntersectionObserver(
|
|
41
|
+
(entries) => {
|
|
42
|
+
const entry = entries[0]
|
|
43
|
+
if (!entry) return
|
|
44
|
+
const isVisible =
|
|
45
|
+
(entry as unknown as { isVisible: boolean | undefined }).isVisible || false
|
|
46
|
+
setVisible(isVisible)
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
delay: 100,
|
|
50
|
+
threshold: [0.99],
|
|
51
|
+
trackVisibility: true,
|
|
52
|
+
} as IntersectionObserverInit,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
observer.observe(ref.current)
|
|
56
|
+
return () => observer.disconnect()
|
|
57
|
+
}, [active])
|
|
58
|
+
|
|
59
|
+
const invokePopup = useCallback(
|
|
60
|
+
() => remote.messenger.send('switch-mode', { mode: 'popup' }),
|
|
61
|
+
[remote],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return { invokePopup, ref, visible }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** React hook to select state from a remote context's store. */
|
|
68
|
+
export function useState(remote: CoreRemote.Remote): CoreRemote.State
|
|
69
|
+
export function useState<selected>(
|
|
70
|
+
remote: CoreRemote.Remote,
|
|
71
|
+
selector: (state: CoreRemote.State) => selected,
|
|
72
|
+
): selected
|
|
73
|
+
export function useState(
|
|
74
|
+
remote: CoreRemote.Remote,
|
|
75
|
+
selector?: (state: CoreRemote.State) => unknown,
|
|
76
|
+
) {
|
|
77
|
+
return useStore(remote.store, selector as never)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export declare namespace useEnsureVisibility {
|
|
81
|
+
type Options = {
|
|
82
|
+
/** Whether visibility monitoring is enabled. @default true */
|
|
83
|
+
enabled?: boolean | undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type ReturnType = {
|
|
87
|
+
/** Requests the host switch to a popup dialog. */
|
|
88
|
+
invokePopup: () => void
|
|
89
|
+
/** Ref to attach to the element being monitored. */
|
|
90
|
+
ref: React.RefObject<HTMLDivElement | null>
|
|
91
|
+
/** Whether the element is currently visible. */
|
|
92
|
+
visible: boolean
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Remote from './Remote.js'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Hex } from 'viem'
|
|
2
|
+
import { describe, expectTypeOf, test } from 'vp/test'
|
|
3
|
+
import * as z from 'zod/mini'
|
|
4
|
+
|
|
5
|
+
import * as CliAuth from './CliAuth.js'
|
|
6
|
+
|
|
7
|
+
describe('createRequest', () => {
|
|
8
|
+
test('includes the v1 device-code request fields', () => {
|
|
9
|
+
expectTypeOf<z.output<typeof CliAuth.createRequest>>().toMatchTypeOf<{
|
|
10
|
+
account?: Hex | undefined
|
|
11
|
+
codeChallenge: string
|
|
12
|
+
expiry?: number | undefined
|
|
13
|
+
keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined
|
|
14
|
+
limits?: readonly { token: Hex; limit: bigint }[] | undefined
|
|
15
|
+
pubKey: Hex
|
|
16
|
+
}>()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('does not include scopes in v1', () => {
|
|
20
|
+
type Request = z.output<typeof CliAuth.createRequest>
|
|
21
|
+
expectTypeOf<Request>().not.toHaveProperty('scopes')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('pollResponse', () => {
|
|
26
|
+
test('authorized responses carry the normal keyAuthorization shape', () => {
|
|
27
|
+
type Response = Extract<z.output<typeof CliAuth.pollResponse>, { status: 'authorized' }>
|
|
28
|
+
expectTypeOf<Response>().toMatchTypeOf<{
|
|
29
|
+
accountAddress: Hex
|
|
30
|
+
keyAuthorization: z.output<typeof CliAuth.keyAuthorization>
|
|
31
|
+
status: 'authorized'
|
|
32
|
+
}>()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('pendingResponse', () => {
|
|
37
|
+
test('pending responses expose the browser approval payload', () => {
|
|
38
|
+
expectTypeOf<z.output<typeof CliAuth.pendingResponse>>().toMatchTypeOf<{
|
|
39
|
+
accessKeyAddress: Hex
|
|
40
|
+
account?: Hex | undefined
|
|
41
|
+
chainId: bigint
|
|
42
|
+
code: string
|
|
43
|
+
expiry: number
|
|
44
|
+
keyType: 'secp256k1' | 'p256' | 'webAuthn'
|
|
45
|
+
limits?: readonly { token: Hex; limit: bigint }[] | undefined
|
|
46
|
+
pubKey: Hex
|
|
47
|
+
status: 'pending'
|
|
48
|
+
}>()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('Store', () => {
|
|
53
|
+
test('memory helper satisfies the shared store contract', () => {
|
|
54
|
+
expectTypeOf(CliAuth.Store.memory).returns.toMatchTypeOf<CliAuth.Store>()
|
|
55
|
+
})
|
|
56
|
+
})
|