@magicstoreai/hydrogen 0.1.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.
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ CACHE_TAGS: () => CACHE_TAGS,
24
+ SIGNATURE_TOLERANCE_SECONDS: () => SIGNATURE_TOLERANCE_SECONDS,
25
+ createWebhookHandler: () => createWebhookHandler,
26
+ nextCacheFetch: () => nextCacheFetch,
27
+ tagForPath: () => tagForPath,
28
+ tagsForTopic: () => tagsForTopic,
29
+ verifyWebhookSignature: () => verifyWebhookSignature
30
+ });
31
+ module.exports = __toCommonJS(server_exports);
32
+
33
+ // src/server/webhook.ts
34
+ var SIGNATURE_TOLERANCE_SECONDS = 300;
35
+ async function verifyWebhookSignature(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1e3)) {
36
+ if (!header || !secret) {
37
+ return false;
38
+ }
39
+ let timestamp = null;
40
+ const signatures = [];
41
+ for (const pair of header.split(",")) {
42
+ const separator = pair.indexOf("=");
43
+ const key = pair.slice(0, separator).trim();
44
+ const value = pair.slice(separator + 1).trim();
45
+ if (key === "t" && /^\d+$/.test(value)) {
46
+ timestamp = Number(value);
47
+ } else if (key === "v1" && value !== "") {
48
+ signatures.push(value.toLowerCase());
49
+ }
50
+ }
51
+ if (timestamp === null || signatures.length === 0 || Math.abs(nowSeconds - timestamp) > SIGNATURE_TOLERANCE_SECONDS) {
52
+ return false;
53
+ }
54
+ const expected = await hmacSha256Hex(secret, `${timestamp}.${rawBody}`);
55
+ return signatures.some((signature) => constantTimeEqual(signature, expected));
56
+ }
57
+ async function hmacSha256Hex(secret, message) {
58
+ const encoder = new TextEncoder();
59
+ const key = await globalThis.crypto.subtle.importKey(
60
+ "raw",
61
+ encoder.encode(secret),
62
+ { name: "HMAC", hash: "SHA-256" },
63
+ false,
64
+ ["sign"]
65
+ );
66
+ const signature = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(message));
67
+ return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
68
+ }
69
+ function constantTimeEqual(a, b) {
70
+ if (a.length !== b.length) {
71
+ return false;
72
+ }
73
+ let difference = 0;
74
+ for (let i = 0; i < a.length; i++) {
75
+ difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
76
+ }
77
+ return difference === 0;
78
+ }
79
+ function createWebhookHandler(options) {
80
+ const seen = /* @__PURE__ */ new Set();
81
+ return async (request) => {
82
+ const rawBody = await request.text();
83
+ const now = Math.floor((options.now?.() ?? Date.now()) / 1e3);
84
+ if (!await verifyWebhookSignature(
85
+ options.secret,
86
+ request.headers.get("X-MagicStore-Signature"),
87
+ rawBody,
88
+ now
89
+ )) {
90
+ return new Response("invalid signature", { status: 401 });
91
+ }
92
+ let event;
93
+ try {
94
+ event = JSON.parse(rawBody);
95
+ } catch {
96
+ return new Response("invalid body", { status: 400 });
97
+ }
98
+ if (typeof event?.id !== "string" || typeof event.topic !== "string") {
99
+ return new Response("invalid body", { status: 400 });
100
+ }
101
+ if (seen.has(event.id)) {
102
+ return new Response(null, { status: 200 });
103
+ }
104
+ seen.add(event.id);
105
+ if (seen.size > 1e3) {
106
+ seen.delete(seen.values().next().value);
107
+ }
108
+ for (const tag of tagsForTopic(event.topic)) {
109
+ await options.revalidateTag?.(tag, "max");
110
+ }
111
+ await options.onEvent?.(event);
112
+ return new Response(null, { status: 200 });
113
+ };
114
+ }
115
+ var CACHE_TAGS = {
116
+ shop: "magicstore:shop",
117
+ catalog: "magicstore:catalog",
118
+ pages: "magicstore:pages",
119
+ home: "magicstore:home"
120
+ };
121
+ function tagsForTopic(topic) {
122
+ switch (topic) {
123
+ case "SHOP_UPDATED":
124
+ return [CACHE_TAGS.shop];
125
+ case "CATALOG_UPDATED":
126
+ return [CACHE_TAGS.catalog];
127
+ case "PAGES_UPDATED":
128
+ return [CACHE_TAGS.pages];
129
+ case "HOME_UPDATED":
130
+ return [CACHE_TAGS.home];
131
+ default:
132
+ return [];
133
+ }
134
+ }
135
+
136
+ // src/server/cache.ts
137
+ function tagForPath(pathname) {
138
+ const path = pathname.replace(/^.*\/api\/v2\/storefront/, "");
139
+ if (path === "/shop" || path === "/contacts" || path === "/theme/section-schema") {
140
+ return CACHE_TAGS.shop;
141
+ }
142
+ if (path === "/home") {
143
+ return CACHE_TAGS.home;
144
+ }
145
+ if (path.startsWith("/pages") || path === "/menus/footer") {
146
+ return CACHE_TAGS.pages;
147
+ }
148
+ if (path.startsWith("/products") || path.startsWith("/collections") || path.startsWith("/menus/") || path === "/reels" || path === "/sitemap" || path === "/locations") {
149
+ return CACHE_TAGS.catalog;
150
+ }
151
+ return null;
152
+ }
153
+ function nextCacheFetch(options = {}) {
154
+ const base = options.fetch ?? globalThis.fetch;
155
+ return (input, init) => {
156
+ const request = input instanceof Request ? input : new Request(input, init);
157
+ const tag = request.method === "GET" && !request.headers.has("Authorization") ? tagForPath(new URL(request.url).pathname) : null;
158
+ const next = tag === null ? { cache: "no-store" } : { next: { tags: [tag], revalidate: options.revalidate ?? 3600 } };
159
+ return base(request, next);
160
+ };
161
+ }
162
+ // Annotate the CommonJS export names for ESM import in node:
163
+ 0 && (module.exports = {
164
+ CACHE_TAGS,
165
+ SIGNATURE_TOLERANCE_SECONDS,
166
+ createWebhookHandler,
167
+ nextCacheFetch,
168
+ tagForPath,
169
+ tagsForTopic,
170
+ verifyWebhookSignature
171
+ });
172
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/server/webhook.ts","../src/server/cache.ts"],"sourcesContent":["export { nextCacheFetch, tagForPath } from './server/cache';\nexport {\n CACHE_TAGS,\n SIGNATURE_TOLERANCE_SECONDS,\n createWebhookHandler,\n tagsForTopic,\n verifyWebhookSignature,\n} from './server/webhook';\nexport type { WebhookEvent, WebhookHandlerOptions, WebhookTopic } from './server/webhook';\n","/** Topics the platform sends today. New ones may be added — ignore what you do not know. */\nexport type WebhookTopic = 'SHOP_UPDATED' | 'CATALOG_UPDATED' | 'PAGES_UPDATED' | 'HOME_UPDATED';\n\nexport interface WebhookEvent {\n /** A ULID, the same across retries: deduplicate by it. */\n id: string;\n topic: WebhookTopic | (string & {});\n /** The shop's key. */\n shop: string;\n occurredAt: string;\n data: Record<string, unknown>;\n}\n\n/** How far the signature's timestamp may be from this clock (replay window). */\nexport const SIGNATURE_TOLERANCE_SECONDS = 300;\n\n/**\n * Verifies `X-MagicStore-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>` over\n * the RAW body, in constant time, with Web Crypto — so it runs in Node, edge runtimes and Workers.\n */\nexport async function verifyWebhookSignature(\n secret: string,\n header: string | null,\n rawBody: string,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): Promise<boolean> {\n if (!header || !secret) {\n return false;\n }\n let timestamp: number | null = null;\n const signatures: string[] = [];\n for (const pair of header.split(',')) {\n const separator = pair.indexOf('=');\n const key = pair.slice(0, separator).trim();\n const value = pair.slice(separator + 1).trim();\n if (key === 't' && /^\\d+$/.test(value)) {\n timestamp = Number(value);\n } else if (key === 'v1' && value !== '') {\n signatures.push(value.toLowerCase());\n }\n }\n if (\n timestamp === null ||\n signatures.length === 0 ||\n Math.abs(nowSeconds - timestamp) > SIGNATURE_TOLERANCE_SECONDS\n ) {\n return false;\n }\n const expected = await hmacSha256Hex(secret, `${timestamp}.${rawBody}`);\n return signatures.some((signature) => constantTimeEqual(signature, expected));\n}\n\nasync function hmacSha256Hex(secret: string, message: string): Promise<string> {\n const encoder = new TextEncoder();\n const key = await globalThis.crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature = await globalThis.crypto.subtle.sign('HMAC', key, encoder.encode(message));\n return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n let difference = 0;\n for (let i = 0; i < a.length; i++) {\n difference |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return difference === 0;\n}\n\nexport interface WebhookHandlerOptions {\n /** The endpoint's signing secret (`whsec_…`). */\n secret: string;\n /**\n * Called with each cache tag a topic invalidates — pass Next.js's `revalidateTag` as is: it gets\n * the `'max'` profile (serve stale while it refetches), which Next.js 16 requires.\n */\n revalidateTag?: (tag: string, profile: 'max') => void | Promise<void>;\n /** Anything else to do with a verified event. Runs after the tags are revalidated. */\n onEvent?: (event: WebhookEvent) => void | Promise<void>;\n /** For tests. */\n now?: () => number;\n}\n\n/**\n * A route handler for the platform's webhook: `export const POST = createWebhookHandler({ secret,\n * revalidateTag })` in `app/api/magicstore/webhook/route.ts`. Answers 401 on a bad signature, 400\n * on a body that is not an event, 200 otherwise — including for a topic it does not know. Events\n * seen before (same id, this instance) are acknowledged and skipped.\n */\nexport function createWebhookHandler(\n options: WebhookHandlerOptions,\n): (request: Request) => Promise<Response> {\n const seen = new Set<string>();\n\n return async (request) => {\n const rawBody = await request.text();\n const now = Math.floor((options.now?.() ?? Date.now()) / 1000);\n if (\n !(await verifyWebhookSignature(\n options.secret,\n request.headers.get('X-MagicStore-Signature'),\n rawBody,\n now,\n ))\n ) {\n return new Response('invalid signature', { status: 401 });\n }\n\n let event: WebhookEvent;\n try {\n event = JSON.parse(rawBody) as WebhookEvent;\n } catch {\n return new Response('invalid body', { status: 400 });\n }\n if (typeof event?.id !== 'string' || typeof event.topic !== 'string') {\n return new Response('invalid body', { status: 400 });\n }\n\n if (seen.has(event.id)) {\n return new Response(null, { status: 200 });\n }\n seen.add(event.id);\n if (seen.size > 1000) {\n seen.delete(seen.values().next().value as string);\n }\n\n for (const tag of tagsForTopic(event.topic)) {\n await options.revalidateTag?.(tag, 'max');\n }\n await options.onEvent?.(event);\n\n return new Response(null, { status: 200 });\n };\n}\n\n/** The cache tags a storefront gives what each topic invalidates. */\nexport const CACHE_TAGS = {\n shop: 'magicstore:shop',\n catalog: 'magicstore:catalog',\n pages: 'magicstore:pages',\n home: 'magicstore:home',\n} as const;\n\nexport function tagsForTopic(topic: string): string[] {\n switch (topic) {\n case 'SHOP_UPDATED':\n return [CACHE_TAGS.shop];\n case 'CATALOG_UPDATED':\n return [CACHE_TAGS.catalog];\n case 'PAGES_UPDATED':\n return [CACHE_TAGS.pages];\n case 'HOME_UPDATED':\n return [CACHE_TAGS.home];\n default:\n return [];\n }\n}\n","import { CACHE_TAGS } from './webhook';\n\n/**\n * The cache tag of an API path — what a webhook topic will invalidate. Personal and live data\n * (customer, cart, checkout, orders, search, stock-bearing listings are still catalog) get none:\n * they must never be cached across visitors.\n */\nexport function tagForPath(pathname: string): string | null {\n const path = pathname.replace(/^.*\\/api\\/v2\\/storefront/, '');\n if (path === '/shop' || path === '/contacts' || path === '/theme/section-schema') {\n return CACHE_TAGS.shop;\n }\n if (path === '/home') {\n return CACHE_TAGS.home;\n }\n if (path.startsWith('/pages') || path === '/menus/footer') {\n return CACHE_TAGS.pages;\n }\n if (\n path.startsWith('/products') ||\n path.startsWith('/collections') ||\n path.startsWith('/menus/') ||\n path === '/reels' ||\n path === '/sitemap' ||\n path === '/locations'\n ) {\n return CACHE_TAGS.catalog;\n }\n return null;\n}\n\n/**\n * A `fetch` for `createStorefrontClient` on a Next.js server: public GETs are cached under the tag\n * a webhook revalidates (and at most `revalidate` seconds); everything else is `no-store`.\n *\n * ```ts\n * const client = createStorefrontClient({ shopDomain, fetch: nextCacheFetch() });\n * ```\n */\nexport function nextCacheFetch(\n options: { revalidate?: number | false; fetch?: typeof globalThis.fetch } = {},\n): typeof globalThis.fetch {\n const base = options.fetch ?? globalThis.fetch;\n return (input, init) => {\n const request = input instanceof Request ? input : new Request(input, init);\n const tag =\n request.method === 'GET' && !request.headers.has('Authorization')\n ? tagForPath(new URL(request.url).pathname)\n : null;\n const next =\n tag === null\n ? { cache: 'no-store' as const }\n : { next: { tags: [tag], revalidate: options.revalidate ?? 3600 } };\n return base(request, next as RequestInit);\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The cache tag of an API path — what a webhook topic will invalidate. Personal and live data
3
+ * (customer, cart, checkout, orders, search, stock-bearing listings are still catalog) get none:
4
+ * they must never be cached across visitors.
5
+ */
6
+ declare function tagForPath(pathname: string): string | null;
7
+ /**
8
+ * A `fetch` for `createStorefrontClient` on a Next.js server: public GETs are cached under the tag
9
+ * a webhook revalidates (and at most `revalidate` seconds); everything else is `no-store`.
10
+ *
11
+ * ```ts
12
+ * const client = createStorefrontClient({ shopDomain, fetch: nextCacheFetch() });
13
+ * ```
14
+ */
15
+ declare function nextCacheFetch(options?: {
16
+ revalidate?: number | false;
17
+ fetch?: typeof globalThis.fetch;
18
+ }): typeof globalThis.fetch;
19
+
20
+ /** Topics the platform sends today. New ones may be added — ignore what you do not know. */
21
+ type WebhookTopic = 'SHOP_UPDATED' | 'CATALOG_UPDATED' | 'PAGES_UPDATED' | 'HOME_UPDATED';
22
+ interface WebhookEvent {
23
+ /** A ULID, the same across retries: deduplicate by it. */
24
+ id: string;
25
+ topic: WebhookTopic | (string & {});
26
+ /** The shop's key. */
27
+ shop: string;
28
+ occurredAt: string;
29
+ data: Record<string, unknown>;
30
+ }
31
+ /** How far the signature's timestamp may be from this clock (replay window). */
32
+ declare const SIGNATURE_TOLERANCE_SECONDS = 300;
33
+ /**
34
+ * Verifies `X-MagicStore-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>` over
35
+ * the RAW body, in constant time, with Web Crypto — so it runs in Node, edge runtimes and Workers.
36
+ */
37
+ declare function verifyWebhookSignature(secret: string, header: string | null, rawBody: string, nowSeconds?: number): Promise<boolean>;
38
+ interface WebhookHandlerOptions {
39
+ /** The endpoint's signing secret (`whsec_…`). */
40
+ secret: string;
41
+ /**
42
+ * Called with each cache tag a topic invalidates — pass Next.js's `revalidateTag` as is: it gets
43
+ * the `'max'` profile (serve stale while it refetches), which Next.js 16 requires.
44
+ */
45
+ revalidateTag?: (tag: string, profile: 'max') => void | Promise<void>;
46
+ /** Anything else to do with a verified event. Runs after the tags are revalidated. */
47
+ onEvent?: (event: WebhookEvent) => void | Promise<void>;
48
+ /** For tests. */
49
+ now?: () => number;
50
+ }
51
+ /**
52
+ * A route handler for the platform's webhook: `export const POST = createWebhookHandler({ secret,
53
+ * revalidateTag })` in `app/api/magicstore/webhook/route.ts`. Answers 401 on a bad signature, 400
54
+ * on a body that is not an event, 200 otherwise — including for a topic it does not know. Events
55
+ * seen before (same id, this instance) are acknowledged and skipped.
56
+ */
57
+ declare function createWebhookHandler(options: WebhookHandlerOptions): (request: Request) => Promise<Response>;
58
+ /** The cache tags a storefront gives what each topic invalidates. */
59
+ declare const CACHE_TAGS: {
60
+ readonly shop: "magicstore:shop";
61
+ readonly catalog: "magicstore:catalog";
62
+ readonly pages: "magicstore:pages";
63
+ readonly home: "magicstore:home";
64
+ };
65
+ declare function tagsForTopic(topic: string): string[];
66
+
67
+ export { CACHE_TAGS, SIGNATURE_TOLERANCE_SECONDS, type WebhookEvent, type WebhookHandlerOptions, type WebhookTopic, createWebhookHandler, nextCacheFetch, tagForPath, tagsForTopic, verifyWebhookSignature };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The cache tag of an API path — what a webhook topic will invalidate. Personal and live data
3
+ * (customer, cart, checkout, orders, search, stock-bearing listings are still catalog) get none:
4
+ * they must never be cached across visitors.
5
+ */
6
+ declare function tagForPath(pathname: string): string | null;
7
+ /**
8
+ * A `fetch` for `createStorefrontClient` on a Next.js server: public GETs are cached under the tag
9
+ * a webhook revalidates (and at most `revalidate` seconds); everything else is `no-store`.
10
+ *
11
+ * ```ts
12
+ * const client = createStorefrontClient({ shopDomain, fetch: nextCacheFetch() });
13
+ * ```
14
+ */
15
+ declare function nextCacheFetch(options?: {
16
+ revalidate?: number | false;
17
+ fetch?: typeof globalThis.fetch;
18
+ }): typeof globalThis.fetch;
19
+
20
+ /** Topics the platform sends today. New ones may be added — ignore what you do not know. */
21
+ type WebhookTopic = 'SHOP_UPDATED' | 'CATALOG_UPDATED' | 'PAGES_UPDATED' | 'HOME_UPDATED';
22
+ interface WebhookEvent {
23
+ /** A ULID, the same across retries: deduplicate by it. */
24
+ id: string;
25
+ topic: WebhookTopic | (string & {});
26
+ /** The shop's key. */
27
+ shop: string;
28
+ occurredAt: string;
29
+ data: Record<string, unknown>;
30
+ }
31
+ /** How far the signature's timestamp may be from this clock (replay window). */
32
+ declare const SIGNATURE_TOLERANCE_SECONDS = 300;
33
+ /**
34
+ * Verifies `X-MagicStore-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>` over
35
+ * the RAW body, in constant time, with Web Crypto — so it runs in Node, edge runtimes and Workers.
36
+ */
37
+ declare function verifyWebhookSignature(secret: string, header: string | null, rawBody: string, nowSeconds?: number): Promise<boolean>;
38
+ interface WebhookHandlerOptions {
39
+ /** The endpoint's signing secret (`whsec_…`). */
40
+ secret: string;
41
+ /**
42
+ * Called with each cache tag a topic invalidates — pass Next.js's `revalidateTag` as is: it gets
43
+ * the `'max'` profile (serve stale while it refetches), which Next.js 16 requires.
44
+ */
45
+ revalidateTag?: (tag: string, profile: 'max') => void | Promise<void>;
46
+ /** Anything else to do with a verified event. Runs after the tags are revalidated. */
47
+ onEvent?: (event: WebhookEvent) => void | Promise<void>;
48
+ /** For tests. */
49
+ now?: () => number;
50
+ }
51
+ /**
52
+ * A route handler for the platform's webhook: `export const POST = createWebhookHandler({ secret,
53
+ * revalidateTag })` in `app/api/magicstore/webhook/route.ts`. Answers 401 on a bad signature, 400
54
+ * on a body that is not an event, 200 otherwise — including for a topic it does not know. Events
55
+ * seen before (same id, this instance) are acknowledged and skipped.
56
+ */
57
+ declare function createWebhookHandler(options: WebhookHandlerOptions): (request: Request) => Promise<Response>;
58
+ /** The cache tags a storefront gives what each topic invalidates. */
59
+ declare const CACHE_TAGS: {
60
+ readonly shop: "magicstore:shop";
61
+ readonly catalog: "magicstore:catalog";
62
+ readonly pages: "magicstore:pages";
63
+ readonly home: "magicstore:home";
64
+ };
65
+ declare function tagsForTopic(topic: string): string[];
66
+
67
+ export { CACHE_TAGS, SIGNATURE_TOLERANCE_SECONDS, type WebhookEvent, type WebhookHandlerOptions, type WebhookTopic, createWebhookHandler, nextCacheFetch, tagForPath, tagsForTopic, verifyWebhookSignature };
package/dist/server.js ADDED
@@ -0,0 +1,139 @@
1
+ // src/server/webhook.ts
2
+ var SIGNATURE_TOLERANCE_SECONDS = 300;
3
+ async function verifyWebhookSignature(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1e3)) {
4
+ if (!header || !secret) {
5
+ return false;
6
+ }
7
+ let timestamp = null;
8
+ const signatures = [];
9
+ for (const pair of header.split(",")) {
10
+ const separator = pair.indexOf("=");
11
+ const key = pair.slice(0, separator).trim();
12
+ const value = pair.slice(separator + 1).trim();
13
+ if (key === "t" && /^\d+$/.test(value)) {
14
+ timestamp = Number(value);
15
+ } else if (key === "v1" && value !== "") {
16
+ signatures.push(value.toLowerCase());
17
+ }
18
+ }
19
+ if (timestamp === null || signatures.length === 0 || Math.abs(nowSeconds - timestamp) > SIGNATURE_TOLERANCE_SECONDS) {
20
+ return false;
21
+ }
22
+ const expected = await hmacSha256Hex(secret, `${timestamp}.${rawBody}`);
23
+ return signatures.some((signature) => constantTimeEqual(signature, expected));
24
+ }
25
+ async function hmacSha256Hex(secret, message) {
26
+ const encoder = new TextEncoder();
27
+ const key = await globalThis.crypto.subtle.importKey(
28
+ "raw",
29
+ encoder.encode(secret),
30
+ { name: "HMAC", hash: "SHA-256" },
31
+ false,
32
+ ["sign"]
33
+ );
34
+ const signature = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(message));
35
+ return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
36
+ }
37
+ function constantTimeEqual(a, b) {
38
+ if (a.length !== b.length) {
39
+ return false;
40
+ }
41
+ let difference = 0;
42
+ for (let i = 0; i < a.length; i++) {
43
+ difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
44
+ }
45
+ return difference === 0;
46
+ }
47
+ function createWebhookHandler(options) {
48
+ const seen = /* @__PURE__ */ new Set();
49
+ return async (request) => {
50
+ const rawBody = await request.text();
51
+ const now = Math.floor((options.now?.() ?? Date.now()) / 1e3);
52
+ if (!await verifyWebhookSignature(
53
+ options.secret,
54
+ request.headers.get("X-MagicStore-Signature"),
55
+ rawBody,
56
+ now
57
+ )) {
58
+ return new Response("invalid signature", { status: 401 });
59
+ }
60
+ let event;
61
+ try {
62
+ event = JSON.parse(rawBody);
63
+ } catch {
64
+ return new Response("invalid body", { status: 400 });
65
+ }
66
+ if (typeof event?.id !== "string" || typeof event.topic !== "string") {
67
+ return new Response("invalid body", { status: 400 });
68
+ }
69
+ if (seen.has(event.id)) {
70
+ return new Response(null, { status: 200 });
71
+ }
72
+ seen.add(event.id);
73
+ if (seen.size > 1e3) {
74
+ seen.delete(seen.values().next().value);
75
+ }
76
+ for (const tag of tagsForTopic(event.topic)) {
77
+ await options.revalidateTag?.(tag, "max");
78
+ }
79
+ await options.onEvent?.(event);
80
+ return new Response(null, { status: 200 });
81
+ };
82
+ }
83
+ var CACHE_TAGS = {
84
+ shop: "magicstore:shop",
85
+ catalog: "magicstore:catalog",
86
+ pages: "magicstore:pages",
87
+ home: "magicstore:home"
88
+ };
89
+ function tagsForTopic(topic) {
90
+ switch (topic) {
91
+ case "SHOP_UPDATED":
92
+ return [CACHE_TAGS.shop];
93
+ case "CATALOG_UPDATED":
94
+ return [CACHE_TAGS.catalog];
95
+ case "PAGES_UPDATED":
96
+ return [CACHE_TAGS.pages];
97
+ case "HOME_UPDATED":
98
+ return [CACHE_TAGS.home];
99
+ default:
100
+ return [];
101
+ }
102
+ }
103
+
104
+ // src/server/cache.ts
105
+ function tagForPath(pathname) {
106
+ const path = pathname.replace(/^.*\/api\/v2\/storefront/, "");
107
+ if (path === "/shop" || path === "/contacts" || path === "/theme/section-schema") {
108
+ return CACHE_TAGS.shop;
109
+ }
110
+ if (path === "/home") {
111
+ return CACHE_TAGS.home;
112
+ }
113
+ if (path.startsWith("/pages") || path === "/menus/footer") {
114
+ return CACHE_TAGS.pages;
115
+ }
116
+ if (path.startsWith("/products") || path.startsWith("/collections") || path.startsWith("/menus/") || path === "/reels" || path === "/sitemap" || path === "/locations") {
117
+ return CACHE_TAGS.catalog;
118
+ }
119
+ return null;
120
+ }
121
+ function nextCacheFetch(options = {}) {
122
+ const base = options.fetch ?? globalThis.fetch;
123
+ return (input, init) => {
124
+ const request = input instanceof Request ? input : new Request(input, init);
125
+ const tag = request.method === "GET" && !request.headers.has("Authorization") ? tagForPath(new URL(request.url).pathname) : null;
126
+ const next = tag === null ? { cache: "no-store" } : { next: { tags: [tag], revalidate: options.revalidate ?? 3600 } };
127
+ return base(request, next);
128
+ };
129
+ }
130
+ export {
131
+ CACHE_TAGS,
132
+ SIGNATURE_TOLERANCE_SECONDS,
133
+ createWebhookHandler,
134
+ nextCacheFetch,
135
+ tagForPath,
136
+ tagsForTopic,
137
+ verifyWebhookSignature
138
+ };
139
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/webhook.ts","../src/server/cache.ts"],"sourcesContent":["/** Topics the platform sends today. New ones may be added — ignore what you do not know. */\nexport type WebhookTopic = 'SHOP_UPDATED' | 'CATALOG_UPDATED' | 'PAGES_UPDATED' | 'HOME_UPDATED';\n\nexport interface WebhookEvent {\n /** A ULID, the same across retries: deduplicate by it. */\n id: string;\n topic: WebhookTopic | (string & {});\n /** The shop's key. */\n shop: string;\n occurredAt: string;\n data: Record<string, unknown>;\n}\n\n/** How far the signature's timestamp may be from this clock (replay window). */\nexport const SIGNATURE_TOLERANCE_SECONDS = 300;\n\n/**\n * Verifies `X-MagicStore-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>` over\n * the RAW body, in constant time, with Web Crypto — so it runs in Node, edge runtimes and Workers.\n */\nexport async function verifyWebhookSignature(\n secret: string,\n header: string | null,\n rawBody: string,\n nowSeconds: number = Math.floor(Date.now() / 1000),\n): Promise<boolean> {\n if (!header || !secret) {\n return false;\n }\n let timestamp: number | null = null;\n const signatures: string[] = [];\n for (const pair of header.split(',')) {\n const separator = pair.indexOf('=');\n const key = pair.slice(0, separator).trim();\n const value = pair.slice(separator + 1).trim();\n if (key === 't' && /^\\d+$/.test(value)) {\n timestamp = Number(value);\n } else if (key === 'v1' && value !== '') {\n signatures.push(value.toLowerCase());\n }\n }\n if (\n timestamp === null ||\n signatures.length === 0 ||\n Math.abs(nowSeconds - timestamp) > SIGNATURE_TOLERANCE_SECONDS\n ) {\n return false;\n }\n const expected = await hmacSha256Hex(secret, `${timestamp}.${rawBody}`);\n return signatures.some((signature) => constantTimeEqual(signature, expected));\n}\n\nasync function hmacSha256Hex(secret: string, message: string): Promise<string> {\n const encoder = new TextEncoder();\n const key = await globalThis.crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const signature = await globalThis.crypto.subtle.sign('HMAC', key, encoder.encode(message));\n return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n let difference = 0;\n for (let i = 0; i < a.length; i++) {\n difference |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return difference === 0;\n}\n\nexport interface WebhookHandlerOptions {\n /** The endpoint's signing secret (`whsec_…`). */\n secret: string;\n /**\n * Called with each cache tag a topic invalidates — pass Next.js's `revalidateTag` as is: it gets\n * the `'max'` profile (serve stale while it refetches), which Next.js 16 requires.\n */\n revalidateTag?: (tag: string, profile: 'max') => void | Promise<void>;\n /** Anything else to do with a verified event. Runs after the tags are revalidated. */\n onEvent?: (event: WebhookEvent) => void | Promise<void>;\n /** For tests. */\n now?: () => number;\n}\n\n/**\n * A route handler for the platform's webhook: `export const POST = createWebhookHandler({ secret,\n * revalidateTag })` in `app/api/magicstore/webhook/route.ts`. Answers 401 on a bad signature, 400\n * on a body that is not an event, 200 otherwise — including for a topic it does not know. Events\n * seen before (same id, this instance) are acknowledged and skipped.\n */\nexport function createWebhookHandler(\n options: WebhookHandlerOptions,\n): (request: Request) => Promise<Response> {\n const seen = new Set<string>();\n\n return async (request) => {\n const rawBody = await request.text();\n const now = Math.floor((options.now?.() ?? Date.now()) / 1000);\n if (\n !(await verifyWebhookSignature(\n options.secret,\n request.headers.get('X-MagicStore-Signature'),\n rawBody,\n now,\n ))\n ) {\n return new Response('invalid signature', { status: 401 });\n }\n\n let event: WebhookEvent;\n try {\n event = JSON.parse(rawBody) as WebhookEvent;\n } catch {\n return new Response('invalid body', { status: 400 });\n }\n if (typeof event?.id !== 'string' || typeof event.topic !== 'string') {\n return new Response('invalid body', { status: 400 });\n }\n\n if (seen.has(event.id)) {\n return new Response(null, { status: 200 });\n }\n seen.add(event.id);\n if (seen.size > 1000) {\n seen.delete(seen.values().next().value as string);\n }\n\n for (const tag of tagsForTopic(event.topic)) {\n await options.revalidateTag?.(tag, 'max');\n }\n await options.onEvent?.(event);\n\n return new Response(null, { status: 200 });\n };\n}\n\n/** The cache tags a storefront gives what each topic invalidates. */\nexport const CACHE_TAGS = {\n shop: 'magicstore:shop',\n catalog: 'magicstore:catalog',\n pages: 'magicstore:pages',\n home: 'magicstore:home',\n} as const;\n\nexport function tagsForTopic(topic: string): string[] {\n switch (topic) {\n case 'SHOP_UPDATED':\n return [CACHE_TAGS.shop];\n case 'CATALOG_UPDATED':\n return [CACHE_TAGS.catalog];\n case 'PAGES_UPDATED':\n return [CACHE_TAGS.pages];\n case 'HOME_UPDATED':\n return [CACHE_TAGS.home];\n default:\n return [];\n }\n}\n","import { CACHE_TAGS } from './webhook';\n\n/**\n * The cache tag of an API path — what a webhook topic will invalidate. Personal and live data\n * (customer, cart, checkout, orders, search, stock-bearing listings are still catalog) get none:\n * they must never be cached across visitors.\n */\nexport function tagForPath(pathname: string): string | null {\n const path = pathname.replace(/^.*\\/api\\/v2\\/storefront/, '');\n if (path === '/shop' || path === '/contacts' || path === '/theme/section-schema') {\n return CACHE_TAGS.shop;\n }\n if (path === '/home') {\n return CACHE_TAGS.home;\n }\n if (path.startsWith('/pages') || path === '/menus/footer') {\n return CACHE_TAGS.pages;\n }\n if (\n path.startsWith('/products') ||\n path.startsWith('/collections') ||\n path.startsWith('/menus/') ||\n path === '/reels' ||\n path === '/sitemap' ||\n path === '/locations'\n ) {\n return CACHE_TAGS.catalog;\n }\n return null;\n}\n\n/**\n * A `fetch` for `createStorefrontClient` on a Next.js server: public GETs are cached under the tag\n * a webhook revalidates (and at most `revalidate` seconds); everything else is `no-store`.\n *\n * ```ts\n * const client = createStorefrontClient({ shopDomain, fetch: nextCacheFetch() });\n * ```\n */\nexport function nextCacheFetch(\n options: { revalidate?: number | false; fetch?: typeof globalThis.fetch } = {},\n): typeof globalThis.fetch {\n const base = options.fetch ?? globalThis.fetch;\n return (input, init) => {\n const request = input instanceof Request ? input : new Request(input, init);\n const tag =\n request.method === 'GET' && !request.headers.has('Authorization')\n ? tagForPath(new URL(request.url).pathname)\n : null;\n const next =\n tag === null\n ? { cache: 'no-store' as const }\n : { next: { tags: [tag], revalidate: options.revalidate ?? 3600 } };\n return base(request, next as RequestInit);\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@magicstoreai/hydrogen",
3
+ "version": "0.1.0",
4
+ "description": "React building blocks for MagicStore storefronts: shop, customer, cart, wishlist, money, analytics, SEO and Next.js cache helpers",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "./core": {
15
+ "types": "./dist/core.d.ts",
16
+ "import": "./dist/core.js",
17
+ "require": "./dist/core.cjs"
18
+ },
19
+ "./server": {
20
+ "types": "./dist/server.d.ts",
21
+ "import": "./dist/server.js",
22
+ "require": "./dist/server.cjs"
23
+ },
24
+ "./seo": {
25
+ "types": "./dist/seo.d.ts",
26
+ "import": "./dist/seo.js",
27
+ "require": "./dist/seo.cjs"
28
+ }
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist",
35
+ "CATALOGUE.md"
36
+ ],
37
+ "peerDependencies": {
38
+ "react": "^18.3.0 || ^19.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@testing-library/react": "^16.3.3",
42
+ "@types/node": "^26.6.2",
43
+ "@types/react": "^19.3.0",
44
+ "@types/react-dom": "^19.3.0",
45
+ "jsdom": "^30.1.1",
46
+ "react": "^19.3.0",
47
+ "react-dom": "^19.3.0",
48
+ "tsup": "^8.5.1",
49
+ "typescript": "5.9.3",
50
+ "vitest": "^5.0.1"
51
+ },
52
+ "dependencies": {
53
+ "@magicstoreai/storefront-client": "0.1.0"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "scripts": {
59
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
60
+ "catalogue": "node scripts/catalogue.mjs",
61
+ "test": "vitest run",
62
+ "typecheck": "tsc --noEmit"
63
+ }
64
+ }