@logto/node 2.2.2 → 2.4.0

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.
@@ -32,7 +32,7 @@ class LogtoClient extends client.default {
32
32
  }
33
33
  }
34
34
 
35
- Object.defineProperty(exports, 'PersistKey', {
35
+ Object.defineProperty(exports, "PersistKey", {
36
36
  enumerable: true,
37
37
  get: function () { return BaseClient.PersistKey; }
38
38
  });
@@ -12,7 +12,7 @@ var BaseClient__default = /*#__PURE__*/_interopDefault(BaseClient);
12
12
  class LogtoNodeBaseClient extends BaseClient__default.default {
13
13
  constructor() {
14
14
  super(...arguments);
15
- this.getContext = async ({ getAccessToken, resource, fetchUserInfo, } = {}) => {
15
+ this.getContext = async ({ getAccessToken, resource, fetchUserInfo, getOrganizationToken, } = {}) => {
16
16
  const isAuthenticated = await this.isAuthenticated();
17
17
  if (!isAuthenticated) {
18
18
  return {
@@ -20,58 +20,60 @@ class LogtoNodeBaseClient extends BaseClient__default.default {
20
20
  };
21
21
  }
22
22
  const claims = await this.getIdTokenClaims();
23
- if (!getAccessToken) {
24
- return {
25
- isAuthenticated,
26
- claims,
27
- userInfo: essentials.conditional(fetchUserInfo && (await this.fetchUserInfo())),
28
- };
29
- }
30
- try {
31
- const accessToken = await this.getAccessToken(resource);
32
- const accessTokenClaims = await this.getAccessTokenClaims(resource);
33
- return {
34
- isAuthenticated,
35
- claims: await this.getIdTokenClaims(),
36
- userInfo: essentials.conditional(fetchUserInfo && (await this.fetchUserInfo())),
37
- accessToken,
38
- scopes: accessTokenClaims.scope?.split(' '),
39
- };
40
- }
41
- catch {
23
+ const { accessToken, accessTokenClaims } = getAccessToken
24
+ ? {
25
+ accessToken: await essentials.trySafe(async () => this.getAccessToken(resource)),
26
+ accessTokenClaims: await essentials.trySafe(async () => this.getAccessTokenClaims(resource)),
27
+ }
28
+ : { accessToken: undefined, accessTokenClaims: undefined };
29
+ if (getAccessToken && !accessToken) {
30
+ // Failed to get access token, the user is not authenticated
42
31
  return {
43
32
  isAuthenticated: false,
44
33
  };
45
34
  }
35
+ const organizationTokens = essentials.conditional(getOrganizationToken &&
36
+ claims.organizations &&
37
+ Object.fromEntries(await Promise.all(claims.organizations.map(async (organizationId) => [
38
+ organizationId,
39
+ await this.getOrganizationToken(organizationId),
40
+ ]))));
41
+ return {
42
+ isAuthenticated,
43
+ claims,
44
+ userInfo: essentials.conditional(fetchUserInfo && (await this.fetchUserInfo())),
45
+ ...essentials.conditional(getAccessToken && { accessToken, scopes: accessTokenClaims?.scope?.split(' ') }),
46
+ organizationTokens,
47
+ };
46
48
  };
47
49
  }
48
50
  }
49
51
 
50
- Object.defineProperty(exports, 'LogtoClientError', {
52
+ Object.defineProperty(exports, "LogtoClientError", {
51
53
  enumerable: true,
52
54
  get: function () { return BaseClient.LogtoClientError; }
53
55
  });
54
- Object.defineProperty(exports, 'LogtoError', {
56
+ Object.defineProperty(exports, "LogtoError", {
55
57
  enumerable: true,
56
58
  get: function () { return BaseClient.LogtoError; }
57
59
  });
58
- Object.defineProperty(exports, 'LogtoRequestError', {
60
+ Object.defineProperty(exports, "LogtoRequestError", {
59
61
  enumerable: true,
60
62
  get: function () { return BaseClient.LogtoRequestError; }
61
63
  });
62
- Object.defineProperty(exports, 'OidcError', {
64
+ Object.defineProperty(exports, "OidcError", {
63
65
  enumerable: true,
64
66
  get: function () { return BaseClient.OidcError; }
65
67
  });
66
- Object.defineProperty(exports, 'Prompt', {
68
+ Object.defineProperty(exports, "Prompt", {
67
69
  enumerable: true,
68
70
  get: function () { return BaseClient.Prompt; }
69
71
  });
70
- Object.defineProperty(exports, 'ReservedScope', {
72
+ Object.defineProperty(exports, "ReservedScope", {
71
73
  enumerable: true,
72
74
  get: function () { return BaseClient.ReservedScope; }
73
75
  });
74
- Object.defineProperty(exports, 'UserScope', {
76
+ Object.defineProperty(exports, "UserScope", {
75
77
  enumerable: true,
76
78
  get: function () { return BaseClient.UserScope; }
77
79
  });
@@ -4,5 +4,5 @@ export type { LogtoContext, GetContextParameters } from './types.js';
4
4
  export type { IdTokenClaims, LogtoErrorCode, LogtoConfig, LogtoClientErrorCode, Storage, StorageKey, InteractionMode, } from '@logto/client';
5
5
  export { LogtoError, LogtoRequestError, LogtoClientError, OidcError, Prompt, ReservedScope, UserScope, } from '@logto/client';
6
6
  export default class LogtoNodeBaseClient extends BaseClient {
7
- getContext: ({ getAccessToken, resource, fetchUserInfo, }?: GetContextParameters) => Promise<LogtoContext>;
7
+ getContext: ({ getAccessToken, resource, fetchUserInfo, getOrganizationToken, }?: GetContextParameters) => Promise<LogtoContext>;
8
8
  }
package/lib/src/client.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import BaseClient from '@logto/client';
2
2
  export { LogtoClientError, LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, UserScope } from '@logto/client';
3
- import { conditional } from '@silverhand/essentials';
3
+ import { trySafe, conditional } from '@silverhand/essentials';
4
4
 
5
5
  class LogtoNodeBaseClient extends BaseClient {
6
6
  constructor() {
7
7
  super(...arguments);
8
- this.getContext = async ({ getAccessToken, resource, fetchUserInfo, } = {}) => {
8
+ this.getContext = async ({ getAccessToken, resource, fetchUserInfo, getOrganizationToken, } = {}) => {
9
9
  const isAuthenticated = await this.isAuthenticated();
10
10
  if (!isAuthenticated) {
11
11
  return {
@@ -13,29 +13,31 @@ class LogtoNodeBaseClient extends BaseClient {
13
13
  };
14
14
  }
15
15
  const claims = await this.getIdTokenClaims();
16
- if (!getAccessToken) {
17
- return {
18
- isAuthenticated,
19
- claims,
20
- userInfo: conditional(fetchUserInfo && (await this.fetchUserInfo())),
21
- };
22
- }
23
- try {
24
- const accessToken = await this.getAccessToken(resource);
25
- const accessTokenClaims = await this.getAccessTokenClaims(resource);
26
- return {
27
- isAuthenticated,
28
- claims: await this.getIdTokenClaims(),
29
- userInfo: conditional(fetchUserInfo && (await this.fetchUserInfo())),
30
- accessToken,
31
- scopes: accessTokenClaims.scope?.split(' '),
32
- };
33
- }
34
- catch {
16
+ const { accessToken, accessTokenClaims } = getAccessToken
17
+ ? {
18
+ accessToken: await trySafe(async () => this.getAccessToken(resource)),
19
+ accessTokenClaims: await trySafe(async () => this.getAccessTokenClaims(resource)),
20
+ }
21
+ : { accessToken: undefined, accessTokenClaims: undefined };
22
+ if (getAccessToken && !accessToken) {
23
+ // Failed to get access token, the user is not authenticated
35
24
  return {
36
25
  isAuthenticated: false,
37
26
  };
38
27
  }
28
+ const organizationTokens = conditional(getOrganizationToken &&
29
+ claims.organizations &&
30
+ Object.fromEntries(await Promise.all(claims.organizations.map(async (organizationId) => [
31
+ organizationId,
32
+ await this.getOrganizationToken(organizationId),
33
+ ]))));
34
+ return {
35
+ isAuthenticated,
36
+ claims,
37
+ userInfo: conditional(fetchUserInfo && (await this.fetchUserInfo())),
38
+ ...conditional(getAccessToken && { accessToken, scopes: accessTokenClaims?.scope?.split(' ') }),
39
+ organizationTokens,
40
+ };
39
41
  };
40
42
  }
41
43
  }
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 fetch = require('node-fetch');
6
+ var generators = require('../edge/generators.cjs');
7
7
  var client = require('./client.cjs');
8
- var generators = require('./utils/generators.cjs');
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 fetch__default.default(input, {
17
+ return fetch(input, {
22
18
  ...init,
23
19
  headers: {
24
20
  Authorization: `Basic ${Buffer.from(
@@ -28,60 +24,69 @@ class LogtoClient extends client.default {
28
24
  },
29
25
  });
30
26
  }
31
- : fetch__default.default),
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
 
39
- Object.defineProperty(exports, 'LogtoClientError', {
36
+ Object.defineProperty(exports, "LogtoClientError", {
40
37
  enumerable: true,
41
38
  get: function () { return BaseClient.LogtoClientError; }
42
39
  });
43
- Object.defineProperty(exports, 'LogtoError', {
40
+ Object.defineProperty(exports, "LogtoError", {
44
41
  enumerable: true,
45
42
  get: function () { return BaseClient.LogtoError; }
46
43
  });
47
- Object.defineProperty(exports, 'LogtoRequestError', {
44
+ Object.defineProperty(exports, "LogtoRequestError", {
48
45
  enumerable: true,
49
46
  get: function () { return BaseClient.LogtoRequestError; }
50
47
  });
51
- Object.defineProperty(exports, 'OidcError', {
48
+ Object.defineProperty(exports, "OidcError", {
52
49
  enumerable: true,
53
50
  get: function () { return BaseClient.OidcError; }
54
51
  });
55
- Object.defineProperty(exports, 'PersistKey', {
52
+ Object.defineProperty(exports, "PersistKey", {
56
53
  enumerable: true,
57
54
  get: function () { return BaseClient.PersistKey; }
58
55
  });
59
- Object.defineProperty(exports, 'Prompt', {
56
+ Object.defineProperty(exports, "Prompt", {
60
57
  enumerable: true,
61
58
  get: function () { return BaseClient.Prompt; }
62
59
  });
63
- Object.defineProperty(exports, 'ReservedResource', {
60
+ Object.defineProperty(exports, "ReservedResource", {
64
61
  enumerable: true,
65
62
  get: function () { return BaseClient.ReservedResource; }
66
63
  });
67
- Object.defineProperty(exports, 'ReservedScope', {
64
+ Object.defineProperty(exports, "ReservedScope", {
68
65
  enumerable: true,
69
66
  get: function () { return BaseClient.ReservedScope; }
70
67
  });
71
- Object.defineProperty(exports, 'UserScope', {
68
+ Object.defineProperty(exports, "StandardLogtoClient", {
69
+ enumerable: true,
70
+ get: function () { return BaseClient.StandardLogtoClient; }
71
+ });
72
+ Object.defineProperty(exports, "UserScope", {
72
73
  enumerable: true,
73
74
  get: function () { return BaseClient.UserScope; }
74
75
  });
75
- Object.defineProperty(exports, 'buildOrganizationUrn', {
76
+ Object.defineProperty(exports, "buildOrganizationUrn", {
76
77
  enumerable: true,
77
78
  get: function () { return BaseClient.buildOrganizationUrn; }
78
79
  });
79
- Object.defineProperty(exports, 'getOrganizationIdFromUrn', {
80
+ Object.defineProperty(exports, "getOrganizationIdFromUrn", {
80
81
  enumerable: true,
81
82
  get: function () { return BaseClient.getOrganizationIdFromUrn; }
82
83
  });
83
- Object.defineProperty(exports, 'organizationUrnPrefix', {
84
+ 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;
@@ -1,8 +1,10 @@
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
3
  export type { LogtoContext, GetContextParameters } from './types.js';
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';
4
+ export * from './utils/session.js';
5
+ export * from './utils/cookie-storage.js';
6
+ export type { IdTokenClaims, LogtoErrorCode, LogtoConfig, LogtoClientErrorCode, Storage, StorageKey, InteractionMode, ClientAdapter, JwtVerifier, UserInfoResponse, } from '@logto/client';
7
+ export { LogtoError, LogtoRequestError, LogtoClientError, OidcError, Prompt, ReservedScope, ReservedResource, UserScope, organizationUrnPrefix, buildOrganizationUrn, getOrganizationIdFromUrn, PersistKey, StandardLogtoClient, } from '@logto/client';
6
8
  export default class LogtoClient extends BaseClient {
7
- constructor(config: LogtoConfig, adapter: Pick<ClientAdapter, 'navigate' | 'storage'>);
9
+ constructor(config: LogtoConfig, adapter: Partial<ClientAdapter> & Pick<ClientAdapter, 'navigate' | 'storage'>, buildJwtVerifier?: (client: StandardLogtoClient) => JwtVerifier);
8
10
  }
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 fetch from 'node-fetch';
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
- import { generateCodeChallenge, generateCodeVerifier, generateState } from './utils/generators.js';
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
 
@@ -10,9 +10,11 @@ export type LogtoContext = {
10
10
  accessToken?: string;
11
11
  userInfo?: UserInfoResponse;
12
12
  scopes?: string[];
13
+ organizationTokens?: Record<string, string>;
13
14
  };
14
15
  export type GetContextParameters = {
15
16
  fetchUserInfo?: boolean;
16
17
  getAccessToken?: boolean;
17
18
  resource?: string;
19
+ getOrganizationToken?: boolean;
18
20
  };
@@ -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.2.2",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "main": "./lib/src/index.cjs",
6
6
  "module": "./lib/src/index.js",
@@ -27,25 +27,25 @@
27
27
  "directory": "packages/node"
28
28
  },
29
29
  "dependencies": {
30
- "@logto/client": "^2.3.1",
31
- "@silverhand/essentials": "^2.6.2",
32
- "js-base64": "^3.7.4",
33
- "node-fetch": "^2.6.7"
30
+ "@logto/client": "^2.4.0",
31
+ "@silverhand/essentials": "^2.8.7",
32
+ "js-base64": "^3.7.4"
34
33
  },
35
34
  "devDependencies": {
36
- "@silverhand/eslint-config": "^4.0.1",
37
- "@silverhand/ts-config": "^4.0.0",
35
+ "@silverhand/eslint-config": "^5.0.0",
36
+ "@silverhand/ts-config": "^5.0.0",
38
37
  "@swc/core": "^1.3.7",
39
38
  "@swc/jest": "^0.2.24",
39
+ "@types/cookie": "^0.6.0",
40
40
  "@types/jest": "^29.5.0",
41
- "@types/node": "^18.15.11",
41
+ "@types/node": "^20.11.19",
42
42
  "eslint": "^8.44.0",
43
43
  "jest": "^29.5.0",
44
44
  "jest-location-mock": "^2.0.0",
45
45
  "jest-matcher-specific-error": "^1.0.0",
46
46
  "lint-staged": "^15.0.0",
47
47
  "prettier": "^3.0.0",
48
- "typescript": "^5.0.0"
48
+ "typescript": "^5.3.3"
49
49
  },
50
50
  "eslintConfig": {
51
51
  "extends": "@silverhand",
@@ -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 {};