@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.
Files changed (115) hide show
  1. package/dist/cjs/api-server-base.cjs +181 -74
  2. package/dist/cjs/api-server-base.d.ts +66 -29
  3. package/dist/cjs/auth-api/auth-module.d.ts +96 -0
  4. package/dist/cjs/auth-api/auth-module.js +1032 -0
  5. package/dist/cjs/auth-api/compat-auth-storage.d.ts +55 -0
  6. package/dist/cjs/auth-api/compat-auth-storage.js +116 -0
  7. package/dist/cjs/auth-api/mem-auth-store.d.ts +66 -0
  8. package/dist/cjs/auth-api/mem-auth-store.js +135 -0
  9. package/dist/cjs/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
  10. package/dist/cjs/{auth-module.cjs → auth-api/module.js} +1 -1
  11. package/dist/cjs/auth-api/sql-auth-store.d.ts +75 -0
  12. package/dist/cjs/auth-api/sql-auth-store.js +166 -0
  13. package/dist/cjs/auth-api/storage.d.ts +36 -0
  14. package/dist/cjs/{auth-storage.cjs → auth-api/storage.js} +2 -2
  15. package/dist/cjs/auth-api/types.d.ts +29 -0
  16. package/dist/cjs/auth-api/types.js +2 -0
  17. package/dist/cjs/index.cjs +41 -7
  18. package/dist/cjs/index.d.ts +29 -5
  19. package/dist/cjs/oauth/base.d.ts +10 -0
  20. package/dist/cjs/oauth/base.js +6 -0
  21. package/dist/cjs/oauth/memory.d.ts +16 -0
  22. package/dist/cjs/oauth/memory.js +99 -0
  23. package/dist/cjs/oauth/models.d.ts +45 -0
  24. package/dist/cjs/oauth/models.js +58 -0
  25. package/dist/cjs/oauth/sequelize.d.ts +68 -0
  26. package/dist/cjs/oauth/sequelize.js +210 -0
  27. package/dist/cjs/oauth/types.d.ts +50 -0
  28. package/dist/cjs/oauth/types.js +3 -0
  29. package/dist/cjs/passkey/base.d.ts +15 -0
  30. package/dist/cjs/passkey/base.js +6 -0
  31. package/dist/cjs/passkey/memory.d.ts +26 -0
  32. package/dist/cjs/passkey/memory.js +82 -0
  33. package/dist/cjs/passkey/models.d.ts +25 -0
  34. package/dist/cjs/passkey/models.js +115 -0
  35. package/dist/cjs/passkey/sequelize.d.ts +54 -0
  36. package/dist/cjs/passkey/sequelize.js +211 -0
  37. package/dist/cjs/passkey/service.d.ts +17 -0
  38. package/dist/cjs/passkey/service.js +221 -0
  39. package/dist/cjs/passkey/types.d.ts +75 -0
  40. package/dist/cjs/passkey/types.js +2 -0
  41. package/dist/cjs/token/base.d.ts +38 -0
  42. package/dist/cjs/token/base.js +114 -0
  43. package/dist/cjs/token/memory.d.ts +19 -0
  44. package/dist/cjs/token/memory.js +149 -0
  45. package/dist/cjs/token/sequelize.d.ts +58 -0
  46. package/dist/cjs/token/sequelize.js +404 -0
  47. package/dist/cjs/token/types.d.ts +27 -0
  48. package/dist/cjs/token/types.js +2 -0
  49. package/dist/cjs/user/base.d.ts +26 -0
  50. package/dist/cjs/user/base.js +45 -0
  51. package/dist/cjs/user/memory.d.ts +35 -0
  52. package/dist/cjs/user/memory.js +173 -0
  53. package/dist/cjs/user/sequelize.d.ts +41 -0
  54. package/dist/cjs/user/sequelize.js +182 -0
  55. package/dist/cjs/user/types.d.ts +11 -0
  56. package/dist/cjs/user/types.js +2 -0
  57. package/dist/esm/api-server-base.d.ts +66 -29
  58. package/dist/esm/api-server-base.js +179 -72
  59. package/dist/esm/auth-api/auth-module.d.ts +96 -0
  60. package/dist/esm/auth-api/auth-module.js +1030 -0
  61. package/dist/esm/auth-api/compat-auth-storage.d.ts +55 -0
  62. package/dist/esm/auth-api/compat-auth-storage.js +112 -0
  63. package/dist/esm/auth-api/mem-auth-store.d.ts +66 -0
  64. package/dist/esm/auth-api/mem-auth-store.js +131 -0
  65. package/dist/esm/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
  66. package/dist/esm/{auth-module.js → auth-api/module.js} +1 -1
  67. package/dist/esm/auth-api/sql-auth-store.d.ts +75 -0
  68. package/dist/esm/auth-api/sql-auth-store.js +162 -0
  69. package/dist/esm/auth-api/storage.d.ts +36 -0
  70. package/dist/esm/{auth-storage.js → auth-api/storage.js} +2 -2
  71. package/dist/esm/auth-api/types.d.ts +29 -0
  72. package/dist/esm/auth-api/types.js +1 -0
  73. package/dist/esm/index.d.ts +29 -5
  74. package/dist/esm/index.js +19 -2
  75. package/dist/esm/oauth/base.d.ts +10 -0
  76. package/dist/esm/oauth/base.js +2 -0
  77. package/dist/esm/oauth/memory.d.ts +16 -0
  78. package/dist/esm/oauth/memory.js +92 -0
  79. package/dist/esm/oauth/models.d.ts +45 -0
  80. package/dist/esm/oauth/models.js +51 -0
  81. package/dist/esm/oauth/sequelize.d.ts +68 -0
  82. package/dist/esm/oauth/sequelize.js +199 -0
  83. package/dist/esm/oauth/types.d.ts +50 -0
  84. package/dist/esm/oauth/types.js +2 -0
  85. package/dist/esm/passkey/base.d.ts +15 -0
  86. package/dist/esm/passkey/base.js +2 -0
  87. package/dist/esm/passkey/memory.d.ts +26 -0
  88. package/dist/esm/passkey/memory.js +78 -0
  89. package/dist/esm/passkey/models.d.ts +25 -0
  90. package/dist/esm/passkey/models.js +108 -0
  91. package/dist/esm/passkey/sequelize.d.ts +54 -0
  92. package/dist/esm/passkey/sequelize.js +207 -0
  93. package/dist/esm/passkey/service.d.ts +17 -0
  94. package/dist/esm/passkey/service.js +217 -0
  95. package/dist/esm/passkey/types.d.ts +75 -0
  96. package/dist/esm/passkey/types.js +1 -0
  97. package/dist/esm/token/base.d.ts +38 -0
  98. package/dist/esm/token/base.js +107 -0
  99. package/dist/esm/token/memory.d.ts +19 -0
  100. package/dist/esm/token/memory.js +145 -0
  101. package/dist/esm/token/sequelize.d.ts +58 -0
  102. package/dist/esm/token/sequelize.js +400 -0
  103. package/dist/esm/token/types.d.ts +27 -0
  104. package/dist/esm/token/types.js +1 -0
  105. package/dist/esm/user/base.d.ts +26 -0
  106. package/dist/esm/user/base.js +38 -0
  107. package/dist/esm/user/memory.d.ts +35 -0
  108. package/dist/esm/user/memory.js +169 -0
  109. package/dist/esm/user/sequelize.d.ts +41 -0
  110. package/dist/esm/user/sequelize.js +176 -0
  111. package/dist/esm/user/types.d.ts +11 -0
  112. package/dist/esm/user/types.js +1 -0
  113. package/package.json +11 -3
  114. package/dist/cjs/auth-storage.d.ts +0 -133
  115. package/dist/esm/auth-storage.d.ts +0 -133
@@ -0,0 +1,217 @@
1
+ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
2
+ import { isoBase64URL } from '@simplewebauthn/server/helpers';
3
+ const ALLOWED_TRANSPORTS = [
4
+ 'ble',
5
+ 'cable',
6
+ 'hybrid',
7
+ 'internal',
8
+ 'nfc',
9
+ 'smart-card',
10
+ 'usb'
11
+ ];
12
+ function sanitizeTransports(input) {
13
+ if (!Array.isArray(input)) {
14
+ return undefined;
15
+ }
16
+ const filtered = input
17
+ .map((value) => String(value))
18
+ .filter((value) => ALLOWED_TRANSPORTS.includes(value));
19
+ return filtered.length > 0 ? filtered : undefined;
20
+ }
21
+ function toBase64Url(buffer) {
22
+ return isoBase64URL.fromBuffer(new Uint8Array(buffer));
23
+ }
24
+ function fromBase64Url(value) {
25
+ return Buffer.from(isoBase64URL.toBuffer(value));
26
+ }
27
+ function toBuffer(value) {
28
+ if (Buffer.isBuffer(value)) {
29
+ return value;
30
+ }
31
+ const view = value instanceof Uint8Array ? value : new Uint8Array(value);
32
+ return Buffer.from(view);
33
+ }
34
+ export class PasskeyService {
35
+ constructor(config, adapter, logger = console) {
36
+ this.config = config;
37
+ this.adapter = adapter;
38
+ this.logger = logger;
39
+ }
40
+ async createChallenge(params) {
41
+ await this.adapter.cleanupChallenges?.(new Date());
42
+ const metadata = {
43
+ domain: typeof params.domain === 'string' ? params.domain : undefined,
44
+ fingerprint: typeof params.fingerprint === 'string' ? params.fingerprint : undefined,
45
+ label: typeof params.label === 'string' ? params.label : undefined,
46
+ userAgent: typeof params.userAgent === 'string' ? params.userAgent : undefined
47
+ };
48
+ if (params.action === 'register') {
49
+ return this.createRegistrationChallenge(params, metadata);
50
+ }
51
+ if (params.action === 'authenticate') {
52
+ return this.createAuthenticationChallenge(params, metadata);
53
+ }
54
+ throw new Error(`Unsupported passkey action: ${String(params.action)}`);
55
+ }
56
+ async verifyResponse(params) {
57
+ await this.adapter.cleanupChallenges?.(new Date());
58
+ const record = await this.adapter.consumeChallenge(params.expectedChallenge);
59
+ if (!record) {
60
+ return { verified: false };
61
+ }
62
+ if (record.expiresAt.getTime() <= Date.now()) {
63
+ return { verified: false };
64
+ }
65
+ try {
66
+ if (record.action === 'register') {
67
+ return this.verifyRegistration(params, record);
68
+ }
69
+ if (record.action === 'authenticate') {
70
+ return this.verifyAuthentication(params, record);
71
+ }
72
+ }
73
+ catch (error) {
74
+ this.logger.error?.('Passkey verification failed', error);
75
+ }
76
+ return { verified: false };
77
+ }
78
+ async createRegistrationChallenge(params, metadata) {
79
+ const user = await this.requireUser({ userId: params.userId, login: params.login });
80
+ const existing = await this.adapter.listUserCredentials(user.id);
81
+ const excludeCredentials = existing.map((credential) => {
82
+ const transports = credential.transports;
83
+ return transports && transports.length > 0
84
+ ? { id: toBase64Url(credential.credentialId), transports }
85
+ : { id: toBase64Url(credential.credentialId) };
86
+ });
87
+ const options = await generateRegistrationOptions({
88
+ rpName: this.config.rpName,
89
+ rpID: this.config.rpId,
90
+ userID: Buffer.from(String(user.id)),
91
+ userName: user.login,
92
+ userDisplayName: user.displayName,
93
+ excludeCredentials
94
+ });
95
+ const expiresAt = this.createExpiry();
96
+ await this.adapter.saveChallenge({
97
+ challenge: options.challenge,
98
+ action: 'register',
99
+ userId: user.id,
100
+ login: user.login,
101
+ expiresAt,
102
+ metadata
103
+ });
104
+ return {
105
+ challenge: options.challenge,
106
+ expiresAt: expiresAt.toISOString(),
107
+ userId: user.id
108
+ };
109
+ }
110
+ async createAuthenticationChallenge(params, metadata) {
111
+ const user = await this.requireUser({ userId: params.userId, login: params.login });
112
+ const credentials = await this.adapter.listUserCredentials(user.id);
113
+ const allowCredentials = credentials.map((credential) => {
114
+ const transports = sanitizeTransports(credential.transports);
115
+ return transports && transports.length > 0
116
+ ? { type: 'public-key', id: toBase64Url(credential.credentialId), transports }
117
+ : { type: 'public-key', id: toBase64Url(credential.credentialId) };
118
+ });
119
+ const options = await generateAuthenticationOptions({
120
+ allowCredentials,
121
+ userVerification: this.config.userVerification,
122
+ rpID: this.config.rpId
123
+ });
124
+ const expiresAt = this.createExpiry();
125
+ await this.adapter.saveChallenge({
126
+ challenge: options.challenge,
127
+ action: 'authenticate',
128
+ userId: user.id,
129
+ login: user.login,
130
+ expiresAt,
131
+ metadata
132
+ });
133
+ return {
134
+ challenge: options.challenge,
135
+ expiresAt: expiresAt.toISOString(),
136
+ userId: user.id
137
+ };
138
+ }
139
+ async verifyRegistration(params, record) {
140
+ const parsed = typeof params.response === 'object' && params.response ? { ...params.response } : {};
141
+ const response = {
142
+ ...parsed,
143
+ id: String(parsed.id ?? ''),
144
+ rawId: String(parsed.rawId ?? '')
145
+ };
146
+ const user = await this.requireUser({ userId: record.userId, login: record.login });
147
+ const result = await verifyRegistrationResponse({
148
+ response,
149
+ expectedChallenge: record.challenge,
150
+ expectedOrigin: this.config.origins,
151
+ expectedRPID: this.config.rpId,
152
+ requireUserVerification: true
153
+ });
154
+ if (!result.verified || !result.registrationInfo) {
155
+ return { verified: false };
156
+ }
157
+ const registrationInfo = result.registrationInfo;
158
+ await this.adapter.saveCredential({
159
+ userId: user.id,
160
+ credentialId: toBuffer(registrationInfo.credentialID),
161
+ publicKey: toBuffer(registrationInfo.credentialPublicKey),
162
+ counter: registrationInfo.counter,
163
+ transports: sanitizeTransports(params.response.transports),
164
+ backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
165
+ deviceType: registrationInfo.credentialDeviceType
166
+ });
167
+ return { verified: true, userId: user.id, login: user.login };
168
+ }
169
+ async verifyAuthentication(params, record) {
170
+ const parsed = typeof params.response === 'object' && params.response ? { ...params.response } : {};
171
+ const response = {
172
+ ...parsed,
173
+ id: String(parsed.id ?? ''),
174
+ rawId: String(parsed.rawId ?? '')
175
+ };
176
+ const credential = await this.adapter.findCredentialById(fromBase64Url(response.id));
177
+ if (!credential) {
178
+ return { verified: false };
179
+ }
180
+ const user = await this.requireUser({ userId: credential.userId, login: record.login });
181
+ const storedAuthData = {
182
+ credentialID: credential.credentialId,
183
+ counter: credential.counter,
184
+ credentialBackedUp: credential.backedUp,
185
+ credentialDeviceType: credential.deviceType,
186
+ credentialPublicKey: credential.publicKey,
187
+ transports: credential.transports ?? undefined
188
+ };
189
+ const result = await verifyAuthenticationResponse({
190
+ response,
191
+ expectedChallenge: record.challenge,
192
+ expectedOrigin: this.config.origins,
193
+ expectedRPID: this.config.rpId,
194
+ authenticator: storedAuthData,
195
+ requireUserVerification: true
196
+ });
197
+ if (!result.verified) {
198
+ return { verified: false };
199
+ }
200
+ await this.adapter.updateCredentialCounter(credential.credentialId, result.authenticationInfo.newCounter);
201
+ return {
202
+ verified: true,
203
+ userId: user.id,
204
+ login: user.login
205
+ };
206
+ }
207
+ async requireUser(params) {
208
+ const user = await this.adapter.resolveUser(params);
209
+ if (!user) {
210
+ throw new Error('User not found');
211
+ }
212
+ return user;
213
+ }
214
+ createExpiry() {
215
+ return new Date(Date.now() + this.config.timeoutMs);
216
+ }
217
+ }
@@ -0,0 +1,75 @@
1
+ import type { AuthIdentifier } from '../auth-api/types.js';
2
+ import type { Token, TokenPair } from '../token/types.js';
3
+ import type { AuthenticatorTransportFuture } from '@simplewebauthn/server';
4
+ export type CredentialDeviceType = 'singleDevice' | 'multiDevice';
5
+ export interface PasskeyServiceConfig {
6
+ rpId: string;
7
+ rpName: string;
8
+ origins: string[];
9
+ timeoutMs: number;
10
+ userVerification?: 'preferred' | 'required' | 'discouraged';
11
+ }
12
+ export interface PasskeyChallengeMetadata {
13
+ domain?: string;
14
+ fingerprint?: string;
15
+ label?: string;
16
+ userAgent?: string;
17
+ }
18
+ export interface PasskeyChallengeRecord {
19
+ challenge: string;
20
+ action: 'register' | 'authenticate';
21
+ userId?: AuthIdentifier;
22
+ login?: string;
23
+ expiresAt: Date;
24
+ metadata: PasskeyChallengeMetadata;
25
+ }
26
+ export interface PasskeyUserDescriptor {
27
+ id: AuthIdentifier;
28
+ login: string;
29
+ displayName: string;
30
+ }
31
+ export interface StoredPasskeyCredential {
32
+ userId: AuthIdentifier;
33
+ credentialId: Buffer;
34
+ publicKey: Buffer;
35
+ counter: number;
36
+ transports?: AuthenticatorTransportFuture[];
37
+ backedUp: boolean;
38
+ deviceType: CredentialDeviceType;
39
+ }
40
+ export interface PasskeyStorageAdapter {
41
+ resolveUser(params: {
42
+ userId?: AuthIdentifier;
43
+ login?: string;
44
+ }): Promise<PasskeyUserDescriptor | null>;
45
+ listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
46
+ findCredentialById(credentialId: Buffer): Promise<StoredPasskeyCredential | null>;
47
+ saveCredential(record: StoredPasskeyCredential): Promise<void>;
48
+ updateCredentialCounter(credentialId: Buffer, counter: number): Promise<void>;
49
+ saveChallenge(record: PasskeyChallengeRecord): Promise<void>;
50
+ consumeChallenge(challenge: string): Promise<PasskeyChallengeRecord | null>;
51
+ cleanupChallenges?(now: Date): Promise<void>;
52
+ }
53
+ export interface PasskeyChallengeParams extends Partial<Omit<Token, 'userId'>> {
54
+ action: 'register' | 'authenticate';
55
+ login?: string;
56
+ userAgent?: string;
57
+ userId?: AuthIdentifier;
58
+ }
59
+ export interface PasskeyChallenge extends Record<string, unknown> {
60
+ challenge: string;
61
+ expiresAt?: string | number | Date;
62
+ userId?: AuthIdentifier;
63
+ }
64
+ export interface PasskeyVerificationParams extends Partial<Omit<Token, 'userId'>> {
65
+ expectedChallenge: string;
66
+ login?: string;
67
+ response: Record<string, unknown>;
68
+ userId?: AuthIdentifier;
69
+ }
70
+ export interface PasskeyVerificationResult extends Record<string, unknown> {
71
+ login?: string;
72
+ tokens?: TokenPair;
73
+ userId?: AuthIdentifier;
74
+ verified: boolean;
75
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,38 @@
1
+ import type { Token } from './types.js';
2
+ import type { DecodeOptions, SignOptions, VerifyOptions } from 'jsonwebtoken';
3
+ export interface JwtSignResult {
4
+ success: boolean;
5
+ token?: string;
6
+ error?: string;
7
+ }
8
+ export interface JwtVerifyResult<T> {
9
+ success: boolean;
10
+ data?: T;
11
+ expired?: boolean;
12
+ error?: string;
13
+ }
14
+ export interface JwtDecodeResult<T> {
15
+ success: boolean;
16
+ data?: T;
17
+ error?: string;
18
+ }
19
+ export declare abstract class TokenStore {
20
+ abstract save(record: Token): Promise<void>;
21
+ abstract get(query: Partial<Token>, opts?: {
22
+ includeExpired?: boolean;
23
+ }): Promise<Token | null>;
24
+ abstract delete(query: Partial<Token>): Promise<number>;
25
+ abstract update(update: Partial<Token> & {
26
+ refreshToken: string;
27
+ }): Promise<boolean>;
28
+ abstract list(userId: string | number, opts?: {
29
+ limit?: number;
30
+ offset?: number;
31
+ includeExpired?: boolean;
32
+ }): Promise<Token[]>;
33
+ abstract close(): Promise<void>;
34
+ normalizeToken(token: Partial<Token>): Token;
35
+ jwtSign(payload: any, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
36
+ jwtVerify<T>(token: string, secret: string, options?: VerifyOptions): JwtVerifyResult<T>;
37
+ jwtDecode<T>(token: string, options?: DecodeOptions): JwtDecodeResult<T>;
38
+ }
@@ -0,0 +1,107 @@
1
+ import jwt from 'jsonwebtoken';
2
+ function normalizeScope(scope) {
3
+ if (!scope) {
4
+ return undefined;
5
+ }
6
+ if (Array.isArray(scope)) {
7
+ return scope.filter((entry) => typeof entry === 'string' && entry.length > 0);
8
+ }
9
+ return scope
10
+ .split(/\s+/)
11
+ .map((entry) => entry.trim())
12
+ .filter((entry) => entry.length > 0);
13
+ }
14
+ function normalizeRefreshTtlSeconds(value) {
15
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
16
+ return Math.floor(value);
17
+ }
18
+ if (typeof value === 'string') {
19
+ const parsed = Number(value);
20
+ if (Number.isFinite(parsed) && parsed > 0) {
21
+ return Math.floor(parsed);
22
+ }
23
+ }
24
+ return undefined;
25
+ }
26
+ function normalizeTokenInternal(input) {
27
+ if (!input.refreshToken) {
28
+ throw new Error('refreshToken is required');
29
+ }
30
+ if (!input.accessToken) {
31
+ throw new Error('accessToken is required');
32
+ }
33
+ if (input.userId === undefined || input.userId === null) {
34
+ throw new Error('userId is required');
35
+ }
36
+ const userId = String(input.userId);
37
+ const ruid = input.ruid === undefined || input.ruid === null ? undefined : String(input.ruid);
38
+ const expires = input.expires ? new Date(input.expires) : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
39
+ const issuedAt = input.issuedAt ? new Date(input.issuedAt) : new Date();
40
+ const lastSeenAt = input.lastSeenAt ? new Date(input.lastSeenAt) : issuedAt;
41
+ const scope = normalizeScope(input.scope);
42
+ const refreshTtlSeconds = normalizeRefreshTtlSeconds(input.refreshTtlSeconds);
43
+ const status = input.status === 'revoked' ? 'revoked' : expires.getTime() < Date.now() ? 'expired' : 'active';
44
+ const sessionCookie = typeof input.sessionCookie === 'boolean' ? input.sessionCookie : false;
45
+ return {
46
+ ...input,
47
+ accessToken: input.accessToken,
48
+ refreshToken: input.refreshToken,
49
+ userId,
50
+ domain: typeof input.domain === 'string' ? input.domain : '',
51
+ fingerprint: typeof input.fingerprint === 'string' ? input.fingerprint : '',
52
+ label: typeof input.label === 'string' ? input.label : '',
53
+ browser: typeof input.browser === 'string' ? input.browser : '',
54
+ device: typeof input.device === 'string' ? input.device : '',
55
+ ip: typeof input.ip === 'string' ? input.ip : '',
56
+ os: typeof input.os === 'string' ? input.os : '',
57
+ loginType: typeof input.loginType === 'string' && input.loginType.length > 0 ? input.loginType : undefined,
58
+ scope,
59
+ refreshTtlSeconds,
60
+ expires,
61
+ issuedAt,
62
+ lastSeenAt,
63
+ status,
64
+ ruid,
65
+ sessionCookie
66
+ };
67
+ }
68
+ export class TokenStore {
69
+ // Instance helpers
70
+ normalizeToken(token) {
71
+ return normalizeTokenInternal(token);
72
+ }
73
+ jwtSign(payload, secret, expiresInSeconds, options) {
74
+ const opts = { ...(options ?? {}), expiresIn: expiresInSeconds };
75
+ try {
76
+ const token = jwt.sign(payload, secret, opts);
77
+ return { success: true, token };
78
+ }
79
+ catch (error) {
80
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
81
+ }
82
+ }
83
+ jwtVerify(token, secret, options) {
84
+ try {
85
+ const data = jwt.verify(token, secret, options ?? {});
86
+ return { success: true, data };
87
+ }
88
+ catch (error) {
89
+ if (error instanceof jwt.TokenExpiredError) {
90
+ return { success: false, expired: true, error: 'Token expired' };
91
+ }
92
+ return { success: false, expired: false, error: error instanceof Error ? error.message : String(error) };
93
+ }
94
+ }
95
+ jwtDecode(token, options) {
96
+ try {
97
+ const data = jwt.decode(token, options ?? {});
98
+ if (data === null) {
99
+ return { success: false, error: 'Invalid token format' };
100
+ }
101
+ return { success: true, data };
102
+ }
103
+ catch (error) {
104
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
105
+ }
106
+ }
107
+ }
@@ -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,145 @@
1
+ import { TokenStore } from './base.js';
2
+ function comparableUserId(value) {
3
+ if (value === undefined || value === null) {
4
+ return undefined;
5
+ }
6
+ return String(value);
7
+ }
8
+ function cloneToken(record) {
9
+ // this.normalizeToken is not available in static context; caller passes through instance.
10
+ // cloning handled via store instance methods.
11
+ const normalized = record;
12
+ return {
13
+ ...normalized,
14
+ scope: normalized.scope ? [...normalized.scope] : undefined
15
+ };
16
+ }
17
+ function matchesQuery(record, query, includeExpired) {
18
+ if (query.refreshToken && record.refreshToken !== query.refreshToken) {
19
+ return false;
20
+ }
21
+ if (query.accessToken && record.accessToken !== query.accessToken) {
22
+ return false;
23
+ }
24
+ if (query.userId !== undefined && comparableUserId(record.userId) !== comparableUserId(query.userId)) {
25
+ return false;
26
+ }
27
+ if (query.clientId && record.clientId !== query.clientId) {
28
+ return false;
29
+ }
30
+ if (query.domain !== undefined && (record.domain ?? '') !== (query.domain ?? '')) {
31
+ return false;
32
+ }
33
+ if (query.fingerprint !== undefined && (record.fingerprint ?? '') !== (query.fingerprint ?? '')) {
34
+ return false;
35
+ }
36
+ if (query.loginType !== undefined && record.loginType !== (query.loginType ?? undefined)) {
37
+ return false;
38
+ }
39
+ if (query.label && record.label !== query.label) {
40
+ return false;
41
+ }
42
+ if (!includeExpired && (record.expires ?? new Date(0)).getTime() < Date.now()) {
43
+ return false;
44
+ }
45
+ return true;
46
+ }
47
+ export class MemoryTokenStore extends TokenStore {
48
+ constructor() {
49
+ super(...arguments);
50
+ this.tokens = [];
51
+ }
52
+ async save(record) {
53
+ const stored = this.normalizeToken(record);
54
+ const normalizedUserId = comparableUserId(stored.userId);
55
+ const domainProvided = record.domain !== undefined;
56
+ const fingerprintProvided = record.fingerprint !== undefined;
57
+ for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
58
+ const existing = this.tokens[index];
59
+ if (comparableUserId(existing.userId) !== normalizedUserId) {
60
+ continue;
61
+ }
62
+ if (record.clientId && existing.clientId !== record.clientId) {
63
+ continue;
64
+ }
65
+ if (domainProvided && existing.domain !== stored.domain) {
66
+ continue;
67
+ }
68
+ if (fingerprintProvided && existing.fingerprint !== stored.fingerprint) {
69
+ continue;
70
+ }
71
+ this.tokens.splice(index, 1);
72
+ }
73
+ this.tokens.push(stored);
74
+ }
75
+ async get(query, opts) {
76
+ if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
77
+ throw new Error('At least one token lookup field must be provided');
78
+ }
79
+ const includeExpired = opts?.includeExpired ?? false;
80
+ const record = this.tokens.find((token) => matchesQuery(token, query, includeExpired));
81
+ return record ? cloneToken(record) : null;
82
+ }
83
+ async delete(query) {
84
+ if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
85
+ return 0;
86
+ }
87
+ let removed = 0;
88
+ for (let index = this.tokens.length - 1; index >= 0; index -= 1) {
89
+ if (matchesQuery(this.tokens[index], query, true)) {
90
+ this.tokens.splice(index, 1);
91
+ removed += 1;
92
+ }
93
+ }
94
+ return removed;
95
+ }
96
+ async update(params) {
97
+ const token = this.tokens.find((record) => {
98
+ if (record.refreshToken !== params.refreshToken) {
99
+ return false;
100
+ }
101
+ if (params.clientId && record.clientId !== params.clientId) {
102
+ return false;
103
+ }
104
+ return true;
105
+ });
106
+ if (!token) {
107
+ return false;
108
+ }
109
+ const merged = { ...token };
110
+ const maybeAssign = (key) => {
111
+ const value = params[key];
112
+ if (value !== undefined) {
113
+ merged[key] = value;
114
+ }
115
+ };
116
+ maybeAssign('accessToken');
117
+ maybeAssign('expires');
118
+ maybeAssign('scope');
119
+ maybeAssign('label');
120
+ maybeAssign('domain');
121
+ maybeAssign('fingerprint');
122
+ maybeAssign('browser');
123
+ maybeAssign('device');
124
+ maybeAssign('ip');
125
+ maybeAssign('os');
126
+ maybeAssign('refreshTtlSeconds');
127
+ maybeAssign('loginType');
128
+ maybeAssign('issuedAt');
129
+ maybeAssign('lastSeenAt');
130
+ maybeAssign('sessionCookie');
131
+ const normalized = this.normalizeToken(merged);
132
+ Object.assign(token, normalized);
133
+ return true;
134
+ }
135
+ async list(userId, opts = {}) {
136
+ const includeExpired = opts.includeExpired ?? false;
137
+ const filtered = this.tokens.filter((token) => matchesQuery(token, { userId: comparableUserId(userId) }, includeExpired));
138
+ const offset = opts.offset ?? 0;
139
+ const limit = opts.limit ?? filtered.length;
140
+ return filtered.slice(offset, offset + limit).map(cloneToken);
141
+ }
142
+ async close() {
143
+ return;
144
+ }
145
+ }
@@ -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 {};