@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 +21 -0
- package/README.md +65 -0
- package/dist/config/config.d.ts +10 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +4 -0
- package/dist/config/config.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/principal/principal.d.ts +15 -0
- package/dist/principal/principal.d.ts.map +1 -0
- package/dist/principal/principal.js +62 -0
- package/dist/principal/principal.js.map +1 -0
- package/package.json +71 -0
- package/src/config/config.ts +10 -0
- package/src/index.ts +140 -0
- package/src/principal/principal.ts +108 -0
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
|
+

|
|
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 @@
|
|
|
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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|
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 */
|