@porulle/core 0.10.5 → 0.10.8
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/dist/hooks/checkout.d.ts +2 -0
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +2 -0
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +19 -2
- package/dist/interfaces/rest/routes/payments.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/payments.js +48 -30
- package/dist/kernel/error-mapper.d.ts.map +1 -1
- package/dist/kernel/error-mapper.js +3 -0
- package/dist/kernel/hooks/executor.d.ts.map +1 -1
- package/dist/kernel/hooks/executor.js +18 -1
- package/dist/modules/orders/service.d.ts +2 -0
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +1 -0
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +8 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +8 -0
- package/package.json +1 -1
- package/src/hooks/checkout.ts +4 -0
- package/src/interfaces/rest/routes/checkout.ts +19 -2
- package/src/interfaces/rest/routes/payments.ts +55 -35
- package/src/kernel/error-mapper.ts +3 -0
- package/src/kernel/hooks/executor.ts +19 -1
- package/src/modules/orders/service.ts +3 -0
- package/src/runtime/server.ts +8 -0
- package/src/utils/id.ts +11 -0
package/dist/hooks/checkout.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface CheckoutLineItem {
|
|
|
42
42
|
}
|
|
43
43
|
export interface CheckoutData {
|
|
44
44
|
checkoutId: string;
|
|
45
|
+
/** Server-owned order id allocated before payment authorization. */
|
|
46
|
+
orderId?: string;
|
|
45
47
|
cartId: string;
|
|
46
48
|
customerId?: string;
|
|
47
49
|
customerGroupIds?: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../src/hooks/checkout.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../src/hooks/checkout.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks/checkout.js
CHANGED
|
@@ -219,6 +219,8 @@ export const authorizePayment = async ({ data, context, }) => {
|
|
|
219
219
|
? { idempotencyKey: data.idempotencyKey }
|
|
220
220
|
: {}),
|
|
221
221
|
metadata: {
|
|
222
|
+
...(data.orderId !== undefined ? { orderId: data.orderId } : {}),
|
|
223
|
+
organizationId: resolveOrgId(context.actor),
|
|
222
224
|
checkoutId: data.checkoutId,
|
|
223
225
|
cartId: data.cartId,
|
|
224
226
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAoBzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAoBzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;AAMhF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,EAC1C,KAAK,EAAE,KAAK,GAAG,IAAI,EACnB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmC7B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,gCAgV5C"}
|
|
@@ -6,7 +6,8 @@ import { createHookContext } from "../../../kernel/hooks/create-context.js";
|
|
|
6
6
|
import { mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
7
7
|
import { isCommerceError } from "../../../kernel/errors.js";
|
|
8
8
|
import { assertPermission } from "../../../auth/permissions.js";
|
|
9
|
-
import {
|
|
9
|
+
import { resolveOrgId } from "../../../auth/org.js";
|
|
10
|
+
import { makeDeterministicId, makeId } from "../../../utils/id.js";
|
|
10
11
|
/**
|
|
11
12
|
* SEC-07 — resolve the customer profile a checkout order is attributed to.
|
|
12
13
|
* A self-service actor may ONLY attribute the order to its own customer
|
|
@@ -93,8 +94,12 @@ export function checkoutRoutes(kernel) {
|
|
|
93
94
|
}, 409);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
97
|
+
const orderId = body.idempotencyKey
|
|
98
|
+
? await makeDeterministicId(`checkout:${resolveOrgId(actor)}:${body.idempotencyKey}`)
|
|
99
|
+
: makeId();
|
|
96
100
|
const checkoutData = {
|
|
97
101
|
checkoutId: makeId(),
|
|
102
|
+
orderId,
|
|
98
103
|
cartId: body.cartId,
|
|
99
104
|
currency: body.currency ?? "USD",
|
|
100
105
|
paymentMethodId: body.paymentMethodId,
|
|
@@ -162,6 +167,7 @@ export function checkoutRoutes(kernel) {
|
|
|
162
167
|
origin: "rest",
|
|
163
168
|
database: { db: kernel.database.db },
|
|
164
169
|
});
|
|
170
|
+
let pipelineStage = "validate-and-calculate";
|
|
165
171
|
try {
|
|
166
172
|
// Phase 1: DB transaction for validation — releases connection immediately after
|
|
167
173
|
const validated = await kernel.database.transaction(async (_tx) => {
|
|
@@ -170,13 +176,16 @@ export function checkoutRoutes(kernel) {
|
|
|
170
176
|
});
|
|
171
177
|
// Phase 2: Payment authorization — NO DB connection held while calling Stripe/etc.
|
|
172
178
|
// If Stripe takes 5s, the DB connection pool is not affected.
|
|
179
|
+
pipelineStage = "authorize-payment";
|
|
173
180
|
context.tx = null;
|
|
174
181
|
const processed = await runBeforeHooks(paymentHooks, validated, "create", context);
|
|
175
182
|
// SEC-07: resolve the order's customer server-side. A self-service actor
|
|
176
183
|
// can only attribute the order to its own profile; a client-supplied
|
|
177
184
|
// foreign customerId is ignored unless the actor is staff.
|
|
185
|
+
pipelineStage = "resolve-customer";
|
|
178
186
|
const customerUuid = await resolveCheckoutCustomerUuid(kernel.services.customers, actor, processed.customerId);
|
|
179
187
|
const orderPayload = {
|
|
188
|
+
id: processed.orderId ?? orderId,
|
|
180
189
|
...(body.idempotencyKey !== undefined
|
|
181
190
|
? { idempotencyKey: body.idempotencyKey }
|
|
182
191
|
: {}),
|
|
@@ -225,6 +234,7 @@ export function checkoutRoutes(kernel) {
|
|
|
225
234
|
// Checkout is a trusted, already-server-priced pipeline (resolveCurrentPrices
|
|
226
235
|
// + promotions/tax) and reserves stock in its own after-hooks — so it hands
|
|
227
236
|
// the order primitive precomputed totals rather than re-deriving them.
|
|
237
|
+
pipelineStage = "create-order";
|
|
228
238
|
const order = await kernel.services.orders.create(orderPayload, actor, undefined, {
|
|
229
239
|
trustedPricing: true,
|
|
230
240
|
});
|
|
@@ -232,6 +242,7 @@ export function checkoutRoutes(kernel) {
|
|
|
232
242
|
return c.json(mapErrorToResponse(order.error), mapErrorToStatus(order.error));
|
|
233
243
|
}
|
|
234
244
|
if (order.ok && (processed.appliedPromotions?.length ?? 0) > 0) {
|
|
245
|
+
pipelineStage = "record-promotion-usage";
|
|
235
246
|
await kernel.services.promotions.recordUsage({
|
|
236
247
|
promotions: processed.appliedPromotions ?? [],
|
|
237
248
|
organizationId: order.value.organizationId,
|
|
@@ -242,6 +253,7 @@ export function checkoutRoutes(kernel) {
|
|
|
242
253
|
});
|
|
243
254
|
}
|
|
244
255
|
if (order.ok) {
|
|
256
|
+
pipelineStage = "report-tax-transaction";
|
|
245
257
|
await kernel.services.tax.reportTransaction({
|
|
246
258
|
transactionId: order.value.id,
|
|
247
259
|
transactionDate: new Date(),
|
|
@@ -271,7 +283,9 @@ export function checkoutRoutes(kernel) {
|
|
|
271
283
|
}
|
|
272
284
|
// Stash paymentMethodId for completeCheckout compensation chain
|
|
273
285
|
context.context.paymentMethodId = processed.paymentMethodId;
|
|
286
|
+
pipelineStage = "complete-checkout";
|
|
274
287
|
const afterReport = await runAfterHooks(afterHooks, null, order.value, "create", context);
|
|
288
|
+
pipelineStage = "mark-cart-checked-out";
|
|
275
289
|
await kernel.services.cart.markAsCheckedOut(body.cartId, actor);
|
|
276
290
|
return c.json({
|
|
277
291
|
data: {
|
|
@@ -297,7 +311,10 @@ export function checkoutRoutes(kernel) {
|
|
|
297
311
|
});
|
|
298
312
|
});
|
|
299
313
|
// Always log the real error — hidden errors in checkout are unacceptable
|
|
300
|
-
const
|
|
314
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
315
|
+
const realMessage = rawMessage === "[object ErrorEvent]"
|
|
316
|
+
? `Checkout stage "${pipelineStage}" failed: ${rawMessage}`
|
|
317
|
+
: rawMessage;
|
|
301
318
|
const realStack = error instanceof Error ? error.stack : undefined;
|
|
302
319
|
console.error("[checkout] Pipeline failed:", { message: realMessage, stack: realStack, code: error?.code });
|
|
303
320
|
const message = isCommerceError(error)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payments.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/payments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"payments.d.ts","sourceRoot":"","sources":["../../../../src/interfaces/rest/routes/payments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,KAAK,MAAM,EAAwC,MAAM,aAAa,CAAC;AAOhF,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,gCAqE3C"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import { mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
3
3
|
import { processedWebhookEvents } from "../../../modules/webhooks/schema.js";
|
|
4
|
+
import { toCommerceError } from "../../../kernel/errors.js";
|
|
4
5
|
export function paymentRoutes(kernel) {
|
|
5
6
|
const router = new OpenAPIHono();
|
|
6
7
|
router.post("/webhook", async (c) => {
|
|
@@ -9,37 +10,54 @@ export function paymentRoutes(kernel) {
|
|
|
9
10
|
return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
10
11
|
}
|
|
11
12
|
const event = result.value;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
13
|
+
const outcome = await kernel.database.transaction(async (transaction) => {
|
|
14
|
+
const db = transaction;
|
|
15
|
+
// Claim and process in one transaction. If reconciliation throws, the
|
|
16
|
+
// claim rolls back so Stripe can safely retry instead of receiving a
|
|
17
|
+
// false 2xx for an event that never changed commerce state.
|
|
18
|
+
const [inserted] = await db
|
|
19
|
+
.insert(processedWebhookEvents)
|
|
20
|
+
.values({
|
|
21
|
+
eventId: event.id,
|
|
22
|
+
provider: "stripe",
|
|
23
|
+
eventType: event.type,
|
|
24
|
+
})
|
|
25
|
+
.onConflictDoNothing()
|
|
26
|
+
.returning({ id: processedWebhookEvents.id });
|
|
27
|
+
if (!inserted)
|
|
28
|
+
return { duplicate: true };
|
|
29
|
+
if (event.type === "payment_intent.succeeded") {
|
|
30
|
+
const data = event.data;
|
|
31
|
+
const metadata = data?.metadata;
|
|
32
|
+
if (typeof metadata?.orderId === "string") {
|
|
33
|
+
const actor = {
|
|
34
|
+
type: "api_key",
|
|
35
|
+
userId: "stripe-webhook",
|
|
36
|
+
email: null,
|
|
37
|
+
name: "Stripe webhook",
|
|
38
|
+
vendorId: null,
|
|
39
|
+
organizationId: typeof metadata.organizationId === "string"
|
|
40
|
+
? metadata.organizationId
|
|
41
|
+
: null,
|
|
42
|
+
role: "system",
|
|
43
|
+
permissions: ["orders:update"],
|
|
44
|
+
};
|
|
45
|
+
const changed = await kernel.services.orders.changeStatus({
|
|
46
|
+
orderId: metadata.orderId,
|
|
47
|
+
newStatus: "confirmed",
|
|
48
|
+
reason: "stripe_webhook_payment_intent_succeeded",
|
|
49
|
+
}, actor, {
|
|
50
|
+
tx: transaction,
|
|
51
|
+
actor,
|
|
52
|
+
requestId: c.get("requestId"),
|
|
53
|
+
});
|
|
54
|
+
if (!changed.ok)
|
|
55
|
+
throw toCommerceError(changed.error);
|
|
56
|
+
}
|
|
40
57
|
}
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
return { duplicate: false };
|
|
59
|
+
});
|
|
60
|
+
return c.json({ data: { received: true, ...(outcome.duplicate ? { duplicate: true } : {}) } });
|
|
43
61
|
});
|
|
44
62
|
return router;
|
|
45
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-mapper.d.ts","sourceRoot":"","sources":["../../src/kernel/error-mapper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"error-mapper.d.ts","sourceRoot":"","sources":["../../src/kernel/error-mapper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAenD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAGrE;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE;QAAE,KAAK,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IACnD,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AAaD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,OAAO,EACf,MAAM,CAAC,EAAE,MAAM,GACd,YAAY,CA8Ed"}
|
|
@@ -8,6 +8,9 @@ const statusByCode = {
|
|
|
8
8
|
CONFLICT: 409,
|
|
9
9
|
INVALID_TRANSITION: 422,
|
|
10
10
|
ORG_RESOLUTION_FAILED: 503,
|
|
11
|
+
WEBHOOK_SIGNATURE_MISSING: 400,
|
|
12
|
+
WEBHOOK_VERIFICATION_FAILED: 400,
|
|
13
|
+
WEBHOOK_SECRET_MISSING: 503,
|
|
11
14
|
};
|
|
12
15
|
export function mapErrorToStatus(error) {
|
|
13
16
|
const normalized = toCommerceError(error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/kernel/hooks/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAKzE;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/kernel/hooks/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAKzE;AAqCD,wBAAsB,cAAc,CAAC,CAAC,EACpC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,EACtB,IAAI,EAAE,CAAC,EACP,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,CAAC,CAmBZ;AAED,wBAAsB,aAAa,CAAC,CAAC,EACnC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EACrB,YAAY,EAAE,CAAC,GAAG,IAAI,EACtB,eAAe,EAAE,CAAC,EAClB,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,CA0BrB"}
|
|
@@ -13,6 +13,23 @@ function withTimeout(promiseOrValue, timeoutMs, hookName) {
|
|
|
13
13
|
promise.then((val) => { clearTimeout(timer); resolve(val); }, (err) => { clearTimeout(timer); reject(err); });
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
+
function actionableHookError(error, hookName) {
|
|
17
|
+
if (error instanceof Error && error.message && error.message !== "[object ErrorEvent]") {
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
const candidate = error;
|
|
21
|
+
const nested = candidate?.error ?? candidate?.cause;
|
|
22
|
+
const nestedMessage = nested instanceof Error
|
|
23
|
+
? nested.message
|
|
24
|
+
: nested && typeof nested === "object" && typeof nested.message === "string"
|
|
25
|
+
? String(nested.message)
|
|
26
|
+
: undefined;
|
|
27
|
+
const directMessage = typeof candidate?.message === "string" && candidate.message !== "[object ErrorEvent]"
|
|
28
|
+
? candidate.message
|
|
29
|
+
: undefined;
|
|
30
|
+
const message = nestedMessage || directMessage || String(error);
|
|
31
|
+
return new Error(`Before-hook "${hookName}" failed: ${message}`, { cause: error });
|
|
32
|
+
}
|
|
16
33
|
export async function runBeforeHooks(hooks, data, operation, context) {
|
|
17
34
|
let current = data;
|
|
18
35
|
for (const hook of hooks) {
|
|
@@ -25,7 +42,7 @@ export async function runBeforeHooks(hooks, data, operation, context) {
|
|
|
25
42
|
error: error instanceof Error ? error.message : String(error),
|
|
26
43
|
requestId: context.requestId,
|
|
27
44
|
});
|
|
28
|
-
throw error; //
|
|
45
|
+
throw actionableHookError(error, hookName); // Before hooks MUST succeed with actionable context.
|
|
29
46
|
}
|
|
30
47
|
}
|
|
31
48
|
return current;
|
|
@@ -7,6 +7,8 @@ import { type TxContext } from "../../kernel/database/tx-context.js";
|
|
|
7
7
|
import type { DatabaseAdapter } from "../../kernel/database/adapter.js";
|
|
8
8
|
import { OrdersRepository, type Order, type OrderLineItem, type OrderNote, type OrderRefund, type OrderStatusHistory } from "./repository/index.js";
|
|
9
9
|
export interface CreateOrderInput {
|
|
10
|
+
/** Server-allocated id for trusted pipelines that must reference the order before persistence. */
|
|
11
|
+
id?: string;
|
|
10
12
|
customerId?: string;
|
|
11
13
|
/** Client-supplied retry key — a repeat create with the same key returns the original order. */
|
|
12
14
|
idempotencyKey?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/orders/service.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/orders/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -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;AAQhD,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;AAOzD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACxD,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,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/runtime/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAQhD,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;AAOzD,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACxD,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,CAcvC;AAeD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,cAAc;;;;;sBAyVxB,MAAM,UAAU,MAAM;GA2FtD"}
|
package/dist/runtime/server.js
CHANGED
|
@@ -22,7 +22,15 @@ import { mapErrorToResponse } from "../kernel/error-mapper.js";
|
|
|
22
22
|
* Exposed for direct unit testing — see test/server-edge-runtime.test.ts.
|
|
23
23
|
*/
|
|
24
24
|
export function isNodeRuntime() {
|
|
25
|
+
const workerUserAgent = typeof navigator !== "undefined"
|
|
26
|
+
? navigator.userAgent
|
|
27
|
+
: undefined;
|
|
28
|
+
const hasWorkersSocketPair = "WebSocketPair" in globalThis;
|
|
29
|
+
if (workerUserAgent === "Cloudflare-Workers" || hasWorkersSocketPair) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
25
32
|
return (typeof process !== "undefined" &&
|
|
33
|
+
typeof process.versions?.node === "string" &&
|
|
26
34
|
typeof process.on === "function" &&
|
|
27
35
|
typeof process.exit === "function");
|
|
28
36
|
}
|
package/dist/utils/id.d.ts
CHANGED
package/dist/utils/id.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,IAAI,MAAM,CAE/B"}
|
|
1
|
+
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/utils/id.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,IAAI,MAAM,CAE/B;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASxE"}
|
package/dist/utils/id.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export function makeId() {
|
|
2
2
|
return crypto.randomUUID();
|
|
3
3
|
}
|
|
4
|
+
export async function makeDeterministicId(value) {
|
|
5
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)));
|
|
6
|
+
const bytes = digest.slice(0, 16);
|
|
7
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x80;
|
|
8
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
9
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
11
|
+
}
|
package/package.json
CHANGED
package/src/hooks/checkout.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface CheckoutLineItem {
|
|
|
57
57
|
|
|
58
58
|
export interface CheckoutData {
|
|
59
59
|
checkoutId: string;
|
|
60
|
+
/** Server-owned order id allocated before payment authorization. */
|
|
61
|
+
orderId?: string;
|
|
60
62
|
cartId: string;
|
|
61
63
|
customerId?: string;
|
|
62
64
|
customerGroupIds?: string[];
|
|
@@ -543,6 +545,8 @@ export const authorizePayment: BeforeHook<CheckoutData> = async ({
|
|
|
543
545
|
? { idempotencyKey: data.idempotencyKey }
|
|
544
546
|
: {}),
|
|
545
547
|
metadata: {
|
|
548
|
+
...(data.orderId !== undefined ? { orderId: data.orderId } : {}),
|
|
549
|
+
organizationId: resolveOrgId(context.actor),
|
|
546
550
|
checkoutId: data.checkoutId,
|
|
547
551
|
cartId: data.cartId,
|
|
548
552
|
},
|
|
@@ -22,7 +22,8 @@ import type { PluginDb } from "../../../kernel/database/plugin-types.js";
|
|
|
22
22
|
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
23
23
|
import { isCommerceError } from "../../../kernel/errors.js";
|
|
24
24
|
import { assertPermission } from "../../../auth/permissions.js";
|
|
25
|
-
import {
|
|
25
|
+
import { resolveOrgId } from "../../../auth/org.js";
|
|
26
|
+
import { makeDeterministicId, makeId } from "../../../utils/id.js";
|
|
26
27
|
import type { ShippingAddress } from "../../../modules/shipping/calculator.js";
|
|
27
28
|
import type { Actor } from "../../../auth/types.js";
|
|
28
29
|
|
|
@@ -126,8 +127,12 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
const orderId = body.idempotencyKey
|
|
131
|
+
? await makeDeterministicId(`checkout:${resolveOrgId(actor)}:${body.idempotencyKey}`)
|
|
132
|
+
: makeId();
|
|
129
133
|
const checkoutData: CheckoutData = {
|
|
130
134
|
checkoutId: makeId(),
|
|
135
|
+
orderId,
|
|
131
136
|
cartId: body.cartId,
|
|
132
137
|
currency: body.currency ?? "USD",
|
|
133
138
|
paymentMethodId: body.paymentMethodId,
|
|
@@ -200,6 +205,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
200
205
|
database: { db: kernel.database.db as PluginDb },
|
|
201
206
|
});
|
|
202
207
|
|
|
208
|
+
let pipelineStage = "validate-and-calculate";
|
|
203
209
|
try {
|
|
204
210
|
// Phase 1: DB transaction for validation — releases connection immediately after
|
|
205
211
|
const validated = await kernel.database.transaction(async (_tx) => {
|
|
@@ -214,6 +220,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
214
220
|
|
|
215
221
|
// Phase 2: Payment authorization — NO DB connection held while calling Stripe/etc.
|
|
216
222
|
// If Stripe takes 5s, the DB connection pool is not affected.
|
|
223
|
+
pipelineStage = "authorize-payment";
|
|
217
224
|
context.tx = null;
|
|
218
225
|
const processed = await runBeforeHooks(
|
|
219
226
|
paymentHooks,
|
|
@@ -225,6 +232,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
225
232
|
// SEC-07: resolve the order's customer server-side. A self-service actor
|
|
226
233
|
// can only attribute the order to its own profile; a client-supplied
|
|
227
234
|
// foreign customerId is ignored unless the actor is staff.
|
|
235
|
+
pipelineStage = "resolve-customer";
|
|
228
236
|
const customerUuid = await resolveCheckoutCustomerUuid(
|
|
229
237
|
kernel.services.customers,
|
|
230
238
|
actor,
|
|
@@ -232,6 +240,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
232
240
|
);
|
|
233
241
|
|
|
234
242
|
const orderPayload = {
|
|
243
|
+
id: processed.orderId ?? orderId,
|
|
235
244
|
...(body.idempotencyKey !== undefined
|
|
236
245
|
? { idempotencyKey: body.idempotencyKey }
|
|
237
246
|
: {}),
|
|
@@ -281,6 +290,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
281
290
|
// Checkout is a trusted, already-server-priced pipeline (resolveCurrentPrices
|
|
282
291
|
// + promotions/tax) and reserves stock in its own after-hooks — so it hands
|
|
283
292
|
// the order primitive precomputed totals rather than re-deriving them.
|
|
293
|
+
pipelineStage = "create-order";
|
|
284
294
|
const order = await kernel.services.orders.create(orderPayload, actor, undefined, {
|
|
285
295
|
trustedPricing: true,
|
|
286
296
|
});
|
|
@@ -293,6 +303,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
if (order.ok && (processed.appliedPromotions?.length ?? 0) > 0) {
|
|
306
|
+
pipelineStage = "record-promotion-usage";
|
|
296
307
|
await kernel.services.promotions.recordUsage({
|
|
297
308
|
promotions: processed.appliedPromotions ?? [],
|
|
298
309
|
organizationId: order.value.organizationId,
|
|
@@ -304,6 +315,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
304
315
|
}
|
|
305
316
|
|
|
306
317
|
if (order.ok) {
|
|
318
|
+
pipelineStage = "report-tax-transaction";
|
|
307
319
|
await kernel.services.tax.reportTransaction({
|
|
308
320
|
transactionId: order.value.id,
|
|
309
321
|
transactionDate: new Date(),
|
|
@@ -336,6 +348,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
336
348
|
// Stash paymentMethodId for completeCheckout compensation chain
|
|
337
349
|
context.context.paymentMethodId = processed.paymentMethodId;
|
|
338
350
|
|
|
351
|
+
pipelineStage = "complete-checkout";
|
|
339
352
|
const afterReport = await runAfterHooks(
|
|
340
353
|
afterHooks,
|
|
341
354
|
null,
|
|
@@ -344,6 +357,7 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
344
357
|
context,
|
|
345
358
|
);
|
|
346
359
|
|
|
360
|
+
pipelineStage = "mark-cart-checked-out";
|
|
347
361
|
await kernel.services.cart.markAsCheckedOut(body.cartId, actor);
|
|
348
362
|
|
|
349
363
|
return c.json(
|
|
@@ -374,7 +388,10 @@ export function checkoutRoutes(kernel: Kernel) {
|
|
|
374
388
|
},
|
|
375
389
|
);
|
|
376
390
|
// Always log the real error — hidden errors in checkout are unacceptable
|
|
377
|
-
const
|
|
391
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
392
|
+
const realMessage = rawMessage === "[object ErrorEvent]"
|
|
393
|
+
? `Checkout stage "${pipelineStage}" failed: ${rawMessage}`
|
|
394
|
+
: rawMessage;
|
|
378
395
|
const realStack = error instanceof Error ? error.stack : undefined;
|
|
379
396
|
console.error("[checkout] Pipeline failed:", { message: realMessage, stack: realStack, code: (error as Record<string, unknown>)?.code });
|
|
380
397
|
|
|
@@ -3,6 +3,8 @@ import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
|
3
3
|
import type { Kernel } from "../../../runtime/kernel.js";
|
|
4
4
|
import { type AppEnv, mapErrorToResponse, mapErrorToStatus } from "../utils.js";
|
|
5
5
|
import { processedWebhookEvents } from "../../../modules/webhooks/schema.js";
|
|
6
|
+
import type { Actor } from "../../../auth/types.js";
|
|
7
|
+
import { toCommerceError } from "../../../kernel/errors.js";
|
|
6
8
|
|
|
7
9
|
type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;
|
|
8
10
|
|
|
@@ -16,44 +18,62 @@ export function paymentRoutes(kernel: Kernel) {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const event = result.value;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Row already existed — this is a duplicate delivery
|
|
37
|
-
return c.json({ data: { received: true, duplicate: true } });
|
|
38
|
-
}
|
|
21
|
+
const outcome = await kernel.database.transaction(async (transaction) => {
|
|
22
|
+
const db = transaction as Db;
|
|
23
|
+
|
|
24
|
+
// Claim and process in one transaction. If reconciliation throws, the
|
|
25
|
+
// claim rolls back so Stripe can safely retry instead of receiving a
|
|
26
|
+
// false 2xx for an event that never changed commerce state.
|
|
27
|
+
const [inserted] = await db
|
|
28
|
+
.insert(processedWebhookEvents)
|
|
29
|
+
.values({
|
|
30
|
+
eventId: event.id,
|
|
31
|
+
provider: "stripe",
|
|
32
|
+
eventType: event.type,
|
|
33
|
+
})
|
|
34
|
+
.onConflictDoNothing()
|
|
35
|
+
.returning({ id: processedWebhookEvents.id });
|
|
36
|
+
|
|
37
|
+
if (!inserted) return { duplicate: true };
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
if (event.type === "payment_intent.succeeded") {
|
|
40
|
+
const data = event.data as Record<string, unknown> | undefined;
|
|
41
|
+
const metadata = data?.metadata as Record<string, unknown> | undefined;
|
|
42
|
+
if (typeof metadata?.orderId === "string") {
|
|
43
|
+
const actor: Actor = {
|
|
44
|
+
type: "api_key",
|
|
45
|
+
userId: "stripe-webhook",
|
|
46
|
+
email: null,
|
|
47
|
+
name: "Stripe webhook",
|
|
48
|
+
vendorId: null,
|
|
49
|
+
organizationId:
|
|
50
|
+
typeof metadata.organizationId === "string"
|
|
51
|
+
? metadata.organizationId
|
|
52
|
+
: null,
|
|
53
|
+
role: "system",
|
|
54
|
+
permissions: ["orders:update"],
|
|
55
|
+
};
|
|
56
|
+
const changed = await kernel.services.orders.changeStatus(
|
|
57
|
+
{
|
|
58
|
+
orderId: metadata.orderId,
|
|
59
|
+
newStatus: "confirmed",
|
|
60
|
+
reason: "stripe_webhook_payment_intent_succeeded",
|
|
61
|
+
},
|
|
62
|
+
actor,
|
|
63
|
+
{
|
|
64
|
+
tx: transaction,
|
|
65
|
+
actor,
|
|
66
|
+
requestId: c.get("requestId"),
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
if (!changed.ok) throw toCommerceError(changed.error);
|
|
70
|
+
}
|
|
53
71
|
}
|
|
54
|
-
}
|
|
55
72
|
|
|
56
|
-
|
|
73
|
+
return { duplicate: false };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return c.json({ data: { received: true, ...(outcome.duplicate ? { duplicate: true } : {}) } });
|
|
57
77
|
});
|
|
58
78
|
|
|
59
79
|
return router;
|
|
@@ -11,6 +11,9 @@ const statusByCode: Record<string, ContentfulStatusCode> = {
|
|
|
11
11
|
CONFLICT: 409,
|
|
12
12
|
INVALID_TRANSITION: 422,
|
|
13
13
|
ORG_RESOLUTION_FAILED: 503,
|
|
14
|
+
WEBHOOK_SIGNATURE_MISSING: 400,
|
|
15
|
+
WEBHOOK_VERIFICATION_FAILED: 400,
|
|
16
|
+
WEBHOOK_SECRET_MISSING: 503,
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
export function mapErrorToStatus(error: unknown): ContentfulStatusCode {
|
|
@@ -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; //
|
|
75
|
+
throw actionableHookError(error, hookName); // Before hooks MUST succeed with actionable context.
|
|
58
76
|
}
|
|
59
77
|
}
|
|
60
78
|
return current;
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
} from "./repository/index.js";
|
|
43
43
|
|
|
44
44
|
export interface CreateOrderInput {
|
|
45
|
+
/** Server-allocated id for trusted pipelines that must reference the order before persistence. */
|
|
46
|
+
id?: string;
|
|
45
47
|
customerId?: string;
|
|
46
48
|
/** Client-supplied retry key — a repeat create with the same key returns the original order. */
|
|
47
49
|
idempotencyKey?: string | undefined;
|
|
@@ -379,6 +381,7 @@ export class OrderService {
|
|
|
379
381
|
try {
|
|
380
382
|
order = await this.repo.create(
|
|
381
383
|
{
|
|
384
|
+
...(opts?.trustedPricing === true && processed.id != null ? { id: processed.id } : {}),
|
|
382
385
|
organizationId: orgId,
|
|
383
386
|
orderNumber,
|
|
384
387
|
status: "pending",
|
package/src/runtime/server.ts
CHANGED
|
@@ -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
|
);
|
package/src/utils/id.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
export function makeId(): string {
|
|
2
2
|
return crypto.randomUUID();
|
|
3
3
|
}
|
|
4
|
+
|
|
5
|
+
export async function makeDeterministicId(value: string): Promise<string> {
|
|
6
|
+
const digest = new Uint8Array(
|
|
7
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)),
|
|
8
|
+
);
|
|
9
|
+
const bytes = digest.slice(0, 16);
|
|
10
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x80;
|
|
11
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
12
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
13
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
14
|
+
}
|