@porulle/core 0.10.5 → 0.10.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/core",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -200,6 +200,7 @@ export function checkoutRoutes(kernel: Kernel) {
200
200
  database: { db: kernel.database.db as PluginDb },
201
201
  });
202
202
 
203
+ let pipelineStage = "validate-and-calculate";
203
204
  try {
204
205
  // Phase 1: DB transaction for validation — releases connection immediately after
205
206
  const validated = await kernel.database.transaction(async (_tx) => {
@@ -214,6 +215,7 @@ export function checkoutRoutes(kernel: Kernel) {
214
215
 
215
216
  // Phase 2: Payment authorization — NO DB connection held while calling Stripe/etc.
216
217
  // If Stripe takes 5s, the DB connection pool is not affected.
218
+ pipelineStage = "authorize-payment";
217
219
  context.tx = null;
218
220
  const processed = await runBeforeHooks(
219
221
  paymentHooks,
@@ -225,6 +227,7 @@ export function checkoutRoutes(kernel: Kernel) {
225
227
  // SEC-07: resolve the order's customer server-side. A self-service actor
226
228
  // can only attribute the order to its own profile; a client-supplied
227
229
  // foreign customerId is ignored unless the actor is staff.
230
+ pipelineStage = "resolve-customer";
228
231
  const customerUuid = await resolveCheckoutCustomerUuid(
229
232
  kernel.services.customers,
230
233
  actor,
@@ -281,6 +284,7 @@ export function checkoutRoutes(kernel: Kernel) {
281
284
  // Checkout is a trusted, already-server-priced pipeline (resolveCurrentPrices
282
285
  // + promotions/tax) and reserves stock in its own after-hooks — so it hands
283
286
  // the order primitive precomputed totals rather than re-deriving them.
287
+ pipelineStage = "create-order";
284
288
  const order = await kernel.services.orders.create(orderPayload, actor, undefined, {
285
289
  trustedPricing: true,
286
290
  });
@@ -293,6 +297,7 @@ export function checkoutRoutes(kernel: Kernel) {
293
297
  }
294
298
 
295
299
  if (order.ok && (processed.appliedPromotions?.length ?? 0) > 0) {
300
+ pipelineStage = "record-promotion-usage";
296
301
  await kernel.services.promotions.recordUsage({
297
302
  promotions: processed.appliedPromotions ?? [],
298
303
  organizationId: order.value.organizationId,
@@ -304,6 +309,7 @@ export function checkoutRoutes(kernel: Kernel) {
304
309
  }
305
310
 
306
311
  if (order.ok) {
312
+ pipelineStage = "report-tax-transaction";
307
313
  await kernel.services.tax.reportTransaction({
308
314
  transactionId: order.value.id,
309
315
  transactionDate: new Date(),
@@ -336,6 +342,7 @@ export function checkoutRoutes(kernel: Kernel) {
336
342
  // Stash paymentMethodId for completeCheckout compensation chain
337
343
  context.context.paymentMethodId = processed.paymentMethodId;
338
344
 
345
+ pipelineStage = "complete-checkout";
339
346
  const afterReport = await runAfterHooks(
340
347
  afterHooks,
341
348
  null,
@@ -344,6 +351,7 @@ export function checkoutRoutes(kernel: Kernel) {
344
351
  context,
345
352
  );
346
353
 
354
+ pipelineStage = "mark-cart-checked-out";
347
355
  await kernel.services.cart.markAsCheckedOut(body.cartId, actor);
348
356
 
349
357
  return c.json(
@@ -374,7 +382,10 @@ export function checkoutRoutes(kernel: Kernel) {
374
382
  },
375
383
  );
376
384
  // Always log the real error — hidden errors in checkout are unacceptable
377
- const realMessage = error instanceof Error ? error.message : String(error);
385
+ const rawMessage = error instanceof Error ? error.message : String(error);
386
+ const realMessage = rawMessage === "[object ErrorEvent]"
387
+ ? `Checkout stage "${pipelineStage}" failed: ${rawMessage}`
388
+ : rawMessage;
378
389
  const realStack = error instanceof Error ? error.stack : undefined;
379
390
  console.error("[checkout] Pipeline failed:", { message: realMessage, stack: realStack, code: (error as Record<string, unknown>)?.code });
380
391
 
@@ -34,6 +34,24 @@ function withTimeout<T>(promiseOrValue: Promise<T> | T, timeoutMs: number, hookN
34
34
  });
35
35
  }
36
36
 
37
+ function actionableHookError(error: unknown, hookName: string): Error {
38
+ if (error instanceof Error && error.message && error.message !== "[object ErrorEvent]") {
39
+ return error;
40
+ }
41
+ const candidate = error as { error?: unknown; cause?: unknown; message?: unknown } | null;
42
+ const nested = candidate?.error ?? candidate?.cause;
43
+ const nestedMessage = nested instanceof Error
44
+ ? nested.message
45
+ : nested && typeof nested === "object" && typeof (nested as { message?: unknown }).message === "string"
46
+ ? String((nested as { message: string }).message)
47
+ : undefined;
48
+ const directMessage = typeof candidate?.message === "string" && candidate.message !== "[object ErrorEvent]"
49
+ ? candidate.message
50
+ : undefined;
51
+ const message = nestedMessage || directMessage || String(error);
52
+ return new Error(`Before-hook "${hookName}" failed: ${message}`, { cause: error });
53
+ }
54
+
37
55
  export async function runBeforeHooks<T>(
38
56
  hooks: BeforeHook<T>[],
39
57
  data: T,
@@ -54,7 +72,7 @@ export async function runBeforeHooks<T>(
54
72
  error: error instanceof Error ? error.message : String(error),
55
73
  requestId: context.requestId,
56
74
  });
57
- throw error; // Re-throw beforeHooks MUST succeed
75
+ throw actionableHookError(error, hookName); // Before hooks MUST succeed with actionable context.
58
76
  }
59
77
  }
60
78
  return current;
@@ -39,8 +39,16 @@ type ServerEnv = {
39
39
  * Exposed for direct unit testing — see test/server-edge-runtime.test.ts.
40
40
  */
41
41
  export function isNodeRuntime(): boolean {
42
+ const workerUserAgent = typeof navigator !== "undefined"
43
+ ? navigator.userAgent
44
+ : undefined;
45
+ const hasWorkersSocketPair = "WebSocketPair" in globalThis;
46
+ if (workerUserAgent === "Cloudflare-Workers" || hasWorkersSocketPair) {
47
+ return false;
48
+ }
42
49
  return (
43
50
  typeof process !== "undefined" &&
51
+ typeof process.versions?.node === "string" &&
44
52
  typeof process.on === "function" &&
45
53
  typeof process.exit === "function"
46
54
  );