@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
@@ -1,4 +1,3 @@
1
- // Numeric database id or lookup string such as username/email.
2
1
  // Handy base you can extend when wiring a real storage adapter. Every method
3
2
  // throws by default so unimplemented hooks fail loudly.
4
3
  export class BaseAuthStorage {
@@ -33,8 +32,9 @@ export class BaseAuthStorage {
33
32
  throw new Error('Auth storage not configured');
34
33
  }
35
34
  // Override to look up a stored token by query
36
- async getToken(query) {
35
+ async getToken(query, opts) {
37
36
  void query;
37
+ void opts;
38
38
  return null;
39
39
  }
40
40
  // Override to remove stored tokens that match the query
@@ -0,0 +1,29 @@
1
+ import type { AuthCodeData, AuthCodeRequest, OAuthClient } from '../oauth/types.js';
2
+ import type { PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from '../passkey/types.js';
3
+ import type { Token } from '../token/types.js';
4
+ export type AuthIdentifier = string | number;
5
+ export interface AuthStorage<UserRow, SafeUser> {
6
+ getUser(identifier: AuthIdentifier): Promise<UserRow | null>;
7
+ getUserPasswordHash(user: UserRow): string;
8
+ getUserId(user: UserRow): AuthIdentifier;
9
+ filterUser(user: UserRow): SafeUser;
10
+ verifyPassword(password: string, hash: string): Promise<boolean>;
11
+ storeToken(data: Token): Promise<void>;
12
+ getToken(query: Partial<Token>, opts?: {
13
+ includeExpired?: boolean;
14
+ }): Promise<Token | null>;
15
+ deleteToken(query: Partial<Token>): Promise<number>;
16
+ updateToken?(updates: Partial<Token> & {
17
+ refreshToken: string;
18
+ }): Promise<boolean>;
19
+ createPasskeyChallenge?(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
20
+ verifyPasskeyResponse?(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
21
+ getClient?(clientId: string): Promise<OAuthClient | null>;
22
+ verifyClientSecret?(client: OAuthClient, clientSecret: string | null): Promise<boolean>;
23
+ createAuthCode?(request: AuthCodeRequest): Promise<AuthCodeData>;
24
+ consumeAuthCode?(code: string, clientId: string): Promise<AuthCodeData | null>;
25
+ canImpersonate?(params: {
26
+ realUserId: AuthIdentifier;
27
+ effectiveUserId: AuthIdentifier;
28
+ }): Promise<boolean>;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,32 @@
1
1
  export { default as ApiServer } from './api-server-base.js';
2
2
  export { ApiError } from './api-server-base.js';
3
3
  export { ApiModule } from './api-module.js';
4
- export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, RequestWithStuff } from './api-server-base.js';
5
- export type { AuthIdentifier, AuthTokenMetadata, AuthTokenData, AuthTokenQuery, AuthTokenPair, AuthTokenPayload, PasskeyChallengeParams, PasskeyChallenge, PasskeyVerificationParams, PasskeyVerificationResult, OAuthClient, AuthCodeData, AuthCodeRequest, AuthStorage } from './auth-storage.js';
6
- export type { AuthProviderModule } from './auth-module.js';
7
- export { nullAuthStorage, BaseAuthStorage } from './auth-storage.js';
8
- export { nullAuthModule, BaseAuthModule } from './auth-module.js';
4
+ export type { ApiErrorParams, ApiHandler, ApiKey, ApiServerConf, ApiRequest, ApiRoute, ApiAuthType, ApiAuthClass, ApiTokenData, ExtendedReq } from './api-server-base.js';
5
+ export type { AuthIdentifier, AuthStorage } from './auth-api/types.js';
6
+ export type { Token, TokenPair, TokenStatus } from './token/types.js';
7
+ export type { JwtSignResult, JwtVerifyResult, JwtDecodeResult } from './token/base.js';
8
+ export type { OAuthClient, AuthCodeData, AuthCodeRequest } from './oauth/types.js';
9
+ export type { AuthProviderModule } from './auth-api/module.js';
10
+ export { nullAuthStorage, BaseAuthStorage } from './auth-api/storage.js';
11
+ export { nullAuthModule, BaseAuthModule } from './auth-api/module.js';
12
+ export { AuthStorageAdapter } from './auth-api/compat-auth-storage.js';
13
+ export { MemAuthStore } from './auth-api/mem-auth-store.js';
14
+ export { SqlAuthStore } from './auth-api/sql-auth-store.js';
15
+ export { default as AuthModule } from './auth-api/auth-module.js';
16
+ export type { OAuthStartParams, OAuthStartResult, OAuthCallbackParams, OAuthCallbackResult } from './oauth/types.js';
17
+ export type { BcryptHasherOptions, CreateUserInput, UpdateUserInput, PublicUserMapper } from './user/types.js';
18
+ export { UserStore } from './user/base.js';
19
+ export { MemoryUserStore } from './user/memory.js';
20
+ export { SequelizeUserStore } from './user/sequelize.js';
21
+ export type { MemoryUserAttributes, MemoryUserStoreOptions } from './user/memory.js';
22
+ export { TokenStore } from './token/base.js';
23
+ export { MemoryTokenStore } from './token/memory.js';
24
+ export { SequelizeTokenStore } from './token/sequelize.js';
25
+ export { PasskeyService } from './passkey/service.js';
26
+ export { PasskeyStore } from './passkey/base.js';
27
+ export { MemoryPasskeyStore } from './passkey/memory.js';
28
+ export { SequelizePasskeyStore } from './passkey/sequelize.js';
29
+ export type { PasskeyServiceConfig, PasskeyChallengeRecord, PasskeyUserDescriptor, StoredPasskeyCredential, PasskeyChallenge, PasskeyChallengeParams, PasskeyVerificationParams, PasskeyVerificationResult } from './passkey/types.js';
30
+ export { OAuthStore } from './oauth/base.js';
31
+ export { MemoryOAuthStore } from './oauth/memory.js';
32
+ export { SequelizeOAuthStore } from './oauth/sequelize.js';
package/dist/esm/index.js CHANGED
@@ -1,5 +1,22 @@
1
1
  export { default as ApiServer } from './api-server-base.js';
2
2
  export { ApiError } from './api-server-base.js';
3
3
  export { ApiModule } from './api-module.js';
4
- export { nullAuthStorage, BaseAuthStorage } from './auth-storage.js';
5
- export { nullAuthModule, BaseAuthModule } from './auth-module.js';
4
+ export { nullAuthStorage, BaseAuthStorage } from './auth-api/storage.js';
5
+ export { nullAuthModule, BaseAuthModule } from './auth-api/module.js';
6
+ export { AuthStorageAdapter } from './auth-api/compat-auth-storage.js';
7
+ export { MemAuthStore } from './auth-api/mem-auth-store.js';
8
+ export { SqlAuthStore } from './auth-api/sql-auth-store.js';
9
+ export { default as AuthModule } from './auth-api/auth-module.js';
10
+ export { UserStore } from './user/base.js';
11
+ export { MemoryUserStore } from './user/memory.js';
12
+ export { SequelizeUserStore } from './user/sequelize.js';
13
+ export { TokenStore } from './token/base.js';
14
+ export { MemoryTokenStore } from './token/memory.js';
15
+ export { SequelizeTokenStore } from './token/sequelize.js';
16
+ export { PasskeyService } from './passkey/service.js';
17
+ export { PasskeyStore } from './passkey/base.js';
18
+ export { MemoryPasskeyStore } from './passkey/memory.js';
19
+ export { SequelizePasskeyStore } from './passkey/sequelize.js';
20
+ export { OAuthStore } from './oauth/base.js';
21
+ export { MemoryOAuthStore } from './oauth/memory.js';
22
+ export { SequelizeOAuthStore } from './oauth/sequelize.js';
@@ -0,0 +1,10 @@
1
+ import type { AuthCode, OAuthClient } from './types.js';
2
+ export declare abstract class OAuthStore {
3
+ abstract getClient(clientId: string): Promise<OAuthClient | null>;
4
+ abstract createClient(input: OAuthClient): Promise<OAuthClient>;
5
+ abstract verifyClientSecret(clientId: string, secret: string | null): Promise<boolean>;
6
+ abstract createAuthCode(code: AuthCode): Promise<void>;
7
+ abstract consumeAuthCode(code: string): Promise<AuthCode | null>;
8
+ abstract close(): Promise<void>;
9
+ }
10
+ export type { OAuthClient, AuthCode } from './types.js';
@@ -0,0 +1,2 @@
1
+ export class OAuthStore {
2
+ }
@@ -0,0 +1,16 @@
1
+ import { OAuthStore, type AuthCode, type OAuthClient } from './base.js';
2
+ export interface MemoryOAuthStoreOptions {
3
+ bcryptRounds?: number;
4
+ }
5
+ export declare class MemoryOAuthStore extends OAuthStore {
6
+ private readonly clients;
7
+ private readonly codes;
8
+ private readonly bcryptRounds;
9
+ constructor(options?: MemoryOAuthStoreOptions);
10
+ getClient(clientId: string): Promise<OAuthClient | null>;
11
+ createClient(input: OAuthClient): Promise<OAuthClient>;
12
+ verifyClientSecret(clientId: string, secret: string | null): Promise<boolean>;
13
+ createAuthCode(code: AuthCode): Promise<void>;
14
+ consumeAuthCode(code: string): Promise<AuthCode | null>;
15
+ close(): Promise<void>;
16
+ }
@@ -0,0 +1,92 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import { OAuthStore } from './base.js';
3
+ function cloneClient(client) {
4
+ if (!client) {
5
+ return null;
6
+ }
7
+ return {
8
+ clientId: client.clientId,
9
+ clientSecret: client.clientSecret,
10
+ name: client.name,
11
+ redirectUris: [...client.redirectUris],
12
+ scope: client.scope ? [...client.scope] : undefined,
13
+ metadata: client.metadata ? { ...client.metadata } : undefined,
14
+ firstParty: client.firstParty
15
+ };
16
+ }
17
+ function cloneCode(code) {
18
+ return {
19
+ ...code,
20
+ scope: code.scope ? [...code.scope] : undefined,
21
+ expiresAt: new Date(code.expiresAt),
22
+ metadata: code.metadata ? { ...code.metadata } : undefined
23
+ };
24
+ }
25
+ function normalizeUserId(identifier) {
26
+ if (typeof identifier === 'number' && Number.isFinite(identifier)) {
27
+ return identifier;
28
+ }
29
+ if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
30
+ return Number(identifier);
31
+ }
32
+ throw new Error(`Unable to normalise user identifier: ${identifier}`);
33
+ }
34
+ export class MemoryOAuthStore extends OAuthStore {
35
+ constructor(options = {}) {
36
+ super();
37
+ this.clients = new Map();
38
+ this.codes = new Map();
39
+ this.bcryptRounds = options.bcryptRounds ?? 12;
40
+ }
41
+ async getClient(clientId) {
42
+ return cloneClient(this.clients.get(clientId));
43
+ }
44
+ async createClient(input) {
45
+ const clientSecret = input.clientSecret ? await bcrypt.hash(input.clientSecret, this.bcryptRounds) : '';
46
+ const stored = {
47
+ clientId: input.clientId,
48
+ clientSecret,
49
+ name: input.name,
50
+ redirectUris: [...input.redirectUris],
51
+ scope: input.scope ? [...input.scope] : undefined,
52
+ metadata: input.metadata ? { ...input.metadata } : undefined,
53
+ firstParty: input.firstParty
54
+ };
55
+ this.clients.set(stored.clientId, stored);
56
+ return cloneClient(stored);
57
+ }
58
+ async verifyClientSecret(clientId, secret) {
59
+ const client = this.clients.get(clientId);
60
+ if (!client) {
61
+ return false;
62
+ }
63
+ if (!client.clientSecret) {
64
+ return !secret || secret.length === 0;
65
+ }
66
+ if (!secret) {
67
+ return false;
68
+ }
69
+ return bcrypt.compare(secret, client.clientSecret);
70
+ }
71
+ async createAuthCode(code) {
72
+ const record = {
73
+ ...code,
74
+ userId: normalizeUserId(code.userId),
75
+ scope: code.scope ? [...code.scope] : undefined,
76
+ expiresAt: code.expiresAt,
77
+ metadata: code.metadata ? { ...code.metadata } : undefined
78
+ };
79
+ this.codes.set(record.code, record);
80
+ }
81
+ async consumeAuthCode(code) {
82
+ const record = this.codes.get(code);
83
+ if (!record) {
84
+ return null;
85
+ }
86
+ this.codes.delete(code);
87
+ return cloneCode(record);
88
+ }
89
+ async close() {
90
+ return;
91
+ }
92
+ }
@@ -0,0 +1,45 @@
1
+ import { Model, type Optional, type Sequelize } from 'sequelize';
2
+ export interface OAuthClientAttributes {
3
+ client_id: string;
4
+ client_secret: string;
5
+ name: string | null;
6
+ redirect_uris: string;
7
+ scope: string;
8
+ metadata: string | null;
9
+ first_party: boolean;
10
+ }
11
+ export type OAuthClientCreationAttributes = Optional<OAuthClientAttributes, 'client_secret' | 'name' | 'scope' | 'metadata' | 'first_party'>;
12
+ export declare class OAuthClientModel extends Model<OAuthClientAttributes, OAuthClientCreationAttributes> implements OAuthClientAttributes {
13
+ client_id: string;
14
+ client_secret: string;
15
+ name: string | null;
16
+ redirect_uris: string;
17
+ scope: string;
18
+ metadata: string | null;
19
+ first_party: boolean;
20
+ }
21
+ export declare function initOAuthClientModel(sequelize: Sequelize): typeof OAuthClientModel;
22
+ export interface OAuthCodeAttributes {
23
+ code: string;
24
+ client_id: string;
25
+ user_id: number;
26
+ redirect_uri: string;
27
+ scope: string;
28
+ code_challenge: string | null;
29
+ code_challenge_method: 'plain' | 'S256' | null;
30
+ expires: Date;
31
+ metadata: string | null;
32
+ }
33
+ export type OAuthCodeCreationAttributes = Optional<OAuthCodeAttributes, 'code_challenge' | 'code_challenge_method' | 'metadata'>;
34
+ export declare class OAuthCodeModel extends Model<OAuthCodeAttributes, OAuthCodeCreationAttributes> implements OAuthCodeAttributes {
35
+ code: string;
36
+ client_id: string;
37
+ user_id: number;
38
+ redirect_uri: string;
39
+ scope: string;
40
+ code_challenge: string | null;
41
+ code_challenge_method: 'plain' | 'S256' | null;
42
+ expires: Date;
43
+ metadata: string | null;
44
+ }
45
+ export declare function initOAuthCodeModel(sequelize: Sequelize): typeof OAuthCodeModel;
@@ -0,0 +1,51 @@
1
+ import { DataTypes, Model } from 'sequelize';
2
+ const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
3
+ function integerIdType(sequelize) {
4
+ return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
5
+ }
6
+ function tableOptions(sequelize, tableName, extra) {
7
+ const opts = { sequelize, tableName };
8
+ if (extra) {
9
+ Object.assign(opts, extra);
10
+ }
11
+ if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
12
+ opts.charset = 'utf8mb4';
13
+ opts.collate = 'utf8mb4_unicode_ci';
14
+ }
15
+ return opts;
16
+ }
17
+ export class OAuthClientModel extends Model {
18
+ }
19
+ export function initOAuthClientModel(sequelize) {
20
+ OAuthClientModel.init({
21
+ client_id: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
22
+ client_secret: { type: DataTypes.STRING(255), allowNull: false, defaultValue: '' },
23
+ name: { type: DataTypes.STRING(128), allowNull: true, defaultValue: null },
24
+ redirect_uris: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
25
+ scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
26
+ metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null },
27
+ first_party: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
28
+ }, {
29
+ ...tableOptions(sequelize, 'oauth_clients', { timestamps: false })
30
+ });
31
+ return OAuthClientModel;
32
+ }
33
+ export class OAuthCodeModel extends Model {
34
+ }
35
+ export function initOAuthCodeModel(sequelize) {
36
+ const idType = integerIdType(sequelize);
37
+ OAuthCodeModel.init({
38
+ code: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
39
+ client_id: { type: DataTypes.STRING(128), allowNull: false },
40
+ user_id: { type: idType, allowNull: false },
41
+ redirect_uri: { type: DataTypes.TEXT, allowNull: false },
42
+ scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
43
+ code_challenge: { type: DataTypes.STRING(255), allowNull: true, defaultValue: null },
44
+ code_challenge_method: { type: DataTypes.STRING(10), allowNull: true, defaultValue: null },
45
+ expires: { type: DataTypes.DATE, allowNull: false },
46
+ metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null }
47
+ }, {
48
+ ...tableOptions(sequelize, 'oauth_codes', { timestamps: false })
49
+ });
50
+ return OAuthCodeModel;
51
+ }
@@ -0,0 +1,68 @@
1
+ import { Model, type Optional, type Sequelize } from 'sequelize';
2
+ import { OAuthStore, type AuthCode, type OAuthClient } from './base.js';
3
+ export interface OAuthClientAttributes {
4
+ client_id: string;
5
+ client_secret: string;
6
+ name: string | null;
7
+ redirect_uris: string;
8
+ scope: string;
9
+ metadata: string | null;
10
+ first_party: boolean;
11
+ }
12
+ export type OAuthClientCreationAttributes = Optional<OAuthClientAttributes, 'client_secret' | 'name' | 'scope' | 'metadata' | 'first_party'>;
13
+ export declare class OAuthClientModel extends Model<OAuthClientAttributes, OAuthClientCreationAttributes> implements OAuthClientAttributes {
14
+ client_id: string;
15
+ client_secret: string;
16
+ name: string | null;
17
+ redirect_uris: string;
18
+ scope: string;
19
+ metadata: string | null;
20
+ first_party: boolean;
21
+ }
22
+ export declare function initOAuthClientModel(sequelize: Sequelize): typeof OAuthClientModel;
23
+ export interface OAuthCodeAttributes {
24
+ code: string;
25
+ client_id: string;
26
+ user_id: number;
27
+ redirect_uri: string;
28
+ scope: string;
29
+ code_challenge: string | null;
30
+ code_challenge_method: 'plain' | 'S256' | null;
31
+ expires: Date;
32
+ metadata: string | null;
33
+ }
34
+ export type OAuthCodeCreationAttributes = Optional<OAuthCodeAttributes, 'code_challenge' | 'code_challenge_method' | 'metadata'>;
35
+ export declare class OAuthCodeModel extends Model<OAuthCodeAttributes, OAuthCodeCreationAttributes> implements OAuthCodeAttributes {
36
+ code: string;
37
+ client_id: string;
38
+ user_id: number;
39
+ redirect_uri: string;
40
+ scope: string;
41
+ code_challenge: string | null;
42
+ code_challenge_method: 'plain' | 'S256' | null;
43
+ expires: Date;
44
+ metadata: string | null;
45
+ }
46
+ export declare function initOAuthCodeModel(sequelize: Sequelize): typeof OAuthCodeModel;
47
+ export interface SequelizeOAuthStoreOptions {
48
+ sequelize: Sequelize;
49
+ clientModel?: typeof OAuthClientModel;
50
+ codeModel?: typeof OAuthCodeModel;
51
+ clientModelFactory?: (sequelize: Sequelize) => typeof OAuthClientModel;
52
+ codeModelFactory?: (sequelize: Sequelize) => typeof OAuthCodeModel;
53
+ bcryptRounds?: number;
54
+ }
55
+ export declare class SequelizeOAuthStore extends OAuthStore {
56
+ private readonly clients;
57
+ private readonly codes;
58
+ private readonly bcryptRounds;
59
+ constructor(options: SequelizeOAuthStoreOptions);
60
+ getClient(clientId: string): Promise<OAuthClient | null>;
61
+ createClient(input: OAuthClient): Promise<OAuthClient>;
62
+ verifyClientSecret(clientId: string, clientSecret: string | null): Promise<boolean>;
63
+ createAuthCode(code: AuthCode): Promise<void>;
64
+ consumeAuthCode(code: string): Promise<AuthCode | null>;
65
+ close(): Promise<void>;
66
+ private toOAuthClient;
67
+ private toAuthCode;
68
+ }
@@ -0,0 +1,199 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import { DataTypes, Model } from 'sequelize';
3
+ import { OAuthStore } from './base.js';
4
+ const DIALECTS_SUPPORTING_UNSIGNED = new Set(['mysql', 'mariadb']);
5
+ function integerIdType(sequelize) {
6
+ return DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect()) ? DataTypes.INTEGER.UNSIGNED : DataTypes.INTEGER;
7
+ }
8
+ function tableOptions(sequelize, tableName, extra) {
9
+ const opts = { sequelize, tableName };
10
+ if (extra) {
11
+ Object.assign(opts, extra);
12
+ }
13
+ if (DIALECTS_SUPPORTING_UNSIGNED.has(sequelize.getDialect())) {
14
+ opts.charset = 'utf8mb4';
15
+ opts.collate = 'utf8mb4_unicode_ci';
16
+ }
17
+ return opts;
18
+ }
19
+ export class OAuthClientModel extends Model {
20
+ }
21
+ export function initOAuthClientModel(sequelize) {
22
+ OAuthClientModel.init({
23
+ client_id: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
24
+ client_secret: { type: DataTypes.STRING(255), allowNull: false, defaultValue: '' },
25
+ name: { type: DataTypes.STRING(128), allowNull: true, defaultValue: null },
26
+ redirect_uris: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
27
+ scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
28
+ metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null },
29
+ first_party: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
30
+ }, tableOptions(sequelize, 'oauth_clients', { timestamps: false }));
31
+ return OAuthClientModel;
32
+ }
33
+ export class OAuthCodeModel extends Model {
34
+ }
35
+ export function initOAuthCodeModel(sequelize) {
36
+ const idType = integerIdType(sequelize);
37
+ OAuthCodeModel.init({
38
+ code: { type: DataTypes.STRING(128), allowNull: false, primaryKey: true },
39
+ client_id: { type: DataTypes.STRING(128), allowNull: false },
40
+ user_id: { type: idType, allowNull: false },
41
+ redirect_uri: { type: DataTypes.TEXT, allowNull: false },
42
+ scope: { type: DataTypes.TEXT, allowNull: false, defaultValue: '[]' },
43
+ code_challenge: { type: DataTypes.STRING(255), allowNull: true, defaultValue: null },
44
+ code_challenge_method: { type: DataTypes.STRING(10), allowNull: true, defaultValue: null },
45
+ expires: { type: DataTypes.DATE, allowNull: false },
46
+ metadata: { type: DataTypes.TEXT, allowNull: true, defaultValue: null }
47
+ }, tableOptions(sequelize, 'oauth_codes', { timestamps: false }));
48
+ return OAuthCodeModel;
49
+ }
50
+ function encodeStringArray(values) {
51
+ return JSON.stringify(values ?? []);
52
+ }
53
+ function decodeStringArray(raw) {
54
+ if (!raw) {
55
+ return [];
56
+ }
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ if (Array.isArray(parsed)) {
60
+ return parsed.filter((entry) => typeof entry === 'string' && entry.length > 0);
61
+ }
62
+ }
63
+ catch {
64
+ // ignore malformed values
65
+ }
66
+ return raw
67
+ .split(/\s+/)
68
+ .map((entry) => entry.trim())
69
+ .filter((entry) => entry.length > 0);
70
+ }
71
+ function serializeMetadata(metadata) {
72
+ if (!metadata) {
73
+ return null;
74
+ }
75
+ return JSON.stringify(metadata);
76
+ }
77
+ function parseMetadata(raw) {
78
+ if (!raw) {
79
+ return undefined;
80
+ }
81
+ try {
82
+ const parsed = JSON.parse(raw);
83
+ if (parsed && typeof parsed === 'object') {
84
+ return parsed;
85
+ }
86
+ }
87
+ catch {
88
+ // ignore
89
+ }
90
+ return undefined;
91
+ }
92
+ function normalizeUserId(identifier) {
93
+ if (typeof identifier === 'number' && Number.isFinite(identifier)) {
94
+ return identifier;
95
+ }
96
+ if (typeof identifier === 'string' && /^\d+$/.test(identifier)) {
97
+ return Number(identifier);
98
+ }
99
+ throw new Error(`Unable to normalise user identifier: ${identifier}`);
100
+ }
101
+ export class SequelizeOAuthStore extends OAuthStore {
102
+ constructor(options) {
103
+ super();
104
+ if (!options?.sequelize) {
105
+ throw new Error('SequelizeOAuthStore requires an initialised Sequelize instance');
106
+ }
107
+ this.clients = options.clientModel ?? (options.clientModelFactory ?? initOAuthClientModel)(options.sequelize);
108
+ this.codes = options.codeModel ?? (options.codeModelFactory ?? initOAuthCodeModel)(options.sequelize);
109
+ this.bcryptRounds = options.bcryptRounds ?? 12;
110
+ }
111
+ async getClient(clientId) {
112
+ const model = await this.clients.findByPk(clientId);
113
+ return model ? this.toOAuthClient(model) : null;
114
+ }
115
+ async createClient(input) {
116
+ const existing = await this.clients.findByPk(input.clientId);
117
+ const hashedSecret = input.clientSecret !== undefined && input.clientSecret !== null
118
+ ? await bcrypt.hash(input.clientSecret, this.bcryptRounds)
119
+ : (existing?.client_secret ?? '');
120
+ const redirectUris = input.redirectUris ?? (existing ? decodeStringArray(existing.redirect_uris) : undefined);
121
+ const scope = input.scope ?? (existing ? decodeStringArray(existing.scope) : undefined);
122
+ const metadata = input.metadata ?? (existing ? parseMetadata(existing.metadata) : undefined);
123
+ await this.clients.upsert({
124
+ client_id: input.clientId,
125
+ client_secret: hashedSecret,
126
+ name: input.name ?? existing?.name ?? null,
127
+ redirect_uris: encodeStringArray(redirectUris),
128
+ scope: encodeStringArray(scope),
129
+ metadata: serializeMetadata(metadata),
130
+ first_party: input.firstParty ?? existing?.first_party ?? false
131
+ });
132
+ const model = await this.clients.findByPk(input.clientId);
133
+ if (!model) {
134
+ throw new Error(`Unable to persist OAuth client ${input.clientId}`);
135
+ }
136
+ return this.toOAuthClient(model);
137
+ }
138
+ async verifyClientSecret(clientId, clientSecret) {
139
+ const model = await this.clients.findByPk(clientId);
140
+ if (!model) {
141
+ return false;
142
+ }
143
+ if (!model.client_secret) {
144
+ return !clientSecret || clientSecret.length === 0;
145
+ }
146
+ if (!clientSecret) {
147
+ return false;
148
+ }
149
+ return bcrypt.compare(clientSecret, model.client_secret);
150
+ }
151
+ async createAuthCode(code) {
152
+ await this.codes.create({
153
+ code: code.code,
154
+ client_id: code.clientId,
155
+ user_id: normalizeUserId(code.userId),
156
+ redirect_uri: code.redirectUri ?? '',
157
+ scope: encodeStringArray(code.scope),
158
+ code_challenge: code.codeChallenge ?? null,
159
+ code_challenge_method: code.codeChallengeMethod ?? null,
160
+ expires: code.expiresAt,
161
+ metadata: serializeMetadata(code.metadata)
162
+ });
163
+ }
164
+ async consumeAuthCode(code) {
165
+ const model = await this.codes.findByPk(code);
166
+ if (!model) {
167
+ return null;
168
+ }
169
+ await model.destroy();
170
+ return this.toAuthCode(model);
171
+ }
172
+ async close() {
173
+ return;
174
+ }
175
+ toOAuthClient(model) {
176
+ return {
177
+ clientId: model.client_id,
178
+ clientSecret: model.client_secret,
179
+ name: model.name ?? undefined,
180
+ redirectUris: decodeStringArray(model.redirect_uris),
181
+ scope: decodeStringArray(model.scope),
182
+ metadata: parseMetadata(model.metadata),
183
+ firstParty: model.first_party ?? false
184
+ };
185
+ }
186
+ toAuthCode(model) {
187
+ return {
188
+ code: model.code,
189
+ clientId: model.client_id,
190
+ userId: model.user_id,
191
+ redirectUri: model.redirect_uri,
192
+ scope: decodeStringArray(model.scope),
193
+ codeChallenge: model.code_challenge ?? undefined,
194
+ codeChallengeMethod: model.code_challenge_method ?? undefined,
195
+ expiresAt: model.expires,
196
+ metadata: parseMetadata(model.metadata)
197
+ };
198
+ }
199
+ }
@@ -0,0 +1,50 @@
1
+ import type { AuthIdentifier } from '../auth-api/types.js';
2
+ export interface OAuthClient {
3
+ clientId: string;
4
+ clientSecret?: string;
5
+ firstParty?: boolean;
6
+ metadata?: Record<string, unknown>;
7
+ name?: string;
8
+ redirectUris: string[];
9
+ scope?: string[];
10
+ }
11
+ export interface AuthCodeData {
12
+ code: string;
13
+ clientId: string;
14
+ codeChallenge?: string;
15
+ codeChallengeMethod?: 'plain' | 'S256';
16
+ expiresAt: Date;
17
+ metadata?: Record<string, unknown>;
18
+ redirectUri?: string;
19
+ scope?: string[];
20
+ userId: AuthIdentifier;
21
+ }
22
+ export type AuthCode = AuthCodeData;
23
+ export type AuthCodeRequest = Omit<AuthCodeData, 'code' | 'expiresAt'> & {
24
+ code?: string;
25
+ expiresInSeconds?: number;
26
+ };
27
+ export interface OAuthStartParams {
28
+ provider: string;
29
+ redirectUri?: string;
30
+ scope?: string | string[];
31
+ state?: string;
32
+ extras?: Record<string, unknown>;
33
+ }
34
+ export interface OAuthStartResult extends Record<string, unknown> {
35
+ url: string;
36
+ state?: string;
37
+ codeVerifier?: string;
38
+ }
39
+ export interface OAuthCallbackParams {
40
+ provider: string;
41
+ query: Record<string, string | string[]>;
42
+ body: Record<string, unknown>;
43
+ }
44
+ export interface OAuthCallbackResult<PublicUser> extends Record<string, unknown> {
45
+ user: PublicUser;
46
+ tokens?: {
47
+ accessToken: string;
48
+ refreshToken: string;
49
+ };
50
+ }
@@ -0,0 +1,2 @@
1
+ // OAuth-specific contracts kept alongside the OAuth module.
2
+ export {};