@technomoron/api-server-base 2.0.0-beta.11 → 2.0.0-beta.14
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/dist/cjs/api-server-base.cjs +58 -0
- package/dist/cjs/api-server-base.d.ts +4 -0
- package/dist/cjs/auth-api/auth-module.js +14 -15
- package/dist/cjs/index.cjs +1 -11
- package/dist/cjs/index.d.ts +0 -5
- package/dist/cjs/passkey/memory.js +5 -3
- package/dist/cjs/passkey/models.d.ts +7 -2
- package/dist/cjs/passkey/models.js +34 -4
- package/dist/cjs/passkey/sequelize.d.ts +7 -1
- package/dist/cjs/passkey/sequelize.js +57 -8
- package/dist/cjs/passkey/service.d.ts +1 -1
- package/dist/cjs/passkey/service.js +21 -15
- package/dist/cjs/passkey/types.d.ts +9 -9
- package/dist/esm/api-server-base.d.ts +4 -0
- package/dist/esm/api-server-base.js +58 -0
- package/dist/esm/auth-api/auth-module.js +14 -15
- package/dist/esm/index.d.ts +0 -5
- package/dist/esm/index.js +0 -5
- package/dist/esm/passkey/memory.js +5 -3
- package/dist/esm/passkey/models.d.ts +7 -2
- package/dist/esm/passkey/models.js +34 -4
- package/dist/esm/passkey/sequelize.d.ts +7 -1
- package/dist/esm/passkey/sequelize.js +57 -8
- package/dist/esm/passkey/service.d.ts +1 -1
- package/dist/esm/passkey/service.js +21 -15
- package/dist/esm/passkey/types.d.ts +9 -9
- package/docs/swagger/openapi.json +2010 -0
- package/package.json +56 -10
|
@@ -11,6 +11,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ApiServer = exports.ApiError = exports.ApiModule = void 0;
|
|
13
13
|
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
15
|
+
const node_module_1 = require("node:module");
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
17
|
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
|
15
18
|
const cors_1 = __importDefault(require("cors"));
|
|
16
19
|
const express_1 = __importDefault(require("express"));
|
|
@@ -340,6 +343,8 @@ function fillConfig(config) {
|
|
|
340
343
|
origins: config.origins ?? [],
|
|
341
344
|
debug: config.debug ?? false,
|
|
342
345
|
apiBasePath: config.apiBasePath ?? '/api',
|
|
346
|
+
swaggerEnabled: config.swaggerEnabled ?? false,
|
|
347
|
+
swaggerPath: config.swaggerPath ?? '',
|
|
343
348
|
accessSecret: config.accessSecret ?? '',
|
|
344
349
|
refreshSecret: config.refreshSecret ?? '',
|
|
345
350
|
cookieDomain: config.cookieDomain ?? '.somewhere-over-the-rainbow.com',
|
|
@@ -391,6 +396,7 @@ class ApiServer {
|
|
|
391
396
|
}
|
|
392
397
|
this.middlewares();
|
|
393
398
|
this.installPingHandler();
|
|
399
|
+
this.installSwaggerHandler();
|
|
394
400
|
// addSwaggerUi(this.app);
|
|
395
401
|
this.installApiNotFoundHandler();
|
|
396
402
|
}
|
|
@@ -630,6 +636,58 @@ class ApiServer {
|
|
|
630
636
|
res.status(200).json({ success: true, code: 200, message: 'Success', data: payload, errors: {} });
|
|
631
637
|
});
|
|
632
638
|
}
|
|
639
|
+
loadSwaggerSpec() {
|
|
640
|
+
const candidates = [node_path_1.default.resolve(process.cwd(), 'docs/swagger/openapi.json')];
|
|
641
|
+
if (typeof __dirname === 'string') {
|
|
642
|
+
candidates.push(node_path_1.default.resolve(__dirname, '../../docs/swagger/openapi.json'));
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
const require = (0, node_module_1.createRequire)(node_path_1.default.join(process.cwd(), 'package.json'));
|
|
646
|
+
const entry = require.resolve('@technomoron/api-server-base');
|
|
647
|
+
const packageRoot = node_path_1.default.resolve(node_path_1.default.dirname(entry), '..', '..');
|
|
648
|
+
candidates.push(node_path_1.default.join(packageRoot, 'docs/swagger/openapi.json'));
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
// Ignore resolution failures; fall back to any existing candidate.
|
|
652
|
+
}
|
|
653
|
+
for (const candidate of candidates) {
|
|
654
|
+
if (!node_fs_1.default.existsSync(candidate)) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const raw = node_fs_1.default.readFileSync(candidate, 'utf8');
|
|
659
|
+
return JSON.parse(raw);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
installSwaggerHandler() {
|
|
668
|
+
const rawPath = typeof this.config.swaggerPath === 'string' ? this.config.swaggerPath.trim() : '';
|
|
669
|
+
const enabled = Boolean(this.config.swaggerEnabled) || rawPath.length > 0;
|
|
670
|
+
if (!enabled) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
|
|
674
|
+
const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
|
|
675
|
+
const path = resolved.startsWith('/') ? resolved : `/${resolved}`;
|
|
676
|
+
const spec = this.loadSwaggerSpec();
|
|
677
|
+
this.app.get(path, (_req, res) => {
|
|
678
|
+
if (!spec) {
|
|
679
|
+
res.status(500).json({
|
|
680
|
+
success: false,
|
|
681
|
+
code: 500,
|
|
682
|
+
message: 'Swagger spec is unavailable',
|
|
683
|
+
data: null,
|
|
684
|
+
errors: {}
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
res.status(200).json(spec);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
633
691
|
normalizeApiBasePath(path) {
|
|
634
692
|
if (!path || typeof path !== 'string') {
|
|
635
693
|
return '/api';
|
|
@@ -95,6 +95,8 @@ export interface ApiServerConf {
|
|
|
95
95
|
origins: string[];
|
|
96
96
|
debug: boolean;
|
|
97
97
|
apiBasePath: string;
|
|
98
|
+
swaggerEnabled?: boolean;
|
|
99
|
+
swaggerPath?: string;
|
|
98
100
|
accessSecret: string;
|
|
99
101
|
refreshSecret: string;
|
|
100
102
|
cookieDomain: string;
|
|
@@ -190,6 +192,8 @@ export declare class ApiServer {
|
|
|
190
192
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
191
193
|
private middlewares;
|
|
192
194
|
private installPingHandler;
|
|
195
|
+
private loadSwaggerSpec;
|
|
196
|
+
private installSwaggerHandler;
|
|
193
197
|
private normalizeApiBasePath;
|
|
194
198
|
private installApiNotFoundHandler;
|
|
195
199
|
private ensureApiNotFoundOrdering;
|
|
@@ -600,15 +600,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
600
600
|
const params = {
|
|
601
601
|
action,
|
|
602
602
|
login: toStringOrNull(body.login) ?? undefined,
|
|
603
|
-
userId: isAuthIdentifier(body.userId) ? body.userId : undefined
|
|
604
|
-
userAgent: toStringOrNull(body.userAgent) ?? undefined,
|
|
605
|
-
domain: toStringOrNull(body.domain) ?? undefined,
|
|
606
|
-
fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
|
|
607
|
-
label: toStringOrNull(body.label) ?? undefined,
|
|
608
|
-
browser: toStringOrNull(body.browser) ?? undefined,
|
|
609
|
-
device: toStringOrNull(body.device) ?? undefined,
|
|
610
|
-
ip: toStringOrNull(body.ip) ?? undefined,
|
|
611
|
-
os: toStringOrNull(body.os) ?? undefined
|
|
603
|
+
userId: isAuthIdentifier(body.userId) ? body.userId : undefined
|
|
612
604
|
};
|
|
613
605
|
const challenge = await this.storage.createPasskeyChallenge(params);
|
|
614
606
|
return [200, challenge];
|
|
@@ -624,18 +616,25 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
624
616
|
if (!expectedChallenge || typeof response !== 'object' || response === null) {
|
|
625
617
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'Malformed passkey verification payload' });
|
|
626
618
|
}
|
|
627
|
-
const
|
|
628
|
-
expectedChallenge,
|
|
629
|
-
response: response,
|
|
630
|
-
login: toStringOrNull(body.login) ?? undefined,
|
|
631
|
-
userId: isAuthIdentifier(body.userId) ? body.userId : undefined,
|
|
619
|
+
const rawMetadata = {
|
|
632
620
|
domain: toStringOrNull(body.domain) ?? undefined,
|
|
633
621
|
fingerprint: toStringOrNull(body.fingerprint) ?? undefined,
|
|
634
622
|
label: toStringOrNull(body.label) ?? undefined,
|
|
635
623
|
browser: toStringOrNull(body.browser) ?? undefined,
|
|
636
624
|
device: toStringOrNull(body.device) ?? undefined,
|
|
637
625
|
ip: toStringOrNull(body.ip) ?? undefined,
|
|
638
|
-
os: toStringOrNull(body.os) ?? undefined
|
|
626
|
+
os: toStringOrNull(body.os) ?? undefined
|
|
627
|
+
};
|
|
628
|
+
const clientInfo = apiReq.getClientInfo();
|
|
629
|
+
const userAgent = toStringOrNull(body.userAgent) ?? (clientInfo.ua ? clientInfo.ua : null);
|
|
630
|
+
const requestMetadata = this.enrichTokenMetadata(apiReq, rawMetadata);
|
|
631
|
+
const params = {
|
|
632
|
+
expectedChallenge,
|
|
633
|
+
response: response,
|
|
634
|
+
login: toStringOrNull(body.login) ?? undefined,
|
|
635
|
+
userId: isAuthIdentifier(body.userId) ? body.userId : undefined,
|
|
636
|
+
userAgent: userAgent ?? undefined,
|
|
637
|
+
...requestMetadata,
|
|
639
638
|
...sessionPrefs
|
|
640
639
|
};
|
|
641
640
|
const result = await this.storage.verifyPasskeyResponse(params);
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.MemoryOAuthStore = exports.OAuthStore = exports.MemoryPasskeyStore = exports.PasskeyStore = exports.PasskeyService = exports.MemoryTokenStore = exports.TokenStore = exports.MemoryUserStore = exports.UserStore = exports.AuthModule = exports.MemAuthStore = exports.CompositeAuthAdapter = exports.BaseAuthModule = exports.nullAuthModule = exports.BaseAuthAdapter = exports.nullAuthAdapter = exports.ApiModule = exports.ApiError = exports.ApiServer = void 0;
|
|
7
7
|
var api_server_base_js_1 = require("./api-server-base.cjs");
|
|
8
8
|
Object.defineProperty(exports, "ApiServer", { enumerable: true, get: function () { return __importDefault(api_server_base_js_1).default; } });
|
|
9
9
|
var api_server_base_js_2 = require("./api-server-base.cjs");
|
|
@@ -20,33 +20,23 @@ var compat_auth_storage_js_1 = require("./auth-api/compat-auth-storage.js");
|
|
|
20
20
|
Object.defineProperty(exports, "CompositeAuthAdapter", { enumerable: true, get: function () { return compat_auth_storage_js_1.CompositeAuthAdapter; } });
|
|
21
21
|
var mem_auth_store_js_1 = require("./auth-api/mem-auth-store.js");
|
|
22
22
|
Object.defineProperty(exports, "MemAuthStore", { enumerable: true, get: function () { return mem_auth_store_js_1.MemAuthStore; } });
|
|
23
|
-
var sql_auth_store_js_1 = require("./auth-api/sql-auth-store.js");
|
|
24
|
-
Object.defineProperty(exports, "SqlAuthStore", { enumerable: true, get: function () { return sql_auth_store_js_1.SqlAuthStore; } });
|
|
25
23
|
var auth_module_js_1 = require("./auth-api/auth-module.js");
|
|
26
24
|
Object.defineProperty(exports, "AuthModule", { enumerable: true, get: function () { return __importDefault(auth_module_js_1).default; } });
|
|
27
25
|
var base_js_1 = require("./user/base.js");
|
|
28
26
|
Object.defineProperty(exports, "UserStore", { enumerable: true, get: function () { return base_js_1.UserStore; } });
|
|
29
27
|
var memory_js_1 = require("./user/memory.js");
|
|
30
28
|
Object.defineProperty(exports, "MemoryUserStore", { enumerable: true, get: function () { return memory_js_1.MemoryUserStore; } });
|
|
31
|
-
var sequelize_js_1 = require("./user/sequelize.js");
|
|
32
|
-
Object.defineProperty(exports, "SequelizeUserStore", { enumerable: true, get: function () { return sequelize_js_1.SequelizeUserStore; } });
|
|
33
29
|
var base_js_2 = require("./token/base.js");
|
|
34
30
|
Object.defineProperty(exports, "TokenStore", { enumerable: true, get: function () { return base_js_2.TokenStore; } });
|
|
35
31
|
var memory_js_2 = require("./token/memory.js");
|
|
36
32
|
Object.defineProperty(exports, "MemoryTokenStore", { enumerable: true, get: function () { return memory_js_2.MemoryTokenStore; } });
|
|
37
|
-
var sequelize_js_2 = require("./token/sequelize.js");
|
|
38
|
-
Object.defineProperty(exports, "SequelizeTokenStore", { enumerable: true, get: function () { return sequelize_js_2.SequelizeTokenStore; } });
|
|
39
33
|
var service_js_1 = require("./passkey/service.js");
|
|
40
34
|
Object.defineProperty(exports, "PasskeyService", { enumerable: true, get: function () { return service_js_1.PasskeyService; } });
|
|
41
35
|
var base_js_3 = require("./passkey/base.js");
|
|
42
36
|
Object.defineProperty(exports, "PasskeyStore", { enumerable: true, get: function () { return base_js_3.PasskeyStore; } });
|
|
43
37
|
var memory_js_3 = require("./passkey/memory.js");
|
|
44
38
|
Object.defineProperty(exports, "MemoryPasskeyStore", { enumerable: true, get: function () { return memory_js_3.MemoryPasskeyStore; } });
|
|
45
|
-
var sequelize_js_3 = require("./passkey/sequelize.js");
|
|
46
|
-
Object.defineProperty(exports, "SequelizePasskeyStore", { enumerable: true, get: function () { return sequelize_js_3.SequelizePasskeyStore; } });
|
|
47
39
|
var base_js_4 = require("./oauth/base.js");
|
|
48
40
|
Object.defineProperty(exports, "OAuthStore", { enumerable: true, get: function () { return base_js_4.OAuthStore; } });
|
|
49
41
|
var memory_js_4 = require("./oauth/memory.js");
|
|
50
42
|
Object.defineProperty(exports, "MemoryOAuthStore", { enumerable: true, get: function () { return memory_js_4.MemoryOAuthStore; } });
|
|
51
|
-
var sequelize_js_4 = require("./oauth/sequelize.js");
|
|
52
|
-
Object.defineProperty(exports, "SequelizeOAuthStore", { enumerable: true, get: function () { return sequelize_js_4.SequelizeOAuthStore; } });
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -11,22 +11,17 @@ export { nullAuthAdapter, BaseAuthAdapter } from './auth-api/storage.js';
|
|
|
11
11
|
export { nullAuthModule, BaseAuthModule } from './auth-api/module.js';
|
|
12
12
|
export { CompositeAuthAdapter } from './auth-api/compat-auth-storage.js';
|
|
13
13
|
export { MemAuthStore } from './auth-api/mem-auth-store.js';
|
|
14
|
-
export { SqlAuthStore } from './auth-api/sql-auth-store.js';
|
|
15
14
|
export { default as AuthModule } from './auth-api/auth-module.js';
|
|
16
15
|
export type { OAuthStartParams, OAuthStartResult, OAuthCallbackParams, OAuthCallbackResult } from './oauth/types.js';
|
|
17
16
|
export type { BcryptHasherOptions, CreateUserInput, UpdateUserInput, PublicUserMapper } from './user/types.js';
|
|
18
17
|
export { UserStore } from './user/base.js';
|
|
19
18
|
export { MemoryUserStore } from './user/memory.js';
|
|
20
|
-
export { SequelizeUserStore } from './user/sequelize.js';
|
|
21
19
|
export type { MemoryUserAttributes, MemoryUserStoreOptions } from './user/memory.js';
|
|
22
20
|
export { TokenStore } from './token/base.js';
|
|
23
21
|
export { MemoryTokenStore } from './token/memory.js';
|
|
24
|
-
export { SequelizeTokenStore } from './token/sequelize.js';
|
|
25
22
|
export { PasskeyService } from './passkey/service.js';
|
|
26
23
|
export { PasskeyStore } from './passkey/base.js';
|
|
27
24
|
export { MemoryPasskeyStore } from './passkey/memory.js';
|
|
28
|
-
export { SequelizePasskeyStore } from './passkey/sequelize.js';
|
|
29
25
|
export type { PasskeyServiceConfig, PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential, PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
|
|
30
26
|
export { OAuthStore } from './oauth/base.js';
|
|
31
27
|
export { MemoryOAuthStore } from './oauth/memory.js';
|
|
32
|
-
export { SequelizeOAuthStore } from './oauth/sequelize.js';
|
|
@@ -62,9 +62,11 @@ class MemoryPasskeyStore extends base_js_1.PasskeyStore {
|
|
|
62
62
|
}
|
|
63
63
|
async saveChallenge(record) {
|
|
64
64
|
this.challenges.set(record.challenge, {
|
|
65
|
-
|
|
65
|
+
challenge: record.challenge,
|
|
66
|
+
action: record.action,
|
|
66
67
|
userId: record.userId !== undefined ? normalizeUserId(record.userId) : undefined,
|
|
67
|
-
|
|
68
|
+
login: record.login ?? undefined,
|
|
69
|
+
expiresAt: record.expiresAt
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
async consumeChallenge(challenge) {
|
|
@@ -73,7 +75,7 @@ class MemoryPasskeyStore extends base_js_1.PasskeyStore {
|
|
|
73
75
|
return null;
|
|
74
76
|
}
|
|
75
77
|
this.challenges.delete(challenge);
|
|
76
|
-
return { ...record
|
|
78
|
+
return { ...record };
|
|
77
79
|
}
|
|
78
80
|
async cleanupChallenges(now) {
|
|
79
81
|
for (const [challenge, record] of this.challenges.entries()) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
-
import type { PasskeyChallengeMetadata } from './service.js';
|
|
3
2
|
export declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCredentialModel>, InferCreationAttributes<PasskeyCredentialModel>> {
|
|
4
3
|
credentialId: Buffer;
|
|
5
4
|
userId: number;
|
|
@@ -8,6 +7,13 @@ export declare class PasskeyCredentialModel extends Model<InferAttributes<Passke
|
|
|
8
7
|
transports: string[] | null;
|
|
9
8
|
backedUp: boolean;
|
|
10
9
|
deviceType: string;
|
|
10
|
+
label: string | null;
|
|
11
|
+
createdDomain: string | null;
|
|
12
|
+
createdUserAgent: string | null;
|
|
13
|
+
createdBrowser: string | null;
|
|
14
|
+
createdOs: string | null;
|
|
15
|
+
createdDevice: string | null;
|
|
16
|
+
createdIp: string | null;
|
|
11
17
|
createdAt?: Date;
|
|
12
18
|
updatedAt?: Date;
|
|
13
19
|
}
|
|
@@ -16,7 +22,6 @@ export declare class PasskeyChallengeModel extends Model<InferAttributes<Passkey
|
|
|
16
22
|
action: 'register' | 'authenticate';
|
|
17
23
|
userId: number | null;
|
|
18
24
|
login: string | null;
|
|
19
|
-
metadata: PasskeyChallengeMetadata | null;
|
|
20
25
|
expiresAt: Date;
|
|
21
26
|
createdAt?: Date;
|
|
22
27
|
updatedAt?: Date;
|
|
@@ -67,6 +67,40 @@ function initPasskeyCredentialModel(sequelize) {
|
|
|
67
67
|
type: sequelize_1.DataTypes.STRING(32),
|
|
68
68
|
allowNull: false,
|
|
69
69
|
defaultValue: 'multiDevice'
|
|
70
|
+
},
|
|
71
|
+
label: {
|
|
72
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
73
|
+
allowNull: true
|
|
74
|
+
},
|
|
75
|
+
createdDomain: {
|
|
76
|
+
field: 'created_domain',
|
|
77
|
+
type: sequelize_1.DataTypes.STRING(255),
|
|
78
|
+
allowNull: true
|
|
79
|
+
},
|
|
80
|
+
createdUserAgent: {
|
|
81
|
+
field: 'created_user_agent',
|
|
82
|
+
type: sequelize_1.DataTypes.TEXT,
|
|
83
|
+
allowNull: true
|
|
84
|
+
},
|
|
85
|
+
createdBrowser: {
|
|
86
|
+
field: 'created_browser',
|
|
87
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
88
|
+
allowNull: true
|
|
89
|
+
},
|
|
90
|
+
createdOs: {
|
|
91
|
+
field: 'created_os',
|
|
92
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
93
|
+
allowNull: true
|
|
94
|
+
},
|
|
95
|
+
createdDevice: {
|
|
96
|
+
field: 'created_device',
|
|
97
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
98
|
+
allowNull: true
|
|
99
|
+
},
|
|
100
|
+
createdIp: {
|
|
101
|
+
field: 'created_ip',
|
|
102
|
+
type: sequelize_1.DataTypes.STRING(45),
|
|
103
|
+
allowNull: true
|
|
70
104
|
}
|
|
71
105
|
}, {
|
|
72
106
|
sequelize,
|
|
@@ -96,10 +130,6 @@ function initPasskeyChallengeModel(sequelize) {
|
|
|
96
130
|
type: sequelize_1.DataTypes.STRING(128),
|
|
97
131
|
allowNull: true
|
|
98
132
|
},
|
|
99
|
-
metadata: {
|
|
100
|
-
type: sequelize_1.DataTypes.JSON,
|
|
101
|
-
allowNull: true
|
|
102
|
-
},
|
|
103
133
|
expiresAt: {
|
|
104
134
|
field: 'expires_at',
|
|
105
135
|
type: sequelize_1.DataTypes.DATE,
|
|
@@ -10,6 +10,13 @@ declare class PasskeyCredentialModel extends Model<InferAttributes<PasskeyCreden
|
|
|
10
10
|
transports: string[] | null;
|
|
11
11
|
backedUp: boolean;
|
|
12
12
|
deviceType: string;
|
|
13
|
+
label: string | null;
|
|
14
|
+
createdDomain: string | null;
|
|
15
|
+
createdUserAgent: string | null;
|
|
16
|
+
createdBrowser: string | null;
|
|
17
|
+
createdOs: string | null;
|
|
18
|
+
createdDevice: string | null;
|
|
19
|
+
createdIp: string | null;
|
|
13
20
|
createdAt?: Date;
|
|
14
21
|
updatedAt?: Date;
|
|
15
22
|
}
|
|
@@ -18,7 +25,6 @@ declare class PasskeyChallengeModel extends Model<InferAttributes<PasskeyChallen
|
|
|
18
25
|
action: 'register' | 'authenticate';
|
|
19
26
|
userId: number | null;
|
|
20
27
|
login: string | null;
|
|
21
|
-
metadata: Record<string, unknown> | null;
|
|
22
28
|
expiresAt: Date;
|
|
23
29
|
createdAt?: Date;
|
|
24
30
|
updatedAt?: Date;
|
|
@@ -76,6 +76,40 @@ function initPasskeyCredentialModel(sequelize) {
|
|
|
76
76
|
type: sequelize_1.DataTypes.STRING(32),
|
|
77
77
|
allowNull: false,
|
|
78
78
|
defaultValue: 'multiDevice'
|
|
79
|
+
},
|
|
80
|
+
label: {
|
|
81
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
82
|
+
allowNull: true
|
|
83
|
+
},
|
|
84
|
+
createdDomain: {
|
|
85
|
+
field: 'created_domain',
|
|
86
|
+
type: sequelize_1.DataTypes.STRING(255),
|
|
87
|
+
allowNull: true
|
|
88
|
+
},
|
|
89
|
+
createdUserAgent: {
|
|
90
|
+
field: 'created_user_agent',
|
|
91
|
+
type: sequelize_1.DataTypes.TEXT,
|
|
92
|
+
allowNull: true
|
|
93
|
+
},
|
|
94
|
+
createdBrowser: {
|
|
95
|
+
field: 'created_browser',
|
|
96
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
97
|
+
allowNull: true
|
|
98
|
+
},
|
|
99
|
+
createdOs: {
|
|
100
|
+
field: 'created_os',
|
|
101
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
102
|
+
allowNull: true
|
|
103
|
+
},
|
|
104
|
+
createdDevice: {
|
|
105
|
+
field: 'created_device',
|
|
106
|
+
type: sequelize_1.DataTypes.STRING(120),
|
|
107
|
+
allowNull: true
|
|
108
|
+
},
|
|
109
|
+
createdIp: {
|
|
110
|
+
field: 'created_ip',
|
|
111
|
+
type: sequelize_1.DataTypes.STRING(45),
|
|
112
|
+
allowNull: true
|
|
79
113
|
}
|
|
80
114
|
}, {
|
|
81
115
|
sequelize,
|
|
@@ -105,10 +139,6 @@ function initPasskeyChallengeModel(sequelize) {
|
|
|
105
139
|
type: sequelize_1.DataTypes.STRING(128),
|
|
106
140
|
allowNull: true
|
|
107
141
|
},
|
|
108
|
-
metadata: {
|
|
109
|
-
type: sequelize_1.DataTypes.JSON,
|
|
110
|
-
allowNull: true
|
|
111
|
-
},
|
|
112
142
|
expiresAt: {
|
|
113
143
|
field: 'expires_at',
|
|
114
144
|
type: sequelize_1.DataTypes.DATE,
|
|
@@ -148,6 +178,13 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
148
178
|
transports: (model.transports ?? undefined),
|
|
149
179
|
backedUp: model.backedUp,
|
|
150
180
|
deviceType: model.deviceType,
|
|
181
|
+
label: model.label ?? undefined,
|
|
182
|
+
createdDomain: model.createdDomain ?? undefined,
|
|
183
|
+
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
184
|
+
createdBrowser: model.createdBrowser ?? undefined,
|
|
185
|
+
createdOs: model.createdOs ?? undefined,
|
|
186
|
+
createdDevice: model.createdDevice ?? undefined,
|
|
187
|
+
createdIp: model.createdIp ?? undefined,
|
|
151
188
|
createdAt: model.createdAt ?? undefined,
|
|
152
189
|
updatedAt: model.updatedAt ?? undefined
|
|
153
190
|
}));
|
|
@@ -170,6 +207,13 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
170
207
|
transports: (model.transports ?? undefined),
|
|
171
208
|
backedUp: model.backedUp,
|
|
172
209
|
deviceType: model.deviceType,
|
|
210
|
+
label: model.label ?? undefined,
|
|
211
|
+
createdDomain: model.createdDomain ?? undefined,
|
|
212
|
+
createdUserAgent: model.createdUserAgent ?? undefined,
|
|
213
|
+
createdBrowser: model.createdBrowser ?? undefined,
|
|
214
|
+
createdOs: model.createdOs ?? undefined,
|
|
215
|
+
createdDevice: model.createdDevice ?? undefined,
|
|
216
|
+
createdIp: model.createdIp ?? undefined,
|
|
173
217
|
createdAt: model.createdAt ?? undefined,
|
|
174
218
|
updatedAt: model.updatedAt ?? undefined
|
|
175
219
|
};
|
|
@@ -182,7 +226,14 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
182
226
|
counter: record.counter,
|
|
183
227
|
transports: record.transports ?? null,
|
|
184
228
|
backedUp: record.backedUp,
|
|
185
|
-
deviceType: record.deviceType
|
|
229
|
+
deviceType: record.deviceType,
|
|
230
|
+
label: record.label ?? null,
|
|
231
|
+
createdDomain: record.createdDomain ?? null,
|
|
232
|
+
createdUserAgent: record.createdUserAgent ?? null,
|
|
233
|
+
createdBrowser: record.createdBrowser ?? null,
|
|
234
|
+
createdOs: record.createdOs ?? null,
|
|
235
|
+
createdDevice: record.createdDevice ?? null,
|
|
236
|
+
createdIp: record.createdIp ?? null
|
|
186
237
|
});
|
|
187
238
|
}
|
|
188
239
|
async updateCredentialCounter(credentialId, counter) {
|
|
@@ -194,7 +245,6 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
194
245
|
action: record.action,
|
|
195
246
|
userId: record.userId !== undefined ? normalizeUserId(record.userId) : null,
|
|
196
247
|
login: record.login ?? null,
|
|
197
|
-
metadata: (record.metadata ?? {}),
|
|
198
248
|
expiresAt: record.expiresAt
|
|
199
249
|
});
|
|
200
250
|
}
|
|
@@ -209,8 +259,7 @@ class SequelizePasskeyStore extends base_js_1.PasskeyStore {
|
|
|
209
259
|
action: model.action,
|
|
210
260
|
userId: model.userId ?? undefined,
|
|
211
261
|
login: model.login ?? undefined,
|
|
212
|
-
expiresAt: model.expiresAt
|
|
213
|
-
metadata: model.metadata ?? {}
|
|
262
|
+
expiresAt: model.expiresAt
|
|
214
263
|
};
|
|
215
264
|
}
|
|
216
265
|
async cleanupChallenges(now) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyStorageAdapter, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, StoredPasskeyCredential } from './types.js';
|
|
2
2
|
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
-
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord,
|
|
3
|
+
export type { CredentialDeviceType, PasskeyChallenge, PasskeyChallengeParams, PasskeyChallengeRecord, PasskeyUserDescriptor, PasskeyVerificationParams, PasskeyVerificationResult, PasskeyServiceConfig, PasskeyStorageAdapter, StoredPasskeyCredential } from './types.js';
|
|
4
4
|
type Logger = Pick<typeof console, 'error' | 'warn'>;
|
|
5
5
|
export declare class PasskeyService {
|
|
6
6
|
private readonly config;
|
|
@@ -54,6 +54,13 @@ function sanitizeTransports(input) {
|
|
|
54
54
|
.filter((value) => ALLOWED_TRANSPORTS.includes(value));
|
|
55
55
|
return filtered.length > 0 ? filtered : undefined;
|
|
56
56
|
}
|
|
57
|
+
function toOptionalString(value) {
|
|
58
|
+
if (typeof value !== 'string') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
63
|
+
}
|
|
57
64
|
function toBase64Url(buffer) {
|
|
58
65
|
return helpers_1.isoBase64URL.fromBuffer(new Uint8Array(buffer));
|
|
59
66
|
}
|
|
@@ -127,17 +134,11 @@ class PasskeyService {
|
|
|
127
134
|
}
|
|
128
135
|
async createChallenge(params) {
|
|
129
136
|
await this.adapter.cleanupChallenges?.(new Date());
|
|
130
|
-
const metadata = {
|
|
131
|
-
domain: typeof params.domain === 'string' ? params.domain : undefined,
|
|
132
|
-
fingerprint: typeof params.fingerprint === 'string' ? params.fingerprint : undefined,
|
|
133
|
-
label: typeof params.label === 'string' ? params.label : undefined,
|
|
134
|
-
userAgent: typeof params.userAgent === 'string' ? params.userAgent : undefined
|
|
135
|
-
};
|
|
136
137
|
if (params.action === 'register') {
|
|
137
|
-
return this.createRegistrationChallenge(params
|
|
138
|
+
return this.createRegistrationChallenge(params);
|
|
138
139
|
}
|
|
139
140
|
if (params.action === 'authenticate') {
|
|
140
|
-
return this.createAuthenticationChallenge(params
|
|
141
|
+
return this.createAuthenticationChallenge(params);
|
|
141
142
|
}
|
|
142
143
|
throw new Error(`Unsupported passkey action: ${String(params.action)}`);
|
|
143
144
|
}
|
|
@@ -163,7 +164,7 @@ class PasskeyService {
|
|
|
163
164
|
}
|
|
164
165
|
return { verified: false };
|
|
165
166
|
}
|
|
166
|
-
async createRegistrationChallenge(params
|
|
167
|
+
async createRegistrationChallenge(params) {
|
|
167
168
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
168
169
|
const existing = await this.adapter.listUserCredentials(user.id);
|
|
169
170
|
const excludeCredentials = existing.map((credential) => {
|
|
@@ -186,8 +187,7 @@ class PasskeyService {
|
|
|
186
187
|
action: 'register',
|
|
187
188
|
userId: user.id,
|
|
188
189
|
login: user.login,
|
|
189
|
-
expiresAt
|
|
190
|
-
metadata
|
|
190
|
+
expiresAt
|
|
191
191
|
});
|
|
192
192
|
return {
|
|
193
193
|
challenge: options.challenge,
|
|
@@ -196,7 +196,7 @@ class PasskeyService {
|
|
|
196
196
|
publicKey: options
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
-
async createAuthenticationChallenge(params
|
|
199
|
+
async createAuthenticationChallenge(params) {
|
|
200
200
|
const user = await this.requireUser({ userId: params.userId, login: params.login });
|
|
201
201
|
const credentials = await this.adapter.listUserCredentials(user.id);
|
|
202
202
|
const allowCredentials = credentials.map((credential) => {
|
|
@@ -216,8 +216,7 @@ class PasskeyService {
|
|
|
216
216
|
action: 'authenticate',
|
|
217
217
|
userId: user.id,
|
|
218
218
|
login: user.login,
|
|
219
|
-
expiresAt
|
|
220
|
-
metadata
|
|
219
|
+
expiresAt
|
|
221
220
|
});
|
|
222
221
|
return {
|
|
223
222
|
challenge: options.challenge,
|
|
@@ -295,7 +294,14 @@ class PasskeyService {
|
|
|
295
294
|
counter: registrationInfo.counter ?? 0,
|
|
296
295
|
transports: sanitizeTransports(params.response.transports),
|
|
297
296
|
backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
|
|
298
|
-
deviceType: registrationInfo.credentialDeviceType
|
|
297
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
298
|
+
label: toOptionalString(params.label),
|
|
299
|
+
createdDomain: toOptionalString(params.domain),
|
|
300
|
+
createdUserAgent: toOptionalString(params.userAgent),
|
|
301
|
+
createdBrowser: toOptionalString(params.browser),
|
|
302
|
+
createdOs: toOptionalString(params.os),
|
|
303
|
+
createdDevice: toOptionalString(params.device),
|
|
304
|
+
createdIp: toOptionalString(params.ip)
|
|
299
305
|
});
|
|
300
306
|
return { verified: true, userId: user.id, login: user.login };
|
|
301
307
|
}
|
|
@@ -9,19 +9,12 @@ export interface PasskeyServiceConfig {
|
|
|
9
9
|
timeoutMs: number;
|
|
10
10
|
userVerification?: 'preferred' | 'required' | 'discouraged';
|
|
11
11
|
}
|
|
12
|
-
export interface PasskeyChallengeMetadata {
|
|
13
|
-
domain?: string;
|
|
14
|
-
fingerprint?: string;
|
|
15
|
-
label?: string;
|
|
16
|
-
userAgent?: string;
|
|
17
|
-
}
|
|
18
12
|
export interface PasskeyChallengeRecord {
|
|
19
13
|
challenge: string;
|
|
20
14
|
action: 'register' | 'authenticate';
|
|
21
15
|
userId?: AuthIdentifier;
|
|
22
16
|
login?: string;
|
|
23
17
|
expiresAt: Date;
|
|
24
|
-
metadata: PasskeyChallengeMetadata;
|
|
25
18
|
}
|
|
26
19
|
export interface PasskeyUserDescriptor {
|
|
27
20
|
id: AuthIdentifier;
|
|
@@ -36,6 +29,13 @@ export interface StoredPasskeyCredential {
|
|
|
36
29
|
transports?: AuthenticatorTransportFuture[];
|
|
37
30
|
backedUp: boolean;
|
|
38
31
|
deviceType: CredentialDeviceType;
|
|
32
|
+
label?: string;
|
|
33
|
+
createdDomain?: string;
|
|
34
|
+
createdUserAgent?: string;
|
|
35
|
+
createdBrowser?: string;
|
|
36
|
+
createdOs?: string;
|
|
37
|
+
createdDevice?: string;
|
|
38
|
+
createdIp?: string;
|
|
39
39
|
createdAt?: Date;
|
|
40
40
|
updatedAt?: Date;
|
|
41
41
|
}
|
|
@@ -53,10 +53,9 @@ export interface PasskeyStorageAdapter {
|
|
|
53
53
|
consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
|
|
54
54
|
cleanupChallenges?(now: Date): Promise<void>;
|
|
55
55
|
}
|
|
56
|
-
export interface PasskeyChallengeParams
|
|
56
|
+
export interface PasskeyChallengeParams {
|
|
57
57
|
action: 'register' | 'authenticate';
|
|
58
58
|
login?: string;
|
|
59
|
-
userAgent?: string;
|
|
60
59
|
userId?: AuthIdentifier;
|
|
61
60
|
}
|
|
62
61
|
export interface PasskeyChallenge extends Record<string, unknown> {
|
|
@@ -69,6 +68,7 @@ export interface PasskeyVerificationParams extends Partial<Omit<Token, 'userId'>
|
|
|
69
68
|
login?: string;
|
|
70
69
|
response: Record<string, unknown>;
|
|
71
70
|
userId?: AuthIdentifier;
|
|
71
|
+
userAgent?: string;
|
|
72
72
|
}
|
|
73
73
|
export interface PasskeyVerificationResult extends Record<string, unknown> {
|
|
74
74
|
login?: string;
|
|
@@ -95,6 +95,8 @@ export interface ApiServerConf {
|
|
|
95
95
|
origins: string[];
|
|
96
96
|
debug: boolean;
|
|
97
97
|
apiBasePath: string;
|
|
98
|
+
swaggerEnabled?: boolean;
|
|
99
|
+
swaggerPath?: string;
|
|
98
100
|
accessSecret: string;
|
|
99
101
|
refreshSecret: string;
|
|
100
102
|
cookieDomain: string;
|
|
@@ -190,6 +192,8 @@ export declare class ApiServer {
|
|
|
190
192
|
protected authorize(apiReq: ApiRequest, requiredClass: ApiAuthClass): Promise<void>;
|
|
191
193
|
private middlewares;
|
|
192
194
|
private installPingHandler;
|
|
195
|
+
private loadSwaggerSpec;
|
|
196
|
+
private installSwaggerHandler;
|
|
193
197
|
private normalizeApiBasePath;
|
|
194
198
|
private installApiNotFoundHandler;
|
|
195
199
|
private ensureApiNotFoundOrdering;
|