@passlock/client 0.9.22 → 0.9.24
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/README.md +10 -5
- package/README.template.md +130 -0
- package/dist/authentication/authenticate.d.ts +16 -16
- package/dist/authentication/authenticate.fixture.d.ts +21 -7
- package/dist/authentication/authenticate.fixture.js +7 -5
- package/dist/authentication/authenticate.fixture.js.map +1 -1
- package/dist/authentication/authenticate.js +19 -8
- package/dist/authentication/authenticate.js.map +1 -1
- package/dist/capabilities/capabilities.d.ts +9 -5
- package/dist/capabilities/capabilities.js +11 -2
- package/dist/capabilities/capabilities.js.map +1 -1
- package/dist/connection/connection.d.ts +11 -7
- package/dist/connection/connection.fixture.d.ts +2 -2
- package/dist/connection/connection.fixture.js +2 -1
- package/dist/connection/connection.fixture.js.map +1 -1
- package/dist/connection/connection.js +12 -3
- package/dist/connection/connection.js.map +1 -1
- package/dist/effect.d.ts +23 -46
- package/dist/effect.js +55 -51
- package/dist/effect.js.map +1 -1
- package/dist/email/email.d.ts +42 -12
- package/dist/email/email.fixture.d.ts +19 -5
- package/dist/email/email.fixture.js +5 -4
- package/dist/email/email.fixture.js.map +1 -1
- package/dist/email/email.js +44 -8
- package/dist/email/email.js.map +1 -1
- package/dist/event/event.d.ts +4 -2
- package/dist/event/event.js +4 -1
- package/dist/event/event.js.map +1 -1
- package/dist/index.d.ts +105 -27
- package/dist/index.js +101 -50
- package/dist/index.js.map +1 -1
- package/dist/logging/eventLogger.d.ts +13 -1
- package/dist/logging/eventLogger.js +14 -1
- package/dist/logging/eventLogger.js.map +1 -1
- package/dist/registration/register.d.ts +19 -22
- package/dist/registration/register.fixture.d.ts +20 -6
- package/dist/registration/register.fixture.js +14 -7
- package/dist/registration/register.fixture.js.map +1 -1
- package/dist/registration/register.js +18 -9
- package/dist/registration/register.js.map +1 -1
- package/dist/rpc/authentication.d.ts +1 -2
- package/dist/rpc/authentication.js +2 -1
- package/dist/rpc/authentication.js.map +1 -1
- package/dist/rpc/client.d.ts +5 -2
- package/dist/rpc/client.js +12 -3
- package/dist/rpc/client.js.map +1 -1
- package/dist/rpc/config.d.ts +0 -1
- package/dist/rpc/connection.d.ts +1 -2
- package/dist/rpc/connection.js +2 -1
- package/dist/rpc/connection.js.map +1 -1
- package/dist/rpc/registration.d.ts +1 -2
- package/dist/rpc/registration.js +2 -1
- package/dist/rpc/registration.js.map +1 -1
- package/dist/rpc/social.d.ts +1 -2
- package/dist/rpc/social.js +2 -1
- package/dist/rpc/social.js.map +1 -1
- package/dist/rpc/user.d.ts +1 -2
- package/dist/rpc/user.js +2 -1
- package/dist/rpc/user.js.map +1 -1
- package/dist/social/social.d.ts +17 -24
- package/dist/social/social.fixture.d.ts +22 -10
- package/dist/social/social.fixture.js +8 -14
- package/dist/social/social.fixture.js.map +1 -1
- package/dist/social/social.js +15 -11
- package/dist/social/social.js.map +1 -1
- package/dist/storage/storage.d.ts +41 -13
- package/dist/storage/storage.fixture.d.ts +2 -2
- package/dist/storage/storage.fixture.js +2 -2
- package/dist/storage/storage.fixture.js.map +1 -1
- package/dist/storage/storage.js +52 -15
- package/dist/storage/storage.js.map +1 -1
- package/dist/test/fixtures.d.ts +2 -3
- package/dist/test/fixtures.js +20 -5
- package/dist/test/fixtures.js.map +1 -1
- package/dist/user/user.d.ts +9 -6
- package/dist/user/user.fixture.d.ts +2 -2
- package/dist/user/user.fixture.js +9 -5
- package/dist/user/user.fixture.js.map +1 -1
- package/dist/user/user.js +12 -3
- package/dist/user/user.js.map +1 -1
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +38 -30
- package/src/authentication/authenticate.fixture.ts +10 -7
- package/src/authentication/authenticate.test.ts +61 -18
- package/src/authentication/authenticate.ts +37 -33
- package/src/capabilities/capabilities.ts +11 -9
- package/src/connection/connection.fixture.ts +4 -1
- package/src/connection/connection.test.ts +4 -3
- package/src/connection/connection.ts +10 -8
- package/src/effect.ts +129 -134
- package/src/email/email.fixture.ts +7 -4
- package/src/email/email.test.ts +6 -5
- package/src/email/email.ts +27 -17
- package/src/event/event.node.test.ts +1 -0
- package/src/event/event.test.ts +1 -0
- package/src/event/event.ts +2 -1
- package/src/index.ts +235 -173
- package/src/logging/eventLogger.test.ts +2 -1
- package/src/logging/eventLogger.ts +3 -3
- package/src/registration/register.fixture.ts +16 -8
- package/src/registration/register.test.ts +16 -10
- package/src/registration/register.ts +37 -35
- package/src/rpc/authentication.ts +43 -0
- package/src/rpc/client.ts +174 -0
- package/src/rpc/config.ts +18 -0
- package/src/rpc/connection.ts +30 -0
- package/src/rpc/registration.ts +41 -0
- package/src/rpc/social.ts +45 -0
- package/src/rpc/user.ts +57 -0
- package/src/social/social.fixture.ts +12 -18
- package/src/social/social.test.ts +16 -30
- package/src/social/social.ts +22 -47
- package/src/storage/storage.fixture.ts +4 -4
- package/src/storage/storage.test.ts +29 -19
- package/src/storage/storage.ts +37 -36
- package/src/test/fixtures.ts +23 -6
- package/src/user/user.fixture.ts +19 -7
- package/src/user/user.test.ts +3 -5
- package/src/user/user.ts +16 -10
- package/src/version.ts +1 -0
- package/dist/authentication/authenticate.d.ts.map +0 -1
- package/dist/authentication/authenticate.fixture.d.ts.map +0 -1
- package/dist/capabilities/capabilities.d.ts.map +0 -1
- package/dist/config.d.ts +0 -18
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -20
- package/dist/config.js.map +0 -1
- package/dist/connection/connection.d.ts.map +0 -1
- package/dist/connection/connection.fixture.d.ts.map +0 -1
- package/dist/effect.d.ts.map +0 -1
- package/dist/email/email.d.ts.map +0 -1
- package/dist/email/email.fixture.d.ts.map +0 -1
- package/dist/event/event.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/logging/eventLogger.d.ts.map +0 -1
- package/dist/registration/register.d.ts.map +0 -1
- package/dist/registration/register.fixture.d.ts.map +0 -1
- package/dist/rpc/authentication.d.ts.map +0 -1
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/config.d.ts.map +0 -1
- package/dist/rpc/connection.d.ts.map +0 -1
- package/dist/rpc/registration.d.ts.map +0 -1
- package/dist/rpc/social.d.ts.map +0 -1
- package/dist/rpc/user.d.ts.map +0 -1
- package/dist/social/social.d.ts.map +0 -1
- package/dist/social/social.fixture.d.ts.map +0 -1
- package/dist/storage/storage.d.ts.map +0 -1
- package/dist/storage/storage.fixture.d.ts.map +0 -1
- package/dist/test/fixtures.d.ts.map +0 -1
- package/dist/user/user.d.ts.map +0 -1
- package/dist/user/user.fixture.d.ts.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/src/config.ts +0 -42
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Duplicate, NotFound } from '@passlock/shared/dist/error/error.js'
|
|
2
|
-
import { SocialClient } from '@passlock/shared/dist/rpc/social.js'
|
|
3
1
|
import { Effect as E, Layer as L, Layer, LogLevel, Logger, pipe } from 'effect'
|
|
4
2
|
import { describe, expect, test } from 'vitest'
|
|
5
3
|
import { mock } from 'vitest-mock-extended'
|
|
4
|
+
|
|
5
|
+
import { Duplicate, NotFound } from '@passlock/shared/dist/error/error.js'
|
|
6
|
+
|
|
6
7
|
import * as Fixture from './social.fixture.js'
|
|
8
|
+
import { SocialClient } from '../rpc/social.js'
|
|
7
9
|
import { SocialService, SocialServiceLive } from './social.js'
|
|
8
10
|
|
|
9
11
|
describe('registerOidc should', () => {
|
|
@@ -25,10 +27,7 @@ describe('registerOidc should', () => {
|
|
|
25
27
|
}),
|
|
26
28
|
)
|
|
27
29
|
|
|
28
|
-
const service = pipe(
|
|
29
|
-
SocialServiceLive,
|
|
30
|
-
L.provide(rpcClientTest),
|
|
31
|
-
)
|
|
30
|
+
const service = pipe(SocialServiceLive, L.provide(rpcClientTest))
|
|
32
31
|
|
|
33
32
|
const layers = Layer.merge(service, rpcClientTest)
|
|
34
33
|
const effect = pipe(E.provide(assertions, layers), Logger.withMinimumLogLevel(LogLevel.None))
|
|
@@ -42,7 +41,7 @@ describe('registerOidc should', () => {
|
|
|
42
41
|
yield* _(service.registerOidc(Fixture.registerOidcReq))
|
|
43
42
|
|
|
44
43
|
const rpcClient = yield* _(SocialClient)
|
|
45
|
-
expect(rpcClient.registerOidc).toHaveBeenCalledWith(Fixture.
|
|
44
|
+
expect(rpcClient.registerOidc).toHaveBeenCalledWith(Fixture.registerOidcReq)
|
|
46
45
|
})
|
|
47
46
|
|
|
48
47
|
const rpcClientTest = L.effect(
|
|
@@ -56,10 +55,7 @@ describe('registerOidc should', () => {
|
|
|
56
55
|
}),
|
|
57
56
|
)
|
|
58
57
|
|
|
59
|
-
const service = pipe(
|
|
60
|
-
SocialServiceLive,
|
|
61
|
-
L.provide(rpcClientTest),
|
|
62
|
-
)
|
|
58
|
+
const service = pipe(SocialServiceLive, L.provide(rpcClientTest))
|
|
63
59
|
|
|
64
60
|
const layers = Layer.merge(service, rpcClientTest)
|
|
65
61
|
const effect = pipe(E.provide(assertions, layers), Logger.withMinimumLogLevel(LogLevel.None))
|
|
@@ -81,16 +77,13 @@ describe('registerOidc should', () => {
|
|
|
81
77
|
E.sync(() => {
|
|
82
78
|
const rpcMock = mock<SocialClient['Type']>()
|
|
83
79
|
|
|
84
|
-
rpcMock.registerOidc.mockReturnValue(E.fail(new Duplicate({ message:
|
|
80
|
+
rpcMock.registerOidc.mockReturnValue(E.fail(new Duplicate({ message: 'Duplicate user' })))
|
|
85
81
|
|
|
86
82
|
return rpcMock
|
|
87
83
|
}),
|
|
88
84
|
)
|
|
89
85
|
|
|
90
|
-
const service = pipe(
|
|
91
|
-
SocialServiceLive,
|
|
92
|
-
L.provide(rpcClientTest),
|
|
93
|
-
)
|
|
86
|
+
const service = pipe(SocialServiceLive, L.provide(rpcClientTest))
|
|
94
87
|
|
|
95
88
|
const layers = Layer.merge(service, rpcClientTest)
|
|
96
89
|
const effect = pipe(E.provide(assertions, layers), Logger.withMinimumLogLevel(LogLevel.None))
|
|
@@ -118,10 +111,7 @@ describe('authenticateIodc should', () => {
|
|
|
118
111
|
}),
|
|
119
112
|
)
|
|
120
113
|
|
|
121
|
-
const service = pipe(
|
|
122
|
-
SocialServiceLive,
|
|
123
|
-
L.provide(rpcClientTest),
|
|
124
|
-
)
|
|
114
|
+
const service = pipe(SocialServiceLive, L.provide(rpcClientTest))
|
|
125
115
|
|
|
126
116
|
const layers = Layer.merge(service, rpcClientTest)
|
|
127
117
|
const effect = pipe(E.provide(assertions, layers), Logger.withMinimumLogLevel(LogLevel.None))
|
|
@@ -135,7 +125,7 @@ describe('authenticateIodc should', () => {
|
|
|
135
125
|
yield* _(service.authenticateOidc(Fixture.authOidcReq))
|
|
136
126
|
|
|
137
127
|
const rpcClient = yield* _(SocialClient)
|
|
138
|
-
expect(rpcClient.authenticateOidc).toHaveBeenCalledWith(Fixture.
|
|
128
|
+
expect(rpcClient.authenticateOidc).toHaveBeenCalledWith(Fixture.authOidcReq)
|
|
139
129
|
})
|
|
140
130
|
|
|
141
131
|
const rpcClientTest = L.effect(
|
|
@@ -149,10 +139,7 @@ describe('authenticateIodc should', () => {
|
|
|
149
139
|
}),
|
|
150
140
|
)
|
|
151
141
|
|
|
152
|
-
const service = pipe(
|
|
153
|
-
SocialServiceLive,
|
|
154
|
-
L.provide(rpcClientTest),
|
|
155
|
-
)
|
|
142
|
+
const service = pipe(SocialServiceLive, L.provide(rpcClientTest))
|
|
156
143
|
|
|
157
144
|
const layers = Layer.merge(service, rpcClientTest)
|
|
158
145
|
const effect = pipe(E.provide(assertions, layers), Logger.withMinimumLogLevel(LogLevel.None))
|
|
@@ -174,16 +161,15 @@ describe('authenticateIodc should', () => {
|
|
|
174
161
|
E.sync(() => {
|
|
175
162
|
const rpcMock = mock<SocialClient['Type']>()
|
|
176
163
|
|
|
177
|
-
rpcMock.authenticateOidc.mockReturnValue(
|
|
164
|
+
rpcMock.authenticateOidc.mockReturnValue(
|
|
165
|
+
E.fail(new NotFound({ message: 'User not found' })),
|
|
166
|
+
)
|
|
178
167
|
|
|
179
168
|
return rpcMock
|
|
180
169
|
}),
|
|
181
170
|
)
|
|
182
171
|
|
|
183
|
-
const service = pipe(
|
|
184
|
-
SocialServiceLive,
|
|
185
|
-
L.provide(rpcClientTest),
|
|
186
|
-
)
|
|
172
|
+
const service = pipe(SocialServiceLive, L.provide(rpcClientTest))
|
|
187
173
|
|
|
188
174
|
const layers = Layer.merge(service, rpcClientTest)
|
|
189
175
|
const effect = pipe(E.provide(assertions, layers), Logger.withMinimumLogLevel(LogLevel.None))
|
package/src/social/social.ts
CHANGED
|
@@ -1,51 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Passkey authentication effects
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
type BadRequest,
|
|
6
|
-
type NotSupported
|
|
7
|
-
} from '@passlock/shared/dist/error/error.js'
|
|
8
|
-
import * as Shared from '@passlock/shared/dist/rpc/social.js'
|
|
9
|
-
import { SocialClient } from '@passlock/shared/dist/rpc/social.js'
|
|
10
|
-
import type {
|
|
11
|
-
Principal
|
|
12
|
-
} from '@passlock/shared/dist/schema/principal.js'
|
|
13
4
|
import { Context, Effect as E, Layer, flow } from 'effect'
|
|
14
5
|
|
|
6
|
+
import * as RPC from '@passlock/shared/dist/rpc/social.js'
|
|
7
|
+
import { type BadRequest, type NotSupported } from '@passlock/shared/dist/error/error.js'
|
|
8
|
+
import type { Principal } from '@passlock/shared/dist/schema/principal.js'
|
|
9
|
+
|
|
10
|
+
import { SocialClient } from '../rpc/social.js'
|
|
11
|
+
|
|
15
12
|
/* Requests */
|
|
16
13
|
|
|
17
14
|
export type Provider = 'apple' | 'google'
|
|
18
15
|
|
|
19
|
-
export type RegisterOidcReq =
|
|
20
|
-
provider: Provider
|
|
21
|
-
idToken: string
|
|
22
|
-
nonce: string
|
|
23
|
-
givenName?: string
|
|
24
|
-
familyName?: string
|
|
25
|
-
}
|
|
16
|
+
export type RegisterOidcReq = RPC.RegisterOidcReq
|
|
26
17
|
|
|
27
|
-
export type AuthenticateOidcReq =
|
|
28
|
-
provider: Provider
|
|
29
|
-
idToken: string
|
|
30
|
-
nonce: string
|
|
31
|
-
}
|
|
18
|
+
export type AuthenticateOidcReq = RPC.AuthOidcReq
|
|
32
19
|
|
|
33
20
|
/* Errors */
|
|
34
21
|
|
|
35
|
-
export type RegistrationErrors = NotSupported | BadRequest |
|
|
22
|
+
export type RegistrationErrors = NotSupported | BadRequest | RPC.RegisterOidcErrors
|
|
36
23
|
|
|
37
|
-
export type AuthenticationErrors = NotSupported | BadRequest |
|
|
24
|
+
export type AuthenticationErrors = NotSupported | BadRequest | RPC.AuthOidcErrors
|
|
38
25
|
|
|
39
26
|
/* Service */
|
|
40
27
|
|
|
41
|
-
export
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
28
|
+
export class SocialService extends Context.Tag('@services/SocialService')<
|
|
29
|
+
SocialService,
|
|
30
|
+
{
|
|
31
|
+
registerOidc: (req: RegisterOidcReq) => E.Effect<Principal, RegistrationErrors>
|
|
32
|
+
authenticateOidc: (req: AuthenticateOidcReq) => E.Effect<Principal, AuthenticationErrors>
|
|
33
|
+
}
|
|
34
|
+
>() {}
|
|
49
35
|
|
|
50
36
|
/* Effects */
|
|
51
37
|
|
|
@@ -58,16 +44,8 @@ export const registerOidc = (
|
|
|
58
44
|
yield* _(E.logInfo('Registering social account'))
|
|
59
45
|
|
|
60
46
|
const rpcClient = yield* _(SocialClient)
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
...request,
|
|
64
|
-
...(request.givenName ? { givenName: request.givenName } : {}),
|
|
65
|
-
...(request.familyName ? { familyName: request.familyName } : {})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const { principal } = yield* _(
|
|
69
|
-
rpcClient.registerOidc(rpcRequest)
|
|
70
|
-
)
|
|
47
|
+
const rpcRequest = new RPC.RegisterOidcReq(request)
|
|
48
|
+
const { principal } = yield* _(rpcClient.registerOidc(rpcRequest))
|
|
71
49
|
|
|
72
50
|
return principal
|
|
73
51
|
})
|
|
@@ -80,11 +58,8 @@ export const authenticateOidc = (
|
|
|
80
58
|
yield* _(E.logInfo('Authenticating with social account'))
|
|
81
59
|
|
|
82
60
|
const rpcClient = yield* _(SocialClient)
|
|
83
|
-
const rpcRequest = new
|
|
84
|
-
|
|
85
|
-
const { principal } = yield* _(
|
|
86
|
-
rpcClient.authenticateOidc(rpcRequest)
|
|
87
|
-
)
|
|
61
|
+
const rpcRequest = new RPC.AuthOidcReq(request)
|
|
62
|
+
const { principal } = yield* _(rpcClient.authenticateOidc(rpcRequest))
|
|
88
63
|
|
|
89
64
|
return principal
|
|
90
65
|
})
|
|
@@ -100,7 +75,7 @@ export const SocialServiceLive = Layer.effect(
|
|
|
100
75
|
|
|
101
76
|
return SocialService.of({
|
|
102
77
|
registerOidc: flow(registerOidc, E.provide(context)),
|
|
103
|
-
authenticateOidc: flow(authenticateOidc, E.provide(context))
|
|
78
|
+
authenticateOidc: flow(authenticateOidc, E.provide(context)),
|
|
104
79
|
})
|
|
105
80
|
}),
|
|
106
81
|
)
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Effect as E, Layer, pipe } from 'effect'
|
|
2
2
|
import { mock } from 'vitest-mock-extended'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { BrowserStorage, StorageServiceLive } from './storage.js'
|
|
4
5
|
|
|
5
6
|
const storageTest = Layer.effect(
|
|
6
|
-
|
|
7
|
+
BrowserStorage,
|
|
7
8
|
E.sync(() => mock<Storage>()),
|
|
8
9
|
)
|
|
9
10
|
|
|
10
|
-
export const testLayers = (storage: Layer.Layer<
|
|
11
|
+
export const testLayers = (storage: Layer.Layer<BrowserStorage> = storageTest) => {
|
|
11
12
|
const storageService = pipe(StorageServiceLive, Layer.provide(storage))
|
|
12
|
-
|
|
13
13
|
return Layer.merge(storage, storageService)
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { Effect as E, Layer, LogLevel, Logger, identity, pipe } from 'effect'
|
|
2
2
|
import { describe, expect, test } from 'vitest'
|
|
3
3
|
import { mock } from 'vitest-mock-extended'
|
|
4
|
+
|
|
4
5
|
import { principal, testLayers } from './storage.fixture.js'
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
BrowserStorage,
|
|
8
|
+
StorageService,
|
|
9
|
+
clearExpiredToken,
|
|
10
|
+
clearToken,
|
|
11
|
+
getToken,
|
|
12
|
+
} from './storage.js'
|
|
6
13
|
|
|
7
14
|
// eslint chokes on expect(storage.setItem) etc
|
|
8
15
|
/* eslint @typescript-eslint/unbound-method: 0 */
|
|
@@ -13,7 +20,7 @@ describe('storeToken should', () => {
|
|
|
13
20
|
const service = yield* _(StorageService)
|
|
14
21
|
yield* _(service.storeToken(principal))
|
|
15
22
|
|
|
16
|
-
const storage = yield* _(
|
|
23
|
+
const storage = yield* _(BrowserStorage)
|
|
17
24
|
expect(storage.setItem).toHaveBeenCalled()
|
|
18
25
|
})
|
|
19
26
|
|
|
@@ -30,7 +37,7 @@ describe('storeToken should', () => {
|
|
|
30
37
|
const service = yield* _(StorageService)
|
|
31
38
|
yield* _(service.storeToken(principal))
|
|
32
39
|
|
|
33
|
-
const storage = yield* _(
|
|
40
|
+
const storage = yield* _(BrowserStorage)
|
|
34
41
|
expect(storage.setItem).toHaveBeenCalledWith('passlock:passkey:token', expect.any(String))
|
|
35
42
|
})
|
|
36
43
|
|
|
@@ -47,10 +54,13 @@ describe('storeToken should', () => {
|
|
|
47
54
|
const service = yield* _(StorageService)
|
|
48
55
|
yield* _(service.storeToken(principal))
|
|
49
56
|
|
|
50
|
-
const storage = yield* _(
|
|
51
|
-
const token = principal.
|
|
52
|
-
const expiry = principal.
|
|
53
|
-
expect(storage.setItem).toHaveBeenCalledWith(
|
|
57
|
+
const storage = yield* _(BrowserStorage)
|
|
58
|
+
const token = principal.jti
|
|
59
|
+
const expiry = principal.exp.getTime()
|
|
60
|
+
expect(storage.setItem).toHaveBeenCalledWith(
|
|
61
|
+
'passlock:passkey:token',
|
|
62
|
+
`${token}:${expiry.toFixed(0)}`,
|
|
63
|
+
)
|
|
54
64
|
})
|
|
55
65
|
|
|
56
66
|
const effect = pipe(
|
|
@@ -68,17 +78,17 @@ describe('getToken should', () => {
|
|
|
68
78
|
const service = yield* _(StorageService)
|
|
69
79
|
yield* _(service.getToken('passkey'))
|
|
70
80
|
|
|
71
|
-
const storage = yield* _(
|
|
81
|
+
const storage = yield* _(BrowserStorage)
|
|
72
82
|
expect(storage.getItem).toHaveBeenCalled()
|
|
73
83
|
expect(storage.getItem).toHaveBeenCalledWith('passlock:passkey:token')
|
|
74
84
|
})
|
|
75
85
|
|
|
76
86
|
const storageTest = Layer.effect(
|
|
77
|
-
|
|
87
|
+
BrowserStorage,
|
|
78
88
|
E.sync(() => {
|
|
79
89
|
const mockStorage = mock<Storage>()
|
|
80
90
|
const expiry = Date.now() + 1000
|
|
81
|
-
mockStorage.getItem.mockReturnValue(`token:${expiry}`)
|
|
91
|
+
mockStorage.getItem.mockReturnValue(`token:${expiry.toFixed(0)}`)
|
|
82
92
|
return mockStorage
|
|
83
93
|
}),
|
|
84
94
|
)
|
|
@@ -106,11 +116,11 @@ describe('getToken should', () => {
|
|
|
106
116
|
)
|
|
107
117
|
|
|
108
118
|
const storageTest = Layer.effect(
|
|
109
|
-
|
|
119
|
+
BrowserStorage,
|
|
110
120
|
E.sync(() => {
|
|
111
121
|
const mockStorage = mock<Storage>()
|
|
112
122
|
const expiry = Date.now() - 1000
|
|
113
|
-
mockStorage.getItem.mockReturnValue(`token:${expiry}`)
|
|
123
|
+
mockStorage.getItem.mockReturnValue(`token:${expiry.toFixed(0)}`)
|
|
114
124
|
return mockStorage
|
|
115
125
|
}),
|
|
116
126
|
)
|
|
@@ -127,7 +137,7 @@ describe('getToken should', () => {
|
|
|
127
137
|
describe('clearToken should', () => {
|
|
128
138
|
test('clear the token in local storage', () => {
|
|
129
139
|
const assertions = E.gen(function* (_) {
|
|
130
|
-
const storage = yield* _(
|
|
140
|
+
const storage = yield* _(BrowserStorage)
|
|
131
141
|
yield* _(clearToken('passkey'))
|
|
132
142
|
expect(storage.removeItem).toHaveBeenCalledWith('passlock:passkey:token')
|
|
133
143
|
})
|
|
@@ -144,18 +154,18 @@ describe('clearToken should', () => {
|
|
|
144
154
|
describe('clearExpiredToken should', () => {
|
|
145
155
|
test('clear an expired token from local storage', () => {
|
|
146
156
|
const assertions = E.gen(function* (_) {
|
|
147
|
-
const storage = yield* _(
|
|
157
|
+
const storage = yield* _(BrowserStorage)
|
|
148
158
|
yield* _(clearExpiredToken('passkey'))
|
|
149
159
|
expect(storage.getItem).toHaveBeenCalledWith('passlock:passkey:token')
|
|
150
160
|
expect(storage.removeItem).toHaveBeenCalledWith('passlock:passkey:token')
|
|
151
161
|
})
|
|
152
162
|
|
|
153
163
|
const storageTest = Layer.effect(
|
|
154
|
-
|
|
164
|
+
BrowserStorage,
|
|
155
165
|
E.sync(() => {
|
|
156
166
|
const mockStorage = mock<Storage>()
|
|
157
167
|
const expiry = Date.now() - 1000
|
|
158
|
-
mockStorage.getItem.mockReturnValue(`token:${expiry}`)
|
|
168
|
+
mockStorage.getItem.mockReturnValue(`token:${expiry.toFixed(0)}`)
|
|
159
169
|
return mockStorage
|
|
160
170
|
}),
|
|
161
171
|
)
|
|
@@ -170,18 +180,18 @@ describe('clearExpiredToken should', () => {
|
|
|
170
180
|
|
|
171
181
|
test('leave a live token in local storage', () => {
|
|
172
182
|
const assertions = E.gen(function* (_) {
|
|
173
|
-
const storage = yield* _(
|
|
183
|
+
const storage = yield* _(BrowserStorage)
|
|
174
184
|
yield* _(clearExpiredToken('passkey'))
|
|
175
185
|
expect(storage.getItem).toHaveBeenCalledWith('passlock:passkey:token')
|
|
176
186
|
expect(storage.removeItem).not.toHaveBeenCalled()
|
|
177
187
|
})
|
|
178
188
|
|
|
179
189
|
const storageTest = Layer.effect(
|
|
180
|
-
|
|
190
|
+
BrowserStorage,
|
|
181
191
|
E.sync(() => {
|
|
182
192
|
const mockStorage = mock<Storage>()
|
|
183
193
|
const expiry = Date.now() + 1000
|
|
184
|
-
mockStorage.getItem.mockReturnValue(`token:${expiry}`)
|
|
194
|
+
mockStorage.getItem.mockReturnValue(`token:${expiry.toFixed(0)}`)
|
|
185
195
|
return mockStorage
|
|
186
196
|
}),
|
|
187
197
|
)
|
package/src/storage/storage.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Wrapper around local storage that allows us to store
|
|
3
3
|
* authentication tokens in local storage for a short period.
|
|
4
4
|
*/
|
|
5
|
-
import type { Principal } from '@passlock/shared/dist/schema/principal.js'
|
|
6
5
|
import { Context, Effect as E, Layer, Option as O, flow, pipe } from 'effect'
|
|
7
6
|
import type { NoSuchElementException } from 'effect/Cause'
|
|
8
7
|
|
|
8
|
+
import type { Principal } from '@passlock/shared/dist/schema/principal.js'
|
|
9
|
+
|
|
9
10
|
/* Requests */
|
|
10
11
|
|
|
11
12
|
export type AuthType = 'email' | 'passkey' | 'apple' | 'google'
|
|
@@ -13,33 +14,31 @@ export type AuthType = 'email' | 'passkey' | 'apple' | 'google'
|
|
|
13
14
|
export type StoredToken = {
|
|
14
15
|
token: string
|
|
15
16
|
authType: AuthType
|
|
16
|
-
|
|
17
|
+
expiry: number
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/* Service */
|
|
20
21
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export const StorageService = Context.GenericTag<StorageService>('@services/StorageService')
|
|
22
|
+
export class StorageService extends Context.Tag('@services/StorageService')<
|
|
23
|
+
StorageService,
|
|
24
|
+
{
|
|
25
|
+
storeToken: (principal: Principal) => E.Effect<void>
|
|
26
|
+
getToken: (authType: AuthType) => E.Effect<StoredToken, NoSuchElementException>
|
|
27
|
+
clearToken: (authType: AuthType) => E.Effect<void>
|
|
28
|
+
clearExpiredToken: (authType: AuthType) => E.Effect<void>
|
|
29
|
+
clearExpiredTokens: E.Effect<void>
|
|
30
|
+
}
|
|
31
|
+
>() {}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
export const Storage = Context.GenericTag<Storage>('@services/Storage')
|
|
33
|
+
export class BrowserStorage extends Context.Tag('@services/Storage')<BrowserStorage, Storage>() {}
|
|
35
34
|
|
|
36
35
|
export const buildKey = (authType: AuthType) => `passlock:${authType}:token`
|
|
37
36
|
|
|
38
37
|
// principal => token:expireAt
|
|
39
38
|
export const compressToken = (principal: Principal): string => {
|
|
40
|
-
const expireAt = principal.
|
|
41
|
-
const token = principal.
|
|
42
|
-
return `${token}:${expireAt}`
|
|
39
|
+
const expireAt = principal.exp.getTime()
|
|
40
|
+
const token = principal.jti
|
|
41
|
+
return `${token}:${expireAt.toFixed(0)}`
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
// token:expireAt => { authType, token, expireAt }
|
|
@@ -50,10 +49,12 @@ export const expandToken =
|
|
|
50
49
|
if (tokens.length !== 2) return O.none()
|
|
51
50
|
|
|
52
51
|
const [token, expireAtString] = tokens
|
|
52
|
+
if (token === undefined || expireAtString === undefined) return O.none()
|
|
53
|
+
|
|
53
54
|
const parse = O.liftThrowable(Number.parseInt)
|
|
54
55
|
const expireAt = parse(expireAtString)
|
|
55
56
|
|
|
56
|
-
return O.map(expireAt,
|
|
57
|
+
return O.map(expireAt, expiry => ({ authType, token, expiry }))
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/* Effects */
|
|
@@ -63,13 +64,13 @@ export const expandToken =
|
|
|
63
64
|
* @param principal
|
|
64
65
|
* @returns
|
|
65
66
|
*/
|
|
66
|
-
export const storeToken = (principal: Principal): E.Effect<void, never,
|
|
67
|
+
export const storeToken = (principal: Principal): E.Effect<void, never, BrowserStorage> => {
|
|
67
68
|
return E.gen(function* (_) {
|
|
68
|
-
const localStorage = yield* _(
|
|
69
|
+
const localStorage = yield* _(BrowserStorage)
|
|
69
70
|
|
|
70
71
|
const storeEffect = E.try(() => {
|
|
71
72
|
const compressed = compressToken(principal)
|
|
72
|
-
const key = buildKey(principal.
|
|
73
|
+
const key = buildKey(principal.authType)
|
|
73
74
|
localStorage.setItem(key, compressed)
|
|
74
75
|
}).pipe(E.orElse(() => E.void)) // We dont care if it fails
|
|
75
76
|
|
|
@@ -79,20 +80,20 @@ export const storeToken = (principal: Principal): E.Effect<void, never, Storage>
|
|
|
79
80
|
|
|
80
81
|
/**
|
|
81
82
|
* Get stored token from local storage
|
|
82
|
-
* @param
|
|
83
|
+
* @param authenticator
|
|
83
84
|
* @returns
|
|
84
85
|
*/
|
|
85
86
|
export const getToken = (
|
|
86
|
-
|
|
87
|
-
): E.Effect<StoredToken, NoSuchElementException,
|
|
87
|
+
authenticator: AuthType,
|
|
88
|
+
): E.Effect<StoredToken, NoSuchElementException, BrowserStorage> => {
|
|
88
89
|
return E.gen(function* (_) {
|
|
89
|
-
const localStorage = yield* _(
|
|
90
|
+
const localStorage = yield* _(BrowserStorage)
|
|
90
91
|
|
|
91
92
|
const getEffect = pipe(
|
|
92
|
-
O.some(buildKey(
|
|
93
|
+
O.some(buildKey(authenticator)),
|
|
93
94
|
O.flatMap(key => pipe(localStorage.getItem(key), O.fromNullable)),
|
|
94
|
-
O.flatMap(expandToken(
|
|
95
|
-
O.filter(({
|
|
95
|
+
O.flatMap(expandToken(authenticator)),
|
|
96
|
+
O.filter(({ expiry }) => expiry > Date.now()),
|
|
96
97
|
)
|
|
97
98
|
|
|
98
99
|
return yield* _(getEffect)
|
|
@@ -104,9 +105,9 @@ export const getToken = (
|
|
|
104
105
|
* @param authType
|
|
105
106
|
* @returns
|
|
106
107
|
*/
|
|
107
|
-
export const clearToken = (authType: AuthType): E.Effect<void, never,
|
|
108
|
+
export const clearToken = (authType: AuthType): E.Effect<void, never, BrowserStorage> => {
|
|
108
109
|
return E.gen(function* (_) {
|
|
109
|
-
const localStorage = yield* _(
|
|
110
|
+
const localStorage = yield* _(BrowserStorage)
|
|
110
111
|
localStorage.removeItem(buildKey(authType))
|
|
111
112
|
})
|
|
112
113
|
}
|
|
@@ -117,15 +118,15 @@ export const clearToken = (authType: AuthType): E.Effect<void, never, Storage> =
|
|
|
117
118
|
* @param defer
|
|
118
119
|
* @returns
|
|
119
120
|
*/
|
|
120
|
-
export const clearExpiredToken = (authType: AuthType): E.Effect<void, never,
|
|
121
|
+
export const clearExpiredToken = (authType: AuthType): E.Effect<void, never, BrowserStorage> => {
|
|
121
122
|
const key = buildKey(authType)
|
|
122
123
|
|
|
123
124
|
const effect = E.gen(function* (_) {
|
|
124
|
-
const storage = yield* _(
|
|
125
|
+
const storage = yield* _(BrowserStorage)
|
|
125
126
|
const item = yield* _(O.fromNullable(storage.getItem(key)))
|
|
126
127
|
const token = yield* _(expandToken(authType)(item))
|
|
127
128
|
|
|
128
|
-
if (token.
|
|
129
|
+
if (token.expiry < Date.now()) {
|
|
129
130
|
storage.removeItem(key)
|
|
130
131
|
}
|
|
131
132
|
})
|
|
@@ -140,7 +141,7 @@ export const clearExpiredToken = (authType: AuthType): E.Effect<void, never, Sto
|
|
|
140
141
|
)
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
export const clearExpiredTokens: E.Effect<void, never,
|
|
144
|
+
export const clearExpiredTokens: E.Effect<void, never, BrowserStorage> = E.all([
|
|
144
145
|
clearExpiredToken('passkey'),
|
|
145
146
|
clearExpiredToken('email'),
|
|
146
147
|
clearExpiredToken('google'),
|
|
@@ -153,7 +154,7 @@ export const clearExpiredTokens: E.Effect<void, never, Storage> = E.all([
|
|
|
153
154
|
export const StorageServiceLive = Layer.effect(
|
|
154
155
|
StorageService,
|
|
155
156
|
E.gen(function* (_) {
|
|
156
|
-
const context = yield* _(E.context<
|
|
157
|
+
const context = yield* _(E.context<BrowserStorage>())
|
|
157
158
|
|
|
158
159
|
return {
|
|
159
160
|
storeToken: flow(storeToken, E.provide(context)),
|
package/src/test/fixtures.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Effect as E, Layer as L } from 'effect'
|
|
2
|
+
|
|
1
3
|
import { BadRequest } from '@passlock/shared/dist/error/error.js'
|
|
2
4
|
import type { Principal } from '@passlock/shared/dist/schema/principal.js'
|
|
3
|
-
|
|
5
|
+
|
|
4
6
|
import { Capabilities } from '../capabilities/capabilities.js'
|
|
5
7
|
import { StorageService, type StoredToken } from '../storage/storage.js'
|
|
6
8
|
|
|
@@ -8,15 +10,30 @@ export const session = 'session'
|
|
|
8
10
|
export const token = 'token'
|
|
9
11
|
export const code = 'code'
|
|
10
12
|
export const authType = 'passkey'
|
|
11
|
-
export const
|
|
13
|
+
export const expiry = Date.now() + 10000
|
|
12
14
|
|
|
13
15
|
export const principal: Principal = {
|
|
16
|
+
jti: 'token',
|
|
14
17
|
token: 'token',
|
|
18
|
+
sub: 'user-1',
|
|
19
|
+
iss: 'idp.passlock.dev',
|
|
20
|
+
aud: 'tenancy_id',
|
|
21
|
+
iat: new Date(),
|
|
22
|
+
nbf: new Date(),
|
|
23
|
+
exp: new Date(Date.now() + 5 * 60 * 1000),
|
|
24
|
+
email: 'john.doe@gmail.com',
|
|
25
|
+
givenName: 'john',
|
|
26
|
+
familyName: 'doe',
|
|
27
|
+
emailVerified: false,
|
|
28
|
+
authType: 'passkey',
|
|
29
|
+
authId: 'auth-1',
|
|
30
|
+
userVerified: true,
|
|
31
|
+
// legacy
|
|
15
32
|
user: {
|
|
16
|
-
id: '1',
|
|
17
|
-
email: 'john.doe@gmail.com',
|
|
33
|
+
id: 'user-1',
|
|
18
34
|
givenName: 'john',
|
|
19
35
|
familyName: 'doe',
|
|
36
|
+
email: 'john.doe@gmail.com',
|
|
20
37
|
emailVerified: false,
|
|
21
38
|
},
|
|
22
39
|
authStatement: {
|
|
@@ -37,7 +54,7 @@ export const capabilitiesTest = L.succeed(
|
|
|
37
54
|
}),
|
|
38
55
|
)
|
|
39
56
|
|
|
40
|
-
export const storedToken: StoredToken = { token, authType,
|
|
57
|
+
export const storedToken: StoredToken = { token, authType, expiry }
|
|
41
58
|
|
|
42
59
|
export const storageServiceTest = L.succeed(
|
|
43
60
|
StorageService,
|
|
@@ -50,4 +67,4 @@ export const storageServiceTest = L.succeed(
|
|
|
50
67
|
}),
|
|
51
68
|
)
|
|
52
69
|
|
|
53
|
-
export const notImplemented = new BadRequest({ message: 'Not implemented' })
|
|
70
|
+
export const notImplemented = new BadRequest({ message: 'Not implemented' })
|
package/src/user/user.fixture.ts
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { Effect as E, Layer as L, Option as O } from 'effect'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
IsExistingUserReq,
|
|
5
|
+
IsExistingUserRes,
|
|
6
|
+
ResendEmailReq,
|
|
7
|
+
ResendEmailRes,
|
|
8
|
+
VerifyEmailRes,
|
|
9
|
+
} from '@passlock/shared/dist/rpc/user.js'
|
|
10
|
+
|
|
3
11
|
import * as Fixtures from '../test/fixtures.js'
|
|
12
|
+
import { UserClient } from '../rpc/user.js'
|
|
4
13
|
import type { ResendEmail } from './user.js'
|
|
5
14
|
|
|
6
15
|
export const email = 'jdoe@gmail.com'
|
|
7
16
|
export const isRegisteredReq = new IsExistingUserReq({ email })
|
|
8
|
-
export const isRegisteredRes = new IsExistingUserRes({ existingUser: false })
|
|
17
|
+
export const isRegisteredRes = new IsExistingUserRes({ existingUser: false, detail: O.none() })
|
|
9
18
|
export const verifyEmailRes = new VerifyEmailRes({ principal: Fixtures.principal })
|
|
10
19
|
export const resendEmailReq: ResendEmail = { userId: '123', method: 'code' }
|
|
11
|
-
export const rpcResendEmailReq = new ResendEmailReq({
|
|
12
|
-
|
|
20
|
+
export const rpcResendEmailReq = new ResendEmailReq({
|
|
21
|
+
userId: '123',
|
|
22
|
+
verifyEmail: { method: 'code' },
|
|
23
|
+
})
|
|
24
|
+
export const rpcResendEmailRes = new ResendEmailRes({})
|
|
13
25
|
|
|
14
26
|
export const rpcClientTest = L.succeed(
|
|
15
27
|
UserClient,
|
|
16
28
|
UserClient.of({
|
|
17
|
-
isExistingUser: () => E.succeed({ existingUser: true }),
|
|
29
|
+
isExistingUser: () => E.succeed({ existingUser: true, detail: O.none() }),
|
|
18
30
|
verifyEmail: () => E.succeed(verifyEmailRes),
|
|
19
31
|
resendVerificationEmail: () => E.fail(Fixtures.notImplemented),
|
|
20
32
|
}),
|
|
21
|
-
)
|
|
33
|
+
)
|
package/src/user/user.test.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { UserClient } from '@passlock/shared/dist/rpc/user.js'
|
|
2
1
|
import { Effect as E, Layer as L, Layer, LogLevel, Logger, pipe } from 'effect'
|
|
3
2
|
import { describe, expect, test } from 'vitest'
|
|
4
3
|
import { mock } from 'vitest-mock-extended'
|
|
4
|
+
|
|
5
5
|
import * as Fixture from './user.fixture.js'
|
|
6
|
+
import { UserClient } from '../rpc/user.js'
|
|
6
7
|
import { UserService, UserServiceLive } from './user.js'
|
|
7
8
|
|
|
8
9
|
describe('isExistingUser should', () => {
|
|
@@ -76,10 +77,7 @@ describe('resendVerificationEmail should', () => {
|
|
|
76
77
|
|
|
77
78
|
const layers = L.merge(service, rpcClientTest)
|
|
78
79
|
|
|
79
|
-
const effect = pipe(
|
|
80
|
-
E.provide(assertions, layers),
|
|
81
|
-
Logger.withMinimumLogLevel(LogLevel.None)
|
|
82
|
-
)
|
|
80
|
+
const effect = pipe(E.provide(assertions, layers), Logger.withMinimumLogLevel(LogLevel.None))
|
|
83
81
|
|
|
84
82
|
return E.runPromise(effect)
|
|
85
83
|
})
|