@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,134 +0,0 @@
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
- }
@@ -1,145 +0,0 @@
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
- }
@@ -1,419 +0,0 @@
1
- import type {
2
- CommerceConfig,
3
- MCPResource,
4
- MCPTool,
5
- } from "../config/types.js";
6
- import { HookRegistry, type HookHandler } from "../kernel/hooks/registry.js";
7
- import {
8
- createDatabaseConnection,
9
- type DatabaseAdapter,
10
- } from "../kernel/database/adapter.js";
11
- import type { PluginDb } from "../kernel/database/plugin-types.js";
12
-
13
- import { CatalogServiceImpl } from "../modules/catalog/service.js";
14
- import { CatalogRepository } from "../modules/catalog/repository/index.js";
15
- import { InventoryRepository } from "../modules/inventory/repository/index.js";
16
- import { CartRepository } from "../modules/cart/repository/index.js";
17
- import { OrdersRepository } from "../modules/orders/repository/index.js";
18
- import { CustomersRepository } from "../modules/customers/repository/index.js";
19
- import { PricingRepository } from "../modules/pricing/repository/index.js";
20
- import { PromotionsRepository } from "../modules/promotions/repository/index.js";
21
- import { FulfillmentRepository } from "../modules/fulfillment/repository/index.js";
22
- import { WebhooksRepository } from "../modules/webhooks/repository/index.js";
23
- import { MediaRepository } from "../modules/media/repository/index.js";
24
- import type { DrizzleDatabase } from "../kernel/database/drizzle-db.js";
25
- import { InventoryService } from "../modules/inventory/service.js";
26
- import { MediaService } from "../modules/media/service.js";
27
- import { CartService } from "../modules/cart/service.js";
28
- import { OrderService } from "../modules/orders/service.js";
29
- import { PaymentsService } from "../modules/payments/service.js";
30
- import { FulfillmentService } from "../modules/fulfillment/service.js";
31
- import { CustomerService } from "../modules/customers/service.js";
32
- import { WebhookService } from "../modules/webhooks/service.js";
33
- import { AnalyticsService } from "../modules/analytics/service.js";
34
- import { DrizzleAnalyticsAdapter } from "../modules/analytics/drizzle-adapter.js";
35
- import { BUILTIN_ANALYTICS_MODELS } from "../modules/analytics/models.js";
36
- import { PricingService } from "../modules/pricing/service.js";
37
- import { PromotionService } from "../modules/promotions/service.js";
38
- import { TaxService } from "../modules/tax/service.js";
39
- import { ShippingService } from "../modules/shipping/service.js";
40
- import { SearchService } from "../modules/search/service.js";
41
- import { WebhookDeliveryWorker } from "../modules/webhooks/worker.js";
42
- import {
43
- createAuditService,
44
- type AuditService,
45
- } from "../modules/audit/service.js";
46
- import { OrganizationService } from "../modules/organization/service.js";
47
- import { createLogger } from "../utils/logger.js";
48
- import { withTiming } from "../kernel/service-timing.js";
49
- import { extendOrderStateMachine } from "../kernel/state-machine/machine.js";
50
- import { DEFAULT_ORG_ID } from "../auth/org.js";
51
- import { deliverWebhooks } from "../modules/webhooks/hook.js";
52
- // Analytics event recording hooks removed (RFC-006): source tables ARE the events.
53
- // The DrizzleAnalyticsAdapter queries orders/inventory directly via SQL.
54
- import { syncToSearchIndex } from "../modules/search/hooks.js";
55
- import { auditHooks } from "../modules/audit/hooks.js";
56
- import { sendOrderStatusEmail } from "../hooks/order-emails.js";
57
- import { DrizzleJobsAdapter } from "../kernel/jobs/drizzle-adapter.js";
58
-
59
- export interface WebhookDeliveryPayload {
60
- endpoint: { id: string; url: string; secret: string };
61
- eventName: string;
62
- payload: unknown;
63
- }
64
-
65
- export interface Kernel {
66
- config: CommerceConfig;
67
- hooks: HookRegistry;
68
- database: DatabaseAdapter;
69
- services: {
70
- catalog: CatalogServiceImpl;
71
- inventory: InventoryService;
72
- media: MediaService;
73
- cart: CartService;
74
- orders: OrderService;
75
- payments: PaymentsService;
76
- fulfillment: FulfillmentService;
77
- customers: CustomerService;
78
- webhooks: WebhookService & {
79
- enqueueDelivery(payload: WebhookDeliveryPayload): Promise<void>;
80
- };
81
- analytics: AnalyticsService;
82
- pricing: PricingService;
83
- promotions: PromotionService;
84
- tax: TaxService;
85
- shipping: ShippingService;
86
- search: SearchService;
87
- audit: AuditService;
88
- email: CommerceConfig["email"];
89
- organization: OrganizationService;
90
- };
91
- mcpTools: MCPTool[];
92
- mcpResources: MCPResource[];
93
- logger: ReturnType<typeof createLogger>;
94
- getMCPActor(): {
95
- type: "api_key";
96
- userId: string;
97
- email: null;
98
- name: string;
99
- vendorId: null;
100
- organizationId: string;
101
- role: string;
102
- permissions: string[];
103
- };
104
- }
105
-
106
- const requiredServiceKeys = [
107
- "catalog",
108
- "inventory",
109
- "media",
110
- "cart",
111
- "orders",
112
- "payments",
113
- "fulfillment",
114
- "customers",
115
- "webhooks",
116
- "analytics",
117
- "pricing",
118
- "promotions",
119
- "tax",
120
- "shipping",
121
- "search",
122
- "audit",
123
- ] as const satisfies Array<keyof Kernel["services"]>;
124
-
125
- function assertServicesReady(
126
- services: Partial<Kernel["services"]>,
127
- ): asserts services is Kernel["services"] {
128
- for (const key of requiredServiceKeys) {
129
- if (services[key] === undefined) {
130
- throw new Error(`Kernel service "${String(key)}" was not initialized.`);
131
- }
132
- }
133
- }
134
-
135
- function registerConfiguredHooks(
136
- config: CommerceConfig,
137
- hooks: HookRegistry,
138
- ): void {
139
- for (const [entityType, entityConfig] of Object.entries(
140
- config.entities ?? {},
141
- )) {
142
- const entityHooks = entityConfig.hooks ?? {};
143
- for (const [hookName, handlers] of Object.entries(entityHooks)) {
144
- hooks.registerConfigHooks(
145
- `catalog.${entityType}.${hookName}`,
146
- handlers ?? [],
147
- );
148
- }
149
- }
150
-
151
- for (const [moduleName, moduleConfig] of [
152
- ["cart", config.cart],
153
- ["checkout", config.checkout],
154
- ["orders", config.orders],
155
- ["inventory", config.inventory],
156
- ] as const) {
157
- const hooksObject = moduleConfig?.hooks;
158
- if (!hooksObject) continue;
159
- for (const [hookName, handlers] of Object.entries(hooksObject)) {
160
- const normalizedHandlers: HookHandler[] = Array.isArray(handlers)
161
- ? (handlers as unknown as HookHandler[])
162
- : [];
163
- hooks.registerConfigHooks(
164
- `${moduleName}.${hookName}`,
165
- normalizedHandlers,
166
- );
167
- }
168
- }
169
-
170
- // Webhook delivery (async via job queue) — 14 event types
171
- hooks.append("orders.afterCreate", deliverWebhooks);
172
- hooks.append("orders.afterStatusChange", deliverWebhooks);
173
- hooks.append("orders.afterStatusChange", sendOrderStatusEmail as (...args: unknown[]) => unknown);
174
- hooks.append("catalog.afterCreate", deliverWebhooks);
175
- hooks.append("catalog.afterUpdate", deliverWebhooks);
176
- hooks.append("catalog.afterDelete", deliverWebhooks);
177
- hooks.append("inventory.afterAdjust", deliverWebhooks);
178
- hooks.append("customers.afterCreate", deliverWebhooks);
179
- hooks.append("customers.afterUpdate", deliverWebhooks);
180
- hooks.append("pricing.afterCreate", deliverWebhooks);
181
- hooks.append("pricing.afterUpdate", deliverWebhooks);
182
- hooks.append("promotions.afterCreate", deliverWebhooks);
183
- hooks.append("promotions.afterUpdate", deliverWebhooks);
184
- hooks.append("fulfillment.afterCreate", deliverWebhooks);
185
- hooks.append("cart.afterAddItem", deliverWebhooks);
186
-
187
- // Analytics: no event recording hooks needed (RFC-006).
188
- // The DrizzleAnalyticsAdapter queries source tables directly via SQL.
189
-
190
- // Search index sync
191
- hooks.append("catalog.afterCreate", syncToSearchIndex);
192
- hooks.append("catalog.afterUpdate", syncToSearchIndex);
193
-
194
- // Auto-audit — records every create/update/delete across all modules
195
- for (const [key, handler] of Object.entries(auditHooks)) {
196
- hooks.append(key, handler);
197
- }
198
- }
199
-
200
- export function createKernel(config: CommerceConfig): Kernel {
201
- const hooks = new HookRegistry();
202
- const logger = createLogger("kernel");
203
- hooks.setLogger(logger as unknown as { error: (obj: Record<string, unknown>, msg: string) => void });
204
-
205
- if (!config.storage) {
206
- throw new Error(
207
- "Storage adapter is required. Configure `storage` in defineConfig (for example: localStorageAdapter for development, or s3StorageAdapter/r2StorageAdapter for object storage).",
208
- );
209
- }
210
-
211
- const database = createDatabaseConnection({
212
- adapter: config.databaseAdapter ?? {
213
- provider: config.database.provider,
214
- db: {},
215
- async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> {
216
- return fn({});
217
- },
218
- },
219
- });
220
- const mcpTools: MCPTool[] = [];
221
- const mcpResources: MCPResource[] = [];
222
-
223
- const services: Partial<Kernel["services"]> = {
224
- email: config.email,
225
- organization: new OrganizationService(database.db),
226
- };
227
-
228
- const serviceContainer = services as Record<string, unknown>;
229
-
230
- // Expose database on service container so plugin hooks can access it
231
- serviceContainer.database = database;
232
-
233
- const db = database.db as DrizzleDatabase;
234
-
235
- // Expose jobs adapter on service container so plugins can enqueue background work
236
- const jobsAdapter = new DrizzleJobsAdapter(db, new Map());
237
- serviceContainer.jobs = jobsAdapter;
238
-
239
- const pricingRepository = new PricingRepository(db);
240
- const promotionsRepository = new PromotionsRepository(db);
241
-
242
- services.tax = new TaxService({ adapter: config.tax?.adapter });
243
- services.payments = new PaymentsService(config.payments);
244
-
245
- const customersRepository = new CustomersRepository(db);
246
- services.customers = new CustomerService({
247
- repository: customersRepository,
248
- });
249
-
250
- const webhooksRepository = new WebhooksRepository(db);
251
- const webhookWorker = new WebhookDeliveryWorker({
252
- repository: webhooksRepository,
253
- });
254
- services.webhooks = Object.assign(
255
- new WebhookService({
256
- repository: webhooksRepository,
257
- }),
258
- {
259
- async enqueueDelivery(payload: WebhookDeliveryPayload) {
260
- await webhookWorker.deliver(payload);
261
- },
262
- },
263
- );
264
-
265
- const inventoryRepository = new InventoryRepository(db);
266
- services.inventory = new InventoryService({
267
- repository: inventoryRepository,
268
- hooks,
269
- config,
270
- services: serviceContainer,
271
- database,
272
- });
273
-
274
- const catalogRepository = new CatalogRepository(db);
275
- services.catalog = new CatalogServiceImpl({
276
- repository: catalogRepository,
277
- hooks,
278
- config,
279
- services: serviceContainer,
280
- });
281
-
282
- services.search = new SearchService({
283
- catalogRepository: catalogRepository,
284
- ...(config.search?.adapter ? { adapter: config.search.adapter } : {}),
285
- ...(config.search?.defaultFacets
286
- ? { defaultFacets: config.search.defaultFacets }
287
- : {}),
288
- });
289
-
290
- const cartRepository = new CartRepository(db);
291
- services.cart = new CartService({
292
- repository: cartRepository,
293
- catalogRepository: catalogRepository,
294
- hooks,
295
- config,
296
- services: serviceContainer,
297
- });
298
-
299
- const ordersRepository = new OrdersRepository(db);
300
- services.orders = new OrderService({
301
- repository: ordersRepository,
302
- hooks,
303
- services: serviceContainer,
304
- kernel: { database: database as { db: PluginDb } },
305
- ...(config.orders?.customTransitions
306
- ? { stateMachine: extendOrderStateMachine(config.orders.customTransitions) }
307
- : {}),
308
- });
309
-
310
- const fulfillmentRepository = new FulfillmentRepository(db);
311
- services.fulfillment = new FulfillmentService({
312
- repository: fulfillmentRepository,
313
- ordersRepository: ordersRepository,
314
- inventoryService: services.inventory,
315
- });
316
-
317
- services.pricing = new PricingService({
318
- repository: pricingRepository,
319
- catalogRepository: catalogRepository,
320
- });
321
-
322
- services.promotions = new PromotionService({
323
- repository: promotionsRepository,
324
- catalogRepository: catalogRepository,
325
- ordersRepository: ordersRepository,
326
- });
327
-
328
- // AnalyticsService — always uses DrizzleAnalyticsAdapter (direct SQL).
329
- // Plugins add their own models via the analyticsModels manifest slot.
330
- const analyticsAdapter = new DrizzleAnalyticsAdapter(db);
331
- for (const model of BUILTIN_ANALYTICS_MODELS) {
332
- analyticsAdapter.registerModel(model);
333
- }
334
- services.analytics = new AnalyticsService({
335
- adapter: analyticsAdapter,
336
- config,
337
- });
338
-
339
- services.shipping = new ShippingService({
340
- config,
341
- catalogRepository: catalogRepository,
342
- });
343
-
344
- const mediaRepository = new MediaRepository(db);
345
- services.media = new MediaService({
346
- repository: mediaRepository,
347
- catalogRepository: catalogRepository,
348
- storage: config.storage,
349
- });
350
-
351
- services.audit = createAuditService(db);
352
-
353
- assertServicesReady(services);
354
-
355
- // Service method observability: wrap each service in a timing proxy
356
- // that logs slow calls (>100ms) and failed calls with duration.
357
- // Disabled in test environment to avoid noisy logs.
358
- if (process.env.NODE_ENV !== "test") {
359
- const timedLogger = logger as unknown as { info: (o: Record<string, unknown>, m: string) => void; error: (o: Record<string, unknown>, m: string) => void };
360
- const serviceKeys = Object.keys(services) as Array<keyof typeof services>;
361
- for (const key of serviceKeys) {
362
- const svc = services[key];
363
- if (svc && typeof svc === "object" && key !== "email") {
364
- (services as Record<string, unknown>)[key] = withTiming(
365
- svc as object,
366
- key,
367
- timedLogger,
368
- );
369
- }
370
- }
371
- }
372
-
373
- registerConfiguredHooks(config, hooks);
374
-
375
- const kernel: Kernel = {
376
- config,
377
- hooks,
378
- database,
379
- services,
380
- mcpTools,
381
- mcpResources,
382
- logger,
383
- getMCPActor() {
384
- return {
385
- type: "api_key",
386
- userId: "mcp-agent",
387
- email: null,
388
- name: "MCP Agent",
389
- vendorId: null,
390
- organizationId: DEFAULT_ORG_ID,
391
- role: "ai_agent",
392
- permissions: config.auth?.roles?.ai_agent?.permissions ?? [
393
- "catalog:read",
394
- "catalog:create",
395
- "inventory:read",
396
- "inventory:adjust",
397
- "orders:read",
398
- "cart:create",
399
- "cart:update",
400
- "mcp:access",
401
- ],
402
- };
403
- },
404
- };
405
-
406
- // Register plugin hooks from config.hooks flat map
407
- for (const [key, handlers] of Object.entries(config.hooks ?? {})) {
408
- for (const handler of handlers) {
409
- hooks.append(key, handler as HookHandler);
410
- }
411
- }
412
-
413
- // Register plugin analytics models from config.analytics.models
414
- for (const model of config.analytics?.models ?? []) {
415
- services.analytics.registerModel(model);
416
- }
417
-
418
- return kernel;
419
- }