@technomoron/api-server-base 2.0.0-beta.4 → 2.0.0-beta.6

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.
@@ -448,6 +448,12 @@ class ApiServer {
448
448
  }
449
449
  return this.passkeyServiceAdapter;
450
450
  }
451
+ async listUserCredentials(userId) {
452
+ return this.ensurePasskeyService().listUserCredentials(userId);
453
+ }
454
+ async deletePasskeyCredential(credentialId) {
455
+ return this.ensurePasskeyService().deleteCredential(credentialId);
456
+ }
451
457
  ensureOAuthStore() {
452
458
  if (!this.oauthStoreAdapter) {
453
459
  throw new Error('OAuth store is not configured');
@@ -13,7 +13,7 @@ import type { AuthAdapter, AuthIdentifier } from './auth-api/types.js';
13
13
  import type { OAuthStore } from './oauth/base.js';
14
14
  import type { AuthCodeData, AuthCodeRequest, OAuthClient } from './oauth/types.js';
15
15
  import type { PasskeyService } from './passkey/service.js';
16
- import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
16
+ import type { PasskeyChallenge, PasskeyChallengeParams, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
17
17
  import type { Token } from './token/types.js';
18
18
  import type { UserStore } from './user/base.js';
19
19
  import type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
@@ -139,6 +139,8 @@ export declare class ApiServer {
139
139
  private ensureUserStore;
140
140
  private ensureTokenStore;
141
141
  private ensurePasskeyService;
142
+ listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
143
+ deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
142
144
  private ensureOAuthStore;
143
145
  getUser(identifier: AuthIdentifier): Promise<any | null>;
144
146
  getUserPasswordHash(user: any): string;
@@ -34,6 +34,26 @@ function toBuffer(value) {
34
34
  const view = value instanceof Uint8Array ? value : new Uint8Array(value);
35
35
  return Buffer.from(view);
36
36
  }
37
+ function toBufferOrNull(value) {
38
+ if (!value) {
39
+ return null;
40
+ }
41
+ if (Buffer.isBuffer(value)) {
42
+ return value;
43
+ }
44
+ if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
45
+ return toBuffer(value);
46
+ }
47
+ if (typeof value === 'string') {
48
+ try {
49
+ return fromBase64Url(value);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ return null;
56
+ }
37
57
  class PasskeyService {
38
58
  constructor(config, adapter, logger = console) {
39
59
  this.config = config;
@@ -113,7 +133,8 @@ class PasskeyService {
113
133
  return {
114
134
  challenge: options.challenge,
115
135
  expiresAt: expiresAt.toISOString(),
116
- userId: user.id
136
+ userId: user.id,
137
+ publicKey: options
117
138
  };
118
139
  }
119
140
  async createAuthenticationChallenge(params, metadata) {
@@ -142,7 +163,8 @@ class PasskeyService {
142
163
  return {
143
164
  challenge: options.challenge,
144
165
  expiresAt: expiresAt.toISOString(),
145
- userId: user.id
166
+ userId: user.id,
167
+ publicKey: options
146
168
  };
147
169
  }
148
170
  async verifyRegistration(params, record) {
@@ -161,14 +183,31 @@ class PasskeyService {
161
183
  requireUserVerification: true
162
184
  });
163
185
  if (!result.verified || !result.registrationInfo) {
186
+ if (!result.verified) {
187
+ const err = result.error ?? result;
188
+ this.logger.error?.('Passkey registration verification failed', err);
189
+ }
164
190
  return { verified: false };
165
191
  }
166
192
  const registrationInfo = result.registrationInfo;
193
+ const attestationResponse = params.response?.response;
194
+ const credentialIdPrimary = toBufferOrNull(registrationInfo.credentialID);
195
+ const credentialIdFallback = toBufferOrNull(params.response?.id);
196
+ const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0 ? credentialIdPrimary : credentialIdFallback;
197
+ const publicKeyPrimary = toBufferOrNull(registrationInfo.credentialPublicKey);
198
+ const publicKeyFallback = toBufferOrNull(attestationResponse?.publicKey);
199
+ const publicKey = publicKeyPrimary && publicKeyPrimary.length > 0 ? publicKeyPrimary : publicKeyFallback;
200
+ if (!credentialId || credentialId.length === 0) {
201
+ return { verified: false, message: 'Missing credential id in registration response' };
202
+ }
203
+ if (!publicKey || publicKey.length === 0) {
204
+ return { verified: false, message: 'Missing public key in registration response' };
205
+ }
167
206
  await this.adapter.saveCredential({
168
207
  userId: user.id,
169
- credentialId: toBuffer(registrationInfo.credentialID),
170
- publicKey: toBuffer(registrationInfo.credentialPublicKey),
171
- counter: registrationInfo.counter,
208
+ credentialId,
209
+ publicKey,
210
+ counter: registrationInfo.counter ?? 0,
172
211
  transports: sanitizeTransports(params.response.transports),
173
212
  backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
174
213
  deviceType: registrationInfo.credentialDeviceType
@@ -204,6 +243,8 @@ class PasskeyService {
204
243
  requireUserVerification: true
205
244
  });
206
245
  if (!result.verified) {
246
+ const err = result.error ?? result;
247
+ this.logger.error?.('Passkey authentication verification failed', err);
207
248
  return { verified: false };
208
249
  }
209
250
  await this.adapter.updateCredentialCounter(credential.credentialId, result.authenticationInfo.newCounter);
@@ -13,7 +13,7 @@ import type { AuthAdapter, AuthIdentifier } from './auth-api/types.js';
13
13
  import type { OAuthStore } from './oauth/base.js';
14
14
  import type { AuthCodeData, AuthCodeRequest, OAuthClient } from './oauth/types.js';
15
15
  import type { PasskeyService } from './passkey/service.js';
16
- import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
16
+ import type { PasskeyChallenge, PasskeyChallengeParams, StoredPasskeyCredential, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
17
17
  import type { Token } from './token/types.js';
18
18
  import type { UserStore } from './user/base.js';
19
19
  import type { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
@@ -139,6 +139,8 @@ export declare class ApiServer {
139
139
  private ensureUserStore;
140
140
  private ensureTokenStore;
141
141
  private ensurePasskeyService;
142
+ listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
143
+ deletePasskeyCredential(credentialId: Buffer | string): Promise<boolean>;
142
144
  private ensureOAuthStore;
143
145
  getUser(identifier: AuthIdentifier): Promise<any | null>;
144
146
  getUserPasswordHash(user: any): string;
@@ -440,6 +440,12 @@ export class ApiServer {
440
440
  }
441
441
  return this.passkeyServiceAdapter;
442
442
  }
443
+ async listUserCredentials(userId) {
444
+ return this.ensurePasskeyService().listUserCredentials(userId);
445
+ }
446
+ async deletePasskeyCredential(credentialId) {
447
+ return this.ensurePasskeyService().deleteCredential(credentialId);
448
+ }
443
449
  ensureOAuthStore() {
444
450
  if (!this.oauthStoreAdapter) {
445
451
  throw new Error('OAuth store is not configured');
@@ -31,6 +31,26 @@ function toBuffer(value) {
31
31
  const view = value instanceof Uint8Array ? value : new Uint8Array(value);
32
32
  return Buffer.from(view);
33
33
  }
34
+ function toBufferOrNull(value) {
35
+ if (!value) {
36
+ return null;
37
+ }
38
+ if (Buffer.isBuffer(value)) {
39
+ return value;
40
+ }
41
+ if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
42
+ return toBuffer(value);
43
+ }
44
+ if (typeof value === 'string') {
45
+ try {
46
+ return fromBase64Url(value);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ return null;
53
+ }
34
54
  export class PasskeyService {
35
55
  constructor(config, adapter, logger = console) {
36
56
  this.config = config;
@@ -110,7 +130,8 @@ export class PasskeyService {
110
130
  return {
111
131
  challenge: options.challenge,
112
132
  expiresAt: expiresAt.toISOString(),
113
- userId: user.id
133
+ userId: user.id,
134
+ publicKey: options
114
135
  };
115
136
  }
116
137
  async createAuthenticationChallenge(params, metadata) {
@@ -139,7 +160,8 @@ export class PasskeyService {
139
160
  return {
140
161
  challenge: options.challenge,
141
162
  expiresAt: expiresAt.toISOString(),
142
- userId: user.id
163
+ userId: user.id,
164
+ publicKey: options
143
165
  };
144
166
  }
145
167
  async verifyRegistration(params, record) {
@@ -158,14 +180,31 @@ export class PasskeyService {
158
180
  requireUserVerification: true
159
181
  });
160
182
  if (!result.verified || !result.registrationInfo) {
183
+ if (!result.verified) {
184
+ const err = result.error ?? result;
185
+ this.logger.error?.('Passkey registration verification failed', err);
186
+ }
161
187
  return { verified: false };
162
188
  }
163
189
  const registrationInfo = result.registrationInfo;
190
+ const attestationResponse = params.response?.response;
191
+ const credentialIdPrimary = toBufferOrNull(registrationInfo.credentialID);
192
+ const credentialIdFallback = toBufferOrNull(params.response?.id);
193
+ const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0 ? credentialIdPrimary : credentialIdFallback;
194
+ const publicKeyPrimary = toBufferOrNull(registrationInfo.credentialPublicKey);
195
+ const publicKeyFallback = toBufferOrNull(attestationResponse?.publicKey);
196
+ const publicKey = publicKeyPrimary && publicKeyPrimary.length > 0 ? publicKeyPrimary : publicKeyFallback;
197
+ if (!credentialId || credentialId.length === 0) {
198
+ return { verified: false, message: 'Missing credential id in registration response' };
199
+ }
200
+ if (!publicKey || publicKey.length === 0) {
201
+ return { verified: false, message: 'Missing public key in registration response' };
202
+ }
164
203
  await this.adapter.saveCredential({
165
204
  userId: user.id,
166
- credentialId: toBuffer(registrationInfo.credentialID),
167
- publicKey: toBuffer(registrationInfo.credentialPublicKey),
168
- counter: registrationInfo.counter,
205
+ credentialId,
206
+ publicKey,
207
+ counter: registrationInfo.counter ?? 0,
169
208
  transports: sanitizeTransports(params.response.transports),
170
209
  backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
171
210
  deviceType: registrationInfo.credentialDeviceType
@@ -201,6 +240,8 @@ export class PasskeyService {
201
240
  requireUserVerification: true
202
241
  });
203
242
  if (!result.verified) {
243
+ const err = result.error ?? result;
244
+ this.logger.error?.('Passkey authentication verification failed', err);
204
245
  return { verified: false };
205
246
  }
206
247
  await this.adapter.updateCredentialCounter(credential.credentialId, result.authenticationInfo.newCounter);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/api-server-base",
3
- "version": "2.0.0-beta.4",
3
+ "version": "2.0.0-beta.6",
4
4
  "description": "Api Server Skeleton / Base Class",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",
@@ -30,6 +30,8 @@
30
30
  "build:esm": "tsc --project tsconfig/tsconfig.esm.json",
31
31
  "build": "node scripts/run-builds.cjs",
32
32
  "test": "vitest run",
33
+ "test:unit": "vitest run tests/unit",
34
+ "test:functional": "vitest run tests/functional",
33
35
  "test:watch": "vitest --watch",
34
36
  "prepublishOnly": "node scripts/run-builds.cjs",
35
37
  "lint": "eslint --ext .js,.ts,.vue ./",