@unifiedcommerce/core 0.1.1 → 0.2.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 (257) hide show
  1. package/dist/auth/setup.d.ts.map +1 -1
  2. package/dist/auth/setup.js +8 -3
  3. package/dist/config/types.d.ts +3 -1
  4. package/dist/config/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/interfaces/mcp/server.d.ts +3 -5
  9. package/dist/interfaces/mcp/server.d.ts.map +1 -1
  10. package/dist/interfaces/mcp/server.js +25 -510
  11. package/dist/interfaces/mcp/tool-builder.d.ts +120 -0
  12. package/dist/interfaces/mcp/tool-builder.d.ts.map +1 -0
  13. package/dist/interfaces/mcp/tool-builder.js +224 -0
  14. package/dist/interfaces/mcp/tools/analytics.d.ts +42 -0
  15. package/dist/interfaces/mcp/tools/analytics.d.ts.map +1 -0
  16. package/dist/interfaces/mcp/tools/analytics.js +70 -0
  17. package/dist/interfaces/mcp/tools/cart.d.ts +14 -0
  18. package/dist/interfaces/mcp/tools/cart.d.ts.map +1 -0
  19. package/dist/interfaces/mcp/tools/cart.js +47 -0
  20. package/dist/interfaces/mcp/tools/catalog.d.ts +53 -0
  21. package/dist/interfaces/mcp/tools/catalog.d.ts.map +1 -0
  22. package/dist/interfaces/mcp/tools/catalog.js +284 -0
  23. package/dist/interfaces/mcp/tools/index.d.ts +3 -0
  24. package/dist/interfaces/mcp/tools/index.d.ts.map +1 -0
  25. package/dist/interfaces/mcp/tools/index.js +20 -0
  26. package/dist/interfaces/mcp/tools/inventory.d.ts +27 -0
  27. package/dist/interfaces/mcp/tools/inventory.d.ts.map +1 -0
  28. package/dist/interfaces/mcp/tools/inventory.js +143 -0
  29. package/dist/interfaces/mcp/tools/orders.d.ts +18 -0
  30. package/dist/interfaces/mcp/tools/orders.d.ts.map +1 -0
  31. package/dist/interfaces/mcp/tools/orders.js +82 -0
  32. package/dist/interfaces/mcp/tools/pricing.d.ts +29 -0
  33. package/dist/interfaces/mcp/tools/pricing.d.ts.map +1 -0
  34. package/dist/interfaces/mcp/tools/pricing.js +90 -0
  35. package/dist/interfaces/mcp/tools/promotions.d.ts +44 -0
  36. package/dist/interfaces/mcp/tools/promotions.d.ts.map +1 -0
  37. package/dist/interfaces/mcp/tools/promotions.js +109 -0
  38. package/dist/interfaces/mcp/tools/registry.d.ts +32 -0
  39. package/dist/interfaces/mcp/tools/registry.d.ts.map +1 -0
  40. package/dist/interfaces/mcp/tools/registry.js +55 -0
  41. package/dist/interfaces/mcp/tools/search.d.ts +14 -0
  42. package/dist/interfaces/mcp/tools/search.d.ts.map +1 -0
  43. package/dist/interfaces/mcp/tools/search.js +39 -0
  44. package/dist/interfaces/mcp/tools/webhooks.d.ts +15 -0
  45. package/dist/interfaces/mcp/tools/webhooks.d.ts.map +1 -0
  46. package/dist/interfaces/mcp/tools/webhooks.js +48 -0
  47. package/dist/interfaces/mcp/transport.d.ts +17 -2
  48. package/dist/interfaces/mcp/transport.d.ts.map +1 -1
  49. package/dist/interfaces/mcp/transport.js +91 -44
  50. package/dist/interfaces/rest/router.d.ts.map +1 -1
  51. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  52. package/dist/interfaces/rest/routes/checkout.js +1 -1
  53. package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
  54. package/dist/interfaces/rest/routes/promotions.js +3 -2
  55. package/dist/kernel/database/adapter.d.ts +8 -0
  56. package/dist/kernel/database/adapter.d.ts.map +1 -1
  57. package/dist/kernel/factory/repository-factory.d.ts.map +1 -1
  58. package/dist/kernel/factory/repository-factory.js +3 -1
  59. package/dist/kernel/local-api.d.ts.map +1 -1
  60. package/dist/kernel/local-api.js +2 -0
  61. package/dist/kernel/plugin/manifest.d.ts +3 -3
  62. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  63. package/dist/kernel/plugin/manifest.js +36 -7
  64. package/dist/runtime/kernel.d.ts +1 -2
  65. package/dist/runtime/kernel.d.ts.map +1 -1
  66. package/dist/runtime/kernel.js +16 -8
  67. package/dist/runtime/server.d.ts.map +1 -1
  68. package/dist/runtime/server.js +8 -3
  69. package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
  70. package/dist/test-utils/create-pglite-adapter.js +7 -6
  71. package/dist/tsconfig.tsbuildinfo +1 -0
  72. package/package.json +3 -1
  73. package/src/adapters/console-email.ts +43 -0
  74. package/src/auth/access.ts +187 -0
  75. package/src/auth/auth-schema.ts +139 -0
  76. package/src/auth/middleware.ts +161 -0
  77. package/src/auth/org.ts +41 -0
  78. package/src/auth/permissions.ts +28 -0
  79. package/src/auth/setup.ts +171 -0
  80. package/src/auth/system-actor.ts +19 -0
  81. package/src/auth/types.ts +10 -0
  82. package/src/config/defaults.ts +82 -0
  83. package/src/config/define-config.ts +53 -0
  84. package/src/config/types.ts +301 -0
  85. package/src/generated/plugin-capabilities.d.ts +20 -0
  86. package/src/generated/plugin-manifest.ts +23 -0
  87. package/src/generated/plugin-repositories.d.ts +20 -0
  88. package/src/hooks/checkout-completion.ts +262 -0
  89. package/src/hooks/checkout.ts +677 -0
  90. package/src/hooks/order-emails.ts +62 -0
  91. package/src/index.ts +215 -0
  92. package/src/interfaces/mcp/agent-prompt.ts +174 -0
  93. package/src/interfaces/mcp/context-enrichment.ts +177 -0
  94. package/src/interfaces/mcp/server.ts +47 -0
  95. package/src/interfaces/mcp/tool-builder.ts +261 -0
  96. package/src/interfaces/mcp/tools/analytics.ts +76 -0
  97. package/src/interfaces/mcp/tools/cart.ts +57 -0
  98. package/src/interfaces/mcp/tools/catalog.ts +299 -0
  99. package/src/interfaces/mcp/tools/index.ts +22 -0
  100. package/src/interfaces/mcp/tools/inventory.ts +161 -0
  101. package/src/interfaces/mcp/tools/orders.ts +104 -0
  102. package/src/interfaces/mcp/tools/pricing.ts +94 -0
  103. package/src/interfaces/mcp/tools/promotions.ts +106 -0
  104. package/src/interfaces/mcp/tools/registry.ts +101 -0
  105. package/src/interfaces/mcp/tools/search.ts +42 -0
  106. package/src/interfaces/mcp/tools/webhooks.ts +48 -0
  107. package/src/interfaces/mcp/transport.ts +128 -0
  108. package/src/interfaces/rest/customer-portal.ts +299 -0
  109. package/src/interfaces/rest/index.ts +74 -0
  110. package/src/interfaces/rest/router.ts +333 -0
  111. package/src/interfaces/rest/routes/admin-jobs.ts +58 -0
  112. package/src/interfaces/rest/routes/audit.ts +50 -0
  113. package/src/interfaces/rest/routes/carts.ts +89 -0
  114. package/src/interfaces/rest/routes/catalog.ts +493 -0
  115. package/src/interfaces/rest/routes/checkout.ts +284 -0
  116. package/src/interfaces/rest/routes/inventory.ts +70 -0
  117. package/src/interfaces/rest/routes/media.ts +86 -0
  118. package/src/interfaces/rest/routes/orders.ts +78 -0
  119. package/src/interfaces/rest/routes/payments.ts +60 -0
  120. package/src/interfaces/rest/routes/pricing.ts +57 -0
  121. package/src/interfaces/rest/routes/promotions.ts +93 -0
  122. package/src/interfaces/rest/routes/search.ts +71 -0
  123. package/src/interfaces/rest/routes/webhooks.ts +46 -0
  124. package/src/interfaces/rest/schemas/admin-jobs.ts +40 -0
  125. package/src/interfaces/rest/schemas/audit.ts +46 -0
  126. package/src/interfaces/rest/schemas/carts.ts +125 -0
  127. package/src/interfaces/rest/schemas/catalog.ts +450 -0
  128. package/src/interfaces/rest/schemas/checkout.ts +66 -0
  129. package/src/interfaces/rest/schemas/customer-portal.ts +195 -0
  130. package/src/interfaces/rest/schemas/inventory.ts +138 -0
  131. package/src/interfaces/rest/schemas/media.ts +75 -0
  132. package/src/interfaces/rest/schemas/orders.ts +104 -0
  133. package/src/interfaces/rest/schemas/pricing.ts +80 -0
  134. package/src/interfaces/rest/schemas/promotions.ts +110 -0
  135. package/src/interfaces/rest/schemas/responses.ts +85 -0
  136. package/src/interfaces/rest/schemas/search.ts +58 -0
  137. package/src/interfaces/rest/schemas/shared.ts +62 -0
  138. package/src/interfaces/rest/schemas/webhooks.ts +68 -0
  139. package/src/interfaces/rest/utils.ts +104 -0
  140. package/src/interfaces/rest/webhook-router.ts +50 -0
  141. package/src/kernel/compensation/executor.ts +61 -0
  142. package/src/kernel/compensation/types.ts +26 -0
  143. package/src/kernel/database/adapter.ts +21 -0
  144. package/src/kernel/database/drizzle-db.ts +56 -0
  145. package/src/kernel/database/migrate.ts +76 -0
  146. package/src/kernel/database/plugin-types.ts +34 -0
  147. package/src/kernel/database/schema.ts +49 -0
  148. package/src/kernel/database/scoped-db.ts +68 -0
  149. package/src/kernel/database/tx-context.ts +46 -0
  150. package/src/kernel/error-mapper.ts +15 -0
  151. package/src/kernel/errors.ts +89 -0
  152. package/src/kernel/factory/repository-factory.ts +244 -0
  153. package/src/kernel/hooks/create-context.ts +43 -0
  154. package/src/kernel/hooks/executor.ts +88 -0
  155. package/src/kernel/hooks/registry.ts +74 -0
  156. package/src/kernel/hooks/types.ts +52 -0
  157. package/src/kernel/http-error.ts +44 -0
  158. package/src/kernel/jobs/adapter.ts +36 -0
  159. package/src/kernel/jobs/drizzle-adapter.ts +58 -0
  160. package/src/kernel/jobs/runner.ts +153 -0
  161. package/src/kernel/jobs/schema.ts +46 -0
  162. package/src/kernel/jobs/types.ts +30 -0
  163. package/src/kernel/local-api.ts +187 -0
  164. package/src/kernel/plugin/manifest.ts +271 -0
  165. package/src/kernel/query/executor.ts +184 -0
  166. package/src/kernel/query/registry.ts +46 -0
  167. package/src/kernel/result.ts +33 -0
  168. package/src/kernel/schema/extra-columns.ts +37 -0
  169. package/src/kernel/service-registry.ts +76 -0
  170. package/src/kernel/service-timing.ts +89 -0
  171. package/src/kernel/state-machine/machine.ts +101 -0
  172. package/src/modules/analytics/drizzle-adapter.ts +426 -0
  173. package/src/modules/analytics/hooks.ts +11 -0
  174. package/src/modules/analytics/models.ts +125 -0
  175. package/src/modules/analytics/repository/index.ts +6 -0
  176. package/src/modules/analytics/service.ts +245 -0
  177. package/src/modules/analytics/types.ts +180 -0
  178. package/src/modules/audit/hooks.ts +78 -0
  179. package/src/modules/audit/schema.ts +33 -0
  180. package/src/modules/audit/service.ts +151 -0
  181. package/src/modules/cart/access.ts +27 -0
  182. package/src/modules/cart/matcher.ts +26 -0
  183. package/src/modules/cart/repository/index.ts +234 -0
  184. package/src/modules/cart/schema.ts +42 -0
  185. package/src/modules/cart/schemas.ts +38 -0
  186. package/src/modules/cart/service.ts +541 -0
  187. package/src/modules/catalog/repository/index.ts +772 -0
  188. package/src/modules/catalog/schema.ts +203 -0
  189. package/src/modules/catalog/schemas.ts +104 -0
  190. package/src/modules/catalog/service.ts +1544 -0
  191. package/src/modules/customers/repository/index.ts +327 -0
  192. package/src/modules/customers/schema.ts +64 -0
  193. package/src/modules/customers/service.ts +171 -0
  194. package/src/modules/fulfillment/repository/index.ts +426 -0
  195. package/src/modules/fulfillment/schema.ts +101 -0
  196. package/src/modules/fulfillment/service.ts +555 -0
  197. package/src/modules/fulfillment/types.ts +59 -0
  198. package/src/modules/inventory/repository/index.ts +509 -0
  199. package/src/modules/inventory/schema.ts +94 -0
  200. package/src/modules/inventory/schemas.ts +38 -0
  201. package/src/modules/inventory/service.ts +490 -0
  202. package/src/modules/media/adapter.ts +17 -0
  203. package/src/modules/media/repository/index.ts +274 -0
  204. package/src/modules/media/schema.ts +41 -0
  205. package/src/modules/media/service.ts +151 -0
  206. package/src/modules/orders/repository/index.ts +287 -0
  207. package/src/modules/orders/schema.ts +66 -0
  208. package/src/modules/orders/service.ts +619 -0
  209. package/src/modules/orders/stale-order-cleanup.ts +76 -0
  210. package/src/modules/organization/service.ts +191 -0
  211. package/src/modules/payments/adapter.ts +47 -0
  212. package/src/modules/payments/repository/index.ts +6 -0
  213. package/src/modules/payments/service.ts +107 -0
  214. package/src/modules/pricing/repository/index.ts +291 -0
  215. package/src/modules/pricing/schema.ts +71 -0
  216. package/src/modules/pricing/schemas.ts +38 -0
  217. package/src/modules/pricing/service.ts +494 -0
  218. package/src/modules/promotions/repository/index.ts +325 -0
  219. package/src/modules/promotions/schema.ts +62 -0
  220. package/src/modules/promotions/schemas.ts +38 -0
  221. package/src/modules/promotions/service.ts +598 -0
  222. package/src/modules/search/adapter.ts +57 -0
  223. package/src/modules/search/hooks.ts +12 -0
  224. package/src/modules/search/repository/index.ts +6 -0
  225. package/src/modules/search/service.ts +315 -0
  226. package/src/modules/shipping/calculator.ts +188 -0
  227. package/src/modules/shipping/repository/index.ts +6 -0
  228. package/src/modules/shipping/service.ts +51 -0
  229. package/src/modules/tax/adapter.ts +60 -0
  230. package/src/modules/tax/repository/index.ts +6 -0
  231. package/src/modules/tax/service.ts +53 -0
  232. package/src/modules/webhooks/hook.ts +34 -0
  233. package/src/modules/webhooks/repository/index.ts +278 -0
  234. package/src/modules/webhooks/schema.ts +56 -0
  235. package/src/modules/webhooks/service.ts +117 -0
  236. package/src/modules/webhooks/signing.ts +6 -0
  237. package/src/modules/webhooks/ssrf-guard.ts +71 -0
  238. package/src/modules/webhooks/tasks.ts +52 -0
  239. package/src/modules/webhooks/worker.ts +134 -0
  240. package/src/runtime/commerce.ts +145 -0
  241. package/src/runtime/kernel.ts +426 -0
  242. package/src/runtime/logger.ts +36 -0
  243. package/src/runtime/server.ts +355 -0
  244. package/src/runtime/shutdown.ts +43 -0
  245. package/src/test-utils/create-pglite-adapter.ts +129 -0
  246. package/src/test-utils/create-plugin-test-app.ts +128 -0
  247. package/src/test-utils/create-repository-test-harness.ts +16 -0
  248. package/src/test-utils/create-test-config.ts +190 -0
  249. package/src/test-utils/create-test-kernel.ts +7 -0
  250. package/src/test-utils/create-test-plugin-context.ts +75 -0
  251. package/src/test-utils/rest-api-test-utils.ts +265 -0
  252. package/src/test-utils/test-actors.ts +62 -0
  253. package/src/test-utils/typed-hooks.ts +54 -0
  254. package/src/types/commerce-types.ts +34 -0
  255. package/src/utils/id.ts +3 -0
  256. package/src/utils/logger.ts +18 -0
  257. package/src/utils/pagination.ts +22 -0
@@ -0,0 +1,134 @@
1
+ import { signWebhookPayload } from "./signing.js";
2
+ import { isPrivateIp } from "./ssrf-guard.js";
3
+ import type { WebhooksRepository } from "./repository/index.js";
4
+
5
+ /**
6
+ * SSRF prevention: reject webhook URLs targeting private/internal hosts.
7
+ * Blocks RFC 1918, loopback, link-local, and common internal domains.
8
+ */
9
+ const PRIVATE_HOST_PATTERNS = [
10
+ /^127\./, // loopback
11
+ /^10\./, // RFC 1918 class A
12
+ /^172\.(1[6-9]|2[0-9]|3[01])\./, // RFC 1918 class B
13
+ /^192\.168\./, // RFC 1918 class C
14
+ /^169\.254\./, // link-local / AWS IMDS
15
+ /^0\.0\.0\.0/, // unspecified
16
+ /^localhost$/i,
17
+ /\.local$/i,
18
+ /\.internal$/i,
19
+ ];
20
+
21
+ function validateWebhookUrl(url: string): void {
22
+ const parsed = new URL(url);
23
+
24
+ if (process.env.NODE_ENV === "production" && parsed.protocol !== "https:") {
25
+ throw new Error("Webhook URLs must use HTTPS in production.");
26
+ }
27
+
28
+ const hostname = parsed.hostname;
29
+ for (const pattern of PRIVATE_HOST_PATTERNS) {
30
+ if (pattern.test(hostname)) {
31
+ throw new Error(`Webhook URLs cannot target private hosts: ${hostname}`);
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Resolve the webhook URL hostname via DNS and verify the resolved IP is not
38
+ * private. This closes the DNS rebinding gap where a hostname initially
39
+ * resolves to a public IP but later rebinds to an internal address.
40
+ */
41
+ async function validateResolvedIp(url: string): Promise<void> {
42
+ const { lookup } = await import("node:dns/promises");
43
+ const parsed = new URL(url);
44
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
45
+
46
+ // Skip DNS resolution for raw IP addresses — they are already checked
47
+ // by validateWebhookUrl via PRIVATE_HOST_PATTERNS.
48
+ const isIpLiteral = /^[\d.]+$/.test(hostname) || hostname.includes(":");
49
+ if (isIpLiteral) return;
50
+
51
+ try {
52
+ const { address } = await lookup(hostname);
53
+ if (isPrivateIp(address)) {
54
+ throw new Error(
55
+ `Webhook URL hostname "${hostname}" resolved to private IP ${address}`,
56
+ );
57
+ }
58
+ } catch (err) {
59
+ if (err instanceof Error && err.message.includes("resolved to private")) {
60
+ throw err;
61
+ }
62
+ throw new Error(`Failed to resolve webhook URL hostname "${hostname}": ${err}`);
63
+ }
64
+ }
65
+
66
+ interface WorkerDeps {
67
+ repository: WebhooksRepository;
68
+ fetchImpl?: typeof fetch;
69
+ }
70
+
71
+ export class WebhookDeliveryWorker {
72
+ private fetchImpl: typeof fetch;
73
+
74
+ constructor(private deps: WorkerDeps) {
75
+ this.fetchImpl = deps.fetchImpl ?? fetch;
76
+ }
77
+
78
+ async deliver(args: {
79
+ endpoint: { id: string; url: string; secret: string };
80
+ eventName: string;
81
+ payload: unknown;
82
+ }): Promise<void> {
83
+ // SSRF prevention: validate URL string patterns + resolved DNS IP
84
+ validateWebhookUrl(args.endpoint.url);
85
+ await validateResolvedIp(args.endpoint.url);
86
+
87
+ let attempt = 0;
88
+ const maxAttempts = 3;
89
+
90
+ while (attempt < maxAttempts) {
91
+ attempt += 1;
92
+ const signature = signWebhookPayload(args.endpoint.secret, args.payload);
93
+
94
+ try {
95
+ const response = await this.fetchImpl(args.endpoint.url, {
96
+ method: "POST",
97
+ headers: {
98
+ "content-type": "application/json",
99
+ "x-commerce-signature": signature,
100
+ "x-commerce-event": args.eventName,
101
+ },
102
+ body: JSON.stringify(args.payload),
103
+ signal: AbortSignal.timeout(10_000),
104
+ });
105
+
106
+ await this.deps.repository.createDelivery({
107
+ endpointId: args.endpoint.id,
108
+ eventName: args.eventName,
109
+ payload: args.payload,
110
+ statusCode: response.status,
111
+ attemptCount: attempt,
112
+ ...(response.ok ? { deliveredAt: new Date() } : {}),
113
+ ...(!response.ok ? { failedAt: new Date() } : {}),
114
+ ...(!response.ok && attempt < maxAttempts
115
+ ? { nextRetryAt: new Date(Date.now() + 2 ** attempt * 1000) }
116
+ : {}),
117
+ });
118
+
119
+ if (response.ok) return;
120
+ } catch {
121
+ await this.deps.repository.createDelivery({
122
+ endpointId: args.endpoint.id,
123
+ eventName: args.eventName,
124
+ payload: args.payload,
125
+ attemptCount: attempt,
126
+ failedAt: new Date(),
127
+ ...(attempt < maxAttempts
128
+ ? { nextRetryAt: new Date(Date.now() + 2 ** attempt * 1000) }
129
+ : {}),
130
+ });
131
+ }
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,145 @@
1
+ import type { Actor } from "../auth/types.js";
2
+ import type { CommerceConfig } from "../config/types.js";
3
+ import type { Kernel } from "./kernel.js";
4
+ import { createKernel } from "./kernel.js";
5
+ import { ensureDefaultOrg } from "../auth/org.js";
6
+ import { createAuth, type AuthInstance } from "../auth/setup.js";
7
+ import { createLogger, type Logger } from "./logger.js";
8
+ import { createLocalAPI, type CommerceLocalAPI, type LocalAPIOptions } from "../kernel/local-api.js";
9
+
10
+ /**
11
+ * The commerce instance returned by `createCommerce()`.
12
+ *
13
+ * This is the headless, framework-agnostic entry point.
14
+ * No HTTP server, no Hono — just typed services and a local API.
15
+ *
16
+ * ## Usage with Next.js App Router:
17
+ *
18
+ * ```typescript
19
+ * // lib/commerce.ts
20
+ * import { createCommerce } from "@unifiedcommerce/core";
21
+ * import config from "../commerce.config.js";
22
+ *
23
+ * export const commerce = await createCommerce(config);
24
+ *
25
+ * // app/products/page.tsx (Server Component)
26
+ * import { commerce } from "@/lib/commerce";
27
+ *
28
+ * export default async function ProductsPage() {
29
+ * const products = await commerce.api.catalog.list({ limit: 20 });
30
+ * if (!products.ok) return <div>Error</div>;
31
+ * return <ProductGrid items={products.value.items} />;
32
+ * }
33
+ * ```
34
+ *
35
+ * ## Usage with TanStack Start:
36
+ *
37
+ * ```typescript
38
+ * // app/routes/products.tsx
39
+ * import { createServerFn } from "@tanstack/start";
40
+ * import { commerce } from "../lib/commerce.js";
41
+ *
42
+ * const getProducts = createServerFn("GET", async () => {
43
+ * return commerce.api.catalog.list({ limit: 20 });
44
+ * });
45
+ * ```
46
+ *
47
+ * ## Usage with SvelteKit:
48
+ *
49
+ * ```typescript
50
+ * // src/lib/server/commerce.ts
51
+ * import { createCommerce } from "@unifiedcommerce/core";
52
+ * import config from "./commerce.config.js";
53
+ * export const commerce = await createCommerce(config);
54
+ *
55
+ * // src/routes/products/+page.server.ts
56
+ * import { commerce } from "$lib/server/commerce";
57
+ * export async function load() {
58
+ * const products = await commerce.api.catalog.list({ limit: 20 });
59
+ * return { products: products.ok ? products.value : { items: [] } };
60
+ * }
61
+ * ```
62
+ */
63
+ export interface CommerceInstance {
64
+ /** Proxy-based local API — auto-injects actor/tx to every service call */
65
+ api: CommerceLocalAPI;
66
+
67
+ /** Raw kernel for advanced usage (hooks, database, config) */
68
+ kernel: Kernel;
69
+
70
+ /** Drizzle database instance for direct queries */
71
+ db: unknown;
72
+
73
+ /** Auth instance (Better Auth) for session management */
74
+ auth: AuthInstance;
75
+
76
+ /** Logger */
77
+ logger: Logger;
78
+
79
+ /**
80
+ * Create a scoped API for a specific user/actor.
81
+ * Use this in authenticated routes to scope data access.
82
+ *
83
+ * ```typescript
84
+ * // In a Next.js server action:
85
+ * const userApi = commerce.withActor({
86
+ * type: "user", userId: session.userId, ...
87
+ * });
88
+ * const orders = await userApi.orders.list({ limit: 10 });
89
+ * // Only returns orders for this user's org
90
+ * ```
91
+ */
92
+ withActor(actor: Actor): CommerceLocalAPI;
93
+
94
+ /**
95
+ * Create a scoped API within a database transaction.
96
+ *
97
+ * ```typescript
98
+ * await commerce.kernel.database.transaction(async (tx) => {
99
+ * const txApi = commerce.withTransaction(tx, actor);
100
+ * await txApi.inventory.adjust({ entityId, adjustment: -1, reason: "sold" });
101
+ * await txApi.orders.create({ ... });
102
+ * });
103
+ * ```
104
+ */
105
+ withTransaction(tx: unknown, actor?: Actor | null): CommerceLocalAPI;
106
+ }
107
+
108
+ /**
109
+ * Create a headless commerce instance.
110
+ *
111
+ * This is the primary entry point for using UnifiedCommerce without an HTTP server.
112
+ * It initializes the kernel, database, auth, and returns a local API that works
113
+ * exactly like the REST API but without HTTP overhead.
114
+ *
115
+ * The Hono server (`createServer`) is optional — use it only when you need
116
+ * a standalone HTTP API. For Next.js, TanStack Start, SvelteKit, Nuxt, etc.,
117
+ * use `createCommerce()` directly.
118
+ */
119
+ export async function createCommerce(
120
+ config: CommerceConfig,
121
+ ): Promise<CommerceInstance> {
122
+ const kernel = createKernel(config);
123
+ await ensureDefaultOrg(kernel.database.db);
124
+ const auth = createAuth(kernel.database, config);
125
+ const logger = createLogger(config);
126
+
127
+ // Default API: no actor (public access), no transaction
128
+ const api = createLocalAPI(kernel);
129
+
130
+ return {
131
+ api,
132
+ kernel,
133
+ db: kernel.database.db,
134
+ auth,
135
+ logger,
136
+
137
+ withActor(actor: Actor): CommerceLocalAPI {
138
+ return createLocalAPI(kernel, { actor });
139
+ },
140
+
141
+ withTransaction(tx: unknown, actor?: Actor | null): CommerceLocalAPI {
142
+ return createLocalAPI(kernel, { actor: actor ?? null, tx });
143
+ },
144
+ };
145
+ }
@@ -0,0 +1,426 @@
1
+ import type {
2
+ CommerceConfig,
3
+ MCPTool,
4
+ } from "../config/types.js";
5
+ import { HookRegistry, type HookHandler } from "../kernel/hooks/registry.js";
6
+ import {
7
+ createDatabaseConnection,
8
+ type DatabaseAdapter,
9
+ } from "../kernel/database/adapter.js";
10
+ import type { PluginDb } from "../kernel/database/plugin-types.js";
11
+
12
+ import { CatalogServiceImpl } from "../modules/catalog/service.js";
13
+ import { CatalogRepository } from "../modules/catalog/repository/index.js";
14
+ import { InventoryRepository } from "../modules/inventory/repository/index.js";
15
+ import { CartRepository } from "../modules/cart/repository/index.js";
16
+ import { OrdersRepository } from "../modules/orders/repository/index.js";
17
+ import { CustomersRepository } from "../modules/customers/repository/index.js";
18
+ import { PricingRepository } from "../modules/pricing/repository/index.js";
19
+ import { PromotionsRepository } from "../modules/promotions/repository/index.js";
20
+ import { FulfillmentRepository } from "../modules/fulfillment/repository/index.js";
21
+ import { WebhooksRepository } from "../modules/webhooks/repository/index.js";
22
+ import { MediaRepository } from "../modules/media/repository/index.js";
23
+ import type { DrizzleDatabase } from "../kernel/database/drizzle-db.js";
24
+ import { InventoryService } from "../modules/inventory/service.js";
25
+ import { MediaService } from "../modules/media/service.js";
26
+ import { CartService } from "../modules/cart/service.js";
27
+ import { OrderService } from "../modules/orders/service.js";
28
+ import { PaymentsService } from "../modules/payments/service.js";
29
+ import { FulfillmentService } from "../modules/fulfillment/service.js";
30
+ import { CustomerService } from "../modules/customers/service.js";
31
+ import { WebhookService } from "../modules/webhooks/service.js";
32
+ import { AnalyticsService } from "../modules/analytics/service.js";
33
+ import { DrizzleAnalyticsAdapter } from "../modules/analytics/drizzle-adapter.js";
34
+ import { BUILTIN_ANALYTICS_MODELS } from "../modules/analytics/models.js";
35
+ import { PricingService } from "../modules/pricing/service.js";
36
+ import { PromotionService } from "../modules/promotions/service.js";
37
+ import { TaxService } from "../modules/tax/service.js";
38
+ import { ShippingService } from "../modules/shipping/service.js";
39
+ import { SearchService } from "../modules/search/service.js";
40
+ import { WebhookDeliveryWorker } from "../modules/webhooks/worker.js";
41
+ import {
42
+ createAuditService,
43
+ type AuditService,
44
+ } from "../modules/audit/service.js";
45
+ import { OrganizationService } from "../modules/organization/service.js";
46
+ import { createLogger } from "../utils/logger.js";
47
+ import { withTiming } from "../kernel/service-timing.js";
48
+ import { extendOrderStateMachine } from "../kernel/state-machine/machine.js";
49
+ import { DEFAULT_ORG_ID } from "../auth/org.js";
50
+ import { deliverWebhooks } from "../modules/webhooks/hook.js";
51
+ // Analytics event recording hooks removed (RFC-006): source tables ARE the events.
52
+ // The DrizzleAnalyticsAdapter queries orders/inventory directly via SQL.
53
+ import { syncToSearchIndex } from "../modules/search/hooks.js";
54
+ import { auditHooks } from "../modules/audit/hooks.js";
55
+ import { sendOrderStatusEmail } from "../hooks/order-emails.js";
56
+ import { DrizzleJobsAdapter } from "../kernel/jobs/drizzle-adapter.js";
57
+
58
+ export interface WebhookDeliveryPayload {
59
+ endpoint: { id: string; url: string; secret: string };
60
+ eventName: string;
61
+ payload: unknown;
62
+ }
63
+
64
+ export interface Kernel {
65
+ config: CommerceConfig;
66
+ hooks: HookRegistry;
67
+ database: DatabaseAdapter;
68
+ services: {
69
+ catalog: CatalogServiceImpl;
70
+ inventory: InventoryService;
71
+ media: MediaService;
72
+ cart: CartService;
73
+ orders: OrderService;
74
+ payments: PaymentsService;
75
+ fulfillment: FulfillmentService;
76
+ customers: CustomerService;
77
+ webhooks: WebhookService & {
78
+ enqueueDelivery(payload: WebhookDeliveryPayload): Promise<void>;
79
+ };
80
+ analytics: AnalyticsService;
81
+ pricing: PricingService;
82
+ promotions: PromotionService;
83
+ tax: TaxService;
84
+ shipping: ShippingService;
85
+ search: SearchService;
86
+ audit: AuditService;
87
+ email: CommerceConfig["email"];
88
+ organization: OrganizationService;
89
+ };
90
+ mcpTools: MCPTool[];
91
+ logger: ReturnType<typeof createLogger>;
92
+ getMCPActor(): {
93
+ type: "api_key";
94
+ userId: string;
95
+ email: null;
96
+ name: string;
97
+ vendorId: null;
98
+ organizationId: string;
99
+ role: string;
100
+ permissions: string[];
101
+ };
102
+ }
103
+
104
+ const requiredServiceKeys = [
105
+ "catalog",
106
+ "inventory",
107
+ "media",
108
+ "cart",
109
+ "orders",
110
+ "payments",
111
+ "fulfillment",
112
+ "customers",
113
+ "webhooks",
114
+ "analytics",
115
+ "pricing",
116
+ "promotions",
117
+ "tax",
118
+ "shipping",
119
+ "search",
120
+ "audit",
121
+ ] as const satisfies Array<keyof Kernel["services"]>;
122
+
123
+ function assertServicesReady(
124
+ services: Partial<Kernel["services"]>,
125
+ ): asserts services is Kernel["services"] {
126
+ for (const key of requiredServiceKeys) {
127
+ if (services[key] === undefined) {
128
+ throw new Error(`Kernel service "${String(key)}" was not initialized.`);
129
+ }
130
+ }
131
+ }
132
+
133
+ function registerConfiguredHooks(
134
+ config: CommerceConfig,
135
+ hooks: HookRegistry,
136
+ ): void {
137
+ for (const [entityType, entityConfig] of Object.entries(
138
+ config.entities ?? {},
139
+ )) {
140
+ const entityHooks = entityConfig.hooks ?? {};
141
+ for (const [hookName, handlers] of Object.entries(entityHooks)) {
142
+ hooks.registerConfigHooks(
143
+ `catalog.${entityType}.${hookName}`,
144
+ handlers ?? [],
145
+ );
146
+ }
147
+ }
148
+
149
+ for (const [moduleName, moduleConfig] of [
150
+ ["cart", config.cart],
151
+ ["checkout", config.checkout],
152
+ ["orders", config.orders],
153
+ ["inventory", config.inventory],
154
+ ] as const) {
155
+ const hooksObject = moduleConfig?.hooks;
156
+ if (!hooksObject) continue;
157
+ for (const [hookName, handlers] of Object.entries(hooksObject)) {
158
+ const normalizedHandlers = (Array.isArray(handlers) ? handlers : []) as HookHandler[];
159
+ hooks.registerConfigHooks(
160
+ `${moduleName}.${hookName}`,
161
+ normalizedHandlers,
162
+ );
163
+ }
164
+ }
165
+
166
+ // Webhook delivery (async via job queue) — 14 event types
167
+ hooks.append("orders.afterCreate", deliverWebhooks);
168
+ hooks.append("orders.afterStatusChange", deliverWebhooks);
169
+ hooks.append("orders.afterStatusChange", sendOrderStatusEmail as (...args: unknown[]) => unknown);
170
+ hooks.append("catalog.afterCreate", deliverWebhooks);
171
+ hooks.append("catalog.afterUpdate", deliverWebhooks);
172
+ hooks.append("catalog.afterDelete", deliverWebhooks);
173
+ hooks.append("inventory.afterAdjust", deliverWebhooks);
174
+ hooks.append("customers.afterCreate", deliverWebhooks);
175
+ hooks.append("customers.afterUpdate", deliverWebhooks);
176
+ hooks.append("pricing.afterCreate", deliverWebhooks);
177
+ hooks.append("pricing.afterUpdate", deliverWebhooks);
178
+ hooks.append("promotions.afterCreate", deliverWebhooks);
179
+ hooks.append("promotions.afterUpdate", deliverWebhooks);
180
+ hooks.append("fulfillment.afterCreate", deliverWebhooks);
181
+ hooks.append("cart.afterAddItem", deliverWebhooks);
182
+
183
+ // Analytics: no event recording hooks needed (RFC-006).
184
+ // The DrizzleAnalyticsAdapter queries source tables directly via SQL.
185
+
186
+ // Search index sync
187
+ hooks.append("catalog.afterCreate", syncToSearchIndex);
188
+ hooks.append("catalog.afterUpdate", syncToSearchIndex);
189
+
190
+ // Auto-audit — records every create/update/delete across all modules
191
+ for (const [key, handler] of Object.entries(auditHooks)) {
192
+ hooks.append(key, handler);
193
+ }
194
+ }
195
+
196
+ export function createKernel(config: CommerceConfig): Kernel {
197
+ const hooks = new HookRegistry();
198
+ const logger = createLogger("kernel");
199
+ hooks.setLogger({ error: (obj, msg) => logger.error(msg, obj) });
200
+
201
+ if (!config.storage) {
202
+ throw new Error(
203
+ "Storage adapter is required. Configure `storage` in defineConfig (for example: localStorageAdapter for development, or s3StorageAdapter/r2StorageAdapter for object storage).",
204
+ );
205
+ }
206
+
207
+ const database = createDatabaseConnection({
208
+ adapter: config.databaseAdapter ?? {
209
+ provider: config.database.provider,
210
+ db: {},
211
+ async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> {
212
+ return fn({});
213
+ },
214
+ },
215
+ });
216
+ const mcpTools: MCPTool[] = [];
217
+
218
+ const services: Partial<Kernel["services"]> = {
219
+ email: config.email,
220
+ organization: new OrganizationService(database.db),
221
+ };
222
+
223
+ const serviceContainer = services as Record<string, unknown>;
224
+
225
+ // Expose database on service container so plugin hooks can access it
226
+ serviceContainer.database = database;
227
+
228
+ const db = database.db as DrizzleDatabase;
229
+
230
+ // Expose jobs adapter on service container so plugins can enqueue background work
231
+ const jobsAdapter = new DrizzleJobsAdapter(db, new Map());
232
+ serviceContainer.jobs = jobsAdapter;
233
+
234
+ const pricingRepository = new PricingRepository(db);
235
+ const promotionsRepository = new PromotionsRepository(db);
236
+
237
+ services.tax = new TaxService({ adapter: config.tax?.adapter });
238
+ services.payments = new PaymentsService(config.payments);
239
+
240
+ const customersRepository = new CustomersRepository(db);
241
+ services.customers = new CustomerService({
242
+ repository: customersRepository,
243
+ });
244
+
245
+ const webhooksRepository = new WebhooksRepository(db);
246
+ const webhookWorker = new WebhookDeliveryWorker({
247
+ repository: webhooksRepository,
248
+ });
249
+ services.webhooks = Object.assign(
250
+ new WebhookService({
251
+ repository: webhooksRepository,
252
+ }),
253
+ {
254
+ async enqueueDelivery(payload: WebhookDeliveryPayload) {
255
+ await webhookWorker.deliver(payload);
256
+ },
257
+ },
258
+ );
259
+
260
+ const inventoryRepository = new InventoryRepository(db);
261
+ services.inventory = new InventoryService({
262
+ repository: inventoryRepository,
263
+ hooks,
264
+ config,
265
+ services: serviceContainer,
266
+ database,
267
+ });
268
+
269
+ const catalogRepository = new CatalogRepository(db);
270
+ services.catalog = new CatalogServiceImpl({
271
+ repository: catalogRepository,
272
+ hooks,
273
+ config,
274
+ services: serviceContainer,
275
+ });
276
+
277
+ services.search = new SearchService({
278
+ catalogRepository: catalogRepository,
279
+ ...(config.search?.adapter ? { adapter: config.search.adapter } : {}),
280
+ ...(config.search?.defaultFacets
281
+ ? { defaultFacets: config.search.defaultFacets }
282
+ : {}),
283
+ });
284
+
285
+ const cartRepository = new CartRepository(db);
286
+ services.cart = new CartService({
287
+ repository: cartRepository,
288
+ catalogRepository: catalogRepository,
289
+ hooks,
290
+ config,
291
+ services: serviceContainer,
292
+ });
293
+
294
+ const ordersRepository = new OrdersRepository(db);
295
+ services.orders = new OrderService({
296
+ repository: ordersRepository,
297
+ hooks,
298
+ services: serviceContainer,
299
+ kernel: { database: { db: database.db as PluginDb } },
300
+ ...(config.orders?.customTransitions
301
+ ? { stateMachine: extendOrderStateMachine(config.orders.customTransitions) }
302
+ : {}),
303
+ });
304
+
305
+ const fulfillmentRepository = new FulfillmentRepository(db);
306
+ services.fulfillment = new FulfillmentService({
307
+ repository: fulfillmentRepository,
308
+ ordersRepository: ordersRepository,
309
+ inventoryService: services.inventory,
310
+ });
311
+
312
+ services.pricing = new PricingService({
313
+ repository: pricingRepository,
314
+ catalogRepository: catalogRepository,
315
+ });
316
+
317
+ services.promotions = new PromotionService({
318
+ repository: promotionsRepository,
319
+ catalogRepository: catalogRepository,
320
+ ordersRepository: ordersRepository,
321
+ });
322
+
323
+ // AnalyticsService — always uses DrizzleAnalyticsAdapter (direct SQL).
324
+ // Plugins add their own models via the analyticsModels manifest slot.
325
+ const analyticsAdapter = new DrizzleAnalyticsAdapter(db);
326
+ for (const model of BUILTIN_ANALYTICS_MODELS) {
327
+ analyticsAdapter.registerModel(model);
328
+ }
329
+ services.analytics = new AnalyticsService({
330
+ adapter: analyticsAdapter,
331
+ config,
332
+ });
333
+
334
+ services.shipping = new ShippingService({
335
+ config,
336
+ catalogRepository: catalogRepository,
337
+ });
338
+
339
+ const mediaRepository = new MediaRepository(db);
340
+ services.media = new MediaService({
341
+ repository: mediaRepository,
342
+ catalogRepository: catalogRepository,
343
+ storage: config.storage,
344
+ });
345
+
346
+ services.audit = createAuditService(db);
347
+
348
+ assertServicesReady(services);
349
+
350
+ // Service method observability: wrap each service in a timing proxy
351
+ // that logs slow calls (>100ms) and failed calls with duration.
352
+ // Disabled in test environment to avoid noisy logs.
353
+ if (process.env.NODE_ENV !== "test") {
354
+ const timedLogger = {
355
+ info: (obj: Record<string, unknown>, msg: string) => logger.info(msg, obj),
356
+ error: (obj: Record<string, unknown>, msg: string) => logger.error(msg, obj),
357
+ };
358
+ const serviceKeys = Object.keys(services) as Array<keyof typeof services>;
359
+ for (const key of serviceKeys) {
360
+ const svc = services[key];
361
+ if (svc && typeof svc === "object" && key !== "email") {
362
+ (services as Record<string, unknown>)[key] = withTiming(
363
+ svc as object,
364
+ key,
365
+ timedLogger,
366
+ );
367
+ }
368
+ }
369
+ }
370
+
371
+ registerConfiguredHooks(config, hooks);
372
+
373
+ const kernel: Kernel = {
374
+ config,
375
+ hooks,
376
+ database,
377
+ services,
378
+ mcpTools,
379
+ logger,
380
+ getMCPActor() {
381
+ return {
382
+ type: "api_key",
383
+ userId: "mcp-agent",
384
+ email: null,
385
+ name: "MCP Agent",
386
+ vendorId: null,
387
+ organizationId: DEFAULT_ORG_ID,
388
+ role: "ai_agent",
389
+ permissions: config.auth?.roles?.ai_agent?.permissions ?? [
390
+ "catalog:read",
391
+ "catalog:create",
392
+ "inventory:read",
393
+ "inventory:adjust",
394
+ "orders:read",
395
+ "cart:create",
396
+ "cart:update",
397
+ "mcp:access",
398
+ ],
399
+ };
400
+ },
401
+ };
402
+
403
+ // Register plugin hooks from config.hooks flat map
404
+ for (const [key, handlers] of Object.entries(config.hooks ?? {})) {
405
+ for (const handler of handlers) {
406
+ hooks.append(key, handler as HookHandler);
407
+ }
408
+ }
409
+
410
+ // Register plugin analytics models from config.analytics.models
411
+ for (const model of config.analytics?.models ?? []) {
412
+ services.analytics.registerModel(model);
413
+ }
414
+
415
+ // Evaluate deferred plugin MCP tools.
416
+ // config.mcpTools is a chained function built by defineCommercePlugin.
417
+ // It receives the kernel and returns MCPTool[] from all plugins.
418
+ if (config.mcpTools) {
419
+ const pluginTools = config.mcpTools(kernel);
420
+ if (Array.isArray(pluginTools)) {
421
+ mcpTools.push(...pluginTools);
422
+ }
423
+ }
424
+
425
+ return kernel;
426
+ }