@jonsoc/console-app 1.1.34

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 (217) hide show
  1. package/.opencode/agent/css.md +149 -0
  2. package/README.md +32 -0
  3. package/package.json +49 -0
  4. package/public/apple-touch-icon-v3.png +1 -0
  5. package/public/apple-touch-icon.png +1 -0
  6. package/public/email +1 -0
  7. package/public/favicon-96x96-v3.png +1 -0
  8. package/public/favicon-96x96.png +1 -0
  9. package/public/favicon-v3.ico +1 -0
  10. package/public/favicon-v3.svg +1 -0
  11. package/public/favicon.ico +1 -0
  12. package/public/favicon.svg +1 -0
  13. package/public/opencode-brand-assets.zip +0 -0
  14. package/public/robots.txt +6 -0
  15. package/public/site.webmanifest +1 -0
  16. package/public/social-share-black.png +1 -0
  17. package/public/social-share-zen.png +1 -0
  18. package/public/social-share.png +1 -0
  19. package/public/theme.json +182 -0
  20. package/public/web-app-manifest-192x192.png +1 -0
  21. package/public/web-app-manifest-512x512.png +1 -0
  22. package/script/generate-sitemap.ts +103 -0
  23. package/src/app.css +1 -0
  24. package/src/app.tsx +27 -0
  25. package/src/asset/black/hero.png +0 -0
  26. package/src/asset/brand/opencode-brand-assets.zip +0 -0
  27. package/src/asset/brand/opencode-logo-dark.png +0 -0
  28. package/src/asset/brand/opencode-logo-dark.svg +16 -0
  29. package/src/asset/brand/opencode-logo-light.png +0 -0
  30. package/src/asset/brand/opencode-logo-light.svg +16 -0
  31. package/src/asset/brand/opencode-wordmark-dark.png +0 -0
  32. package/src/asset/brand/opencode-wordmark-dark.svg +30 -0
  33. package/src/asset/brand/opencode-wordmark-light.png +0 -0
  34. package/src/asset/brand/opencode-wordmark-light.svg +30 -0
  35. package/src/asset/brand/opencode-wordmark-simple-dark.png +0 -0
  36. package/src/asset/brand/opencode-wordmark-simple-dark.svg +22 -0
  37. package/src/asset/brand/opencode-wordmark-simple-light.png +0 -0
  38. package/src/asset/brand/opencode-wordmark-simple-light.svg +22 -0
  39. package/src/asset/brand/preview-opencode-dark.png +0 -0
  40. package/src/asset/brand/preview-opencode-logo-dark.png +0 -0
  41. package/src/asset/brand/preview-opencode-logo-light.png +0 -0
  42. package/src/asset/brand/preview-opencode-wordmark-dark.png +0 -0
  43. package/src/asset/brand/preview-opencode-wordmark-light.png +0 -0
  44. package/src/asset/brand/preview-opencode-wordmark-simple-dark.png +0 -0
  45. package/src/asset/brand/preview-opencode-wordmark-simple-light.png +0 -0
  46. package/src/asset/lander/avatar-adam.png +0 -0
  47. package/src/asset/lander/avatar-david.png +0 -0
  48. package/src/asset/lander/avatar-dax.png +0 -0
  49. package/src/asset/lander/avatar-frank.png +0 -0
  50. package/src/asset/lander/avatar-jay.png +0 -0
  51. package/src/asset/lander/brand-assets-dark.svg +10 -0
  52. package/src/asset/lander/brand-assets-light.svg +10 -0
  53. package/src/asset/lander/brand.png +0 -0
  54. package/src/asset/lander/check.svg +3 -0
  55. package/src/asset/lander/copy.svg +3 -0
  56. package/src/asset/lander/desktop-app-icon.png +0 -0
  57. package/src/asset/lander/dock.png +0 -0
  58. package/src/asset/lander/logo-dark.svg +11 -0
  59. package/src/asset/lander/logo-light.svg +11 -0
  60. package/src/asset/lander/opencode-comparison-min.mp4 +0 -0
  61. package/src/asset/lander/opencode-comparison-poster.png +0 -0
  62. package/src/asset/lander/opencode-desktop-icon.png +0 -0
  63. package/src/asset/lander/opencode-logo-dark.svg +11 -0
  64. package/src/asset/lander/opencode-logo-light.svg +11 -0
  65. package/src/asset/lander/opencode-min.mp4 +0 -0
  66. package/src/asset/lander/opencode-poster.png +0 -0
  67. package/src/asset/lander/opencode-wordmark-dark.svg +25 -0
  68. package/src/asset/lander/opencode-wordmark-light.svg +25 -0
  69. package/src/asset/lander/screenshot-github.png +0 -0
  70. package/src/asset/lander/screenshot-splash.png +0 -0
  71. package/src/asset/lander/screenshot-vscode.png +0 -0
  72. package/src/asset/lander/screenshot.png +0 -0
  73. package/src/asset/lander/wordmark-dark.svg +3 -0
  74. package/src/asset/lander/wordmark-light.svg +3 -0
  75. package/src/asset/logo-ornate-dark.svg +18 -0
  76. package/src/asset/logo-ornate-light.svg +18 -0
  77. package/src/asset/logo.svg +18 -0
  78. package/src/asset/zen-ornate-dark.svg +8 -0
  79. package/src/asset/zen-ornate-light.svg +8 -0
  80. package/src/component/dropdown.css +80 -0
  81. package/src/component/dropdown.tsx +79 -0
  82. package/src/component/email-signup.tsx +48 -0
  83. package/src/component/faq.tsx +33 -0
  84. package/src/component/footer.tsx +38 -0
  85. package/src/component/header-context-menu.css +63 -0
  86. package/src/component/header.tsx +279 -0
  87. package/src/component/icon.tsx +257 -0
  88. package/src/component/legal.tsx +20 -0
  89. package/src/component/modal.css +66 -0
  90. package/src/component/modal.tsx +24 -0
  91. package/src/component/spotlight.css +15 -0
  92. package/src/component/spotlight.tsx +820 -0
  93. package/src/config.ts +29 -0
  94. package/src/context/auth.session.ts +0 -0
  95. package/src/context/auth.ts +116 -0
  96. package/src/context/auth.withActor.ts +7 -0
  97. package/src/entry-client.tsx +4 -0
  98. package/src/entry-server.tsx +30 -0
  99. package/src/global.d.ts +5 -0
  100. package/src/lib/github.ts +38 -0
  101. package/src/middleware.ts +5 -0
  102. package/src/routes/[...404].css +130 -0
  103. package/src/routes/[...404].tsx +38 -0
  104. package/src/routes/api/enterprise.ts +47 -0
  105. package/src/routes/auth/[...callback].ts +41 -0
  106. package/src/routes/auth/authorize.ts +10 -0
  107. package/src/routes/auth/index.ts +12 -0
  108. package/src/routes/auth/logout.ts +17 -0
  109. package/src/routes/auth/status.ts +7 -0
  110. package/src/routes/bench/[id].tsx +365 -0
  111. package/src/routes/bench/index.tsx +86 -0
  112. package/src/routes/bench/submission.ts +29 -0
  113. package/src/routes/black/common.tsx +62 -0
  114. package/src/routes/black/index.tsx +108 -0
  115. package/src/routes/black/subscribe/[plan].tsx +449 -0
  116. package/src/routes/black/workspace.css +214 -0
  117. package/src/routes/black/workspace.tsx +229 -0
  118. package/src/routes/black.css +828 -0
  119. package/src/routes/black.tsx +285 -0
  120. package/src/routes/brand/index.css +555 -0
  121. package/src/routes/brand/index.tsx +252 -0
  122. package/src/routes/changelog/index.css +477 -0
  123. package/src/routes/changelog/index.tsx +147 -0
  124. package/src/routes/debug/index.ts +13 -0
  125. package/src/routes/desktop-feedback.ts +5 -0
  126. package/src/routes/discord.ts +5 -0
  127. package/src/routes/docs/[...path].ts +20 -0
  128. package/src/routes/docs/index.ts +20 -0
  129. package/src/routes/download/[platform].ts +38 -0
  130. package/src/routes/download/index.css +750 -0
  131. package/src/routes/download/index.tsx +482 -0
  132. package/src/routes/download/types.ts +4 -0
  133. package/src/routes/enterprise/index.css +578 -0
  134. package/src/routes/enterprise/index.tsx +251 -0
  135. package/src/routes/index.css +1251 -0
  136. package/src/routes/index.tsx +840 -0
  137. package/src/routes/legal/privacy-policy/index.css +343 -0
  138. package/src/routes/legal/privacy-policy/index.tsx +1512 -0
  139. package/src/routes/legal/terms-of-service/index.css +254 -0
  140. package/src/routes/legal/terms-of-service/index.tsx +512 -0
  141. package/src/routes/openapi.json.ts +7 -0
  142. package/src/routes/s/[id].ts +20 -0
  143. package/src/routes/stripe/webhook.ts +532 -0
  144. package/src/routes/t/[...path].tsx +20 -0
  145. package/src/routes/temp.tsx +172 -0
  146. package/src/routes/user-menu.css +18 -0
  147. package/src/routes/user-menu.tsx +32 -0
  148. package/src/routes/workspace/[id]/billing/billing-section.module.css +185 -0
  149. package/src/routes/workspace/[id]/billing/billing-section.tsx +240 -0
  150. package/src/routes/workspace/[id]/billing/black-section.module.css +142 -0
  151. package/src/routes/workspace/[id]/billing/black-section.tsx +269 -0
  152. package/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +23 -0
  153. package/src/routes/workspace/[id]/billing/index.tsx +32 -0
  154. package/src/routes/workspace/[id]/billing/monthly-limit-section.module.css +96 -0
  155. package/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +133 -0
  156. package/src/routes/workspace/[id]/billing/payment-section.module.css +93 -0
  157. package/src/routes/workspace/[id]/billing/payment-section.tsx +122 -0
  158. package/src/routes/workspace/[id]/billing/reload-section.module.css +261 -0
  159. package/src/routes/workspace/[id]/billing/reload-section.tsx +213 -0
  160. package/src/routes/workspace/[id]/graph-section.module.css +145 -0
  161. package/src/routes/workspace/[id]/graph-section.tsx +475 -0
  162. package/src/routes/workspace/[id]/index.tsx +81 -0
  163. package/src/routes/workspace/[id]/keys/index.tsx +11 -0
  164. package/src/routes/workspace/[id]/keys/key-section.module.css +197 -0
  165. package/src/routes/workspace/[id]/keys/key-section.tsx +176 -0
  166. package/src/routes/workspace/[id]/members/index.tsx +11 -0
  167. package/src/routes/workspace/[id]/members/member-section.module.css +249 -0
  168. package/src/routes/workspace/[id]/members/member-section.tsx +343 -0
  169. package/src/routes/workspace/[id]/members/role-dropdown.css +72 -0
  170. package/src/routes/workspace/[id]/members/role-dropdown.tsx +43 -0
  171. package/src/routes/workspace/[id]/model-section.module.css +173 -0
  172. package/src/routes/workspace/[id]/model-section.tsx +174 -0
  173. package/src/routes/workspace/[id]/new-user-section.module.css +143 -0
  174. package/src/routes/workspace/[id]/new-user-section.tsx +104 -0
  175. package/src/routes/workspace/[id]/provider-section.module.css +138 -0
  176. package/src/routes/workspace/[id]/provider-section.tsx +188 -0
  177. package/src/routes/workspace/[id]/settings/index.tsx +11 -0
  178. package/src/routes/workspace/[id]/settings/settings-section.module.css +94 -0
  179. package/src/routes/workspace/[id]/settings/settings-section.tsx +122 -0
  180. package/src/routes/workspace/[id]/usage-section.module.css +185 -0
  181. package/src/routes/workspace/[id]/usage-section.tsx +200 -0
  182. package/src/routes/workspace/[id].css +308 -0
  183. package/src/routes/workspace/[id].tsx +62 -0
  184. package/src/routes/workspace/common.tsx +120 -0
  185. package/src/routes/workspace-picker.css +74 -0
  186. package/src/routes/workspace-picker.tsx +122 -0
  187. package/src/routes/workspace.css +107 -0
  188. package/src/routes/workspace.tsx +38 -0
  189. package/src/routes/zen/index.css +866 -0
  190. package/src/routes/zen/index.tsx +343 -0
  191. package/src/routes/zen/util/dataDumper.ts +44 -0
  192. package/src/routes/zen/util/error.ts +13 -0
  193. package/src/routes/zen/util/handler.ts +784 -0
  194. package/src/routes/zen/util/logger.ts +12 -0
  195. package/src/routes/zen/util/provider/anthropic.ts +752 -0
  196. package/src/routes/zen/util/provider/google.ts +75 -0
  197. package/src/routes/zen/util/provider/openai-compatible.ts +546 -0
  198. package/src/routes/zen/util/provider/openai.ts +630 -0
  199. package/src/routes/zen/util/provider/provider.ts +210 -0
  200. package/src/routes/zen/util/rateLimiter.ts +41 -0
  201. package/src/routes/zen/util/stickyProviderTracker.ts +16 -0
  202. package/src/routes/zen/util/trialLimiter.ts +49 -0
  203. package/src/routes/zen/v1/chat/completions.ts +11 -0
  204. package/src/routes/zen/v1/messages.ts +11 -0
  205. package/src/routes/zen/v1/models/[model].ts +13 -0
  206. package/src/routes/zen/v1/models.ts +60 -0
  207. package/src/routes/zen/v1/responses.ts +11 -0
  208. package/src/style/base.css +21 -0
  209. package/src/style/component/button.css +102 -0
  210. package/src/style/index.css +8 -0
  211. package/src/style/reset.css +76 -0
  212. package/src/style/token/color.css +91 -0
  213. package/src/style/token/font.css +21 -0
  214. package/src/style/token/space.css +46 -0
  215. package/sst-env.d.ts +9 -0
  216. package/tsconfig.json +21 -0
  217. package/vite.config.ts +25 -0
@@ -0,0 +1,532 @@
1
+ import { Billing } from "@jonsoc/console-core/billing.js"
2
+ import type { APIEvent } from "@solidjs/start/server"
3
+ import { and, Database, eq, isNull, sql } from "@jonsoc/console-core/drizzle/index.js"
4
+ import { BillingTable, PaymentTable, SubscriptionTable } from "@jonsoc/console-core/schema/billing.sql.js"
5
+ import { Identifier } from "@jonsoc/console-core/identifier.js"
6
+ import { centsToMicroCents } from "@jonsoc/console-core/util/price.js"
7
+ import { Actor } from "@jonsoc/console-core/actor.js"
8
+ import { Resource } from "@jonsoc/console-resource"
9
+ import { UserTable } from "@jonsoc/console-core/schema/user.sql.js"
10
+ import { AuthTable } from "@jonsoc/console-core/schema/auth.sql.js"
11
+
12
+ export async function POST(input: APIEvent) {
13
+ const body = await Billing.stripe().webhooks.constructEventAsync(
14
+ await input.request.text(),
15
+ input.request.headers.get("stripe-signature")!,
16
+ Resource.STRIPE_WEBHOOK_SECRET.value,
17
+ )
18
+ console.log(body.type, JSON.stringify(body, null, 2))
19
+
20
+ return (async () => {
21
+ if (body.type === "customer.updated") {
22
+ // check default payment method changed
23
+ const prevInvoiceSettings = body.data.previous_attributes?.invoice_settings ?? {}
24
+ if (!("default_payment_method" in prevInvoiceSettings)) return "ignored"
25
+
26
+ const customerID = body.data.object.id
27
+ const paymentMethodID = body.data.object.invoice_settings.default_payment_method as string
28
+
29
+ if (!customerID) throw new Error("Customer ID not found")
30
+ if (!paymentMethodID) throw new Error("Payment method ID not found")
31
+
32
+ const paymentMethod = await Billing.stripe().paymentMethods.retrieve(paymentMethodID)
33
+ await Database.use(async (tx) => {
34
+ await tx
35
+ .update(BillingTable)
36
+ .set({
37
+ paymentMethodID,
38
+ paymentMethodLast4: paymentMethod.card?.last4 ?? null,
39
+ paymentMethodType: paymentMethod.type,
40
+ })
41
+ .where(eq(BillingTable.customerID, customerID))
42
+ })
43
+ }
44
+ if (body.type === "checkout.session.completed" && body.data.object.mode === "payment") {
45
+ const workspaceID = body.data.object.metadata?.workspaceID
46
+ const amountInCents = body.data.object.metadata?.amount && parseInt(body.data.object.metadata?.amount)
47
+ const customerID = body.data.object.customer as string
48
+ const paymentID = body.data.object.payment_intent as string
49
+ const invoiceID = body.data.object.invoice as string
50
+
51
+ if (!workspaceID) throw new Error("Workspace ID not found")
52
+ if (!customerID) throw new Error("Customer ID not found")
53
+ if (!amountInCents) throw new Error("Amount not found")
54
+ if (!paymentID) throw new Error("Payment ID not found")
55
+ if (!invoiceID) throw new Error("Invoice ID not found")
56
+
57
+ await Actor.provide("system", { workspaceID }, async () => {
58
+ const customer = await Billing.get()
59
+ if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
60
+
61
+ // set customer metadata
62
+ if (!customer?.customerID) {
63
+ await Billing.stripe().customers.update(customerID, {
64
+ metadata: {
65
+ workspaceID,
66
+ },
67
+ })
68
+ }
69
+
70
+ // get payment method for the payment intent
71
+ const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
72
+ expand: ["payment_method"],
73
+ })
74
+ const paymentMethod = paymentIntent.payment_method
75
+ if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
76
+
77
+ await Database.transaction(async (tx) => {
78
+ await tx
79
+ .update(BillingTable)
80
+ .set({
81
+ balance: sql`${BillingTable.balance} + ${centsToMicroCents(amountInCents)}`,
82
+ customerID,
83
+ paymentMethodID: paymentMethod.id,
84
+ paymentMethodLast4: paymentMethod.card?.last4 ?? null,
85
+ paymentMethodType: paymentMethod.type,
86
+ // enable reload if first time enabling billing
87
+ ...(customer?.customerID
88
+ ? {}
89
+ : {
90
+ reloadError: null,
91
+ timeReloadError: null,
92
+ }),
93
+ })
94
+ .where(eq(BillingTable.workspaceID, workspaceID))
95
+ await tx.insert(PaymentTable).values({
96
+ workspaceID,
97
+ id: Identifier.create("payment"),
98
+ amount: centsToMicroCents(amountInCents),
99
+ paymentID,
100
+ invoiceID,
101
+ customerID,
102
+ })
103
+ })
104
+ })
105
+ }
106
+ if (body.type === "checkout.session.completed" && body.data.object.mode === "subscription") {
107
+ const workspaceID = body.data.object.custom_fields.find((f) => f.key === "workspaceid")?.text?.value
108
+ const amountInCents = body.data.object.amount_total as number
109
+ const customerID = body.data.object.customer as string
110
+ const customerEmail = body.data.object.customer_details?.email as string
111
+ const invoiceID = body.data.object.invoice as string
112
+ const subscriptionID = body.data.object.subscription as string
113
+ const promoCode = body.data.object.discounts?.[0]?.promotion_code as string
114
+
115
+ if (!workspaceID) throw new Error("Workspace ID not found")
116
+ if (!customerID) throw new Error("Customer ID not found")
117
+ if (!amountInCents) throw new Error("Amount not found")
118
+ if (!invoiceID) throw new Error("Invoice ID not found")
119
+ if (!subscriptionID) throw new Error("Subscription ID not found")
120
+
121
+ // get payment id from invoice
122
+ const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
123
+ expand: ["payments"],
124
+ })
125
+ const paymentID = invoice.payments?.data[0].payment.payment_intent as string
126
+ if (!paymentID) throw new Error("Payment ID not found")
127
+
128
+ // get payment method for the payment intent
129
+ const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
130
+ expand: ["payment_method"],
131
+ })
132
+ const paymentMethod = paymentIntent.payment_method
133
+ if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
134
+
135
+ // get coupon id from promotion code
136
+ const couponID = await (async () => {
137
+ if (!promoCode) return
138
+ const coupon = await Billing.stripe().promotionCodes.retrieve(promoCode)
139
+ const couponID = coupon.coupon.id
140
+ if (!couponID) throw new Error("Coupon not found for promotion code")
141
+ return couponID
142
+ })()
143
+
144
+ // get user
145
+
146
+ await Actor.provide("system", { workspaceID }, async () => {
147
+ // look up current billing
148
+ const billing = await Billing.get()
149
+ if (!billing) throw new Error(`Workspace with ID ${workspaceID} not found`)
150
+
151
+ // Temporarily skip this check because during Black drop, user can checkout
152
+ // as a new customer
153
+ //if (billing.customerID !== customerID) throw new Error("Customer ID mismatch")
154
+
155
+ // Temporarily check the user to apply to. After Black drop, we will allow
156
+ // look up the user to apply to
157
+ const users = await Database.use((tx) =>
158
+ tx
159
+ .select({ id: UserTable.id, email: AuthTable.subject })
160
+ .from(UserTable)
161
+ .innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
162
+ .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
163
+ )
164
+ const user = users.find((u) => u.email === customerEmail) ?? users[0]
165
+ if (!user) {
166
+ console.error(`Error: User with email ${customerEmail} not found in workspace ${workspaceID}`)
167
+ process.exit(1)
168
+ }
169
+
170
+ // set customer metadata
171
+ if (!billing?.customerID) {
172
+ await Billing.stripe().customers.update(customerID, {
173
+ metadata: {
174
+ workspaceID,
175
+ },
176
+ })
177
+ }
178
+
179
+ await Database.transaction(async (tx) => {
180
+ await tx
181
+ .update(BillingTable)
182
+ .set({
183
+ customerID,
184
+ subscriptionID,
185
+ subscription: {
186
+ status: "subscribed",
187
+ coupon: couponID,
188
+ seats: 1,
189
+ plan: "200",
190
+ },
191
+ paymentMethodID: paymentMethod.id,
192
+ paymentMethodLast4: paymentMethod.card?.last4 ?? null,
193
+ paymentMethodType: paymentMethod.type,
194
+ })
195
+ .where(eq(BillingTable.workspaceID, workspaceID))
196
+
197
+ await tx.insert(SubscriptionTable).values({
198
+ workspaceID,
199
+ id: Identifier.create("subscription"),
200
+ userID: user.id,
201
+ })
202
+
203
+ await tx.insert(PaymentTable).values({
204
+ workspaceID,
205
+ id: Identifier.create("payment"),
206
+ amount: centsToMicroCents(amountInCents),
207
+ paymentID,
208
+ invoiceID,
209
+ customerID,
210
+ enrichment: {
211
+ type: "subscription",
212
+ couponID,
213
+ },
214
+ })
215
+ })
216
+ })
217
+ }
218
+ if (body.type === "customer.subscription.created") {
219
+ /*
220
+ {
221
+ id: "evt_1Smq802SrMQ2Fneksse5FMNV",
222
+ object: "event",
223
+ api_version: "2025-07-30.basil",
224
+ created: 1767766916,
225
+ data: {
226
+ object: {
227
+ id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
228
+ object: "subscription",
229
+ application: null,
230
+ application_fee_percent: null,
231
+ automatic_tax: {
232
+ disabled_reason: null,
233
+ enabled: false,
234
+ liability: null,
235
+ },
236
+ billing_cycle_anchor: 1770445200,
237
+ billing_cycle_anchor_config: null,
238
+ billing_mode: {
239
+ flexible: {
240
+ proration_discounts: "included",
241
+ },
242
+ type: "flexible",
243
+ updated_at: 1770445200,
244
+ },
245
+ billing_thresholds: null,
246
+ cancel_at: null,
247
+ cancel_at_period_end: false,
248
+ canceled_at: null,
249
+ cancellation_details: {
250
+ comment: null,
251
+ feedback: null,
252
+ reason: null,
253
+ },
254
+ collection_method: "charge_automatically",
255
+ created: 1770445200,
256
+ currency: "usd",
257
+ customer: "cus_TkKmZZvysJ2wej",
258
+ customer_account: null,
259
+ days_until_due: null,
260
+ default_payment_method: null,
261
+ default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq",
262
+ default_tax_rates: [],
263
+ description: null,
264
+ discounts: [],
265
+ ended_at: null,
266
+ invoice_settings: {
267
+ account_tax_ids: null,
268
+ issuer: {
269
+ type: "self",
270
+ },
271
+ },
272
+ items: {
273
+ object: "list",
274
+ data: [
275
+ {
276
+ id: "si_TkKnBKXFX76t0O",
277
+ object: "subscription_item",
278
+ billing_thresholds: null,
279
+ created: 1770445200,
280
+ current_period_end: 1772864400,
281
+ current_period_start: 1770445200,
282
+ discounts: [],
283
+ metadata: {},
284
+ plan: {
285
+ id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
286
+ object: "plan",
287
+ active: true,
288
+ amount: 20000,
289
+ amount_decimal: "20000",
290
+ billing_scheme: "per_unit",
291
+ created: 1767725082,
292
+ currency: "usd",
293
+ interval: "month",
294
+ interval_count: 1,
295
+ livemode: false,
296
+ metadata: {},
297
+ meter: null,
298
+ nickname: null,
299
+ product: "prod_Tk9LjWT1n0DgYm",
300
+ tiers_mode: null,
301
+ transform_usage: null,
302
+ trial_period_days: null,
303
+ usage_type: "licensed",
304
+ },
305
+ price: {
306
+ id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
307
+ object: "price",
308
+ active: true,
309
+ billing_scheme: "per_unit",
310
+ created: 1767725082,
311
+ currency: "usd",
312
+ custom_unit_amount: null,
313
+ livemode: false,
314
+ lookup_key: null,
315
+ metadata: {},
316
+ nickname: null,
317
+ product: "prod_Tk9LjWT1n0DgYm",
318
+ recurring: {
319
+ interval: "month",
320
+ interval_count: 1,
321
+ meter: null,
322
+ trial_period_days: null,
323
+ usage_type: "licensed",
324
+ },
325
+ tax_behavior: "unspecified",
326
+ tiers_mode: null,
327
+ transform_quantity: null,
328
+ type: "recurring",
329
+ unit_amount: 20000,
330
+ unit_amount_decimal: "20000",
331
+ },
332
+ quantity: 1,
333
+ subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
334
+ tax_rates: [],
335
+ },
336
+ ],
337
+ has_more: false,
338
+ total_count: 1,
339
+ url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
340
+ },
341
+ latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE",
342
+ livemode: false,
343
+ metadata: {},
344
+ next_pending_invoice_item_invoice: null,
345
+ on_behalf_of: null,
346
+ pause_collection: null,
347
+ payment_settings: {
348
+ payment_method_options: null,
349
+ payment_method_types: null,
350
+ save_default_payment_method: "off",
351
+ },
352
+ pending_invoice_item_interval: null,
353
+ pending_setup_intent: null,
354
+ pending_update: null,
355
+ plan: {
356
+ id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
357
+ object: "plan",
358
+ active: true,
359
+ amount: 20000,
360
+ amount_decimal: "20000",
361
+ billing_scheme: "per_unit",
362
+ created: 1767725082,
363
+ currency: "usd",
364
+ interval: "month",
365
+ interval_count: 1,
366
+ livemode: false,
367
+ metadata: {},
368
+ meter: null,
369
+ nickname: null,
370
+ product: "prod_Tk9LjWT1n0DgYm",
371
+ tiers_mode: null,
372
+ transform_usage: null,
373
+ trial_period_days: null,
374
+ usage_type: "licensed",
375
+ },
376
+ quantity: 1,
377
+ schedule: null,
378
+ start_date: 1770445200,
379
+ status: "active",
380
+ test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ",
381
+ transfer_data: null,
382
+ trial_end: null,
383
+ trial_settings: {
384
+ end_behavior: {
385
+ missing_payment_method: "create_invoice",
386
+ },
387
+ },
388
+ trial_start: null,
389
+ },
390
+ },
391
+ livemode: false,
392
+ pending_webhooks: 0,
393
+ request: {
394
+ id: "req_6YO9stvB155WJD",
395
+ idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322",
396
+ },
397
+ type: "customer.subscription.created",
398
+ }
399
+ */
400
+ }
401
+ if (body.type === "customer.subscription.deleted") {
402
+ const subscriptionID = body.data.object.id
403
+ if (!subscriptionID) throw new Error("Subscription ID not found")
404
+
405
+ const workspaceID = await Database.use((tx) =>
406
+ tx
407
+ .select({ workspaceID: BillingTable.workspaceID })
408
+ .from(BillingTable)
409
+ .where(eq(BillingTable.subscriptionID, subscriptionID))
410
+ .then((rows) => rows[0]?.workspaceID),
411
+ )
412
+ if (!workspaceID) throw new Error("Workspace ID not found for subscription")
413
+
414
+ await Database.transaction(async (tx) => {
415
+ await tx
416
+ .update(BillingTable)
417
+ .set({ subscriptionID: null, subscription: null })
418
+ .where(eq(BillingTable.workspaceID, workspaceID))
419
+
420
+ await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
421
+ })
422
+ }
423
+ if (body.type === "invoice.payment_succeeded") {
424
+ if (
425
+ body.data.object.billing_reason === "subscription_cycle" ||
426
+ body.data.object.billing_reason === "subscription_create"
427
+ ) {
428
+ const invoiceID = body.data.object.id as string
429
+ const amountInCents = body.data.object.amount_paid
430
+ const customerID = body.data.object.customer as string
431
+ const subscriptionID = body.data.object.parent?.subscription_details?.subscription as string
432
+
433
+ if (!customerID) throw new Error("Customer ID not found")
434
+ if (!invoiceID) throw new Error("Invoice ID not found")
435
+ if (!subscriptionID) throw new Error("Subscription ID not found")
436
+
437
+ // get coupon id from subscription
438
+ const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscriptionID, {
439
+ expand: ["discounts"],
440
+ })
441
+ const couponID =
442
+ typeof subscriptionData.discounts[0] === "string"
443
+ ? subscriptionData.discounts[0]
444
+ : subscriptionData.discounts[0]?.coupon?.id
445
+
446
+ // get payment id from invoice
447
+ const invoice = await Billing.stripe().invoices.retrieve(invoiceID, {
448
+ expand: ["payments"],
449
+ })
450
+ const paymentID = invoice.payments?.data[0].payment.payment_intent as string
451
+ if (!paymentID) {
452
+ // payment id can be undefined when using coupon
453
+ if (!couponID) throw new Error("Payment ID not found")
454
+ }
455
+
456
+ const workspaceID = await Database.use((tx) =>
457
+ tx
458
+ .select({ workspaceID: BillingTable.workspaceID })
459
+ .from(BillingTable)
460
+ .where(eq(BillingTable.customerID, customerID))
461
+ .then((rows) => rows[0]?.workspaceID),
462
+ )
463
+ if (!workspaceID) throw new Error("Workspace ID not found for customer")
464
+
465
+ await Database.use((tx) =>
466
+ tx.insert(PaymentTable).values({
467
+ workspaceID,
468
+ id: Identifier.create("payment"),
469
+ amount: centsToMicroCents(amountInCents),
470
+ paymentID,
471
+ invoiceID,
472
+ customerID,
473
+ enrichment: {
474
+ type: "subscription",
475
+ couponID,
476
+ },
477
+ }),
478
+ )
479
+ }
480
+ }
481
+ if (body.type === "charge.refunded") {
482
+ const customerID = body.data.object.customer as string
483
+ const paymentIntentID = body.data.object.payment_intent as string
484
+ if (!customerID) throw new Error("Customer ID not found")
485
+ if (!paymentIntentID) throw new Error("Payment ID not found")
486
+
487
+ const workspaceID = await Database.use((tx) =>
488
+ tx
489
+ .select({
490
+ workspaceID: BillingTable.workspaceID,
491
+ })
492
+ .from(BillingTable)
493
+ .where(eq(BillingTable.customerID, customerID))
494
+ .then((rows) => rows[0]?.workspaceID),
495
+ )
496
+ if (!workspaceID) throw new Error("Workspace ID not found")
497
+
498
+ const amount = await Database.use((tx) =>
499
+ tx
500
+ .select({
501
+ amount: PaymentTable.amount,
502
+ })
503
+ .from(PaymentTable)
504
+ .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
505
+ .then((rows) => rows[0]?.amount),
506
+ )
507
+ if (!amount) throw new Error("Payment not found")
508
+
509
+ await Database.transaction(async (tx) => {
510
+ await tx
511
+ .update(PaymentTable)
512
+ .set({
513
+ timeRefunded: new Date(body.created * 1000),
514
+ })
515
+ .where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
516
+
517
+ await tx
518
+ .update(BillingTable)
519
+ .set({
520
+ balance: sql`${BillingTable.balance} - ${amount}`,
521
+ })
522
+ .where(eq(BillingTable.workspaceID, workspaceID))
523
+ })
524
+ }
525
+ })()
526
+ .then((message) => {
527
+ return Response.json({ message: message ?? "done" }, { status: 200 })
528
+ })
529
+ .catch((error: any) => {
530
+ return Response.json({ message: error.message }, { status: 500 })
531
+ })
532
+ }
@@ -0,0 +1,20 @@
1
+ import type { APIEvent } from "@solidjs/start/server"
2
+
3
+ async function handler(evt: APIEvent) {
4
+ const req = evt.request.clone()
5
+ const url = new URL(req.url)
6
+ const targetUrl = `https://enterprise.jonsoc.com/${url.pathname}${url.search}`
7
+ const response = await fetch(targetUrl, {
8
+ method: req.method,
9
+ headers: req.headers,
10
+ body: req.body,
11
+ })
12
+ return response
13
+ }
14
+
15
+ export const GET = handler
16
+ export const POST = handler
17
+ export const PUT = handler
18
+ export const DELETE = handler
19
+ export const OPTIONS = handler
20
+ export const PATCH = handler