@passlock/node 0.9.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Passlock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ <div align="center">
2
+ <a href="https://github.com/passlock-dev/passkeys-frontend">
3
+ <img src="https://github.com/passlock-dev/passkeys-frontend/assets/208345/53ee00d3-8e6c-49ea-b43c-3f901450c73b" alt="Passlock logo" width="80" height="80">
4
+ </a>
5
+ </div>
6
+
7
+ <a name="readme-top"></a>
8
+ <h1 align="center">Serverless Passkeys</h1>
9
+
10
+ <p align="center">
11
+ Node SDK for passkey authentication
12
+ <br />
13
+ <a href="https://passlock.dev"><strong>Project website »</strong></a>
14
+ <br />
15
+ <a href="https://passlock.dev/#demo">Demo</a>
16
+ ·
17
+ <a href="https://docs.passlock.dev">Documentation</a>
18
+ ·
19
+ <a href="https://docs.passlock.dev/docs/tutorial/intro">Tutorial</a>
20
+ </p>
21
+ </div>
22
+
23
+ <br />
24
+
25
+ ## See also
26
+
27
+ For frontend usage please see the accompanying [@passlock/client][client] package
28
+
29
+ ## Features
30
+
31
+ Passkeys and the WebAuthn API are quite complex. We've taken an opinionated approach to the implementation and feature set to simplify things for you. Following the 80/20 principle we've tried to focus on the features most valuable to developers and users. We welcome feature requests so do [get in touch][contact].
32
+
33
+ 1. **🔐 Primary or secondary authentication** - Replace password based logins with passkeys, or use passkeys alongside passwords for secondary authentication.
34
+
35
+ 2. **☝🏻 Biometrics** - We've made it really easy to implement facial or fingerprint recognition in your webapps.
36
+
37
+ 3. **🔐 Step up authentication** - Require biometric or PIN verification for some actions e.g. changing account details, whilst allowing frictionless authentication for others.
38
+
39
+ 4. **🖥️ Full management console** - Manage all security related aspects of your userbase through a web based console.
40
+
41
+ 6. **🕵️ Audit trail** - View a full audit trail for each user: when they add a new passkey, when they login, verify their email address and much more.
42
+
43
+ ## Screenshot
44
+
45
+ ![Passlock user profile](https://github.com/passlock-dev/passkeys/assets/208345/a4a5c4b8-86cb-4076-bd26-7c29ed2151c6)
46
+ <p align="center">Viewing a user's authentication activity on their profile page</p>
47
+
48
+ ## Usage
49
+
50
+ Generate a secure token in your frontend then use this API to obtain the passkey registration or authentication details:
51
+
52
+ ```typescript
53
+ import { Passlock } from '@passlock/node'
54
+
55
+ const passlock = new Passlock({ tenancyId, apiKey })
56
+
57
+ // token comes from your frontend
58
+ const principal = await passlock.fetchPrincipal({ token })
59
+
60
+ // get the user id
61
+ console.log(principal.subject.id)
62
+ ```
63
+
64
+ [contact]: https://passlock.dev/contact
65
+ [client]: https://www.npmjs.com/package/@passlock/client
@@ -0,0 +1,10 @@
1
+ import { Context } from "effect";
2
+ declare const Config_base: Context.TagClass<Config, "Config", {
3
+ readonly tenancyId: string;
4
+ readonly apiKey: string;
5
+ readonly endpoint?: string;
6
+ }>;
7
+ export declare class Config extends Config_base {
8
+ }
9
+ export {};
10
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;;wBAKT,MAAM;qBACT,MAAM;wBACH,MAAM;;AAL9B,qBAAa,MAAO,SAAQ,WAOzB;CAAG"}
@@ -0,0 +1,4 @@
1
+ import { Context } from "effect";
2
+ export class Config extends Context.Tag("Config")() {
3
+ }
4
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEjC,MAAM,OAAO,MAAO,SAAQ,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAO9C;CAAG"}
@@ -0,0 +1,61 @@
1
+ import { ErrorCode } from "@passlock/shared/dist/error/error.js";
2
+ import { type PrincipalRequest } from "./principal/principal.js";
3
+ export type { PrincipalRequest } from './principal/principal.js';
4
+ export { ErrorCode } from '@passlock/shared/dist/error/error.js';
5
+ export declare class PasslockError extends Error {
6
+ readonly _tag = "PasslockError";
7
+ readonly code: ErrorCode;
8
+ constructor(message: string, code: ErrorCode);
9
+ static readonly isError: (error: unknown) => error is PasslockError;
10
+ }
11
+ export declare class PasslockUnsafe {
12
+ private readonly runtime;
13
+ constructor(config: {
14
+ tenancyId: string;
15
+ apiKey: string;
16
+ endpoint?: string;
17
+ });
18
+ private readonly runPromise;
19
+ fetchPrincipal: (request: PrincipalRequest) => Promise<{
20
+ readonly token: string;
21
+ readonly subject: {
22
+ readonly id: string;
23
+ readonly firstName: string;
24
+ readonly lastName: string;
25
+ readonly email: string;
26
+ readonly emailVerified: boolean;
27
+ };
28
+ readonly authStatement: {
29
+ readonly authType: "email" | "passkey";
30
+ readonly userVerified: boolean;
31
+ readonly authTimestamp: Date;
32
+ };
33
+ readonly expireAt: Date;
34
+ }>;
35
+ }
36
+ export declare class Passlock {
37
+ private readonly runtime;
38
+ constructor(config: {
39
+ tenancyId: string;
40
+ apiKey: string;
41
+ endpoint?: string;
42
+ });
43
+ private readonly runPromise;
44
+ fetchPrincipal: (request: PrincipalRequest) => Promise<{
45
+ readonly token: string;
46
+ readonly subject: {
47
+ readonly id: string;
48
+ readonly firstName: string;
49
+ readonly lastName: string;
50
+ readonly email: string;
51
+ readonly emailVerified: boolean;
52
+ };
53
+ readonly authStatement: {
54
+ readonly authType: "email" | "passkey";
55
+ readonly userVerified: boolean;
56
+ readonly authTimestamp: Date;
57
+ };
58
+ readonly expireAt: Date;
59
+ } | PasslockError>;
60
+ }
61
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyE,MAAM,sCAAsC,CAAC;AAExI,OAAO,EAA0C,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGzG,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAEhE,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAEjE,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,mBAAkB;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;gBAEZ,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS;IAK5C,MAAM,CAAC,QAAQ,CAAC,OAAO,UAAW,OAAO,4BAOxC;CACF;AA4DD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;gBAE3C,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAO5E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAQ1B;IAED,cAAc,YAAa,gBAAgB;;;;;;;;;;;;;;;OAKxC;CACJ;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;gBAE3C,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAO5E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAO1B;IAED,cAAc,YAAa,gBAAgB;;;;;;;;;;;;;;;uBAKxC;CACJ"}
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ import { ErrorCode, InternalServerError } from "@passlock/shared/dist/error/error.js";
2
+ import { Effect as E, Layer as L, Runtime, Scope, pipe } from "effect";
3
+ import { PrincipalService, PrincipalServiceLive } from "./principal/principal.js";
4
+ import { Config } from "./config/config.js";
5
+ export { ErrorCode } from '@passlock/shared/dist/error/error.js';
6
+ export class PasslockError extends Error {
7
+ _tag = 'PasslockError';
8
+ code;
9
+ constructor(message, code) {
10
+ super(message);
11
+ this.code = code;
12
+ }
13
+ static isError = (error) => {
14
+ return (typeof error === 'object' &&
15
+ error !== null &&
16
+ '_tag' in error &&
17
+ error['_tag'] === 'PasslockError');
18
+ };
19
+ }
20
+ const hasMessage = (defect) => {
21
+ return (typeof defect === 'object' &&
22
+ defect !== null &&
23
+ 'message' in defect &&
24
+ typeof defect['message'] === 'string');
25
+ };
26
+ const transformErrors = (effect) => {
27
+ const withErrorHandling = E.catchTags(effect, {
28
+ NotFound: e => E.succeed(new PasslockError(e.message, ErrorCode.NotFound)),
29
+ Unauthorized: e => E.succeed(new PasslockError(e.message, ErrorCode.Unauthorized)),
30
+ Forbidden: e => E.succeed(new PasslockError(e.message, ErrorCode.Forbidden)),
31
+ InternalServerError: e => E.succeed(new PasslockError(e.message, ErrorCode.InternalServerError)),
32
+ });
33
+ const sandboxed = E.sandbox(withErrorHandling);
34
+ const withSandboxing = E.catchTags(sandboxed, {
35
+ Die: ({ defect }) => {
36
+ return hasMessage(defect)
37
+ ? E.succeed(new PasslockError(defect.message, ErrorCode.InternalServerError))
38
+ : E.succeed(new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError));
39
+ },
40
+ Interrupt: () => {
41
+ return E.succeed(new PasslockError('Operation aborted', ErrorCode.InternalBrowserError));
42
+ },
43
+ Sequential: errors => {
44
+ console.error(errors);
45
+ return E.succeed(new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError));
46
+ },
47
+ Parallel: errors => {
48
+ console.error(errors);
49
+ return E.succeed(new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError));
50
+ },
51
+ });
52
+ return E.unsandbox(withSandboxing);
53
+ };
54
+ export class PasslockUnsafe {
55
+ runtime;
56
+ constructor(config) {
57
+ const rpcConfig = L.succeed(Config, Config.of(config));
58
+ const allLayers = pipe(PrincipalServiceLive, L.provide(rpcConfig));
59
+ const scope = E.runSync(Scope.make());
60
+ this.runtime = E.runSync(L.toRuntime(allLayers).pipe(Scope.extend(scope)));
61
+ }
62
+ runPromise = (effect) => {
63
+ return pipe(transformErrors(effect), E.flatMap(result => (PasslockError.isError(result) ? E.fail(result) : E.succeed(result))), effect => Runtime.runPromise(this.runtime)(effect));
64
+ };
65
+ fetchPrincipal = (request) => pipe(PrincipalService, E.flatMap(service => service.fetchPrincipal(request)), effect => this.runPromise(effect));
66
+ }
67
+ export class Passlock {
68
+ runtime;
69
+ constructor(config) {
70
+ const rpcConfig = L.succeed(Config, Config.of(config));
71
+ const allLayers = pipe(PrincipalServiceLive, L.provide(rpcConfig));
72
+ const scope = E.runSync(Scope.make());
73
+ this.runtime = E.runSync(L.toRuntime(allLayers).pipe(Scope.extend(scope)));
74
+ }
75
+ runPromise = (effect) => {
76
+ return pipe(transformErrors(effect), effect => Runtime.runPromise(this.runtime)(effect));
77
+ };
78
+ fetchPrincipal = (request) => pipe(PrincipalService, E.flatMap(service => service.fetchPrincipal(request)), effect => this.runPromise(effect));
79
+ }
80
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { Forbidden, InternalServerError, NotFound, Unauthorized } from '@passlock/shared/dist/error/error.js';
2
+ import { Principal } from '@passlock/shared/dist/schema/schema.js';
3
+ import { Context, Effect as E, Layer } from "effect";
4
+ import { Config } from "../config/config.js";
5
+ export type PrincipalErrors = NotFound | Unauthorized | Forbidden | InternalServerError;
6
+ export type PrincipalRequest = {
7
+ token: string;
8
+ };
9
+ export type PrincipalService = {
10
+ fetchPrincipal: (request: PrincipalRequest) => E.Effect<Principal, PrincipalErrors>;
11
+ };
12
+ export declare const PrincipalService: Context.Tag<PrincipalService, PrincipalService>;
13
+ export declare const fetchPrincipal: (request: PrincipalRequest) => E.Effect<Principal, PrincipalErrors, Config>;
14
+ export declare const PrincipalServiceLive: Layer.Layer<PrincipalService, never, Config>;
15
+ //# sourceMappingURL=principal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"principal.d.ts","sourceRoot":"","sources":["../../src/principal/principal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAC7G,OAAO,EAAE,SAAS,EAAgB,MAAM,wCAAwC,CAAA;AAChF,OAAO,EAAkB,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,KAAK,EAA0C,MAAM,QAAQ,CAAA;AAE5G,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAI5C,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,YAAY,GAAG,SAAS,GAAG,mBAAmB,CAAA;AACvF,MAAM,MAAM,gBAAgB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,CAAA;CACpF,CAAA;AAED,eAAO,MAAM,gBAAgB,iDAA8D,CAAA;AAgD3F,eAAO,MAAM,cAAc,YAAa,gBAAgB,KAAG,EAAE,MAAM,CAAC,SAAS,EAAE,eAAe,EAAE,MAAM,CA8BrG,CAAA;AAKD,eAAO,MAAM,oBAAoB,8CAQhC,CAAA"}
@@ -0,0 +1,62 @@
1
+ import { Forbidden, InternalServerError, NotFound, Unauthorized } from '@passlock/shared/dist/error/error.js';
2
+ import { Principal, createParser } from '@passlock/shared/dist/schema/schema.js';
3
+ import { Chunk, Console, Context, Effect as E, Layer, Option, Stream, StreamEmit, flow, pipe } from "effect";
4
+ import * as https from "https";
5
+ import { Config } from "../config/config.js";
6
+ const parsePrincipal = createParser(Principal);
7
+ export const PrincipalService = Context.GenericTag('@services/Principal');
8
+ const buildOptions = (token) => pipe(Config, E.map(({ endpoint, tenancyId, apiKey }) => ({
9
+ hostname: endpoint || 'https://api.passlock.dev',
10
+ port: 443,
11
+ path: `/${tenancyId}/token/${token}`,
12
+ method: 'GET',
13
+ headers: {
14
+ 'Accept': 'application/json',
15
+ 'X-API-KEY': apiKey
16
+ }
17
+ })));
18
+ const buildError = (res) => {
19
+ if (res.statusCode === 404)
20
+ return new NotFound({ message: "Invalid token" });
21
+ if (res.statusCode === 401)
22
+ return new Unauthorized({ message: 'Unauthorized' });
23
+ if (res.statusCode === 403)
24
+ return new Forbidden({ message: 'Forbidden' });
25
+ if (res.statusCode && res.statusMessage)
26
+ return new InternalServerError({ message: `${res.statusCode} - ${res.statusMessage}` });
27
+ if (res.statusCode)
28
+ return new InternalServerError({ message: String(res.statusCode) });
29
+ if (res.statusMessage)
30
+ return new InternalServerError({ message: res.statusMessage });
31
+ return new InternalServerError({ message: 'Received non 200 response' });
32
+ };
33
+ const fail = (error) => E.fail(Option.some(error));
34
+ const succeed = (data) => E.succeed(Chunk.of(data));
35
+ const close = E.fail(Option.none());
36
+ const buildStream = (token) => pipe(Stream.fromEffect(buildOptions(token)), Stream.flatMap(options => Stream.async((emit) => {
37
+ https.request(options, res => {
38
+ if (200 !== res.statusCode)
39
+ emit(fail(buildError(res)));
40
+ res.on('data', data => emit(succeed(data)));
41
+ res.on('close', () => emit(close));
42
+ res.on('error', (e) => emit(fail(new InternalServerError({ message: e.message }))));
43
+ }).end();
44
+ })));
45
+ export const fetchPrincipal = (request) => {
46
+ const stream = buildStream(request.token);
47
+ const json = pipe(Stream.runCollect(stream), E.map(Chunk.toReadonlyArray), E.map(Buffer.concat), E.flatMap(buffer => E.try({
48
+ try: () => buffer.toString(),
49
+ catch: (e) => new InternalServerError({ message: 'Unable to convert response to string', detail: String(e) })
50
+ })), E.flatMap(buffer => E.try({
51
+ try: () => JSON.parse(buffer),
52
+ catch: (e) => new InternalServerError({ message: 'Unable to parse response to json', detail: String(e) })
53
+ })), E.tap(json => Console.log(json)), E.flatMap(json => pipe(parsePrincipal(json), E.tapError(error => Console.error(error.detail)), E.mapError(() => new InternalServerError({ message: "Unable to parse response as Principal" })))));
54
+ return json;
55
+ };
56
+ export const PrincipalServiceLive = Layer.effect(PrincipalService, E.gen(function* (_) {
57
+ const context = yield* _(E.context());
58
+ return PrincipalService.of({
59
+ fetchPrincipal: flow(fetchPrincipal, E.provide(context)),
60
+ });
61
+ }));
62
+ //# sourceMappingURL=principal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"principal.js","sourceRoot":"","sources":["../../src/principal/principal.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@passlock/node",
3
+ "type": "module",
4
+ "version": "0.9.2",
5
+ "description": "Server side passkey library for node/express",
6
+ "keywords": [
7
+ "passkey",
8
+ "passkeys",
9
+ "webauthn",
10
+ "node",
11
+ "express"
12
+ ],
13
+ "author": {
14
+ "name": "Toby Hobson",
15
+ "email": "toby@passlock.dev"
16
+ },
17
+ "license": "MIT",
18
+ "homepage": "https://passlock.dev",
19
+ "repository": "github.com/passlock-dev/node",
20
+ "bugs": {
21
+ "url": "https://github.com/passlock-dev/node/issues",
22
+ "email": "team@passlock.dev"
23
+ },
24
+ "dependencies": {
25
+ "effect": "2.4.9",
26
+ "@passlock/shared": "^0.9.2"
27
+ },
28
+ "devDependencies": {
29
+ "@tsconfig/node16": "^16.1.1",
30
+ "@types/node": "^20.11.30",
31
+ "@typescript-eslint/eslint-plugin": "^7.3.1",
32
+ "@typescript-eslint/parser": "^7.3.1",
33
+ "@vitest/coverage-v8": "^1.4.0",
34
+ "@vitest/ui": "^1.4.0",
35
+ "eslint": "^8.57.0",
36
+ "eslint-config-prettier": "^9.1.0",
37
+ "eslint-import-resolver-typescript": "^3.6.1",
38
+ "eslint-plugin-import": "^2.29.1",
39
+ "prettier": "^3.2.5",
40
+ "rimraf": "^5.0.5",
41
+ "tsx": "^4.7.1",
42
+ "typescript": "^5.4.2",
43
+ "vite": "^5.2.2",
44
+ "vitest": "^1.4.0",
45
+ "vitest-mock-extended": "^1.3.1"
46
+ },
47
+ "exports": {
48
+ ".": {
49
+ "import": "./dist/index.js",
50
+ "types": "./dist/index.d.ts"
51
+ }
52
+ },
53
+ "files": [
54
+ "src",
55
+ "dist"
56
+ ],
57
+ "scripts": {
58
+ "clean": "tsc --build --clean",
59
+ "build": "tsc --build",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest dev",
62
+ "test:ui": "vitest --coverage.enabled=true --ui",
63
+ "test:coverage": "vitest run --coverage",
64
+ "typecheck": "tsc --noEmit",
65
+ "lint": "eslint --ext .ts src",
66
+ "lint:fix": "pnpm run lint --fix",
67
+ "format": "prettier --write \"src/**/*.+(js|ts|json)\"",
68
+ "ncu": "ncu -x @passlock/shared",
69
+ "ncu:save": "ncu -u -x @passlock/shared"
70
+ }
71
+ }
@@ -0,0 +1,10 @@
1
+ import { Context } from "effect";
2
+
3
+ export class Config extends Context.Tag("Config")<
4
+ Config,
5
+ {
6
+ readonly tenancyId: string,
7
+ readonly apiKey: string,
8
+ readonly endpoint?: string
9
+ }
10
+ >() {}
package/src/index.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { ErrorCode, InternalServerError, type Forbidden, type NotFound, type Unauthorized } from "@passlock/shared/dist/error/error.js";
2
+ import { Effect as E, Layer as L, Runtime, Scope, pipe } from "effect";
3
+ import { PrincipalService, PrincipalServiceLive, type PrincipalRequest } from "./principal/principal.js";
4
+ import { Config } from "./config/config.js";
5
+
6
+ export type { PrincipalRequest } from './principal/principal.js'
7
+
8
+ export { ErrorCode } from '@passlock/shared/dist/error/error.js';
9
+
10
+ export class PasslockError extends Error {
11
+ readonly _tag = 'PasslockError'
12
+ readonly code: ErrorCode
13
+
14
+ constructor(message: string, code: ErrorCode) {
15
+ super(message)
16
+ this.code = code
17
+ }
18
+
19
+ static readonly isError = (error: unknown): error is PasslockError => {
20
+ return (
21
+ typeof error === 'object' &&
22
+ error !== null &&
23
+ '_tag' in error &&
24
+ error['_tag'] === 'PasslockError'
25
+ )
26
+ }
27
+ }
28
+
29
+ type PasslockErrors =
30
+ | NotFound
31
+ | Unauthorized
32
+ | Forbidden
33
+ | InternalServerError
34
+
35
+ const hasMessage = (defect: unknown): defect is { message: string } => {
36
+ return (
37
+ typeof defect === 'object' &&
38
+ defect !== null &&
39
+ 'message' in defect &&
40
+ typeof defect['message'] === 'string'
41
+ )
42
+ }
43
+
44
+ const transformErrors = <A, R>(
45
+ effect: E.Effect<A, PasslockErrors, R>,
46
+ ): E.Effect<A | PasslockError, never, R> => {
47
+ const withErrorHandling = E.catchTags(effect, {
48
+ NotFound: e => E.succeed(new PasslockError(e.message, ErrorCode.NotFound)),
49
+ Unauthorized: e => E.succeed(new PasslockError(e.message, ErrorCode.Unauthorized)),
50
+ Forbidden: e => E.succeed(new PasslockError(e.message, ErrorCode.Forbidden)),
51
+ InternalServerError: e => E.succeed(new PasslockError(e.message, ErrorCode.InternalServerError)),
52
+ })
53
+
54
+ const sandboxed = E.sandbox(withErrorHandling)
55
+
56
+ const withSandboxing = E.catchTags(sandboxed, {
57
+ Die: ({ defect }) => {
58
+ return hasMessage(defect)
59
+ ? E.succeed(new PasslockError(defect.message, ErrorCode.InternalServerError))
60
+ : E.succeed(new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError))
61
+ },
62
+
63
+ Interrupt: () => {
64
+ return E.succeed(new PasslockError('Operation aborted', ErrorCode.InternalBrowserError))
65
+ },
66
+
67
+ Sequential: errors => {
68
+ console.error(errors)
69
+ return E.succeed(
70
+ new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError),
71
+ )
72
+ },
73
+
74
+ Parallel: errors => {
75
+ console.error(errors)
76
+ return E.succeed(
77
+ new PasslockError('Sorry, something went wrong', ErrorCode.InternalServerError),
78
+ )
79
+ },
80
+ })
81
+
82
+ return E.unsandbox(withSandboxing)
83
+ }
84
+
85
+ type Requirements = PrincipalService
86
+
87
+ export class PasslockUnsafe {
88
+ private readonly runtime: Runtime.Runtime<Requirements>
89
+
90
+ constructor(config: { tenancyId: string; apiKey: string; endpoint?: string }) {
91
+ const rpcConfig = L.succeed(Config, Config.of(config))
92
+ const allLayers = pipe(PrincipalServiceLive, L.provide(rpcConfig))
93
+ const scope = E.runSync(Scope.make())
94
+ this.runtime = E.runSync(L.toRuntime(allLayers).pipe(Scope.extend(scope)))
95
+ }
96
+
97
+ private readonly runPromise = <A, R extends Requirements>(
98
+ effect: E.Effect<A, PasslockErrors, R>,
99
+ ) => {
100
+ return pipe(
101
+ transformErrors(effect),
102
+ E.flatMap(result => (PasslockError.isError(result) ? E.fail(result) : E.succeed(result))),
103
+ effect => Runtime.runPromise(this.runtime)(effect),
104
+ )
105
+ }
106
+
107
+ fetchPrincipal = (request: PrincipalRequest) =>
108
+ pipe(
109
+ PrincipalService,
110
+ E.flatMap(service => service.fetchPrincipal(request)),
111
+ effect => this.runPromise(effect),
112
+ )
113
+ }
114
+
115
+ export class Passlock {
116
+ private readonly runtime: Runtime.Runtime<Requirements>
117
+
118
+ constructor(config: { tenancyId: string; apiKey: string; endpoint?: string }) {
119
+ const rpcConfig = L.succeed(Config, Config.of(config))
120
+ const allLayers = pipe(PrincipalServiceLive, L.provide(rpcConfig))
121
+ const scope = E.runSync(Scope.make())
122
+ this.runtime = E.runSync(L.toRuntime(allLayers).pipe(Scope.extend(scope)))
123
+ }
124
+
125
+ private readonly runPromise = <A, R extends Requirements>(
126
+ effect: E.Effect<A, PasslockErrors, R>,
127
+ ) => {
128
+ return pipe(
129
+ transformErrors(effect),
130
+ effect => Runtime.runPromise(this.runtime)(effect)
131
+ )
132
+ }
133
+
134
+ fetchPrincipal = (request: PrincipalRequest) =>
135
+ pipe(
136
+ PrincipalService,
137
+ E.flatMap(service => service.fetchPrincipal(request)),
138
+ effect => this.runPromise(effect),
139
+ )
140
+ }
@@ -0,0 +1,108 @@
1
+ import { Forbidden, InternalServerError, NotFound, Unauthorized } from '@passlock/shared/dist/error/error.js'
2
+ import { Principal, createParser } from '@passlock/shared/dist/schema/schema.js'
3
+ import { Chunk, Console, Context, Effect as E, Layer, Option, Stream, StreamEmit, flow, pipe } from "effect"
4
+ import * as https from "https"
5
+ import { Config } from "../config/config.js"
6
+
7
+ const parsePrincipal = createParser(Principal)
8
+
9
+ export type PrincipalErrors = NotFound | Unauthorized | Forbidden | InternalServerError
10
+ export type PrincipalRequest = { token: string }
11
+
12
+ export type PrincipalService = {
13
+ fetchPrincipal: (request: PrincipalRequest) => E.Effect<Principal, PrincipalErrors>
14
+ }
15
+
16
+ export const PrincipalService = Context.GenericTag<PrincipalService>('@services/Principal')
17
+
18
+ const buildOptions = (token: string) => pipe(
19
+ Config,
20
+ E.map(({ endpoint, tenancyId, apiKey }) => ({
21
+ hostname: endpoint || 'https://api.passlock.dev',
22
+ port: 443,
23
+ path: `/${tenancyId}/token/${token}`,
24
+ method: 'GET',
25
+ headers: {
26
+ 'Accept': 'application/json',
27
+ 'X-API-KEY': apiKey
28
+ }
29
+ }))
30
+ )
31
+
32
+ const buildError = (res: { statusCode?: number | undefined, statusMessage?: string | undefined }) => {
33
+ if (res.statusCode === 404) return new NotFound({ message: "Invalid token" })
34
+ if (res.statusCode === 401) return new Unauthorized({ message: 'Unauthorized' })
35
+ if (res.statusCode === 403) return new Forbidden({ message: 'Forbidden' })
36
+
37
+ if (res.statusCode && res.statusMessage) return new InternalServerError({ message: `${res.statusCode} - ${res.statusMessage}` })
38
+ if (res.statusCode) return new InternalServerError({ message: String(res.statusCode) })
39
+ if (res.statusMessage) return new InternalServerError({ message: res.statusMessage })
40
+
41
+ return new InternalServerError({ message: 'Received non 200 response'})
42
+ }
43
+
44
+ const fail = (error: PrincipalErrors) => E.fail(Option.some(error))
45
+ const succeed = (data: Buffer) => E.succeed(Chunk.of(data))
46
+ const close = E.fail(Option.none())
47
+
48
+ const buildStream = (token: string) => pipe(
49
+ Stream.fromEffect(buildOptions(token)),
50
+ Stream.flatMap(options =>
51
+ Stream.async((emit: StreamEmit.Emit<never, PrincipalErrors, Buffer, void>) => {
52
+ https.request(options, res => {
53
+ if (200 !== res.statusCode) emit(fail(buildError(res)))
54
+ res.on('data', data => emit(succeed(data)))
55
+ res.on('close', () => emit(close))
56
+ res.on('error', (e) => emit(
57
+ fail(new InternalServerError({ message: e.message }))
58
+ ))
59
+ }).end()
60
+ })
61
+ )
62
+ )
63
+
64
+ export const fetchPrincipal = (request: PrincipalRequest): E.Effect<Principal, PrincipalErrors, Config> => {
65
+ const stream = buildStream(request.token)
66
+
67
+ const json = pipe(
68
+ Stream.runCollect(stream),
69
+ E.map(Chunk.toReadonlyArray),
70
+ E.map(Buffer.concat),
71
+ E.flatMap(buffer =>
72
+ E.try({
73
+ try: () => buffer.toString(),
74
+ catch: (e) => new InternalServerError({ message: 'Unable to convert response to string', detail: String(e) })
75
+ })
76
+ ),
77
+ E.flatMap(buffer =>
78
+ E.try({
79
+ try: () => JSON.parse(buffer),
80
+ catch: (e) => new InternalServerError({ message: 'Unable to parse response to json', detail: String(e) })
81
+ })
82
+ ),
83
+ E.tap(json => Console.log(json)),
84
+ E.flatMap(json =>
85
+ pipe(
86
+ parsePrincipal(json),
87
+ E.tapError(error => Console.error(error.detail)),
88
+ E.mapError(() => new InternalServerError({ message: "Unable to parse response as Principal" }))
89
+ )
90
+ )
91
+ )
92
+
93
+ return json
94
+ }
95
+
96
+ /* Live */
97
+
98
+ /* v8 ignore start */
99
+ export const PrincipalServiceLive = Layer.effect(
100
+ PrincipalService,
101
+ E.gen(function* (_) {
102
+ const context = yield* _(E.context<Config>())
103
+ return PrincipalService.of({
104
+ fetchPrincipal: flow(fetchPrincipal, E.provide(context)),
105
+ })
106
+ }),
107
+ )
108
+ /* v8 ignore stop */