@passlock/node 0.9.30 → 2.0.0-beta.1

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 (47) hide show
  1. package/README.md +12 -21
  2. package/README.template.md +11 -20
  3. package/dist/index.d.ts +4 -88
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +2 -80
  6. package/dist/index.js.map +1 -1
  7. package/dist/principal/effect.d.ts +53 -0
  8. package/dist/principal/effect.d.ts.map +1 -0
  9. package/dist/principal/effect.js +78 -0
  10. package/dist/principal/effect.js.map +1 -0
  11. package/dist/principal/index.d.ts +42 -0
  12. package/dist/principal/index.d.ts.map +1 -0
  13. package/dist/principal/index.js +60 -0
  14. package/dist/principal/index.js.map +1 -0
  15. package/dist/shared.d.ts +39 -0
  16. package/dist/shared.d.ts.map +1 -0
  17. package/dist/shared.js +21 -0
  18. package/dist/shared.js.map +1 -0
  19. package/dist/user/effect.d.ts +18 -0
  20. package/dist/user/effect.d.ts.map +1 -0
  21. package/dist/user/effect.js +27 -0
  22. package/dist/user/effect.js.map +1 -0
  23. package/dist/user/index.d.ts +18 -0
  24. package/dist/user/index.d.ts.map +1 -0
  25. package/dist/user/index.js +36 -0
  26. package/dist/user/index.js.map +1 -0
  27. package/package.json +59 -52
  28. package/LICENSE +0 -21
  29. package/dist/config/config.d.ts +0 -9
  30. package/dist/config/config.js +0 -4
  31. package/dist/config/config.js.map +0 -1
  32. package/dist/principal/principal.d.ts +0 -27
  33. package/dist/principal/principal.fixture.d.ts +0 -15
  34. package/dist/principal/principal.fixture.js +0 -67
  35. package/dist/principal/principal.fixture.js.map +0 -1
  36. package/dist/principal/principal.js +0 -88
  37. package/dist/principal/principal.js.map +0 -1
  38. package/dist/tsconfig.tsbuildinfo +0 -1
  39. package/dist/version.d.ts +0 -1
  40. package/dist/version.js +0 -2
  41. package/dist/version.js.map +0 -1
  42. package/src/config/config.ts +0 -10
  43. package/src/index.ts +0 -159
  44. package/src/principal/principal.fixture.ts +0 -107
  45. package/src/principal/principal.test.ts +0 -113
  46. package/src/principal/principal.ts +0 -160
  47. package/src/version.ts +0 -1
package/src/index.ts DELETED
@@ -1,159 +0,0 @@
1
- import { Effect as E, Layer as L, Runtime, Scope, pipe } from 'effect'
2
-
3
- import { ErrorCode } from '@passlock/shared/dist/error/error.js'
4
- import type {
5
- Forbidden,
6
- InternalServerError,
7
- NotFound,
8
- Unauthorized,
9
- } from '@passlock/shared/dist/error/error.js'
10
-
11
- import { Config } from './config/config.js'
12
- import {
13
- type PrincipalRequest,
14
- PrincipalService,
15
- PrincipalServiceLive,
16
- StreamResponseLive,
17
- } from './principal/principal.js'
18
-
19
- export type { PrincipalRequest } from './principal/principal.js'
20
-
21
- export { ErrorCode } from '@passlock/shared/dist/error/error.js'
22
-
23
- export class PasslockError extends Error {
24
- readonly _tag = 'PasslockError'
25
- readonly code: ErrorCode
26
-
27
- constructor(message: string, code: ErrorCode) {
28
- super(message)
29
- this.code = code
30
- }
31
-
32
- static readonly isError = (error: unknown): error is PasslockError => {
33
- return (
34
- typeof error === 'object' &&
35
- error !== null &&
36
- '_tag' in error &&
37
- error['_tag'] === 'PasslockError'
38
- )
39
- }
40
- }
41
-
42
- type PasslockErrors = NotFound | Unauthorized | Forbidden | InternalServerError
43
-
44
- const hasMessage = (defect: unknown): defect is { message: string } => {
45
- return (
46
- typeof defect === 'object' &&
47
- defect !== null &&
48
- 'message' in defect &&
49
- typeof defect['message'] === 'string'
50
- )
51
- }
52
-
53
- const transformErrors = <A, R>(
54
- effect: E.Effect<A, PasslockErrors, R>,
55
- ): E.Effect<A | PasslockError, never, R> => {
56
- const withErrorHandling = E.catchTags(effect, {
57
- NotFound: e => E.succeed(new PasslockError(e.message, ErrorCode.NotFound)),
58
- Unauthorized: e => E.succeed(new PasslockError(e.message, ErrorCode.Unauthorized)),
59
- Forbidden: e => E.succeed(new PasslockError(e.message, ErrorCode.Forbidden)),
60
- InternalServerError: e =>
61
- E.succeed(new PasslockError(e.message, ErrorCode.InternalServerError)),
62
- })
63
-
64
- const sandboxed = E.sandbox(withErrorHandling)
65
-
66
- const withSandboxing = E.catchTags(sandboxed, {
67
- Die: ({ defect }) => {
68
- return hasMessage(defect)
69
- ? E.succeed(new PasslockError(defect.message, ErrorCode.InternalServerError))
70
- : E.succeed(new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError))
71
- },
72
-
73
- Interrupt: () => {
74
- console.error('Interrupt')
75
-
76
- return E.succeed(new PasslockError('Operation aborted', ErrorCode.InternalBrowserError))
77
- },
78
-
79
- Sequential: errors => {
80
- console.error(errors)
81
-
82
- return E.succeed(
83
- new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError),
84
- )
85
- },
86
-
87
- Parallel: errors => {
88
- console.error(errors)
89
-
90
- return E.succeed(
91
- new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError),
92
- )
93
- },
94
- })
95
-
96
- return E.unsandbox(withSandboxing)
97
- }
98
-
99
- type Requirements = PrincipalService
100
-
101
- export class PasslockUnsafe {
102
- private readonly runtime: Runtime.Runtime<Requirements>
103
-
104
- constructor(config: { tenancyId: string; apiKey: string; endpoint?: string }) {
105
- const configLive = L.succeed(Config, Config.of(config))
106
- const allLayers = pipe(
107
- PrincipalServiceLive,
108
- L.provide(configLive),
109
- L.provide(StreamResponseLive),
110
- )
111
- const scope = E.runSync(Scope.make())
112
- this.runtime = E.runSync(L.toRuntime(allLayers).pipe(Scope.extend(scope)))
113
- }
114
-
115
- private readonly runPromise = <A, R extends Requirements>(
116
- effect: E.Effect<A, PasslockErrors, R>,
117
- ) => {
118
- return pipe(
119
- transformErrors(effect),
120
- E.flatMap(result => (PasslockError.isError(result) ? E.fail(result) : E.succeed(result))),
121
- effect => Runtime.runPromise(this.runtime)(effect),
122
- )
123
- }
124
-
125
- fetchPrincipal = (request: PrincipalRequest) =>
126
- pipe(
127
- PrincipalService,
128
- E.flatMap(service => service.fetchPrincipal(request)),
129
- effect => this.runPromise(effect),
130
- )
131
- }
132
-
133
- export class Passlock {
134
- private readonly runtime: Runtime.Runtime<Requirements>
135
-
136
- constructor(config: { tenancyId: string; apiKey: string; endpoint?: string }) {
137
- const configLive = L.succeed(Config, Config.of(config))
138
- const allLayers = pipe(
139
- PrincipalServiceLive,
140
- L.provide(configLive),
141
- L.provide(StreamResponseLive),
142
- )
143
- const scope = E.runSync(Scope.make())
144
- this.runtime = E.runSync(L.toRuntime(allLayers).pipe(Scope.extend(scope)))
145
- }
146
-
147
- private readonly runPromise = <A, R extends Requirements>(
148
- effect: E.Effect<A, PasslockErrors, R>,
149
- ) => {
150
- return pipe(transformErrors(effect), effect => Runtime.runPromise(this.runtime)(effect))
151
- }
152
-
153
- fetchPrincipal = (request: PrincipalRequest) =>
154
- pipe(
155
- PrincipalService,
156
- E.flatMap(service => service.fetchPrincipal(request)),
157
- effect => this.runPromise(effect),
158
- )
159
- }
@@ -1,107 +0,0 @@
1
- import * as S from '@effect/schema/Schema'
2
- import { Context, Effect as E, Layer as L, LogLevel, Logger, Ref, Stream, pipe } from 'effect'
3
- import type { RequestOptions } from 'https'
4
-
5
- import { Principal } from '@passlock/shared/dist/schema/principal.js'
6
-
7
- import { Config } from '../config/config.js'
8
- import {
9
- type PrincipalService,
10
- PrincipalServiceLive,
11
- StreamResponse,
12
- buildError,
13
- } from './principal.js'
14
-
15
- export const principal: Principal = {
16
- jti: 'token',
17
- token: 'token',
18
- sub: 'user-1',
19
- iss: 'idp.passlock.dev',
20
- aud: 'tenancy_id',
21
- // must be at least 1 second
22
- // as it's truncated to seconds
23
- iat: new Date(60 * 1000),
24
- nbf: new Date(120 * 100),
25
- exp: new Date(180 * 1000),
26
- email: 'john.doe@gmail.com',
27
- givenName: 'john',
28
- familyName: 'doe',
29
- emailVerified: false,
30
- authType: 'passkey',
31
- authId: 'auth-1',
32
- userVerified: true,
33
- // legacy
34
- user: {
35
- id: 'user-1',
36
- givenName: 'john',
37
- familyName: 'doe',
38
- email: 'john.doe@gmail.com',
39
- emailVerified: false,
40
- },
41
- authStatement: {
42
- authType: 'passkey',
43
- userVerified: false,
44
- authTimestamp: new Date(0),
45
- },
46
- expireAt: new Date(0),
47
- }
48
-
49
- export const tenancyId = 'tenancyId'
50
- export const apiKey = 'apiKey'
51
-
52
- export const configTest = L.succeed(Config, Config.of({ tenancyId, apiKey }))
53
-
54
- export class State extends Context.Tag('State')<State, Ref.Ref<RequestOptions | undefined>>() {}
55
-
56
- export const buildEffect = <A, E>(
57
- assertions: E.Effect<A, E, PrincipalService | State>,
58
- ): E.Effect<void, E> => {
59
- const streamResponseTest = L.effect(
60
- StreamResponse,
61
- E.gen(function* (_) {
62
- const ref = yield* _(State)
63
- const res = S.encodeSync(Principal)(principal)
64
- const buff = Buffer.from(JSON.stringify(res))
65
-
66
- return {
67
- streamResponse: options =>
68
- pipe(Stream.fromEffect(Ref.set(ref, options)), Stream.zipRight(Stream.make(buff))),
69
- }
70
- }),
71
- )
72
-
73
- const service = pipe(PrincipalServiceLive, L.provide(streamResponseTest), L.provide(configTest))
74
-
75
- const args = L.effect(State, Ref.make<RequestOptions | undefined>(undefined))
76
-
77
- const effect = pipe(
78
- E.provide(assertions, service),
79
- E.provide(args),
80
- Logger.withMinimumLogLevel(LogLevel.None),
81
- )
82
-
83
- return effect
84
- }
85
-
86
- export const buildErrorEffect =
87
- (statusCode: number) =>
88
- <A>(assertions: E.Effect<void, A, PrincipalService>): E.Effect<void, A> => {
89
- const streamResponseTest = L.succeed(
90
- StreamResponse,
91
- StreamResponse.of({
92
- streamResponse: () => Stream.fail(buildError({ statusCode })),
93
- }),
94
- )
95
-
96
- const service = pipe(PrincipalServiceLive, L.provide(streamResponseTest), L.provide(configTest))
97
-
98
- const args = L.effect(State, Ref.make<RequestOptions | undefined>(undefined))
99
-
100
- const effect = pipe(
101
- E.provide(assertions, service),
102
- E.provide(args),
103
- Logger.withMinimumLogLevel(LogLevel.None),
104
- )
105
-
106
- return effect
107
- }
@@ -1,113 +0,0 @@
1
- import { Effect as E, Effect, Ref } from 'effect'
2
- import { describe, expect, test } from 'vitest'
3
-
4
- import {
5
- Forbidden,
6
- InternalServerError,
7
- NotFound,
8
- Unauthorized,
9
- } from '@passlock/shared/dist/error/error.js'
10
-
11
- import * as Fixture from './principal.fixture.js'
12
- import { PrincipalService } from './principal.js'
13
-
14
- describe('fetchPrincipal should', () => {
15
- test('return a valid principal', () => {
16
- const assertions = E.gen(function* (_) {
17
- const service = yield* _(PrincipalService)
18
- const result = yield* _(service.fetchPrincipal({ token: 'token' }))
19
-
20
- expect(result).toEqual(Fixture.principal)
21
- })
22
-
23
- const effect = Fixture.buildEffect(assertions)
24
-
25
- return E.runPromise(effect)
26
- })
27
-
28
- test('call the correct url', () => {
29
- const assertions = E.gen(function* (_) {
30
- const service = yield* _(PrincipalService)
31
- yield* _(service.fetchPrincipal({ token: 'myToken' }))
32
-
33
- const state = yield* _(Fixture.State)
34
- const args = yield* _(Ref.get(state))
35
-
36
- expect(args?.hostname).toEqual('api.passlock.dev')
37
- expect(args?.method).toEqual('GET')
38
- expect(args?.path).toEqual(`/${Fixture.tenancyId}/token/myToken`)
39
- })
40
-
41
- const effect = Fixture.buildEffect(assertions)
42
-
43
- return E.runPromise(effect)
44
- })
45
-
46
- test('pass the api key as a header', () => {
47
- const assertions = E.gen(function* (_) {
48
- const service = yield* _(PrincipalService)
49
- yield* _(service.fetchPrincipal({ token: 'myToken' }))
50
-
51
- const state = yield* _(Fixture.State)
52
- const args = yield* _(Ref.get(state))
53
-
54
- expect(args?.headers?.['Authorization']).toEqual(`Bearer ${Fixture.apiKey}`)
55
- })
56
-
57
- const effect = Fixture.buildEffect(assertions)
58
-
59
- return E.runPromise(effect)
60
- })
61
-
62
- test('propagate a 401 error', () => {
63
- const assertions = E.gen(function* (_) {
64
- const service = yield* _(PrincipalService)
65
- const result = service.fetchPrincipal({ token: 'myToken' })
66
- const error = yield* _(Effect.flip(result))
67
- expect(error).toBeInstanceOf(Unauthorized)
68
- })
69
-
70
- const effect = Fixture.buildErrorEffect(401)(assertions)
71
-
72
- return E.runPromise(effect)
73
- })
74
-
75
- test('propagate a 403 error', () => {
76
- const assertions = E.gen(function* (_) {
77
- const service = yield* _(PrincipalService)
78
- const result = service.fetchPrincipal({ token: 'myToken' })
79
- const error = yield* _(Effect.flip(result))
80
- expect(error).toBeInstanceOf(Forbidden)
81
- })
82
-
83
- const effect = Fixture.buildErrorEffect(403)(assertions)
84
-
85
- return E.runPromise(effect)
86
- })
87
-
88
- test('propagate a 404 error', () => {
89
- const assertions = E.gen(function* (_) {
90
- const service = yield* _(PrincipalService)
91
- const result = service.fetchPrincipal({ token: 'myToken' })
92
- const error = yield* _(Effect.flip(result))
93
- expect(error).toBeInstanceOf(NotFound)
94
- })
95
-
96
- const effect = Fixture.buildErrorEffect(404)(assertions)
97
-
98
- return E.runPromise(effect)
99
- })
100
-
101
- test('propagate a 500 error', () => {
102
- const assertions = E.gen(function* (_) {
103
- const service = yield* _(PrincipalService)
104
- const result = service.fetchPrincipal({ token: 'myToken' })
105
- const error = yield* _(Effect.flip(result))
106
- expect(error).toBeInstanceOf(InternalServerError)
107
- })
108
-
109
- const effect = Fixture.buildErrorEffect(500)(assertions)
110
-
111
- return E.runPromise(effect)
112
- })
113
- })
@@ -1,160 +0,0 @@
1
- import * as https from 'https'
2
- import type { StreamEmit } from 'effect'
3
- import { Chunk, Console, Context, Effect as E, Layer, Option, Stream, flow, pipe } from 'effect'
4
-
5
- import {
6
- Forbidden,
7
- InternalServerError,
8
- NotFound,
9
- Unauthorized,
10
- } from '@passlock/shared/dist/error/error.js'
11
- import { Principal } from '@passlock/shared/dist/schema/principal.js'
12
- import { createParser } from '@passlock/shared/dist/schema/utils.js'
13
-
14
- import { Config } from '../config/config.js'
15
- import { PASSLOCK_CLIENT_VERSION } from '../version.js'
16
-
17
- /* Dependencies */
18
-
19
- export class StreamResponse extends Context.Tag('@services/StreamResponse')<
20
- StreamResponse,
21
- {
22
- streamResponse: (options: https.RequestOptions) => Stream.Stream<Buffer, PrincipalErrors>
23
- }
24
- >() {}
25
-
26
- /* Service */
27
-
28
- const parsePrincipal = createParser(Principal)
29
-
30
- export type PrincipalErrors = NotFound | Unauthorized | Forbidden | InternalServerError
31
- export type PrincipalRequest = { token: string }
32
-
33
- export class PrincipalService extends Context.Tag('@services/PrincipalService')<
34
- PrincipalService,
35
- {
36
- fetchPrincipal: (request: PrincipalRequest) => E.Effect<Principal, PrincipalErrors>
37
- }
38
- >() {}
39
-
40
- /* Effects */
41
-
42
- const buildHostname = (endpoint: string | undefined) => {
43
- return new URL(endpoint || 'https://api.passlock.dev').hostname
44
- }
45
-
46
- const buildOptions = (token: string) =>
47
- pipe(
48
- Config,
49
- E.map(({ endpoint, tenancyId, apiKey }) => ({
50
- hostname: buildHostname(endpoint),
51
- port: 443,
52
- path: `/${tenancyId}/token/${token}`,
53
- method: 'GET',
54
- headers: {
55
- 'Accept': 'application/json',
56
- 'Authorization': `Bearer ${apiKey}`,
57
- 'X-PASSLOCK-CLIENT-VERSION': PASSLOCK_CLIENT_VERSION,
58
- },
59
- })),
60
- )
61
-
62
- export const buildError = (res: {
63
- statusCode?: number | undefined
64
- statusMessage?: string | undefined
65
- }) => {
66
- if (res.statusCode === 404) return new NotFound({ message: 'Invalid token' })
67
- if (res.statusCode === 401) return new Unauthorized({ message: 'Unauthorized' })
68
- if (res.statusCode === 403) return new Forbidden({ message: 'Forbidden' })
69
-
70
- if (res.statusCode && res.statusMessage)
71
- return new InternalServerError({ message: `${String(res.statusCode)} - ${res.statusMessage}` })
72
-
73
- if (res.statusCode) return new InternalServerError({ message: String(res.statusCode) })
74
-
75
- if (res.statusMessage) return new InternalServerError({ message: res.statusMessage })
76
-
77
- return new InternalServerError({ message: 'Received non 200 response' })
78
- }
79
-
80
- const fail = (error: PrincipalErrors) => E.fail(Option.some(error))
81
- const succeed = (data: Buffer) => E.succeed(Chunk.of(data))
82
- const close = E.fail(Option.none())
83
-
84
- const buildStream = (token: string) =>
85
- pipe(
86
- Stream.fromEffect(buildOptions(token)),
87
- Stream.zip(StreamResponse),
88
- Stream.flatMap(([options, { streamResponse }]) => streamResponse(options)),
89
- )
90
-
91
- export const fetchPrincipal = (
92
- request: PrincipalRequest,
93
- ): E.Effect<Principal, PrincipalErrors, StreamResponse | Config> => {
94
- const stream = buildStream(request.token)
95
-
96
- const json = pipe(
97
- Stream.runCollect(stream),
98
- E.map(Chunk.toReadonlyArray),
99
- E.map(buffers => Buffer.concat(buffers)),
100
- E.flatMap(buffer =>
101
- E.try({
102
- try: () => buffer.toString(),
103
- catch: e =>
104
- new InternalServerError({
105
- message: 'Unable to convert response to string',
106
- detail: String(e),
107
- }),
108
- }),
109
- ),
110
- E.flatMap(buffer =>
111
- E.try({
112
- try: () => JSON.parse(buffer) as unknown,
113
- catch: e =>
114
- new InternalServerError({
115
- message: 'Unable to parse response to json',
116
- detail: String(e),
117
- }),
118
- }),
119
- ),
120
- E.flatMap(json =>
121
- pipe(
122
- parsePrincipal(json),
123
- E.tapError(error => Console.error(error.detail)),
124
- E.mapError(
125
- () => new InternalServerError({ message: 'Unable to parse response as Principal' }),
126
- ),
127
- ),
128
- ),
129
- )
130
-
131
- return json
132
- }
133
-
134
- /* Live */
135
-
136
- /* v8 ignore start */
137
- export const StreamResponseLive = Layer.succeed(StreamResponse, {
138
- streamResponse: options =>
139
- Stream.async((emit: StreamEmit.Emit<never, PrincipalErrors, Buffer, void>) => {
140
- https
141
- .request(options, res => {
142
- if (200 !== res.statusCode) void emit(fail(buildError(res)))
143
- res.on('data', (data: Buffer) => void emit(succeed(data)))
144
- res.on('close', () => void emit(close))
145
- res.on('error', e => void emit(fail(new InternalServerError({ message: e.message }))))
146
- })
147
- .end()
148
- }),
149
- })
150
-
151
- export const PrincipalServiceLive = Layer.effect(
152
- PrincipalService,
153
- E.gen(function* (_) {
154
- const context = yield* _(E.context<Config | StreamResponse>())
155
- return PrincipalService.of({
156
- fetchPrincipal: flow(fetchPrincipal, E.provide(context)),
157
- })
158
- }),
159
- )
160
- /* v8 ignore stop */
package/src/version.ts DELETED
@@ -1 +0,0 @@
1
- export const PASSLOCK_CLIENT_VERSION = '#{LATEST}#'