@unifiedcommerce/core 0.1.0 → 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.
- package/package.json +1 -2
- package/src/adapters/console-email.ts +0 -43
- package/src/auth/access.ts +0 -187
- package/src/auth/auth-schema.ts +0 -139
- package/src/auth/middleware.ts +0 -161
- package/src/auth/org.ts +0 -41
- package/src/auth/permissions.ts +0 -28
- package/src/auth/setup.ts +0 -169
- package/src/auth/system-actor.ts +0 -19
- package/src/auth/types.ts +0 -10
- package/src/config/defaults.ts +0 -82
- package/src/config/define-config.ts +0 -53
- package/src/config/types.ts +0 -299
- package/src/generated/plugin-capabilities.d.ts +0 -20
- package/src/generated/plugin-manifest.ts +0 -23
- package/src/generated/plugin-repositories.d.ts +0 -20
- package/src/hooks/checkout-completion.ts +0 -262
- package/src/hooks/checkout.ts +0 -677
- package/src/hooks/order-emails.ts +0 -62
- package/src/index.ts +0 -214
- package/src/interfaces/mcp/agent-prompt.ts +0 -174
- package/src/interfaces/mcp/context-enrichment.ts +0 -177
- package/src/interfaces/mcp/server.ts +0 -617
- package/src/interfaces/mcp/transport.ts +0 -68
- package/src/interfaces/rest/customer-portal.ts +0 -299
- package/src/interfaces/rest/index.ts +0 -74
- package/src/interfaces/rest/router.ts +0 -334
- package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
- package/src/interfaces/rest/routes/audit.ts +0 -50
- package/src/interfaces/rest/routes/carts.ts +0 -89
- package/src/interfaces/rest/routes/catalog.ts +0 -493
- package/src/interfaces/rest/routes/checkout.ts +0 -283
- package/src/interfaces/rest/routes/inventory.ts +0 -70
- package/src/interfaces/rest/routes/media.ts +0 -86
- package/src/interfaces/rest/routes/orders.ts +0 -78
- package/src/interfaces/rest/routes/payments.ts +0 -60
- package/src/interfaces/rest/routes/pricing.ts +0 -57
- package/src/interfaces/rest/routes/promotions.ts +0 -92
- package/src/interfaces/rest/routes/search.ts +0 -71
- package/src/interfaces/rest/routes/webhooks.ts +0 -46
- package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
- package/src/interfaces/rest/schemas/audit.ts +0 -46
- package/src/interfaces/rest/schemas/carts.ts +0 -125
- package/src/interfaces/rest/schemas/catalog.ts +0 -450
- package/src/interfaces/rest/schemas/checkout.ts +0 -66
- package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
- package/src/interfaces/rest/schemas/inventory.ts +0 -138
- package/src/interfaces/rest/schemas/media.ts +0 -75
- package/src/interfaces/rest/schemas/orders.ts +0 -104
- package/src/interfaces/rest/schemas/pricing.ts +0 -80
- package/src/interfaces/rest/schemas/promotions.ts +0 -110
- package/src/interfaces/rest/schemas/responses.ts +0 -85
- package/src/interfaces/rest/schemas/search.ts +0 -58
- package/src/interfaces/rest/schemas/shared.ts +0 -62
- package/src/interfaces/rest/schemas/webhooks.ts +0 -68
- package/src/interfaces/rest/utils.ts +0 -104
- package/src/interfaces/rest/webhook-router.ts +0 -50
- package/src/kernel/compensation/executor.ts +0 -61
- package/src/kernel/compensation/types.ts +0 -26
- package/src/kernel/database/adapter.ts +0 -13
- package/src/kernel/database/drizzle-db.ts +0 -56
- package/src/kernel/database/migrate.ts +0 -76
- package/src/kernel/database/plugin-types.ts +0 -34
- package/src/kernel/database/schema.ts +0 -49
- package/src/kernel/database/scoped-db.ts +0 -68
- package/src/kernel/database/tx-context.ts +0 -46
- package/src/kernel/error-mapper.ts +0 -15
- package/src/kernel/errors.ts +0 -89
- package/src/kernel/factory/repository-factory.ts +0 -242
- package/src/kernel/hooks/create-context.ts +0 -43
- package/src/kernel/hooks/executor.ts +0 -88
- package/src/kernel/hooks/registry.ts +0 -74
- package/src/kernel/hooks/types.ts +0 -52
- package/src/kernel/http-error.ts +0 -44
- package/src/kernel/jobs/adapter.ts +0 -36
- package/src/kernel/jobs/drizzle-adapter.ts +0 -58
- package/src/kernel/jobs/runner.ts +0 -153
- package/src/kernel/jobs/schema.ts +0 -46
- package/src/kernel/jobs/types.ts +0 -30
- package/src/kernel/local-api.ts +0 -185
- package/src/kernel/plugin/manifest.ts +0 -253
- package/src/kernel/query/executor.ts +0 -184
- package/src/kernel/query/registry.ts +0 -46
- package/src/kernel/result.ts +0 -33
- package/src/kernel/schema/extra-columns.ts +0 -37
- package/src/kernel/service-registry.ts +0 -76
- package/src/kernel/service-timing.ts +0 -89
- package/src/kernel/state-machine/machine.ts +0 -101
- package/src/modules/analytics/drizzle-adapter.ts +0 -426
- package/src/modules/analytics/hooks.ts +0 -11
- package/src/modules/analytics/models.ts +0 -125
- package/src/modules/analytics/repository/index.ts +0 -6
- package/src/modules/analytics/service.ts +0 -245
- package/src/modules/analytics/types.ts +0 -180
- package/src/modules/audit/hooks.ts +0 -78
- package/src/modules/audit/schema.ts +0 -33
- package/src/modules/audit/service.ts +0 -151
- package/src/modules/cart/access.ts +0 -27
- package/src/modules/cart/matcher.ts +0 -26
- package/src/modules/cart/repository/index.ts +0 -234
- package/src/modules/cart/schema.ts +0 -42
- package/src/modules/cart/schemas.ts +0 -38
- package/src/modules/cart/service.ts +0 -541
- package/src/modules/catalog/repository/index.ts +0 -772
- package/src/modules/catalog/schema.ts +0 -203
- package/src/modules/catalog/schemas.ts +0 -104
- package/src/modules/catalog/service.ts +0 -1544
- package/src/modules/customers/repository/index.ts +0 -327
- package/src/modules/customers/schema.ts +0 -64
- package/src/modules/customers/service.ts +0 -171
- package/src/modules/fulfillment/repository/index.ts +0 -426
- package/src/modules/fulfillment/schema.ts +0 -101
- package/src/modules/fulfillment/service.ts +0 -555
- package/src/modules/fulfillment/types.ts +0 -59
- package/src/modules/inventory/repository/index.ts +0 -509
- package/src/modules/inventory/schema.ts +0 -94
- package/src/modules/inventory/schemas.ts +0 -38
- package/src/modules/inventory/service.ts +0 -490
- package/src/modules/media/adapter.ts +0 -17
- package/src/modules/media/repository/index.ts +0 -274
- package/src/modules/media/schema.ts +0 -41
- package/src/modules/media/service.ts +0 -151
- package/src/modules/orders/repository/index.ts +0 -287
- package/src/modules/orders/schema.ts +0 -66
- package/src/modules/orders/service.ts +0 -619
- package/src/modules/orders/stale-order-cleanup.ts +0 -76
- package/src/modules/organization/service.ts +0 -191
- package/src/modules/payments/adapter.ts +0 -47
- package/src/modules/payments/repository/index.ts +0 -6
- package/src/modules/payments/service.ts +0 -107
- package/src/modules/pricing/repository/index.ts +0 -291
- package/src/modules/pricing/schema.ts +0 -71
- package/src/modules/pricing/schemas.ts +0 -38
- package/src/modules/pricing/service.ts +0 -494
- package/src/modules/promotions/repository/index.ts +0 -325
- package/src/modules/promotions/schema.ts +0 -62
- package/src/modules/promotions/schemas.ts +0 -38
- package/src/modules/promotions/service.ts +0 -598
- package/src/modules/search/adapter.ts +0 -57
- package/src/modules/search/hooks.ts +0 -12
- package/src/modules/search/repository/index.ts +0 -6
- package/src/modules/search/service.ts +0 -315
- package/src/modules/shipping/calculator.ts +0 -188
- package/src/modules/shipping/repository/index.ts +0 -6
- package/src/modules/shipping/service.ts +0 -51
- package/src/modules/tax/adapter.ts +0 -60
- package/src/modules/tax/repository/index.ts +0 -6
- package/src/modules/tax/service.ts +0 -53
- package/src/modules/webhooks/hook.ts +0 -34
- package/src/modules/webhooks/repository/index.ts +0 -278
- package/src/modules/webhooks/schema.ts +0 -56
- package/src/modules/webhooks/service.ts +0 -117
- package/src/modules/webhooks/signing.ts +0 -6
- package/src/modules/webhooks/ssrf-guard.ts +0 -71
- package/src/modules/webhooks/tasks.ts +0 -52
- package/src/modules/webhooks/worker.ts +0 -134
- package/src/runtime/commerce.ts +0 -145
- package/src/runtime/kernel.ts +0 -419
- package/src/runtime/logger.ts +0 -36
- package/src/runtime/server.ts +0 -349
- package/src/runtime/shutdown.ts +0 -43
- package/src/test-utils/create-pglite-adapter.ts +0 -129
- package/src/test-utils/create-plugin-test-app.ts +0 -128
- package/src/test-utils/create-repository-test-harness.ts +0 -16
- package/src/test-utils/create-test-config.ts +0 -190
- package/src/test-utils/create-test-kernel.ts +0 -7
- package/src/test-utils/create-test-plugin-context.ts +0 -75
- package/src/test-utils/rest-api-test-utils.ts +0 -265
- package/src/test-utils/test-actors.ts +0 -62
- package/src/test-utils/typed-hooks.ts +0 -54
- package/src/types/commerce-types.ts +0 -34
- package/src/utils/id.ts +0 -3
- package/src/utils/logger.ts +0 -18
- 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
|
-
}
|
package/src/runtime/commerce.ts
DELETED
|
@@ -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
|
-
}
|
package/src/runtime/kernel.ts
DELETED
|
@@ -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
|
-
}
|