@unifiedcommerce/core 0.0.4 → 0.1.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 (179) hide show
  1. package/dist/auth/auth-schema.d.ts +92 -0
  2. package/dist/auth/auth-schema.d.ts.map +1 -1
  3. package/dist/auth/auth-schema.js +7 -0
  4. package/dist/auth/setup.d.ts.map +1 -1
  5. package/dist/auth/setup.js +3 -1
  6. package/package.json +1 -2
  7. package/src/adapters/console-email.ts +0 -43
  8. package/src/auth/access.ts +0 -187
  9. package/src/auth/auth-schema.ts +0 -131
  10. package/src/auth/middleware.ts +0 -161
  11. package/src/auth/org.ts +0 -41
  12. package/src/auth/permissions.ts +0 -28
  13. package/src/auth/setup.ts +0 -165
  14. package/src/auth/system-actor.ts +0 -19
  15. package/src/auth/types.ts +0 -10
  16. package/src/config/defaults.ts +0 -82
  17. package/src/config/define-config.ts +0 -53
  18. package/src/config/types.ts +0 -299
  19. package/src/generated/plugin-capabilities.d.ts +0 -20
  20. package/src/generated/plugin-manifest.ts +0 -23
  21. package/src/generated/plugin-repositories.d.ts +0 -20
  22. package/src/hooks/checkout-completion.ts +0 -262
  23. package/src/hooks/checkout.ts +0 -677
  24. package/src/hooks/order-emails.ts +0 -62
  25. package/src/index.ts +0 -214
  26. package/src/interfaces/mcp/agent-prompt.ts +0 -174
  27. package/src/interfaces/mcp/context-enrichment.ts +0 -177
  28. package/src/interfaces/mcp/server.ts +0 -617
  29. package/src/interfaces/mcp/transport.ts +0 -68
  30. package/src/interfaces/rest/customer-portal.ts +0 -299
  31. package/src/interfaces/rest/index.ts +0 -74
  32. package/src/interfaces/rest/router.ts +0 -334
  33. package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
  34. package/src/interfaces/rest/routes/audit.ts +0 -50
  35. package/src/interfaces/rest/routes/carts.ts +0 -89
  36. package/src/interfaces/rest/routes/catalog.ts +0 -493
  37. package/src/interfaces/rest/routes/checkout.ts +0 -283
  38. package/src/interfaces/rest/routes/inventory.ts +0 -70
  39. package/src/interfaces/rest/routes/media.ts +0 -86
  40. package/src/interfaces/rest/routes/orders.ts +0 -78
  41. package/src/interfaces/rest/routes/payments.ts +0 -60
  42. package/src/interfaces/rest/routes/pricing.ts +0 -57
  43. package/src/interfaces/rest/routes/promotions.ts +0 -92
  44. package/src/interfaces/rest/routes/search.ts +0 -71
  45. package/src/interfaces/rest/routes/webhooks.ts +0 -46
  46. package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
  47. package/src/interfaces/rest/schemas/audit.ts +0 -46
  48. package/src/interfaces/rest/schemas/carts.ts +0 -125
  49. package/src/interfaces/rest/schemas/catalog.ts +0 -450
  50. package/src/interfaces/rest/schemas/checkout.ts +0 -66
  51. package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
  52. package/src/interfaces/rest/schemas/inventory.ts +0 -138
  53. package/src/interfaces/rest/schemas/media.ts +0 -75
  54. package/src/interfaces/rest/schemas/orders.ts +0 -104
  55. package/src/interfaces/rest/schemas/pricing.ts +0 -80
  56. package/src/interfaces/rest/schemas/promotions.ts +0 -110
  57. package/src/interfaces/rest/schemas/responses.ts +0 -85
  58. package/src/interfaces/rest/schemas/search.ts +0 -58
  59. package/src/interfaces/rest/schemas/shared.ts +0 -62
  60. package/src/interfaces/rest/schemas/webhooks.ts +0 -68
  61. package/src/interfaces/rest/utils.ts +0 -104
  62. package/src/interfaces/rest/webhook-router.ts +0 -50
  63. package/src/kernel/compensation/executor.ts +0 -61
  64. package/src/kernel/compensation/types.ts +0 -26
  65. package/src/kernel/database/adapter.ts +0 -13
  66. package/src/kernel/database/drizzle-db.ts +0 -56
  67. package/src/kernel/database/migrate.ts +0 -76
  68. package/src/kernel/database/plugin-types.ts +0 -34
  69. package/src/kernel/database/schema.ts +0 -49
  70. package/src/kernel/database/scoped-db.ts +0 -68
  71. package/src/kernel/database/tx-context.ts +0 -46
  72. package/src/kernel/error-mapper.ts +0 -15
  73. package/src/kernel/errors.ts +0 -89
  74. package/src/kernel/factory/repository-factory.ts +0 -242
  75. package/src/kernel/hooks/create-context.ts +0 -43
  76. package/src/kernel/hooks/executor.ts +0 -88
  77. package/src/kernel/hooks/registry.ts +0 -74
  78. package/src/kernel/hooks/types.ts +0 -52
  79. package/src/kernel/http-error.ts +0 -44
  80. package/src/kernel/jobs/adapter.ts +0 -36
  81. package/src/kernel/jobs/drizzle-adapter.ts +0 -58
  82. package/src/kernel/jobs/runner.ts +0 -153
  83. package/src/kernel/jobs/schema.ts +0 -46
  84. package/src/kernel/jobs/types.ts +0 -30
  85. package/src/kernel/local-api.ts +0 -185
  86. package/src/kernel/plugin/manifest.ts +0 -253
  87. package/src/kernel/query/executor.ts +0 -184
  88. package/src/kernel/query/registry.ts +0 -46
  89. package/src/kernel/result.ts +0 -33
  90. package/src/kernel/schema/extra-columns.ts +0 -37
  91. package/src/kernel/service-registry.ts +0 -76
  92. package/src/kernel/service-timing.ts +0 -89
  93. package/src/kernel/state-machine/machine.ts +0 -101
  94. package/src/modules/analytics/drizzle-adapter.ts +0 -426
  95. package/src/modules/analytics/hooks.ts +0 -11
  96. package/src/modules/analytics/models.ts +0 -125
  97. package/src/modules/analytics/repository/index.ts +0 -6
  98. package/src/modules/analytics/service.ts +0 -245
  99. package/src/modules/analytics/types.ts +0 -180
  100. package/src/modules/audit/hooks.ts +0 -78
  101. package/src/modules/audit/schema.ts +0 -33
  102. package/src/modules/audit/service.ts +0 -151
  103. package/src/modules/cart/access.ts +0 -27
  104. package/src/modules/cart/matcher.ts +0 -26
  105. package/src/modules/cart/repository/index.ts +0 -234
  106. package/src/modules/cart/schema.ts +0 -42
  107. package/src/modules/cart/schemas.ts +0 -38
  108. package/src/modules/cart/service.ts +0 -541
  109. package/src/modules/catalog/repository/index.ts +0 -772
  110. package/src/modules/catalog/schema.ts +0 -203
  111. package/src/modules/catalog/schemas.ts +0 -104
  112. package/src/modules/catalog/service.ts +0 -1544
  113. package/src/modules/customers/repository/index.ts +0 -327
  114. package/src/modules/customers/schema.ts +0 -64
  115. package/src/modules/customers/service.ts +0 -171
  116. package/src/modules/fulfillment/repository/index.ts +0 -426
  117. package/src/modules/fulfillment/schema.ts +0 -101
  118. package/src/modules/fulfillment/service.ts +0 -555
  119. package/src/modules/fulfillment/types.ts +0 -59
  120. package/src/modules/inventory/repository/index.ts +0 -509
  121. package/src/modules/inventory/schema.ts +0 -94
  122. package/src/modules/inventory/schemas.ts +0 -38
  123. package/src/modules/inventory/service.ts +0 -490
  124. package/src/modules/media/adapter.ts +0 -17
  125. package/src/modules/media/repository/index.ts +0 -274
  126. package/src/modules/media/schema.ts +0 -41
  127. package/src/modules/media/service.ts +0 -151
  128. package/src/modules/orders/repository/index.ts +0 -287
  129. package/src/modules/orders/schema.ts +0 -66
  130. package/src/modules/orders/service.ts +0 -619
  131. package/src/modules/orders/stale-order-cleanup.ts +0 -76
  132. package/src/modules/organization/service.ts +0 -191
  133. package/src/modules/payments/adapter.ts +0 -47
  134. package/src/modules/payments/repository/index.ts +0 -6
  135. package/src/modules/payments/service.ts +0 -107
  136. package/src/modules/pricing/repository/index.ts +0 -291
  137. package/src/modules/pricing/schema.ts +0 -71
  138. package/src/modules/pricing/schemas.ts +0 -38
  139. package/src/modules/pricing/service.ts +0 -494
  140. package/src/modules/promotions/repository/index.ts +0 -325
  141. package/src/modules/promotions/schema.ts +0 -62
  142. package/src/modules/promotions/schemas.ts +0 -38
  143. package/src/modules/promotions/service.ts +0 -598
  144. package/src/modules/search/adapter.ts +0 -57
  145. package/src/modules/search/hooks.ts +0 -12
  146. package/src/modules/search/repository/index.ts +0 -6
  147. package/src/modules/search/service.ts +0 -315
  148. package/src/modules/shipping/calculator.ts +0 -188
  149. package/src/modules/shipping/repository/index.ts +0 -6
  150. package/src/modules/shipping/service.ts +0 -51
  151. package/src/modules/tax/adapter.ts +0 -60
  152. package/src/modules/tax/repository/index.ts +0 -6
  153. package/src/modules/tax/service.ts +0 -53
  154. package/src/modules/webhooks/hook.ts +0 -34
  155. package/src/modules/webhooks/repository/index.ts +0 -278
  156. package/src/modules/webhooks/schema.ts +0 -56
  157. package/src/modules/webhooks/service.ts +0 -117
  158. package/src/modules/webhooks/signing.ts +0 -6
  159. package/src/modules/webhooks/ssrf-guard.ts +0 -71
  160. package/src/modules/webhooks/tasks.ts +0 -52
  161. package/src/modules/webhooks/worker.ts +0 -134
  162. package/src/runtime/commerce.ts +0 -145
  163. package/src/runtime/kernel.ts +0 -419
  164. package/src/runtime/logger.ts +0 -36
  165. package/src/runtime/server.ts +0 -349
  166. package/src/runtime/shutdown.ts +0 -43
  167. package/src/test-utils/create-pglite-adapter.ts +0 -129
  168. package/src/test-utils/create-plugin-test-app.ts +0 -128
  169. package/src/test-utils/create-repository-test-harness.ts +0 -16
  170. package/src/test-utils/create-test-config.ts +0 -190
  171. package/src/test-utils/create-test-kernel.ts +0 -7
  172. package/src/test-utils/create-test-plugin-context.ts +0 -75
  173. package/src/test-utils/rest-api-test-utils.ts +0 -265
  174. package/src/test-utils/test-actors.ts +0 -62
  175. package/src/test-utils/typed-hooks.ts +0 -54
  176. package/src/types/commerce-types.ts +0 -34
  177. package/src/utils/id.ts +0 -3
  178. package/src/utils/logger.ts +0 -18
  179. package/src/utils/pagination.ts +0 -22
@@ -1,36 +0,0 @@
1
- import pino from "pino";
2
- import type { CommerceConfig } from "../config/types.js";
3
-
4
- export type Logger = pino.Logger;
5
-
6
- /**
7
- * Create a structured JSON logger for the commerce engine.
8
- *
9
- * Automatically redacts sensitive fields (authorization, password, token, etc.)
10
- * from all log output. Uses Pino for high-performance, newline-delimited JSON.
11
- */
12
- export function createLogger(config: CommerceConfig): Logger {
13
- return pino({
14
- level: config.logLevel ?? "info",
15
- redact: {
16
- paths: [
17
- "req.headers.authorization",
18
- "req.headers.cookie",
19
- "*.password",
20
- "*.secret",
21
- "*.apiKey",
22
- "*.creditCard",
23
- "*.token",
24
- "*.apiToken",
25
- ],
26
- censor: "[REDACTED]",
27
- },
28
- serializers: {
29
- req: pino.stdSerializers.req,
30
- err: pino.stdSerializers.err,
31
- },
32
- formatters: {
33
- level: (label) => ({ level: label }),
34
- },
35
- });
36
- }
@@ -1,349 +0,0 @@
1
- import { Hono } from "hono";
2
- import { OpenAPIHono } from "@hono/zod-openapi";
3
- import { cors } from "hono/cors";
4
- import { csrf } from "hono/csrf";
5
- import { bodyLimit } from "hono/body-limit";
6
- import { rateLimiter } from "hono-rate-limiter";
7
- import type { Actor } from "../auth/types.js";
8
- import type { AuthInstance } from "../auth/setup.js";
9
- import type { CommerceConfig } from "../config/types.js";
10
- import { authMiddleware } from "../auth/middleware.js";
11
- import { createMCPHandler } from "../interfaces/mcp/transport.js";
12
- import { createRestRoutes } from "../interfaces/rest/index.js";
13
- import { createCustomerPortalRoutes } from "../interfaces/rest/customer-portal.js";
14
- import { createKernel } from "./kernel.js";
15
- import { ensureDefaultOrg } from "../auth/org.js";
16
- import { createLogger, type Logger } from "./logger.js";
17
- import { createCommerce, type CommerceInstance } from "./commerce.js";
18
-
19
- type ServerEnv = {
20
- Variables: {
21
- auth: AuthInstance;
22
- actor: Actor | null;
23
- requestId: string;
24
- logger: Logger;
25
- };
26
- };
27
-
28
- /**
29
- * Create a full HTTP server (Hono) with all REST routes, auth, and middleware.
30
- *
31
- * For server-side frameworks (Next.js, TanStack Start, SvelteKit), use
32
- * `createCommerce()` instead — it gives you a local API without HTTP overhead.
33
- */
34
- export async function createServer(config: CommerceConfig) {
35
- const commerce = await createCommerce(config);
36
- const { kernel, auth, logger } = commerce;
37
- const isProdEnv = process.env.NODE_ENV === "production";
38
-
39
-
40
- console.log("config", config);
41
- console.log("isProdEnv", isProdEnv);
42
-
43
- const app = new OpenAPIHono<ServerEnv>({
44
- defaultHook: (result, c) => {
45
- if (!result.success) {
46
- return c.json({
47
- error: {
48
- code: "VALIDATION_FAILED",
49
- message: isProdEnv
50
- ? "Invalid input."
51
- : result.error.issues
52
- .map((i) => `${i.path.join(".")}: ${i.message}`)
53
- .join("; "),
54
- },
55
- }, 422);
56
- }
57
- },
58
- });
59
-
60
- // ─── Security Guards ──────────────────────────────────────────────
61
- if (config.auth?.enableDevKey && process.env.NODE_ENV === "production") {
62
- throw new Error(
63
- "FATAL: Dev key MUST NOT be enabled in production. " +
64
- "Set auth.enableDevKey: false in production config.",
65
- );
66
- }
67
- if (config.auth?.enableDevKey && process.env.NODE_ENV !== "development") {
68
- logger.warn(
69
- "Dev key is enabled outside NODE_ENV=development. " +
70
- "Set auth.enableDevKey: false in production config to disable the dev backdoor.",
71
- );
72
- }
73
-
74
- // ─── Process Crash Handlers (F4) ─────────────────────────────────────
75
- process.on("unhandledRejection", (reason) => {
76
- logger.fatal({ err: reason }, "unhandled promise rejection -- exiting");
77
- process.exit(1);
78
- });
79
-
80
- process.on("uncaughtException", (err) => {
81
- logger.fatal({ err }, "uncaught exception -- exiting");
82
- process.exit(1);
83
- });
84
-
85
- // ─── Security Response Headers ──────────────────────────────────────
86
- app.use("*", async (c, next) => {
87
- c.header("X-Content-Type-Options", "nosniff");
88
- c.header("X-Frame-Options", "DENY");
89
- c.header("Referrer-Policy", "strict-origin-when-cross-origin");
90
- c.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
91
- if (isProdEnv) {
92
- c.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
93
- }
94
- await next();
95
- });
96
-
97
- // ─── Request ID + Logging (F2, F12) ──────────────────────────────────
98
- app.use("*", async (c, next) => {
99
- const requestId = c.req.header("x-request-id") ?? crypto.randomUUID();
100
- c.header("x-request-id", requestId);
101
- c.set("requestId", requestId);
102
- const child = logger.child({ requestId, method: c.req.method, path: c.req.path });
103
- c.set("logger", child);
104
-
105
- const start = performance.now();
106
- await next();
107
-
108
- // Sanitize ZodError responses in production.
109
- // @hono/zod-openapi returns { success: false, error: { name: "ZodError" } }
110
- // which leaks schema details. The library does not respect defaultHook for
111
- // this format, so we intercept at the response level.
112
- if (isProdEnv && c.res.status >= 400 && c.res.status < 500) {
113
- const ct = c.res.headers.get("content-type");
114
- if (ct?.includes("json")) {
115
- try {
116
- const body = await c.res.clone().json();
117
- if (body?.error?.name === "ZodError") {
118
- c.res = new Response(
119
- JSON.stringify({ error: { code: "VALIDATION_FAILED", message: "Invalid input." } }),
120
- { status: 422, headers: { "content-type": "application/json" } },
121
- );
122
- }
123
- } catch { /* non-parseable, skip */ }
124
- }
125
- }
126
-
127
- const duration = Math.round(performance.now() - start);
128
-
129
- child.info({ status: c.res.status, durationMs: duration }, "request completed");
130
- });
131
-
132
- // ─── CORS (hardened by default) ──────────────────────────────────────
133
- const trustedOrigins = config.auth?.trustedOrigins ?? [];
134
- app.use("*", cors({
135
- origin: trustedOrigins.length > 0
136
- ? trustedOrigins
137
- : (process.env.NODE_ENV === "production" ? [] : ["http://localhost:*"]),
138
- credentials: true,
139
- allowMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
140
- allowHeaders: ["Content-Type", "Authorization", "x-api-key", "x-request-id"],
141
- maxAge: 86400,
142
- }));
143
-
144
- // ─── CSRF Protection (F14) ──────────────────────────────────────────
145
- app.use("/api/*", csrf({
146
- origin: trustedOrigins.length > 0
147
- ? trustedOrigins
148
- : (process.env.NODE_ENV === "production" ? [] : ["http://localhost:*"]),
149
- }));
150
-
151
- // ─── Body Size Limit (F6) ──────────────────────────────────────────
152
- app.use("*", bodyLimit({
153
- maxSize: 1024 * 1024, // 1 MB default
154
- onError: (c) => c.json({
155
- error: { code: "PAYLOAD_TOO_LARGE", message: "Request body exceeds 1MB limit." },
156
- }, 413),
157
- }));
158
-
159
- // ─── Rate Limiting (F1) ──────────────────────────────────────────────
160
- // Trust X-Forwarded-For ONLY from a known reverse proxy IP.
161
- // Set TRUSTED_PROXY_IP env var to the proxy's IP (e.g., "127.0.0.1").
162
- const trustedProxyIp = process.env.TRUSTED_PROXY_IP;
163
- const getClientIp = (c: { req: { raw: unknown; header: (name: string) => string | undefined } }): string => {
164
- const raw = c.req.raw as { socket?: { remoteAddress?: string } };
165
- const remoteAddress = raw.socket?.remoteAddress;
166
- // Only trust X-Forwarded-For when the direct connection is from a trusted proxy
167
- if (trustedProxyIp && remoteAddress === trustedProxyIp) {
168
- const xff = c.req.header("x-forwarded-for")?.split(",")[0]?.trim();
169
- if (xff) return xff;
170
- }
171
- return remoteAddress ?? "unknown";
172
- };
173
- const keyGenerator = getClientIp;
174
-
175
- app.use("/api/auth/*", rateLimiter({
176
- windowMs: 60 * 1000,
177
- limit: config.rateLimits?.auth ?? 10,
178
- keyGenerator,
179
- }));
180
-
181
- app.use("/api/checkout", rateLimiter({
182
- windowMs: 60 * 1000,
183
- limit: config.rateLimits?.checkout ?? 5,
184
- keyGenerator,
185
- }));
186
-
187
- app.use("/api/*", rateLimiter({
188
- windowMs: 60 * 1000,
189
- limit: config.rateLimits?.api ?? 100,
190
- keyGenerator,
191
- }));
192
-
193
- // ─── Custom Middleware ──────────────────────────────────────────────
194
- if (config.middleware) {
195
- for (const middleware of config.middleware) {
196
- app.use("*", middleware);
197
- }
198
- }
199
-
200
- // ─── Auth ──────────────────────────────────────────────────────────
201
- app.use("/api/auth/*", async (c, next) => {
202
- if (c.req.path.startsWith("/api/auth/pos/")) {
203
- await next();
204
- return;
205
- }
206
- return auth.handler(c.req.raw);
207
- });
208
-
209
- app.use("*", authMiddleware(auth, config));
210
- app.use("*", async (c, next) => {
211
- c.set("auth", auth);
212
- await next();
213
- });
214
-
215
- // ─── Global Error Handler ─────────────────────────────────────────
216
- // Sanitize errors in production — no stack traces, no class names, no schema details
217
- const isProd = process.env.NODE_ENV === "production";
218
- app.onError((err, c) => {
219
- // Catch ZodError from @hono/zod-openapi validation (bypasses defaultHook)
220
- if (err.constructor?.name === "ZodError" || "issues" in err) {
221
- if (isProd) {
222
- return c.json(
223
- { error: { code: "VALIDATION_FAILED", message: "Invalid input." } },
224
- 422,
225
- );
226
- }
227
- const issues = (err as { issues?: Array<{ path: string[]; message: string }> }).issues ?? [];
228
- return c.json(
229
- {
230
- error: {
231
- code: "VALIDATION_FAILED",
232
- message: issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; "),
233
- },
234
- },
235
- 422,
236
- );
237
- }
238
-
239
- if (isProd) {
240
- logger.error({ err }, "unhandled request error");
241
- return c.json(
242
- { error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred." } },
243
- 500,
244
- );
245
- }
246
- return c.json(
247
- { error: { code: "INTERNAL_ERROR", message: err.message } },
248
- 500,
249
- );
250
- });
251
-
252
- // ─── Routes ──────────────────────────────────────────────────────────
253
- app.route("/api", createRestRoutes(kernel));
254
- app.route("/mcp", createMCPHandler(kernel, config.mcpTools));
255
- app.route("/api/me", createCustomerPortalRoutes(kernel));
256
-
257
- if (config.routes) {
258
- config.routes(app as unknown as Hono, kernel);
259
- }
260
-
261
- // OpenAPI spec — disabled in production unless explicitly enabled
262
- const exposeSpec = config.exposeOpenApiSpec ?? !isProd;
263
- if (exposeSpec) {
264
- app.doc("/api/doc", {
265
- openapi: "3.0.0",
266
- info: {
267
- title: "UnifiedCommerce API",
268
- version: config.version ?? "0.0.1",
269
- description: "Headless commerce engine REST API. Includes core and plugin endpoints.",
270
- },
271
- });
272
- } else {
273
- app.get("/api/doc", (c) => c.json({ error: { code: "NOT_FOUND", message: "Not found." } }, 404));
274
- }
275
-
276
- // ─── Job Queue ───────────────────────────────────────────────────────
277
- // Background job processing: webhook delivery, scheduled orders, email tasks.
278
- //
279
- // Three runner strategies (inspired by Payload CMS):
280
- // 1. autorun: in-process polling (long-running servers)
281
- // 2. GET /api/jobs/run: cron endpoint (serverless — Vercel, Cloudflare)
282
- // 3. runPendingJobs() export (custom worker process)
283
-
284
- const { runPendingJobs } = await import("../kernel/jobs/runner.js");
285
- const taskMap = new Map<string, unknown>();
286
- for (const task of config.jobs?.tasks ?? []) {
287
- taskMap.set((task as unknown as { slug?: string; name?: string }).slug ?? (task as unknown as { name: string }).name, task);
288
- }
289
-
290
- const runJobs = async (queue?: string, limit?: number) => {
291
- return runPendingJobs({
292
- db: kernel.database.db as Parameters<typeof runPendingJobs>[0]["db"],
293
- tasks: taskMap as Parameters<typeof runPendingJobs>[0]["tasks"],
294
- queue: queue ?? "default",
295
- limit: limit ?? 10,
296
- logger: logger as Parameters<typeof runPendingJobs>[0]["logger"],
297
- services: kernel.services as unknown as Parameters<typeof runPendingJobs>[0]["services"],
298
- });
299
- };
300
-
301
- // Strategy 1: Built-in cron endpoint for serverless deployments.
302
- // Point Vercel Cron or Cloudflare Cron Trigger at GET /api/jobs/run
303
- // Optional query params: ?queue=emails&limit=20
304
- app.get("/api/jobs/run", async (c) => {
305
- // Always require admin — cron triggers must authenticate
306
- const actor = c.get("actor") as { permissions?: string[] } | null;
307
- if (!actor?.permissions?.includes("*:*")) {
308
- return c.json({ error: { code: "FORBIDDEN", message: "Job runner requires admin access" } }, 403);
309
- }
310
-
311
- const queue = c.req.query("queue") ?? "default";
312
- const limit = parseInt(c.req.query("limit") ?? "10", 10);
313
-
314
- try {
315
- const result = await runJobs(queue, limit);
316
- return c.json({ data: result });
317
- } catch (err) {
318
- logger.error({ err }, "Job runner endpoint failed");
319
- return c.json({ error: { code: "INTERNAL_ERROR", message: "Job processing failed" } }, 500);
320
- }
321
- });
322
-
323
- // Strategy 2: In-process polling for long-running servers (ECS, Cloud Run, Docker).
324
- // Enable via config.jobs.autorun.enabled = true
325
- if (config.jobs?.autorun?.enabled) {
326
- const intervalMs = config.jobs.autorun.intervalMs ?? 10_000;
327
-
328
- const jobInterval = setInterval(async () => {
329
- try {
330
- await runJobs();
331
- } catch (err) {
332
- logger.error({ err }, "Job runner iteration failed");
333
- }
334
- }, intervalMs);
335
-
336
- const cleanup = () => clearInterval(jobInterval);
337
- process.on("SIGTERM", cleanup);
338
- process.on("SIGINT", cleanup);
339
-
340
- logger.info({ intervalMs }, "Job queue autorun started (in-process polling)");
341
- } else {
342
- logger.info(
343
- "Job queue autorun disabled. Use GET /api/jobs/run for serverless cron, " +
344
- "or set config.jobs.autorun.enabled = true for in-process polling.",
345
- );
346
- }
347
-
348
- return { app, kernel, logger, commerce };
349
- }
@@ -1,43 +0,0 @@
1
- import type { Logger } from "./logger.js";
2
-
3
- /**
4
- * Sets up graceful shutdown handlers for SIGTERM and SIGINT.
5
- *
6
- * On signal:
7
- * 1. Stops accepting new connections
8
- * 2. Runs cleanup (close DB pool, flush logs)
9
- * 3. Force-exits after timeout if cleanup hangs
10
- */
11
- export function setupGracefulShutdown(opts: {
12
- cleanup: () => Promise<void>;
13
- logger: Logger;
14
- timeoutMs?: number;
15
- }): void {
16
- const { cleanup, logger, timeoutMs = 30_000 } = opts;
17
- let isShuttingDown = false;
18
-
19
- async function shutdown(signal: string) {
20
- if (isShuttingDown) return;
21
- isShuttingDown = true;
22
-
23
- logger.info({ signal }, "shutdown signal received, draining connections");
24
-
25
- const forceTimer = setTimeout(() => {
26
- logger.error("shutdown timeout exceeded, forcing exit");
27
- process.exit(1);
28
- }, timeoutMs);
29
- forceTimer.unref();
30
-
31
- try {
32
- await cleanup();
33
- logger.info("graceful shutdown complete");
34
- process.exit(0);
35
- } catch (err) {
36
- logger.error({ err }, "error during shutdown");
37
- process.exit(1);
38
- }
39
- }
40
-
41
- process.on("SIGTERM", () => shutdown("SIGTERM"));
42
- process.on("SIGINT", () => shutdown("SIGINT"));
43
- }
@@ -1,129 +0,0 @@
1
- /**
2
- * PGlite-backed test adapter for real PostgreSQL behavior in tests.
3
- *
4
- * Creates an in-memory PGlite (WASM PostgreSQL) instance, pushes the
5
- * core Drizzle schema programmatically via drizzle-kit/api, and provides
6
- * a cleanup function to reset data between tests.
7
- *
8
- * Benefits over in-memory repository doubles:
9
- * - Real SQL execution (Drizzle query generation)
10
- * - PostgreSQL type coercion and constraint enforcement
11
- * - Real transaction rollback semantics
12
- * - Production parity for query edge cases
13
- */
14
-
15
- import { PGlite } from "@electric-sql/pglite";
16
- import { drizzle } from "drizzle-orm/pglite";
17
- import { sql } from "drizzle-orm";
18
- import { createRequire } from "node:module";
19
- import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
20
- import type { DatabaseAdapter } from "../kernel/database/adapter.js";
21
- import { getSchema } from "../kernel/database/migrate.js";
22
- import { ensureDefaultOrg } from "../auth/org.js";
23
-
24
- // Single barrel import --- includes all core modules + auth tables
25
- import * as fullSchema from "../kernel/database/schema.js";
26
- import type { DrizzleDatabase } from "../kernel/database/drizzle-db.js";
27
-
28
- // drizzle-kit/api uses CJS internally; createRequire provides ESM compat.
29
- const require = createRequire(import.meta.url);
30
-
31
- /**
32
- * Pushes the core Drizzle schema to the database using drizzle-kit/api.
33
- *
34
- * Replaces the previous approach of reading migration SQL files. Instead,
35
- * drizzle-kit introspects the live database via information_schema, diffs
36
- * against the pgTable definitions, and generates the minimal DDL needed.
37
- *
38
- * For a fresh PGlite instance, all statements are CREATE TABLE + CREATE INDEX.
39
- */
40
- async function pushCoreSchema(db: PgDatabase<PgQueryResultHKT>): Promise<void> {
41
- const coreSchema = getSchema();
42
- const drizzleKit = require("drizzle-kit/api") as {
43
- pushSchema(
44
- imports: Record<string, unknown>,
45
- drizzleInstance: PgDatabase<PgQueryResultHKT>,
46
- ): Promise<{ apply: () => Promise<void> }>;
47
- };
48
- const { apply } = await drizzleKit.pushSchema(coreSchema, db);
49
- await apply();
50
- }
51
-
52
- /**
53
- * Creates a PGlite-backed database adapter for testing.
54
- *
55
- * Each call creates a new isolated PGlite instance with its own
56
- * in-memory database. Core schema is pushed once during initialization.
57
- *
58
- * @returns A promise resolving to an object containing:
59
- * - adapter: The DatabaseAdapter for use with createKernel
60
- * - db: The Drizzle ORM instance for direct queries
61
- * - cleanup: Function to truncate all tables (call between tests)
62
- */
63
- export async function createPGliteTestAdapter(): Promise<{
64
- adapter: DatabaseAdapter;
65
- db: DrizzleDatabase;
66
- cleanup: () => Promise<void>;
67
- }> {
68
- // Create in-memory PGlite instance
69
- const pg = new PGlite();
70
-
71
- // Wrap with Drizzle ORM first (pushSchema needs the Drizzle instance)
72
- const db = drizzle(pg, { schema: fullSchema });
73
-
74
- // Push core schema via drizzle-kit/api (no migration files needed)
75
- await pushCoreSchema(db as unknown as PgDatabase<PgQueryResultHKT>);
76
-
77
- // Ensure the default organization exists for all tests
78
- await ensureDefaultOrg(db);
79
-
80
- /**
81
- * PGlite-compatible transaction wrapper.
82
- *
83
- * Drizzle's transaction() method can hang with PGlite due to how
84
- * the adapter manages transaction state. This implementation uses
85
- * manual BEGIN/COMMIT control which works more reliably.
86
- */
87
- async function transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> {
88
- await pg.exec("BEGIN");
89
- try {
90
- const result = await fn(db);
91
- await pg.exec("COMMIT");
92
- return result;
93
- } catch (error) {
94
- await pg.exec("ROLLBACK");
95
- throw error;
96
- }
97
- }
98
-
99
- const adapter: DatabaseAdapter = {
100
- provider: "postgresql",
101
- db,
102
- async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> {
103
- return transaction(fn);
104
- },
105
- };
106
-
107
- /**
108
- * Cleanup function to reset data between tests.
109
- * Truncates all tables in reverse dependency order with CASCADE.
110
- */
111
- async function cleanup(): Promise<void> {
112
- await db.execute(sql`
113
- DO $$ DECLARE
114
- r RECORD;
115
- BEGIN
116
- FOR r IN (
117
- SELECT tablename FROM pg_tables
118
- WHERE schemaname = 'public'
119
- ) LOOP
120
- EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
121
- END LOOP;
122
- END $$;
123
- `);
124
- // Re-insert default org after truncation (CASCADE wipes it)
125
- await ensureDefaultOrg(db);
126
- }
127
-
128
- return { adapter, db, cleanup };
129
- }
@@ -1,128 +0,0 @@
1
- /**
2
- * One-call plugin E2E test setup.
3
- *
4
- * Boots a PGlite kernel (or real PG if overridden), programmatically pushes
5
- * the merged schema (core + plugin tables) via drizzle-kit/api, mounts the
6
- * test actor middleware on an OpenAPIHono instance, and registers all plugin
7
- * routes --- matching the production server.ts boot sequence.
8
- *
9
- * Usage:
10
- * import { createPluginTestApp, jsonHeaders, testAdminActor } from "@unifiedcommerce/core";
11
- * const { app } = await createPluginTestApp(myPlugin());
12
- * const res = await app.request("/api/my-route", {
13
- * method: "POST",
14
- * headers: jsonHeaders(testAdminActor),
15
- * body: JSON.stringify({ ... }),
16
- * });
17
- * expect(res.status).toBe(201);
18
- */
19
-
20
- import { OpenAPIHono } from "@hono/zod-openapi";
21
- import { createRequire } from "node:module";
22
- import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
23
- import type { CommerceConfig, CommercePlugin } from "../config/types.js";
24
- import type { Actor } from "../auth/types.js";
25
- import { createTestConfig } from "./create-test-config.js";
26
- import { createKernel } from "../runtime/kernel.js";
27
- import type { Kernel } from "../runtime/kernel.js";
28
- import { buildSchema } from "../kernel/database/migrate.js";
29
- import { ensureDefaultOrg } from "../auth/org.js";
30
-
31
- // drizzle-kit/api uses CJS internally; createRequire provides ESM compat.
32
- const require = createRequire(import.meta.url);
33
-
34
- type DrizzleKitPushResult = {
35
- hasDataLoss: boolean;
36
- warnings: string[];
37
- statementsToExecute: string[];
38
- apply: () => Promise<void>;
39
- };
40
-
41
- /**
42
- * Hono environment type for the test app. Declares the `actor` context
43
- * variable so c.set("actor", ...) / c.get("actor") are properly typed
44
- * without `as never` casts.
45
- */
46
- export type TestAppEnv = {
47
- Variables: {
48
- actor: Actor | null;
49
- };
50
- };
51
-
52
- export interface PluginTestApp {
53
- /** OpenAPIHono instance with test actor middleware and all plugin routes registered. */
54
- app: OpenAPIHono<TestAppEnv>;
55
- /** The booted kernel with database, services, and config. */
56
- kernel: Kernel;
57
- /** Drizzle database instance for direct queries in test assertions. */
58
- db: PgDatabase<PgQueryResultHKT, Record<string, unknown>>;
59
- }
60
-
61
- /**
62
- * Creates a fully-wired test application for plugin E2E testing.
63
- *
64
- * @param plugin - The plugin under test (e.g., `appointmentPlugin()`)
65
- * @param configOverrides - Optional config overrides. Pass `databaseAdapter`
66
- * to use a real PostgreSQL instance instead of PGlite.
67
- */
68
- export async function createPluginTestApp(
69
- plugin: CommercePlugin,
70
- configOverrides: Partial<CommerceConfig> = {},
71
- ): Promise<PluginTestApp> {
72
- // 1. Build config with plugin applied (PGlite auto-provisioned if no adapter)
73
- const config = await createTestConfig({
74
- plugins: [plugin],
75
- ...configOverrides,
76
- });
77
-
78
- // 2. Boot kernel (creates core services, hook registry)
79
- const kernel = createKernel(config);
80
-
81
- // 3. Merge core + plugin schemas
82
- const mergedSchema = buildSchema(config);
83
-
84
- // 4. Programmatic schema push via drizzle-kit/api
85
- // Diffs current DB state against pgTable definitions, generates DDL, applies it.
86
- // On fresh PGlite: creates all tables. On existing DB: creates only missing tables.
87
- const drizzleKit = require("drizzle-kit/api") as {
88
- pushSchema(
89
- imports: Record<string, unknown>,
90
- drizzleInstance: PgDatabase<PgQueryResultHKT>,
91
- ): Promise<DrizzleKitPushResult>;
92
- };
93
- const { apply } = await drizzleKit.pushSchema(
94
- mergedSchema,
95
- kernel.database.db as PgDatabase<PgQueryResultHKT>,
96
- );
97
- await apply();
98
-
99
- // Ensure the default organization exists for plugin tests
100
- await ensureDefaultOrg(kernel.database.db);
101
-
102
- // 5. Create OpenAPIHono --- matching production server.ts
103
- // Plugin routes register via manifest.ts which calls app.openapi().
104
- const app = new OpenAPIHono<TestAppEnv>();
105
-
106
- // 6. Test actor middleware: parse x-test-actor header -> set on context
107
- app.use("*", async (c, next) => {
108
- const header = c.req.header("x-test-actor");
109
- if (header) {
110
- try {
111
- c.set("actor", JSON.parse(header) as Actor);
112
- } catch { /* malformed JSON --- fall through without actor */ }
113
- }
114
- await next();
115
- });
116
-
117
- // 7. Register plugin routes (deferred via config.routes)
118
- const routes = config.routes as
119
- | ((app: unknown, kernel: unknown) => void)
120
- | undefined;
121
- routes?.(app, kernel);
122
-
123
- return {
124
- app,
125
- kernel,
126
- db: kernel.database.db as PgDatabase<PgQueryResultHKT, Record<string, unknown>>,
127
- };
128
- }