@spindlecommerce/storefront-client 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +27 -2
- package/dist/builder.d.ts +6 -0
- package/dist/builder.js +5 -0
- package/dist/cart.d.ts +105 -0
- package/dist/cart.js +230 -0
- package/dist/client.d.ts +102 -0
- package/dist/client.js +108 -0
- package/dist/documents.d.ts +39 -0
- package/dist/documents.js +217 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +20 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/media.d.ts +2 -0
- package/dist/media.js +2 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +25 -0
- package/dist/prices.d.ts +4 -0
- package/dist/prices.js +3 -0
- package/dist/types.d.ts +455 -0
- package/dist/types.js +2 -0
- package/dist/variants.d.ts +29 -0
- package/dist/variants.js +26 -0
- package/dist/visual-editing.d.ts +47 -0
- package/dist/visual-editing.js +23 -0
- package/dist/webhooks.d.ts +52 -0
- package/dist/webhooks.js +80 -0
- package/package.json +37 -3
- package/schema.graphql +1453 -0
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Older or newer signatures are refused, so a captured request cannot be replayed. */
|
|
2
|
+
export const SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
/** Checks the signature over the exact text received. Verify before parsing. */
|
|
5
|
+
export const verifyWebhookSignature = async (secret, body, header, nowSeconds = Date.now() / 1000) => {
|
|
6
|
+
if (!header)
|
|
7
|
+
return false;
|
|
8
|
+
const parts = new Map(header.split(",").map((part) => {
|
|
9
|
+
const at = part.indexOf("=");
|
|
10
|
+
return [part.slice(0, at).trim(), part.slice(at + 1).trim()];
|
|
11
|
+
}));
|
|
12
|
+
const timestamp = Number(parts.get("t"));
|
|
13
|
+
const signature = parts.get("v1") ?? "";
|
|
14
|
+
if (!Number.isInteger(timestamp) || Math.abs(nowSeconds - timestamp) > SIGNATURE_TOLERANCE_SECONDS)
|
|
15
|
+
return false;
|
|
16
|
+
if (!/^[0-9a-f]{64}$/i.test(signature))
|
|
17
|
+
return false;
|
|
18
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
19
|
+
const bytes = Uint8Array.from(signature.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));
|
|
20
|
+
// Constant time, so timing does not reveal how much of a guess matched.
|
|
21
|
+
return crypto.subtle.verify("HMAC", key, bytes, encoder.encode(`${timestamp}.${body}`));
|
|
22
|
+
};
|
|
23
|
+
/** A verified body as an event; null when it is not one. */
|
|
24
|
+
export const parseWebhookEvent = (body) => {
|
|
25
|
+
try {
|
|
26
|
+
const event = JSON.parse(body);
|
|
27
|
+
return typeof event?.type === "string" && typeof event.data === "object" && event.data !== null ? event : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const text = (value) => (typeof value === "string" && value ? value : null);
|
|
34
|
+
const templateKind = (value) => (value === "product" || value === "category" ? value : null);
|
|
35
|
+
export const changesFor = (event) => {
|
|
36
|
+
const { type, data } = event;
|
|
37
|
+
switch (type) {
|
|
38
|
+
// The component library changed: any page may render differently.
|
|
39
|
+
case "component.updated":
|
|
40
|
+
return { changes: [{ kind: "everything" }], immediate: true };
|
|
41
|
+
case "page.published":
|
|
42
|
+
case "page.unpublished": {
|
|
43
|
+
const path = text(data.path);
|
|
44
|
+
const previousPath = text(data.previousPath);
|
|
45
|
+
const templateFor = templateKind(data.templateFor);
|
|
46
|
+
const sectionsFor = templateKind(data.sectionsFor);
|
|
47
|
+
const changes = [];
|
|
48
|
+
if (path)
|
|
49
|
+
changes.push({ kind: "path", path });
|
|
50
|
+
if (previousPath && previousPath !== path)
|
|
51
|
+
changes.push({ kind: "path", path: previousPath });
|
|
52
|
+
// A template has no path: any product or category page may now render with it, or stop.
|
|
53
|
+
if (templateFor)
|
|
54
|
+
changes.push({ kind: "templates", for: templateFor });
|
|
55
|
+
// Nor do a product's or category's own sections.
|
|
56
|
+
if (sectionsFor)
|
|
57
|
+
changes.push({ kind: "sections", for: sectionsFor });
|
|
58
|
+
// Singletons (site_header, site_footer) have no path: they are named by content type.
|
|
59
|
+
const pageType = text(data.type);
|
|
60
|
+
if (pageType && !templateFor && !sectionsFor && !path && !previousPath)
|
|
61
|
+
changes.push({ kind: "singleton", type: pageType });
|
|
62
|
+
// A page that appeared, moved or went away changes links on other pages.
|
|
63
|
+
if (type === "page.unpublished" || path !== previousPath)
|
|
64
|
+
changes.push({ kind: "pages" });
|
|
65
|
+
return { changes, immediate: true };
|
|
66
|
+
}
|
|
67
|
+
// A template's assignments changed: those pages may now render with another template.
|
|
68
|
+
case "page.targets_updated": {
|
|
69
|
+
const templateFor = templateKind(data.templateFor);
|
|
70
|
+
return { changes: templateFor ? [{ kind: "templates", for: templateFor }] : [], immediate: true };
|
|
71
|
+
}
|
|
72
|
+
// Pages published together. The event names them only by id.
|
|
73
|
+
case "release.published":
|
|
74
|
+
return { changes: [{ kind: "pages" }], immediate: true };
|
|
75
|
+
}
|
|
76
|
+
// Prices and stock are checked again by the cart anyway: serve stale while refetching.
|
|
77
|
+
if (/^(product|category|price|inventory)\./.test(type))
|
|
78
|
+
return { changes: [{ kind: "catalog" }], immediate: false };
|
|
79
|
+
return { changes: [], immediate: false };
|
|
80
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spindlecommerce/storefront-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A typed client for the SpindleCommerce Storefront API, for any JavaScript runtime.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"spindlecommerce",
|
|
7
|
+
"storefront"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://github.com/spindlecommerce/storefront-sdk/tree/main/packages/storefront-client#readme",
|
|
10
|
+
"bugs": "https://github.com/spindlecommerce/storefront-sdk/issues",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/spindlecommerce/storefront-sdk.git",
|
|
14
|
+
"directory": "packages/storefront-client"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./schema.graphql": "./schema.graphql"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"schema.graphql"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20.9"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp ../../schema/storefront.graphql schema.graphql",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
39
|
+
}
|
|
6
40
|
}
|