@corgelabs/corge 0.0.1 → 1.0.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 (341) hide show
  1. package/dist/ai/index.d.ts +3 -0
  2. package/dist/ai/index.d.ts.map +1 -0
  3. package/dist/ai/index.js +3 -0
  4. package/dist/ai/index.js.map +1 -0
  5. package/dist/ai/schema.d.ts +83 -0
  6. package/dist/ai/schema.d.ts.map +1 -0
  7. package/dist/ai/schema.js +19 -0
  8. package/dist/ai/schema.js.map +1 -0
  9. package/dist/ai/service.d.ts +14 -0
  10. package/dist/ai/service.d.ts.map +1 -0
  11. package/dist/ai/service.js +266 -0
  12. package/dist/ai/service.js.map +1 -0
  13. package/dist/ai/utils.d.ts +10 -0
  14. package/dist/ai/utils.d.ts.map +1 -0
  15. package/dist/ai/utils.js +27 -0
  16. package/dist/ai/utils.js.map +1 -0
  17. package/dist/attachments/index.d.ts +4 -0
  18. package/dist/attachments/index.d.ts.map +1 -0
  19. package/dist/attachments/index.js +4 -0
  20. package/dist/attachments/index.js.map +1 -0
  21. package/dist/attachments/routes.d.ts +75 -0
  22. package/dist/attachments/routes.d.ts.map +1 -0
  23. package/dist/attachments/routes.js +18 -0
  24. package/dist/attachments/routes.js.map +1 -0
  25. package/dist/attachments/schema.d.ts +61 -0
  26. package/dist/attachments/schema.d.ts.map +1 -0
  27. package/dist/attachments/schema.js +30 -0
  28. package/dist/attachments/schema.js.map +1 -0
  29. package/dist/attachments/service.d.ts +27 -0
  30. package/dist/attachments/service.d.ts.map +1 -0
  31. package/dist/attachments/service.js +224 -0
  32. package/dist/attachments/service.js.map +1 -0
  33. package/dist/attachments/utils.d.ts +9 -0
  34. package/dist/attachments/utils.d.ts.map +1 -0
  35. package/dist/attachments/utils.js +80 -0
  36. package/dist/attachments/utils.js.map +1 -0
  37. package/dist/auth/index.d.ts +4 -0
  38. package/dist/auth/index.d.ts.map +1 -0
  39. package/dist/auth/index.js +4 -0
  40. package/dist/auth/index.js.map +1 -0
  41. package/dist/auth/oidc.d.ts +25 -0
  42. package/dist/auth/oidc.d.ts.map +1 -0
  43. package/dist/auth/oidc.js +298 -0
  44. package/dist/auth/oidc.js.map +1 -0
  45. package/dist/auth/recovery.d.ts +41 -0
  46. package/dist/auth/recovery.d.ts.map +1 -0
  47. package/dist/auth/recovery.js +250 -0
  48. package/dist/auth/recovery.js.map +1 -0
  49. package/dist/auth/routes.d.ts +226 -0
  50. package/dist/auth/routes.d.ts.map +1 -0
  51. package/dist/auth/routes.js +96 -0
  52. package/dist/auth/routes.js.map +1 -0
  53. package/dist/auth/schema.d.ts +158 -0
  54. package/dist/auth/schema.d.ts.map +1 -0
  55. package/dist/auth/schema.js +82 -0
  56. package/dist/auth/schema.js.map +1 -0
  57. package/dist/auth/service.d.ts +96 -0
  58. package/dist/auth/service.d.ts.map +1 -0
  59. package/dist/auth/service.js +129 -0
  60. package/dist/auth/service.js.map +1 -0
  61. package/dist/auth/session.d.ts +51 -0
  62. package/dist/auth/session.d.ts.map +1 -0
  63. package/dist/auth/session.js +344 -0
  64. package/dist/auth/session.js.map +1 -0
  65. package/dist/auth/utils.d.ts +17 -0
  66. package/dist/auth/utils.d.ts.map +1 -0
  67. package/dist/auth/utils.js +96 -0
  68. package/dist/auth/utils.js.map +1 -0
  69. package/dist/bin/corge.js +547 -0
  70. package/dist/chat/context.d.ts +10 -0
  71. package/dist/chat/context.d.ts.map +1 -0
  72. package/dist/chat/context.js +221 -0
  73. package/dist/chat/context.js.map +1 -0
  74. package/dist/chat/index.d.ts +4 -0
  75. package/dist/chat/index.d.ts.map +1 -0
  76. package/dist/chat/index.js +4 -0
  77. package/dist/chat/index.js.map +1 -0
  78. package/dist/chat/routes.d.ts +160 -0
  79. package/dist/chat/routes.d.ts.map +1 -0
  80. package/dist/chat/routes.js +210 -0
  81. package/dist/chat/routes.js.map +1 -0
  82. package/dist/chat/schema.d.ts +285 -0
  83. package/dist/chat/schema.d.ts.map +1 -0
  84. package/dist/chat/schema.js +74 -0
  85. package/dist/chat/schema.js.map +1 -0
  86. package/dist/chat/service.d.ts +64 -0
  87. package/dist/chat/service.d.ts.map +1 -0
  88. package/dist/chat/service.js +279 -0
  89. package/dist/chat/service.js.map +1 -0
  90. package/dist/chat/stream.d.ts +53 -0
  91. package/dist/chat/stream.d.ts.map +1 -0
  92. package/dist/chat/stream.js +339 -0
  93. package/dist/chat/stream.js.map +1 -0
  94. package/dist/chat/utils.d.ts +61 -0
  95. package/dist/chat/utils.d.ts.map +1 -0
  96. package/dist/chat/utils.js +285 -0
  97. package/dist/chat/utils.js.map +1 -0
  98. package/dist/cli/builder.d.ts +6 -0
  99. package/dist/cli/builder.d.ts.map +1 -0
  100. package/dist/cli/builder.js +91 -0
  101. package/dist/cli/builder.js.map +1 -0
  102. package/dist/cli/deployer.d.ts +2 -0
  103. package/dist/cli/deployer.d.ts.map +1 -0
  104. package/dist/cli/deployer.js +152 -0
  105. package/dist/cli/deployer.js.map +1 -0
  106. package/dist/cli/dev-server.d.ts +2 -0
  107. package/dist/cli/dev-server.d.ts.map +1 -0
  108. package/dist/cli/dev-server.js +70 -0
  109. package/dist/cli/dev-server.js.map +1 -0
  110. package/dist/cli/env.d.ts +4 -0
  111. package/dist/cli/env.d.ts.map +1 -0
  112. package/dist/cli/env.js +68 -0
  113. package/dist/cli/env.js.map +1 -0
  114. package/dist/cli/init.d.ts +6 -0
  115. package/dist/cli/init.d.ts.map +1 -0
  116. package/dist/cli/init.js +103 -0
  117. package/dist/cli/init.js.map +1 -0
  118. package/dist/config.d.ts +262 -0
  119. package/dist/config.d.ts.map +1 -0
  120. package/dist/config.js +212 -0
  121. package/dist/config.js.map +1 -0
  122. package/dist/db/index.d.ts +3 -0
  123. package/dist/db/index.d.ts.map +1 -0
  124. package/dist/db/index.js +3 -0
  125. package/dist/db/index.js.map +1 -0
  126. package/dist/db/schema.d.ts +410 -0
  127. package/dist/db/schema.d.ts.map +1 -0
  128. package/dist/db/schema.js +294 -0
  129. package/dist/db/schema.js.map +1 -0
  130. package/dist/db/service.d.ts +42 -0
  131. package/dist/db/service.d.ts.map +1 -0
  132. package/dist/db/service.js +372 -0
  133. package/dist/db/service.js.map +1 -0
  134. package/dist/db/utils.d.ts +25 -0
  135. package/dist/db/utils.d.ts.map +1 -0
  136. package/dist/db/utils.js +188 -0
  137. package/dist/db/utils.js.map +1 -0
  138. package/dist/emails/index.d.ts +4 -0
  139. package/dist/emails/index.d.ts.map +1 -0
  140. package/dist/emails/index.js +4 -0
  141. package/dist/emails/index.js.map +1 -0
  142. package/dist/emails/routes.d.ts +191 -0
  143. package/dist/emails/routes.d.ts.map +1 -0
  144. package/dist/emails/routes.js +39 -0
  145. package/dist/emails/routes.js.map +1 -0
  146. package/dist/emails/schema.d.ts +237 -0
  147. package/dist/emails/schema.d.ts.map +1 -0
  148. package/dist/emails/schema.js +98 -0
  149. package/dist/emails/schema.js.map +1 -0
  150. package/dist/emails/service.d.ts +22 -0
  151. package/dist/emails/service.d.ts.map +1 -0
  152. package/dist/emails/service.js +337 -0
  153. package/dist/emails/service.js.map +1 -0
  154. package/dist/emails/utils.d.ts +49 -0
  155. package/dist/emails/utils.d.ts.map +1 -0
  156. package/dist/emails/utils.js +341 -0
  157. package/dist/emails/utils.js.map +1 -0
  158. package/dist/files/index.d.ts +4 -0
  159. package/dist/files/index.d.ts.map +1 -0
  160. package/dist/files/index.js +4 -0
  161. package/dist/files/index.js.map +1 -0
  162. package/dist/files/routes.d.ts +72 -0
  163. package/dist/files/routes.d.ts.map +1 -0
  164. package/dist/files/routes.js +38 -0
  165. package/dist/files/routes.js.map +1 -0
  166. package/dist/files/schema.d.ts +57 -0
  167. package/dist/files/schema.d.ts.map +1 -0
  168. package/dist/files/schema.js +39 -0
  169. package/dist/files/schema.js.map +1 -0
  170. package/dist/files/service.d.ts +27 -0
  171. package/dist/files/service.d.ts.map +1 -0
  172. package/dist/files/service.js +175 -0
  173. package/dist/files/service.js.map +1 -0
  174. package/dist/files/utils.d.ts +3 -0
  175. package/dist/files/utils.d.ts.map +1 -0
  176. package/dist/files/utils.js +63 -0
  177. package/dist/files/utils.js.map +1 -0
  178. package/dist/index.d.ts +875 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +9919 -0
  181. package/dist/index.js.map +7 -0
  182. package/dist/payments/billing.d.ts +29 -0
  183. package/dist/payments/billing.d.ts.map +1 -0
  184. package/dist/payments/billing.js +378 -0
  185. package/dist/payments/billing.js.map +1 -0
  186. package/dist/payments/index.d.ts +4 -0
  187. package/dist/payments/index.d.ts.map +1 -0
  188. package/dist/payments/index.js +4 -0
  189. package/dist/payments/index.js.map +1 -0
  190. package/dist/payments/routes.d.ts +340 -0
  191. package/dist/payments/routes.d.ts.map +1 -0
  192. package/dist/payments/routes.js +56 -0
  193. package/dist/payments/routes.js.map +1 -0
  194. package/dist/payments/schema.d.ts +412 -0
  195. package/dist/payments/schema.d.ts.map +1 -0
  196. package/dist/payments/schema.js +260 -0
  197. package/dist/payments/schema.js.map +1 -0
  198. package/dist/payments/service.d.ts +31 -0
  199. package/dist/payments/service.d.ts.map +1 -0
  200. package/dist/payments/service.js +322 -0
  201. package/dist/payments/service.js.map +1 -0
  202. package/dist/payments/utils.d.ts +61 -0
  203. package/dist/payments/utils.d.ts.map +1 -0
  204. package/dist/payments/utils.js +369 -0
  205. package/dist/payments/utils.js.map +1 -0
  206. package/dist/router/guard.d.ts +19 -0
  207. package/dist/router/guard.d.ts.map +1 -0
  208. package/dist/router/guard.js +243 -0
  209. package/dist/router/guard.js.map +1 -0
  210. package/dist/router/index.d.ts +3 -0
  211. package/dist/router/index.d.ts.map +1 -0
  212. package/dist/router/index.js +3 -0
  213. package/dist/router/index.js.map +1 -0
  214. package/dist/router/schema.d.ts +103 -0
  215. package/dist/router/schema.d.ts.map +1 -0
  216. package/dist/router/schema.js +282 -0
  217. package/dist/router/schema.js.map +1 -0
  218. package/dist/router/service.d.ts +25 -0
  219. package/dist/router/service.d.ts.map +1 -0
  220. package/dist/router/service.js +339 -0
  221. package/dist/router/service.js.map +1 -0
  222. package/dist/tasks/index.d.ts +4 -0
  223. package/dist/tasks/index.d.ts.map +1 -0
  224. package/dist/tasks/index.js +4 -0
  225. package/dist/tasks/index.js.map +1 -0
  226. package/dist/tasks/routes.d.ts +54 -0
  227. package/dist/tasks/routes.d.ts.map +1 -0
  228. package/dist/tasks/routes.js +195 -0
  229. package/dist/tasks/routes.js.map +1 -0
  230. package/dist/tasks/schema.d.ts +261 -0
  231. package/dist/tasks/schema.d.ts.map +1 -0
  232. package/dist/tasks/schema.js +35 -0
  233. package/dist/tasks/schema.js.map +1 -0
  234. package/dist/tasks/service.d.ts +32 -0
  235. package/dist/tasks/service.d.ts.map +1 -0
  236. package/dist/tasks/service.js +201 -0
  237. package/dist/tasks/service.js.map +1 -0
  238. package/dist/tasks/steps.d.ts +18 -0
  239. package/dist/tasks/steps.d.ts.map +1 -0
  240. package/dist/tasks/steps.js +365 -0
  241. package/dist/tasks/steps.js.map +1 -0
  242. package/dist/tasks/utils.d.ts +75 -0
  243. package/dist/tasks/utils.d.ts.map +1 -0
  244. package/dist/tasks/utils.js +311 -0
  245. package/dist/tasks/utils.js.map +1 -0
  246. package/dist/tasks/watcher.d.ts +13 -0
  247. package/dist/tasks/watcher.d.ts.map +1 -0
  248. package/dist/tasks/watcher.js +255 -0
  249. package/dist/tasks/watcher.js.map +1 -0
  250. package/dist/tasks/worker.d.ts +28 -0
  251. package/dist/tasks/worker.d.ts.map +1 -0
  252. package/dist/tasks/worker.js +326 -0
  253. package/dist/tasks/worker.js.map +1 -0
  254. package/dist/utils/index.d.ts +3 -0
  255. package/dist/utils/index.d.ts.map +1 -0
  256. package/dist/utils/index.js +3 -0
  257. package/dist/utils/index.js.map +1 -0
  258. package/dist/utils/schema.d.ts +79 -0
  259. package/dist/utils/schema.d.ts.map +1 -0
  260. package/dist/utils/schema.js +41 -0
  261. package/dist/utils/schema.js.map +1 -0
  262. package/dist/utils/service.d.ts +32 -0
  263. package/dist/utils/service.d.ts.map +1 -0
  264. package/dist/utils/service.js +384 -0
  265. package/dist/utils/service.js.map +1 -0
  266. package/docs/SDK_SPEC.md +1645 -0
  267. package/infra/firestore.base.indexes.json +138 -0
  268. package/infra/firestore.base.rules +152 -0
  269. package/infra/storage.base.rules +8 -0
  270. package/package.json +59 -13
  271. package/src/ai/index.ts +2 -0
  272. package/src/ai/schema.ts +64 -0
  273. package/src/ai/service.ts +317 -0
  274. package/src/ai/utils.ts +38 -0
  275. package/src/attachments/index.ts +3 -0
  276. package/src/attachments/routes.ts +28 -0
  277. package/src/attachments/schema.ts +45 -0
  278. package/src/attachments/service.ts +315 -0
  279. package/src/attachments/utils.ts +115 -0
  280. package/src/auth/index.ts +3 -0
  281. package/src/auth/oidc.ts +419 -0
  282. package/src/auth/recovery.ts +365 -0
  283. package/src/auth/routes.ts +142 -0
  284. package/src/auth/schema.ts +181 -0
  285. package/src/auth/service.ts +183 -0
  286. package/src/auth/session.ts +414 -0
  287. package/src/auth/utils.ts +106 -0
  288. package/src/chat/context.ts +310 -0
  289. package/src/chat/index.ts +3 -0
  290. package/src/chat/routes.ts +264 -0
  291. package/src/chat/schema.ts +295 -0
  292. package/src/chat/service.ts +360 -0
  293. package/src/chat/stream.ts +481 -0
  294. package/src/chat/utils.ts +370 -0
  295. package/src/cli/builder.ts +93 -0
  296. package/src/cli/deployer.ts +184 -0
  297. package/src/cli/dev-server.ts +77 -0
  298. package/src/cli/env.ts +89 -0
  299. package/src/cli/init.ts +125 -0
  300. package/src/config.ts +447 -0
  301. package/src/db/index.ts +2 -0
  302. package/src/db/schema.ts +409 -0
  303. package/src/db/service.ts +477 -0
  304. package/src/db/utils.ts +250 -0
  305. package/src/emails/index.ts +3 -0
  306. package/src/emails/routes.ts +64 -0
  307. package/src/emails/schema.ts +239 -0
  308. package/src/emails/service.ts +488 -0
  309. package/src/emails/utils.ts +449 -0
  310. package/src/files/index.ts +3 -0
  311. package/src/files/routes.ts +44 -0
  312. package/src/files/schema.ts +76 -0
  313. package/src/files/service.ts +238 -0
  314. package/src/files/utils.ts +77 -0
  315. package/src/index.ts +425 -0
  316. package/src/payments/billing.ts +488 -0
  317. package/src/payments/index.ts +3 -0
  318. package/src/payments/routes.ts +92 -0
  319. package/src/payments/schema.ts +401 -0
  320. package/src/payments/service.ts +445 -0
  321. package/src/payments/utils.ts +472 -0
  322. package/src/router/guard.ts +319 -0
  323. package/src/router/index.ts +2 -0
  324. package/src/router/schema.ts +458 -0
  325. package/src/router/service.ts +453 -0
  326. package/src/tasks/index.ts +11 -0
  327. package/src/tasks/routes.ts +230 -0
  328. package/src/tasks/schema.ts +310 -0
  329. package/src/tasks/service.ts +254 -0
  330. package/src/tasks/steps.ts +472 -0
  331. package/src/tasks/utils.ts +445 -0
  332. package/src/tasks/watcher.ts +307 -0
  333. package/src/tasks/worker.ts +461 -0
  334. package/src/utils/index.ts +2 -0
  335. package/src/utils/schema.ts +69 -0
  336. package/src/utils/service.ts +461 -0
  337. package/tests/e2e.test.ts +2531 -0
  338. package/tsconfig.base.json +12 -0
  339. package/README.md +0 -2
  340. package/index.d.ts +0 -1
  341. package/index.js +0 -1
@@ -0,0 +1,472 @@
1
+ import { createHash, createHmac, timingSafeEqual } from 'node:crypto'
2
+ import {
3
+ getPlanDefinition, getProductDefinition, type CustomLineItem, type EnvScopedString, type NormalizedPaymentWebhook,
4
+ type PaymentProvider, type PaymentTransactionType, type PlanDefinition, type PlanQuotaSpec, type SubscriptionStatus
5
+ } from './schema'
6
+
7
+ export const resolveEnvScopedValue = (val: EnvScopedString | undefined, isDev: boolean): string | undefined =>
8
+ typeof val === 'string' ? val : val ? (isDev ? val.dev : val.prod) : undefined
9
+
10
+ export const currentPeriodKey = (periodStartIso?: string): string =>
11
+ periodStartIso && /^\d{4}-\d{2}/.test(periodStartIso) ? periodStartIso.slice(0, 7) : new Date().toISOString().slice(0, 7)
12
+
13
+ export const paymentUsageDocId = (userId: string, periodKey: string): string => `${userId}_${periodKey}`
14
+
15
+ export const paymentTxDocId = (provider: PaymentProvider, externalId: string): string =>
16
+ `tx_${createHash('sha256').update(`${provider}:${externalId}`).digest('hex').slice(0, 40)}`
17
+
18
+ export const idempotencyPaymentKey = (prefix: string, rawKey?: string): string | undefined =>
19
+ rawKey ? `${prefix}_${createHash('sha256').update(rawKey.trim()).digest('hex').slice(0, 32)}` : undefined
20
+
21
+ export const defaultFallbackPlan = (defaultPlanId: string, defaultCurrency: string): PlanDefinition & { id: string; quota: PlanQuotaSpec } => ({
22
+ id: defaultPlanId,
23
+ name: defaultPlanId === 'free' ? 'Free Plan' : defaultPlanId,
24
+ interval: 'free',
25
+ amount: 0,
26
+ currency: defaultCurrency.toLowerCase() as Lowercase<string>,
27
+ quota: { overage: { enabled: false } }
28
+ })
29
+
30
+ export const encodeStripeForm = (data: Record<string, unknown>, prefix = '', out = new URLSearchParams()): URLSearchParams => {
31
+ for (const [key, value] of Object.entries(data)) {
32
+ if (value === undefined || value === null) continue
33
+ const formKey = prefix ? `${prefix}[${key}]` : key
34
+ if (Array.isArray(value)) {
35
+ value.forEach((item, idx) => {
36
+ if (item !== null && typeof item === 'object') encodeStripeForm(item as Record<string, unknown>, `${formKey}[${idx}]`, out)
37
+ else if (item !== undefined) out.append(`${formKey}[${idx}]`, String(item))
38
+ })
39
+ } else if (typeof value === 'object') {
40
+ encodeStripeForm(value as Record<string, unknown>, formKey, out)
41
+ } else {
42
+ out.append(formKey, String(value))
43
+ }
44
+ }
45
+ return out
46
+ }
47
+
48
+ const executePaymentFetch = async <T>(label: string, url: string, method: string, headers: Record<string, string>, body?: string): Promise<T> => {
49
+ const res = await fetch(url, { method, headers, ...(body ? { body } : {}), signal: AbortSignal.timeout(15_000) })
50
+ if (!res.ok) {
51
+ const err = new Error(`${label} HTTP ${res.status}: ${await res.text().catch(() => '')}`)
52
+ ;(err as { status?: number }).status = res.status
53
+ throw err
54
+ }
55
+ return (await res.json().catch(() => ({}))) as T
56
+ }
57
+
58
+ export const requestStripeApi = async <T = Record<string, any>>(
59
+ baseUrl: string,
60
+ apiKey: string,
61
+ apiVersion: string,
62
+ method: string,
63
+ path: string,
64
+ formParams?: Record<string, unknown>,
65
+ idempotencyKey?: string
66
+ ): Promise<T> => {
67
+ const url = `${baseUrl.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}`
68
+ const isGet = method.toUpperCase() === 'GET'
69
+ const encoded = formParams ? encodeStripeForm(formParams).toString() : ''
70
+ return executePaymentFetch<T>(
71
+ 'Stripe',
72
+ isGet && encoded ? `${url}?${encoded}` : url,
73
+ method.toUpperCase(),
74
+ {
75
+ Authorization: `Bearer ${apiKey}`,
76
+ 'Stripe-Version': apiVersion,
77
+ ...(!isGet ? { 'content-type': 'application/x-www-form-urlencoded' } : {}),
78
+ ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {})
79
+ },
80
+ !isGet && encoded ? encoded : undefined
81
+ )
82
+ }
83
+
84
+ export const requestDodoApi = async <T = Record<string, any>>(
85
+ baseUrl: string,
86
+ apiKey: string,
87
+ method: string,
88
+ path: string,
89
+ body?: unknown,
90
+ idempotencyKey?: string
91
+ ): Promise<T> => {
92
+ const url = `${baseUrl.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}`
93
+ const isGet = method.toUpperCase() === 'GET'
94
+ return executePaymentFetch<T>(
95
+ 'Dodo',
96
+ url,
97
+ method.toUpperCase(),
98
+ { Authorization: `Bearer ${apiKey}`, 'content-type': 'application/json', ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}) },
99
+ !isGet && body !== undefined ? JSON.stringify(body) : undefined
100
+ )
101
+ }
102
+
103
+ const constantTimeEqual = (a: string, b: string): boolean => {
104
+ if (!a || !b) return false
105
+ const bufA = createHash('sha256').update(a).digest()
106
+ const bufB = createHash('sha256').update(b).digest()
107
+ return timingSafeEqual(bufA, bufB)
108
+ }
109
+
110
+ const getHeader = (headers: Record<string, string | undefined>, name: string): string | undefined => {
111
+ const lower = name.toLowerCase()
112
+ for (const [k, v] of Object.entries(headers)) {
113
+ if (k.toLowerCase() === lower && v) return v.trim()
114
+ }
115
+ return undefined
116
+ }
117
+
118
+ export const verifyStripeWebhookHmac = (
119
+ rawBody: string,
120
+ headers: Record<string, string | undefined>,
121
+ secret: string,
122
+ toleranceSeconds: number
123
+ ): Record<string, any> => {
124
+ const sigHeader = getHeader(headers, 'stripe-signature')
125
+ if (!sigHeader || !secret) {
126
+ throw new Error('Missing Stripe-Signature header or webhook secret')
127
+ }
128
+ const parts = sigHeader.split(',').map((p) => p.trim())
129
+ const tsPart = parts.find((p) => p.startsWith('t='))?.slice(2)
130
+ const v1Sigs = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3))
131
+ if (!tsPart || v1Sigs.length === 0) {
132
+ throw new Error('Malformed Stripe-Signature header')
133
+ }
134
+ const tsSeconds = Number(tsPart)
135
+ const nowSeconds = Math.floor(Date.now() / 1000)
136
+ if (!Number.isFinite(tsSeconds) || Math.abs(nowSeconds - tsSeconds) > toleranceSeconds) {
137
+ throw new Error('Stripe webhook timestamp outside tolerance window')
138
+ }
139
+ const expectedHex = createHmac('sha256', secret).update(`${tsPart}.${rawBody}`, 'utf8').digest('hex')
140
+ if (!v1Sigs.some((sig) => constantTimeEqual(sig, expectedHex))) {
141
+ throw new Error('Invalid Stripe webhook HMAC signature')
142
+ }
143
+ return JSON.parse(rawBody) as Record<string, any>
144
+ }
145
+
146
+ export const verifyDodoWebhookHmac = (
147
+ rawBody: string,
148
+ headers: Record<string, string | undefined>,
149
+ secret: string,
150
+ toleranceSeconds: number
151
+ ): Record<string, any> => {
152
+ const msgId = getHeader(headers, 'webhook-id') ?? getHeader(headers, 'svix-id')
153
+ const msgTimestamp = getHeader(headers, 'webhook-timestamp') ?? getHeader(headers, 'svix-timestamp')
154
+ const msgSignature = getHeader(headers, 'webhook-signature') ?? getHeader(headers, 'svix-signature')
155
+ if (!msgId || !msgTimestamp || !msgSignature || !secret) {
156
+ throw new Error('Missing Dodo Standard Webhooks signature headers or secret')
157
+ }
158
+ const tsSeconds = Number(msgTimestamp)
159
+ const nowSeconds = Math.floor(Date.now() / 1000)
160
+ if (!Number.isFinite(tsSeconds) || Math.abs(nowSeconds - tsSeconds) > toleranceSeconds) {
161
+ throw new Error('Dodo webhook timestamp outside tolerance window')
162
+ }
163
+ const signedContent = `${msgId}.${msgTimestamp}.${rawBody}`
164
+ const rawSecret = secret.startsWith('whsec_') ? secret.slice(6) : secret
165
+ const expectedDecoded = createHmac('sha256', Buffer.from(rawSecret, 'base64'))
166
+ .update(signedContent, 'utf8')
167
+ .digest('base64')
168
+ const expectedUtf8 = createHmac('sha256', Buffer.from(secret, 'utf8'))
169
+ .update(signedContent, 'utf8')
170
+ .digest('base64')
171
+ const candidates = msgSignature
172
+ .split(/\s+/)
173
+ .map((part) => (part.startsWith('v1,') ? part.slice(3) : part.trim()))
174
+ .filter(Boolean)
175
+ if (!candidates.some((sig) => constantTimeEqual(sig, expectedDecoded) || constantTimeEqual(sig, expectedUtf8))) {
176
+ throw new Error('Invalid Dodo webhook HMAC signature')
177
+ }
178
+ const parsed = JSON.parse(rawBody) as Record<string, any>
179
+ return { ...parsed, _webhookId: msgId }
180
+ }
181
+
182
+ const STRIPE_EVENT_TX_MAP: Record<string, PaymentTransactionType> = {
183
+ 'checkout.session.completed': 'checkout.completed',
184
+ 'payment_intent.succeeded': 'payment.succeeded',
185
+ 'payment_intent.payment_failed': 'payment.failed',
186
+ 'invoice.created': 'invoice.created',
187
+ 'invoice.paid': 'invoice.paid',
188
+ 'invoice.payment_failed': 'subscription.past_due',
189
+ 'customer.subscription.created': 'subscription.active',
190
+ 'customer.subscription.updated': 'subscription.tier_changed',
191
+ 'customer.subscription.deleted': 'subscription.cancelled'
192
+ }
193
+
194
+ const DODO_EVENT_TX_MAP: Record<string, PaymentTransactionType> = {
195
+ 'payment.succeeded': 'payment.succeeded',
196
+ 'payment.failed': 'payment.failed',
197
+ 'subscription.active': 'subscription.active',
198
+ 'subscription.renewed': 'subscription.renewed',
199
+ 'subscription.plan_changed': 'subscription.tier_changed',
200
+ 'subscription.on_hold': 'subscription.past_due',
201
+ 'subscription.cancelled': 'subscription.cancelled',
202
+ 'subscription.expired': 'subscription.cancelled'
203
+ }
204
+
205
+ const mapSubStatus = (rawStatus?: string): SubscriptionStatus | undefined => {
206
+ if (!rawStatus) return undefined
207
+ const s = rawStatus.toLowerCase()
208
+ if (s === 'active') return 'active'
209
+ if (s === 'trialing') return 'trialing'
210
+ if (s === 'past_due' || s === 'unpaid') return 'past_due'
211
+ if (s === 'on_hold' || s === 'paused') return 'on_hold'
212
+ if (s === 'canceled' || s === 'cancelled') return 'cancelled'
213
+ if (s === 'expired' || s === 'incomplete_expired') return 'expired'
214
+ return undefined
215
+ }
216
+
217
+ const toIsoIfEpochOrString = (val: unknown): string | undefined => {
218
+ if (typeof val === 'number' && Number.isFinite(val)) {
219
+ return new Date(val > 1e11 ? val : val * 1000).toISOString()
220
+ }
221
+ if (typeof val === 'string' && val.trim()) {
222
+ const d = new Date(val)
223
+ return Number.isNaN(d.getTime()) ? undefined : d.toISOString()
224
+ }
225
+ return undefined
226
+ }
227
+
228
+ export const normalizePaymentWebhook = (
229
+ provider: PaymentProvider,
230
+ rawBody: string,
231
+ headers: Record<string, string | undefined>,
232
+ secret: string,
233
+ toleranceSeconds: number
234
+ ): NormalizedPaymentWebhook => {
235
+ const raw =
236
+ provider === 'stripe'
237
+ ? verifyStripeWebhookHmac(rawBody, headers, secret, toleranceSeconds)
238
+ : verifyDodoWebhookHmac(rawBody, headers, secret, toleranceSeconds)
239
+
240
+ const rawEventType = String(raw.type ?? raw.event_type ?? '')
241
+ const obj = (raw.data?.object ?? raw.data ?? raw) as Record<string, any>
242
+ const meta = (obj.metadata ?? raw.metadata ?? {}) as Record<string, string | undefined>
243
+ const eventId = String(raw.id ?? raw.event_id ?? raw._webhookId ?? obj.id ?? `evt_${Date.now()}`)
244
+ const txType =
245
+ (provider === 'stripe' ? STRIPE_EVENT_TX_MAP[rawEventType] : DODO_EVENT_TX_MAP[rawEventType]) ?? null
246
+ const paymentId =
247
+ typeof obj.payment_intent === 'string'
248
+ ? obj.payment_intent
249
+ : typeof obj.payment_id === 'string'
250
+ ? obj.payment_id
251
+ : txType === 'payment.succeeded' && typeof obj.id === 'string'
252
+ ? obj.id
253
+ : undefined
254
+
255
+ const customerId =
256
+ typeof obj.customer === 'string'
257
+ ? obj.customer
258
+ : typeof obj.customer?.customer_id === 'string'
259
+ ? obj.customer.customer_id
260
+ : typeof obj.customer_id === 'string'
261
+ ? obj.customer_id
262
+ : undefined
263
+
264
+ const subscriptionId =
265
+ typeof obj.subscription === 'string'
266
+ ? obj.subscription
267
+ : typeof obj.subscription_id === 'string'
268
+ ? obj.subscription_id
269
+ : rawEventType.includes('subscription') && typeof obj.id === 'string'
270
+ ? obj.id
271
+ : undefined
272
+
273
+ const subStatus =
274
+ mapSubStatus(obj.status) ??
275
+ (txType === 'subscription.active' || txType === 'subscription.renewed'
276
+ ? 'active'
277
+ : txType === 'subscription.cancelled'
278
+ ? 'cancelled'
279
+ : txType === 'subscription.past_due'
280
+ ? 'past_due'
281
+ : undefined)
282
+
283
+ const amountRaw = obj.amount_total ?? obj.amount_paid ?? obj.amount ?? obj.total_amount
284
+ const amount = typeof amountRaw === 'number' ? Math.round(amountRaw) : undefined
285
+ const currency = typeof obj.currency === 'string' ? obj.currency.toLowerCase() : undefined
286
+ const currentPeriodStart = toIsoIfEpochOrString(obj.current_period_start ?? obj.previous_billing_date)
287
+ const currentPeriodEnd = toIsoIfEpochOrString(obj.current_period_end ?? obj.next_billing_date)
288
+ const cancelAtPeriodEnd =
289
+ typeof obj.cancel_at_period_end === 'boolean'
290
+ ? obj.cancel_at_period_end
291
+ : typeof obj.cancel_at_next_billing_date === 'boolean'
292
+ ? obj.cancel_at_next_billing_date
293
+ : undefined
294
+
295
+ return {
296
+ eventId,
297
+ ...(paymentId ? { paymentId } : {}),
298
+ rawEventType,
299
+ txType,
300
+ ...(meta.userId ? { userId: meta.userId } : {}),
301
+ ...(customerId ? { customerId } : {}),
302
+ ...(subscriptionId ? { subscriptionId } : {}),
303
+ ...(subStatus ? { subscriptionStatus: subStatus } : {}),
304
+ ...(meta.planId ? { planId: meta.planId } : {}),
305
+ ...(meta.productId ? { productId: meta.productId } : {}),
306
+ ...(amount !== undefined ? { amount } : {}),
307
+ ...(currency ? { currency } : {}),
308
+ ...(currentPeriodStart ? { currentPeriodStart } : {}),
309
+ ...(currentPeriodEnd ? { currentPeriodEnd } : {}),
310
+ ...(cancelAtPeriodEnd !== undefined ? { cancelAtPeriodEnd } : {}),
311
+ resetPeriodUsage: txType === 'subscription.renewed' || rawEventType === 'invoice.paid'
312
+ }
313
+ }
314
+
315
+ export interface ResolvedPurchaseItem {
316
+ kind: 'product' | 'plan' | 'custom'
317
+ id?: string
318
+ name: string
319
+ description?: string
320
+ amount: number
321
+ currency: string
322
+ quantity: number
323
+ interval?: 'month' | 'year' | 'free'
324
+ stripePriceId?: string
325
+ dodoProductId?: string
326
+ metadata: Record<string, string>
327
+ }
328
+
329
+ export const resolvePurchaseItem = (
330
+ input: {
331
+ product?: string
332
+ plan?: string
333
+ custom?: CustomLineItem
334
+ quantity?: number
335
+ amount?: number
336
+ currency?: string
337
+ description?: string
338
+ metadata?: Record<string, string>
339
+ },
340
+ userId: string,
341
+ isDev: boolean,
342
+ defaultCurrency: string
343
+ ): ResolvedPurchaseItem | { error: string } => {
344
+ const quantity = input.quantity ?? input.custom?.quantity ?? 1
345
+ if (input.product) {
346
+ const prod = getProductDefinition(input.product)
347
+ if (!prod) return { error: `Unknown product alias: ${input.product}` }
348
+ const stripePriceId = resolveEnvScopedValue(prod.providers?.stripe?.priceId, isDev)
349
+ const dodoProductId = resolveEnvScopedValue(prod.providers?.dodo?.productId, isDev)
350
+ return {
351
+ kind: 'product',
352
+ id: prod.id,
353
+ name: prod.name,
354
+ ...(prod.description ? { description: prod.description } : {}),
355
+ amount: prod.amount,
356
+ currency: (prod.currency ?? defaultCurrency).toLowerCase(),
357
+ quantity,
358
+ ...(stripePriceId ? { stripePriceId } : {}),
359
+ ...(dodoProductId ? { dodoProductId } : {}),
360
+ metadata: { ...prod.metadata, ...input.metadata, userId, productId: prod.id }
361
+ }
362
+ }
363
+ if (input.plan) {
364
+ const plan = getPlanDefinition(input.plan)
365
+ if (!plan) return { error: `Unknown plan alias: ${input.plan}` }
366
+ const stripePriceId = resolveEnvScopedValue(plan.providers?.stripe?.priceId, isDev)
367
+ const dodoProductId = resolveEnvScopedValue(plan.providers?.dodo?.productId, isDev)
368
+ return {
369
+ kind: 'plan',
370
+ id: plan.id,
371
+ name: plan.name,
372
+ ...(plan.description ? { description: plan.description } : {}),
373
+ amount: plan.amount,
374
+ currency: (plan.currency ?? defaultCurrency).toLowerCase(),
375
+ quantity,
376
+ interval: plan.interval,
377
+ ...(stripePriceId ? { stripePriceId } : {}),
378
+ ...(dodoProductId ? { dodoProductId } : {}),
379
+ metadata: { ...plan.metadata, ...input.metadata, userId, planId: plan.id }
380
+ }
381
+ }
382
+ const c = input.custom
383
+ const amount = c?.amount ?? input.amount ?? 0
384
+ const name = c?.name ?? input.description ?? 'Custom Purchase'
385
+ const description = c?.description ?? input.description
386
+ return {
387
+ kind: 'custom',
388
+ name,
389
+ ...(description ? { description } : {}),
390
+ amount,
391
+ currency: (c?.currency ?? input.currency ?? defaultCurrency).toLowerCase(),
392
+ quantity,
393
+ metadata: { ...input.metadata, userId }
394
+ }
395
+ }
396
+
397
+ export const buildStripeLineItem = (item: ResolvedPurchaseItem): Record<string, unknown> =>
398
+ item.stripePriceId
399
+ ? { price: item.stripePriceId, quantity: item.quantity }
400
+ : {
401
+ quantity: item.quantity,
402
+ price_data: {
403
+ currency: item.currency,
404
+ unit_amount: item.amount,
405
+ product_data: {
406
+ name: item.name,
407
+ ...(item.description ? { description: item.description } : {})
408
+ },
409
+ ...(item.kind === 'plan' && item.interval && item.interval !== 'free'
410
+ ? { recurring: { interval: item.interval } }
411
+ : {})
412
+ }
413
+ }
414
+
415
+ export const buildDodoCartOrCustom = (item: ResolvedPurchaseItem): Record<string, unknown> =>
416
+ item.dodoProductId
417
+ ? { product_cart: [{ product_id: item.dodoProductId, quantity: item.quantity }] }
418
+ : {
419
+ billing_currency: item.currency,
420
+ custom_item: {
421
+ name: item.name,
422
+ ...(item.description ? { description: item.description } : {}),
423
+ amount: item.amount * item.quantity,
424
+ currency: item.currency,
425
+ quantity: item.quantity
426
+ }
427
+ }
428
+
429
+ export const computeUsageDelta = (
430
+ usageData: Record<string, unknown>,
431
+ custData: Record<string, unknown>,
432
+ plan: PlanDefinition & { quota: PlanQuotaSpec },
433
+ parsed: { metric: string; amount: number; inputTokens?: number; outputTokens?: number },
434
+ skipMutation = false
435
+ ) => {
436
+ let aiTokensUsed = Number(usageData.aiTokensUsed ?? 0)
437
+ let aiInputTokens = Number(usageData.aiInputTokens ?? 0)
438
+ let aiOutputTokens = Number(usageData.aiOutputTokens ?? 0)
439
+ let overageTokensReported = Number(usageData.overageTokensReported ?? 0)
440
+ let bonusAiTokens = Number(custData.bonusAiTokens ?? 0)
441
+ const metricsUsed: Record<string, number> = { ...(usageData.metricsUsed as Record<string, number> | undefined) }
442
+ const bonusMetrics: Record<string, number> = { ...(custData.bonusMetrics as Record<string, number> | undefined) }
443
+ let overageDelta = 0
444
+ if (skipMutation) return { aiTokensUsed, aiInputTokens, aiOutputTokens, overageTokensReported, bonusAiTokens, metricsUsed, bonusMetrics, overageDelta }
445
+
446
+ if (parsed.metric === 'ai_tokens') {
447
+ const limit = plan.quota.aiTokens
448
+ const prevUncovered = typeof limit === 'number' ? Math.max(0, aiTokensUsed - limit) : 0
449
+ aiTokensUsed += Math.round(parsed.amount)
450
+ aiInputTokens += parsed.inputTokens ?? 0
451
+ aiOutputTokens += parsed.outputTokens ?? 0
452
+ const nextUncovered = typeof limit === 'number' ? Math.max(0, aiTokensUsed - limit) : 0
453
+ const uncoveredIncrease = Math.max(0, nextUncovered - prevUncovered)
454
+ const bonusDeducted = Math.min(bonusAiTokens, uncoveredIncrease)
455
+ bonusAiTokens -= bonusDeducted
456
+ overageDelta = uncoveredIncrease - bonusDeducted
457
+ if (overageDelta > 0 && plan.quota.overage?.enabled) overageTokensReported += overageDelta
458
+ } else {
459
+ const limit = plan.quota.metrics?.[parsed.metric]
460
+ const prev = metricsUsed[parsed.metric] ?? 0
461
+ const prevUncovered = typeof limit === 'number' ? Math.max(0, prev - limit) : 0
462
+ const next = prev + parsed.amount
463
+ metricsUsed[parsed.metric] = next
464
+ const nextUncovered = typeof limit === 'number' ? Math.max(0, next - limit) : 0
465
+ const uncoveredIncrease = Math.max(0, nextUncovered - prevUncovered)
466
+ const availBonus = bonusMetrics[parsed.metric] ?? 0
467
+ const bonusDeducted = Math.min(availBonus, uncoveredIncrease)
468
+ if (bonusDeducted > 0) bonusMetrics[parsed.metric] = availBonus - bonusDeducted
469
+ }
470
+ return { aiTokensUsed, aiInputTokens, aiOutputTokens, overageTokensReported, bonusAiTokens, metricsUsed, bonusMetrics, overageDelta }
471
+ }
472
+