@primstack/cli 0.0.1

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 (210) hide show
  1. package/dist/generators/crud/templates/drizzle-table.ts.template +12 -0
  2. package/dist/generators/crud/templates/handlers.ts.template +136 -0
  3. package/dist/generators/crud/templates/routes.ts.template +21 -0
  4. package/dist/generators/crud/templates/schema.ts.template +20 -0
  5. package/dist/index.js +9618 -0
  6. package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/index.ts.template +79 -0
  7. package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/types.ts.template +12 -0
  8. package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/index.ts.template +62 -0
  9. package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/types.ts.template +12 -0
  10. package/dist/integrations/analytics/providers/posthog/templates/src/analytics/index.ts.template +67 -0
  11. package/dist/integrations/analytics/providers/posthog/templates/src/analytics/types.ts.template +12 -0
  12. package/dist/integrations/auth/providers/authjoy/templates/api/src/middleware/auth.ts.template +89 -0
  13. package/dist/integrations/auth/providers/authjoy/templates/api/src/routes/auth.ts.template +27 -0
  14. package/dist/integrations/auth/providers/authjoy/templates/web/src/components/AuthProvider.tsx.template +40 -0
  15. package/dist/integrations/auth/providers/authjoy/templates/web/src/hooks/use-auth.ts.template +71 -0
  16. package/dist/integrations/auth/providers/authjoy/templates/web/src/lib/auth.ts.template +59 -0
  17. package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/account.tsx.template +84 -0
  18. package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/login.tsx.template +73 -0
  19. package/dist/integrations/cache/providers/memory/templates/src/cache/index.ts.template +43 -0
  20. package/dist/integrations/cache/providers/memory/templates/src/cache/types.ts.template +3 -0
  21. package/dist/integrations/cache/providers/redis/templates/src/cache/index.ts.template +37 -0
  22. package/dist/integrations/cache/providers/redis/templates/src/cache/types.ts.template +3 -0
  23. package/dist/integrations/cache/providers/valkey/templates/src/cache/index.ts.template +38 -0
  24. package/dist/integrations/cache/providers/valkey/templates/src/cache/types.ts.template +3 -0
  25. package/dist/integrations/db/providers/postgres/templates/drizzle.config.ts.template +10 -0
  26. package/dist/integrations/db/providers/postgres/templates/src/db/index.ts.template +13 -0
  27. package/dist/integrations/db/providers/postgres/templates/src/db/migrate.ts.template +19 -0
  28. package/dist/integrations/db/providers/postgres/templates/src/db/schema/index.ts.template +1 -0
  29. package/dist/integrations/db/providers/postgres/templates/src/db/schema/users.ts.template +12 -0
  30. package/dist/integrations/db/providers/postgres/templates/src/db/seed.ts.template +28 -0
  31. package/dist/integrations/db/providers/sqlite/templates/drizzle.config.ts.template +10 -0
  32. package/dist/integrations/db/providers/sqlite/templates/src/db/index.ts.template +10 -0
  33. package/dist/integrations/db/providers/sqlite/templates/src/db/schema/index.ts.template +1 -0
  34. package/dist/integrations/db/providers/sqlite/templates/src/db/schema/users.ts.template +12 -0
  35. package/dist/integrations/db/providers/sqlite/templates/src/db/seed.ts.template +28 -0
  36. package/dist/integrations/db/providers/supabase/templates/drizzle.config.ts.template +10 -0
  37. package/dist/integrations/db/providers/supabase/templates/src/db/index.ts.template +13 -0
  38. package/dist/integrations/db/providers/supabase/templates/src/db/migrate.ts.template +19 -0
  39. package/dist/integrations/db/providers/supabase/templates/src/db/schema/index.ts.template +1 -0
  40. package/dist/integrations/db/providers/supabase/templates/src/db/schema/users.ts.template +12 -0
  41. package/dist/integrations/db/providers/supabase/templates/src/db/seed.ts.template +28 -0
  42. package/dist/integrations/db/providers/turso/templates/drizzle.config.ts.template +11 -0
  43. package/dist/integrations/db/providers/turso/templates/src/db/index.ts.template +14 -0
  44. package/dist/integrations/db/providers/turso/templates/src/db/schema/index.ts.template +1 -0
  45. package/dist/integrations/db/providers/turso/templates/src/db/schema/users.ts.template +12 -0
  46. package/dist/integrations/db/providers/turso/templates/src/db/seed.ts.template +28 -0
  47. package/dist/integrations/email/providers/nodemailer/templates/src/email/index.ts.template +24 -0
  48. package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/index.ts.template +1 -0
  49. package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/welcome.ts.template +7 -0
  50. package/dist/integrations/email/providers/nodemailer/templates/src/email/types.ts.template +7 -0
  51. package/dist/integrations/email/providers/resend/templates/src/email/index.ts.template +18 -0
  52. package/dist/integrations/email/providers/resend/templates/src/email/templates/index.ts.template +1 -0
  53. package/dist/integrations/email/providers/resend/templates/src/email/templates/welcome.ts.template +7 -0
  54. package/dist/integrations/email/providers/resend/templates/src/email/types.ts.template +7 -0
  55. package/dist/integrations/email/providers/sendgrid/templates/src/email/index.ts.template +16 -0
  56. package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/index.ts.template +1 -0
  57. package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/welcome.ts.template +7 -0
  58. package/dist/integrations/email/providers/sendgrid/templates/src/email/types.ts.template +7 -0
  59. package/dist/integrations/flags/providers/local/templates/api/src/lib/flags.ts.template +97 -0
  60. package/dist/integrations/flags/providers/local/templates/api/src/routes/flags.ts.template +36 -0
  61. package/dist/integrations/flags/providers/local/templates/flags.json.template +8 -0
  62. package/dist/integrations/flags/providers/local/templates/web/src/hooks/use-flag.ts.template +60 -0
  63. package/dist/integrations/logging/providers/axiom/templates/src/logging/index.ts.template +56 -0
  64. package/dist/integrations/logging/providers/axiom/templates/src/logging/types.ts.template +5 -0
  65. package/dist/integrations/logging/providers/pino/templates/src/logging/index.ts.template +21 -0
  66. package/dist/integrations/logging/providers/pino/templates/src/logging/types.ts.template +5 -0
  67. package/dist/integrations/logging/providers/winston/templates/src/logging/index.ts.template +30 -0
  68. package/dist/integrations/logging/providers/winston/templates/src/logging/types.ts.template +5 -0
  69. package/dist/integrations/monitor/providers/datadog/templates/src/monitor/index.ts.template +78 -0
  70. package/dist/integrations/monitor/providers/datadog/templates/src/monitor/types.ts.template +12 -0
  71. package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/index.ts.template +60 -0
  72. package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/types.ts.template +12 -0
  73. package/dist/integrations/monitor/providers/sentry/templates/src/monitor/index.ts.template +70 -0
  74. package/dist/integrations/monitor/providers/sentry/templates/src/monitor/types.ts.template +12 -0
  75. package/dist/integrations/queue/providers/bullmq/templates/src/queue/index.ts.template +56 -0
  76. package/dist/integrations/queue/providers/bullmq/templates/src/queue/types.ts.template +10 -0
  77. package/dist/integrations/queue/providers/memory/templates/src/queue/index.ts.template +73 -0
  78. package/dist/integrations/queue/providers/memory/templates/src/queue/types.ts.template +10 -0
  79. package/dist/integrations/queue/providers/pgboss/templates/src/queue/index.ts.template +34 -0
  80. package/dist/integrations/queue/providers/pgboss/templates/src/queue/types.ts.template +10 -0
  81. package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/index.ts.template +95 -0
  82. package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/types.ts.template +12 -0
  83. package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/index.ts.template +80 -0
  84. package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/types.ts.template +12 -0
  85. package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/index.ts.template +67 -0
  86. package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/types.ts.template +12 -0
  87. package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/index.ts.template +81 -0
  88. package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/types.ts.template +10 -0
  89. package/dist/integrations/schedule/providers/croner/templates/src/schedule/index.ts.template +47 -0
  90. package/dist/integrations/schedule/providers/croner/templates/src/schedule/types.ts.template +10 -0
  91. package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/index.ts.template +45 -0
  92. package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/types.ts.template +10 -0
  93. package/dist/integrations/search/providers/algolia/templates/src/search/index.ts.template +52 -0
  94. package/dist/integrations/search/providers/algolia/templates/src/search/types.ts.template +18 -0
  95. package/dist/integrations/search/providers/meilisearch/templates/src/search/index.ts.template +49 -0
  96. package/dist/integrations/search/providers/meilisearch/templates/src/search/types.ts.template +18 -0
  97. package/dist/integrations/search/providers/typesense/templates/src/search/index.ts.template +71 -0
  98. package/dist/integrations/search/providers/typesense/templates/src/search/types.ts.template +35 -0
  99. package/dist/integrations/storage/providers/local/templates/src/storage/index.ts.template +69 -0
  100. package/dist/integrations/storage/providers/local/templates/src/storage/types.ts.template +12 -0
  101. package/dist/integrations/storage/providers/r2/templates/src/storage/index.ts.template +80 -0
  102. package/dist/integrations/storage/providers/r2/templates/src/storage/types.ts.template +12 -0
  103. package/dist/integrations/storage/providers/s3/templates/src/storage/index.ts.template +78 -0
  104. package/dist/integrations/storage/providers/s3/templates/src/storage/types.ts.template +12 -0
  105. package/dist/integrations/stripe/templates/api/src/lib/stripe.ts.template +259 -0
  106. package/dist/integrations/stripe/templates/api/src/routes/stripe-webhooks.ts.template +284 -0
  107. package/dist/integrations/stripe/templates/api/stripe.config.ts.template +178 -0
  108. package/dist/integrations/stripe/templates/shared/src/pricing.ts.template +117 -0
  109. package/dist/integrations/stripe/templates/shared/src/stripe-types.ts.template +133 -0
  110. package/dist/integrations/stripe/templates/web/src/components/billing-settings.tsx.template +123 -0
  111. package/dist/integrations/stripe/templates/web/src/components/pricing-cards.tsx.template +115 -0
  112. package/dist/integrations/stripe/templates/web/src/pages/pricing.tsx.template +95 -0
  113. package/dist/templates/api/fastify/.env.example.template +7 -0
  114. package/dist/templates/api/fastify/.gitignore.template +24 -0
  115. package/dist/templates/api/fastify/package.json.template +23 -0
  116. package/dist/templates/api/fastify/src/index.ts.template +52 -0
  117. package/dist/templates/api/fastify/src/lib/env.ts.template +20 -0
  118. package/dist/templates/api/fastify/src/routes/health.ts.template +12 -0
  119. package/dist/templates/api/fastify/tsconfig.json.template +18 -0
  120. package/dist/templates/api/fastify-postgres/.env.example.template +10 -0
  121. package/dist/templates/api/fastify-postgres/drizzle.config.ts.template +10 -0
  122. package/dist/templates/api/fastify-postgres/package.json.template +16 -0
  123. package/dist/templates/api/fastify-postgres/src/db/index.ts.template +9 -0
  124. package/dist/templates/api/fastify-postgres/src/db/schema/users.ts.template +12 -0
  125. package/dist/templates/api/fastify-sqlite/.env.example.template +10 -0
  126. package/dist/templates/api/fastify-sqlite/drizzle.config.ts.template +10 -0
  127. package/dist/templates/api/fastify-sqlite/package.json.template +16 -0
  128. package/dist/templates/api/fastify-sqlite/src/db/index.ts.template +6 -0
  129. package/dist/templates/api/fastify-sqlite/src/db/schema/users.ts.template +12 -0
  130. package/dist/templates/api/fastify-supabase/.env.example.template +10 -0
  131. package/dist/templates/api/fastify-supabase/drizzle.config.ts.template +10 -0
  132. package/dist/templates/api/fastify-supabase/package.json.template +15 -0
  133. package/dist/templates/api/fastify-supabase/src/db/index.ts.template +9 -0
  134. package/dist/templates/api/fastify-supabase/src/db/schema/users.ts.template +12 -0
  135. package/dist/templates/api/fastify-turso/.env.example.template +11 -0
  136. package/dist/templates/api/fastify-turso/drizzle.config.ts.template +11 -0
  137. package/dist/templates/api/fastify-turso/package.json.template +15 -0
  138. package/dist/templates/api/fastify-turso/src/db/index.ts.template +10 -0
  139. package/dist/templates/api/fastify-turso/src/db/schema/users.ts.template +12 -0
  140. package/dist/templates/fullstack/api/.env.example.template +10 -0
  141. package/dist/templates/fullstack/api/.gitignore.template +4 -0
  142. package/dist/templates/fullstack/api/drizzle.config.ts.template +14 -0
  143. package/dist/templates/fullstack/api/package.json.template +33 -0
  144. package/dist/templates/fullstack/api/src/db/index.ts.template +13 -0
  145. package/dist/templates/fullstack/api/src/db/schema/api-keys.ts.template +19 -0
  146. package/dist/templates/fullstack/api/src/db/schema/audit-logs.ts.template +23 -0
  147. package/dist/templates/fullstack/api/src/db/schema/index.ts.template +8 -0
  148. package/dist/templates/fullstack/api/src/db/schema/invites.ts.template +19 -0
  149. package/dist/templates/fullstack/api/src/db/schema/memberships.ts.template +16 -0
  150. package/dist/templates/fullstack/api/src/db/schema/organizations.ts.template +13 -0
  151. package/dist/templates/fullstack/api/src/db/schema/plans.ts.template +29 -0
  152. package/dist/templates/fullstack/api/src/db/schema/subscriptions.ts.template +38 -0
  153. package/dist/templates/fullstack/api/src/db/schema/users.ts.template +14 -0
  154. package/dist/templates/fullstack/api/src/index.ts.template +54 -0
  155. package/dist/templates/fullstack/api/src/lib/env.ts.template +22 -0
  156. package/dist/templates/fullstack/api/src/routes/health.ts.template +14 -0
  157. package/dist/templates/fullstack/api/tsconfig.json.template +15 -0
  158. package/dist/templates/fullstack/root/.gitignore.template +26 -0
  159. package/dist/templates/fullstack/root/package.json.template +15 -0
  160. package/dist/templates/fullstack/root/pnpm-workspace.yaml.template +3 -0
  161. package/dist/templates/fullstack/root/turbo.json.template +17 -0
  162. package/dist/templates/fullstack/shared/package.json.template +36 -0
  163. package/dist/templates/fullstack/shared/src/index.ts.template +8 -0
  164. package/dist/templates/fullstack/shared/src/schemas/api-key.ts.template +28 -0
  165. package/dist/templates/fullstack/shared/src/schemas/audit-log.ts.template +41 -0
  166. package/dist/templates/fullstack/shared/src/schemas/index.ts.template +8 -0
  167. package/dist/templates/fullstack/shared/src/schemas/invite.ts.template +25 -0
  168. package/dist/templates/fullstack/shared/src/schemas/membership.ts.template +20 -0
  169. package/dist/templates/fullstack/shared/src/schemas/organization.ts.template +18 -0
  170. package/dist/templates/fullstack/shared/src/schemas/plan.ts.template +38 -0
  171. package/dist/templates/fullstack/shared/src/schemas/subscription.ts.template +56 -0
  172. package/dist/templates/fullstack/shared/src/schemas/user.ts.template +21 -0
  173. package/dist/templates/fullstack/shared/src/types/index.ts.template +75 -0
  174. package/dist/templates/fullstack/shared/src/validators/index.ts.template +53 -0
  175. package/dist/templates/fullstack/shared/tsconfig.json.template +17 -0
  176. package/dist/templates/fullstack/web/.gitignore.template +3 -0
  177. package/dist/templates/fullstack/web/index.html.template +13 -0
  178. package/dist/templates/fullstack/web/package.json.template +23 -0
  179. package/dist/templates/fullstack/web/src/App.tsx.template +47 -0
  180. package/dist/templates/fullstack/web/src/index.css.template +54 -0
  181. package/dist/templates/fullstack/web/src/main.tsx.template +10 -0
  182. package/dist/templates/fullstack/web/src/vite-env.d.ts.template +1 -0
  183. package/dist/templates/fullstack/web/tsconfig.json.template +21 -0
  184. package/dist/templates/fullstack/web/tsconfig.node.json.template +11 -0
  185. package/dist/templates/fullstack/web/vite.config.ts.template +15 -0
  186. package/dist/templates/hosted/root/.env.local.template +13 -0
  187. package/dist/templates/hosted/root/.gitignore.template +32 -0
  188. package/dist/templates/hosted/root/CLAUDE.md.template +139 -0
  189. package/dist/templates/hosted/root/drizzle.config.ts.template +10 -0
  190. package/dist/templates/hosted/root/next.config.ts.template +15 -0
  191. package/dist/templates/hosted/root/package.json.template +40 -0
  192. package/dist/templates/hosted/root/postcss.config.mjs.template +9 -0
  193. package/dist/templates/hosted/root/primstack.config.json.template +5 -0
  194. package/dist/templates/hosted/root/tailwind.config.ts.template +14 -0
  195. package/dist/templates/hosted/root/tsconfig.json.template +25 -0
  196. package/dist/templates/hosted/root/wrangler.toml.template +9 -0
  197. package/dist/templates/hosted/src/app/actions/example.ts.template +50 -0
  198. package/dist/templates/hosted/src/app/api/health/route.ts.template +5 -0
  199. package/dist/templates/hosted/src/app/auth/login/page.tsx.template +32 -0
  200. package/dist/templates/hosted/src/app/globals.css.template +59 -0
  201. package/dist/templates/hosted/src/app/layout.tsx.template +24 -0
  202. package/dist/templates/hosted/src/app/page.tsx.template +34 -0
  203. package/dist/templates/hosted/src/db/migrations/0000_initial.sql.template +43 -0
  204. package/dist/templates/hosted/src/db/schema.ts.template +52 -0
  205. package/dist/templates/hosted/src/env.d.ts.template +10 -0
  206. package/dist/templates/hosted/src/instrumentation.ts.template +6 -0
  207. package/dist/templates/hosted/src/lib/auth.ts.template +35 -0
  208. package/dist/templates/hosted/src/lib/db.ts.template +17 -0
  209. package/dist/templates/hosted/src/middleware.ts.template +6 -0
  210. package/package.json +46 -0
@@ -0,0 +1,49 @@
1
+ import { MeiliSearch } from 'meilisearch';
2
+ import type { SearchDocument, SearchOptions, SearchResult } from './types.js';
3
+
4
+ export const client = new MeiliSearch({
5
+ host: process.env.MEILISEARCH_URL || 'http://localhost:7700',
6
+ apiKey: process.env.MEILISEARCH_API_KEY || '',
7
+ });
8
+
9
+ export function getIndex(name: string) {
10
+ return client.index(name);
11
+ }
12
+
13
+ export async function addDocuments<T extends SearchDocument>(
14
+ indexName: string,
15
+ documents: T[],
16
+ ): Promise<void> {
17
+ const index = client.index(indexName);
18
+ await index.addDocuments(documents);
19
+ }
20
+
21
+ export async function search<T extends SearchDocument>(
22
+ indexName: string,
23
+ query: string,
24
+ options?: SearchOptions,
25
+ ): Promise<SearchResult<T>> {
26
+ const index = client.index(indexName);
27
+ const result = await index.search(query, {
28
+ limit: options?.limit ?? 20,
29
+ offset: options?.offset,
30
+ filter: options?.filter,
31
+ sort: options?.sort,
32
+ });
33
+ return {
34
+ hits: result.hits as T[],
35
+ totalHits: result.estimatedTotalHits ?? result.hits.length,
36
+ query,
37
+ processingTimeMs: result.processingTimeMs,
38
+ };
39
+ }
40
+
41
+ export async function deleteDocuments(
42
+ indexName: string,
43
+ ids: (string | number)[],
44
+ ): Promise<void> {
45
+ const index = client.index(indexName);
46
+ await index.deleteDocuments(ids);
47
+ }
48
+
49
+ export type { SearchDocument, SearchOptions, SearchResult } from './types.js';
@@ -0,0 +1,18 @@
1
+ export interface SearchDocument {
2
+ id: string | number;
3
+ [key: string]: unknown;
4
+ }
5
+
6
+ export interface SearchOptions {
7
+ limit?: number;
8
+ offset?: number;
9
+ filter?: string;
10
+ sort?: string[];
11
+ }
12
+
13
+ export interface SearchResult<T = SearchDocument> {
14
+ hits: T[];
15
+ totalHits: number;
16
+ query: string;
17
+ processingTimeMs: number;
18
+ }
@@ -0,0 +1,71 @@
1
+ import Typesense from 'typesense';
2
+ import type { SearchDocument, SearchOptions, SearchResult, CollectionSchema } from './types.js';
3
+
4
+ const url = new URL(process.env.TYPESENSE_URL || 'http://localhost:8108');
5
+
6
+ export const client = new Typesense.Client({
7
+ nodes: [
8
+ {
9
+ host: url.hostname,
10
+ port: Number(url.port) || 8108,
11
+ protocol: url.protocol.replace(':', ''),
12
+ },
13
+ ],
14
+ apiKey: process.env.TYPESENSE_API_KEY || '',
15
+ });
16
+
17
+ export function getCollection(name: string) {
18
+ return client.collections(name);
19
+ }
20
+
21
+ /**
22
+ * Create a collection if it doesn't already exist.
23
+ * Must be called before addDocuments for new collections.
24
+ */
25
+ export async function ensureCollection(schema: CollectionSchema): Promise<void> {
26
+ try {
27
+ await client.collections(schema.name).retrieve();
28
+ } catch {
29
+ await client.collections().create(schema);
30
+ }
31
+ }
32
+
33
+ export async function addDocuments<T extends SearchDocument>(
34
+ collectionName: string,
35
+ documents: T[],
36
+ ): Promise<void> {
37
+ await client.collections(collectionName).documents().import(documents, { action: 'upsert' });
38
+ }
39
+
40
+ export async function search<T extends SearchDocument>(
41
+ collectionName: string,
42
+ query: string,
43
+ options?: SearchOptions,
44
+ ): Promise<SearchResult<T>> {
45
+ const start = Date.now();
46
+ const result = await client.collections(collectionName).documents().search({
47
+ q: query,
48
+ query_by: options?.queryBy || 'title',
49
+ per_page: options?.limit ?? 20,
50
+ page: options?.offset ? Math.floor(options.offset / (options?.limit ?? 20)) + 1 : 1,
51
+ filter_by: options?.filter,
52
+ sort_by: options?.sort?.join(','),
53
+ });
54
+ return {
55
+ hits: (result.hits ?? []).map((h) => h.document as T),
56
+ totalHits: result.found ?? 0,
57
+ query,
58
+ processingTimeMs: Date.now() - start,
59
+ };
60
+ }
61
+
62
+ export async function deleteDocuments(
63
+ collectionName: string,
64
+ ids: (string | number)[],
65
+ ): Promise<void> {
66
+ await Promise.all(
67
+ ids.map((id) => client.collections(collectionName).documents(String(id)).delete()),
68
+ );
69
+ }
70
+
71
+ export type { SearchDocument, SearchOptions, SearchResult, CollectionSchema } from './types.js';
@@ -0,0 +1,35 @@
1
+ export interface SearchDocument {
2
+ id: string | number;
3
+ [key: string]: unknown;
4
+ }
5
+
6
+ export interface SearchOptions {
7
+ limit?: number;
8
+ offset?: number;
9
+ filter?: string;
10
+ sort?: string[];
11
+ /** Comma-separated field names to search (required for Typesense, e.g. 'title,body') */
12
+ queryBy?: string;
13
+ }
14
+
15
+ export interface SearchResult<T = SearchDocument> {
16
+ hits: T[];
17
+ totalHits: number;
18
+ query: string;
19
+ processingTimeMs: number;
20
+ }
21
+
22
+ export interface CollectionSchema {
23
+ name: string;
24
+ fields: CollectionField[];
25
+ default_sorting_field?: string;
26
+ }
27
+
28
+ export interface CollectionField {
29
+ name: string;
30
+ type: 'string' | 'int32' | 'int64' | 'float' | 'bool' | 'string[]' | 'int32[]' | 'int64[]' | 'float[]' | 'bool[]' | 'auto';
31
+ facet?: boolean;
32
+ optional?: boolean;
33
+ index?: boolean;
34
+ sort?: boolean;
35
+ }
@@ -0,0 +1,69 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import type { UploadOptions, StorageObject } from './types.js';
4
+
5
+ const STORAGE_ROOT = process.env.STORAGE_PATH || './uploads';
6
+
7
+ function resolve(bucket?: string, key?: string): string {
8
+ const parts = [STORAGE_ROOT];
9
+ if (bucket) parts.push(bucket);
10
+ if (key) parts.push(key);
11
+ const resolved = path.resolve(...parts);
12
+ const root = path.resolve(STORAGE_ROOT);
13
+ if (resolved !== root && !resolved.startsWith(root + path.sep)) {
14
+ throw new Error('Path traversal detected');
15
+ }
16
+ return resolved;
17
+ }
18
+
19
+ export async function uploadFile(options: UploadOptions): Promise<{ key: string }> {
20
+ const filePath = resolve(options.bucket, options.key);
21
+ await fs.ensureDir(path.dirname(filePath));
22
+ await fs.writeFile(filePath, options.body);
23
+ return { key: options.key };
24
+ }
25
+
26
+ export async function downloadFile(key: string, bucket?: string): Promise<Buffer> {
27
+ const filePath = resolve(bucket, key);
28
+ return fs.readFile(filePath);
29
+ }
30
+
31
+ export async function deleteFile(key: string, bucket?: string): Promise<void> {
32
+ const filePath = resolve(bucket, key);
33
+ await fs.remove(filePath);
34
+ }
35
+
36
+ export async function listFiles(prefix?: string, bucket?: string): Promise<StorageObject[]> {
37
+ const dir = resolve(bucket);
38
+ if (!(await fs.pathExists(dir))) return [];
39
+
40
+ const entries = await fs.readdir(dir);
41
+ const results: StorageObject[] = [];
42
+
43
+ for (const entry of entries) {
44
+ if (prefix && !entry.startsWith(prefix)) continue;
45
+ const filePath = path.join(dir, entry);
46
+ const stat = await fs.stat(filePath);
47
+ if (stat.isFile()) {
48
+ results.push({
49
+ key: entry,
50
+ size: stat.size,
51
+ lastModified: stat.mtime,
52
+ });
53
+ }
54
+ }
55
+
56
+ return results;
57
+ }
58
+
59
+ export async function listBuckets(): Promise<string[]> {
60
+ await fs.ensureDir(STORAGE_ROOT);
61
+ const entries = await fs.readdir(STORAGE_ROOT, { withFileTypes: true });
62
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
63
+ }
64
+
65
+ export async function createBucket(name: string): Promise<void> {
66
+ await fs.ensureDir(resolve(name));
67
+ }
68
+
69
+ export type { UploadOptions, StorageObject } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface UploadOptions {
2
+ key: string;
3
+ body: Buffer | Uint8Array | string;
4
+ contentType?: string;
5
+ bucket?: string;
6
+ }
7
+
8
+ export interface StorageObject {
9
+ key: string;
10
+ size: number;
11
+ lastModified: Date;
12
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ S3Client,
3
+ PutObjectCommand,
4
+ GetObjectCommand,
5
+ DeleteObjectCommand,
6
+ ListObjectsV2Command,
7
+ } from '@aws-sdk/client-s3';
8
+ import type { UploadOptions, StorageObject } from './types.js';
9
+
10
+ const s3 = new S3Client({
11
+ region: 'auto',
12
+ endpoint: `https://${process.env.R2_ACCOUNT_ID!}.r2.cloudflarestorage.com`,
13
+ credentials: {
14
+ accessKeyId: process.env.R2_ACCESS_KEY_ID!,
15
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
16
+ },
17
+ });
18
+
19
+ const DEFAULT_BUCKET = process.env.R2_BUCKET!;
20
+
21
+ export async function uploadFile(options: UploadOptions): Promise<{ key: string }> {
22
+ const bucket = options.bucket || DEFAULT_BUCKET;
23
+ await s3.send(
24
+ new PutObjectCommand({
25
+ Bucket: bucket,
26
+ Key: options.key,
27
+ Body: options.body,
28
+ ContentType: options.contentType,
29
+ }),
30
+ );
31
+ return { key: options.key };
32
+ }
33
+
34
+ export async function downloadFile(key: string, bucket?: string): Promise<Buffer> {
35
+ const response = await s3.send(
36
+ new GetObjectCommand({
37
+ Bucket: bucket || DEFAULT_BUCKET,
38
+ Key: key,
39
+ }),
40
+ );
41
+ const bytes = await response.Body!.transformToByteArray();
42
+ return Buffer.from(bytes);
43
+ }
44
+
45
+ export async function deleteFile(key: string, bucket?: string): Promise<void> {
46
+ await s3.send(
47
+ new DeleteObjectCommand({
48
+ Bucket: bucket || DEFAULT_BUCKET,
49
+ Key: key,
50
+ }),
51
+ );
52
+ }
53
+
54
+ export async function listFiles(prefix?: string, bucket?: string): Promise<StorageObject[]> {
55
+ const response = await s3.send(
56
+ new ListObjectsV2Command({
57
+ Bucket: bucket || DEFAULT_BUCKET,
58
+ Prefix: prefix,
59
+ }),
60
+ );
61
+ return (response.Contents || []).map((obj) => ({
62
+ key: obj.Key!,
63
+ size: obj.Size || 0,
64
+ lastModified: obj.LastModified || new Date(),
65
+ }));
66
+ }
67
+
68
+ export async function listBuckets(): Promise<string[]> {
69
+ throw new Error(
70
+ 'R2 does not support ListBuckets via the S3 API. Use the Cloudflare dashboard or wrangler CLI instead.',
71
+ );
72
+ }
73
+
74
+ export async function createBucket(name: string): Promise<void> {
75
+ throw new Error(
76
+ `R2 does not support CreateBucket via the S3 API. Use the Cloudflare dashboard or 'wrangler r2 bucket create ${name}'.`,
77
+ );
78
+ }
79
+
80
+ export type { UploadOptions, StorageObject } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface UploadOptions {
2
+ key: string;
3
+ body: Buffer | Uint8Array | string;
4
+ contentType?: string;
5
+ bucket?: string;
6
+ }
7
+
8
+ export interface StorageObject {
9
+ key: string;
10
+ size: number;
11
+ lastModified: Date;
12
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ S3Client,
3
+ PutObjectCommand,
4
+ GetObjectCommand,
5
+ DeleteObjectCommand,
6
+ ListObjectsV2Command,
7
+ ListBucketsCommand,
8
+ CreateBucketCommand,
9
+ } from '@aws-sdk/client-s3';
10
+ import type { UploadOptions, StorageObject } from './types.js';
11
+
12
+ const s3 = new S3Client({
13
+ region: process.env.AWS_REGION || 'us-east-1',
14
+ credentials: {
15
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
16
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
17
+ },
18
+ });
19
+
20
+ const DEFAULT_BUCKET = process.env.S3_BUCKET!;
21
+
22
+ export async function uploadFile(options: UploadOptions): Promise<{ key: string }> {
23
+ const bucket = options.bucket || DEFAULT_BUCKET;
24
+ await s3.send(
25
+ new PutObjectCommand({
26
+ Bucket: bucket,
27
+ Key: options.key,
28
+ Body: options.body,
29
+ ContentType: options.contentType,
30
+ }),
31
+ );
32
+ return { key: options.key };
33
+ }
34
+
35
+ export async function downloadFile(key: string, bucket?: string): Promise<Buffer> {
36
+ const response = await s3.send(
37
+ new GetObjectCommand({
38
+ Bucket: bucket || DEFAULT_BUCKET,
39
+ Key: key,
40
+ }),
41
+ );
42
+ const bytes = await response.Body!.transformToByteArray();
43
+ return Buffer.from(bytes);
44
+ }
45
+
46
+ export async function deleteFile(key: string, bucket?: string): Promise<void> {
47
+ await s3.send(
48
+ new DeleteObjectCommand({
49
+ Bucket: bucket || DEFAULT_BUCKET,
50
+ Key: key,
51
+ }),
52
+ );
53
+ }
54
+
55
+ export async function listFiles(prefix?: string, bucket?: string): Promise<StorageObject[]> {
56
+ const response = await s3.send(
57
+ new ListObjectsV2Command({
58
+ Bucket: bucket || DEFAULT_BUCKET,
59
+ Prefix: prefix,
60
+ }),
61
+ );
62
+ return (response.Contents || []).map((obj) => ({
63
+ key: obj.Key!,
64
+ size: obj.Size || 0,
65
+ lastModified: obj.LastModified || new Date(),
66
+ }));
67
+ }
68
+
69
+ export async function listBuckets(): Promise<string[]> {
70
+ const response = await s3.send(new ListBucketsCommand({}));
71
+ return (response.Buckets || []).map((b) => b.Name!);
72
+ }
73
+
74
+ export async function createBucket(name: string): Promise<void> {
75
+ await s3.send(new CreateBucketCommand({ Bucket: name }));
76
+ }
77
+
78
+ export type { UploadOptions, StorageObject } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface UploadOptions {
2
+ key: string;
3
+ body: Buffer | Uint8Array | string;
4
+ contentType?: string;
5
+ bucket?: string;
6
+ }
7
+
8
+ export interface StorageObject {
9
+ key: string;
10
+ size: number;
11
+ lastModified: Date;
12
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Stripe Client
3
+ *
4
+ * This module provides a configured Stripe client and helper functions
5
+ * for common billing operations.
6
+ */
7
+
8
+ import Stripe from 'stripe';
9
+ import { stripeConfig } from '../../stripe.config.js';
10
+
11
+ type StripeEnvironment = 'test' | 'live';
12
+
13
+ // Get current environment
14
+ const STRIPE_ENV = (process.env.STRIPE_ENV || 'test') as StripeEnvironment;
15
+
16
+ // Initialize Stripe client
17
+ const credentials = stripeConfig.environments[STRIPE_ENV];
18
+
19
+ if (!credentials?.secretKey) {
20
+ throw new Error(
21
+ `Stripe ${STRIPE_ENV} secret key not configured. ` +
22
+ `Set STRIPE_${STRIPE_ENV.toUpperCase()}_SECRET_KEY environment variable.`
23
+ );
24
+ }
25
+
26
+ export const stripe = new Stripe(credentials.secretKey, {
27
+ apiVersion: '2024-06-20' as any,
28
+ typescript: true,
29
+ });
30
+
31
+ /**
32
+ * Get the current Stripe environment
33
+ */
34
+ export function getStripeEnvironment(): StripeEnvironment {
35
+ return STRIPE_ENV;
36
+ }
37
+
38
+ /**
39
+ * Get the publishable key for the current environment
40
+ */
41
+ export function getPublishableKey(): string {
42
+ return credentials.publishableKey;
43
+ }
44
+
45
+ /**
46
+ * Get the webhook secret for the current environment
47
+ */
48
+ export function getWebhookSecret(): string {
49
+ return credentials.webhookSecret;
50
+ }
51
+
52
+ /**
53
+ * Get or create a Stripe customer for a user
54
+ *
55
+ * IMPORTANT: This function searches by userId metadata to avoid
56
+ * matching wrong customers when emails are shared or reused.
57
+ */
58
+ export async function getOrCreateCustomer(
59
+ userId: string,
60
+ email: string,
61
+ name?: string
62
+ ): Promise<Stripe.Customer> {
63
+ // First, search for existing customer by userId metadata
64
+ // This is the primary lookup to ensure we don't mix up customers
65
+ const customersByUserId = await stripe.customers.search({
66
+ query: `metadata['userId']:'${userId}'`,
67
+ limit: 1,
68
+ });
69
+
70
+ if (customersByUserId.data.length > 0) {
71
+ const customer = customersByUserId.data[0];
72
+ // Update email if it changed (but keep userId as-is)
73
+ if (customer.email !== email) {
74
+ return stripe.customers.update(customer.id, { email });
75
+ }
76
+ return customer;
77
+ }
78
+
79
+ // No customer found by userId - create a new one
80
+ // Do NOT associate existing email-only customers to avoid billing mix-ups
81
+ return stripe.customers.create({
82
+ email,
83
+ name,
84
+ metadata: {
85
+ userId,
86
+ environment: STRIPE_ENV,
87
+ },
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Create a checkout session for a subscription
93
+ */
94
+ export async function createCheckoutSession(options: {
95
+ customerId: string;
96
+ priceLookupKey: string;
97
+ successUrl: string;
98
+ cancelUrl: string;
99
+ trialDays?: number;
100
+ metadata?: Record<string, string>;
101
+ }): Promise<Stripe.Checkout.Session> {
102
+ const { customerId, priceLookupKey, successUrl, cancelUrl, trialDays, metadata } = options;
103
+
104
+ // Get price by lookup key
105
+ const prices = await stripe.prices.list({
106
+ lookup_keys: [priceLookupKey],
107
+ limit: 1,
108
+ });
109
+
110
+ if (prices.data.length === 0) {
111
+ throw new Error(`Price not found for lookup key: ${priceLookupKey}`);
112
+ }
113
+
114
+ const price = prices.data[0];
115
+
116
+ return stripe.checkout.sessions.create({
117
+ customer: customerId,
118
+ mode: 'subscription',
119
+ line_items: [
120
+ {
121
+ price: price.id,
122
+ quantity: 1,
123
+ },
124
+ ],
125
+ success_url: successUrl,
126
+ cancel_url: cancelUrl,
127
+ subscription_data: trialDays
128
+ ? {
129
+ trial_period_days: trialDays,
130
+ metadata,
131
+ }
132
+ : { metadata },
133
+ metadata,
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Create a customer portal session
139
+ */
140
+ export async function createPortalSession(
141
+ customerId: string,
142
+ returnUrl: string
143
+ ): Promise<Stripe.BillingPortal.Session> {
144
+ return stripe.billingPortal.sessions.create({
145
+ customer: customerId,
146
+ return_url: returnUrl,
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Get a subscription by ID
152
+ */
153
+ export async function getSubscription(
154
+ subscriptionId: string
155
+ ): Promise<Stripe.Subscription | null> {
156
+ try {
157
+ return await stripe.subscriptions.retrieve(subscriptionId);
158
+ } catch (error) {
159
+ // Handle Stripe API errors - check for resource not found
160
+ const stripeError = error as Stripe.errors.StripeError;
161
+ if (stripeError.type === 'invalid_request_error' && stripeError.code === 'resource_missing') {
162
+ return null;
163
+ }
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Cancel a subscription
170
+ */
171
+ export async function cancelSubscription(
172
+ subscriptionId: string,
173
+ immediately = false
174
+ ): Promise<Stripe.Subscription> {
175
+ if (immediately) {
176
+ return stripe.subscriptions.cancel(subscriptionId);
177
+ }
178
+
179
+ // Cancel at period end
180
+ return stripe.subscriptions.update(subscriptionId, {
181
+ cancel_at_period_end: true,
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Update subscription to a new price
187
+ *
188
+ * Note: This updates the first subscription item (the main plan).
189
+ * If the subscription has multiple items (add-ons, metered), they remain unchanged.
190
+ */
191
+ export async function updateSubscription(
192
+ subscriptionId: string,
193
+ newPriceLookupKey: string
194
+ ): Promise<Stripe.Subscription> {
195
+ // Get the subscription
196
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId);
197
+
198
+ // Validate subscription has at least one item
199
+ const firstItem = subscription.items.data[0];
200
+ if (!firstItem) {
201
+ throw new Error(`Subscription ${subscriptionId} has no items to update`);
202
+ }
203
+
204
+ // Get new price by lookup key
205
+ const prices = await stripe.prices.list({
206
+ lookup_keys: [newPriceLookupKey],
207
+ limit: 1,
208
+ });
209
+
210
+ if (prices.data.length === 0) {
211
+ throw new Error(`Price not found for lookup key: ${newPriceLookupKey}`);
212
+ }
213
+
214
+ const newPrice = prices.data[0];
215
+
216
+ // Update only the first subscription item (main plan)
217
+ return stripe.subscriptions.update(subscriptionId, {
218
+ items: [
219
+ {
220
+ id: firstItem.id,
221
+ price: newPrice.id,
222
+ },
223
+ ],
224
+ proration_behavior: 'create_prorations',
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Report usage for a meter
230
+ */
231
+ export async function reportUsage(
232
+ subscriptionItemId: string,
233
+ quantity: number,
234
+ timestamp?: number
235
+ ): Promise<Stripe.UsageRecord> {
236
+ return stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
237
+ quantity,
238
+ timestamp: timestamp || Math.floor(Date.now() / 1000),
239
+ action: 'increment',
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Construct and verify a webhook event
245
+ */
246
+ export function constructWebhookEvent(
247
+ payload: string | Buffer,
248
+ signature: string
249
+ ): Stripe.Event {
250
+ const webhookSecret = getWebhookSecret();
251
+
252
+ if (!webhookSecret) {
253
+ throw new Error('Webhook secret not configured');
254
+ }
255
+
256
+ return stripe.webhooks.constructEvent(payload, signature, webhookSecret);
257
+ }
258
+
259
+ export default stripe;