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.
Files changed (140) hide show
  1. package/README.md +64 -0
  2. package/build/adonis_peers.d.ts +14 -0
  3. package/build/adonis_peers.js +56 -0
  4. package/build/cli.d.ts +2 -0
  5. package/build/cli.js +111 -0
  6. package/build/commands/add.d.ts +18 -0
  7. package/build/commands/add.js +246 -0
  8. package/build/commands/check.d.ts +4 -0
  9. package/build/commands/check.js +159 -0
  10. package/build/commands/diff.d.ts +2 -0
  11. package/build/commands/diff.js +74 -0
  12. package/build/commands/init.d.ts +6 -0
  13. package/build/commands/init.js +124 -0
  14. package/build/commands/list.d.ts +2 -0
  15. package/build/commands/list.js +34 -0
  16. package/build/config_merge.d.ts +10 -0
  17. package/build/config_merge.js +71 -0
  18. package/build/constants_merge.d.ts +4 -0
  19. package/build/constants_merge.js +85 -0
  20. package/build/copy.d.ts +8 -0
  21. package/build/copy.js +33 -0
  22. package/build/host.d.ts +15 -0
  23. package/build/host.js +85 -0
  24. package/build/registry/index.d.ts +26 -0
  25. package/build/registry/index.js +116 -0
  26. package/build/registry/plan.d.ts +53 -0
  27. package/build/registry/plan.js +159 -0
  28. package/build/schema.d.ts +136 -0
  29. package/build/schema.js +58 -0
  30. package/build/types.d.ts +21 -0
  31. package/build/types.js +1 -0
  32. package/build/wire.d.ts +16 -0
  33. package/build/wire.js +162 -0
  34. package/package.json +51 -0
  35. package/registry/README.md +86 -0
  36. package/registry/account/contracts/avatar_storage.ts +7 -0
  37. package/registry/account/contracts/index.ts +7 -0
  38. package/registry/account/contracts/types.ts +23 -0
  39. package/registry/account/events.ts +14 -0
  40. package/registry/account/module.json +31 -0
  41. package/registry/account/options.ts +11 -0
  42. package/registry/account/service.ts +155 -0
  43. package/registry/account/stubs/adapters/account.ts +21 -0
  44. package/registry/account/stubs/adapters/drive_avatar_storage.ts +15 -0
  45. package/registry/account/stubs/controllers/account_controller.ts +190 -0
  46. package/registry/account/stubs/routes/account.ts +53 -0
  47. package/registry/account/stubs/validators/account.ts +37 -0
  48. package/registry/account/tests/account_service.spec.ts +132 -0
  49. package/registry/account/tests/fakes/memory_avatar_storage.ts +12 -0
  50. package/registry/api/envelope.ts +66 -0
  51. package/registry/api/exception.ts +38 -0
  52. package/registry/api/exception_handler.ts +54 -0
  53. package/registry/api/module.json +27 -0
  54. package/registry/api/provider.ts +129 -0
  55. package/registry/api/service.ts +88 -0
  56. package/registry/api/tests/emit_safe.spec.ts +107 -0
  57. package/registry/api/tests/respond.spec.ts +40 -0
  58. package/registry/auth/contracts/index.ts +18 -0
  59. package/registry/auth/contracts/password_reset_store.ts +21 -0
  60. package/registry/auth/contracts/session_manager.ts +19 -0
  61. package/registry/auth/contracts/types.ts +32 -0
  62. package/registry/auth/events.ts +21 -0
  63. package/registry/auth/module.json +45 -0
  64. package/registry/auth/options.ts +23 -0
  65. package/registry/auth/service.ts +150 -0
  66. package/registry/auth/stubs/adapters/auth.ts +21 -0
  67. package/registry/auth/stubs/adapters/lucid_access_token_session.ts +33 -0
  68. package/registry/auth/stubs/adapters/lucid_password_reset_store.ts +68 -0
  69. package/registry/auth/stubs/adapters/lucid_user_store.ts +86 -0
  70. package/registry/auth/stubs/adapters/map_user.ts +19 -0
  71. package/registry/auth/stubs/controllers/auth_controller.ts +126 -0
  72. package/registry/auth/stubs/migrations/add_pending_email_to_users_table.ts +17 -0
  73. package/registry/auth/stubs/migrations/create_password_resets_table.ts +27 -0
  74. package/registry/auth/stubs/migrations/create_users_table.ts +31 -0
  75. package/registry/auth/stubs/models/password_reset.ts +29 -0
  76. package/registry/auth/stubs/models/user.ts +99 -0
  77. package/registry/auth/stubs/providers/vine_provider.ts +43 -0
  78. package/registry/auth/stubs/routes/auth.ts +36 -0
  79. package/registry/auth/stubs/start/limiter.ts +17 -0
  80. package/registry/auth/stubs/validators/auth.ts +48 -0
  81. package/registry/auth/stubs/validators/rules/exists.ts +21 -0
  82. package/registry/auth/stubs/validators/rules/unique.ts +21 -0
  83. package/registry/auth/stubs/validators/rules/valid_password.ts +20 -0
  84. package/registry/auth/stubs/validators/rules/valid_token.ts +27 -0
  85. package/registry/auth/stubs/validators/vine.d.ts +10 -0
  86. package/registry/auth/tests/auth_service.spec.ts +145 -0
  87. package/registry/auth/tests/auth_update_password.spec.ts +99 -0
  88. package/registry/auth/tests/fakes/index.ts +3 -0
  89. package/registry/auth/tests/fakes/memory_password_reset_store.ts +71 -0
  90. package/registry/auth/tests/fakes/memory_session_manager.ts +23 -0
  91. package/registry/auth/tests/fakes/memory_user_store.ts +89 -0
  92. package/registry/constants/exceptions.ts +64 -0
  93. package/registry/constants/index.ts +12 -0
  94. package/registry/constants/responseCodes.ts +87 -0
  95. package/registry/contracts/index.ts +5 -0
  96. package/registry/contracts/user.ts +78 -0
  97. package/registry/creem/contracts/creem_client.ts +34 -0
  98. package/registry/creem/contracts/index.ts +8 -0
  99. package/registry/creem/contracts/types.ts +30 -0
  100. package/registry/creem/events.ts +9 -0
  101. package/registry/creem/module.json +28 -0
  102. package/registry/creem/service.ts +87 -0
  103. package/registry/creem/stubs/adapters/creem.ts +23 -0
  104. package/registry/creem/stubs/adapters/creem_io_client.ts +70 -0
  105. package/registry/creem/tests/fakes/fake_creem_client.ts +68 -0
  106. package/registry/notification/contracts/app_notification_store.ts +8 -0
  107. package/registry/notification/contracts/index.ts +9 -0
  108. package/registry/notification/contracts/mail_transport.ts +7 -0
  109. package/registry/notification/contracts/types.ts +32 -0
  110. package/registry/notification/module.json +23 -0
  111. package/registry/notification/options.ts +11 -0
  112. package/registry/notification/service.ts +103 -0
  113. package/registry/notification/stubs/adapters/adonis_mail_transport.ts +15 -0
  114. package/registry/notification/stubs/adapters/lucid_app_notification_store.ts +21 -0
  115. package/registry/notification/stubs/adapters/notification.ts +36 -0
  116. package/registry/notification/stubs/migrations/create_notifications_table.ts +23 -0
  117. package/registry/notification/stubs/models/app_notification.ts +45 -0
  118. package/registry/notification/tests/fakes/memory_app_notification_store.ts +19 -0
  119. package/registry/notification/tests/fakes/memory_mail_transport.ts +16 -0
  120. package/registry/notification/tests/notification_service.spec.ts +63 -0
  121. package/registry/registry.json +4 -0
  122. package/registry/subscription/contracts/index.ts +9 -0
  123. package/registry/subscription/contracts/subscription_store.ts +15 -0
  124. package/registry/subscription/contracts/types.ts +39 -0
  125. package/registry/subscription/events.ts +13 -0
  126. package/registry/subscription/module.json +31 -0
  127. package/registry/subscription/options.ts +14 -0
  128. package/registry/subscription/service.ts +140 -0
  129. package/registry/subscription/stubs/adapters/lucid_subscription_store.ts +69 -0
  130. package/registry/subscription/stubs/adapters/subscription.ts +21 -0
  131. package/registry/subscription/stubs/migrations/create_subscriptions_table.ts +26 -0
  132. package/registry/subscription/stubs/migrations/subscriptions_user_creem_unique.ts +19 -0
  133. package/registry/subscription/stubs/models/subscription.ts +45 -0
  134. package/registry/subscription/tests/billing_service.spec.ts +189 -0
  135. package/registry/subscription/tests/fakes/memory_subscription_store.ts +63 -0
  136. package/registry/types/api.ts +41 -0
  137. package/registry/types/constants.ts +7 -0
  138. package/registry/types/index.ts +6 -0
  139. package/registry/types/models.ts +32 -0
  140. 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,5 @@
1
+ /**
2
+ * Shared module contracts (cross-cutting ports).
3
+ * Module-specific contracts stay under modules/<name>/contracts.
4
+ */
5
+ export type { AuthMethod, User, RegisterUserInput, UserStore } from './user.ts'
@@ -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,8 @@
1
+ export type {
2
+ CreemPlan,
3
+ CreemConfig,
4
+ CreemClientOptions,
5
+ CreemInvoice,
6
+ } from './types.ts'
7
+
8
+ export type { CreemClient, CreemWebhookHandlers } from './creem_client.ts'
@@ -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
+ }