@revenexx/cover 0.1.8 → 0.1.9
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/nuxt.config.ts +3 -0
- package/package.json +1 -1
- package/server/utils/checkoutSession.ts +55 -17
package/nuxt.config.ts
CHANGED
|
@@ -72,6 +72,9 @@ export default defineNuxtConfig({
|
|
|
72
72
|
typesensePort: 8108,
|
|
73
73
|
typesenseProtocol: "http",
|
|
74
74
|
typesenseApiKey: "xyz",
|
|
75
|
+
// HMAC secret for the stateless checkout session tokens — override
|
|
76
|
+
// via NUXT_CHECKOUT_SESSION_SECRET in multi-tenant deployments.
|
|
77
|
+
checkoutSessionSecret: "",
|
|
75
78
|
public: {
|
|
76
79
|
// Cart state keys the layer's stores read — apps may override, but
|
|
77
80
|
// the layer must ship working defaults (an undefined TTL turns the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/cover",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Cover \u2014 revenexx design system for Nuxt. Distributed as a Nuxt layer: generic UI components, theming tokens and stores shared by the demo shop, custom storefronts and the Blokkli theme.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -1,38 +1,76 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
const TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Stateless, HMAC-signed checkout session tokens: `{expiry}.{nonce}.{sig}`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Deployed sites run multiple runtime containers — an in-memory token store
|
|
9
|
+
* only validates on the instance that issued the token. A signed token
|
|
10
|
+
* validates on every instance without shared state. Strict one-time use is
|
|
11
|
+
* therefore best-effort (per-instance replay cache); order dedup is anchored
|
|
12
|
+
* by the payments app's idempotency key, which is this same token.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* The signing secret is shared by all instances: NUXT_CHECKOUT_SESSION_SECRET
|
|
15
|
+
* (runtimeConfig.checkoutSessionSecret), falling back to the tenant API key.
|
|
14
16
|
*/
|
|
15
|
-
|
|
17
|
+
function sessionSecret(): string {
|
|
18
|
+
const config = useRuntimeConfig();
|
|
19
|
+
return (config.checkoutSessionSecret as string)
|
|
20
|
+
|| (config.revenexxApiKey as string)
|
|
21
|
+
|| "cover-checkout-session";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sign(payload: string): string {
|
|
25
|
+
return createHmac("sha256", sessionSecret()).update(payload).digest("hex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Per-instance replay cache (best-effort one-time use). */
|
|
29
|
+
const consumed = new Map<string, number>();
|
|
30
|
+
|
|
31
|
+
function pruneConsumed(): void {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
for (const [token, expiry] of consumed) {
|
|
34
|
+
if (now > expiry) {
|
|
35
|
+
consumed.delete(token);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
16
39
|
|
|
17
40
|
export function createCheckoutSession(): string {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
41
|
+
const expiry = Date.now() + TTL_MS;
|
|
42
|
+
const nonce = randomBytes(16).toString("hex");
|
|
43
|
+
const payload = `${expiry}.${nonce}`;
|
|
44
|
+
return `${payload}.${sign(payload)}`;
|
|
21
45
|
}
|
|
22
46
|
|
|
23
47
|
/**
|
|
24
|
-
* Validates
|
|
25
|
-
*
|
|
48
|
+
* Validates a checkout session token (signature + expiry) and marks it
|
|
49
|
+
* consumed on this instance.
|
|
26
50
|
*/
|
|
27
51
|
export function consumeCheckoutSession(token: string): boolean {
|
|
28
52
|
if (!token) {
|
|
29
53
|
return false;
|
|
30
54
|
}
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
|
|
55
|
+
const parts = token.split(".");
|
|
56
|
+
if (parts.length !== 3) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const [expiryRaw, nonce, signature] = parts as [string, string, string];
|
|
60
|
+
const expected = sign(`${expiryRaw}.${nonce}`);
|
|
61
|
+
const a = Buffer.from(signature, "utf8");
|
|
62
|
+
const b = Buffer.from(expected, "utf8");
|
|
63
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const expiry = Number(expiryRaw);
|
|
67
|
+
if (!Number.isFinite(expiry) || Date.now() > expiry) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (consumed.has(token)) {
|
|
34
71
|
return false;
|
|
35
72
|
}
|
|
36
|
-
|
|
73
|
+
pruneConsumed();
|
|
74
|
+
consumed.set(token, expiry);
|
|
37
75
|
return true;
|
|
38
76
|
}
|