@unifiedcommerce/core 0.2.0 → 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 (186) hide show
  1. package/package.json +2 -1
  2. package/src/adapters/console-email.ts +43 -0
  3. package/src/auth/access.ts +187 -0
  4. package/src/auth/auth-schema.ts +139 -0
  5. package/src/auth/middleware.ts +161 -0
  6. package/src/auth/org.ts +41 -0
  7. package/src/auth/permissions.ts +28 -0
  8. package/src/auth/setup.ts +171 -0
  9. package/src/auth/system-actor.ts +19 -0
  10. package/src/auth/types.ts +10 -0
  11. package/src/config/defaults.ts +82 -0
  12. package/src/config/define-config.ts +53 -0
  13. package/src/config/types.ts +301 -0
  14. package/src/generated/plugin-capabilities.d.ts +20 -0
  15. package/src/generated/plugin-manifest.ts +23 -0
  16. package/src/generated/plugin-repositories.d.ts +20 -0
  17. package/src/hooks/checkout-completion.ts +262 -0
  18. package/src/hooks/checkout.ts +677 -0
  19. package/src/hooks/order-emails.ts +62 -0
  20. package/src/index.ts +215 -0
  21. package/src/interfaces/mcp/agent-prompt.ts +174 -0
  22. package/src/interfaces/mcp/context-enrichment.ts +177 -0
  23. package/src/interfaces/mcp/server.ts +47 -0
  24. package/src/interfaces/mcp/tool-builder.ts +261 -0
  25. package/src/interfaces/mcp/tools/analytics.ts +76 -0
  26. package/src/interfaces/mcp/tools/cart.ts +57 -0
  27. package/src/interfaces/mcp/tools/catalog.ts +299 -0
  28. package/src/interfaces/mcp/tools/index.ts +22 -0
  29. package/src/interfaces/mcp/tools/inventory.ts +161 -0
  30. package/src/interfaces/mcp/tools/orders.ts +104 -0
  31. package/src/interfaces/mcp/tools/pricing.ts +94 -0
  32. package/src/interfaces/mcp/tools/promotions.ts +106 -0
  33. package/src/interfaces/mcp/tools/registry.ts +101 -0
  34. package/src/interfaces/mcp/tools/search.ts +42 -0
  35. package/src/interfaces/mcp/tools/webhooks.ts +48 -0
  36. package/src/interfaces/mcp/transport.ts +128 -0
  37. package/src/interfaces/rest/customer-portal.ts +299 -0
  38. package/src/interfaces/rest/index.ts +74 -0
  39. package/src/interfaces/rest/router.ts +333 -0
  40. package/src/interfaces/rest/routes/admin-jobs.ts +58 -0
  41. package/src/interfaces/rest/routes/audit.ts +50 -0
  42. package/src/interfaces/rest/routes/carts.ts +89 -0
  43. package/src/interfaces/rest/routes/catalog.ts +493 -0
  44. package/src/interfaces/rest/routes/checkout.ts +284 -0
  45. package/src/interfaces/rest/routes/inventory.ts +70 -0
  46. package/src/interfaces/rest/routes/media.ts +86 -0
  47. package/src/interfaces/rest/routes/orders.ts +78 -0
  48. package/src/interfaces/rest/routes/payments.ts +60 -0
  49. package/src/interfaces/rest/routes/pricing.ts +57 -0
  50. package/src/interfaces/rest/routes/promotions.ts +93 -0
  51. package/src/interfaces/rest/routes/search.ts +71 -0
  52. package/src/interfaces/rest/routes/webhooks.ts +46 -0
  53. package/src/interfaces/rest/schemas/admin-jobs.ts +40 -0
  54. package/src/interfaces/rest/schemas/audit.ts +46 -0
  55. package/src/interfaces/rest/schemas/carts.ts +125 -0
  56. package/src/interfaces/rest/schemas/catalog.ts +450 -0
  57. package/src/interfaces/rest/schemas/checkout.ts +66 -0
  58. package/src/interfaces/rest/schemas/customer-portal.ts +195 -0
  59. package/src/interfaces/rest/schemas/inventory.ts +138 -0
  60. package/src/interfaces/rest/schemas/media.ts +75 -0
  61. package/src/interfaces/rest/schemas/orders.ts +104 -0
  62. package/src/interfaces/rest/schemas/pricing.ts +80 -0
  63. package/src/interfaces/rest/schemas/promotions.ts +110 -0
  64. package/src/interfaces/rest/schemas/responses.ts +85 -0
  65. package/src/interfaces/rest/schemas/search.ts +58 -0
  66. package/src/interfaces/rest/schemas/shared.ts +62 -0
  67. package/src/interfaces/rest/schemas/webhooks.ts +68 -0
  68. package/src/interfaces/rest/utils.ts +104 -0
  69. package/src/interfaces/rest/webhook-router.ts +50 -0
  70. package/src/kernel/compensation/executor.ts +61 -0
  71. package/src/kernel/compensation/types.ts +26 -0
  72. package/src/kernel/database/adapter.ts +21 -0
  73. package/src/kernel/database/drizzle-db.ts +56 -0
  74. package/src/kernel/database/migrate.ts +76 -0
  75. package/src/kernel/database/plugin-types.ts +34 -0
  76. package/src/kernel/database/schema.ts +49 -0
  77. package/src/kernel/database/scoped-db.ts +68 -0
  78. package/src/kernel/database/tx-context.ts +46 -0
  79. package/src/kernel/error-mapper.ts +15 -0
  80. package/src/kernel/errors.ts +89 -0
  81. package/src/kernel/factory/repository-factory.ts +244 -0
  82. package/src/kernel/hooks/create-context.ts +43 -0
  83. package/src/kernel/hooks/executor.ts +88 -0
  84. package/src/kernel/hooks/registry.ts +74 -0
  85. package/src/kernel/hooks/types.ts +52 -0
  86. package/src/kernel/http-error.ts +44 -0
  87. package/src/kernel/jobs/adapter.ts +36 -0
  88. package/src/kernel/jobs/drizzle-adapter.ts +58 -0
  89. package/src/kernel/jobs/runner.ts +153 -0
  90. package/src/kernel/jobs/schema.ts +46 -0
  91. package/src/kernel/jobs/types.ts +30 -0
  92. package/src/kernel/local-api.ts +187 -0
  93. package/src/kernel/plugin/manifest.ts +271 -0
  94. package/src/kernel/query/executor.ts +184 -0
  95. package/src/kernel/query/registry.ts +46 -0
  96. package/src/kernel/result.ts +33 -0
  97. package/src/kernel/schema/extra-columns.ts +37 -0
  98. package/src/kernel/service-registry.ts +76 -0
  99. package/src/kernel/service-timing.ts +89 -0
  100. package/src/kernel/state-machine/machine.ts +101 -0
  101. package/src/modules/analytics/drizzle-adapter.ts +426 -0
  102. package/src/modules/analytics/hooks.ts +11 -0
  103. package/src/modules/analytics/models.ts +125 -0
  104. package/src/modules/analytics/repository/index.ts +6 -0
  105. package/src/modules/analytics/service.ts +245 -0
  106. package/src/modules/analytics/types.ts +180 -0
  107. package/src/modules/audit/hooks.ts +78 -0
  108. package/src/modules/audit/schema.ts +33 -0
  109. package/src/modules/audit/service.ts +151 -0
  110. package/src/modules/cart/access.ts +27 -0
  111. package/src/modules/cart/matcher.ts +26 -0
  112. package/src/modules/cart/repository/index.ts +234 -0
  113. package/src/modules/cart/schema.ts +42 -0
  114. package/src/modules/cart/schemas.ts +38 -0
  115. package/src/modules/cart/service.ts +541 -0
  116. package/src/modules/catalog/repository/index.ts +772 -0
  117. package/src/modules/catalog/schema.ts +203 -0
  118. package/src/modules/catalog/schemas.ts +104 -0
  119. package/src/modules/catalog/service.ts +1544 -0
  120. package/src/modules/customers/repository/index.ts +327 -0
  121. package/src/modules/customers/schema.ts +64 -0
  122. package/src/modules/customers/service.ts +171 -0
  123. package/src/modules/fulfillment/repository/index.ts +426 -0
  124. package/src/modules/fulfillment/schema.ts +101 -0
  125. package/src/modules/fulfillment/service.ts +555 -0
  126. package/src/modules/fulfillment/types.ts +59 -0
  127. package/src/modules/inventory/repository/index.ts +509 -0
  128. package/src/modules/inventory/schema.ts +94 -0
  129. package/src/modules/inventory/schemas.ts +38 -0
  130. package/src/modules/inventory/service.ts +490 -0
  131. package/src/modules/media/adapter.ts +17 -0
  132. package/src/modules/media/repository/index.ts +274 -0
  133. package/src/modules/media/schema.ts +41 -0
  134. package/src/modules/media/service.ts +151 -0
  135. package/src/modules/orders/repository/index.ts +287 -0
  136. package/src/modules/orders/schema.ts +66 -0
  137. package/src/modules/orders/service.ts +619 -0
  138. package/src/modules/orders/stale-order-cleanup.ts +76 -0
  139. package/src/modules/organization/service.ts +191 -0
  140. package/src/modules/payments/adapter.ts +47 -0
  141. package/src/modules/payments/repository/index.ts +6 -0
  142. package/src/modules/payments/service.ts +107 -0
  143. package/src/modules/pricing/repository/index.ts +291 -0
  144. package/src/modules/pricing/schema.ts +71 -0
  145. package/src/modules/pricing/schemas.ts +38 -0
  146. package/src/modules/pricing/service.ts +494 -0
  147. package/src/modules/promotions/repository/index.ts +325 -0
  148. package/src/modules/promotions/schema.ts +62 -0
  149. package/src/modules/promotions/schemas.ts +38 -0
  150. package/src/modules/promotions/service.ts +598 -0
  151. package/src/modules/search/adapter.ts +57 -0
  152. package/src/modules/search/hooks.ts +12 -0
  153. package/src/modules/search/repository/index.ts +6 -0
  154. package/src/modules/search/service.ts +315 -0
  155. package/src/modules/shipping/calculator.ts +188 -0
  156. package/src/modules/shipping/repository/index.ts +6 -0
  157. package/src/modules/shipping/service.ts +51 -0
  158. package/src/modules/tax/adapter.ts +60 -0
  159. package/src/modules/tax/repository/index.ts +6 -0
  160. package/src/modules/tax/service.ts +53 -0
  161. package/src/modules/webhooks/hook.ts +34 -0
  162. package/src/modules/webhooks/repository/index.ts +278 -0
  163. package/src/modules/webhooks/schema.ts +56 -0
  164. package/src/modules/webhooks/service.ts +117 -0
  165. package/src/modules/webhooks/signing.ts +6 -0
  166. package/src/modules/webhooks/ssrf-guard.ts +71 -0
  167. package/src/modules/webhooks/tasks.ts +52 -0
  168. package/src/modules/webhooks/worker.ts +134 -0
  169. package/src/runtime/commerce.ts +145 -0
  170. package/src/runtime/kernel.ts +426 -0
  171. package/src/runtime/logger.ts +36 -0
  172. package/src/runtime/server.ts +355 -0
  173. package/src/runtime/shutdown.ts +43 -0
  174. package/src/test-utils/create-pglite-adapter.ts +129 -0
  175. package/src/test-utils/create-plugin-test-app.ts +128 -0
  176. package/src/test-utils/create-repository-test-harness.ts +16 -0
  177. package/src/test-utils/create-test-config.ts +190 -0
  178. package/src/test-utils/create-test-kernel.ts +7 -0
  179. package/src/test-utils/create-test-plugin-context.ts +75 -0
  180. package/src/test-utils/rest-api-test-utils.ts +265 -0
  181. package/src/test-utils/test-actors.ts +62 -0
  182. package/src/test-utils/typed-hooks.ts +54 -0
  183. package/src/types/commerce-types.ts +34 -0
  184. package/src/utils/id.ts +3 -0
  185. package/src/utils/logger.ts +18 -0
  186. package/src/utils/pagination.ts +22 -0
@@ -0,0 +1,16 @@
1
+ import type { CommerceConfig } from "../config/types.js";
2
+ import { createTestConfig } from "./create-test-config.js";
3
+ import { createKernel } from "../runtime/kernel.js";
4
+
5
+ export interface RepositoryTestHarness {
6
+ config: CommerceConfig;
7
+ kernel: ReturnType<typeof createKernel>;
8
+ }
9
+
10
+ export async function createRepositoryTestHarness(
11
+ overrides: Partial<CommerceConfig> = {},
12
+ ): Promise<RepositoryTestHarness> {
13
+ const config = await createTestConfig(overrides);
14
+ const kernel = createKernel(config);
15
+ return { config, kernel };
16
+ }
@@ -0,0 +1,190 @@
1
+ import { defineConfig } from "../config/define-config.js";
2
+ import type { CommerceConfig } from "../config/types.js";
3
+ import { Ok } from "../kernel/result.js";
4
+ import type { StorageAdapter } from "../modules/media/adapter.js";
5
+
6
+ function createInMemoryStorageAdapter(): StorageAdapter {
7
+ const files = new Map<string, { data: ArrayBuffer; contentType: string }>();
8
+ const baseUrl = "http://localhost:3000/test-assets";
9
+
10
+ return {
11
+ providerId: "test-memory-storage",
12
+ async upload(key, data, contentType) {
13
+ const body =
14
+ data instanceof ArrayBuffer
15
+ ? data
16
+ : await new Response(data).arrayBuffer();
17
+ files.set(key, { data: body, contentType });
18
+ return Ok({
19
+ key,
20
+ url: `${baseUrl}/${key}`,
21
+ contentType,
22
+ size: body.byteLength,
23
+ });
24
+ },
25
+ async getUrl(key) {
26
+ return Ok(`${baseUrl}/${key}`);
27
+ },
28
+ async getSignedUrl(key, expiresIn) {
29
+ return Ok(`${baseUrl}/${key}?expiresIn=${expiresIn}`);
30
+ },
31
+ async delete(key) {
32
+ files.delete(key);
33
+ return Ok(undefined);
34
+ },
35
+ async list(prefix) {
36
+ return Ok(
37
+ Array.from(files.entries())
38
+ .filter(([key]) => key.startsWith(prefix))
39
+ .map(([key, file]) => ({
40
+ key,
41
+ url: `${baseUrl}/${key}`,
42
+ contentType: file.contentType,
43
+ size: file.data.byteLength,
44
+ })),
45
+ );
46
+ },
47
+ };
48
+ }
49
+
50
+ export async function createTestConfig(
51
+ overrides: Partial<CommerceConfig> = {},
52
+ ): Promise<CommerceConfig> {
53
+ // Auto-provision PGlite when no databaseAdapter is provided
54
+ if (!overrides.databaseAdapter) {
55
+ const { createPGliteTestAdapter } = await import("./create-pglite-adapter.js");
56
+ const { adapter } = await createPGliteTestAdapter();
57
+ overrides = { ...overrides, databaseAdapter: adapter };
58
+ }
59
+
60
+ return defineConfig({
61
+ version: "0.0.1-test",
62
+ storeName: "Test Store",
63
+ database: {
64
+ provider: "postgresql",
65
+ },
66
+ auth: {
67
+ requireEmailVerification: false,
68
+ apiKeys: { enabled: true, defaultPermissions: ["catalog:read"] },
69
+ enableDevKey: true,
70
+ devKey: "dev-staff-key",
71
+ posPin: { enabled: true },
72
+ roles: {
73
+ owner: { permissions: ["*:*"] },
74
+ admin: { permissions: ["*:*"] },
75
+ staff: {
76
+ permissions: [
77
+ "catalog:create",
78
+ "catalog:update",
79
+ "catalog:delete",
80
+ "catalog:read",
81
+ "inventory:adjust",
82
+ "orders:create",
83
+ "orders:read",
84
+ "orders:update",
85
+ "cart:create",
86
+ "cart:update",
87
+ "customers:update:self",
88
+ ],
89
+ },
90
+ ai_agent: {
91
+ permissions: [
92
+ "catalog:read",
93
+ "catalog:create",
94
+ "inventory:read",
95
+ "inventory:adjust",
96
+ "orders:read",
97
+ "cart:create",
98
+ "cart:update",
99
+ "mcp:access",
100
+ ],
101
+ },
102
+ },
103
+ customerPermissions: [
104
+ "catalog:read",
105
+ "cart:create",
106
+ "cart:read",
107
+ "cart:update",
108
+ "orders:create",
109
+ "orders:read:own",
110
+ "customers:read:self",
111
+ "customers:update:self",
112
+ ],
113
+ },
114
+ entities: {
115
+ product: {
116
+ fields: [
117
+ { name: "weight", type: "number" },
118
+ { name: "brand", type: "text" },
119
+ ],
120
+ variants: { enabled: true, optionTypes: ["size", "color"] },
121
+ fulfillment: "physical",
122
+ },
123
+ digitalDownload: {
124
+ fields: [{ name: "fileAssetId", type: "text" }],
125
+ variants: { enabled: false },
126
+ fulfillment: "digital-download",
127
+ },
128
+ course: {
129
+ fields: [{ name: "modules", type: "json" }],
130
+ variants: { enabled: false },
131
+ fulfillment: "digital-access",
132
+ },
133
+ },
134
+ cart: {
135
+ ttlMinutes: 5,
136
+ hooks: {},
137
+ },
138
+ checkout: {
139
+ hooks: {
140
+ beforeCreate: [],
141
+ afterCreate: [],
142
+ },
143
+ },
144
+ orders: {
145
+ hooks: {
146
+ beforeCreate: [],
147
+ afterCreate: [],
148
+ beforeStatusChange: [],
149
+ afterStatusChange: [],
150
+ },
151
+ },
152
+ inventory: {
153
+ hooks: {
154
+ afterAdjust: [],
155
+ },
156
+ },
157
+ email: {
158
+ async send() {
159
+ // no-op for tests
160
+ },
161
+ },
162
+ storage: createInMemoryStorageAdapter(),
163
+ ...overrides,
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Creates a test config backed by PGlite (in-memory PostgreSQL).
169
+ *
170
+ * This provides production parity for tests by using real SQL execution
171
+ * and PostgreSQL behavior while remaining fast and self-contained.
172
+ *
173
+ * @param overrides - Optional config overrides
174
+ * @returns A promise resolving to an object containing:
175
+ * - config: The CommerceConfig to pass to createKernel
176
+ * - cleanup: Async function to reset data between tests
177
+ */
178
+ export async function createPGliteTestConfig(
179
+ overrides: Partial<CommerceConfig> = {},
180
+ ): Promise<{ config: CommerceConfig; cleanup: () => Promise<void> }> {
181
+ const { createPGliteTestAdapter } = await import("./create-pglite-adapter.js");
182
+ const { adapter, cleanup } = await createPGliteTestAdapter();
183
+
184
+ const config = await createTestConfig({
185
+ databaseAdapter: adapter,
186
+ ...overrides,
187
+ });
188
+
189
+ return { config, cleanup };
190
+ }
@@ -0,0 +1,7 @@
1
+ import type { CommerceConfig } from "../config/types.js";
2
+ import { createKernel } from "../runtime/kernel.js";
3
+ import { createTestConfig } from "./create-test-config.js";
4
+
5
+ export async function createTestKernel(overrides: Partial<CommerceConfig> = {}) {
6
+ return createKernel(await createTestConfig(overrides));
7
+ }
@@ -0,0 +1,75 @@
1
+ import { HookRegistry } from "../kernel/hooks/registry.js";
2
+ import { createLogger } from "../utils/logger.js";
3
+ import { createTestConfig } from "./create-test-config.js";
4
+ import type { CommerceConfig, MCPTool } from "../config/types.js";
5
+
6
+ interface PluginContextShape {
7
+ hooks: HookRegistry;
8
+ config: CommerceConfig;
9
+ services: Record<string, unknown>;
10
+ routes: { add(method: string, path: string, handler: (...args: unknown[]) => unknown): void };
11
+ mcp: { registerTool(tool: MCPTool): void };
12
+ analytics: { registerModel(model: unknown): void };
13
+ database: {
14
+ registerSchema(schema: Record<string, unknown>): void;
15
+ query: unknown;
16
+ transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T>;
17
+ };
18
+ logger: { info(message: string, data?: unknown): void; warn(message: string, data?: unknown): void; error(message: string, data?: unknown): void };
19
+ }
20
+
21
+ export interface TestPluginContext extends PluginContextShape {
22
+ registeredRoutes: Array<{ method: string; path: string; handler: (...args: unknown[]) => unknown }>;
23
+ registeredMCPTools: MCPTool[];
24
+ registeredAnalyticsModels: unknown[];
25
+ registeredSchemas: Array<Record<string, unknown>>;
26
+ }
27
+
28
+ export async function createTestPluginContext(options?: {
29
+ config?: Partial<CommerceConfig>;
30
+ services?: Record<string, unknown>;
31
+ }): Promise<TestPluginContext> {
32
+ const hooks = new HookRegistry();
33
+ const config = await createTestConfig(options?.config ?? {});
34
+ const services = options?.services ?? {};
35
+
36
+ const registeredRoutes: TestPluginContext["registeredRoutes"] = [];
37
+ const registeredMCPTools: MCPTool[] = [];
38
+ const registeredAnalyticsModels: unknown[] = [];
39
+ const registeredSchemas: Array<Record<string, unknown>> = [];
40
+
41
+ return {
42
+ hooks,
43
+ config,
44
+ services,
45
+ routes: {
46
+ add(method, path, handler) {
47
+ registeredRoutes.push({ method: method.toUpperCase(), path, handler });
48
+ },
49
+ },
50
+ mcp: {
51
+ registerTool(tool) {
52
+ registeredMCPTools.push(tool);
53
+ },
54
+ },
55
+ analytics: {
56
+ registerModel(model) {
57
+ registeredAnalyticsModels.push(model);
58
+ },
59
+ },
60
+ database: {
61
+ registerSchema(schema) {
62
+ registeredSchemas.push(schema);
63
+ },
64
+ query: {},
65
+ async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> {
66
+ return fn({});
67
+ },
68
+ },
69
+ logger: createLogger("test-plugin-context"),
70
+ registeredRoutes,
71
+ registeredMCPTools,
72
+ registeredAnalyticsModels,
73
+ registeredSchemas,
74
+ };
75
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Integration test utilities for REST API endpoints.
3
+ *
4
+ * Provides helper functions to test Hono routes with a real kernel,
5
+ * PGlite database, and authentication middleware.
6
+ */
7
+
8
+ import { Hono } from "hono";
9
+ import { createKernel } from "../runtime/kernel.js";
10
+ import { createAuth } from "../auth/setup.js";
11
+ import { authMiddleware } from "../auth/middleware.js";
12
+ import { createRestRoutes } from "../interfaces/rest/index.js";
13
+ import { createPGliteTestConfig } from "./create-test-config.js";
14
+ import { CommerceValidationError } from "../kernel/errors.js";
15
+ import { Ok, Err } from "../kernel/result.js";
16
+ import type { CommerceConfig } from "../config/types.js";
17
+ import type { Actor } from "../auth/types.js";
18
+ import type { AuthInstance } from "../auth/setup.js";
19
+
20
+ type ServerEnv = {
21
+ Variables: {
22
+ auth: AuthInstance;
23
+ actor: Actor | null;
24
+ };
25
+ };
26
+
27
+ const mockPaymentAdapter = {
28
+ providerId: "test-payments",
29
+ async createPaymentIntent(params: { amount: number; currency: string }) {
30
+ return Ok({
31
+ id: "pi_test_" + Date.now(),
32
+ status: "requires_capture",
33
+ amount: params.amount,
34
+ currency: params.currency,
35
+ clientSecret: "secret_test",
36
+ });
37
+ },
38
+ async capturePayment() {
39
+ return Ok({ id: "pi_test_" + Date.now(), status: "succeeded", amountCaptured: 1000 });
40
+ },
41
+ async refundPayment() {
42
+ return Ok({ id: "re_test_" + Date.now(), status: "succeeded", amountRefunded: 1000 });
43
+ },
44
+ async cancelPaymentIntent() {
45
+ return Ok(undefined);
46
+ },
47
+ async verifyWebhook(request: Request) {
48
+ // Extract signature from headers
49
+ const signature = request.headers.get("stripe-signature") || request.headers.get("webhook-signature");
50
+
51
+ // Extract payload from request body
52
+ let payload: Record<string, unknown> | undefined;
53
+ try {
54
+ payload = await request.clone().json();
55
+ } catch {
56
+ return Err(new CommerceValidationError("Webhook payload is invalid or missing"));
57
+ }
58
+
59
+ // Simulate signature verification for production-hardened testing
60
+ // Reject invalid signatures like "invalid_signature"
61
+ if (signature === "invalid_signature") {
62
+ return Err(new CommerceValidationError("Invalid webhook signature"));
63
+ }
64
+
65
+ // Reject requests with no required fields
66
+ if (!payload || !payload.type) {
67
+ return Err(new CommerceValidationError("Webhook payload is missing required fields"));
68
+ }
69
+
70
+ return Ok({ id: "evt_test_" + Date.now(), type: String(payload.type), data: (payload.data ?? {}) as unknown });
71
+ },
72
+ };
73
+
74
+ /**
75
+ * Creates a test server with PGlite-backed kernel for REST API testing.
76
+ */
77
+ export async function createTestServer(
78
+ overrides: Partial<CommerceConfig> = {},
79
+ ): Promise<{
80
+ server: Hono<ServerEnv>;
81
+ kernel: ReturnType<typeof createKernel>;
82
+ auth: AuthInstance;
83
+ cleanup: () => Promise<void>;
84
+ }> {
85
+ const { config, cleanup } = await createPGliteTestConfig({
86
+ payments: [mockPaymentAdapter],
87
+ ...overrides,
88
+ });
89
+
90
+ const kernel = createKernel(config);
91
+ const auth = createAuth(kernel.database, config);
92
+ const app = new Hono<ServerEnv>();
93
+
94
+ // Set auth in context (like createServer does)
95
+ app.use("*", async (c, next) => {
96
+ c.set("auth", auth);
97
+ await next();
98
+ });
99
+
100
+ // Test middleware: allow direct actor injection via x-test-actor header for testing
101
+ app.use("*", async (c, next) => {
102
+ const testActorHeader = c.req.header("x-test-actor");
103
+ if (testActorHeader) {
104
+ try {
105
+ const actor = JSON.parse(testActorHeader) as Actor;
106
+ c.set("actor", actor);
107
+ await next();
108
+ return;
109
+ } catch {
110
+ // Invalid JSON, continue to auth middleware
111
+ }
112
+ }
113
+ await next();
114
+ });
115
+
116
+ // Add auth middleware
117
+ app.use("*", authMiddleware(auth, config));
118
+
119
+ // Error handling middleware - catch thrown errors and convert to JSON
120
+ app.use("*", async (c, next) => {
121
+ try {
122
+ await next();
123
+ } catch (error) {
124
+ // Import error handling utilities
125
+ const { mapErrorToResponse, mapErrorToStatus } = await import("../interfaces/rest/utils.js");
126
+ return c.json(
127
+ mapErrorToResponse(error),
128
+ mapErrorToStatus(error),
129
+ );
130
+ }
131
+ });
132
+
133
+ // Add REST routes
134
+ app.route("/api", createRestRoutes(kernel));
135
+
136
+ return { server: app, kernel, auth, cleanup };
137
+ }
138
+
139
+ /**
140
+ * Helper to parse JSON response from Hono Response
141
+ */
142
+ export async function parseJsonResponse<T = unknown>(response: Response): Promise<T> {
143
+ return response.json() as Promise<T>;
144
+ }
145
+
146
+ /**
147
+ * Common test actor with staff permissions
148
+ */
149
+ export const testActor: Actor = {
150
+ type: "user",
151
+ userId: "00000000-0000-0000-0000-000000000001",
152
+ email: "test@example.com",
153
+ name: "Test Staff",
154
+ vendorId: null,
155
+ organizationId: "org_default",
156
+ role: "staff",
157
+ permissions: [
158
+ "catalog:create",
159
+ "catalog:update",
160
+ "catalog:read",
161
+ "inventory:adjust",
162
+ "inventory:read",
163
+ "orders:create",
164
+ "orders:read",
165
+ "orders:update",
166
+ "cart:create",
167
+ "cart:update",
168
+ "cart:read",
169
+ "customers:update:self",
170
+ "webhooks:manage",
171
+ "pricing:manage",
172
+ "promotions:manage",
173
+ "promotions:read",
174
+ "audit:read",
175
+ "media:write",
176
+ ],
177
+ };
178
+
179
+ /**
180
+ * Test actor with read-only permissions
181
+ */
182
+ export const readonlyActor: Actor = {
183
+ type: "user",
184
+ userId: "00000000-0000-0000-0000-000000000002",
185
+ email: "readonly@example.com",
186
+ name: "Read Only User",
187
+ vendorId: null,
188
+ organizationId: "org_default",
189
+ role: "customer",
190
+ permissions: ["catalog:read", "cart:read", "orders:read:own"],
191
+ };
192
+
193
+ /**
194
+ * Test actor with no permissions
195
+ */
196
+ export const noPermActor: Actor = {
197
+ type: "user",
198
+ userId: "00000000-0000-0000-0000-000000000003",
199
+ email: "noperm@example.com",
200
+ name: "No Perm",
201
+ vendorId: null,
202
+ organizationId: "org_default",
203
+ role: "customer",
204
+ permissions: [],
205
+ };
206
+
207
+ /**
208
+ * Helper to create a mock request with actor context
209
+ */
210
+ export function createMockRequest(server: Hono<ServerEnv>, options: {
211
+ method: string;
212
+ url: string;
213
+ body?: unknown;
214
+ headers?: Record<string, string>;
215
+ actor?: Actor;
216
+ }) {
217
+ const url = new URL(options.url, "http://localhost");
218
+
219
+ const headers: Record<string, string> = {
220
+ "content-type": "application/json",
221
+ ...options.headers,
222
+ };
223
+
224
+ // Add actor as header for test middleware
225
+ if (options.actor) {
226
+ headers["x-test-actor"] = JSON.stringify(options.actor);
227
+ }
228
+
229
+ // Build the request
230
+ const requestInit: RequestInit = {
231
+ method: options.method,
232
+ headers,
233
+ };
234
+ if (options.body) {
235
+ requestInit.body = JSON.stringify(options.body);
236
+ }
237
+ const request = new Request(url, requestInit);
238
+
239
+ return request;
240
+ }
241
+
242
+ /**
243
+ * Helper to make authenticated requests to the test server
244
+ */
245
+ export async function makeRequest(
246
+ server: Hono<ServerEnv>,
247
+ options: {
248
+ method: string;
249
+ url: string;
250
+ body?: unknown;
251
+ headers?: Record<string, string>;
252
+ actor?: Actor;
253
+ },
254
+ ) {
255
+ // Create request with actor header (defaults to testActor)
256
+ const request = createMockRequest(server, {
257
+ ...options,
258
+ actor: options.actor ?? testActor,
259
+ });
260
+
261
+ // Route the request through Hono
262
+ const response = await server.fetch(request);
263
+
264
+ return response;
265
+ }
@@ -0,0 +1,62 @@
1
+ import type { Actor } from "../auth/types.js";
2
+
3
+ /** Admin with wildcard permissions. Use for setup operations in beforeAll. */
4
+ export const testAdminActor: Actor = {
5
+ type: "user",
6
+ userId: "test-admin-1",
7
+ email: "admin@test.local",
8
+ name: "Test Admin",
9
+ vendorId: null,
10
+ organizationId: "org_default",
11
+ role: "admin",
12
+ permissions: ["*:*"],
13
+ };
14
+
15
+ /** Staff with common operational permissions. */
16
+ export const testStaffActor: Actor = {
17
+ type: "user",
18
+ userId: "test-staff-1",
19
+ email: "staff@test.local",
20
+ name: "Test Staff",
21
+ vendorId: null,
22
+ organizationId: "org_default",
23
+ role: "staff",
24
+ permissions: [
25
+ "catalog:read", "catalog:create", "catalog:update",
26
+ "inventory:adjust", "orders:read", "orders:create", "orders:update",
27
+ ],
28
+ };
29
+
30
+ /** Customer with minimal read/write-own permissions. */
31
+ export const testCustomerActor: Actor = {
32
+ type: "user",
33
+ userId: "test-customer-1",
34
+ email: "customer@test.local",
35
+ name: "Test Customer",
36
+ vendorId: null,
37
+ organizationId: "org_default",
38
+ role: "customer",
39
+ permissions: ["catalog:read", "cart:create", "cart:read", "orders:read:own"],
40
+ };
41
+
42
+ /** Actor with zero permissions. Use for negative auth/perm tests. */
43
+ export const testNoPermActor: Actor = {
44
+ type: "user",
45
+ userId: "test-noperm-1",
46
+ email: "noperm@test.local",
47
+ name: "No Permissions",
48
+ vendorId: null,
49
+ organizationId: "org_default",
50
+ role: "customer",
51
+ permissions: [],
52
+ };
53
+
54
+ /**
55
+ * Builds request headers with optional test actor injection.
56
+ * The x-test-actor header is parsed by createPluginTestApp's middleware.
57
+ */
58
+ export function jsonHeaders(actor?: Actor): Record<string, string> {
59
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
60
+ if (actor) headers["x-test-actor"] = JSON.stringify(actor);
61
+ return headers;
62
+ }
@@ -0,0 +1,54 @@
1
+ import type { PluginHookRegistration } from "../kernel/plugin/manifest.js";
2
+ import type { HookContext, HookOperation } from "../kernel/hooks/types.js";
3
+
4
+ /**
5
+ * Creates a typed before-hook registration for plugins.
6
+ *
7
+ * Narrows the handler signature from the loose `(...args: unknown[]) => unknown`
8
+ * on PluginHookRegistration to the actual `{ data, operation, context }` shape,
9
+ * providing autocomplete on context.jobs.enqueue(), context.logger.info(), etc.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * hooks: () => [
14
+ * beforeHook<{ customerId: string }>("orders.beforeCreate", async ({ data, context }) => {
15
+ * context.logger.info("order_creating", { customerId: data.customerId });
16
+ * return data;
17
+ * }),
18
+ * ],
19
+ * ```
20
+ */
21
+ export function beforeHook<TData>(
22
+ key: string,
23
+ handler: (args: {
24
+ data: TData;
25
+ operation: HookOperation;
26
+ context: HookContext;
27
+ }) => Promise<TData> | TData,
28
+ ): PluginHookRegistration {
29
+ return { key, handler: handler as PluginHookRegistration["handler"] };
30
+ }
31
+
32
+ /**
33
+ * Creates a typed after-hook registration for plugins.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * hooks: () => [
38
+ * afterHook<{ id: string; grandTotal: number }>("orders.afterCreate", async ({ result, context }) => {
39
+ * await context.jobs.enqueue("loyalty:award-points", { orderId: result.id });
40
+ * }),
41
+ * ],
42
+ * ```
43
+ */
44
+ export function afterHook<TData>(
45
+ key: string,
46
+ handler: (args: {
47
+ data: TData | null;
48
+ result: TData;
49
+ operation: HookOperation;
50
+ context: HookContext;
51
+ }) => Promise<void> | void,
52
+ ): PluginHookRegistration {
53
+ return { key, handler: handler as PluginHookRegistration["handler"] };
54
+ }