@technomoron/api-server-base 1.1.13 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/api-server-base.cjs +181 -74
- package/dist/cjs/api-server-base.d.ts +66 -29
- package/dist/cjs/auth-api/auth-module.d.ts +96 -0
- package/dist/cjs/auth-api/auth-module.js +1032 -0
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +55 -0
- package/dist/cjs/auth-api/compat-auth-storage.js +116 -0
- package/dist/cjs/auth-api/mem-auth-store.d.ts +66 -0
- package/dist/cjs/auth-api/mem-auth-store.js +135 -0
- package/dist/cjs/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
- package/dist/cjs/{auth-module.cjs → auth-api/module.js} +1 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +75 -0
- package/dist/cjs/auth-api/sql-auth-store.js +166 -0
- package/dist/cjs/auth-api/storage.d.ts +36 -0
- package/dist/cjs/{auth-storage.cjs → auth-api/storage.js} +2 -2
- package/dist/cjs/auth-api/types.d.ts +29 -0
- package/dist/cjs/auth-api/types.js +2 -0
- package/dist/cjs/index.cjs +41 -7
- package/dist/cjs/index.d.ts +29 -5
- package/dist/cjs/oauth/base.d.ts +10 -0
- package/dist/cjs/oauth/base.js +6 -0
- package/dist/cjs/oauth/memory.d.ts +16 -0
- package/dist/cjs/oauth/memory.js +99 -0
- package/dist/cjs/oauth/models.d.ts +45 -0
- package/dist/cjs/oauth/models.js +58 -0
- package/dist/cjs/oauth/sequelize.d.ts +68 -0
- package/dist/cjs/oauth/sequelize.js +210 -0
- package/dist/cjs/oauth/types.d.ts +50 -0
- package/dist/cjs/oauth/types.js +3 -0
- package/dist/cjs/passkey/base.d.ts +15 -0
- package/dist/cjs/passkey/base.js +6 -0
- package/dist/cjs/passkey/memory.d.ts +26 -0
- package/dist/cjs/passkey/memory.js +82 -0
- package/dist/cjs/passkey/models.d.ts +25 -0
- package/dist/cjs/passkey/models.js +115 -0
- package/dist/cjs/passkey/sequelize.d.ts +54 -0
- package/dist/cjs/passkey/sequelize.js +211 -0
- package/dist/cjs/passkey/service.d.ts +17 -0
- package/dist/cjs/passkey/service.js +221 -0
- package/dist/cjs/passkey/types.d.ts +75 -0
- package/dist/cjs/passkey/types.js +2 -0
- package/dist/cjs/token/base.d.ts +38 -0
- package/dist/cjs/token/base.js +114 -0
- package/dist/cjs/token/memory.d.ts +19 -0
- package/dist/cjs/token/memory.js +149 -0
- package/dist/cjs/token/sequelize.d.ts +58 -0
- package/dist/cjs/token/sequelize.js +404 -0
- package/dist/cjs/token/types.d.ts +27 -0
- package/dist/cjs/token/types.js +2 -0
- package/dist/cjs/user/base.d.ts +26 -0
- package/dist/cjs/user/base.js +45 -0
- package/dist/cjs/user/memory.d.ts +35 -0
- package/dist/cjs/user/memory.js +173 -0
- package/dist/cjs/user/sequelize.d.ts +41 -0
- package/dist/cjs/user/sequelize.js +182 -0
- package/dist/cjs/user/types.d.ts +11 -0
- package/dist/cjs/user/types.js +2 -0
- package/dist/esm/api-server-base.d.ts +66 -29
- package/dist/esm/api-server-base.js +179 -72
- package/dist/esm/auth-api/auth-module.d.ts +96 -0
- package/dist/esm/auth-api/auth-module.js +1030 -0
- package/dist/esm/auth-api/compat-auth-storage.d.ts +55 -0
- package/dist/esm/auth-api/compat-auth-storage.js +112 -0
- package/dist/esm/auth-api/mem-auth-store.d.ts +66 -0
- package/dist/esm/auth-api/mem-auth-store.js +131 -0
- package/dist/esm/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
- package/dist/esm/{auth-module.js → auth-api/module.js} +1 -1
- package/dist/esm/auth-api/sql-auth-store.d.ts +75 -0
- package/dist/esm/auth-api/sql-auth-store.js +162 -0
- package/dist/esm/auth-api/storage.d.ts +36 -0
- package/dist/esm/{auth-storage.js → auth-api/storage.js} +2 -2
- package/dist/esm/auth-api/types.d.ts +29 -0
- package/dist/esm/auth-api/types.js +1 -0
- package/dist/esm/index.d.ts +29 -5
- package/dist/esm/index.js +19 -2
- package/dist/esm/oauth/base.d.ts +10 -0
- package/dist/esm/oauth/base.js +2 -0
- package/dist/esm/oauth/memory.d.ts +16 -0
- package/dist/esm/oauth/memory.js +92 -0
- package/dist/esm/oauth/models.d.ts +45 -0
- package/dist/esm/oauth/models.js +51 -0
- package/dist/esm/oauth/sequelize.d.ts +68 -0
- package/dist/esm/oauth/sequelize.js +199 -0
- package/dist/esm/oauth/types.d.ts +50 -0
- package/dist/esm/oauth/types.js +2 -0
- package/dist/esm/passkey/base.d.ts +15 -0
- package/dist/esm/passkey/base.js +2 -0
- package/dist/esm/passkey/memory.d.ts +26 -0
- package/dist/esm/passkey/memory.js +78 -0
- package/dist/esm/passkey/models.d.ts +25 -0
- package/dist/esm/passkey/models.js +108 -0
- package/dist/esm/passkey/sequelize.d.ts +54 -0
- package/dist/esm/passkey/sequelize.js +207 -0
- package/dist/esm/passkey/service.d.ts +17 -0
- package/dist/esm/passkey/service.js +217 -0
- package/dist/esm/passkey/types.d.ts +75 -0
- package/dist/esm/passkey/types.js +1 -0
- package/dist/esm/token/base.d.ts +38 -0
- package/dist/esm/token/base.js +107 -0
- package/dist/esm/token/memory.d.ts +19 -0
- package/dist/esm/token/memory.js +145 -0
- package/dist/esm/token/sequelize.d.ts +58 -0
- package/dist/esm/token/sequelize.js +400 -0
- package/dist/esm/token/types.d.ts +27 -0
- package/dist/esm/token/types.js +1 -0
- package/dist/esm/user/base.d.ts +26 -0
- package/dist/esm/user/base.js +38 -0
- package/dist/esm/user/memory.d.ts +35 -0
- package/dist/esm/user/memory.js +169 -0
- package/dist/esm/user/sequelize.d.ts +41 -0
- package/dist/esm/user/sequelize.js +176 -0
- package/dist/esm/user/types.d.ts +11 -0
- package/dist/esm/user/types.js +1 -0
- package/package.json +11 -3
- package/dist/cjs/auth-storage.d.ts +0 -133
- package/dist/esm/auth-storage.d.ts +0 -133
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TokenStore } from './base.js';
|
|
2
|
+
import type { Token } from './types.js';
|
|
3
|
+
export declare class MemoryTokenStore extends TokenStore {
|
|
4
|
+
private readonly tokens;
|
|
5
|
+
save(record: Token): Promise<void>;
|
|
6
|
+
get(query: Partial<Token>, opts?: {
|
|
7
|
+
includeExpired?: boolean;
|
|
8
|
+
}): Promise<Token | null>;
|
|
9
|
+
delete(query: Partial<Token>): Promise<number>;
|
|
10
|
+
update(params: Partial<Token> & {
|
|
11
|
+
refreshToken: string;
|
|
12
|
+
}): Promise<boolean>;
|
|
13
|
+
list(userId: string | number, opts?: {
|
|
14
|
+
limit?: number;
|
|
15
|
+
offset?: number;
|
|
16
|
+
includeExpired?: boolean;
|
|
17
|
+
}): Promise<Token[]>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryTokenStore = void 0;
|
|
4
|
+
const base_js_1 = require("./base.js");
|
|
5
|
+
function comparableUserId(value) {
|
|
6
|
+
if (value === undefined || value === null) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
return String(value);
|
|
10
|
+
}
|
|
11
|
+
function cloneToken(record) {
|
|
12
|
+
// this.normalizeToken is not available in static context; caller passes through instance.
|
|
13
|
+
// cloning handled via store instance methods.
|
|
14
|
+
const normalized = record;
|
|
15
|
+
return {
|
|
16
|
+
...normalized,
|
|
17
|
+
scope: normalized.scope ? [...normalized.scope] : undefined
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function matchesQuery(record, query, includeExpired) {
|
|
21
|
+
if (query.refreshToken && record.refreshToken !== query.refreshToken) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (query.accessToken && record.accessToken !== query.accessToken) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (query.userId !== undefined && comparableUserId(record.userId) !== comparableUserId(query.userId)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (query.clientId && record.clientId !== query.clientId) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (query.domain !== undefined && (record.domain ?? '') !== (query.domain ?? '')) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (query.fingerprint !== undefined && (record.fingerprint ?? '') !== (query.fingerprint ?? '')) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (query.loginType !== undefined && record.loginType !== (query.loginType ?? undefined)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (query.label && record.label !== query.label) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (!includeExpired && (record.expires ?? new Date(0)).getTime() < Date.now()) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
class MemoryTokenStore extends base_js_1.TokenStore {
|
|
51
|
+
constructor() {
|
|
52
|
+
super(...arguments);
|
|
53
|
+
this.tokens = [];
|
|
54
|
+
}
|
|
55
|
+
async save(record) {
|
|
56
|
+
const stored = this.normalizeToken(record);
|
|
57
|
+
const normalizedUserId = comparableUserId(stored.userId);
|
|
58
|
+
const domainProvided = record.domain !== undefined;
|
|
59
|
+
const fingerprintProvided = record.fingerprint !== undefined;
|
|
60
|
+
for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
|
|
61
|
+
const existing = this.tokens[index];
|
|
62
|
+
if (comparableUserId(existing.userId) !== normalizedUserId) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (record.clientId && existing.clientId !== record.clientId) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (domainProvided && existing.domain !== stored.domain) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (fingerprintProvided && existing.fingerprint !== stored.fingerprint) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
this.tokens.splice(index, 1);
|
|
75
|
+
}
|
|
76
|
+
this.tokens.push(stored);
|
|
77
|
+
}
|
|
78
|
+
async get(query, opts) {
|
|
79
|
+
if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
|
|
80
|
+
throw new Error('At least one token lookup field must be provided');
|
|
81
|
+
}
|
|
82
|
+
const includeExpired = opts?.includeExpired ?? false;
|
|
83
|
+
const record = this.tokens.find((token) => matchesQuery(token, query, includeExpired));
|
|
84
|
+
return record ? cloneToken(record) : null;
|
|
85
|
+
}
|
|
86
|
+
async delete(query) {
|
|
87
|
+
if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
let removed = 0;
|
|
91
|
+
for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
|
|
92
|
+
if (matchesQuery(this.tokens[index], query, true)) {
|
|
93
|
+
this.tokens.splice(index, 1);
|
|
94
|
+
removed += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return removed;
|
|
98
|
+
}
|
|
99
|
+
async update(params) {
|
|
100
|
+
const token = this.tokens.find((record) => {
|
|
101
|
+
if (record.refreshToken !== params.refreshToken) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (params.clientId && record.clientId !== params.clientId) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
if (!token) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const merged = { ...token };
|
|
113
|
+
const maybeAssign = (key) => {
|
|
114
|
+
const value = params[key];
|
|
115
|
+
if (value !== undefined) {
|
|
116
|
+
merged[key] = value;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
maybeAssign('accessToken');
|
|
120
|
+
maybeAssign('expires');
|
|
121
|
+
maybeAssign('scope');
|
|
122
|
+
maybeAssign('label');
|
|
123
|
+
maybeAssign('domain');
|
|
124
|
+
maybeAssign('fingerprint');
|
|
125
|
+
maybeAssign('browser');
|
|
126
|
+
maybeAssign('device');
|
|
127
|
+
maybeAssign('ip');
|
|
128
|
+
maybeAssign('os');
|
|
129
|
+
maybeAssign('refreshTtlSeconds');
|
|
130
|
+
maybeAssign('loginType');
|
|
131
|
+
maybeAssign('issuedAt');
|
|
132
|
+
maybeAssign('lastSeenAt');
|
|
133
|
+
maybeAssign('sessionCookie');
|
|
134
|
+
const normalized = this.normalizeToken(merged);
|
|
135
|
+
Object.assign(token, normalized);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
async list(userId, opts = {}) {
|
|
139
|
+
const includeExpired = opts.includeExpired ?? false;
|
|
140
|
+
const filtered = this.tokens.filter((token) => matchesQuery(token, { userId: comparableUserId(userId) }, includeExpired));
|
|
141
|
+
const offset = opts.offset ?? 0;
|
|
142
|
+
const limit = opts.limit ?? filtered.length;
|
|
143
|
+
return filtered.slice(offset, offset + limit).map(cloneToken);
|
|
144
|
+
}
|
|
145
|
+
async close() {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.MemoryTokenStore = MemoryTokenStore;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CreationOptional, Model, type InferAttributes, type InferCreationAttributes, type ModelStatic, type Sequelize } from 'sequelize';
|
|
2
|
+
import { TokenStore } from './base.js';
|
|
3
|
+
import type { Token } from './types.js';
|
|
4
|
+
declare class TokenModel extends Model<InferAttributes<TokenModel>, InferCreationAttributes<TokenModel>> implements InferAttributes<TokenModel> {
|
|
5
|
+
token_id: CreationOptional<number>;
|
|
6
|
+
user_id: string;
|
|
7
|
+
real_user_id: CreationOptional<string | null>;
|
|
8
|
+
expires: Date;
|
|
9
|
+
issued_at: CreationOptional<Date>;
|
|
10
|
+
last_seen_at: CreationOptional<Date>;
|
|
11
|
+
access: string;
|
|
12
|
+
refresh: string;
|
|
13
|
+
domain: CreationOptional<string>;
|
|
14
|
+
fingerprint: CreationOptional<string>;
|
|
15
|
+
label: CreationOptional<string>;
|
|
16
|
+
browser: CreationOptional<string>;
|
|
17
|
+
device: CreationOptional<string>;
|
|
18
|
+
ip: CreationOptional<string>;
|
|
19
|
+
os: CreationOptional<string>;
|
|
20
|
+
client_id: CreationOptional<string | null>;
|
|
21
|
+
scope: CreationOptional<string>;
|
|
22
|
+
login_type: CreationOptional<string>;
|
|
23
|
+
refresh_ttl_seconds: CreationOptional<number | null>;
|
|
24
|
+
session_cookie: CreationOptional<boolean>;
|
|
25
|
+
}
|
|
26
|
+
export type TokenAttributes = InferAttributes<TokenModel>;
|
|
27
|
+
export type TokenCreationAttributes = InferCreationAttributes<TokenModel>;
|
|
28
|
+
export interface SequelizeTokenStoreOptions {
|
|
29
|
+
sequelize: Sequelize;
|
|
30
|
+
tokenModel?: ModelStatic<TokenModel>;
|
|
31
|
+
tokenModelFactory?: (sequelize: Sequelize) => ModelStatic<TokenModel>;
|
|
32
|
+
}
|
|
33
|
+
export declare class SequelizeTokenStore extends TokenStore {
|
|
34
|
+
readonly Tokens: ModelStatic<TokenModel>;
|
|
35
|
+
constructor(options: SequelizeTokenStoreOptions);
|
|
36
|
+
save(record: Token): Promise<void>;
|
|
37
|
+
get(query: Partial<Token>, opts?: {
|
|
38
|
+
includeExpired?: boolean;
|
|
39
|
+
}): Promise<Token | null>;
|
|
40
|
+
delete(query: Partial<Token>): Promise<number>;
|
|
41
|
+
update(params: Partial<Token> & {
|
|
42
|
+
refreshToken: string;
|
|
43
|
+
}): Promise<boolean>;
|
|
44
|
+
list(userId: string | number, opts?: {
|
|
45
|
+
limit?: number;
|
|
46
|
+
offset?: number;
|
|
47
|
+
includeExpired?: boolean;
|
|
48
|
+
}): Promise<Token[]>;
|
|
49
|
+
close(): Promise<void>;
|
|
50
|
+
private normalizeUserId;
|
|
51
|
+
private resolveRealUserId;
|
|
52
|
+
private encodeStringArray;
|
|
53
|
+
private decodeStringArray;
|
|
54
|
+
private encodeScope;
|
|
55
|
+
private decodeScope;
|
|
56
|
+
private toTokenRecord;
|
|
57
|
+
}
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SequelizeTokenStore = void 0;
|
|
4
|
+
const sequelize_1 = require("sequelize");
|
|
5
|
+
const base_js_1 = require("./base.js");
|
|
6
|
+
const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
|
|
7
|
+
class TokenModel extends sequelize_1.Model {
|
|
8
|
+
}
|
|
9
|
+
function tokenTableOptions(sequelize) {
|
|
10
|
+
const opts = {
|
|
11
|
+
sequelize,
|
|
12
|
+
tableName: 'jwttokens',
|
|
13
|
+
timestamps: false
|
|
14
|
+
};
|
|
15
|
+
if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
|
|
16
|
+
opts.charset = 'utf8mb4';
|
|
17
|
+
opts.collate = 'utf8mb4_unicode_ci';
|
|
18
|
+
}
|
|
19
|
+
return opts;
|
|
20
|
+
}
|
|
21
|
+
function initTokenModel(sequelize) {
|
|
22
|
+
TokenModel.init({
|
|
23
|
+
token_id: {
|
|
24
|
+
type: DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())
|
|
25
|
+
? sequelize_1.DataTypes.INTEGER.UNSIGNED
|
|
26
|
+
: sequelize_1.DataTypes.INTEGER,
|
|
27
|
+
autoIncrement: true,
|
|
28
|
+
allowNull: false,
|
|
29
|
+
primaryKey: true
|
|
30
|
+
},
|
|
31
|
+
user_id: {
|
|
32
|
+
type: sequelize_1.DataTypes.STRING(191),
|
|
33
|
+
allowNull: false
|
|
34
|
+
},
|
|
35
|
+
real_user_id: {
|
|
36
|
+
type: sequelize_1.DataTypes.STRING(191),
|
|
37
|
+
allowNull: true,
|
|
38
|
+
defaultValue: null
|
|
39
|
+
},
|
|
40
|
+
expires: {
|
|
41
|
+
type: sequelize_1.DataTypes.DATE,
|
|
42
|
+
allowNull: false
|
|
43
|
+
},
|
|
44
|
+
issued_at: {
|
|
45
|
+
type: sequelize_1.DataTypes.DATE,
|
|
46
|
+
allowNull: false,
|
|
47
|
+
defaultValue: sequelize_1.DataTypes.NOW
|
|
48
|
+
},
|
|
49
|
+
last_seen_at: {
|
|
50
|
+
type: sequelize_1.DataTypes.DATE,
|
|
51
|
+
allowNull: false,
|
|
52
|
+
defaultValue: sequelize_1.DataTypes.NOW
|
|
53
|
+
},
|
|
54
|
+
access: {
|
|
55
|
+
type: sequelize_1.DataTypes.STRING(512),
|
|
56
|
+
allowNull: false
|
|
57
|
+
},
|
|
58
|
+
refresh: {
|
|
59
|
+
type: sequelize_1.DataTypes.STRING(512),
|
|
60
|
+
allowNull: false
|
|
61
|
+
},
|
|
62
|
+
domain: {
|
|
63
|
+
type: sequelize_1.DataTypes.STRING(64),
|
|
64
|
+
allowNull: false,
|
|
65
|
+
defaultValue: ''
|
|
66
|
+
},
|
|
67
|
+
fingerprint: {
|
|
68
|
+
type: sequelize_1.DataTypes.STRING(64),
|
|
69
|
+
allowNull: false,
|
|
70
|
+
defaultValue: ''
|
|
71
|
+
},
|
|
72
|
+
label: {
|
|
73
|
+
type: sequelize_1.DataTypes.STRING(128),
|
|
74
|
+
allowNull: false,
|
|
75
|
+
defaultValue: ''
|
|
76
|
+
},
|
|
77
|
+
browser: {
|
|
78
|
+
type: sequelize_1.DataTypes.STRING(64),
|
|
79
|
+
allowNull: false,
|
|
80
|
+
defaultValue: ''
|
|
81
|
+
},
|
|
82
|
+
device: {
|
|
83
|
+
type: sequelize_1.DataTypes.STRING(64),
|
|
84
|
+
allowNull: false,
|
|
85
|
+
defaultValue: ''
|
|
86
|
+
},
|
|
87
|
+
ip: {
|
|
88
|
+
type: sequelize_1.DataTypes.STRING(45),
|
|
89
|
+
allowNull: false,
|
|
90
|
+
defaultValue: ''
|
|
91
|
+
},
|
|
92
|
+
os: {
|
|
93
|
+
type: sequelize_1.DataTypes.STRING(64),
|
|
94
|
+
allowNull: false,
|
|
95
|
+
defaultValue: ''
|
|
96
|
+
},
|
|
97
|
+
login_type: {
|
|
98
|
+
type: sequelize_1.DataTypes.STRING(64),
|
|
99
|
+
allowNull: false,
|
|
100
|
+
defaultValue: ''
|
|
101
|
+
},
|
|
102
|
+
refresh_ttl_seconds: {
|
|
103
|
+
type: sequelize_1.DataTypes.INTEGER,
|
|
104
|
+
allowNull: true,
|
|
105
|
+
defaultValue: null
|
|
106
|
+
},
|
|
107
|
+
session_cookie: {
|
|
108
|
+
type: sequelize_1.DataTypes.BOOLEAN,
|
|
109
|
+
allowNull: false,
|
|
110
|
+
defaultValue: false
|
|
111
|
+
},
|
|
112
|
+
client_id: {
|
|
113
|
+
type: sequelize_1.DataTypes.STRING(128),
|
|
114
|
+
allowNull: true,
|
|
115
|
+
defaultValue: null
|
|
116
|
+
},
|
|
117
|
+
scope: {
|
|
118
|
+
type: sequelize_1.DataTypes.TEXT,
|
|
119
|
+
allowNull: false,
|
|
120
|
+
defaultValue: '[]'
|
|
121
|
+
}
|
|
122
|
+
}, {
|
|
123
|
+
...tokenTableOptions(sequelize),
|
|
124
|
+
indexes: [
|
|
125
|
+
{ name: 'jwt_access_unique', unique: true, fields: ['access'] },
|
|
126
|
+
{ name: 'jwt_refresh_unique', unique: true, fields: ['refresh'] }
|
|
127
|
+
]
|
|
128
|
+
});
|
|
129
|
+
return TokenModel;
|
|
130
|
+
}
|
|
131
|
+
class SequelizeTokenStore extends base_js_1.TokenStore {
|
|
132
|
+
constructor(options) {
|
|
133
|
+
super();
|
|
134
|
+
if (!options?.sequelize) {
|
|
135
|
+
throw new Error('SequelizeTokenStore requires an initialised Sequelize instance');
|
|
136
|
+
}
|
|
137
|
+
this.Tokens = options.tokenModel ?? (options.tokenModelFactory ?? initTokenModel)(options.sequelize);
|
|
138
|
+
}
|
|
139
|
+
async save(record) {
|
|
140
|
+
const normalized = this.normalizeToken(record);
|
|
141
|
+
const resolvedUserId = this.normalizeUserId(normalized.userId);
|
|
142
|
+
const resolvedRealUserId = this.resolveRealUserId(normalized.ruid);
|
|
143
|
+
const domain = normalized.domain;
|
|
144
|
+
const fingerprint = normalized.fingerprint;
|
|
145
|
+
const browser = normalized.browser;
|
|
146
|
+
const device = normalized.device;
|
|
147
|
+
const ip = normalized.ip;
|
|
148
|
+
const os = normalized.os;
|
|
149
|
+
const label = normalized.label;
|
|
150
|
+
const loginType = normalized.loginType ?? '';
|
|
151
|
+
const refreshTtlSeconds = typeof normalized.refreshTtlSeconds === 'number' && normalized.refreshTtlSeconds > 0
|
|
152
|
+
? Math.floor(normalized.refreshTtlSeconds)
|
|
153
|
+
: null;
|
|
154
|
+
const issuedAt = normalized.issuedAt ?? new Date();
|
|
155
|
+
const lastSeenAt = normalized.lastSeenAt ?? issuedAt;
|
|
156
|
+
const sessionCookie = normalized.sessionCookie ?? false;
|
|
157
|
+
const removalWhere = { user_id: resolvedUserId };
|
|
158
|
+
if (normalized.domain !== undefined && record.domain !== undefined) {
|
|
159
|
+
removalWhere.domain = domain;
|
|
160
|
+
}
|
|
161
|
+
if (normalized.fingerprint !== undefined && record.fingerprint !== undefined) {
|
|
162
|
+
removalWhere.fingerprint = fingerprint;
|
|
163
|
+
}
|
|
164
|
+
if (normalized.clientId) {
|
|
165
|
+
removalWhere.client_id = normalized.clientId;
|
|
166
|
+
}
|
|
167
|
+
await this.Tokens.destroy({ where: removalWhere });
|
|
168
|
+
await this.Tokens.create({
|
|
169
|
+
user_id: resolvedUserId,
|
|
170
|
+
real_user_id: resolvedRealUserId,
|
|
171
|
+
access: normalized.accessToken ?? '',
|
|
172
|
+
refresh: normalized.refreshToken,
|
|
173
|
+
expires: normalized.expires,
|
|
174
|
+
issued_at: issuedAt,
|
|
175
|
+
last_seen_at: lastSeenAt,
|
|
176
|
+
domain,
|
|
177
|
+
fingerprint,
|
|
178
|
+
label,
|
|
179
|
+
browser,
|
|
180
|
+
device,
|
|
181
|
+
ip,
|
|
182
|
+
os,
|
|
183
|
+
client_id: normalized.clientId ?? null,
|
|
184
|
+
scope: this.encodeScope(normalized.scope),
|
|
185
|
+
login_type: loginType,
|
|
186
|
+
refresh_ttl_seconds: refreshTtlSeconds,
|
|
187
|
+
session_cookie: sessionCookie
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async get(query, opts) {
|
|
191
|
+
if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
|
|
192
|
+
throw new Error('At least one token lookup field must be provided');
|
|
193
|
+
}
|
|
194
|
+
const where = {};
|
|
195
|
+
if (query.refreshToken) {
|
|
196
|
+
where.refresh = query.refreshToken;
|
|
197
|
+
}
|
|
198
|
+
if (query.accessToken) {
|
|
199
|
+
where.access = query.accessToken;
|
|
200
|
+
}
|
|
201
|
+
if (query.userId !== undefined) {
|
|
202
|
+
where.user_id = this.normalizeUserId(query.userId);
|
|
203
|
+
}
|
|
204
|
+
if (query.clientId) {
|
|
205
|
+
where.client_id = query.clientId;
|
|
206
|
+
}
|
|
207
|
+
if (query.domain !== undefined && query.domain !== null) {
|
|
208
|
+
where.domain = query.domain;
|
|
209
|
+
}
|
|
210
|
+
if (query.fingerprint !== undefined && query.fingerprint !== null) {
|
|
211
|
+
where.fingerprint = query.fingerprint;
|
|
212
|
+
}
|
|
213
|
+
if (query.label) {
|
|
214
|
+
where.label = query.label;
|
|
215
|
+
}
|
|
216
|
+
if (!(opts?.includeExpired ?? false)) {
|
|
217
|
+
where.expires = { [sequelize_1.Op.gt]: new Date() };
|
|
218
|
+
}
|
|
219
|
+
const model = await this.Tokens.findOne({ where });
|
|
220
|
+
return model ? this.toTokenRecord(model) : null;
|
|
221
|
+
}
|
|
222
|
+
async delete(query) {
|
|
223
|
+
if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
const where = {};
|
|
227
|
+
if (query.refreshToken) {
|
|
228
|
+
where.refresh = query.refreshToken;
|
|
229
|
+
}
|
|
230
|
+
if (query.accessToken) {
|
|
231
|
+
where.access = query.accessToken;
|
|
232
|
+
}
|
|
233
|
+
if (query.userId !== undefined) {
|
|
234
|
+
where.user_id = this.normalizeUserId(query.userId);
|
|
235
|
+
}
|
|
236
|
+
if (query.clientId) {
|
|
237
|
+
where.client_id = query.clientId;
|
|
238
|
+
}
|
|
239
|
+
if (query.domain !== undefined && query.domain !== null) {
|
|
240
|
+
where.domain = query.domain;
|
|
241
|
+
}
|
|
242
|
+
if (query.fingerprint !== undefined && query.fingerprint !== null) {
|
|
243
|
+
where.fingerprint = query.fingerprint;
|
|
244
|
+
}
|
|
245
|
+
if (query.label) {
|
|
246
|
+
where.label = query.label;
|
|
247
|
+
}
|
|
248
|
+
return this.Tokens.destroy({ where });
|
|
249
|
+
}
|
|
250
|
+
async update(params) {
|
|
251
|
+
const where = { refresh: params.refreshToken };
|
|
252
|
+
if (params.clientId) {
|
|
253
|
+
where.client_id = params.clientId;
|
|
254
|
+
}
|
|
255
|
+
const updates = {};
|
|
256
|
+
if (params.accessToken !== undefined) {
|
|
257
|
+
updates.access = params.accessToken ?? null;
|
|
258
|
+
}
|
|
259
|
+
if (params.expires !== undefined) {
|
|
260
|
+
updates.expires = params.expires ?? null;
|
|
261
|
+
}
|
|
262
|
+
if (params.scope !== undefined) {
|
|
263
|
+
updates.scope = this.encodeScope(params.scope);
|
|
264
|
+
}
|
|
265
|
+
if (params.label !== undefined) {
|
|
266
|
+
updates.label = params.label ?? '';
|
|
267
|
+
}
|
|
268
|
+
if (params.domain !== undefined) {
|
|
269
|
+
updates.domain = params.domain ?? '';
|
|
270
|
+
}
|
|
271
|
+
if (params.fingerprint !== undefined) {
|
|
272
|
+
updates.fingerprint = params.fingerprint ?? '';
|
|
273
|
+
}
|
|
274
|
+
if (params.browser !== undefined) {
|
|
275
|
+
updates.browser = params.browser ?? '';
|
|
276
|
+
}
|
|
277
|
+
if (params.device !== undefined) {
|
|
278
|
+
updates.device = params.device ?? '';
|
|
279
|
+
}
|
|
280
|
+
if (params.ip !== undefined) {
|
|
281
|
+
updates.ip = params.ip ?? '';
|
|
282
|
+
}
|
|
283
|
+
if (params.os !== undefined) {
|
|
284
|
+
updates.os = params.os ?? '';
|
|
285
|
+
}
|
|
286
|
+
if (params.refreshTtlSeconds !== undefined) {
|
|
287
|
+
updates.refresh_ttl_seconds =
|
|
288
|
+
typeof params.refreshTtlSeconds === 'number' && params.refreshTtlSeconds > 0
|
|
289
|
+
? Math.floor(params.refreshTtlSeconds)
|
|
290
|
+
: null;
|
|
291
|
+
}
|
|
292
|
+
if (params.loginType !== undefined) {
|
|
293
|
+
updates.login_type = params.loginType ?? '';
|
|
294
|
+
}
|
|
295
|
+
if (params.sessionCookie !== undefined) {
|
|
296
|
+
updates.session_cookie = params.sessionCookie;
|
|
297
|
+
}
|
|
298
|
+
if (params.issuedAt !== undefined) {
|
|
299
|
+
updates.issued_at = params.issuedAt ?? null;
|
|
300
|
+
}
|
|
301
|
+
if (params.lastSeenAt !== undefined) {
|
|
302
|
+
updates.last_seen_at = params.lastSeenAt ?? null;
|
|
303
|
+
}
|
|
304
|
+
if (Object.keys(updates).length === 0) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
const [updated] = await this.Tokens.update(updates, { where });
|
|
308
|
+
return updated > 0;
|
|
309
|
+
}
|
|
310
|
+
async list(userId, opts = {}) {
|
|
311
|
+
const where = { user_id: this.normalizeUserId(userId) };
|
|
312
|
+
if (!(opts.includeExpired ?? false)) {
|
|
313
|
+
where.expires = { [sequelize_1.Op.gt]: new Date() };
|
|
314
|
+
}
|
|
315
|
+
const models = await this.Tokens.findAll({
|
|
316
|
+
where,
|
|
317
|
+
order: [['issued_at', 'DESC']],
|
|
318
|
+
limit: opts.limit,
|
|
319
|
+
offset: opts.offset
|
|
320
|
+
});
|
|
321
|
+
return models.map((model) => this.toTokenRecord(model));
|
|
322
|
+
}
|
|
323
|
+
async close() {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
normalizeUserId(identifier) {
|
|
327
|
+
if (identifier === undefined || identifier === null) {
|
|
328
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
329
|
+
}
|
|
330
|
+
return String(identifier);
|
|
331
|
+
}
|
|
332
|
+
resolveRealUserId(ruid) {
|
|
333
|
+
if (ruid === undefined || ruid === null) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const value = String(ruid);
|
|
337
|
+
if (value.length === 0 || value === '0') {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
encodeStringArray(values) {
|
|
343
|
+
return JSON.stringify(values ?? []);
|
|
344
|
+
}
|
|
345
|
+
decodeStringArray(raw) {
|
|
346
|
+
if (!raw) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const parsed = JSON.parse(raw);
|
|
351
|
+
if (Array.isArray(parsed)) {
|
|
352
|
+
return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// ignore malformed values
|
|
357
|
+
}
|
|
358
|
+
return raw
|
|
359
|
+
.split(/\s+/)
|
|
360
|
+
.map((entry) => entry.trim())
|
|
361
|
+
.filter((entry) => entry.length > 0);
|
|
362
|
+
}
|
|
363
|
+
encodeScope(scope) {
|
|
364
|
+
if (!scope || (Array.isArray(scope) && scope.length === 0)) {
|
|
365
|
+
return '[]';
|
|
366
|
+
}
|
|
367
|
+
if (Array.isArray(scope)) {
|
|
368
|
+
return this.encodeStringArray(scope);
|
|
369
|
+
}
|
|
370
|
+
return this.encodeStringArray(scope.split(/\s+/).filter((entry) => entry.length > 0));
|
|
371
|
+
}
|
|
372
|
+
decodeScope(raw) {
|
|
373
|
+
return this.decodeStringArray(raw);
|
|
374
|
+
}
|
|
375
|
+
toTokenRecord(model) {
|
|
376
|
+
const scope = this.decodeScope(model.scope);
|
|
377
|
+
const normalized = this.normalizeToken({
|
|
378
|
+
userId: model.user_id,
|
|
379
|
+
refreshToken: model.refresh,
|
|
380
|
+
accessToken: model.access,
|
|
381
|
+
expires: model.expires,
|
|
382
|
+
issuedAt: model.issued_at,
|
|
383
|
+
lastSeenAt: model.last_seen_at,
|
|
384
|
+
domain: model.domain,
|
|
385
|
+
fingerprint: model.fingerprint,
|
|
386
|
+
label: model.label,
|
|
387
|
+
browser: model.browser,
|
|
388
|
+
device: model.device,
|
|
389
|
+
ip: model.ip,
|
|
390
|
+
os: model.os,
|
|
391
|
+
clientId: model.client_id ?? undefined,
|
|
392
|
+
scope,
|
|
393
|
+
loginType: model.login_type || undefined,
|
|
394
|
+
refreshTtlSeconds: model.refresh_ttl_seconds ?? undefined,
|
|
395
|
+
ruid: model.real_user_id ?? undefined,
|
|
396
|
+
sessionCookie: model.session_cookie
|
|
397
|
+
});
|
|
398
|
+
return {
|
|
399
|
+
...normalized,
|
|
400
|
+
scope: normalized.scope ? [...normalized.scope] : undefined
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
exports.SequelizeTokenStore = SequelizeTokenStore;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type TokenStatus = 'active' | 'expired' | 'revoked';
|
|
2
|
+
export interface Token {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
expires?: Date;
|
|
7
|
+
issuedAt?: Date;
|
|
8
|
+
lastSeenAt?: Date;
|
|
9
|
+
status?: TokenStatus;
|
|
10
|
+
ruid?: string;
|
|
11
|
+
clientId?: string;
|
|
12
|
+
domain?: string;
|
|
13
|
+
fingerprint?: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
browser?: string;
|
|
16
|
+
device?: string;
|
|
17
|
+
ip?: string;
|
|
18
|
+
os?: string;
|
|
19
|
+
scope?: string | string[];
|
|
20
|
+
loginType?: string;
|
|
21
|
+
refreshTtlSeconds?: number;
|
|
22
|
+
sessionCookie?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface TokenPair {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
refreshToken: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
|
|
2
|
+
import type { AuthIdentifier } from '../auth-api/types.js';
|
|
3
|
+
export declare abstract class UserStore<User, PublicUser> {
|
|
4
|
+
protected readonly toPublicUser: PublicUserMapper<User, PublicUser>;
|
|
5
|
+
private readonly bcryptRounds;
|
|
6
|
+
private readonly bcryptPepper?;
|
|
7
|
+
constructor(opts?: {
|
|
8
|
+
toPublic?: PublicUserMapper<User, PublicUser>;
|
|
9
|
+
bcryptRounds?: number;
|
|
10
|
+
bcryptPepper?: string;
|
|
11
|
+
});
|
|
12
|
+
protected hashPassword(plain: string): Promise<string>;
|
|
13
|
+
verifyPassword(plain: string, hashed: string): Promise<boolean>;
|
|
14
|
+
protected normalizeUserInput(input: Partial<CreateUserInput>): CreateUserInput;
|
|
15
|
+
abstract findUser(identifier: AuthIdentifier | string): Promise<User | null>;
|
|
16
|
+
abstract findById(id: AuthIdentifier): Promise<User | null>;
|
|
17
|
+
abstract findByLoginOrEmail(loginOrEmail: string): Promise<User | null>;
|
|
18
|
+
abstract createUser(input: CreateUserInput): Promise<User>;
|
|
19
|
+
abstract upsertUser(input: CreateUserInput): Promise<User>;
|
|
20
|
+
abstract updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<User>;
|
|
21
|
+
abstract setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
|
|
22
|
+
abstract getPasswordHash(user: User): string | null;
|
|
23
|
+
abstract getUserId(user: User): AuthIdentifier;
|
|
24
|
+
toPublic(user: User): PublicUser;
|
|
25
|
+
}
|
|
26
|
+
export type { CreateUserInput, UpdateUserInput, PublicUserMapper } from './types.js';
|