@porulle/core 0.1.0 → 0.6.0

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 (166) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth/middleware.d.ts.map +1 -1
  3. package/dist/auth/middleware.js +17 -1
  4. package/dist/config/define-config.d.ts.map +1 -1
  5. package/dist/config/define-config.js +6 -0
  6. package/dist/config/types.d.ts +12 -0
  7. package/dist/config/types.d.ts.map +1 -1
  8. package/dist/hooks/checkout.d.ts +3 -3
  9. package/dist/hooks/checkout.js +4 -4
  10. package/dist/index.d.ts +9 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +6 -1
  13. package/dist/interfaces/rest/audit-middleware.d.ts +46 -0
  14. package/dist/interfaces/rest/audit-middleware.d.ts.map +1 -0
  15. package/dist/interfaces/rest/audit-middleware.js +82 -0
  16. package/dist/interfaces/rest/parse-json.d.ts +39 -0
  17. package/dist/interfaces/rest/parse-json.d.ts.map +1 -0
  18. package/dist/interfaces/rest/parse-json.js +45 -0
  19. package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
  20. package/dist/interfaces/rest/routes/catalog.js +17 -2
  21. package/dist/interfaces/rest/routes/customers.d.ts.map +1 -1
  22. package/dist/interfaces/rest/routes/customers.js +59 -4
  23. package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
  24. package/dist/interfaces/rest/routes/inventory.js +4 -2
  25. package/dist/interfaces/rest/routes/media.d.ts.map +1 -1
  26. package/dist/interfaces/rest/routes/media.js +6 -0
  27. package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
  28. package/dist/interfaces/rest/routes/orders.js +23 -1
  29. package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
  30. package/dist/interfaces/rest/routes/promotions.js +21 -1
  31. package/dist/interfaces/rest/schemas/carts.d.ts +175 -0
  32. package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
  33. package/dist/interfaces/rest/schemas/catalog.d.ts +1410 -0
  34. package/dist/interfaces/rest/schemas/catalog.d.ts.map +1 -1
  35. package/dist/interfaces/rest/schemas/catalog.js +28 -1
  36. package/dist/interfaces/rest/schemas/checkout.d.ts +35 -0
  37. package/dist/interfaces/rest/schemas/checkout.d.ts.map +1 -1
  38. package/dist/interfaces/rest/schemas/customer-portal.d.ts +280 -0
  39. package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
  40. package/dist/interfaces/rest/schemas/customers.d.ts +863 -0
  41. package/dist/interfaces/rest/schemas/customers.d.ts.map +1 -1
  42. package/dist/interfaces/rest/schemas/customers.js +88 -0
  43. package/dist/interfaces/rest/schemas/inventory.d.ts +182 -1
  44. package/dist/interfaces/rest/schemas/inventory.d.ts.map +1 -1
  45. package/dist/interfaces/rest/schemas/media.d.ts +105 -0
  46. package/dist/interfaces/rest/schemas/media.d.ts.map +1 -1
  47. package/dist/interfaces/rest/schemas/orders.d.ts +136 -0
  48. package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
  49. package/dist/interfaces/rest/schemas/orders.js +20 -0
  50. package/dist/interfaces/rest/schemas/pricing.d.ts +70 -0
  51. package/dist/interfaces/rest/schemas/pricing.d.ts.map +1 -1
  52. package/dist/interfaces/rest/schemas/promotions.d.ts +298 -0
  53. package/dist/interfaces/rest/schemas/promotions.d.ts.map +1 -1
  54. package/dist/interfaces/rest/schemas/promotions.js +25 -1
  55. package/dist/interfaces/rest/schemas/shared.d.ts +47 -0
  56. package/dist/interfaces/rest/schemas/shared.d.ts.map +1 -1
  57. package/dist/interfaces/rest/schemas/shared.js +8 -0
  58. package/dist/interfaces/rest/schemas/webhooks.d.ts +70 -0
  59. package/dist/interfaces/rest/schemas/webhooks.d.ts.map +1 -1
  60. package/dist/kernel/database/adapter.d.ts +6 -0
  61. package/dist/kernel/database/adapter.d.ts.map +1 -1
  62. package/dist/kernel/database/adapter.js +62 -1
  63. package/dist/kernel/database/migrate.d.ts +22 -2
  64. package/dist/kernel/database/migrate.d.ts.map +1 -1
  65. package/dist/kernel/database/migrate.js +42 -2
  66. package/dist/modules/analytics/hooks.d.ts +5 -5
  67. package/dist/modules/analytics/hooks.js +5 -5
  68. package/dist/modules/analytics/repository/index.d.ts.map +1 -1
  69. package/dist/modules/catalog/category-service.d.ts +6 -1
  70. package/dist/modules/catalog/category-service.d.ts.map +1 -1
  71. package/dist/modules/catalog/category-service.js +35 -5
  72. package/dist/modules/catalog/schema.d.ts +17 -0
  73. package/dist/modules/catalog/schema.d.ts.map +1 -1
  74. package/dist/modules/catalog/schema.js +2 -0
  75. package/dist/modules/catalog/service.d.ts +11 -2
  76. package/dist/modules/catalog/service.d.ts.map +1 -1
  77. package/dist/modules/catalog/service.js +8 -2
  78. package/dist/modules/customers/repository/index.d.ts +8 -1
  79. package/dist/modules/customers/repository/index.d.ts.map +1 -1
  80. package/dist/modules/customers/repository/index.js +42 -2
  81. package/dist/modules/customers/schema.d.ts +162 -0
  82. package/dist/modules/customers/schema.d.ts.map +1 -1
  83. package/dist/modules/customers/schema.js +18 -0
  84. package/dist/modules/customers/service.d.ts +32 -2
  85. package/dist/modules/customers/service.d.ts.map +1 -1
  86. package/dist/modules/customers/service.js +119 -3
  87. package/dist/modules/inventory/schemas.d.ts +7 -1
  88. package/dist/modules/inventory/schemas.d.ts.map +1 -1
  89. package/dist/modules/inventory/schemas.js +6 -1
  90. package/dist/modules/inventory/service.d.ts +18 -0
  91. package/dist/modules/inventory/service.d.ts.map +1 -1
  92. package/dist/modules/inventory/service.js +58 -21
  93. package/dist/modules/media/noop-adapter.d.ts +11 -0
  94. package/dist/modules/media/noop-adapter.d.ts.map +1 -0
  95. package/dist/modules/media/noop-adapter.js +31 -0
  96. package/dist/modules/orders/repository/index.d.ts +19 -0
  97. package/dist/modules/orders/repository/index.d.ts.map +1 -1
  98. package/dist/modules/orders/repository/index.js +42 -1
  99. package/dist/modules/orders/service.d.ts +31 -0
  100. package/dist/modules/orders/service.d.ts.map +1 -1
  101. package/dist/modules/orders/service.js +52 -2
  102. package/dist/modules/payments/repository/index.d.ts.map +1 -1
  103. package/dist/modules/promotions/schemas.d.ts +46 -0
  104. package/dist/modules/promotions/schemas.d.ts.map +1 -1
  105. package/dist/modules/promotions/schemas.js +19 -8
  106. package/dist/modules/promotions/service.d.ts +8 -2
  107. package/dist/modules/promotions/service.d.ts.map +1 -1
  108. package/dist/modules/promotions/service.js +74 -9
  109. package/dist/modules/search/repository/index.d.ts.map +1 -1
  110. package/dist/modules/shipping/repository/index.d.ts.map +1 -1
  111. package/dist/modules/tax/repository/index.d.ts.map +1 -1
  112. package/dist/runtime/client-ip.d.ts +31 -0
  113. package/dist/runtime/client-ip.d.ts.map +1 -0
  114. package/dist/runtime/client-ip.js +31 -0
  115. package/dist/runtime/server.d.ts +4 -0
  116. package/dist/runtime/server.d.ts.map +1 -1
  117. package/dist/runtime/server.js +35 -19
  118. package/dist/test-utils/create-plugin-test-app.d.ts.map +1 -1
  119. package/dist/test-utils/create-plugin-test-app.js +4 -1
  120. package/dist/test-utils/test-actors.d.ts +3 -2
  121. package/dist/test-utils/test-actors.d.ts.map +1 -1
  122. package/dist/test-utils/test-actors.js +3 -2
  123. package/package.json +15 -15
  124. package/src/auth/middleware.ts +18 -1
  125. package/src/config/define-config.ts +7 -0
  126. package/src/config/types.ts +12 -0
  127. package/src/hooks/checkout.ts +4 -4
  128. package/src/index.ts +18 -1
  129. package/src/interfaces/rest/audit-middleware.ts +110 -0
  130. package/src/interfaces/rest/parse-json.ts +81 -0
  131. package/src/interfaces/rest/routes/catalog.ts +21 -1
  132. package/src/interfaces/rest/routes/customers.ts +64 -3
  133. package/src/interfaces/rest/routes/inventory.ts +4 -2
  134. package/src/interfaces/rest/routes/media.ts +7 -0
  135. package/src/interfaces/rest/routes/orders.ts +21 -1
  136. package/src/interfaces/rest/routes/promotions.ts +22 -1
  137. package/src/interfaces/rest/schemas/catalog.ts +30 -1
  138. package/src/interfaces/rest/schemas/customers.ts +99 -0
  139. package/src/interfaces/rest/schemas/orders.ts +21 -0
  140. package/src/interfaces/rest/schemas/promotions.ts +32 -1
  141. package/src/interfaces/rest/schemas/shared.ts +9 -0
  142. package/src/kernel/database/adapter.ts +64 -1
  143. package/src/kernel/database/migrate.ts +50 -2
  144. package/src/modules/analytics/hooks.ts +5 -5
  145. package/src/modules/analytics/repository/index.ts +0 -1
  146. package/src/modules/catalog/category-service.ts +26 -5
  147. package/src/modules/catalog/schema.ts +2 -0
  148. package/src/modules/catalog/service.ts +25 -3
  149. package/src/modules/customers/repository/index.ts +55 -1
  150. package/src/modules/customers/schema.ts +19 -0
  151. package/src/modules/customers/service.ts +137 -2
  152. package/src/modules/inventory/schemas.ts +6 -1
  153. package/src/modules/inventory/service.ts +84 -29
  154. package/src/modules/media/noop-adapter.ts +36 -0
  155. package/src/modules/orders/repository/index.ts +63 -1
  156. package/src/modules/orders/service.ts +86 -3
  157. package/src/modules/payments/repository/index.ts +0 -1
  158. package/src/modules/promotions/schemas.ts +21 -4
  159. package/src/modules/promotions/service.ts +90 -18
  160. package/src/modules/search/repository/index.ts +0 -1
  161. package/src/modules/shipping/repository/index.ts +0 -1
  162. package/src/modules/tax/repository/index.ts +0 -1
  163. package/src/runtime/client-ip.ts +49 -0
  164. package/src/runtime/server.ts +39 -18
  165. package/src/test-utils/create-plugin-test-app.ts +3 -1
  166. package/src/test-utils/test-actors.ts +3 -2
@@ -4,6 +4,9 @@ import { createHookContext } from "../../kernel/hooks/create-context.js";
4
4
  import { Err, Ok } from "../../kernel/result.js";
5
5
  import { createLogger } from "../../utils/logger.js";
6
6
  import { resolveOrgId } from "../../auth/org.js";
7
+ // PromotionType is derived from the zod enum (single source of truth) in
8
+ // ./schemas.js; re-export it here for backwards-compatible imports.
9
+ import { promotionTypeEnum } from "./schemas.js";
7
10
  function hookContext(actor, services, database, tx) {
8
11
  return createHookContext({
9
12
  actor,
@@ -74,15 +77,8 @@ export class PromotionService {
74
77
  if (input.value < 0) {
75
78
  return Err(new CommerceValidationError("Promotion value cannot be negative."));
76
79
  }
77
- // Validate promotion type
78
- const validTypes = [
79
- "percentage_off_order",
80
- "fixed_off_order",
81
- "percentage_off_item",
82
- "fixed_off_item",
83
- "free_shipping",
84
- "buy_x_get_y",
85
- ];
80
+ // Validate promotion type against the single-source enum
81
+ const validTypes = promotionTypeEnum.options;
86
82
  if (!validTypes.includes(input.type)) {
87
83
  return Err(new CommerceValidationError(`Invalid promotion type "${input.type}". Must be one of: ${validTypes.join(", ")}`));
88
84
  }
@@ -131,6 +127,75 @@ export class PromotionService {
131
127
  await runAfterHooks(afterHooks, promotion, updated, "update", hctx);
132
128
  return Ok(updated);
133
129
  }
130
+ /**
131
+ * Edit any subset of a promotion's fields. Validated the same way create is
132
+ * (value bounds, type enum, code uniqueness). Only provided keys are written.
133
+ */
134
+ async update(orgId, id, input, actor, ctx) {
135
+ const existing = await this.repo.findById(orgId, id, ctx);
136
+ if (!existing) {
137
+ return Err(new CommerceNotFoundError("Promotion not found."));
138
+ }
139
+ if (input.value !== undefined && input.value < 0) {
140
+ return Err(new CommerceValidationError("Promotion value cannot be negative."));
141
+ }
142
+ if (input.type !== undefined) {
143
+ const validTypes = promotionTypeEnum.options;
144
+ if (!validTypes.includes(input.type)) {
145
+ return Err(new CommerceValidationError(`Invalid promotion type "${input.type}". Must be one of: ${validTypes.join(", ")}`));
146
+ }
147
+ }
148
+ let normalizedCode;
149
+ if (input.code !== undefined) {
150
+ normalizedCode = input.code ? input.code.trim().toUpperCase() : null;
151
+ if (normalizedCode) {
152
+ const other = await this.repo.findByCode(orgId, normalizedCode, ctx);
153
+ if (other && other.id !== id) {
154
+ return Err(new CommerceValidationError(`Promotion code ${normalizedCode} already exists.`));
155
+ }
156
+ }
157
+ }
158
+ const patch = {};
159
+ if (input.name !== undefined)
160
+ patch.name = input.name;
161
+ if (input.type !== undefined)
162
+ patch.type = input.type;
163
+ if (input.value !== undefined)
164
+ patch.value = roundMoney(input.value);
165
+ if (input.isAutomatic !== undefined)
166
+ patch.isAutomatic = input.isAutomatic;
167
+ if (input.isActive !== undefined)
168
+ patch.isActive = input.isActive;
169
+ if (input.priority !== undefined)
170
+ patch.priority = input.priority;
171
+ if (input.conditions !== undefined)
172
+ patch.conditions = input.conditions;
173
+ if (input.metadata !== undefined)
174
+ patch.metadata = input.metadata;
175
+ if (normalizedCode !== undefined)
176
+ patch.code = normalizedCode;
177
+ if (input.buyQuantity !== undefined)
178
+ patch.buyQuantity = input.buyQuantity;
179
+ if (input.getQuantity !== undefined)
180
+ patch.getQuantity = input.getQuantity;
181
+ if (input.usageLimitTotal !== undefined)
182
+ patch.usageLimitTotal = input.usageLimitTotal;
183
+ if (input.usageLimitPerCustomer !== undefined) {
184
+ patch.usageLimitPerCustomer = input.usageLimitPerCustomer;
185
+ }
186
+ if (input.validFrom !== undefined)
187
+ patch.validFrom = input.validFrom;
188
+ if (input.validUntil !== undefined)
189
+ patch.validUntil = input.validUntil;
190
+ const updated = await this.repo.update(id, patch, ctx);
191
+ if (!updated) {
192
+ return Err(new CommerceNotFoundError("Promotion not found."));
193
+ }
194
+ const afterHooks = this.deps.hooks.resolve("promotions.afterUpdate");
195
+ const hctx = hookContext(actor ?? ctx?.actor ?? null, this.deps.services, this.deps.database, ctx?.tx ?? null);
196
+ await runAfterHooks(afterHooks, existing, updated, "update", hctx);
197
+ return Ok(updated);
198
+ }
134
199
  async list(filter, actor, ctx) {
135
200
  const orgId = resolveOrgId(actor ?? ctx?.actor ?? null);
136
201
  const all = await this.repo.findAll(orgId, ctx);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/search/repository/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AAExE,MAAM,WAAW,gBAAgB;IAE/B,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/search/repository/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AAExE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/shipping/repository/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AAExE,MAAM,WAAW,kBAAkB;IAEjC,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/shipping/repository/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AAExE,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/tax/repository/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AAExE,MAAM,WAAW,aAAa;IAE5B,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/modules/tax/repository/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Client-IP resolution for rate limiting.
3
+ *
4
+ * The Node default reads `c.req.raw.socket.remoteAddress` — which is always
5
+ * undefined on edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge),
6
+ * collapsing every client onto a single rate-limit key (a trivial DoS). Edge
7
+ * deployments inject `runtime.getClientIp` to read the platform header instead
8
+ * (CF: `cf-connecting-ip`, Vercel Edge: `x-real-ip`, Fly: `fly-client-ip`).
9
+ */
10
+ export interface ClientIpContext {
11
+ req: {
12
+ raw: unknown;
13
+ header(name: string): string | undefined;
14
+ };
15
+ }
16
+ export type ClientIpResolver = (c: ClientIpContext) => string;
17
+ export interface RuntimeConfig {
18
+ /** Resolve the client IP from the request (used as the rate-limit key). */
19
+ getClientIp?: ClientIpResolver;
20
+ /** Direct-connection IP of the trusted reverse proxy (Node default only). */
21
+ trustedProxyIp?: string;
22
+ }
23
+ /**
24
+ * Build the client-IP resolver. Prefers an injected `runtime.getClientIp`;
25
+ * otherwise falls back to the Node behavior (socket address, trusting
26
+ * X-Forwarded-For only from `trustedProxyIp` / `TRUSTED_PROXY_IP`).
27
+ */
28
+ export declare function createClientIpResolver(config: {
29
+ runtime?: RuntimeConfig;
30
+ }): ClientIpResolver;
31
+ //# sourceMappingURL=client-ip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip.d.ts","sourceRoot":"","sources":["../../src/runtime/client-ip.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE;QAAE,GAAG,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;CACjE;AAED,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,EAAE,eAAe,KAAK,MAAM,CAAC;AAE9D,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,GAAG,gBAAgB,CAkBnB"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Client-IP resolution for rate limiting.
3
+ *
4
+ * The Node default reads `c.req.raw.socket.remoteAddress` — which is always
5
+ * undefined on edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge),
6
+ * collapsing every client onto a single rate-limit key (a trivial DoS). Edge
7
+ * deployments inject `runtime.getClientIp` to read the platform header instead
8
+ * (CF: `cf-connecting-ip`, Vercel Edge: `x-real-ip`, Fly: `fly-client-ip`).
9
+ */
10
+ /**
11
+ * Build the client-IP resolver. Prefers an injected `runtime.getClientIp`;
12
+ * otherwise falls back to the Node behavior (socket address, trusting
13
+ * X-Forwarded-For only from `trustedProxyIp` / `TRUSTED_PROXY_IP`).
14
+ */
15
+ export function createClientIpResolver(config) {
16
+ if (config.runtime?.getClientIp) {
17
+ return config.runtime.getClientIp;
18
+ }
19
+ const trustedProxyIp = config.runtime?.trustedProxyIp ??
20
+ (typeof process !== "undefined" ? process.env?.TRUSTED_PROXY_IP : undefined);
21
+ return (c) => {
22
+ const raw = c.req.raw;
23
+ const remoteAddress = raw?.socket?.remoteAddress;
24
+ if (trustedProxyIp && remoteAddress === trustedProxyIp) {
25
+ const xff = c.req.header("x-forwarded-for")?.split(",")[0]?.trim();
26
+ if (xff)
27
+ return xff;
28
+ }
29
+ return remoteAddress ?? "unknown";
30
+ };
31
+ }
@@ -32,6 +32,10 @@ export declare function createServer(config: CommerceConfig): Promise<{
32
32
  kernel: import("./kernel-types.js").Kernel;
33
33
  logger: Logger;
34
34
  commerce: CommerceInstance;
35
+ runJobs: (queue?: string, limit?: number) => Promise<{
36
+ processed: number;
37
+ failed: number;
38
+ }>;
35
39
  }>;
36
40
  export {};
37
41
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAMhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAMzD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItE,KAAK,SAAS,GAAG;IACf,SAAS,EAAE;QACT,IAAI,EAAE,YAAY,CAAC;QACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAMvC;AAeD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,cAAc;;;;;GAoZxD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOhD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAMzD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItE,KAAK,SAAS,GAAG;IACf,SAAS,EAAE;QACT,IAAI,EAAE,YAAY,CAAC;QACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAMvC;AAeD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,cAAc;;;;;sBAwVxB,MAAM,UAAU,MAAM;;;;GAgFtD"}
@@ -4,6 +4,7 @@ import { csrf } from "hono/csrf";
4
4
  import { bodyLimit } from "hono/body-limit";
5
5
  import { rateLimiter } from "hono-rate-limiter";
6
6
  import { createHash } from "node:crypto";
7
+ import { createClientIpResolver } from "./client-ip.js";
7
8
  import { authMiddleware } from "../auth/middleware.js";
8
9
  import { createRestRoutes } from "../interfaces/rest/index.js";
9
10
  import { createCustomerPortalRoutes } from "../interfaces/rest/customer-portal.js";
@@ -64,12 +65,12 @@ export async function createServer(config) {
64
65
  if (config.auth?.enableDevKey !== undefined) {
65
66
  throw new Error("FATAL: auth.enableDevKey has been removed. " +
66
67
  "Use 'bunx @porulle/cli api-key create --scope admin' to generate a real API key. " +
67
- "See RFC-050: https://github.com/octalpixel/unified-commerce/blob/main/RFC-050-AUTH-KEY-SCOPING.md");
68
+ "See https://github.com/asyncdotengineering/porulle/blob/main/SECURITY.md for the auth/key scoping rationale.");
68
69
  }
69
70
  if (config.auth?.devKey !== undefined) {
70
71
  throw new Error("FATAL: auth.devKey has been removed. " +
71
72
  "Use 'bunx @porulle/cli api-key create --scope admin' to generate a real API key. " +
72
- "See RFC-050: https://github.com/octalpixel/unified-commerce/blob/main/RFC-050-AUTH-KEY-SCOPING.md");
73
+ "See https://github.com/asyncdotengineering/porulle/blob/main/SECURITY.md for the auth/key scoping rationale.");
73
74
  }
74
75
  // exposed for direct unit testing — see test/server-edge-runtime.test.ts
75
76
  // ─── Process Crash Handlers (F4) ─────────────────────────────────────
@@ -143,28 +144,30 @@ export async function createServer(config) {
143
144
  : (process.env.NODE_ENV === "production" ? [] : ["http://localhost:*"]),
144
145
  }));
145
146
  // ─── Body Size Limit (F6) ──────────────────────────────────────────
146
- app.use("*", bodyLimit({
147
+ // Media uploads (phone photos are 3–8MB) get their own larger limit and are
148
+ // exempt from the global 1MB limit. Everything else stays at 1MB.
149
+ const mediaMaxUploadSize = config.media?.maxUploadSize ?? 10 * 1024 * 1024;
150
+ const MEDIA_UPLOAD_PATH = "/api/media/upload";
151
+ app.use(MEDIA_UPLOAD_PATH, bodyLimit({
152
+ maxSize: mediaMaxUploadSize,
153
+ onError: (c) => c.json({
154
+ error: { code: "FILE_TOO_LARGE", message: `Upload exceeds the ${mediaMaxUploadSize}-byte limit.` },
155
+ }, 413),
156
+ }));
157
+ const globalBodyLimit = bodyLimit({
147
158
  maxSize: 1024 * 1024, // 1 MB default
148
159
  onError: (c) => c.json({
149
160
  error: { code: "PAYLOAD_TOO_LARGE", message: "Request body exceeds 1MB limit." },
150
161
  }, 413),
151
- }));
162
+ });
163
+ app.use("*", (c, next) => c.req.path === MEDIA_UPLOAD_PATH ? next() : globalBodyLimit(c, next));
152
164
  // ─── Rate Limiting (F1) ──────────────────────────────────────────────
153
- // Trust X-Forwarded-For ONLY from a known reverse proxy IP.
154
- // Set TRUSTED_PROXY_IP env var to the proxy's IP (e.g., "127.0.0.1").
155
- const trustedProxyIp = process.env.TRUSTED_PROXY_IP;
165
+ // Client IP drives the rate-limit key. Defaults to the Node socket address
166
+ // (trusting X-Forwarded-For only from a known proxy via config.runtime
167
+ // .trustedProxyIp / TRUSTED_PROXY_IP); edge runtimes inject
168
+ // config.runtime.getClientIp to read the platform header instead.
156
169
  const signInEmailRateKeyCache = new WeakMap();
157
- const getClientIp = (c) => {
158
- const raw = c.req.raw;
159
- const remoteAddress = raw.socket?.remoteAddress;
160
- // Only trust X-Forwarded-For when the direct connection is from a trusted proxy
161
- if (trustedProxyIp && remoteAddress === trustedProxyIp) {
162
- const xff = c.req.header("x-forwarded-for")?.split(",")[0]?.trim();
163
- if (xff)
164
- return xff;
165
- }
166
- return remoteAddress ?? "unknown";
167
- };
170
+ const getClientIp = createClientIpResolver(config);
168
171
  const keyGenerator = getClientIp;
169
172
  app.use("/api/auth/*", rateLimiter({
170
173
  windowMs: 60 * 1000,
@@ -394,5 +397,18 @@ export async function createServer(config) {
394
397
  const rewritten = rewriteCommerceAliasRequest(req, config.entities);
395
398
  return dispatchFetch(rewritten, env, executionCtx);
396
399
  });
397
- return { app, kernel, logger, commerce };
400
+ // `runJobs` triggers one job-runner tick. On Cloudflare Workers, call it
401
+ // from `scheduled()` so cron triggers drive the queue without an in-process
402
+ // setInterval (which can't outlive a request on Workers):
403
+ //
404
+ // // wrangler.toml
405
+ // [triggers]
406
+ // crons = ["*/5 * * * *"]
407
+ //
408
+ // // worker.ts
409
+ // export default {
410
+ // fetch: (req, env, ctx) => server.app.fetch(req, env, ctx),
411
+ // scheduled: (_event, env, ctx) => ctx.waitUntil(server.runJobs()),
412
+ // };
413
+ return { app, kernel, logger, commerce, runJobs };
398
414
  }
@@ -1 +1 @@
1
- {"version":3,"file":"create-plugin-test-app.d.ts","sourceRoot":"","sources":["../../src/test-utils/create-plugin-test-app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAenD;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE;QACT,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;KACrB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,wFAAwF;IACxF,GAAG,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;IAC7B,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,EAAE,EAAE,UAAU,CAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,cAAc,EACtB,eAAe,GAAE,OAAO,CAAC,cAAc,CAAM,GAC5C,OAAO,CAAC,aAAa,CAAC,CA+ExB"}
1
+ {"version":3,"file":"create-plugin-test-app.d.ts","sourceRoot":"","sources":["../../src/test-utils/create-plugin-test-app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAgBnD;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE;QACT,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;KACrB,CAAC;CACH,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,wFAAwF;IACxF,GAAG,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;IAC7B,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,EAAE,EAAE,UAAU,CAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,cAAc,EACtB,eAAe,GAAE,OAAO,CAAC,cAAc,CAAM,GAC5C,OAAO,CAAC,aAAa,CAAC,CAgFxB"}
@@ -21,6 +21,7 @@ import { createRequire } from "node:module";
21
21
  import { createTestConfig } from "./create-test-config.js";
22
22
  import { createKernel } from "../runtime/kernel.js";
23
23
  import { buildSchema } from "../kernel/database/migrate.js";
24
+ import { unwrapDb } from "../kernel/database/adapter.js";
24
25
  import { ensureDefaultOrg } from "../auth/org.js";
25
26
  import { mapErrorToStatus } from "../kernel/error-mapper.js";
26
27
  // drizzle-kit/api uses CJS internally; createRequire provides ESM compat.
@@ -46,7 +47,9 @@ export async function createPluginTestApp(plugin, configOverrides = {}) {
46
47
  // Diffs current DB state against pgTable definitions, generates DDL, applies it.
47
48
  // On fresh PGlite: creates all tables. On existing DB: creates only missing tables.
48
49
  const drizzleKit = require("drizzle-kit/api");
49
- const { apply } = await drizzleKit.pushSchema(mergedSchema, kernel.database.db);
50
+ const { apply } = await drizzleKit.pushSchema(mergedSchema,
51
+ // drizzle-kit needs the native driver result shape; unwrap the normalized db.
52
+ unwrapDb(kernel.database.db));
50
53
  await apply();
51
54
  // Ensure the default organization exists for plugin tests
52
55
  await ensureDefaultOrg(kernel.database.db);
@@ -1,8 +1,9 @@
1
1
  import type { Actor } from "../auth/types.js";
2
2
  /**
3
3
  * Organization ID used in test fixtures. Tests use the deprecated DEFAULT_ORG_ID
4
- * because ensureDefaultOrg() creates this org in test setup. When RFC-060 Phase 6
5
- * removes DEFAULT_ORG_ID, test setup will create a real org via Better Auth API.
4
+ * because ensureDefaultOrg() creates this org during test setup. When
5
+ * DEFAULT_ORG_ID is removed, test setup will create a real org via the
6
+ * Better Auth API instead.
6
7
  */
7
8
  export declare const TEST_ORG_ID = "org_default";
8
9
  /** Admin with wildcard permissions. Use for setup operations in beforeAll. */
@@ -1 +1 @@
1
- {"version":3,"file":"test-actors.d.ts","sourceRoot":"","sources":["../../src/test-utils/test-actors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAG9C;;;;GAIG;AACH,eAAO,MAAM,WAAW,gBAAiB,CAAC;AAE1C,8EAA8E;AAC9E,eAAO,MAAM,cAAc,EAAE,KAS5B,CAAC;AAEF,iDAAiD;AACjD,eAAO,MAAM,cAAc,EAAE,KAY5B,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,KAS/B,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,eAAe,EAAE,KAS7B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIjE"}
1
+ {"version":3,"file":"test-actors.d.ts","sourceRoot":"","sources":["../../src/test-utils/test-actors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAG9C;;;;;GAKG;AACH,eAAO,MAAM,WAAW,gBAAiB,CAAC;AAE1C,8EAA8E;AAC9E,eAAO,MAAM,cAAc,EAAE,KAS5B,CAAC;AAEF,iDAAiD;AACjD,eAAO,MAAM,cAAc,EAAE,KAY5B,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,KAS/B,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,eAAe,EAAE,KAS7B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIjE"}
@@ -1,8 +1,9 @@
1
1
  import { DEFAULT_ORG_ID } from "../auth/org.js";
2
2
  /**
3
3
  * Organization ID used in test fixtures. Tests use the deprecated DEFAULT_ORG_ID
4
- * because ensureDefaultOrg() creates this org in test setup. When RFC-060 Phase 6
5
- * removes DEFAULT_ORG_ID, test setup will create a real org via Better Auth API.
4
+ * because ensureDefaultOrg() creates this org during test setup. When
5
+ * DEFAULT_ORG_ID is removed, test setup will create a real org via the
6
+ * Better Auth API instead.
6
7
  */
7
8
  export const TEST_ORG_ID = DEFAULT_ORG_ID;
8
9
  /** Admin with wildcard permissions. Use for setup operations in beforeAll. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/core",
3
- "version": "0.1.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -35,15 +35,8 @@
35
35
  "types": "./src/drizzle.ts"
36
36
  }
37
37
  },
38
- "scripts": {
39
- "generate:plugins": "node ./scripts/generate-plugin-types.mjs",
40
- "check:store-guard": "node ./scripts/check-store-guard.mjs",
41
- "build": "rm -rf dist tsconfig.build.tsbuildinfo && npm run generate:plugins && npm run check:store-guard && tsc -p tsconfig.build.json",
42
- "check-types": "npm run generate:plugins && npm run check:store-guard && tsc --noEmit",
43
- "lint": "eslint . --max-warnings 1000",
44
- "test": "vitest run"
45
- },
46
38
  "dependencies": {
39
+ "@better-auth/api-key": "^1.3.8",
47
40
  "@better-auth/drizzle-adapter": "^1.3.8",
48
41
  "@hono/zod-openapi": "^1.2.2",
49
42
  "@scalar/hono-api-reference": "^0.10.5",
@@ -56,16 +49,15 @@
56
49
  "zod": "^4.1.11"
57
50
  },
58
51
  "devDependencies": {
59
- "@better-auth/api-key": "^1.5.4",
60
52
  "@electric-sql/pglite": "^0.3.15",
61
- "@repo/eslint-config": "*",
62
- "@repo/typescript-config": "*",
63
53
  "@types/node": "^25.5.0",
64
54
  "@vitest/coverage-v8": "^3.2.4",
65
55
  "drizzle-kit": "^0.31.9",
66
56
  "eslint": "^9.39.1",
67
57
  "typescript": "5.9.2",
68
- "vitest": "^3.2.4"
58
+ "vitest": "^3.2.4",
59
+ "@porulle/eslint-config": "0.1.0",
60
+ "@porulle/typescript-config": "0.1.0"
69
61
  },
70
62
  "publishConfig": {
71
63
  "access": "public"
@@ -85,5 +77,13 @@
85
77
  "url": "git+https://github.com/asyncdotengineering/porulle.git",
86
78
  "directory": "packages/core"
87
79
  },
88
- "author": "Porulle contributors"
89
- }
80
+ "author": "Porulle contributors",
81
+ "scripts": {
82
+ "generate:plugins": "node ./scripts/generate-plugin-types.mjs",
83
+ "check:store-guard": "node ./scripts/check-store-guard.mjs",
84
+ "build": "rm -rf dist tsconfig.build.tsbuildinfo && npm run generate:plugins && npm run check:store-guard && tsc -p tsconfig.build.json",
85
+ "check-types": "npm run generate:plugins && npm run check:store-guard && tsc --noEmit",
86
+ "lint": "eslint . --max-warnings 1000",
87
+ "test": "vitest run"
88
+ }
89
+ }
@@ -146,11 +146,28 @@ export function authMiddleware(
146
146
  auth.api.verifyApiKey
147
147
  ) {
148
148
  try {
149
+ // Resolve the key's configId from its prefix. Better Auth's apiKey
150
+ // plugin throws "No default api-key configuration found" when named
151
+ // scopes are configured and none is `default`/unset — and verifyApiKey
152
+ // only resolves a named scope (and enforces its configId match) when
153
+ // the configId is forwarded. Match the key's prefix to a configured
154
+ // scope so named-scope keys authenticate instead of silently 401-ing.
155
+ let configId: string | undefined;
156
+ const scopes = config.auth?.apiKeyScopes;
157
+ if (scopes) {
158
+ for (const [scopeId, scope] of Object.entries(scopes)) {
159
+ if (scope.prefix && apiKeyHeader.startsWith(scope.prefix)) {
160
+ configId = scopeId;
161
+ break;
162
+ }
163
+ }
164
+ }
165
+
149
166
  // Better Auth server-side calls require { body: { ... } } wrapper.
150
167
  // Returns { valid, error, key: Omit<ApiKey,"key"> | null }.
151
168
  // See: https://better-auth.com/docs/plugins/api-key/reference
152
169
  const result = await auth.api.verifyApiKey({
153
- body: { key: apiKeyHeader },
170
+ body: { key: apiKeyHeader, ...(configId ? { configId } : {}) },
154
171
  });
155
172
  if (result?.valid && result.key) {
156
173
  const apiKey = result.key as Record<string, unknown>;
@@ -3,6 +3,7 @@ import type { CommerceConfig, DefineConfigInput } from "./types.js";
3
3
  import type { TaskDefinition } from "../kernel/jobs/types.js";
4
4
  import { defaultKernelJobTasks } from "../kernel/jobs/builtin-job-tasks.js";
5
5
  import { _resetRegisteredPlugins } from "../kernel/plugin/manifest.js";
6
+ import { noopStorageAdapter } from "../modules/media/noop-adapter.js";
6
7
 
7
8
  function isRecord(value: unknown): value is Record<string, unknown> {
8
9
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -77,5 +78,11 @@ export async function defineConfig(
77
78
  jobs: mergeBuiltinJobTasks(config.jobs),
78
79
  };
79
80
 
81
+ // Storage is opt-in: default to a no-op adapter so catalog-only deployments
82
+ // boot with zero storage config. A real adapter (local/S3/R2) enables media.
83
+ if (!config.storage) {
84
+ config = { ...config, storage: noopStorageAdapter };
85
+ }
86
+
80
87
  return Object.freeze(config);
81
88
  }
@@ -306,9 +306,21 @@ export interface CommerceConfig {
306
306
  shipping?: ShippingConfig;
307
307
  payments?: PaymentAdapter[];
308
308
  storage?: StorageAdapter;
309
+ /**
310
+ * Runtime/edge overrides. `getClientIp` lets edge deployments (Workers,
311
+ * Vercel Edge, Fly) resolve the client IP from the platform header instead
312
+ * of the Node socket. Defaults to Node behavior.
313
+ */
314
+ runtime?: import("../runtime/client-ip.js").RuntimeConfig;
309
315
  media?: {
310
316
  allowedMimeTypes?: string[];
311
317
  allowSvg?: boolean;
318
+ /**
319
+ * Max request body size (bytes) for `POST /api/media/upload`. Phone photos
320
+ * are typically 3–8MB, so this path is exempt from the global 1MB body
321
+ * limit. Defaults to 10MB.
322
+ */
323
+ maxUploadSize?: number;
312
324
  };
313
325
  email?: {
314
326
  send(input: {
@@ -596,14 +596,14 @@ export const sendConfirmation: AfterHook<OrderResult> = async ({ result, context
596
596
  };
597
597
 
598
598
  /**
599
- * Analytics event recording — no-op since RFC-006.
599
+ * Analytics event recording — intentional no-op.
600
600
  *
601
601
  * The DrizzleAnalyticsAdapter queries source tables (orders, inventory)
602
- * directly via SQL. No separate event recording is needed.
603
- * Kept as an export for backwards compatibility with checkout route.
602
+ * directly via SQL, so no separate event recording is needed. The export
603
+ * is preserved for backwards compatibility with checkout route imports.
604
604
  */
605
605
  export const recordAnalyticsEvent: AfterHook<OrderResult> = async () => {
606
- // No-op: source tables ARE the analytics events (RFC-006)
606
+ // No-op: source tables ARE the analytics events.
607
607
  };
608
608
 
609
609
  /**
package/src/index.ts CHANGED
@@ -18,6 +18,8 @@ export { router } from "./interfaces/rest/router.js";
18
18
  export { webhookRouter, type WebhookRouterResult } from "./interfaces/rest/webhook-router.js";
19
19
  export { isPrivateUrl, isPrivateIp } from "./modules/webhooks/ssrf-guard.js";
20
20
  export { createServer } from "./runtime/server.js";
21
+ export { createClientIpResolver } from "./runtime/client-ip.js";
22
+ export type { ClientIpResolver, ClientIpContext, RuntimeConfig } from "./runtime/client-ip.js";
21
23
  export { createLogger } from "./runtime/logger.js";
22
24
  export type { Logger as PinoLogger } from "./runtime/logger.js";
23
25
  export { setupGracefulShutdown } from "./runtime/shutdown.js";
@@ -125,6 +127,7 @@ export type {
125
127
  PaymentWebhookEvent,
126
128
  } from "./modules/payments/adapter.js";
127
129
  export type { StorageAdapter } from "./modules/media/adapter.js";
130
+ export { noopStorageAdapter } from "./modules/media/noop-adapter.js";
128
131
  export type {
129
132
  SearchAdapter,
130
133
  SearchDocument,
@@ -154,9 +157,23 @@ export type {
154
157
  TaxVoidParams,
155
158
  } from "./modules/tax/adapter.js";
156
159
 
157
- export { getSchema, buildSchema, getTableNames } from "./kernel/database/migrate.js";
160
+ export {
161
+ getSchema,
162
+ buildSchema,
163
+ getTableNames,
164
+ getSchemaFiles,
165
+ pushSchema,
166
+ } from "./kernel/database/migrate.js";
158
167
  export { consoleEmailAdapter } from "./adapters/console-email.js";
159
168
 
169
+ export { promotionTypeEnum, type PromotionType } from "./modules/promotions/schemas.js";
170
+
171
+ export { parseJson, err } from "./interfaces/rest/parse-json.js";
172
+ export type { ValidationIssue, ErrorDetails } from "./interfaces/rest/parse-json.js";
173
+
174
+ export { auditMiddleware } from "./interfaces/rest/audit-middleware.js";
175
+ export type { AuditVars } from "./interfaces/rest/audit-middleware.js";
176
+
160
177
  export { runCompensationChain } from "./kernel/compensation/executor.js";
161
178
  export type {
162
179
  CompensationContext,