@thebes/cadmea-plugin-ecommerce-square 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BowenLabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @thebes/cadmea-plugin-ecommerce-square
2
+
3
+ Square's [`PaymentProvider`](https://www.npmjs.com/package/@thebes/cadmea-plugin-ecommerce)
4
+ implementation for [Cadmea](https://github.com/bowenlabs/project-thebes)'s
5
+ ecommerce extension — raw `fetch()` against Square's REST API +
6
+ `crypto.subtle` for webhook signature verification. **No Square Node SDK,
7
+ ever** — it's Node-targeted and never runs in a V8 isolate.
8
+
9
+ ```bash
10
+ pnpm add @thebes/cadmea-plugin-ecommerce-square @thebes/cadmea-plugin-ecommerce
11
+ ```
12
+
13
+ ## Server-side
14
+
15
+ ```ts
16
+ import { createSquarePaymentProvider } from "@thebes/cadmea-plugin-ecommerce-square";
17
+
18
+ const provider = createSquarePaymentProvider({
19
+ accessToken: env.SQUARE_ACCESS_TOKEN,
20
+ locationId: env.SQUARE_LOCATION_ID, // or string[] for multi-location
21
+ environment: "sandbox", // or "production"
22
+ });
23
+ ```
24
+
25
+ Pass `provider` to `@thebes/cadmea-plugin-ecommerce`'s `createCheckoutHandler`/
26
+ `createWebhookHandler`. Webhook signature verification follows Square's
27
+ documented algorithm exactly: `HMAC-SHA256(notificationUrl + rawBody,
28
+ signatureKey)`, base64-encoded, checked against the
29
+ `x-square-hmacsha256-signature` header — fails closed (returns `false`,
30
+ never throws) if no `notificationUrl` is configured, since Square's scheme
31
+ genuinely needs one.
32
+
33
+ **Not implemented in this first cut:** `catalogSync` and `subscriptions` —
34
+ Square's loyalty/recurring-order model doesn't map cleanly onto either
35
+ optional `PaymentProvider` capability without further design. Use
36
+ `@thebes/cadmea-plugin-ecommerce-stripe` if you need native subscriptions.
37
+
38
+ ## Client-side tokenization
39
+
40
+ A separate `/client` subpath — card data must never reach the Worker, so
41
+ tokenization happens entirely in the browser via Square's Web Payments SDK
42
+ (loaded dynamically, no bundler config needed). This is **not a SolidJS
43
+ component** — it's a thin, framework-agnostic helper, the same precedent
44
+ this codebase already set for Phosphor icons and TipTap (no official Solid
45
+ binding exists for Square's SDK either, so the vendor's own
46
+ framework-agnostic build is used directly):
47
+
48
+ ```ts
49
+ import { createSquareCardField } from "@thebes/cadmea-plugin-ecommerce-square/client";
50
+
51
+ const card = await createSquareCardField(containerEl, {
52
+ applicationId: PUBLIC_SQUARE_APPLICATION_ID,
53
+ locationId: PUBLIC_SQUARE_LOCATION_ID,
54
+ environment: "sandbox",
55
+ });
56
+
57
+ // on checkout submit:
58
+ const paymentSourceToken = await card.tokenize();
59
+ ```
60
+
61
+ `@thebes/cadmea-plugin-ecommerce-stripe/client`'s `createStripeCardField`
62
+ shares the exact same `{ tokenize(), destroy() }` shape — swapping
63
+ providers means swapping which helper you call, not rewriting your
64
+ checkout UI.
65
+
66
+ ## License
67
+
68
+ MIT © BowenLabs
@@ -0,0 +1,50 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/client.ts
3
+ function sdkUrl(environment) {
4
+ return environment === "production" ? "https://web.squarecdn.com/v1/square.js" : "https://sandbox.web.squarecdn.com/v1/square.js";
5
+ }
6
+ let sdkLoadPromise;
7
+ function loadSquareSdk(environment) {
8
+ if (window.Square) return Promise.resolve(window.Square);
9
+ if (sdkLoadPromise) return sdkLoadPromise;
10
+ sdkLoadPromise = new Promise((resolve, reject) => {
11
+ const script = document.createElement("script");
12
+ script.src = sdkUrl(environment);
13
+ script.onload = () => resolve(window.Square);
14
+ script.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to load Square Web Payments SDK from ${script.src}`));
15
+ document.head.appendChild(script);
16
+ });
17
+ return sdkLoadPromise;
18
+ }
19
+ /**
20
+ * Loads the Square Web Payments SDK (if not already present), initializes
21
+ * a card field, and attaches it to `container`. The returned `tokenize()`
22
+ * produces a one-time payment source token suitable for
23
+ * `PaymentProvider.checkout`'s `paymentSourceToken` — raw card data never
24
+ * leaves the browser.
25
+ *
26
+ * ```ts
27
+ * const card = await createSquareCardField(containerEl, { applicationId, locationId });
28
+ * // ...on checkout submit:
29
+ * const token = await card.tokenize();
30
+ * await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ paymentSourceToken: token, ... }) });
31
+ * ```
32
+ */
33
+ async function createSquareCardField(container, options) {
34
+ const card = await (await loadSquareSdk(options.environment)).payments(options.applicationId, options.locationId).card();
35
+ await card.attach(container);
36
+ return {
37
+ async tokenize() {
38
+ const result = await card.tokenize();
39
+ if (result.status !== "OK") throw new Error(`Square card tokenization failed: ${result.status}${result.errors ? ` — ${JSON.stringify(result.errors)}` : ""}`);
40
+ return result.token;
41
+ },
42
+ async destroy() {
43
+ await card.destroy();
44
+ }
45
+ };
46
+ }
47
+ //#endregion
48
+ exports.createSquareCardField = createSquareCardField;
49
+
50
+ //# sourceMappingURL=client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.cjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n//\n// Browser-only client-side tokenization helper — a separate subpath\n// (`@thebes/cadmea-plugin-ecommerce-square/client`) so the server-side\n// provider entry never pulls in anything DOM-shaped, and vice versa.\n//\n// Square's Web Payments SDK is already vanilla, framework-agnostic,\n// DOM-attach JS with no official Solid binding — same shape of gap as\n// Phosphor icons and TipTap (both resolved in this codebase's DECISIONS.md\n// 2026-06-19 entry by using the vendor's framework-agnostic build\n// directly, not an unofficial wrapper). This file follows that precedent:\n// it is NOT a Solid component, has no JSX — just script-tag loading + SDK\n// init + card.attach()/card.tokenize() boilerplate, so every consumer\n// doesn't hand-roll the same dance. Card data never reaches the Worker —\n// tokenization happens entirely in the browser, here.\n\nexport interface SquareCardFieldOptions {\n applicationId: string;\n locationId: string;\n environment?: \"sandbox\" | \"production\";\n}\n\nexport interface CardField {\n tokenize(): Promise<string>;\n destroy(): Promise<void>;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: the Square Web Payments SDK ships no official TypeScript types for the `window.Square` global\ntype SquareGlobal = any;\n\nfunction sdkUrl(environment: SquareCardFieldOptions[\"environment\"]): string {\n return environment === \"production\"\n ? \"https://web.squarecdn.com/v1/square.js\"\n : \"https://sandbox.web.squarecdn.com/v1/square.js\";\n}\n\nlet sdkLoadPromise: Promise<SquareGlobal> | undefined;\n\nfunction loadSquareSdk(\n environment: SquareCardFieldOptions[\"environment\"],\n): Promise<SquareGlobal> {\n if ((window as { Square?: SquareGlobal }).Square) {\n return Promise.resolve((window as { Square?: SquareGlobal }).Square);\n }\n if (sdkLoadPromise) return sdkLoadPromise;\n\n sdkLoadPromise = new Promise((resolve, reject) => {\n const script = document.createElement(\"script\");\n script.src = sdkUrl(environment);\n script.onload = () => resolve((window as { Square?: SquareGlobal }).Square);\n script.onerror = () =>\n reject(\n new Error(`Failed to load Square Web Payments SDK from ${script.src}`),\n );\n document.head.appendChild(script);\n });\n return sdkLoadPromise;\n}\n\n/**\n * Loads the Square Web Payments SDK (if not already present), initializes\n * a card field, and attaches it to `container`. The returned `tokenize()`\n * produces a one-time payment source token suitable for\n * `PaymentProvider.checkout`'s `paymentSourceToken` — raw card data never\n * leaves the browser.\n *\n * ```ts\n * const card = await createSquareCardField(containerEl, { applicationId, locationId });\n * // ...on checkout submit:\n * const token = await card.tokenize();\n * await fetch(\"/api/checkout\", { method: \"POST\", body: JSON.stringify({ paymentSourceToken: token, ... }) });\n * ```\n */\nexport async function createSquareCardField(\n container: HTMLElement | string,\n options: SquareCardFieldOptions,\n): Promise<CardField> {\n const Square = await loadSquareSdk(options.environment);\n const payments = Square.payments(options.applicationId, options.locationId);\n const card = await payments.card();\n await card.attach(container);\n\n return {\n async tokenize() {\n const result = await card.tokenize();\n if (result.status !== \"OK\") {\n throw new Error(\n `Square card tokenization failed: ${result.status}${\n result.errors ? ` — ${JSON.stringify(result.errors)}` : \"\"\n }`,\n );\n }\n return result.token as string;\n },\n async destroy() {\n await card.destroy();\n },\n };\n}\n"],"mappings":";;AA+BA,SAAS,OAAO,aAA4D;CAC1E,OAAO,gBAAgB,eACnB,2CACA;AACN;AAEA,IAAI;AAEJ,SAAS,cACP,aACuB;CACvB,IAAK,OAAqC,QACxC,OAAO,QAAQ,QAAS,OAAqC,MAAM;CAErE,IAAI,gBAAgB,OAAO;CAE3B,iBAAiB,IAAI,SAAS,SAAS,WAAW;EAChD,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,OAAO,MAAM,OAAO,WAAW;EAC/B,OAAO,eAAe,QAAS,OAAqC,MAAM;EAC1E,OAAO,gBACL,uBACE,IAAI,MAAM,+CAA+C,OAAO,KAAK,CACvE;EACF,SAAS,KAAK,YAAY,MAAM;CAClC,CAAC;CACD,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,eAAsB,sBACpB,WACA,SACoB;CAGpB,MAAM,OAAO,OADI,MADI,cAAc,QAAQ,WAAW,EAAA,CAC9B,SAAS,QAAQ,eAAe,QAAQ,UACtC,CAAC,CAAC,KAAK;CACjC,MAAM,KAAK,OAAO,SAAS;CAE3B,OAAO;EACL,MAAM,WAAW;GACf,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,IAAI,OAAO,WAAW,MACpB,MAAM,IAAI,MACR,oCAAoC,OAAO,SACzC,OAAO,SAAS,MAAM,KAAK,UAAU,OAAO,MAAM,MAAM,IAE5D;GAEF,OAAO,OAAO;EAChB;EACA,MAAM,UAAU;GACd,MAAM,KAAK,QAAQ;EACrB;CACF;AACF"}
@@ -0,0 +1,28 @@
1
+ //#region src/client.d.ts
2
+ interface SquareCardFieldOptions {
3
+ applicationId: string;
4
+ locationId: string;
5
+ environment?: "sandbox" | "production";
6
+ }
7
+ interface CardField {
8
+ tokenize(): Promise<string>;
9
+ destroy(): Promise<void>;
10
+ }
11
+ /**
12
+ * Loads the Square Web Payments SDK (if not already present), initializes
13
+ * a card field, and attaches it to `container`. The returned `tokenize()`
14
+ * produces a one-time payment source token suitable for
15
+ * `PaymentProvider.checkout`'s `paymentSourceToken` — raw card data never
16
+ * leaves the browser.
17
+ *
18
+ * ```ts
19
+ * const card = await createSquareCardField(containerEl, { applicationId, locationId });
20
+ * // ...on checkout submit:
21
+ * const token = await card.tokenize();
22
+ * await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ paymentSourceToken: token, ... }) });
23
+ * ```
24
+ */
25
+ declare function createSquareCardField(container: HTMLElement | string, options: SquareCardFieldOptions): Promise<CardField>;
26
+ //#endregion
27
+ export { CardField, SquareCardFieldOptions, createSquareCardField };
28
+ //# sourceMappingURL=client.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.cts","names":[],"sources":["../src/client.ts"],"mappings":";UAiBiB,sBAAA;EACf,aAAA;EACA,UAAA;EACA,WAAA;AAAA;AAAA,UAGe,SAAA;EACf,QAAA,IAAY,OAAA;EACZ,OAAA,IAAW,OAAO;AAAA;AALP;AAGb;;;;;;;;;AAEoB;AAiDpB;;;AAtDa,iBAsDS,qBAAA,CACpB,SAAA,EAAW,WAAA,WACX,OAAA,EAAS,sBAAA,GACR,OAAA,CAAQ,SAAA"}
@@ -0,0 +1,28 @@
1
+ //#region src/client.d.ts
2
+ interface SquareCardFieldOptions {
3
+ applicationId: string;
4
+ locationId: string;
5
+ environment?: "sandbox" | "production";
6
+ }
7
+ interface CardField {
8
+ tokenize(): Promise<string>;
9
+ destroy(): Promise<void>;
10
+ }
11
+ /**
12
+ * Loads the Square Web Payments SDK (if not already present), initializes
13
+ * a card field, and attaches it to `container`. The returned `tokenize()`
14
+ * produces a one-time payment source token suitable for
15
+ * `PaymentProvider.checkout`'s `paymentSourceToken` — raw card data never
16
+ * leaves the browser.
17
+ *
18
+ * ```ts
19
+ * const card = await createSquareCardField(containerEl, { applicationId, locationId });
20
+ * // ...on checkout submit:
21
+ * const token = await card.tokenize();
22
+ * await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ paymentSourceToken: token, ... }) });
23
+ * ```
24
+ */
25
+ declare function createSquareCardField(container: HTMLElement | string, options: SquareCardFieldOptions): Promise<CardField>;
26
+ //#endregion
27
+ export { CardField, SquareCardFieldOptions, createSquareCardField };
28
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";UAiBiB,sBAAA;EACf,aAAA;EACA,UAAA;EACA,WAAA;AAAA;AAAA,UAGe,SAAA;EACf,QAAA,IAAY,OAAA;EACZ,OAAA,IAAW,OAAO;AAAA;AALP;AAGb;;;;;;;;;AAEoB;AAiDpB;;;AAtDa,iBAsDS,qBAAA,CACpB,SAAA,EAAW,WAAA,WACX,OAAA,EAAS,sBAAA,GACR,OAAA,CAAQ,SAAA"}
package/dist/client.js ADDED
@@ -0,0 +1,49 @@
1
+ //#region src/client.ts
2
+ function sdkUrl(environment) {
3
+ return environment === "production" ? "https://web.squarecdn.com/v1/square.js" : "https://sandbox.web.squarecdn.com/v1/square.js";
4
+ }
5
+ let sdkLoadPromise;
6
+ function loadSquareSdk(environment) {
7
+ if (window.Square) return Promise.resolve(window.Square);
8
+ if (sdkLoadPromise) return sdkLoadPromise;
9
+ sdkLoadPromise = new Promise((resolve, reject) => {
10
+ const script = document.createElement("script");
11
+ script.src = sdkUrl(environment);
12
+ script.onload = () => resolve(window.Square);
13
+ script.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to load Square Web Payments SDK from ${script.src}`));
14
+ document.head.appendChild(script);
15
+ });
16
+ return sdkLoadPromise;
17
+ }
18
+ /**
19
+ * Loads the Square Web Payments SDK (if not already present), initializes
20
+ * a card field, and attaches it to `container`. The returned `tokenize()`
21
+ * produces a one-time payment source token suitable for
22
+ * `PaymentProvider.checkout`'s `paymentSourceToken` — raw card data never
23
+ * leaves the browser.
24
+ *
25
+ * ```ts
26
+ * const card = await createSquareCardField(containerEl, { applicationId, locationId });
27
+ * // ...on checkout submit:
28
+ * const token = await card.tokenize();
29
+ * await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ paymentSourceToken: token, ... }) });
30
+ * ```
31
+ */
32
+ async function createSquareCardField(container, options) {
33
+ const card = await (await loadSquareSdk(options.environment)).payments(options.applicationId, options.locationId).card();
34
+ await card.attach(container);
35
+ return {
36
+ async tokenize() {
37
+ const result = await card.tokenize();
38
+ if (result.status !== "OK") throw new Error(`Square card tokenization failed: ${result.status}${result.errors ? ` — ${JSON.stringify(result.errors)}` : ""}`);
39
+ return result.token;
40
+ },
41
+ async destroy() {
42
+ await card.destroy();
43
+ }
44
+ };
45
+ }
46
+ //#endregion
47
+ export { createSquareCardField };
48
+
49
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n//\n// Browser-only client-side tokenization helper — a separate subpath\n// (`@thebes/cadmea-plugin-ecommerce-square/client`) so the server-side\n// provider entry never pulls in anything DOM-shaped, and vice versa.\n//\n// Square's Web Payments SDK is already vanilla, framework-agnostic,\n// DOM-attach JS with no official Solid binding — same shape of gap as\n// Phosphor icons and TipTap (both resolved in this codebase's DECISIONS.md\n// 2026-06-19 entry by using the vendor's framework-agnostic build\n// directly, not an unofficial wrapper). This file follows that precedent:\n// it is NOT a Solid component, has no JSX — just script-tag loading + SDK\n// init + card.attach()/card.tokenize() boilerplate, so every consumer\n// doesn't hand-roll the same dance. Card data never reaches the Worker —\n// tokenization happens entirely in the browser, here.\n\nexport interface SquareCardFieldOptions {\n applicationId: string;\n locationId: string;\n environment?: \"sandbox\" | \"production\";\n}\n\nexport interface CardField {\n tokenize(): Promise<string>;\n destroy(): Promise<void>;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: the Square Web Payments SDK ships no official TypeScript types for the `window.Square` global\ntype SquareGlobal = any;\n\nfunction sdkUrl(environment: SquareCardFieldOptions[\"environment\"]): string {\n return environment === \"production\"\n ? \"https://web.squarecdn.com/v1/square.js\"\n : \"https://sandbox.web.squarecdn.com/v1/square.js\";\n}\n\nlet sdkLoadPromise: Promise<SquareGlobal> | undefined;\n\nfunction loadSquareSdk(\n environment: SquareCardFieldOptions[\"environment\"],\n): Promise<SquareGlobal> {\n if ((window as { Square?: SquareGlobal }).Square) {\n return Promise.resolve((window as { Square?: SquareGlobal }).Square);\n }\n if (sdkLoadPromise) return sdkLoadPromise;\n\n sdkLoadPromise = new Promise((resolve, reject) => {\n const script = document.createElement(\"script\");\n script.src = sdkUrl(environment);\n script.onload = () => resolve((window as { Square?: SquareGlobal }).Square);\n script.onerror = () =>\n reject(\n new Error(`Failed to load Square Web Payments SDK from ${script.src}`),\n );\n document.head.appendChild(script);\n });\n return sdkLoadPromise;\n}\n\n/**\n * Loads the Square Web Payments SDK (if not already present), initializes\n * a card field, and attaches it to `container`. The returned `tokenize()`\n * produces a one-time payment source token suitable for\n * `PaymentProvider.checkout`'s `paymentSourceToken` — raw card data never\n * leaves the browser.\n *\n * ```ts\n * const card = await createSquareCardField(containerEl, { applicationId, locationId });\n * // ...on checkout submit:\n * const token = await card.tokenize();\n * await fetch(\"/api/checkout\", { method: \"POST\", body: JSON.stringify({ paymentSourceToken: token, ... }) });\n * ```\n */\nexport async function createSquareCardField(\n container: HTMLElement | string,\n options: SquareCardFieldOptions,\n): Promise<CardField> {\n const Square = await loadSquareSdk(options.environment);\n const payments = Square.payments(options.applicationId, options.locationId);\n const card = await payments.card();\n await card.attach(container);\n\n return {\n async tokenize() {\n const result = await card.tokenize();\n if (result.status !== \"OK\") {\n throw new Error(\n `Square card tokenization failed: ${result.status}${\n result.errors ? ` — ${JSON.stringify(result.errors)}` : \"\"\n }`,\n );\n }\n return result.token as string;\n },\n async destroy() {\n await card.destroy();\n },\n };\n}\n"],"mappings":";AA+BA,SAAS,OAAO,aAA4D;CAC1E,OAAO,gBAAgB,eACnB,2CACA;AACN;AAEA,IAAI;AAEJ,SAAS,cACP,aACuB;CACvB,IAAK,OAAqC,QACxC,OAAO,QAAQ,QAAS,OAAqC,MAAM;CAErE,IAAI,gBAAgB,OAAO;CAE3B,iBAAiB,IAAI,SAAS,SAAS,WAAW;EAChD,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,OAAO,MAAM,OAAO,WAAW;EAC/B,OAAO,eAAe,QAAS,OAAqC,MAAM;EAC1E,OAAO,gBACL,uBACE,IAAI,MAAM,+CAA+C,OAAO,KAAK,CACvE;EACF,SAAS,KAAK,YAAY,MAAM;CAClC,CAAC;CACD,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,eAAsB,sBACpB,WACA,SACoB;CAGpB,MAAM,OAAO,OADI,MADI,cAAc,QAAQ,WAAW,EAAA,CAC9B,SAAS,QAAQ,eAAe,QAAQ,UACtC,CAAC,CAAC,KAAK;CACjC,MAAM,KAAK,OAAO,SAAS;CAE3B,OAAO;EACL,MAAM,WAAW;GACf,MAAM,SAAS,MAAM,KAAK,SAAS;GACnC,IAAI,OAAO,WAAW,MACpB,MAAM,IAAI,MACR,oCAAoC,OAAO,SACzC,OAAO,SAAS,MAAM,KAAK,UAAU,OAAO,MAAM,MAAM,IAE5D;GAEF,OAAO,OAAO;EAChB;EACA,MAAM,UAAU;GACd,MAAM,KAAK,QAAQ;EACrB;CACF;AACF"}
package/dist/index.cjs ADDED
@@ -0,0 +1,213 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/provider.ts
3
+ const DEFAULT_API_VERSION = "2025-01-23";
4
+ function baseUrl(environment) {
5
+ return environment === "production" ? "https://connect.squareup.com" : "https://connect.squareupsandbox.com";
6
+ }
7
+ function primaryLocation(locationId) {
8
+ return Array.isArray(locationId) ? locationId[0] : locationId;
9
+ }
10
+ function allLocations(locationId) {
11
+ return Array.isArray(locationId) ? locationId : [locationId];
12
+ }
13
+ var SquareApiError = class extends Error {
14
+ status;
15
+ body;
16
+ constructor(message, status, body) {
17
+ super(message);
18
+ this.status = status;
19
+ this.body = body;
20
+ this.name = "SquareApiError";
21
+ }
22
+ };
23
+ async function squareFetch(config, path, init) {
24
+ const response = await fetch(`${baseUrl(config.environment)}${path}`, {
25
+ method: init.method,
26
+ headers: {
27
+ Authorization: `Bearer ${config.accessToken}`,
28
+ "Content-Type": "application/json",
29
+ "Square-Version": config.apiVersion ?? DEFAULT_API_VERSION
30
+ },
31
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
32
+ });
33
+ const text = await response.text();
34
+ const parsed = text ? JSON.parse(text) : {};
35
+ if (!response.ok) throw new SquareApiError(`Square API request to "${path}" failed with status ${response.status}`, response.status, parsed);
36
+ return parsed;
37
+ }
38
+ async function hmacSha256Base64(message, secret) {
39
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
40
+ name: "HMAC",
41
+ hash: "SHA-256"
42
+ }, false, ["sign"]);
43
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
44
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
45
+ }
46
+ function timingSafeEqual(a, b) {
47
+ if (a.length !== b.length) return false;
48
+ let mismatch = 0;
49
+ for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
50
+ return mismatch === 0;
51
+ }
52
+ async function checkCatalogPrices(config, refs) {
53
+ if (refs.length === 0) return [];
54
+ const objects = (await squareFetch(config, "/v2/catalog/batch-retrieve", {
55
+ method: "POST",
56
+ body: { object_ids: refs }
57
+ })).objects ?? [];
58
+ const priceByRef = new Map(objects.map((object) => [object.id, object.item_variation_data?.price_money]));
59
+ const counts = (await squareFetch(config, "/v2/inventory/batch-retrieve-counts", {
60
+ method: "POST",
61
+ body: {
62
+ catalog_object_ids: refs,
63
+ location_ids: allLocations(config.locationId)
64
+ }
65
+ })).counts ?? [];
66
+ const quantityByRef = /* @__PURE__ */ new Map();
67
+ for (const count of counts) {
68
+ const existing = quantityByRef.get(count.catalog_object_id) ?? 0;
69
+ quantityByRef.set(count.catalog_object_id, existing + Number.parseFloat(count.quantity));
70
+ }
71
+ return refs.map((catalogRef) => {
72
+ const price = priceByRef.get(catalogRef);
73
+ return {
74
+ catalogRef,
75
+ serverUnitPrice: {
76
+ amount: price?.amount ?? 0,
77
+ currency: price?.currency ?? "USD"
78
+ },
79
+ availableQuantity: quantityByRef.get(catalogRef)
80
+ };
81
+ });
82
+ }
83
+ async function findOrCreateCustomer(config, email, idempotencyKey) {
84
+ const existing = (await squareFetch(config, "/v2/customers/search", {
85
+ method: "POST",
86
+ body: { query: { filter: { email_address: { exact: email } } } }
87
+ })).customers?.[0];
88
+ if (existing) return existing.id;
89
+ return (await squareFetch(config, "/v2/customers", {
90
+ method: "POST",
91
+ body: {
92
+ idempotency_key: idempotencyKey,
93
+ email_address: email
94
+ }
95
+ })).customer.id;
96
+ }
97
+ function mapSquarePaymentStatus(status) {
98
+ if (status === "COMPLETED" || status === "APPROVED") return "succeeded";
99
+ if (status === "PENDING") return "requires_action";
100
+ return "failed";
101
+ }
102
+ async function checkout(config, request) {
103
+ const order = (await squareFetch(config, "/v2/orders", {
104
+ method: "POST",
105
+ body: {
106
+ idempotency_key: request.idempotencyKey,
107
+ order: {
108
+ location_id: primaryLocation(config.locationId),
109
+ line_items: request.lineItems.map((item) => ({
110
+ catalog_object_id: item.catalogRef,
111
+ quantity: String(item.quantity)
112
+ }))
113
+ }
114
+ }
115
+ })).order;
116
+ const paymentResponse = await squareFetch(config, "/v2/payments", {
117
+ method: "POST",
118
+ body: {
119
+ idempotency_key: crypto.randomUUID(),
120
+ source_id: request.paymentSourceToken,
121
+ amount_money: order.total_money,
122
+ order_id: order.id,
123
+ location_id: primaryLocation(config.locationId)
124
+ }
125
+ });
126
+ const payment = paymentResponse.payment;
127
+ return {
128
+ providerOrderRef: order.id,
129
+ providerPaymentRef: payment.id,
130
+ status: mapSquarePaymentStatus(payment.status),
131
+ amount: {
132
+ amount: payment.amount_money.amount,
133
+ currency: payment.amount_money.currency
134
+ },
135
+ raw: paymentResponse
136
+ };
137
+ }
138
+ async function verifyWebhookSignature(args) {
139
+ if (!args.notificationUrl) return false;
140
+ const signatureHeader = args.headers.get("x-square-hmacsha256-signature");
141
+ if (!signatureHeader) return false;
142
+ return timingSafeEqual(await hmacSha256Base64(args.notificationUrl + args.rawBody, args.secret), signatureHeader);
143
+ }
144
+ function parseWebhookEvent(rawBody) {
145
+ const payload = JSON.parse(rawBody);
146
+ const eventId = payload.event_id;
147
+ if (payload.type === "payment.updated") {
148
+ const payment = payload.data.object.payment;
149
+ if (payment) return {
150
+ eventId,
151
+ event: {
152
+ kind: "payment.updated",
153
+ providerPaymentRef: payment.id,
154
+ status: mapSquarePaymentStatus(payment.status) === "succeeded" ? "succeeded" : "failed"
155
+ }
156
+ };
157
+ }
158
+ if (payload.type === "order.updated") {
159
+ const orderUpdated = payload.data.object.order_updated;
160
+ if (orderUpdated) {
161
+ const status = orderUpdated.state === "COMPLETED" ? "paid" : orderUpdated.state === "CANCELED" ? "canceled" : "failed";
162
+ return {
163
+ eventId,
164
+ event: {
165
+ kind: "order.updated",
166
+ providerOrderRef: orderUpdated.order_id,
167
+ status
168
+ }
169
+ };
170
+ }
171
+ }
172
+ if (payload.type === "subscription.updated") {
173
+ const subscription = payload.data.object.subscription;
174
+ if (subscription) return {
175
+ eventId,
176
+ event: {
177
+ kind: "subscription.updated",
178
+ providerSubscriptionRef: subscription.id,
179
+ status: subscription.status
180
+ }
181
+ };
182
+ }
183
+ return {
184
+ eventId,
185
+ event: {
186
+ kind: "unhandled",
187
+ rawType: payload.type
188
+ }
189
+ };
190
+ }
191
+ /**
192
+ * Creates a `PaymentProvider` backed by Square's REST API — raw `fetch()`
193
+ * + `crypto.subtle`, no Square Node SDK. Implements the required
194
+ * `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/
195
+ * `verifyWebhookSignature`/`parseWebhookEvent`; omits `catalogSync` and
196
+ * `subscriptions` in this first cut (Square's loyalty/recurring-order
197
+ * model doesn't map cleanly onto either optional capability without
198
+ * further design — see the package README for the gap).
199
+ */
200
+ function createSquarePaymentProvider(config) {
201
+ return {
202
+ name: "square",
203
+ checkCatalogPrices: (refs) => checkCatalogPrices(config, refs),
204
+ findOrCreateCustomer: (email, idempotencyKey) => findOrCreateCustomer(config, email, idempotencyKey),
205
+ checkout: (request) => checkout(config, request),
206
+ verifyWebhookSignature,
207
+ parseWebhookEvent
208
+ };
209
+ }
210
+ //#endregion
211
+ exports.createSquarePaymentProvider = createSquarePaymentProvider;
212
+
213
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/provider.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n//\n// Square's REST API directly via fetch() — never the `square` npm SDK\n// (Node-targeted, BigInt-typed monetary amounts the raw REST API doesn't\n// actually return — its JSON responses use plain numbers; BigInt is purely\n// an artifact of the SDK's own typed wrapper). Webhook signature\n// verification uses crypto.subtle, mirroring the exact HMAC idiom already\n// in @thebes/cadmus/cms's webhooks.ts (compute-and-compare instead of\n// sign-and-attach).\n\nimport type {\n CatalogPriceCheck,\n CheckoutRequest,\n CheckoutResult,\n PaymentProvider,\n} from \"@thebes/cadmea-plugin-ecommerce\";\n\nexport interface SquareProviderConfig {\n accessToken: string;\n /** First entry is used as the primary location for orders/payments; all entries are queried for inventory. */\n locationId: string | string[];\n environment?: \"sandbox\" | \"production\";\n /** Square's API version header. Default: a fixed, tested version — bump deliberately, not implicitly. */\n apiVersion?: string;\n}\n\nconst DEFAULT_API_VERSION = \"2025-01-23\";\n\nfunction baseUrl(environment: SquareProviderConfig[\"environment\"]): string {\n return environment === \"production\"\n ? \"https://connect.squareup.com\"\n : \"https://connect.squareupsandbox.com\";\n}\n\nfunction primaryLocation(\n locationId: SquareProviderConfig[\"locationId\"],\n): string {\n return Array.isArray(locationId) ? locationId[0] : locationId;\n}\n\nfunction allLocations(\n locationId: SquareProviderConfig[\"locationId\"],\n): string[] {\n return Array.isArray(locationId) ? locationId : [locationId];\n}\n\nclass SquareApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message);\n this.name = \"SquareApiError\";\n }\n}\n\nasync function squareFetch(\n config: SquareProviderConfig,\n path: string,\n init: { method: string; body?: unknown },\n): Promise<Record<string, unknown>> {\n const response = await fetch(`${baseUrl(config.environment)}${path}`, {\n method: init.method,\n headers: {\n Authorization: `Bearer ${config.accessToken}`,\n \"Content-Type\": \"application/json\",\n \"Square-Version\": config.apiVersion ?? DEFAULT_API_VERSION,\n },\n body: init.body !== undefined ? JSON.stringify(init.body) : undefined,\n });\n const text = await response.text();\n const parsed = text ? JSON.parse(text) : {};\n if (!response.ok) {\n throw new SquareApiError(\n `Square API request to \"${path}\" failed with status ${response.status}`,\n response.status,\n parsed,\n );\n }\n return parsed;\n}\n\nasync function hmacSha256Base64(\n message: string,\n secret: string,\n): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const signature = await crypto.subtle.sign(\n \"HMAC\",\n key,\n new TextEncoder().encode(message),\n );\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\n// Constant-time string comparison — a plain `===` on a signature leaks,\n// via early-exit timing, how many leading bytes matched, which is enough\n// to forge a valid HMAC byte-by-byte. Mirrors the Stripe provider's helper.\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let mismatch = 0;\n for (let i = 0; i < a.length; i++) {\n mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return mismatch === 0;\n}\n\ninterface SquareCatalogObject {\n id: string;\n type: string;\n item_variation_data?: {\n price_money?: { amount: number; currency: string };\n };\n}\n\ninterface SquareInventoryCount {\n catalog_object_id: string;\n quantity: string;\n}\n\nasync function checkCatalogPrices(\n config: SquareProviderConfig,\n refs: string[],\n): Promise<CatalogPriceCheck[]> {\n if (refs.length === 0) return [];\n\n const catalogResponse = await squareFetch(\n config,\n \"/v2/catalog/batch-retrieve\",\n {\n method: \"POST\",\n body: { object_ids: refs },\n },\n );\n const objects = (catalogResponse.objects ?? []) as SquareCatalogObject[];\n const priceByRef = new Map(\n objects.map((object) => [\n object.id,\n object.item_variation_data?.price_money,\n ]),\n );\n\n const inventoryResponse = await squareFetch(\n config,\n \"/v2/inventory/batch-retrieve-counts\",\n {\n method: \"POST\",\n body: {\n catalog_object_ids: refs,\n location_ids: allLocations(config.locationId),\n },\n },\n );\n const counts = (inventoryResponse.counts ?? []) as SquareInventoryCount[];\n const quantityByRef = new Map<string, number>();\n for (const count of counts) {\n const existing = quantityByRef.get(count.catalog_object_id) ?? 0;\n quantityByRef.set(\n count.catalog_object_id,\n existing + Number.parseFloat(count.quantity),\n );\n }\n\n return refs.map((catalogRef) => {\n const price = priceByRef.get(catalogRef);\n return {\n catalogRef,\n serverUnitPrice: {\n amount: price?.amount ?? 0,\n currency: price?.currency ?? \"USD\",\n },\n availableQuantity: quantityByRef.get(catalogRef),\n };\n });\n}\n\nasync function findOrCreateCustomer(\n config: SquareProviderConfig,\n email: string,\n idempotencyKey: string,\n): Promise<string> {\n const searchResponse = await squareFetch(config, \"/v2/customers/search\", {\n method: \"POST\",\n body: { query: { filter: { email_address: { exact: email } } } },\n });\n const existing = (\n searchResponse.customers as Array<{ id: string }> | undefined\n )?.[0];\n if (existing) return existing.id;\n\n const createResponse = await squareFetch(config, \"/v2/customers\", {\n method: \"POST\",\n body: { idempotency_key: idempotencyKey, email_address: email },\n });\n return (createResponse.customer as { id: string }).id;\n}\n\nfunction mapSquarePaymentStatus(\n status: string | undefined,\n): CheckoutResult[\"status\"] {\n if (status === \"COMPLETED\" || status === \"APPROVED\") return \"succeeded\";\n if (status === \"PENDING\") return \"requires_action\";\n return \"failed\";\n}\n\nasync function checkout(\n config: SquareProviderConfig,\n request: CheckoutRequest,\n): Promise<CheckoutResult> {\n const orderResponse = await squareFetch(config, \"/v2/orders\", {\n method: \"POST\",\n body: {\n idempotency_key: request.idempotencyKey,\n order: {\n location_id: primaryLocation(config.locationId),\n line_items: request.lineItems.map((item) => ({\n catalog_object_id: item.catalogRef,\n quantity: String(item.quantity),\n })),\n },\n },\n });\n const order = orderResponse.order as {\n id: string;\n total_money: { amount: number; currency: string };\n };\n\n const paymentResponse = await squareFetch(config, \"/v2/payments\", {\n method: \"POST\",\n body: {\n idempotency_key: crypto.randomUUID(),\n source_id: request.paymentSourceToken,\n amount_money: order.total_money,\n order_id: order.id,\n location_id: primaryLocation(config.locationId),\n },\n });\n const payment = paymentResponse.payment as {\n id: string;\n status: string;\n amount_money: { amount: number; currency: string };\n };\n\n return {\n providerOrderRef: order.id,\n providerPaymentRef: payment.id,\n status: mapSquarePaymentStatus(payment.status),\n amount: {\n amount: payment.amount_money.amount,\n currency: payment.amount_money.currency,\n },\n raw: paymentResponse as Record<string, unknown>,\n };\n}\n\nasync function verifyWebhookSignature(args: {\n rawBody: string;\n headers: Headers;\n secret: string;\n notificationUrl?: string;\n}): Promise<boolean> {\n // Square signs `notificationUrl + rawBody` (in that order) with the\n // webhook signature key, base64-encoded, checked against the\n // `x-square-hmacsha256-signature` header — without a notification URL\n // there's nothing correct to verify against, so this fails closed.\n if (!args.notificationUrl) return false;\n const signatureHeader = args.headers.get(\"x-square-hmacsha256-signature\");\n if (!signatureHeader) return false;\n const expected = await hmacSha256Base64(\n args.notificationUrl + args.rawBody,\n args.secret,\n );\n return timingSafeEqual(expected, signatureHeader);\n}\n\ninterface SquareWebhookPayload {\n event_id: string;\n type: string;\n data: {\n object: Record<string, unknown>;\n };\n}\n\nfunction parseWebhookEvent(rawBody: string) {\n const payload = JSON.parse(rawBody) as SquareWebhookPayload;\n const eventId = payload.event_id;\n\n if (payload.type === \"payment.updated\") {\n const payment = payload.data.object.payment as\n | { id: string; status: string }\n | undefined;\n if (payment) {\n return {\n eventId,\n event: {\n kind: \"payment.updated\" as const,\n providerPaymentRef: payment.id,\n status:\n mapSquarePaymentStatus(payment.status) === \"succeeded\"\n ? (\"succeeded\" as const)\n : (\"failed\" as const),\n },\n };\n }\n }\n\n if (payload.type === \"order.updated\") {\n const orderUpdated = payload.data.object.order_updated as\n | { order_id: string; state: string }\n | undefined;\n if (orderUpdated) {\n const status =\n orderUpdated.state === \"COMPLETED\"\n ? (\"paid\" as const)\n : orderUpdated.state === \"CANCELED\"\n ? (\"canceled\" as const)\n : (\"failed\" as const);\n return {\n eventId,\n event: {\n kind: \"order.updated\" as const,\n providerOrderRef: orderUpdated.order_id,\n status,\n },\n };\n }\n }\n\n if (payload.type === \"subscription.updated\") {\n const subscription = payload.data.object.subscription as\n | { id: string; status: string }\n | undefined;\n if (subscription) {\n return {\n eventId,\n event: {\n kind: \"subscription.updated\" as const,\n providerSubscriptionRef: subscription.id,\n status: subscription.status,\n },\n };\n }\n }\n\n return {\n eventId,\n event: { kind: \"unhandled\" as const, rawType: payload.type },\n };\n}\n\n/**\n * Creates a `PaymentProvider` backed by Square's REST API — raw `fetch()`\n * + `crypto.subtle`, no Square Node SDK. Implements the required\n * `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/\n * `verifyWebhookSignature`/`parseWebhookEvent`; omits `catalogSync` and\n * `subscriptions` in this first cut (Square's loyalty/recurring-order\n * model doesn't map cleanly onto either optional capability without\n * further design — see the package README for the gap).\n */\nexport function createSquarePaymentProvider(\n config: SquareProviderConfig,\n): PaymentProvider {\n return {\n name: \"square\",\n checkCatalogPrices: (refs) => checkCatalogPrices(config, refs),\n findOrCreateCustomer: (email, idempotencyKey) =>\n findOrCreateCustomer(config, email, idempotencyKey),\n checkout: (request) => checkout(config, request),\n verifyWebhookSignature,\n parseWebhookEvent,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { PaymentProvider } from "@thebes/cadmea-plugin-ecommerce";
2
+
3
+ //#region src/provider.d.ts
4
+ interface SquareProviderConfig {
5
+ accessToken: string;
6
+ /** First entry is used as the primary location for orders/payments; all entries are queried for inventory. */
7
+ locationId: string | string[];
8
+ environment?: "sandbox" | "production";
9
+ /** Square's API version header. Default: a fixed, tested version — bump deliberately, not implicitly. */
10
+ apiVersion?: string;
11
+ }
12
+ /**
13
+ * Creates a `PaymentProvider` backed by Square's REST API — raw `fetch()`
14
+ * + `crypto.subtle`, no Square Node SDK. Implements the required
15
+ * `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/
16
+ * `verifyWebhookSignature`/`parseWebhookEvent`; omits `catalogSync` and
17
+ * `subscriptions` in this first cut (Square's loyalty/recurring-order
18
+ * model doesn't map cleanly onto either optional capability without
19
+ * further design — see the package README for the gap).
20
+ */
21
+ declare function createSquarePaymentProvider(config: SquareProviderConfig): PaymentProvider;
22
+ //#endregion
23
+ export { SquareProviderConfig, createSquarePaymentProvider };
24
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/provider.ts"],"mappings":";;;UAkBiB,oBAAA;EACf,WAAA;EADe;EAGf,UAAA;EACA,WAAA;EAJmC;EAMnC,UAAA;AAAA;;;;AAAU;AAuVZ;;;;;iBAAgB,2BAAA,CACd,MAAA,EAAQ,oBAAA,GACP,eAAe"}
@@ -0,0 +1,24 @@
1
+ import { PaymentProvider } from "@thebes/cadmea-plugin-ecommerce";
2
+
3
+ //#region src/provider.d.ts
4
+ interface SquareProviderConfig {
5
+ accessToken: string;
6
+ /** First entry is used as the primary location for orders/payments; all entries are queried for inventory. */
7
+ locationId: string | string[];
8
+ environment?: "sandbox" | "production";
9
+ /** Square's API version header. Default: a fixed, tested version — bump deliberately, not implicitly. */
10
+ apiVersion?: string;
11
+ }
12
+ /**
13
+ * Creates a `PaymentProvider` backed by Square's REST API — raw `fetch()`
14
+ * + `crypto.subtle`, no Square Node SDK. Implements the required
15
+ * `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/
16
+ * `verifyWebhookSignature`/`parseWebhookEvent`; omits `catalogSync` and
17
+ * `subscriptions` in this first cut (Square's loyalty/recurring-order
18
+ * model doesn't map cleanly onto either optional capability without
19
+ * further design — see the package README for the gap).
20
+ */
21
+ declare function createSquarePaymentProvider(config: SquareProviderConfig): PaymentProvider;
22
+ //#endregion
23
+ export { SquareProviderConfig, createSquarePaymentProvider };
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/provider.ts"],"mappings":";;;UAkBiB,oBAAA;EACf,WAAA;EADe;EAGf,UAAA;EACA,WAAA;EAJmC;EAMnC,UAAA;AAAA;;;;AAAU;AAuVZ;;;;;iBAAgB,2BAAA,CACd,MAAA,EAAQ,oBAAA,GACP,eAAe"}
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ //#region src/provider.ts
2
+ const DEFAULT_API_VERSION = "2025-01-23";
3
+ function baseUrl(environment) {
4
+ return environment === "production" ? "https://connect.squareup.com" : "https://connect.squareupsandbox.com";
5
+ }
6
+ function primaryLocation(locationId) {
7
+ return Array.isArray(locationId) ? locationId[0] : locationId;
8
+ }
9
+ function allLocations(locationId) {
10
+ return Array.isArray(locationId) ? locationId : [locationId];
11
+ }
12
+ var SquareApiError = class extends Error {
13
+ status;
14
+ body;
15
+ constructor(message, status, body) {
16
+ super(message);
17
+ this.status = status;
18
+ this.body = body;
19
+ this.name = "SquareApiError";
20
+ }
21
+ };
22
+ async function squareFetch(config, path, init) {
23
+ const response = await fetch(`${baseUrl(config.environment)}${path}`, {
24
+ method: init.method,
25
+ headers: {
26
+ Authorization: `Bearer ${config.accessToken}`,
27
+ "Content-Type": "application/json",
28
+ "Square-Version": config.apiVersion ?? DEFAULT_API_VERSION
29
+ },
30
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
31
+ });
32
+ const text = await response.text();
33
+ const parsed = text ? JSON.parse(text) : {};
34
+ if (!response.ok) throw new SquareApiError(`Square API request to "${path}" failed with status ${response.status}`, response.status, parsed);
35
+ return parsed;
36
+ }
37
+ async function hmacSha256Base64(message, secret) {
38
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
39
+ name: "HMAC",
40
+ hash: "SHA-256"
41
+ }, false, ["sign"]);
42
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
43
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
44
+ }
45
+ function timingSafeEqual(a, b) {
46
+ if (a.length !== b.length) return false;
47
+ let mismatch = 0;
48
+ for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
49
+ return mismatch === 0;
50
+ }
51
+ async function checkCatalogPrices(config, refs) {
52
+ if (refs.length === 0) return [];
53
+ const objects = (await squareFetch(config, "/v2/catalog/batch-retrieve", {
54
+ method: "POST",
55
+ body: { object_ids: refs }
56
+ })).objects ?? [];
57
+ const priceByRef = new Map(objects.map((object) => [object.id, object.item_variation_data?.price_money]));
58
+ const counts = (await squareFetch(config, "/v2/inventory/batch-retrieve-counts", {
59
+ method: "POST",
60
+ body: {
61
+ catalog_object_ids: refs,
62
+ location_ids: allLocations(config.locationId)
63
+ }
64
+ })).counts ?? [];
65
+ const quantityByRef = /* @__PURE__ */ new Map();
66
+ for (const count of counts) {
67
+ const existing = quantityByRef.get(count.catalog_object_id) ?? 0;
68
+ quantityByRef.set(count.catalog_object_id, existing + Number.parseFloat(count.quantity));
69
+ }
70
+ return refs.map((catalogRef) => {
71
+ const price = priceByRef.get(catalogRef);
72
+ return {
73
+ catalogRef,
74
+ serverUnitPrice: {
75
+ amount: price?.amount ?? 0,
76
+ currency: price?.currency ?? "USD"
77
+ },
78
+ availableQuantity: quantityByRef.get(catalogRef)
79
+ };
80
+ });
81
+ }
82
+ async function findOrCreateCustomer(config, email, idempotencyKey) {
83
+ const existing = (await squareFetch(config, "/v2/customers/search", {
84
+ method: "POST",
85
+ body: { query: { filter: { email_address: { exact: email } } } }
86
+ })).customers?.[0];
87
+ if (existing) return existing.id;
88
+ return (await squareFetch(config, "/v2/customers", {
89
+ method: "POST",
90
+ body: {
91
+ idempotency_key: idempotencyKey,
92
+ email_address: email
93
+ }
94
+ })).customer.id;
95
+ }
96
+ function mapSquarePaymentStatus(status) {
97
+ if (status === "COMPLETED" || status === "APPROVED") return "succeeded";
98
+ if (status === "PENDING") return "requires_action";
99
+ return "failed";
100
+ }
101
+ async function checkout(config, request) {
102
+ const order = (await squareFetch(config, "/v2/orders", {
103
+ method: "POST",
104
+ body: {
105
+ idempotency_key: request.idempotencyKey,
106
+ order: {
107
+ location_id: primaryLocation(config.locationId),
108
+ line_items: request.lineItems.map((item) => ({
109
+ catalog_object_id: item.catalogRef,
110
+ quantity: String(item.quantity)
111
+ }))
112
+ }
113
+ }
114
+ })).order;
115
+ const paymentResponse = await squareFetch(config, "/v2/payments", {
116
+ method: "POST",
117
+ body: {
118
+ idempotency_key: crypto.randomUUID(),
119
+ source_id: request.paymentSourceToken,
120
+ amount_money: order.total_money,
121
+ order_id: order.id,
122
+ location_id: primaryLocation(config.locationId)
123
+ }
124
+ });
125
+ const payment = paymentResponse.payment;
126
+ return {
127
+ providerOrderRef: order.id,
128
+ providerPaymentRef: payment.id,
129
+ status: mapSquarePaymentStatus(payment.status),
130
+ amount: {
131
+ amount: payment.amount_money.amount,
132
+ currency: payment.amount_money.currency
133
+ },
134
+ raw: paymentResponse
135
+ };
136
+ }
137
+ async function verifyWebhookSignature(args) {
138
+ if (!args.notificationUrl) return false;
139
+ const signatureHeader = args.headers.get("x-square-hmacsha256-signature");
140
+ if (!signatureHeader) return false;
141
+ return timingSafeEqual(await hmacSha256Base64(args.notificationUrl + args.rawBody, args.secret), signatureHeader);
142
+ }
143
+ function parseWebhookEvent(rawBody) {
144
+ const payload = JSON.parse(rawBody);
145
+ const eventId = payload.event_id;
146
+ if (payload.type === "payment.updated") {
147
+ const payment = payload.data.object.payment;
148
+ if (payment) return {
149
+ eventId,
150
+ event: {
151
+ kind: "payment.updated",
152
+ providerPaymentRef: payment.id,
153
+ status: mapSquarePaymentStatus(payment.status) === "succeeded" ? "succeeded" : "failed"
154
+ }
155
+ };
156
+ }
157
+ if (payload.type === "order.updated") {
158
+ const orderUpdated = payload.data.object.order_updated;
159
+ if (orderUpdated) {
160
+ const status = orderUpdated.state === "COMPLETED" ? "paid" : orderUpdated.state === "CANCELED" ? "canceled" : "failed";
161
+ return {
162
+ eventId,
163
+ event: {
164
+ kind: "order.updated",
165
+ providerOrderRef: orderUpdated.order_id,
166
+ status
167
+ }
168
+ };
169
+ }
170
+ }
171
+ if (payload.type === "subscription.updated") {
172
+ const subscription = payload.data.object.subscription;
173
+ if (subscription) return {
174
+ eventId,
175
+ event: {
176
+ kind: "subscription.updated",
177
+ providerSubscriptionRef: subscription.id,
178
+ status: subscription.status
179
+ }
180
+ };
181
+ }
182
+ return {
183
+ eventId,
184
+ event: {
185
+ kind: "unhandled",
186
+ rawType: payload.type
187
+ }
188
+ };
189
+ }
190
+ /**
191
+ * Creates a `PaymentProvider` backed by Square's REST API — raw `fetch()`
192
+ * + `crypto.subtle`, no Square Node SDK. Implements the required
193
+ * `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/
194
+ * `verifyWebhookSignature`/`parseWebhookEvent`; omits `catalogSync` and
195
+ * `subscriptions` in this first cut (Square's loyalty/recurring-order
196
+ * model doesn't map cleanly onto either optional capability without
197
+ * further design — see the package README for the gap).
198
+ */
199
+ function createSquarePaymentProvider(config) {
200
+ return {
201
+ name: "square",
202
+ checkCatalogPrices: (refs) => checkCatalogPrices(config, refs),
203
+ findOrCreateCustomer: (email, idempotencyKey) => findOrCreateCustomer(config, email, idempotencyKey),
204
+ checkout: (request) => checkout(config, request),
205
+ verifyWebhookSignature,
206
+ parseWebhookEvent
207
+ };
208
+ }
209
+ //#endregion
210
+ export { createSquarePaymentProvider };
211
+
212
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/provider.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// MIT licensed. See LICENSE in the repo root.\n//\n// Square's REST API directly via fetch() — never the `square` npm SDK\n// (Node-targeted, BigInt-typed monetary amounts the raw REST API doesn't\n// actually return — its JSON responses use plain numbers; BigInt is purely\n// an artifact of the SDK's own typed wrapper). Webhook signature\n// verification uses crypto.subtle, mirroring the exact HMAC idiom already\n// in @thebes/cadmus/cms's webhooks.ts (compute-and-compare instead of\n// sign-and-attach).\n\nimport type {\n CatalogPriceCheck,\n CheckoutRequest,\n CheckoutResult,\n PaymentProvider,\n} from \"@thebes/cadmea-plugin-ecommerce\";\n\nexport interface SquareProviderConfig {\n accessToken: string;\n /** First entry is used as the primary location for orders/payments; all entries are queried for inventory. */\n locationId: string | string[];\n environment?: \"sandbox\" | \"production\";\n /** Square's API version header. Default: a fixed, tested version — bump deliberately, not implicitly. */\n apiVersion?: string;\n}\n\nconst DEFAULT_API_VERSION = \"2025-01-23\";\n\nfunction baseUrl(environment: SquareProviderConfig[\"environment\"]): string {\n return environment === \"production\"\n ? \"https://connect.squareup.com\"\n : \"https://connect.squareupsandbox.com\";\n}\n\nfunction primaryLocation(\n locationId: SquareProviderConfig[\"locationId\"],\n): string {\n return Array.isArray(locationId) ? locationId[0] : locationId;\n}\n\nfunction allLocations(\n locationId: SquareProviderConfig[\"locationId\"],\n): string[] {\n return Array.isArray(locationId) ? locationId : [locationId];\n}\n\nclass SquareApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message);\n this.name = \"SquareApiError\";\n }\n}\n\nasync function squareFetch(\n config: SquareProviderConfig,\n path: string,\n init: { method: string; body?: unknown },\n): Promise<Record<string, unknown>> {\n const response = await fetch(`${baseUrl(config.environment)}${path}`, {\n method: init.method,\n headers: {\n Authorization: `Bearer ${config.accessToken}`,\n \"Content-Type\": \"application/json\",\n \"Square-Version\": config.apiVersion ?? DEFAULT_API_VERSION,\n },\n body: init.body !== undefined ? JSON.stringify(init.body) : undefined,\n });\n const text = await response.text();\n const parsed = text ? JSON.parse(text) : {};\n if (!response.ok) {\n throw new SquareApiError(\n `Square API request to \"${path}\" failed with status ${response.status}`,\n response.status,\n parsed,\n );\n }\n return parsed;\n}\n\nasync function hmacSha256Base64(\n message: string,\n secret: string,\n): Promise<string> {\n const key = await crypto.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const signature = await crypto.subtle.sign(\n \"HMAC\",\n key,\n new TextEncoder().encode(message),\n );\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\n// Constant-time string comparison — a plain `===` on a signature leaks,\n// via early-exit timing, how many leading bytes matched, which is enough\n// to forge a valid HMAC byte-by-byte. Mirrors the Stripe provider's helper.\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let mismatch = 0;\n for (let i = 0; i < a.length; i++) {\n mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return mismatch === 0;\n}\n\ninterface SquareCatalogObject {\n id: string;\n type: string;\n item_variation_data?: {\n price_money?: { amount: number; currency: string };\n };\n}\n\ninterface SquareInventoryCount {\n catalog_object_id: string;\n quantity: string;\n}\n\nasync function checkCatalogPrices(\n config: SquareProviderConfig,\n refs: string[],\n): Promise<CatalogPriceCheck[]> {\n if (refs.length === 0) return [];\n\n const catalogResponse = await squareFetch(\n config,\n \"/v2/catalog/batch-retrieve\",\n {\n method: \"POST\",\n body: { object_ids: refs },\n },\n );\n const objects = (catalogResponse.objects ?? []) as SquareCatalogObject[];\n const priceByRef = new Map(\n objects.map((object) => [\n object.id,\n object.item_variation_data?.price_money,\n ]),\n );\n\n const inventoryResponse = await squareFetch(\n config,\n \"/v2/inventory/batch-retrieve-counts\",\n {\n method: \"POST\",\n body: {\n catalog_object_ids: refs,\n location_ids: allLocations(config.locationId),\n },\n },\n );\n const counts = (inventoryResponse.counts ?? []) as SquareInventoryCount[];\n const quantityByRef = new Map<string, number>();\n for (const count of counts) {\n const existing = quantityByRef.get(count.catalog_object_id) ?? 0;\n quantityByRef.set(\n count.catalog_object_id,\n existing + Number.parseFloat(count.quantity),\n );\n }\n\n return refs.map((catalogRef) => {\n const price = priceByRef.get(catalogRef);\n return {\n catalogRef,\n serverUnitPrice: {\n amount: price?.amount ?? 0,\n currency: price?.currency ?? \"USD\",\n },\n availableQuantity: quantityByRef.get(catalogRef),\n };\n });\n}\n\nasync function findOrCreateCustomer(\n config: SquareProviderConfig,\n email: string,\n idempotencyKey: string,\n): Promise<string> {\n const searchResponse = await squareFetch(config, \"/v2/customers/search\", {\n method: \"POST\",\n body: { query: { filter: { email_address: { exact: email } } } },\n });\n const existing = (\n searchResponse.customers as Array<{ id: string }> | undefined\n )?.[0];\n if (existing) return existing.id;\n\n const createResponse = await squareFetch(config, \"/v2/customers\", {\n method: \"POST\",\n body: { idempotency_key: idempotencyKey, email_address: email },\n });\n return (createResponse.customer as { id: string }).id;\n}\n\nfunction mapSquarePaymentStatus(\n status: string | undefined,\n): CheckoutResult[\"status\"] {\n if (status === \"COMPLETED\" || status === \"APPROVED\") return \"succeeded\";\n if (status === \"PENDING\") return \"requires_action\";\n return \"failed\";\n}\n\nasync function checkout(\n config: SquareProviderConfig,\n request: CheckoutRequest,\n): Promise<CheckoutResult> {\n const orderResponse = await squareFetch(config, \"/v2/orders\", {\n method: \"POST\",\n body: {\n idempotency_key: request.idempotencyKey,\n order: {\n location_id: primaryLocation(config.locationId),\n line_items: request.lineItems.map((item) => ({\n catalog_object_id: item.catalogRef,\n quantity: String(item.quantity),\n })),\n },\n },\n });\n const order = orderResponse.order as {\n id: string;\n total_money: { amount: number; currency: string };\n };\n\n const paymentResponse = await squareFetch(config, \"/v2/payments\", {\n method: \"POST\",\n body: {\n idempotency_key: crypto.randomUUID(),\n source_id: request.paymentSourceToken,\n amount_money: order.total_money,\n order_id: order.id,\n location_id: primaryLocation(config.locationId),\n },\n });\n const payment = paymentResponse.payment as {\n id: string;\n status: string;\n amount_money: { amount: number; currency: string };\n };\n\n return {\n providerOrderRef: order.id,\n providerPaymentRef: payment.id,\n status: mapSquarePaymentStatus(payment.status),\n amount: {\n amount: payment.amount_money.amount,\n currency: payment.amount_money.currency,\n },\n raw: paymentResponse as Record<string, unknown>,\n };\n}\n\nasync function verifyWebhookSignature(args: {\n rawBody: string;\n headers: Headers;\n secret: string;\n notificationUrl?: string;\n}): Promise<boolean> {\n // Square signs `notificationUrl + rawBody` (in that order) with the\n // webhook signature key, base64-encoded, checked against the\n // `x-square-hmacsha256-signature` header — without a notification URL\n // there's nothing correct to verify against, so this fails closed.\n if (!args.notificationUrl) return false;\n const signatureHeader = args.headers.get(\"x-square-hmacsha256-signature\");\n if (!signatureHeader) return false;\n const expected = await hmacSha256Base64(\n args.notificationUrl + args.rawBody,\n args.secret,\n );\n return timingSafeEqual(expected, signatureHeader);\n}\n\ninterface SquareWebhookPayload {\n event_id: string;\n type: string;\n data: {\n object: Record<string, unknown>;\n };\n}\n\nfunction parseWebhookEvent(rawBody: string) {\n const payload = JSON.parse(rawBody) as SquareWebhookPayload;\n const eventId = payload.event_id;\n\n if (payload.type === \"payment.updated\") {\n const payment = payload.data.object.payment as\n | { id: string; status: string }\n | undefined;\n if (payment) {\n return {\n eventId,\n event: {\n kind: \"payment.updated\" as const,\n providerPaymentRef: payment.id,\n status:\n mapSquarePaymentStatus(payment.status) === \"succeeded\"\n ? (\"succeeded\" as const)\n : (\"failed\" as const),\n },\n };\n }\n }\n\n if (payload.type === \"order.updated\") {\n const orderUpdated = payload.data.object.order_updated as\n | { order_id: string; state: string }\n | undefined;\n if (orderUpdated) {\n const status =\n orderUpdated.state === \"COMPLETED\"\n ? (\"paid\" as const)\n : orderUpdated.state === \"CANCELED\"\n ? (\"canceled\" as const)\n : (\"failed\" as const);\n return {\n eventId,\n event: {\n kind: \"order.updated\" as const,\n providerOrderRef: orderUpdated.order_id,\n status,\n },\n };\n }\n }\n\n if (payload.type === \"subscription.updated\") {\n const subscription = payload.data.object.subscription as\n | { id: string; status: string }\n | undefined;\n if (subscription) {\n return {\n eventId,\n event: {\n kind: \"subscription.updated\" as const,\n providerSubscriptionRef: subscription.id,\n status: subscription.status,\n },\n };\n }\n }\n\n return {\n eventId,\n event: { kind: \"unhandled\" as const, rawType: payload.type },\n };\n}\n\n/**\n * Creates a `PaymentProvider` backed by Square's REST API — raw `fetch()`\n * + `crypto.subtle`, no Square Node SDK. Implements the required\n * `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/\n * `verifyWebhookSignature`/`parseWebhookEvent`; omits `catalogSync` and\n * `subscriptions` in this first cut (Square's loyalty/recurring-order\n * model doesn't map cleanly onto either optional capability without\n * further design — see the package README for the gap).\n */\nexport function createSquarePaymentProvider(\n config: SquareProviderConfig,\n): PaymentProvider {\n return {\n name: \"square\",\n checkCatalogPrices: (refs) => checkCatalogPrices(config, refs),\n findOrCreateCustomer: (email, idempotencyKey) =>\n findOrCreateCustomer(config, email, idempotencyKey),\n checkout: (request) => checkout(config, request),\n verifyWebhookSignature,\n parseWebhookEvent,\n };\n}\n"],"mappings":";AA2BA,MAAM,sBAAsB;AAE5B,SAAS,QAAQ,aAA0D;CACzE,OAAO,gBAAgB,eACnB,iCACA;AACN;AAEA,SAAS,gBACP,YACQ;CACR,OAAO,MAAM,QAAQ,UAAU,IAAI,WAAW,KAAK;AACrD;AAEA,SAAS,aACP,YACU;CACV,OAAO,MAAM,QAAQ,UAAU,IAAI,aAAa,CAAC,UAAU;AAC7D;AAEA,IAAM,iBAAN,cAA6B,MAAM;CAGf;CACA;CAHlB,YACE,SACA,QACA,MACA;EACA,MAAM,OAAO;EAHG,KAAA,SAAA;EACA,KAAA,OAAA;EAGhB,KAAK,OAAO;CACd;AACF;AAEA,eAAe,YACb,QACA,MACA,MACkC;CAClC,MAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,OAAO,WAAW,IAAI,QAAQ;EACpE,QAAQ,KAAK;EACb,SAAS;GACP,eAAe,UAAU,OAAO;GAChC,gBAAgB;GAChB,kBAAkB,OAAO,cAAc;EACzC;EACA,MAAM,KAAK,SAAS,KAAA,IAAY,KAAK,UAAU,KAAK,IAAI,IAAI,KAAA;CAC9D,CAAC;CACD,MAAM,OAAO,MAAM,SAAS,KAAK;CACjC,MAAM,SAAS,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;CAC1C,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,eACR,0BAA0B,KAAK,uBAAuB,SAAS,UAC/D,SAAS,QACT,MACF;CAEF,OAAO;AACT;AAEA,eAAe,iBACb,SACA,QACiB;CACjB,MAAM,MAAM,MAAM,OAAO,OAAO,UAC9B,OACA,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAC/B;EAAE,MAAM;EAAQ,MAAM;CAAU,GAChC,OACA,CAAC,MAAM,CACT;CACA,MAAM,YAAY,MAAM,OAAO,OAAO,KACpC,QACA,KACA,IAAI,YAAY,CAAC,CAAC,OAAO,OAAO,CAClC;CACA,OAAO,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,SAAS,CAAC,CAAC;AAC/D;AAKA,SAAS,gBAAgB,GAAW,GAAoB;CACtD,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,IAAI,WAAW;CACf,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAC5B,YAAY,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;CAE9C,OAAO,aAAa;AACtB;AAeA,eAAe,mBACb,QACA,MAC8B;CAC9B,IAAI,KAAK,WAAW,GAAG,OAAO,CAAC;CAU/B,MAAM,WAAW,MARa,YAC5B,QACA,8BACA;EACE,QAAQ;EACR,MAAM,EAAE,YAAY,KAAK;CAC3B,CACF,EAAA,CACiC,WAAW,CAAC;CAC7C,MAAM,aAAa,IAAI,IACrB,QAAQ,KAAK,WAAW,CACtB,OAAO,IACP,OAAO,qBAAqB,WAC9B,CAAC,CACH;CAaA,MAAM,UAAU,MAXgB,YAC9B,QACA,uCACA;EACE,QAAQ;EACR,MAAM;GACJ,oBAAoB;GACpB,cAAc,aAAa,OAAO,UAAU;EAC9C;CACF,CACF,EAAA,CACkC,UAAU,CAAC;CAC7C,MAAM,gCAAgB,IAAI,IAAoB;CAC9C,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,cAAc,IAAI,MAAM,iBAAiB,KAAK;EAC/D,cAAc,IACZ,MAAM,mBACN,WAAW,OAAO,WAAW,MAAM,QAAQ,CAC7C;CACF;CAEA,OAAO,KAAK,KAAK,eAAe;EAC9B,MAAM,QAAQ,WAAW,IAAI,UAAU;EACvC,OAAO;GACL;GACA,iBAAiB;IACf,QAAQ,OAAO,UAAU;IACzB,UAAU,OAAO,YAAY;GAC/B;GACA,mBAAmB,cAAc,IAAI,UAAU;EACjD;CACF,CAAC;AACH;AAEA,eAAe,qBACb,QACA,OACA,gBACiB;CAKjB,MAAM,YACJ,MAL2B,YAAY,QAAQ,wBAAwB;EACvE,QAAQ;EACR,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,OAAO,MAAM,EAAE,EAAE,EAAE;CACjE,CAAC,EAAA,CAEgB,YACb;CACJ,IAAI,UAAU,OAAO,SAAS;CAM9B,QAAQ,MAJqB,YAAY,QAAQ,iBAAiB;EAChE,QAAQ;EACR,MAAM;GAAE,iBAAiB;GAAgB,eAAe;EAAM;CAChE,CAAC,EAAA,CACsB,SAA4B;AACrD;AAEA,SAAS,uBACP,QAC0B;CAC1B,IAAI,WAAW,eAAe,WAAW,YAAY,OAAO;CAC5D,IAAI,WAAW,WAAW,OAAO;CACjC,OAAO;AACT;AAEA,eAAe,SACb,QACA,SACyB;CAczB,MAAM,SAAQ,MAbc,YAAY,QAAQ,cAAc;EAC5D,QAAQ;EACR,MAAM;GACJ,iBAAiB,QAAQ;GACzB,OAAO;IACL,aAAa,gBAAgB,OAAO,UAAU;IAC9C,YAAY,QAAQ,UAAU,KAAK,UAAU;KAC3C,mBAAmB,KAAK;KACxB,UAAU,OAAO,KAAK,QAAQ;IAChC,EAAE;GACJ;EACF;CACF,CAAC,EAAA,CAC2B;CAK5B,MAAM,kBAAkB,MAAM,YAAY,QAAQ,gBAAgB;EAChE,QAAQ;EACR,MAAM;GACJ,iBAAiB,OAAO,WAAW;GACnC,WAAW,QAAQ;GACnB,cAAc,MAAM;GACpB,UAAU,MAAM;GAChB,aAAa,gBAAgB,OAAO,UAAU;EAChD;CACF,CAAC;CACD,MAAM,UAAU,gBAAgB;CAMhC,OAAO;EACL,kBAAkB,MAAM;EACxB,oBAAoB,QAAQ;EAC5B,QAAQ,uBAAuB,QAAQ,MAAM;EAC7C,QAAQ;GACN,QAAQ,QAAQ,aAAa;GAC7B,UAAU,QAAQ,aAAa;EACjC;EACA,KAAK;CACP;AACF;AAEA,eAAe,uBAAuB,MAKjB;CAKnB,IAAI,CAAC,KAAK,iBAAiB,OAAO;CAClC,MAAM,kBAAkB,KAAK,QAAQ,IAAI,+BAA+B;CACxE,IAAI,CAAC,iBAAiB,OAAO;CAK7B,OAAO,gBAAgB,MAJA,iBACrB,KAAK,kBAAkB,KAAK,SAC5B,KAAK,MACP,GACiC,eAAe;AAClD;AAUA,SAAS,kBAAkB,SAAiB;CAC1C,MAAM,UAAU,KAAK,MAAM,OAAO;CAClC,MAAM,UAAU,QAAQ;CAExB,IAAI,QAAQ,SAAS,mBAAmB;EACtC,MAAM,UAAU,QAAQ,KAAK,OAAO;EAGpC,IAAI,SACF,OAAO;GACL;GACA,OAAO;IACL,MAAM;IACN,oBAAoB,QAAQ;IAC5B,QACE,uBAAuB,QAAQ,MAAM,MAAM,cACtC,cACA;GACT;EACF;CAEJ;CAEA,IAAI,QAAQ,SAAS,iBAAiB;EACpC,MAAM,eAAe,QAAQ,KAAK,OAAO;EAGzC,IAAI,cAAc;GAChB,MAAM,SACJ,aAAa,UAAU,cAClB,SACD,aAAa,UAAU,aACpB,aACA;GACT,OAAO;IACL;IACA,OAAO;KACL,MAAM;KACN,kBAAkB,aAAa;KAC/B;IACF;GACF;EACF;CACF;CAEA,IAAI,QAAQ,SAAS,wBAAwB;EAC3C,MAAM,eAAe,QAAQ,KAAK,OAAO;EAGzC,IAAI,cACF,OAAO;GACL;GACA,OAAO;IACL,MAAM;IACN,yBAAyB,aAAa;IACtC,QAAQ,aAAa;GACvB;EACF;CAEJ;CAEA,OAAO;EACL;EACA,OAAO;GAAE,MAAM;GAAsB,SAAS,QAAQ;EAAK;CAC7D;AACF;;;;;;;;;;AAWA,SAAgB,4BACd,QACiB;CACjB,OAAO;EACL,MAAM;EACN,qBAAqB,SAAS,mBAAmB,QAAQ,IAAI;EAC7D,uBAAuB,OAAO,mBAC5B,qBAAqB,QAAQ,OAAO,cAAc;EACpD,WAAW,YAAY,SAAS,QAAQ,OAAO;EAC/C;EACA;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@thebes/cadmea-plugin-ecommerce-square",
3
+ "version": "1.0.0",
4
+ "description": "Square PaymentProvider for @thebes/cadmea-plugin-ecommerce — raw fetch() + crypto.subtle, no Square Node SDK",
5
+ "author": "BowenLabs <hello@bowenlabs.io>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/bowenlabs/project-thebes",
10
+ "directory": "packages/cadmea-plugin-ecommerce-square"
11
+ },
12
+ "homepage": "https://github.com/bowenlabs/project-thebes/tree/main/packages/cadmea-plugin-ecommerce-square#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/bowenlabs/project-thebes/issues"
15
+ },
16
+ "keywords": [
17
+ "cadmea",
18
+ "cadmus",
19
+ "cms",
20
+ "ecommerce",
21
+ "square",
22
+ "payments",
23
+ "cloudflare"
24
+ ],
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./client": {
32
+ "types": "./dist/client.d.ts",
33
+ "default": "./dist/client.js"
34
+ }
35
+ },
36
+ "peerDependencies": {
37
+ "@thebes/cadmea-plugin-ecommerce": "^1.0.0",
38
+ "@thebes/cadmus": "^0.2.1"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "latest",
42
+ "vite-plus": "latest",
43
+ "vitest": "latest",
44
+ "@thebes/cadmus": "^0.2.1",
45
+ "@thebes/cadmea-plugin-ecommerce": "^1.0.0"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md",
50
+ "LICENSE"
51
+ ],
52
+ "scripts": {
53
+ "build": "vp pack",
54
+ "dev": "vp pack --watch",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest"
57
+ }
58
+ }