adonia 0.1.0
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/README.md +64 -0
- package/build/adonis_peers.d.ts +14 -0
- package/build/adonis_peers.js +56 -0
- package/build/cli.d.ts +2 -0
- package/build/cli.js +111 -0
- package/build/commands/add.d.ts +18 -0
- package/build/commands/add.js +246 -0
- package/build/commands/check.d.ts +4 -0
- package/build/commands/check.js +159 -0
- package/build/commands/diff.d.ts +2 -0
- package/build/commands/diff.js +74 -0
- package/build/commands/init.d.ts +6 -0
- package/build/commands/init.js +124 -0
- package/build/commands/list.d.ts +2 -0
- package/build/commands/list.js +34 -0
- package/build/config_merge.d.ts +10 -0
- package/build/config_merge.js +71 -0
- package/build/constants_merge.d.ts +4 -0
- package/build/constants_merge.js +85 -0
- package/build/copy.d.ts +8 -0
- package/build/copy.js +33 -0
- package/build/host.d.ts +15 -0
- package/build/host.js +85 -0
- package/build/registry/index.d.ts +26 -0
- package/build/registry/index.js +116 -0
- package/build/registry/plan.d.ts +53 -0
- package/build/registry/plan.js +159 -0
- package/build/schema.d.ts +136 -0
- package/build/schema.js +58 -0
- package/build/types.d.ts +21 -0
- package/build/types.js +1 -0
- package/build/wire.d.ts +16 -0
- package/build/wire.js +162 -0
- package/package.json +51 -0
- package/registry/README.md +86 -0
- package/registry/account/contracts/avatar_storage.ts +7 -0
- package/registry/account/contracts/index.ts +7 -0
- package/registry/account/contracts/types.ts +23 -0
- package/registry/account/events.ts +14 -0
- package/registry/account/module.json +31 -0
- package/registry/account/options.ts +11 -0
- package/registry/account/service.ts +155 -0
- package/registry/account/stubs/adapters/account.ts +21 -0
- package/registry/account/stubs/adapters/drive_avatar_storage.ts +15 -0
- package/registry/account/stubs/controllers/account_controller.ts +190 -0
- package/registry/account/stubs/routes/account.ts +53 -0
- package/registry/account/stubs/validators/account.ts +37 -0
- package/registry/account/tests/account_service.spec.ts +132 -0
- package/registry/account/tests/fakes/memory_avatar_storage.ts +12 -0
- package/registry/api/envelope.ts +66 -0
- package/registry/api/exception.ts +38 -0
- package/registry/api/exception_handler.ts +54 -0
- package/registry/api/module.json +27 -0
- package/registry/api/provider.ts +129 -0
- package/registry/api/service.ts +88 -0
- package/registry/api/tests/emit_safe.spec.ts +107 -0
- package/registry/api/tests/respond.spec.ts +40 -0
- package/registry/auth/contracts/index.ts +18 -0
- package/registry/auth/contracts/password_reset_store.ts +21 -0
- package/registry/auth/contracts/session_manager.ts +19 -0
- package/registry/auth/contracts/types.ts +32 -0
- package/registry/auth/events.ts +21 -0
- package/registry/auth/module.json +45 -0
- package/registry/auth/options.ts +23 -0
- package/registry/auth/service.ts +150 -0
- package/registry/auth/stubs/adapters/auth.ts +21 -0
- package/registry/auth/stubs/adapters/lucid_access_token_session.ts +33 -0
- package/registry/auth/stubs/adapters/lucid_password_reset_store.ts +68 -0
- package/registry/auth/stubs/adapters/lucid_user_store.ts +86 -0
- package/registry/auth/stubs/adapters/map_user.ts +19 -0
- package/registry/auth/stubs/controllers/auth_controller.ts +126 -0
- package/registry/auth/stubs/migrations/add_pending_email_to_users_table.ts +17 -0
- package/registry/auth/stubs/migrations/create_password_resets_table.ts +27 -0
- package/registry/auth/stubs/migrations/create_users_table.ts +31 -0
- package/registry/auth/stubs/models/password_reset.ts +29 -0
- package/registry/auth/stubs/models/user.ts +99 -0
- package/registry/auth/stubs/providers/vine_provider.ts +43 -0
- package/registry/auth/stubs/routes/auth.ts +36 -0
- package/registry/auth/stubs/start/limiter.ts +17 -0
- package/registry/auth/stubs/validators/auth.ts +48 -0
- package/registry/auth/stubs/validators/rules/exists.ts +21 -0
- package/registry/auth/stubs/validators/rules/unique.ts +21 -0
- package/registry/auth/stubs/validators/rules/valid_password.ts +20 -0
- package/registry/auth/stubs/validators/rules/valid_token.ts +27 -0
- package/registry/auth/stubs/validators/vine.d.ts +10 -0
- package/registry/auth/tests/auth_service.spec.ts +145 -0
- package/registry/auth/tests/auth_update_password.spec.ts +99 -0
- package/registry/auth/tests/fakes/index.ts +3 -0
- package/registry/auth/tests/fakes/memory_password_reset_store.ts +71 -0
- package/registry/auth/tests/fakes/memory_session_manager.ts +23 -0
- package/registry/auth/tests/fakes/memory_user_store.ts +89 -0
- package/registry/constants/exceptions.ts +64 -0
- package/registry/constants/index.ts +12 -0
- package/registry/constants/responseCodes.ts +87 -0
- package/registry/contracts/index.ts +5 -0
- package/registry/contracts/user.ts +78 -0
- package/registry/creem/contracts/creem_client.ts +34 -0
- package/registry/creem/contracts/index.ts +8 -0
- package/registry/creem/contracts/types.ts +30 -0
- package/registry/creem/events.ts +9 -0
- package/registry/creem/module.json +28 -0
- package/registry/creem/service.ts +87 -0
- package/registry/creem/stubs/adapters/creem.ts +23 -0
- package/registry/creem/stubs/adapters/creem_io_client.ts +70 -0
- package/registry/creem/tests/fakes/fake_creem_client.ts +68 -0
- package/registry/notification/contracts/app_notification_store.ts +8 -0
- package/registry/notification/contracts/index.ts +9 -0
- package/registry/notification/contracts/mail_transport.ts +7 -0
- package/registry/notification/contracts/types.ts +32 -0
- package/registry/notification/module.json +23 -0
- package/registry/notification/options.ts +11 -0
- package/registry/notification/service.ts +103 -0
- package/registry/notification/stubs/adapters/adonis_mail_transport.ts +15 -0
- package/registry/notification/stubs/adapters/lucid_app_notification_store.ts +21 -0
- package/registry/notification/stubs/adapters/notification.ts +36 -0
- package/registry/notification/stubs/migrations/create_notifications_table.ts +23 -0
- package/registry/notification/stubs/models/app_notification.ts +45 -0
- package/registry/notification/tests/fakes/memory_app_notification_store.ts +19 -0
- package/registry/notification/tests/fakes/memory_mail_transport.ts +16 -0
- package/registry/notification/tests/notification_service.spec.ts +63 -0
- package/registry/registry.json +4 -0
- package/registry/subscription/contracts/index.ts +9 -0
- package/registry/subscription/contracts/subscription_store.ts +15 -0
- package/registry/subscription/contracts/types.ts +39 -0
- package/registry/subscription/events.ts +13 -0
- package/registry/subscription/module.json +31 -0
- package/registry/subscription/options.ts +14 -0
- package/registry/subscription/service.ts +140 -0
- package/registry/subscription/stubs/adapters/lucid_subscription_store.ts +69 -0
- package/registry/subscription/stubs/adapters/subscription.ts +21 -0
- package/registry/subscription/stubs/migrations/create_subscriptions_table.ts +26 -0
- package/registry/subscription/stubs/migrations/subscriptions_user_creem_unique.ts +19 -0
- package/registry/subscription/stubs/models/subscription.ts +45 -0
- package/registry/subscription/tests/billing_service.spec.ts +189 -0
- package/registry/subscription/tests/fakes/memory_subscription_store.ts +63 -0
- package/registry/types/api.ts +41 -0
- package/registry/types/constants.ts +7 -0
- package/registry/types/index.ts +6 -0
- package/registry/types/models.ts +32 -0
- package/schema/adonia.schema.json +39 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { User } from '#modules/contracts/index'
|
|
2
|
+
import type {
|
|
3
|
+
SessionManager,
|
|
4
|
+
SessionResult,
|
|
5
|
+
} from '#modules/auth/contracts/index'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* In-memory SessionManager. Cookie-style hosts return {}.
|
|
9
|
+
*/
|
|
10
|
+
export class MemorySessionManager implements SessionManager {
|
|
11
|
+
logins: User[] = []
|
|
12
|
+
logouts = 0
|
|
13
|
+
result: SessionResult = {}
|
|
14
|
+
|
|
15
|
+
async login(user: User): Promise<SessionResult> {
|
|
16
|
+
this.logins.push(user)
|
|
17
|
+
return this.result
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async logout(): Promise<void> {
|
|
21
|
+
this.logouts++
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { RegisterUserInput, User, UserStore } from '#modules/contracts/index'
|
|
2
|
+
|
|
3
|
+
type StoredUser = User & { password: string }
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory UserStore for unit tests. Records saves for side-effect assertions.
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryUserStore implements UserStore {
|
|
9
|
+
users = new Map<number, StoredUser>()
|
|
10
|
+
saves: User[] = []
|
|
11
|
+
nextId = 1
|
|
12
|
+
|
|
13
|
+
async createEmailUser(data: RegisterUserInput): Promise<User> {
|
|
14
|
+
const user: StoredUser = {
|
|
15
|
+
id: this.nextId++,
|
|
16
|
+
email: data.email,
|
|
17
|
+
pendingEmail: null,
|
|
18
|
+
password: data.password,
|
|
19
|
+
authMethod: 'email',
|
|
20
|
+
firstName: data.firstName ?? data.fullName?.split(' ')[0] ?? '',
|
|
21
|
+
lastName: data.lastName ?? data.fullName?.split(' ').slice(1).join(' ') ?? '',
|
|
22
|
+
avatarKey: null,
|
|
23
|
+
emailVerifiedAt: null,
|
|
24
|
+
emailVerificationToken: null,
|
|
25
|
+
}
|
|
26
|
+
this.users.set(user.id, user)
|
|
27
|
+
return this.toDto(user)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async findOrFail(id: number): Promise<User> {
|
|
31
|
+
const user = this.users.get(id)
|
|
32
|
+
if (!user) throw new Error(`User ${id} not found`)
|
|
33
|
+
return this.toDto(user)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
37
|
+
const user = [...this.users.values()].find((row) => row.email === email)
|
|
38
|
+
return user ? this.toDto(user) : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async findByEmailOrFail(email: string): Promise<User> {
|
|
42
|
+
const user = await this.findByEmail(email)
|
|
43
|
+
if (!user) throw new Error(`User ${email} not found`)
|
|
44
|
+
return user
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async findByVerificationToken(token: string): Promise<User | null> {
|
|
48
|
+
const user = [...this.users.values()].find((row) => row.emailVerificationToken === token)
|
|
49
|
+
return user ? this.toDto(user) : null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async verifyCredentials(email: string, password: string): Promise<User> {
|
|
53
|
+
const user = [...this.users.values()].find(
|
|
54
|
+
(row) => row.email === email && row.password === password
|
|
55
|
+
)
|
|
56
|
+
if (!user) throw new Error('Invalid credentials')
|
|
57
|
+
return this.toDto(user)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async verifyPassword(userId: number, password: string): Promise<boolean> {
|
|
61
|
+
const user = this.users.get(userId)
|
|
62
|
+
return !!user && user.password === password
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async save(dto: User): Promise<User> {
|
|
66
|
+
const existing = this.users.get(dto.id)
|
|
67
|
+
if (!existing) throw new Error(`User ${dto.id} not found`)
|
|
68
|
+
const next: StoredUser = { ...existing, ...dto, password: existing.password }
|
|
69
|
+
this.users.set(dto.id, next)
|
|
70
|
+
this.saves.push(this.toDto(next))
|
|
71
|
+
return this.toDto(next)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async updatePassword(dto: User, password: string): Promise<User> {
|
|
75
|
+
const existing = this.users.get(dto.id)
|
|
76
|
+
if (!existing) throw new Error(`User ${dto.id} not found`)
|
|
77
|
+
existing.password = password
|
|
78
|
+
return this.toDto(existing)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async delete(userId: number): Promise<void> {
|
|
82
|
+
this.users.delete(userId)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private toDto(user: StoredUser): User {
|
|
86
|
+
const { password: _, ...dto } = user
|
|
87
|
+
return { ...dto }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { StatusCodeEntry } from '#modules/types/constants'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Exception codes for modules + documented host-facing codes.
|
|
5
|
+
* Hosts extend via defineCodes in app/constants/.
|
|
6
|
+
*
|
|
7
|
+
* Host-facing (not thrown by a module service today):
|
|
8
|
+
* - INVALID_CREDENTIALS, UNAUTHENTICATED, SESSION_EXPIRED — auth middleware
|
|
9
|
+
* - FORBIDDEN — authorization failures
|
|
10
|
+
* - TOO_MANY_ATTEMPTS, TOO_MANY_REQUESTS — rate limiting
|
|
11
|
+
* - PAYLOAD_TOO_LARGE — file upload limits (HTTP 413)
|
|
12
|
+
*/
|
|
13
|
+
export const exceptions = {
|
|
14
|
+
INVALID_CREDENTIALS: {
|
|
15
|
+
status: 401,
|
|
16
|
+
code: 'E_INVALID_CREDENTIALS',
|
|
17
|
+
},
|
|
18
|
+
UNAUTHENTICATED: {
|
|
19
|
+
status: 401,
|
|
20
|
+
code: 'E_UNAUTHENTICATED',
|
|
21
|
+
},
|
|
22
|
+
SESSION_EXPIRED: {
|
|
23
|
+
status: 401,
|
|
24
|
+
code: 'E_SESSION_EXPIRED',
|
|
25
|
+
},
|
|
26
|
+
ACCOUNT_UNVERIFIED: {
|
|
27
|
+
status: 403,
|
|
28
|
+
code: 'E_ACCOUNT_UNVERIFIED',
|
|
29
|
+
},
|
|
30
|
+
FORBIDDEN: {
|
|
31
|
+
status: 403,
|
|
32
|
+
code: 'E_FORBIDDEN',
|
|
33
|
+
},
|
|
34
|
+
SAME_EMAIL: {
|
|
35
|
+
status: 400,
|
|
36
|
+
code: 'E_SAME_EMAIL',
|
|
37
|
+
},
|
|
38
|
+
INVALID_PASSWORD: {
|
|
39
|
+
status: 400,
|
|
40
|
+
code: 'E_INVALID_PASSWORD',
|
|
41
|
+
},
|
|
42
|
+
INVALID_TOKEN: {
|
|
43
|
+
status: 400,
|
|
44
|
+
code: 'E_INVALID_TOKEN',
|
|
45
|
+
},
|
|
46
|
+
PAYLOAD_TOO_LARGE: {
|
|
47
|
+
status: 413,
|
|
48
|
+
code: 'E_PAYLOAD_TOO_LARGE',
|
|
49
|
+
},
|
|
50
|
+
EMAIL_EXISTS: {
|
|
51
|
+
status: 409,
|
|
52
|
+
code: 'E_EMAIL_EXISTS',
|
|
53
|
+
},
|
|
54
|
+
TOO_MANY_ATTEMPTS: {
|
|
55
|
+
status: 429,
|
|
56
|
+
code: 'E_TOO_MANY_ATTEMPTS',
|
|
57
|
+
},
|
|
58
|
+
TOO_MANY_REQUESTS: {
|
|
59
|
+
status: 429,
|
|
60
|
+
code: 'E_TOO_MANY_REQUESTS',
|
|
61
|
+
},
|
|
62
|
+
} as const satisfies Record<string, StatusCodeEntry>
|
|
63
|
+
|
|
64
|
+
export type ExceptionKey = keyof typeof exceptions
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StatusCodeEntry } from '#modules/types/constants'
|
|
2
|
+
|
|
3
|
+
export { exceptions } from './exceptions.ts'
|
|
4
|
+
export { responseCodes } from './responseCodes.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Typed helper so hosts get the same `as const satisfies` ergonomics when
|
|
8
|
+
* extending catalogs in app/constants/.
|
|
9
|
+
*/
|
|
10
|
+
export function defineCodes<T extends Record<string, StatusCodeEntry>>(codes: T): T {
|
|
11
|
+
return codes
|
|
12
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { StatusCodeEntry } from '#modules/types/constants'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Success message codes for auth/account controllers.
|
|
5
|
+
* Domain services do not import these — controllers build envelopes.
|
|
6
|
+
*/
|
|
7
|
+
export const responseCodes = {
|
|
8
|
+
AUTH_LOGIN: {
|
|
9
|
+
status: 200,
|
|
10
|
+
code: 'M_AUTH_LOGIN',
|
|
11
|
+
},
|
|
12
|
+
AUTH_LOGOUT: {
|
|
13
|
+
status: 200,
|
|
14
|
+
code: 'M_AUTH_LOGOUT',
|
|
15
|
+
},
|
|
16
|
+
AUTH_REGISTER: {
|
|
17
|
+
status: 201,
|
|
18
|
+
code: 'M_AUTH_REGISTER',
|
|
19
|
+
},
|
|
20
|
+
AUTH_RESET_PASSWORD_REQUEST: {
|
|
21
|
+
status: 200,
|
|
22
|
+
code: 'M_AUTH_RESET_PASSWORD_REQUEST',
|
|
23
|
+
},
|
|
24
|
+
AUTH_UPDATE_PASSWORD: {
|
|
25
|
+
status: 200,
|
|
26
|
+
code: 'M_AUTH_UPDATE_PASSWORD',
|
|
27
|
+
},
|
|
28
|
+
AUTH_ACTIVATE_ACCOUNT_REQUEST: {
|
|
29
|
+
status: 200,
|
|
30
|
+
code: 'M_AUTH_ACTIVATE_ACCOUNT_REQUEST',
|
|
31
|
+
},
|
|
32
|
+
AUTH_ACTIVATE_ACCOUNT: {
|
|
33
|
+
status: 200,
|
|
34
|
+
code: 'M_AUTH_ACTIVATE_ACCOUNT',
|
|
35
|
+
},
|
|
36
|
+
AUTH_VALIDATE_PASSWORD_RESET: {
|
|
37
|
+
status: 200,
|
|
38
|
+
code: 'M_AUTH_VALIDATE_PASSWORD_RESET',
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
PROFILE_UPDATED: {
|
|
42
|
+
status: 200,
|
|
43
|
+
code: 'M_PROFILE_UPDATED',
|
|
44
|
+
},
|
|
45
|
+
EMAIL_VERIFICATION_SENT: {
|
|
46
|
+
status: 200,
|
|
47
|
+
code: 'M_EMAIL_VERIFICATION_SENT',
|
|
48
|
+
},
|
|
49
|
+
EMAIL_CHANGE_CONFIRMED: {
|
|
50
|
+
status: 200,
|
|
51
|
+
code: 'M_EMAIL_CHANGE_CONFIRMED',
|
|
52
|
+
},
|
|
53
|
+
PASSWORD_UPDATED: {
|
|
54
|
+
status: 200,
|
|
55
|
+
code: 'M_PASSWORD_UPDATED',
|
|
56
|
+
},
|
|
57
|
+
ACCOUNT_DELETED: {
|
|
58
|
+
status: 200,
|
|
59
|
+
code: 'M_ACCOUNT_DELETED',
|
|
60
|
+
},
|
|
61
|
+
USER_SHOW: {
|
|
62
|
+
status: 200,
|
|
63
|
+
code: 'M_USER_SHOW',
|
|
64
|
+
},
|
|
65
|
+
NOTIFICATIONS_FETCHED: {
|
|
66
|
+
status: 200,
|
|
67
|
+
code: 'M_NOTIFICATIONS_FETCHED',
|
|
68
|
+
},
|
|
69
|
+
NOTIFICATIONS_UPDATED: {
|
|
70
|
+
status: 200,
|
|
71
|
+
code: 'M_NOTIFICATIONS_UPDATED',
|
|
72
|
+
},
|
|
73
|
+
INBOX_FETCHED: {
|
|
74
|
+
status: 200,
|
|
75
|
+
code: 'M_INBOX_FETCHED',
|
|
76
|
+
},
|
|
77
|
+
INBOX_MARKED_READ: {
|
|
78
|
+
status: 200,
|
|
79
|
+
code: 'M_INBOX_MARKED_READ',
|
|
80
|
+
},
|
|
81
|
+
INBOX_MARKED_ALL_READ: {
|
|
82
|
+
status: 200,
|
|
83
|
+
code: 'M_INBOX_MARKED_ALL_READ',
|
|
84
|
+
},
|
|
85
|
+
} as const satisfies Record<string, StatusCodeEntry>
|
|
86
|
+
|
|
87
|
+
export type ResponseCodeKey = keyof typeof responseCodes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared user port — used by auth, account, and any future user-facing module.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type AuthMethod = 'email' | 'github' | 'google'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Portable user DTO. Host adapters map persistence models ↔ this shape.
|
|
9
|
+
*/
|
|
10
|
+
export type User = {
|
|
11
|
+
id: number
|
|
12
|
+
email: string
|
|
13
|
+
/** Staged email awaiting confirmation; current email stays valid until then. */
|
|
14
|
+
pendingEmail: string | null
|
|
15
|
+
authMethod: AuthMethod
|
|
16
|
+
firstName: string
|
|
17
|
+
lastName: string
|
|
18
|
+
avatarKey: string | null
|
|
19
|
+
emailVerifiedAt: Date | null
|
|
20
|
+
emailVerificationToken: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Input for creating an email-auth user via UserStore.createEmailUser.
|
|
25
|
+
*/
|
|
26
|
+
export type RegisterUserInput = {
|
|
27
|
+
email: string
|
|
28
|
+
password: string
|
|
29
|
+
firstName?: string
|
|
30
|
+
lastName?: string
|
|
31
|
+
fullName?: string | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Host must implement this. Auth/Account services talk only to this contract.
|
|
36
|
+
* Persistence, hashing, and schema details stay in the host adapter.
|
|
37
|
+
*/
|
|
38
|
+
export interface UserStore {
|
|
39
|
+
/**
|
|
40
|
+
* Persist a new email-auth user (hash password).
|
|
41
|
+
* Do not set emailVerifiedAt or verification token — the service owns that flow.
|
|
42
|
+
*/
|
|
43
|
+
createEmailUser(data: RegisterUserInput): Promise<User>
|
|
44
|
+
|
|
45
|
+
findOrFail(id: number): Promise<User>
|
|
46
|
+
|
|
47
|
+
findByEmail(email: string): Promise<User | null>
|
|
48
|
+
|
|
49
|
+
findByEmailOrFail(email: string): Promise<User>
|
|
50
|
+
|
|
51
|
+
findByVerificationToken(token: string): Promise<User | null>
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Verify email/password. Throw a framework/auth error on failure
|
|
55
|
+
* (mapped by the global exception handler).
|
|
56
|
+
*/
|
|
57
|
+
verifyCredentials(email: string, password: string): Promise<User>
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Verify a stored password for an existing user (no login).
|
|
61
|
+
*/
|
|
62
|
+
verifyPassword(userId: number, password: string): Promise<boolean>
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Persist User field changes (verification token, emailVerifiedAt, profile, …).
|
|
66
|
+
*/
|
|
67
|
+
save(user: User): Promise<User>
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hash and persist a new password for the user.
|
|
71
|
+
*/
|
|
72
|
+
updatePassword(user: User, password: string): Promise<User>
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Permanently delete the user row.
|
|
76
|
+
*/
|
|
77
|
+
delete(userId: number): Promise<void>
|
|
78
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CreemInvoice } from './types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Narrow port over the Creem payment SDK.
|
|
5
|
+
* Host supplies a creem_io adapter (or a test double).
|
|
6
|
+
*/
|
|
7
|
+
export interface CreemClient {
|
|
8
|
+
createCheckout(input: {
|
|
9
|
+
successUrl: string
|
|
10
|
+
productId: string
|
|
11
|
+
customerEmail: string
|
|
12
|
+
userId: number
|
|
13
|
+
}): Promise<{ checkoutUrl: string | null | undefined }>
|
|
14
|
+
|
|
15
|
+
createPortalLink(customerId: string): Promise<{ portalUrl: string }>
|
|
16
|
+
|
|
17
|
+
listTransactions(
|
|
18
|
+
customerId: string,
|
|
19
|
+
limit: number
|
|
20
|
+
): Promise<{ items: CreemInvoice[] }>
|
|
21
|
+
|
|
22
|
+
handleWebhookEvents(
|
|
23
|
+
rawBody: string,
|
|
24
|
+
signature: string,
|
|
25
|
+
handlers: CreemWebhookHandlers
|
|
26
|
+
): Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Webhook handlers passed through to the SDK.
|
|
31
|
+
* Typed loosely so the host can use creem_io's WebhookOptions shape
|
|
32
|
+
* without this contract importing the SDK.
|
|
33
|
+
*/
|
|
34
|
+
export type CreemWebhookHandlers = Record<string, ((...args: any[]) => any) | undefined>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creem module config + shared value types.
|
|
3
|
+
* Product→plan mapping and default plan are host-owned.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Plan names are free-form strings owned by the host. */
|
|
7
|
+
export type CreemPlan = string
|
|
8
|
+
|
|
9
|
+
export type CreemConfig = {
|
|
10
|
+
successUrl: string
|
|
11
|
+
/** Creem product id → plan name */
|
|
12
|
+
productPlans: Record<string, CreemPlan>
|
|
13
|
+
/** Plan when a product id is unmapped. Also used by SubscriptionService. */
|
|
14
|
+
defaultPlan: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CreemClientOptions = {
|
|
18
|
+
apiKey: string
|
|
19
|
+
webhookSecret?: string
|
|
20
|
+
testMode?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CreemInvoice = {
|
|
24
|
+
id: string
|
|
25
|
+
type: 'payment' | 'invoice'
|
|
26
|
+
amount: number
|
|
27
|
+
currency: string
|
|
28
|
+
status: string
|
|
29
|
+
createdAt: number
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creem module events. Import this file so the EventsList augmentation loads.
|
|
3
|
+
*/
|
|
4
|
+
declare module '@adonisjs/core/types' {
|
|
5
|
+
interface EventsList {
|
|
6
|
+
'Creem:CheckoutCreated': { userId: number; productId: string; checkoutUrl: string }
|
|
7
|
+
'Creem:PortalLinkCreated': { customerId: string }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "creem",
|
|
3
|
+
"description": "Creem checkout, portal, invoices, and webhook verification",
|
|
4
|
+
"files": [
|
|
5
|
+
"service.ts",
|
|
6
|
+
"events.ts",
|
|
7
|
+
"contracts/**",
|
|
8
|
+
"module.json"
|
|
9
|
+
],
|
|
10
|
+
"stubs": [
|
|
11
|
+
"stubs/adapters/**/*"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"creem_io": "^1.0.0"
|
|
15
|
+
},
|
|
16
|
+
"registryDependencies": ["api"],
|
|
17
|
+
"peerModels": [],
|
|
18
|
+
"configKeys": ["modules.creem"],
|
|
19
|
+
"env": [
|
|
20
|
+
"CREEM_API_KEY",
|
|
21
|
+
"CREEM_WEBHOOK_SECRET",
|
|
22
|
+
"CREEM_TEST_MODE",
|
|
23
|
+
"CREEM_SUCCESS_URL"
|
|
24
|
+
],
|
|
25
|
+
"events": ["Creem:CheckoutCreated", "Creem:PortalLinkCreated"],
|
|
26
|
+
"hostFacingExceptions": [],
|
|
27
|
+
"hostTests": []
|
|
28
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreemService
|
|
3
|
+
*
|
|
4
|
+
* Checkout, customer portal, product→plan resolution, invoices, webhooks.
|
|
5
|
+
* Depends on CreemClient + CreemConfig — no env, no creem_io import.
|
|
6
|
+
*/
|
|
7
|
+
import ApiService from '#modules/api/service'
|
|
8
|
+
import type {
|
|
9
|
+
CreemClient,
|
|
10
|
+
CreemConfig,
|
|
11
|
+
CreemInvoice,
|
|
12
|
+
CreemPlan,
|
|
13
|
+
CreemWebhookHandlers,
|
|
14
|
+
} from '#modules/creem/contracts/index'
|
|
15
|
+
import '#modules/creem/events'
|
|
16
|
+
|
|
17
|
+
export class CreemService extends ApiService {
|
|
18
|
+
namespace = 'creem'
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private client: CreemClient,
|
|
22
|
+
private config: CreemConfig
|
|
23
|
+
) {
|
|
24
|
+
super()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get defaultPlan(): string {
|
|
28
|
+
return this.config.defaultPlan
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a Creem checkout session.
|
|
33
|
+
* Passes userId in metadata so it can be linked on checkout.completed.
|
|
34
|
+
*/
|
|
35
|
+
async createCheckout(productId: string, userId: number, userEmail: string): Promise<string> {
|
|
36
|
+
const checkout = await this.client.createCheckout({
|
|
37
|
+
successUrl: this.config.successUrl,
|
|
38
|
+
productId,
|
|
39
|
+
customerEmail: userEmail,
|
|
40
|
+
userId,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (!checkout.checkoutUrl) {
|
|
44
|
+
throw new Error('Creem did not return a checkout URL')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.emitSafe('Creem:CheckoutCreated', {
|
|
48
|
+
userId,
|
|
49
|
+
productId,
|
|
50
|
+
checkoutUrl: checkout.checkoutUrl,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return checkout.checkoutUrl
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generates a Creem customer portal link.
|
|
58
|
+
*/
|
|
59
|
+
async createPortalLink(creemCustomerId: string): Promise<string> {
|
|
60
|
+
const portal = await this.client.createPortalLink(creemCustomerId)
|
|
61
|
+
this.emitSafe('Creem:PortalLinkCreated', { customerId: creemCustomerId })
|
|
62
|
+
return portal.portalUrl
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Maps a Creem product ID to a plan string. Falls back to config.defaultPlan.
|
|
67
|
+
*/
|
|
68
|
+
resolveProductPlan(creemProductId: string): CreemPlan {
|
|
69
|
+
return this.config.productPlans[creemProductId] ?? this.config.defaultPlan
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getLastInvoices(creemCustomerId: string): Promise<CreemInvoice[]> {
|
|
73
|
+
const result = await this.client.listTransactions(creemCustomerId, 3)
|
|
74
|
+
return result.items.filter((t) => t.type === 'invoice')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Delegates to the client webhook handler (signature verification included).
|
|
79
|
+
*/
|
|
80
|
+
async handleWebhookEvents(
|
|
81
|
+
rawBody: string,
|
|
82
|
+
signature: string,
|
|
83
|
+
handlers: CreemWebhookHandlers
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
await this.client.handleWebhookEvents(rawBody, signature, handlers)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Creem factory. Copied to app/adapters/creem.ts.
|
|
3
|
+
* Import as `#adapters/creem`.
|
|
4
|
+
*/
|
|
5
|
+
import { CreemService } from '#modules/creem/service'
|
|
6
|
+
import { CreemIoClient } from './creem_io_client.ts'
|
|
7
|
+
import type {
|
|
8
|
+
CreemClientOptions,
|
|
9
|
+
CreemConfig,
|
|
10
|
+
} from '#modules/creem/contracts/index'
|
|
11
|
+
|
|
12
|
+
export { CreemIoClient } from './creem_io_client.ts'
|
|
13
|
+
|
|
14
|
+
export type CreateCreemServiceOptions = CreemClientOptions & CreemConfig
|
|
15
|
+
|
|
16
|
+
export function createCreemService(options: CreateCreemServiceOptions) {
|
|
17
|
+
const { successUrl, productPlans, defaultPlan, ...clientOptions } = options
|
|
18
|
+
return new CreemService(new CreemIoClient(clientOptions), {
|
|
19
|
+
successUrl,
|
|
20
|
+
productPlans,
|
|
21
|
+
defaultPlan,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default CreemClient backed by creem_io. Copied to app/adapters/creem_io_client.ts.
|
|
3
|
+
* Host supplies API keys from env.
|
|
4
|
+
*/
|
|
5
|
+
import { createCreem } from 'creem_io'
|
|
6
|
+
import type { WebhookOptions } from 'creem_io'
|
|
7
|
+
import type {
|
|
8
|
+
CreemClient,
|
|
9
|
+
CreemClientOptions,
|
|
10
|
+
CreemWebhookHandlers,
|
|
11
|
+
} from '#modules/creem/contracts/index'
|
|
12
|
+
|
|
13
|
+
export class CreemIoClient implements CreemClient {
|
|
14
|
+
private client: ReturnType<typeof createCreem>
|
|
15
|
+
|
|
16
|
+
constructor(options: CreemClientOptions) {
|
|
17
|
+
this.client = createCreem({
|
|
18
|
+
apiKey: options.apiKey,
|
|
19
|
+
webhookSecret: options.webhookSecret,
|
|
20
|
+
testMode: options.testMode,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async createCheckout(input: {
|
|
25
|
+
successUrl: string
|
|
26
|
+
productId: string
|
|
27
|
+
customerEmail: string
|
|
28
|
+
userId: number
|
|
29
|
+
}) {
|
|
30
|
+
const checkout = await this.client.checkouts.create({
|
|
31
|
+
successUrl: input.successUrl,
|
|
32
|
+
productId: input.productId,
|
|
33
|
+
customer: { email: input.customerEmail },
|
|
34
|
+
metadata: { userId: String(input.userId) },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return { checkoutUrl: checkout.checkoutUrl }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async createPortalLink(customerId: string) {
|
|
41
|
+
const portal = await this.client.customers.createPortal({ customerId })
|
|
42
|
+
return { portalUrl: portal.customerPortalLink }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async listTransactions(customerId: string, limit: number) {
|
|
46
|
+
const result = await this.client.transactions.list({ customerId, limit })
|
|
47
|
+
return {
|
|
48
|
+
items: result.items.map((t) => ({
|
|
49
|
+
id: t.id,
|
|
50
|
+
type: t.type,
|
|
51
|
+
amount: t.amount,
|
|
52
|
+
currency: t.currency,
|
|
53
|
+
status: t.status,
|
|
54
|
+
createdAt: t.createdAt,
|
|
55
|
+
})),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async handleWebhookEvents(
|
|
60
|
+
rawBody: string,
|
|
61
|
+
signature: string,
|
|
62
|
+
handlers: CreemWebhookHandlers
|
|
63
|
+
) {
|
|
64
|
+
await this.client.webhooks.handleEvents(
|
|
65
|
+
rawBody,
|
|
66
|
+
signature,
|
|
67
|
+
handlers as Omit<WebhookOptions, 'webhookSecret'>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|