@logto/node 2.3.0 → 2.4.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.
- package/lib/src/exports.d.ts +5 -0
- package/lib/src/index.cjs +16 -11
- package/lib/src/index.d.ts +3 -5
- package/lib/src/index.js +7 -6
- package/lib/src/utils/cookie-storage.cjs +61 -0
- package/lib/src/utils/cookie-storage.d.ts +45 -0
- package/lib/src/utils/cookie-storage.js +59 -0
- package/lib/src/utils/promise-queue.cjs +46 -0
- package/lib/src/utils/promise-queue.d.ts +9 -0
- package/lib/src/utils/promise-queue.js +44 -0
- package/lib/src/utils/session.cjs +73 -0
- package/lib/src/utils/session.d.ts +19 -0
- package/lib/src/utils/session.js +69 -0
- package/package.json +11 -9
- package/lib/src/index.test.d.ts +0 -1
- package/lib/src/utils/generators.cjs +0 -37
- package/lib/src/utils/generators.d.ts +0 -17
- package/lib/src/utils/generators.js +0 -33
- package/lib/src/utils/generators.test.d.ts +0 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './utils/session.js';
|
|
2
|
+
export * from './utils/cookie-storage.js';
|
|
3
|
+
export type { LogtoContext, GetContextParameters } from './types.js';
|
|
4
|
+
export type { IdTokenClaims, LogtoErrorCode, LogtoConfig, LogtoClientErrorCode, Storage, StorageKey, InteractionMode, ClientAdapter, JwtVerifier, UserInfoResponse, SignInOptions, } from '@logto/client';
|
|
5
|
+
export { LogtoError, LogtoRequestError, LogtoClientError, OidcError, Prompt, ReservedScope, ReservedResource, UserScope, organizationUrnPrefix, buildOrganizationUrn, getOrganizationIdFromUrn, PersistKey, StandardLogtoClient, } from '@logto/client';
|
package/lib/src/index.cjs
CHANGED
|
@@ -3,22 +3,18 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
var BaseClient = require('@logto/client');
|
|
6
|
-
var
|
|
6
|
+
var generators = require('../edge/generators.cjs');
|
|
7
7
|
var client = require('./client.cjs');
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
-
|
|
12
|
-
var fetch__default = /*#__PURE__*/_interopDefault(fetch);
|
|
8
|
+
var session = require('./utils/session.cjs');
|
|
9
|
+
var cookieStorage = require('./utils/cookie-storage.cjs');
|
|
13
10
|
|
|
14
11
|
class LogtoClient extends client.default {
|
|
15
|
-
constructor(config, adapter) {
|
|
12
|
+
constructor(config, adapter, buildJwtVerifier) {
|
|
16
13
|
super(config, {
|
|
17
|
-
...adapter,
|
|
18
14
|
requester: BaseClient.createRequester(config.appSecret
|
|
19
15
|
? async (...args) => {
|
|
20
16
|
const [input, init] = args;
|
|
21
|
-
return
|
|
17
|
+
return fetch(input, {
|
|
22
18
|
...init,
|
|
23
19
|
headers: {
|
|
24
20
|
Authorization: `Basic ${Buffer.from(
|
|
@@ -28,11 +24,12 @@ class LogtoClient extends client.default {
|
|
|
28
24
|
},
|
|
29
25
|
});
|
|
30
26
|
}
|
|
31
|
-
:
|
|
27
|
+
: fetch),
|
|
32
28
|
generateCodeChallenge: generators.generateCodeChallenge,
|
|
33
29
|
generateCodeVerifier: generators.generateCodeVerifier,
|
|
34
30
|
generateState: generators.generateState,
|
|
35
|
-
|
|
31
|
+
...adapter,
|
|
32
|
+
}, buildJwtVerifier);
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
|
|
@@ -68,6 +65,10 @@ Object.defineProperty(exports, "ReservedScope", {
|
|
|
68
65
|
enumerable: true,
|
|
69
66
|
get: function () { return BaseClient.ReservedScope; }
|
|
70
67
|
});
|
|
68
|
+
Object.defineProperty(exports, "StandardLogtoClient", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
get: function () { return BaseClient.StandardLogtoClient; }
|
|
71
|
+
});
|
|
71
72
|
Object.defineProperty(exports, "UserScope", {
|
|
72
73
|
enumerable: true,
|
|
73
74
|
get: function () { return BaseClient.UserScope; }
|
|
@@ -84,4 +85,8 @@ Object.defineProperty(exports, "organizationUrnPrefix", {
|
|
|
84
85
|
enumerable: true,
|
|
85
86
|
get: function () { return BaseClient.organizationUrnPrefix; }
|
|
86
87
|
});
|
|
88
|
+
exports.createSession = session.createSession;
|
|
89
|
+
exports.unwrapSession = session.unwrapSession;
|
|
90
|
+
exports.wrapSession = session.wrapSession;
|
|
91
|
+
exports.CookieStorage = cookieStorage.CookieStorage;
|
|
87
92
|
exports.default = LogtoClient;
|
package/lib/src/index.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import type { LogtoConfig, ClientAdapter } from '@logto/client';
|
|
1
|
+
import type { LogtoConfig, ClientAdapter, StandardLogtoClient, JwtVerifier } from '@logto/client';
|
|
2
2
|
import BaseClient from './client.js';
|
|
3
|
-
export
|
|
4
|
-
export type { IdTokenClaims, LogtoErrorCode, LogtoConfig, LogtoClientErrorCode, Storage, StorageKey, InteractionMode, } from '@logto/client';
|
|
5
|
-
export { LogtoError, LogtoRequestError, LogtoClientError, OidcError, Prompt, ReservedScope, ReservedResource, UserScope, organizationUrnPrefix, buildOrganizationUrn, getOrganizationIdFromUrn, PersistKey, } from '@logto/client';
|
|
3
|
+
export * from './exports.js';
|
|
6
4
|
export default class LogtoClient extends BaseClient {
|
|
7
|
-
constructor(config: LogtoConfig, adapter: Pick<ClientAdapter, 'navigate' | 'storage'
|
|
5
|
+
constructor(config: LogtoConfig, adapter: Partial<ClientAdapter> & Pick<ClientAdapter, 'navigate' | 'storage'>, buildJwtVerifier?: (client: StandardLogtoClient) => JwtVerifier);
|
|
8
6
|
}
|
package/lib/src/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createRequester } from '@logto/client';
|
|
2
|
-
export { LogtoClientError, LogtoError, LogtoRequestError, OidcError, PersistKey, Prompt, ReservedResource, ReservedScope, UserScope, buildOrganizationUrn, getOrganizationIdFromUrn, organizationUrnPrefix } from '@logto/client';
|
|
3
|
-
import
|
|
2
|
+
export { LogtoClientError, LogtoError, LogtoRequestError, OidcError, PersistKey, Prompt, ReservedResource, ReservedScope, StandardLogtoClient, UserScope, buildOrganizationUrn, getOrganizationIdFromUrn, organizationUrnPrefix } from '@logto/client';
|
|
3
|
+
import { generateCodeChallenge, generateCodeVerifier, generateState } from '../edge/generators.js';
|
|
4
4
|
import LogtoNodeBaseClient from './client.js';
|
|
5
|
-
|
|
5
|
+
export { createSession, unwrapSession, wrapSession } from './utils/session.js';
|
|
6
|
+
export { CookieStorage } from './utils/cookie-storage.js';
|
|
6
7
|
|
|
7
8
|
class LogtoClient extends LogtoNodeBaseClient {
|
|
8
|
-
constructor(config, adapter) {
|
|
9
|
+
constructor(config, adapter, buildJwtVerifier) {
|
|
9
10
|
super(config, {
|
|
10
|
-
...adapter,
|
|
11
11
|
requester: createRequester(config.appSecret
|
|
12
12
|
? async (...args) => {
|
|
13
13
|
const [input, init] = args;
|
|
@@ -25,7 +25,8 @@ class LogtoClient extends LogtoNodeBaseClient {
|
|
|
25
25
|
generateCodeChallenge,
|
|
26
26
|
generateCodeVerifier,
|
|
27
27
|
generateState,
|
|
28
|
-
|
|
28
|
+
...adapter,
|
|
29
|
+
}, buildJwtVerifier);
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promiseQueue = require('./promise-queue.cjs');
|
|
4
|
+
var session = require('./session.cjs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A storage that persists data in cookies with encryption.
|
|
8
|
+
*/
|
|
9
|
+
class CookieStorage {
|
|
10
|
+
get cookieOptions() {
|
|
11
|
+
return Object.freeze({
|
|
12
|
+
httpOnly: true,
|
|
13
|
+
path: '/',
|
|
14
|
+
sameSite: 'lax',
|
|
15
|
+
secure: this.#isSecure,
|
|
16
|
+
maxAge: 14 * 24 * 3600, // 14 days
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
get cookieKey() {
|
|
20
|
+
return this.config.cookieKey ?? 'logtoCookies';
|
|
21
|
+
}
|
|
22
|
+
get data() {
|
|
23
|
+
return this.sessionData;
|
|
24
|
+
}
|
|
25
|
+
#isSecure;
|
|
26
|
+
constructor(config, request) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.sessionData = {};
|
|
29
|
+
this.saveQueue = new promiseQueue.PromiseQueue();
|
|
30
|
+
if (!config.encryptionKey) {
|
|
31
|
+
throw new TypeError('The `encryptionKey` string is required for `CookieStorage`');
|
|
32
|
+
}
|
|
33
|
+
this.#isSecure =
|
|
34
|
+
request.headers.get('x-forwarded-proto') === 'https' || request.url.startsWith('https');
|
|
35
|
+
}
|
|
36
|
+
async init() {
|
|
37
|
+
const { encryptionKey } = this.config;
|
|
38
|
+
this.sessionData = await session.unwrapSession(this.config.getCookie(this.cookieKey) ?? '', encryptionKey);
|
|
39
|
+
}
|
|
40
|
+
async getItem(key) {
|
|
41
|
+
return this.sessionData[key] ?? null;
|
|
42
|
+
}
|
|
43
|
+
async setItem(key, value) {
|
|
44
|
+
this.sessionData[key] = value;
|
|
45
|
+
await this.save();
|
|
46
|
+
}
|
|
47
|
+
async removeItem(key) {
|
|
48
|
+
// eslint-disable-next-line @silverhand/fp/no-delete, @typescript-eslint/no-dynamic-delete
|
|
49
|
+
delete this.sessionData[key];
|
|
50
|
+
await this.save();
|
|
51
|
+
}
|
|
52
|
+
async save() {
|
|
53
|
+
return this.saveQueue.enqueue(async () => this.write());
|
|
54
|
+
}
|
|
55
|
+
async write(data = this.sessionData) {
|
|
56
|
+
const { encryptionKey } = this.config;
|
|
57
|
+
this.config.setCookie(this.cookieKey, await session.wrapSession(data, encryptionKey), this.cookieOptions);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
exports.CookieStorage = CookieStorage;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type PersistKey, type Storage } from '@logto/client';
|
|
2
|
+
import type { CookieSerializeOptions } from 'cookie';
|
|
3
|
+
import { PromiseQueue } from './promise-queue.js';
|
|
4
|
+
import { type SessionData } from './session.js';
|
|
5
|
+
type Nullable<T> = T | null;
|
|
6
|
+
export type CookieConfig = {
|
|
7
|
+
/** The encryption key to encrypt the session data. It should be a random string. */
|
|
8
|
+
encryptionKey: string;
|
|
9
|
+
/** The name of the cookie key. Default to `logtoCookies`. */
|
|
10
|
+
cookieKey?: string;
|
|
11
|
+
getCookie: (name: string) => string | undefined;
|
|
12
|
+
setCookie: (name: string, value: string, options: CookieSerializeOptions & {
|
|
13
|
+
path: string;
|
|
14
|
+
}) => void;
|
|
15
|
+
};
|
|
16
|
+
export type PartialRequest = {
|
|
17
|
+
headers: Headers;
|
|
18
|
+
url: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A storage that persists data in cookies with encryption.
|
|
22
|
+
*/
|
|
23
|
+
export declare class CookieStorage implements Storage<PersistKey> {
|
|
24
|
+
#private;
|
|
25
|
+
config: CookieConfig;
|
|
26
|
+
protected get cookieOptions(): Readonly<{
|
|
27
|
+
httpOnly: true;
|
|
28
|
+
path: string;
|
|
29
|
+
sameSite: "lax";
|
|
30
|
+
secure: boolean;
|
|
31
|
+
maxAge: number;
|
|
32
|
+
}>;
|
|
33
|
+
protected get cookieKey(): string;
|
|
34
|
+
get data(): SessionData;
|
|
35
|
+
protected sessionData: SessionData;
|
|
36
|
+
protected saveQueue: PromiseQueue;
|
|
37
|
+
constructor(config: CookieConfig, request: PartialRequest);
|
|
38
|
+
init(): Promise<void>;
|
|
39
|
+
getItem(key: PersistKey): Promise<Nullable<string>>;
|
|
40
|
+
setItem(key: PersistKey, value: string): Promise<void>;
|
|
41
|
+
removeItem(key: PersistKey): Promise<void>;
|
|
42
|
+
protected save(): Promise<void>;
|
|
43
|
+
protected write(data?: SessionData): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PromiseQueue } from './promise-queue.js';
|
|
2
|
+
import { unwrapSession, wrapSession } from './session.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A storage that persists data in cookies with encryption.
|
|
6
|
+
*/
|
|
7
|
+
class CookieStorage {
|
|
8
|
+
get cookieOptions() {
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
httpOnly: true,
|
|
11
|
+
path: '/',
|
|
12
|
+
sameSite: 'lax',
|
|
13
|
+
secure: this.#isSecure,
|
|
14
|
+
maxAge: 14 * 24 * 3600, // 14 days
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
get cookieKey() {
|
|
18
|
+
return this.config.cookieKey ?? 'logtoCookies';
|
|
19
|
+
}
|
|
20
|
+
get data() {
|
|
21
|
+
return this.sessionData;
|
|
22
|
+
}
|
|
23
|
+
#isSecure;
|
|
24
|
+
constructor(config, request) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.sessionData = {};
|
|
27
|
+
this.saveQueue = new PromiseQueue();
|
|
28
|
+
if (!config.encryptionKey) {
|
|
29
|
+
throw new TypeError('The `encryptionKey` string is required for `CookieStorage`');
|
|
30
|
+
}
|
|
31
|
+
this.#isSecure =
|
|
32
|
+
request.headers.get('x-forwarded-proto') === 'https' || request.url.startsWith('https');
|
|
33
|
+
}
|
|
34
|
+
async init() {
|
|
35
|
+
const { encryptionKey } = this.config;
|
|
36
|
+
this.sessionData = await unwrapSession(this.config.getCookie(this.cookieKey) ?? '', encryptionKey);
|
|
37
|
+
}
|
|
38
|
+
async getItem(key) {
|
|
39
|
+
return this.sessionData[key] ?? null;
|
|
40
|
+
}
|
|
41
|
+
async setItem(key, value) {
|
|
42
|
+
this.sessionData[key] = value;
|
|
43
|
+
await this.save();
|
|
44
|
+
}
|
|
45
|
+
async removeItem(key) {
|
|
46
|
+
// eslint-disable-next-line @silverhand/fp/no-delete, @typescript-eslint/no-dynamic-delete
|
|
47
|
+
delete this.sessionData[key];
|
|
48
|
+
await this.save();
|
|
49
|
+
}
|
|
50
|
+
async save() {
|
|
51
|
+
return this.saveQueue.enqueue(async () => this.write());
|
|
52
|
+
}
|
|
53
|
+
async write(data = this.sessionData) {
|
|
54
|
+
const { encryptionKey } = this.config;
|
|
55
|
+
this.config.setCookie(this.cookieKey, await wrapSession(data, encryptionKey), this.cookieOptions);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { CookieStorage };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A simple promise queue that processes tasks sequentially in the order they are enqueued.
|
|
5
|
+
*/
|
|
6
|
+
class PromiseQueue {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.queue = [];
|
|
9
|
+
this.processing = false;
|
|
10
|
+
}
|
|
11
|
+
async enqueue(task) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
// Wrap the task along with its resolve and reject callbacks
|
|
14
|
+
const wrappedTask = async () => {
|
|
15
|
+
try {
|
|
16
|
+
resolve(await task());
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
reject(error);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
23
|
+
this.queue.push(wrappedTask);
|
|
24
|
+
if (!this.processing) {
|
|
25
|
+
void this.processQueue();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async processQueue() {
|
|
30
|
+
if (this.processing) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.processing = true;
|
|
34
|
+
while (this.queue.length > 0) {
|
|
35
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
36
|
+
const task = this.queue.shift();
|
|
37
|
+
if (task) {
|
|
38
|
+
// eslint-disable-next-line no-await-in-loop
|
|
39
|
+
await task();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.processing = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
exports.PromiseQueue = PromiseQueue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple promise queue that processes tasks sequentially in the order they are enqueued.
|
|
3
|
+
*/
|
|
4
|
+
export declare class PromiseQueue {
|
|
5
|
+
private readonly queue;
|
|
6
|
+
private processing;
|
|
7
|
+
enqueue<T>(task: () => Promise<T>): Promise<T>;
|
|
8
|
+
private processQueue;
|
|
9
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple promise queue that processes tasks sequentially in the order they are enqueued.
|
|
3
|
+
*/
|
|
4
|
+
class PromiseQueue {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.queue = [];
|
|
7
|
+
this.processing = false;
|
|
8
|
+
}
|
|
9
|
+
async enqueue(task) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
// Wrap the task along with its resolve and reject callbacks
|
|
12
|
+
const wrappedTask = async () => {
|
|
13
|
+
try {
|
|
14
|
+
resolve(await task());
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
reject(error);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
21
|
+
this.queue.push(wrappedTask);
|
|
22
|
+
if (!this.processing) {
|
|
23
|
+
void this.processQueue();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async processQueue() {
|
|
28
|
+
if (this.processing) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.processing = true;
|
|
32
|
+
while (this.queue.length > 0) {
|
|
33
|
+
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
|
34
|
+
const task = this.queue.shift();
|
|
35
|
+
if (task) {
|
|
36
|
+
// eslint-disable-next-line no-await-in-loop
|
|
37
|
+
await task();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.processing = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { PromiseQueue };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function getKeyFromPassword(password, crypto) {
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const data = encoder.encode(password);
|
|
6
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
7
|
+
// Convert the hash to a hex string
|
|
8
|
+
return Array.from(new Uint8Array(hash))
|
|
9
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
async function encrypt(text, password, crypto) {
|
|
13
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
14
|
+
const encodedPlaintext = new TextEncoder().encode(text);
|
|
15
|
+
const secretKey = await crypto.subtle.importKey('raw', Buffer.from(await getKeyFromPassword(password, crypto), 'hex'), {
|
|
16
|
+
name: 'AES-GCM',
|
|
17
|
+
length: 256,
|
|
18
|
+
}, true, ['encrypt', 'decrypt']);
|
|
19
|
+
const ciphertext = await crypto.subtle.encrypt({
|
|
20
|
+
name: 'AES-GCM',
|
|
21
|
+
iv,
|
|
22
|
+
}, secretKey, encodedPlaintext);
|
|
23
|
+
return {
|
|
24
|
+
ciphertext: Buffer.from(ciphertext).toString('base64'),
|
|
25
|
+
iv: Buffer.from(iv).toString('base64'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function decrypt(ciphertext, iv, password, crypto) {
|
|
29
|
+
const secretKey = await crypto.subtle.importKey('raw', Buffer.from(await getKeyFromPassword(password, crypto), 'hex'), {
|
|
30
|
+
name: 'AES-GCM',
|
|
31
|
+
length: 256,
|
|
32
|
+
}, true, ['encrypt', 'decrypt']);
|
|
33
|
+
const cleartext = await crypto.subtle.decrypt({
|
|
34
|
+
name: 'AES-GCM',
|
|
35
|
+
iv: Buffer.from(iv, 'base64'),
|
|
36
|
+
}, secretKey, Buffer.from(ciphertext, 'base64'));
|
|
37
|
+
return new TextDecoder().decode(cleartext);
|
|
38
|
+
}
|
|
39
|
+
const unwrapSession = async (cookie, secret, crypto = global.crypto) => {
|
|
40
|
+
try {
|
|
41
|
+
const [ciphertext, iv] = cookie.split('.');
|
|
42
|
+
if (!ciphertext || !iv) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
const decrypted = await decrypt(ciphertext, iv, secret, crypto);
|
|
46
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
47
|
+
return JSON.parse(decrypted);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Ignore invalid session
|
|
51
|
+
}
|
|
52
|
+
return {};
|
|
53
|
+
};
|
|
54
|
+
const wrapSession = async (session, secret, crypto = global.crypto) => {
|
|
55
|
+
const { ciphertext, iv } = await encrypt(JSON.stringify(session), secret, crypto);
|
|
56
|
+
return `${ciphertext}.${iv}`;
|
|
57
|
+
};
|
|
58
|
+
const createSession = async ({ secret, crypto }, cookie, setCookie) => {
|
|
59
|
+
const data = await unwrapSession(cookie, secret, crypto);
|
|
60
|
+
const getValues = async () => wrapSession(session, secret, crypto);
|
|
61
|
+
const session = {
|
|
62
|
+
...data,
|
|
63
|
+
save: async () => {
|
|
64
|
+
setCookie?.(await getValues());
|
|
65
|
+
},
|
|
66
|
+
getValues,
|
|
67
|
+
};
|
|
68
|
+
return session;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
exports.createSession = createSession;
|
|
72
|
+
exports.unwrapSession = unwrapSession;
|
|
73
|
+
exports.wrapSession = wrapSession;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type PersistKey } from '@logto/client';
|
|
2
|
+
export type SessionData = {
|
|
3
|
+
[PersistKey.AccessToken]?: string;
|
|
4
|
+
[PersistKey.IdToken]?: string;
|
|
5
|
+
[PersistKey.SignInSession]?: string;
|
|
6
|
+
[PersistKey.RefreshToken]?: string;
|
|
7
|
+
};
|
|
8
|
+
export type Session = SessionData & {
|
|
9
|
+
save: () => Promise<void>;
|
|
10
|
+
getValues?: () => Promise<string>;
|
|
11
|
+
};
|
|
12
|
+
export declare const unwrapSession: (cookie: string, secret: string, crypto?: Crypto) => Promise<SessionData>;
|
|
13
|
+
export declare const wrapSession: (session: SessionData, secret: string, crypto?: Crypto) => Promise<string>;
|
|
14
|
+
type SessionConfigs = {
|
|
15
|
+
secret: string;
|
|
16
|
+
crypto: Crypto;
|
|
17
|
+
};
|
|
18
|
+
export declare const createSession: ({ secret, crypto }: SessionConfigs, cookie: string, setCookie?: ((value: string) => void) | undefined) => Promise<Session>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
async function getKeyFromPassword(password, crypto) {
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const data = encoder.encode(password);
|
|
4
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
5
|
+
// Convert the hash to a hex string
|
|
6
|
+
return Array.from(new Uint8Array(hash))
|
|
7
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
8
|
+
.join('');
|
|
9
|
+
}
|
|
10
|
+
async function encrypt(text, password, crypto) {
|
|
11
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
12
|
+
const encodedPlaintext = new TextEncoder().encode(text);
|
|
13
|
+
const secretKey = await crypto.subtle.importKey('raw', Buffer.from(await getKeyFromPassword(password, crypto), 'hex'), {
|
|
14
|
+
name: 'AES-GCM',
|
|
15
|
+
length: 256,
|
|
16
|
+
}, true, ['encrypt', 'decrypt']);
|
|
17
|
+
const ciphertext = await crypto.subtle.encrypt({
|
|
18
|
+
name: 'AES-GCM',
|
|
19
|
+
iv,
|
|
20
|
+
}, secretKey, encodedPlaintext);
|
|
21
|
+
return {
|
|
22
|
+
ciphertext: Buffer.from(ciphertext).toString('base64'),
|
|
23
|
+
iv: Buffer.from(iv).toString('base64'),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function decrypt(ciphertext, iv, password, crypto) {
|
|
27
|
+
const secretKey = await crypto.subtle.importKey('raw', Buffer.from(await getKeyFromPassword(password, crypto), 'hex'), {
|
|
28
|
+
name: 'AES-GCM',
|
|
29
|
+
length: 256,
|
|
30
|
+
}, true, ['encrypt', 'decrypt']);
|
|
31
|
+
const cleartext = await crypto.subtle.decrypt({
|
|
32
|
+
name: 'AES-GCM',
|
|
33
|
+
iv: Buffer.from(iv, 'base64'),
|
|
34
|
+
}, secretKey, Buffer.from(ciphertext, 'base64'));
|
|
35
|
+
return new TextDecoder().decode(cleartext);
|
|
36
|
+
}
|
|
37
|
+
const unwrapSession = async (cookie, secret, crypto = global.crypto) => {
|
|
38
|
+
try {
|
|
39
|
+
const [ciphertext, iv] = cookie.split('.');
|
|
40
|
+
if (!ciphertext || !iv) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
const decrypted = await decrypt(ciphertext, iv, secret, crypto);
|
|
44
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
45
|
+
return JSON.parse(decrypted);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Ignore invalid session
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
};
|
|
52
|
+
const wrapSession = async (session, secret, crypto = global.crypto) => {
|
|
53
|
+
const { ciphertext, iv } = await encrypt(JSON.stringify(session), secret, crypto);
|
|
54
|
+
return `${ciphertext}.${iv}`;
|
|
55
|
+
};
|
|
56
|
+
const createSession = async ({ secret, crypto }, cookie, setCookie) => {
|
|
57
|
+
const data = await unwrapSession(cookie, secret, crypto);
|
|
58
|
+
const getValues = async () => wrapSession(session, secret, crypto);
|
|
59
|
+
const session = {
|
|
60
|
+
...data,
|
|
61
|
+
save: async () => {
|
|
62
|
+
setCookie?.(await getValues());
|
|
63
|
+
},
|
|
64
|
+
getValues,
|
|
65
|
+
};
|
|
66
|
+
return session;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export { createSession, unwrapSession, wrapSession };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logto/node",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/src/index.cjs",
|
|
6
6
|
"module": "./lib/src/index.js",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"require": "./lib/edge/index.cjs",
|
|
16
16
|
"import": "./lib/edge/index.js",
|
|
17
17
|
"types": "./lib/edge/index.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./exports": {
|
|
20
|
+
"require": "./lib/exports/index.cjs",
|
|
21
|
+
"import": "./lib/exports/index.js",
|
|
22
|
+
"types": "./lib/exports/index.d.ts"
|
|
18
23
|
}
|
|
19
24
|
},
|
|
20
25
|
"files": [
|
|
@@ -27,31 +32,28 @@
|
|
|
27
32
|
"directory": "packages/node"
|
|
28
33
|
},
|
|
29
34
|
"dependencies": {
|
|
30
|
-
"@logto/client": "^2.3.3",
|
|
31
35
|
"@silverhand/essentials": "^2.8.7",
|
|
32
36
|
"js-base64": "^3.7.4",
|
|
33
|
-
"
|
|
37
|
+
"@logto/client": "^2.6.0"
|
|
34
38
|
},
|
|
35
39
|
"devDependencies": {
|
|
36
40
|
"@silverhand/eslint-config": "^5.0.0",
|
|
37
41
|
"@silverhand/ts-config": "^5.0.0",
|
|
38
42
|
"@swc/core": "^1.3.7",
|
|
39
43
|
"@swc/jest": "^0.2.24",
|
|
44
|
+
"@types/cookie": "^0.6.0",
|
|
40
45
|
"@types/jest": "^29.5.0",
|
|
41
|
-
"@types/node": "^20.
|
|
46
|
+
"@types/node": "^20.11.19",
|
|
42
47
|
"eslint": "^8.44.0",
|
|
43
48
|
"jest": "^29.5.0",
|
|
44
49
|
"jest-location-mock": "^2.0.0",
|
|
45
50
|
"jest-matcher-specific-error": "^1.0.0",
|
|
46
51
|
"lint-staged": "^15.0.0",
|
|
47
52
|
"prettier": "^3.0.0",
|
|
48
|
-
"typescript": "^5.
|
|
53
|
+
"typescript": "^5.3.3"
|
|
49
54
|
},
|
|
50
55
|
"eslintConfig": {
|
|
51
|
-
"extends": "@silverhand"
|
|
52
|
-
"rules": {
|
|
53
|
-
"unicorn/prefer-node-protocol": "off"
|
|
54
|
-
}
|
|
56
|
+
"extends": "@silverhand"
|
|
55
57
|
},
|
|
56
58
|
"prettier": "@silverhand/eslint-config/.prettierrc",
|
|
57
59
|
"publishConfig": {
|
package/lib/src/index.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var crypto = require('crypto');
|
|
4
|
-
var jsBase64 = require('js-base64');
|
|
5
|
-
|
|
6
|
-
/** @link [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636) */
|
|
7
|
-
/**
|
|
8
|
-
* @param length The length of the raw random data.
|
|
9
|
-
*/
|
|
10
|
-
const generateRandomString = (length = 64) => jsBase64.fromUint8Array(crypto.randomFillSync(new Uint8Array(length)), true);
|
|
11
|
-
/**
|
|
12
|
-
* Generates random string for state and encodes them in url safe base64
|
|
13
|
-
*/
|
|
14
|
-
const generateState = () => generateRandomString();
|
|
15
|
-
/**
|
|
16
|
-
* Generates code verifier
|
|
17
|
-
*
|
|
18
|
-
* @link [Client Creates a Code Verifier](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1)
|
|
19
|
-
*/
|
|
20
|
-
const generateCodeVerifier = () => generateRandomString();
|
|
21
|
-
/**
|
|
22
|
-
* Calculates the S256 PKCE code challenge for an arbitrary code verifier and encodes it in url safe base64
|
|
23
|
-
*
|
|
24
|
-
* @param {String} codeVerifier Code verifier to calculate the S256 code challenge for
|
|
25
|
-
* @link [Client Creates the Code Challenge](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2)
|
|
26
|
-
*/
|
|
27
|
-
const generateCodeChallenge = async (codeVerifier) => {
|
|
28
|
-
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier);
|
|
29
|
-
const hash = crypto.createHash('sha256');
|
|
30
|
-
hash.update(encodedCodeVerifier);
|
|
31
|
-
const codeChallenge = hash.digest();
|
|
32
|
-
return jsBase64.fromUint8Array(codeChallenge, true);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
exports.generateCodeChallenge = generateCodeChallenge;
|
|
36
|
-
exports.generateCodeVerifier = generateCodeVerifier;
|
|
37
|
-
exports.generateState = generateState;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generates random string for state and encodes them in url safe base64
|
|
3
|
-
*/
|
|
4
|
-
export declare const generateState: () => string;
|
|
5
|
-
/**
|
|
6
|
-
* Generates code verifier
|
|
7
|
-
*
|
|
8
|
-
* @link [Client Creates a Code Verifier](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1)
|
|
9
|
-
*/
|
|
10
|
-
export declare const generateCodeVerifier: () => string;
|
|
11
|
-
/**
|
|
12
|
-
* Calculates the S256 PKCE code challenge for an arbitrary code verifier and encodes it in url safe base64
|
|
13
|
-
*
|
|
14
|
-
* @param {String} codeVerifier Code verifier to calculate the S256 code challenge for
|
|
15
|
-
* @link [Client Creates the Code Challenge](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2)
|
|
16
|
-
*/
|
|
17
|
-
export declare const generateCodeChallenge: (codeVerifier: string) => Promise<string>;
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { createHash, randomFillSync } from 'crypto';
|
|
2
|
-
import { fromUint8Array } from 'js-base64';
|
|
3
|
-
|
|
4
|
-
/** @link [Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636) */
|
|
5
|
-
/**
|
|
6
|
-
* @param length The length of the raw random data.
|
|
7
|
-
*/
|
|
8
|
-
const generateRandomString = (length = 64) => fromUint8Array(randomFillSync(new Uint8Array(length)), true);
|
|
9
|
-
/**
|
|
10
|
-
* Generates random string for state and encodes them in url safe base64
|
|
11
|
-
*/
|
|
12
|
-
const generateState = () => generateRandomString();
|
|
13
|
-
/**
|
|
14
|
-
* Generates code verifier
|
|
15
|
-
*
|
|
16
|
-
* @link [Client Creates a Code Verifier](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1)
|
|
17
|
-
*/
|
|
18
|
-
const generateCodeVerifier = () => generateRandomString();
|
|
19
|
-
/**
|
|
20
|
-
* Calculates the S256 PKCE code challenge for an arbitrary code verifier and encodes it in url safe base64
|
|
21
|
-
*
|
|
22
|
-
* @param {String} codeVerifier Code verifier to calculate the S256 code challenge for
|
|
23
|
-
* @link [Client Creates the Code Challenge](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2)
|
|
24
|
-
*/
|
|
25
|
-
const generateCodeChallenge = async (codeVerifier) => {
|
|
26
|
-
const encodedCodeVerifier = new TextEncoder().encode(codeVerifier);
|
|
27
|
-
const hash = createHash('sha256');
|
|
28
|
-
hash.update(encodedCodeVerifier);
|
|
29
|
-
const codeChallenge = hash.digest();
|
|
30
|
-
return fromUint8Array(codeChallenge, true);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export { generateCodeChallenge, generateCodeVerifier, generateState };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|