@thebes/cadmea-plugin-ecommerce-stripe 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 +21 -0
- package/README.md +70 -0
- package/dist/client.cjs +51 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +25 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +50 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +234 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +24 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
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,70 @@
|
|
|
1
|
+
# @thebes/cadmea-plugin-ecommerce-stripe
|
|
2
|
+
|
|
3
|
+
Stripe'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 Stripe's REST API +
|
|
6
|
+
`crypto.subtle` for webhook signature verification. **No Stripe Node SDK,
|
|
7
|
+
ever** — it relies on `node:crypto`/`node:http` internally and never runs
|
|
8
|
+
in a V8 isolate.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @thebes/cadmea-plugin-ecommerce-stripe @thebes/cadmea-plugin-ecommerce
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Server-side
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createStripePaymentProvider } from "@thebes/cadmea-plugin-ecommerce-stripe";
|
|
18
|
+
|
|
19
|
+
const provider = createStripePaymentProvider({
|
|
20
|
+
secretKey: env.STRIPE_SECRET_KEY,
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Pass `provider` to `@thebes/cadmea-plugin-ecommerce`'s `createCheckoutHandler`/
|
|
25
|
+
`createWebhookHandler`. Webhook signature verification follows Stripe's
|
|
26
|
+
documented scheme: `Stripe-Signature: t=<timestamp>,v1=<signature>`, message
|
|
27
|
+
`${timestamp}.${rawBody}`, `HMAC-SHA256` hex — with a staleness check on the
|
|
28
|
+
timestamp (`webhookToleranceSeconds`, default `300`, matching Stripe's own
|
|
29
|
+
recommended tolerance) before the signature is even compared.
|
|
30
|
+
|
|
31
|
+
### Real interface friction vs. Square — by design, not a bug
|
|
32
|
+
|
|
33
|
+
`PaymentProvider` was deliberately pressure-tested against a second real
|
|
34
|
+
provider after Square. The asymmetries this package absorbs:
|
|
35
|
+
|
|
36
|
+
- Stripe's API takes `application/x-www-form-urlencoded` bodies, not JSON.
|
|
37
|
+
- Stripe's idempotency key is an `Idempotency-Key` HTTP **header**, not a
|
|
38
|
+
body field the way Square's `idempotency_key` is.
|
|
39
|
+
- Stripe has no object analogous to Square's separate Order — a
|
|
40
|
+
`PaymentIntent` is both "the order" and "the payment" in one call. This
|
|
41
|
+
provider sets `providerOrderRef` and `providerPaymentRef` to the same
|
|
42
|
+
PaymentIntent id rather than inventing a fake second id.
|
|
43
|
+
- Stripe has no native inventory/catalog concept matching Square's
|
|
44
|
+
Catalog+Inventory APIs — `checkCatalogPrices` reads `Price` objects
|
|
45
|
+
(`unit_amount`/`currency`) and always returns `availableQuantity:
|
|
46
|
+
undefined`.
|
|
47
|
+
- Stripe's native Subscriptions API **is** implemented here (unlike the
|
|
48
|
+
Square provider, which omits the optional `subscriptions` capability) —
|
|
49
|
+
it's a much better fit than Square's loyalty/recurring-order model.
|
|
50
|
+
|
|
51
|
+
## Client-side tokenization
|
|
52
|
+
|
|
53
|
+
A separate `/client` subpath, the same shape as
|
|
54
|
+
`@thebes/cadmea-plugin-ecommerce-square/client`'s `createSquareCardField` —
|
|
55
|
+
card data never reaches the Worker:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createStripeCardField } from "@thebes/cadmea-plugin-ecommerce-stripe/client";
|
|
59
|
+
|
|
60
|
+
const card = await createStripeCardField(containerEl, {
|
|
61
|
+
publishableKey: PUBLIC_STRIPE_PUBLISHABLE_KEY,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// on checkout submit:
|
|
65
|
+
const paymentSourceToken = await card.tokenize();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT © BowenLabs
|
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/client.ts
|
|
3
|
+
const SDK_URL = "https://js.stripe.com/v3/";
|
|
4
|
+
let sdkLoadPromise;
|
|
5
|
+
function loadStripeSdk() {
|
|
6
|
+
if (window.Stripe) return Promise.resolve(window.Stripe);
|
|
7
|
+
if (sdkLoadPromise) return sdkLoadPromise;
|
|
8
|
+
sdkLoadPromise = new Promise((resolve, reject) => {
|
|
9
|
+
const script = document.createElement("script");
|
|
10
|
+
script.src = SDK_URL;
|
|
11
|
+
script.onload = () => resolve(window.Stripe);
|
|
12
|
+
script.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to load Stripe.js from ${SDK_URL}`));
|
|
13
|
+
document.head.appendChild(script);
|
|
14
|
+
});
|
|
15
|
+
return sdkLoadPromise;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Loads Stripe.js (if not already present), mounts a Card Element on
|
|
19
|
+
* `container`. The returned `tokenize()` produces a PaymentMethod id
|
|
20
|
+
* suitable for `PaymentProvider.checkout`'s `paymentSourceToken` — raw
|
|
21
|
+
* card data never leaves the browser.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const card = await createStripeCardField(containerEl, { publishableKey });
|
|
25
|
+
* // ...on checkout submit:
|
|
26
|
+
* const token = await card.tokenize();
|
|
27
|
+
* await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ paymentSourceToken: token, ... }) });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
async function createStripeCardField(container, options) {
|
|
31
|
+
const stripe = (await loadStripeSdk())(options.publishableKey);
|
|
32
|
+
const card = stripe.elements().create("card");
|
|
33
|
+
card.mount(container);
|
|
34
|
+
return {
|
|
35
|
+
async tokenize() {
|
|
36
|
+
const { paymentMethod, error } = await stripe.createPaymentMethod({
|
|
37
|
+
type: "card",
|
|
38
|
+
card
|
|
39
|
+
});
|
|
40
|
+
if (error) throw new Error(`Stripe card tokenization failed: ${error.message}`);
|
|
41
|
+
return paymentMethod.id;
|
|
42
|
+
},
|
|
43
|
+
async destroy() {
|
|
44
|
+
card.unmount();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
exports.createStripeCardField = createStripeCardField;
|
|
50
|
+
|
|
51
|
+
//# 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-stripe/client`), matching\n// `@thebes/cadmea-plugin-ecommerce-square/client`'s shape exactly\n// (`createXCardField` → `{ tokenize(), destroy() }`) so a consumer's\n// checkout UI can swap providers without rewriting its own component —\n// the client-side mirror of `PaymentProvider`'s own swappability.\n//\n// Stripe.js/Elements is likewise vanilla and framework-agnostic with no\n// official Solid binding — same precedent as the Square client (see\n// DECISIONS.md's 2026-06-19 entry on Phosphor/TipTap). Card data never\n// reaches the Worker — tokenization happens entirely in the browser, here.\n\nexport interface StripeCardFieldOptions {\n publishableKey: string;\n}\n\nexport interface CardField {\n tokenize(): Promise<string>;\n destroy(): Promise<void>;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: Stripe.js ships no official TypeScript types loadable without the `@stripe/stripe-js` npm package, which this plugin deliberately doesn't depend on\ntype StripeGlobal = any;\n\nconst SDK_URL = \"https://js.stripe.com/v3/\";\n\nlet sdkLoadPromise: Promise<StripeGlobal> | undefined;\n\nfunction loadStripeSdk(): Promise<StripeGlobal> {\n if ((window as { Stripe?: StripeGlobal }).Stripe) {\n return Promise.resolve((window as { Stripe?: StripeGlobal }).Stripe);\n }\n if (sdkLoadPromise) return sdkLoadPromise;\n\n sdkLoadPromise = new Promise((resolve, reject) => {\n const script = document.createElement(\"script\");\n script.src = SDK_URL;\n script.onload = () => resolve((window as { Stripe?: StripeGlobal }).Stripe);\n script.onerror = () =>\n reject(new Error(`Failed to load Stripe.js from ${SDK_URL}`));\n document.head.appendChild(script);\n });\n return sdkLoadPromise;\n}\n\n/**\n * Loads Stripe.js (if not already present), mounts a Card Element on\n * `container`. The returned `tokenize()` produces a PaymentMethod id\n * suitable for `PaymentProvider.checkout`'s `paymentSourceToken` — raw\n * card data never leaves the browser.\n *\n * ```ts\n * const card = await createStripeCardField(containerEl, { publishableKey });\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 createStripeCardField(\n container: HTMLElement | string,\n options: StripeCardFieldOptions,\n): Promise<CardField> {\n const StripeCtor = await loadStripeSdk();\n const stripe = StripeCtor(options.publishableKey);\n const elements = stripe.elements();\n const card = elements.create(\"card\");\n card.mount(container);\n\n return {\n async tokenize() {\n const { paymentMethod, error } = await stripe.createPaymentMethod({\n type: \"card\",\n card,\n });\n if (error) {\n throw new Error(`Stripe card tokenization failed: ${error.message}`);\n }\n return paymentMethod.id as string;\n },\n async destroy() {\n card.unmount();\n },\n };\n}\n"],"mappings":";;AA2BA,MAAM,UAAU;AAEhB,IAAI;AAEJ,SAAS,gBAAuC;CAC9C,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;EACb,OAAO,eAAe,QAAS,OAAqC,MAAM;EAC1E,OAAO,gBACL,uBAAO,IAAI,MAAM,iCAAiC,SAAS,CAAC;EAC9D,SAAS,KAAK,YAAY,MAAM;CAClC,CAAC;CACD,OAAO;AACT;;;;;;;;;;;;;;AAeA,eAAsB,sBACpB,WACA,SACoB;CAEpB,MAAM,UAAS,MADU,cAAc,EAAA,CACb,QAAQ,cAAc;CAEhD,MAAM,OADW,OAAO,SACJ,CAAC,CAAC,OAAO,MAAM;CACnC,KAAK,MAAM,SAAS;CAEpB,OAAO;EACL,MAAM,WAAW;GACf,MAAM,EAAE,eAAe,UAAU,MAAM,OAAO,oBAAoB;IAChE,MAAM;IACN;GACF,CAAC;GACD,IAAI,OACF,MAAM,IAAI,MAAM,oCAAoC,MAAM,SAAS;GAErE,OAAO,cAAc;EACvB;EACA,MAAM,UAAU;GACd,KAAK,QAAQ;EACf;CACF;AACF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/client.d.ts
|
|
2
|
+
interface StripeCardFieldOptions {
|
|
3
|
+
publishableKey: string;
|
|
4
|
+
}
|
|
5
|
+
interface CardField {
|
|
6
|
+
tokenize(): Promise<string>;
|
|
7
|
+
destroy(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Loads Stripe.js (if not already present), mounts a Card Element on
|
|
11
|
+
* `container`. The returned `tokenize()` produces a PaymentMethod id
|
|
12
|
+
* suitable for `PaymentProvider.checkout`'s `paymentSourceToken` — raw
|
|
13
|
+
* card data never leaves the browser.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const card = await createStripeCardField(containerEl, { publishableKey });
|
|
17
|
+
* // ...on checkout submit:
|
|
18
|
+
* const token = await card.tokenize();
|
|
19
|
+
* await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ paymentSourceToken: token, ... }) });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare function createStripeCardField(container: HTMLElement | string, options: StripeCardFieldOptions): Promise<CardField>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { CardField, StripeCardFieldOptions, createStripeCardField };
|
|
25
|
+
//# sourceMappingURL=client.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.cts","names":[],"sources":["../src/client.ts"],"mappings":";UAeiB,sBAAA;EACf,cAAc;AAAA;AAAA,UAGC,SAAA;EACf,QAAA,IAAY,OAAA;EACZ,OAAA,IAAW,OAAO;AAAA;;;;;;;;;AAAA;AAwCpB;;;;iBAAsB,qBAAA,CACpB,SAAA,EAAW,WAAA,WACX,OAAA,EAAS,sBAAA,GACR,OAAA,CAAQ,SAAA"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/client.d.ts
|
|
2
|
+
interface StripeCardFieldOptions {
|
|
3
|
+
publishableKey: string;
|
|
4
|
+
}
|
|
5
|
+
interface CardField {
|
|
6
|
+
tokenize(): Promise<string>;
|
|
7
|
+
destroy(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Loads Stripe.js (if not already present), mounts a Card Element on
|
|
11
|
+
* `container`. The returned `tokenize()` produces a PaymentMethod id
|
|
12
|
+
* suitable for `PaymentProvider.checkout`'s `paymentSourceToken` — raw
|
|
13
|
+
* card data never leaves the browser.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const card = await createStripeCardField(containerEl, { publishableKey });
|
|
17
|
+
* // ...on checkout submit:
|
|
18
|
+
* const token = await card.tokenize();
|
|
19
|
+
* await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ paymentSourceToken: token, ... }) });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare function createStripeCardField(container: HTMLElement | string, options: StripeCardFieldOptions): Promise<CardField>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { CardField, StripeCardFieldOptions, createStripeCardField };
|
|
25
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","names":[],"sources":["../src/client.ts"],"mappings":";UAeiB,sBAAA;EACf,cAAc;AAAA;AAAA,UAGC,SAAA;EACf,QAAA,IAAY,OAAA;EACZ,OAAA,IAAW,OAAO;AAAA;;;;;;;;;AAAA;AAwCpB;;;;iBAAsB,qBAAA,CACpB,SAAA,EAAW,WAAA,WACX,OAAA,EAAS,sBAAA,GACR,OAAA,CAAQ,SAAA"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/client.ts
|
|
2
|
+
const SDK_URL = "https://js.stripe.com/v3/";
|
|
3
|
+
let sdkLoadPromise;
|
|
4
|
+
function loadStripeSdk() {
|
|
5
|
+
if (window.Stripe) return Promise.resolve(window.Stripe);
|
|
6
|
+
if (sdkLoadPromise) return sdkLoadPromise;
|
|
7
|
+
sdkLoadPromise = new Promise((resolve, reject) => {
|
|
8
|
+
const script = document.createElement("script");
|
|
9
|
+
script.src = SDK_URL;
|
|
10
|
+
script.onload = () => resolve(window.Stripe);
|
|
11
|
+
script.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to load Stripe.js from ${SDK_URL}`));
|
|
12
|
+
document.head.appendChild(script);
|
|
13
|
+
});
|
|
14
|
+
return sdkLoadPromise;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Loads Stripe.js (if not already present), mounts a Card Element on
|
|
18
|
+
* `container`. The returned `tokenize()` produces a PaymentMethod id
|
|
19
|
+
* suitable for `PaymentProvider.checkout`'s `paymentSourceToken` — raw
|
|
20
|
+
* card data never leaves the browser.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const card = await createStripeCardField(containerEl, { publishableKey });
|
|
24
|
+
* // ...on checkout submit:
|
|
25
|
+
* const token = await card.tokenize();
|
|
26
|
+
* await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ paymentSourceToken: token, ... }) });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
async function createStripeCardField(container, options) {
|
|
30
|
+
const stripe = (await loadStripeSdk())(options.publishableKey);
|
|
31
|
+
const card = stripe.elements().create("card");
|
|
32
|
+
card.mount(container);
|
|
33
|
+
return {
|
|
34
|
+
async tokenize() {
|
|
35
|
+
const { paymentMethod, error } = await stripe.createPaymentMethod({
|
|
36
|
+
type: "card",
|
|
37
|
+
card
|
|
38
|
+
});
|
|
39
|
+
if (error) throw new Error(`Stripe card tokenization failed: ${error.message}`);
|
|
40
|
+
return paymentMethod.id;
|
|
41
|
+
},
|
|
42
|
+
async destroy() {
|
|
43
|
+
card.unmount();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { createStripeCardField };
|
|
49
|
+
|
|
50
|
+
//# 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-stripe/client`), matching\n// `@thebes/cadmea-plugin-ecommerce-square/client`'s shape exactly\n// (`createXCardField` → `{ tokenize(), destroy() }`) so a consumer's\n// checkout UI can swap providers without rewriting its own component —\n// the client-side mirror of `PaymentProvider`'s own swappability.\n//\n// Stripe.js/Elements is likewise vanilla and framework-agnostic with no\n// official Solid binding — same precedent as the Square client (see\n// DECISIONS.md's 2026-06-19 entry on Phosphor/TipTap). Card data never\n// reaches the Worker — tokenization happens entirely in the browser, here.\n\nexport interface StripeCardFieldOptions {\n publishableKey: string;\n}\n\nexport interface CardField {\n tokenize(): Promise<string>;\n destroy(): Promise<void>;\n}\n\n// biome-ignore lint/suspicious/noExplicitAny: Stripe.js ships no official TypeScript types loadable without the `@stripe/stripe-js` npm package, which this plugin deliberately doesn't depend on\ntype StripeGlobal = any;\n\nconst SDK_URL = \"https://js.stripe.com/v3/\";\n\nlet sdkLoadPromise: Promise<StripeGlobal> | undefined;\n\nfunction loadStripeSdk(): Promise<StripeGlobal> {\n if ((window as { Stripe?: StripeGlobal }).Stripe) {\n return Promise.resolve((window as { Stripe?: StripeGlobal }).Stripe);\n }\n if (sdkLoadPromise) return sdkLoadPromise;\n\n sdkLoadPromise = new Promise((resolve, reject) => {\n const script = document.createElement(\"script\");\n script.src = SDK_URL;\n script.onload = () => resolve((window as { Stripe?: StripeGlobal }).Stripe);\n script.onerror = () =>\n reject(new Error(`Failed to load Stripe.js from ${SDK_URL}`));\n document.head.appendChild(script);\n });\n return sdkLoadPromise;\n}\n\n/**\n * Loads Stripe.js (if not already present), mounts a Card Element on\n * `container`. The returned `tokenize()` produces a PaymentMethod id\n * suitable for `PaymentProvider.checkout`'s `paymentSourceToken` — raw\n * card data never leaves the browser.\n *\n * ```ts\n * const card = await createStripeCardField(containerEl, { publishableKey });\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 createStripeCardField(\n container: HTMLElement | string,\n options: StripeCardFieldOptions,\n): Promise<CardField> {\n const StripeCtor = await loadStripeSdk();\n const stripe = StripeCtor(options.publishableKey);\n const elements = stripe.elements();\n const card = elements.create(\"card\");\n card.mount(container);\n\n return {\n async tokenize() {\n const { paymentMethod, error } = await stripe.createPaymentMethod({\n type: \"card\",\n card,\n });\n if (error) {\n throw new Error(`Stripe card tokenization failed: ${error.message}`);\n }\n return paymentMethod.id as string;\n },\n async destroy() {\n card.unmount();\n },\n };\n}\n"],"mappings":";AA2BA,MAAM,UAAU;AAEhB,IAAI;AAEJ,SAAS,gBAAuC;CAC9C,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;EACb,OAAO,eAAe,QAAS,OAAqC,MAAM;EAC1E,OAAO,gBACL,uBAAO,IAAI,MAAM,iCAAiC,SAAS,CAAC;EAC9D,SAAS,KAAK,YAAY,MAAM;CAClC,CAAC;CACD,OAAO;AACT;;;;;;;;;;;;;;AAeA,eAAsB,sBACpB,WACA,SACoB;CAEpB,MAAM,UAAS,MADU,cAAc,EAAA,CACb,QAAQ,cAAc;CAEhD,MAAM,OADW,OAAO,SACJ,CAAC,CAAC,OAAO,MAAM;CACnC,KAAK,MAAM,SAAS;CAEpB,OAAO;EACL,MAAM,WAAW;GACf,MAAM,EAAE,eAAe,UAAU,MAAM,OAAO,oBAAoB;IAChE,MAAM;IACN;GACF,CAAC;GACD,IAAI,OACF,MAAM,IAAI,MAAM,oCAAoC,MAAM,SAAS;GAErE,OAAO,cAAc;EACvB;EACA,MAAM,UAAU;GACd,KAAK,QAAQ;EACf;CACF;AACF"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/provider.ts
|
|
3
|
+
const DEFAULT_API_VERSION = "2024-12-18.acacia";
|
|
4
|
+
const DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
5
|
+
const BASE_URL = "https://api.stripe.com";
|
|
6
|
+
var StripeApiError = class extends Error {
|
|
7
|
+
status;
|
|
8
|
+
body;
|
|
9
|
+
constructor(message, status, body) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.body = body;
|
|
13
|
+
this.name = "StripeApiError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function toFormBody(params, prefix) {
|
|
17
|
+
const body = new URLSearchParams();
|
|
18
|
+
const append = (key, value) => {
|
|
19
|
+
if (value === void 0 || value === null) return;
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
value.forEach((item, index) => {
|
|
22
|
+
for (const [k, v] of toFormBody({ [String(index)]: item }, key).entries()) body.append(k, v);
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "object") {
|
|
27
|
+
for (const [k, v] of toFormBody(value, key).entries()) body.append(k, v);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
body.append(key, String(value));
|
|
31
|
+
};
|
|
32
|
+
for (const [key, value] of Object.entries(params)) append(prefix ? `${prefix}[${key}]` : key, value);
|
|
33
|
+
return body;
|
|
34
|
+
}
|
|
35
|
+
async function stripeFetch(config, path, init) {
|
|
36
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
37
|
+
if (init.query) for (const [key, value] of Object.entries(init.query)) url.searchParams.set(key, value);
|
|
38
|
+
const headers = {
|
|
39
|
+
Authorization: `Bearer ${config.secretKey}`,
|
|
40
|
+
"Stripe-Version": config.apiVersion ?? DEFAULT_API_VERSION
|
|
41
|
+
};
|
|
42
|
+
if (init.idempotencyKey) headers["Idempotency-Key"] = init.idempotencyKey;
|
|
43
|
+
if (init.body) headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
44
|
+
const response = await fetch(url, {
|
|
45
|
+
method: init.method,
|
|
46
|
+
headers,
|
|
47
|
+
body: init.body ? toFormBody(init.body) : void 0
|
|
48
|
+
});
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
51
|
+
if (!response.ok) throw new StripeApiError(`Stripe API request to "${path}" failed with status ${response.status}`, response.status, parsed);
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
async function checkCatalogPrices(config, refs) {
|
|
55
|
+
return Promise.all(refs.map(async (catalogRef) => {
|
|
56
|
+
const price = await stripeFetch(config, `/v1/prices/${catalogRef}`, { method: "GET" });
|
|
57
|
+
return {
|
|
58
|
+
catalogRef,
|
|
59
|
+
serverUnitPrice: {
|
|
60
|
+
amount: price.unit_amount ?? 0,
|
|
61
|
+
currency: (price.currency ?? "usd").toUpperCase()
|
|
62
|
+
},
|
|
63
|
+
availableQuantity: void 0
|
|
64
|
+
};
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
async function findOrCreateCustomer(config, email, idempotencyKey) {
|
|
68
|
+
const existing = (await stripeFetch(config, "/v1/customers", {
|
|
69
|
+
method: "GET",
|
|
70
|
+
query: {
|
|
71
|
+
email,
|
|
72
|
+
limit: "1"
|
|
73
|
+
}
|
|
74
|
+
})).data?.[0];
|
|
75
|
+
if (existing) return existing.id;
|
|
76
|
+
return (await stripeFetch(config, "/v1/customers", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
body: { email },
|
|
79
|
+
idempotencyKey
|
|
80
|
+
})).id;
|
|
81
|
+
}
|
|
82
|
+
function mapStripePaymentIntentStatus(status) {
|
|
83
|
+
if (status === "succeeded") return "succeeded";
|
|
84
|
+
if (status === "requires_action" || status === "requires_confirmation") return "requires_action";
|
|
85
|
+
return "failed";
|
|
86
|
+
}
|
|
87
|
+
async function checkout(config, request) {
|
|
88
|
+
const amount = request.lineItems.reduce((sum, item) => sum + item.clientUnitPrice.amount * item.quantity, 0);
|
|
89
|
+
const currency = (request.lineItems[0]?.clientUnitPrice.currency ?? "USD").toLowerCase();
|
|
90
|
+
const paymentIntent = await stripeFetch(config, "/v1/payment_intents", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: {
|
|
93
|
+
amount,
|
|
94
|
+
currency,
|
|
95
|
+
payment_method: request.paymentSourceToken,
|
|
96
|
+
confirm: true,
|
|
97
|
+
automatic_payment_methods: {
|
|
98
|
+
enabled: true,
|
|
99
|
+
allow_redirects: "never"
|
|
100
|
+
},
|
|
101
|
+
metadata: request.metadata
|
|
102
|
+
},
|
|
103
|
+
idempotencyKey: request.idempotencyKey
|
|
104
|
+
});
|
|
105
|
+
const id = paymentIntent.id;
|
|
106
|
+
return {
|
|
107
|
+
providerOrderRef: id,
|
|
108
|
+
providerPaymentRef: id,
|
|
109
|
+
status: mapStripePaymentIntentStatus(paymentIntent.status),
|
|
110
|
+
amount: {
|
|
111
|
+
amount: paymentIntent.amount,
|
|
112
|
+
currency: (paymentIntent.currency ?? currency).toUpperCase()
|
|
113
|
+
},
|
|
114
|
+
raw: paymentIntent
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function timingSafeEqual(a, b) {
|
|
118
|
+
if (a.length !== b.length) return false;
|
|
119
|
+
let mismatch = 0;
|
|
120
|
+
for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
121
|
+
return mismatch === 0;
|
|
122
|
+
}
|
|
123
|
+
async function hmacSha256Hex(message, secret) {
|
|
124
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
|
|
125
|
+
name: "HMAC",
|
|
126
|
+
hash: "SHA-256"
|
|
127
|
+
}, false, ["sign"]);
|
|
128
|
+
const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
|
|
129
|
+
return Array.from(new Uint8Array(signature), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
130
|
+
}
|
|
131
|
+
function createVerifyWebhookSignature(config) {
|
|
132
|
+
return async ({ rawBody, headers, secret }) => {
|
|
133
|
+
const header = headers.get("stripe-signature");
|
|
134
|
+
if (!header) return false;
|
|
135
|
+
const parts = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const part of header.split(",")) {
|
|
137
|
+
const [key, value] = part.split("=");
|
|
138
|
+
if (key && value) parts.set(key, value);
|
|
139
|
+
}
|
|
140
|
+
const timestamp = parts.get("t");
|
|
141
|
+
const signature = parts.get("v1");
|
|
142
|
+
if (!timestamp || !signature) return false;
|
|
143
|
+
const tolerance = config.webhookToleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
|
|
144
|
+
if (Math.abs(Date.now() / 1e3 - Number.parseInt(timestamp, 10)) > tolerance) return false;
|
|
145
|
+
return timingSafeEqual(await hmacSha256Hex(`${timestamp}.${rawBody}`, secret), signature);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function parseWebhookEvent(rawBody) {
|
|
149
|
+
const payload = JSON.parse(rawBody);
|
|
150
|
+
const eventId = payload.id;
|
|
151
|
+
const object = payload.data.object;
|
|
152
|
+
if (payload.type === "payment_intent.succeeded") return {
|
|
153
|
+
eventId,
|
|
154
|
+
event: {
|
|
155
|
+
kind: "payment.updated",
|
|
156
|
+
providerPaymentRef: object.id,
|
|
157
|
+
status: "succeeded"
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
if (payload.type === "payment_intent.payment_failed") return {
|
|
161
|
+
eventId,
|
|
162
|
+
event: {
|
|
163
|
+
kind: "payment.updated",
|
|
164
|
+
providerPaymentRef: object.id,
|
|
165
|
+
status: "failed"
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
if (payload.type === "charge.refunded") return {
|
|
169
|
+
eventId,
|
|
170
|
+
event: {
|
|
171
|
+
kind: "payment.updated",
|
|
172
|
+
providerPaymentRef: object.payment_intent,
|
|
173
|
+
status: "refunded"
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
if (payload.type === "customer.subscription.updated" || payload.type === "customer.subscription.created") return {
|
|
177
|
+
eventId,
|
|
178
|
+
event: {
|
|
179
|
+
kind: "subscription.updated",
|
|
180
|
+
providerSubscriptionRef: object.id,
|
|
181
|
+
status: object.status
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
eventId,
|
|
186
|
+
event: {
|
|
187
|
+
kind: "unhandled",
|
|
188
|
+
rawType: payload.type
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Creates a `PaymentProvider` backed by Stripe's REST API — raw `fetch()`
|
|
194
|
+
* + `crypto.subtle`, no Stripe Node SDK. Implements the required
|
|
195
|
+
* `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/
|
|
196
|
+
* `verifyWebhookSignature`/`parseWebhookEvent`, plus the optional
|
|
197
|
+
* `subscriptions` capability via Stripe's native Subscriptions API (a
|
|
198
|
+
* better fit than the Square provider's loyalty/recurring-order model,
|
|
199
|
+
* which omits this capability entirely). `catalogSync` is omitted, same
|
|
200
|
+
* as the Square provider's first cut.
|
|
201
|
+
*/
|
|
202
|
+
function createStripePaymentProvider(config) {
|
|
203
|
+
return {
|
|
204
|
+
name: "stripe",
|
|
205
|
+
checkCatalogPrices: (refs) => checkCatalogPrices(config, refs),
|
|
206
|
+
findOrCreateCustomer: (email, idempotencyKey) => findOrCreateCustomer(config, email, idempotencyKey),
|
|
207
|
+
checkout: (request) => checkout(config, request),
|
|
208
|
+
verifyWebhookSignature: createVerifyWebhookSignature(config),
|
|
209
|
+
parseWebhookEvent,
|
|
210
|
+
subscriptions: {
|
|
211
|
+
async create(args) {
|
|
212
|
+
const subscription = await stripeFetch(config, "/v1/subscriptions", {
|
|
213
|
+
method: "POST",
|
|
214
|
+
body: {
|
|
215
|
+
customer: args.customerRef,
|
|
216
|
+
items: [{ price: args.planRef }]
|
|
217
|
+
},
|
|
218
|
+
idempotencyKey: args.idempotencyKey
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
providerSubscriptionRef: subscription.id,
|
|
222
|
+
status: subscription.status
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
async cancel(providerSubscriptionRef) {
|
|
226
|
+
await stripeFetch(config, `/v1/subscriptions/${providerSubscriptionRef}`, { method: "DELETE" });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
exports.createStripePaymentProvider = createStripePaymentProvider;
|
|
233
|
+
|
|
234
|
+
//# 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// Stripe's REST API directly via fetch() — never the `stripe` npm SDK\n// (Node-targeted; relies on node:crypto/node:http internally for some\n// operations). Webhook signature verification uses crypto.subtle.\n//\n// Real interface friction vs. the Square provider (PaymentProvider was\n// pressure-tested against this on purpose, per the build plan — these\n// aren't bugs, they're the asymmetry the interface has to absorb):\n// 1. Stripe's API takes `application/x-www-form-urlencoded` bodies, not\n// JSON — see `toFormBody` below.\n// 2. Stripe's idempotency key is an `Idempotency-Key` HTTP header, not a\n// body field the way Square's `idempotency_key` is.\n// 3. Stripe has no object analogous to Square's separate Order — a\n// `PaymentIntent` is both \"the order\" and \"the payment\" in one. This\n// provider sets `providerOrderRef` and `providerPaymentRef` to the\n// same PaymentIntent id rather than inventing a fake second id.\n// 4. Stripe has no native inventory/catalog concept matching Square's\n// Catalog+Inventory APIs — `checkCatalogPrices` reads `Price` objects\n// (`unit_amount`/`currency`) and always returns `availableQuantity:\n// undefined` (Stripe doesn't track it, so there's nothing honest to\n// report).\n// 5. Stripe's native Subscriptions API is a better fit for the optional\n// `subscriptions` capability than Square's loyalty/recurring-order\n// model — implemented here, omitted in the Square provider.\n\nimport type {\n CatalogPriceCheck,\n CheckoutRequest,\n CheckoutResult,\n PaymentProvider,\n} from \"@thebes/cadmea-plugin-ecommerce\";\n\nexport interface StripeProviderConfig {\n secretKey: string;\n /** Stripe's pinned API version header (`Stripe-Version`). Default: a fixed, tested version — bump deliberately, not implicitly. */\n apiVersion?: string;\n /** Seconds of clock skew tolerated on a webhook's `t=` timestamp before it's rejected as stale. Default: 300 (Stripe's own recommended tolerance). */\n webhookToleranceSeconds?: number;\n}\n\nconst DEFAULT_API_VERSION = \"2024-12-18.acacia\";\nconst DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;\nconst BASE_URL = \"https://api.stripe.com\";\n\nclass StripeApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message);\n this.name = \"StripeApiError\";\n }\n}\n\n// Stripe's form-encoding for nested objects uses bracket notation\n// (`items[0][price]=...`) — this plugin's own usage is shallow enough\n// that a small recursive flattener covers it without needing a general\n// qs-style library dependency.\nfunction toFormBody(\n params: Record<string, unknown>,\n prefix?: string,\n): URLSearchParams {\n const body = new URLSearchParams();\n const append = (key: string, value: unknown) => {\n if (value === undefined || value === null) return;\n if (Array.isArray(value)) {\n value.forEach((item, index) => {\n for (const [k, v] of toFormBody(\n { [String(index)]: item },\n key,\n ).entries()) {\n body.append(k, v);\n }\n });\n return;\n }\n if (typeof value === \"object\") {\n for (const [k, v] of toFormBody(\n value as Record<string, unknown>,\n key,\n ).entries()) {\n body.append(k, v);\n }\n return;\n }\n body.append(key, String(value));\n };\n for (const [key, value] of Object.entries(params)) {\n append(prefix ? `${prefix}[${key}]` : key, value);\n }\n return body;\n}\n\nasync function stripeFetch(\n config: StripeProviderConfig,\n path: string,\n init: {\n method: string;\n body?: Record<string, unknown>;\n idempotencyKey?: string;\n query?: Record<string, string>;\n },\n): Promise<Record<string, unknown>> {\n const url = new URL(`${BASE_URL}${path}`);\n if (init.query) {\n for (const [key, value] of Object.entries(init.query)) {\n url.searchParams.set(key, value);\n }\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.secretKey}`,\n \"Stripe-Version\": config.apiVersion ?? DEFAULT_API_VERSION,\n };\n if (init.idempotencyKey) headers[\"Idempotency-Key\"] = init.idempotencyKey;\n if (init.body) headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\";\n\n const response = await fetch(url, {\n method: init.method,\n headers,\n body: init.body ? toFormBody(init.body) : undefined,\n });\n const text = await response.text();\n const parsed = text ? JSON.parse(text) : {};\n if (!response.ok) {\n throw new StripeApiError(\n `Stripe API request to \"${path}\" failed with status ${response.status}`,\n response.status,\n parsed,\n );\n }\n return parsed;\n}\n\nasync function checkCatalogPrices(\n config: StripeProviderConfig,\n refs: string[],\n): Promise<CatalogPriceCheck[]> {\n return Promise.all(\n refs.map(async (catalogRef) => {\n const price = await stripeFetch(config, `/v1/prices/${catalogRef}`, {\n method: \"GET\",\n });\n return {\n catalogRef,\n serverUnitPrice: {\n amount: (price.unit_amount as number) ?? 0,\n currency: ((price.currency as string) ?? \"usd\").toUpperCase(),\n },\n // Stripe has no native inventory concept — nothing honest to report.\n availableQuantity: undefined,\n };\n }),\n );\n}\n\nasync function findOrCreateCustomer(\n config: StripeProviderConfig,\n email: string,\n idempotencyKey: string,\n): Promise<string> {\n const list = await stripeFetch(config, \"/v1/customers\", {\n method: \"GET\",\n query: { email, limit: \"1\" },\n });\n const existing = (list.data as Array<{ id: string }> | undefined)?.[0];\n if (existing) return existing.id;\n\n const created = await stripeFetch(config, \"/v1/customers\", {\n method: \"POST\",\n body: { email },\n idempotencyKey,\n });\n return created.id as string;\n}\n\nfunction mapStripePaymentIntentStatus(\n status: string | undefined,\n): CheckoutResult[\"status\"] {\n if (status === \"succeeded\") return \"succeeded\";\n if (status === \"requires_action\" || status === \"requires_confirmation\") {\n return \"requires_action\";\n }\n return \"failed\";\n}\n\nasync function checkout(\n config: StripeProviderConfig,\n request: CheckoutRequest,\n): Promise<CheckoutResult> {\n const amount = request.lineItems.reduce(\n (sum, item) => sum + item.clientUnitPrice.amount * item.quantity,\n 0,\n );\n const currency = (\n request.lineItems[0]?.clientUnitPrice.currency ?? \"USD\"\n ).toLowerCase();\n\n // One call, confirmed immediately — Stripe's PaymentIntent is both \"the\n // order\" and \"the charge\" at once, unlike Square's separate Orders +\n // Payments calls.\n const paymentIntent = await stripeFetch(config, \"/v1/payment_intents\", {\n method: \"POST\",\n body: {\n amount,\n currency,\n payment_method: request.paymentSourceToken,\n confirm: true,\n // automatic_payment_methods.allow_redirects: \"never\" keeps this a\n // single synchronous confirm — redirect-based methods would need a\n // requires_action round trip this checkout flow doesn't implement.\n automatic_payment_methods: { enabled: true, allow_redirects: \"never\" },\n metadata: request.metadata,\n },\n idempotencyKey: request.idempotencyKey,\n });\n\n const id = paymentIntent.id as string;\n return {\n providerOrderRef: id,\n providerPaymentRef: id,\n status: mapStripePaymentIntentStatus(paymentIntent.status as string),\n amount: {\n amount: paymentIntent.amount as number,\n currency: ((paymentIntent.currency as string) ?? currency).toUpperCase(),\n },\n raw: paymentIntent,\n };\n}\n\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\nasync function hmacSha256Hex(message: string, secret: string): 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 Array.from(new Uint8Array(signature), (b) =>\n b.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n}\n\nfunction createVerifyWebhookSignature(\n config: StripeProviderConfig,\n): PaymentProvider[\"verifyWebhookSignature\"] {\n return async ({ rawBody, headers, secret }) => {\n const header = headers.get(\"stripe-signature\");\n if (!header) return false;\n\n // Header format: \"t=<timestamp>,v1=<signature>[,v0=<old_signature>]\" —\n // parse rather than regex-matching positionally, since Stripe doesn't\n // guarantee component order.\n const parts = new Map<string, string>();\n for (const part of header.split(\",\")) {\n const [key, value] = part.split(\"=\");\n if (key && value) parts.set(key, value);\n }\n const timestamp = parts.get(\"t\");\n const signature = parts.get(\"v1\");\n if (!timestamp || !signature) return false;\n\n const tolerance =\n config.webhookToleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;\n const age = Math.abs(Date.now() / 1000 - Number.parseInt(timestamp, 10));\n if (age > tolerance) return false;\n\n const expected = await hmacSha256Hex(`${timestamp}.${rawBody}`, secret);\n return timingSafeEqual(expected, signature);\n };\n}\n\ninterface StripeWebhookPayload {\n id: string;\n type: string;\n data: { object: Record<string, unknown> };\n}\n\nfunction parseWebhookEvent(rawBody: string) {\n const payload = JSON.parse(rawBody) as StripeWebhookPayload;\n const eventId = payload.id;\n const object = payload.data.object;\n\n if (payload.type === \"payment_intent.succeeded\") {\n return {\n eventId,\n event: {\n kind: \"payment.updated\" as const,\n providerPaymentRef: object.id as string,\n status: \"succeeded\" as const,\n },\n };\n }\n\n if (payload.type === \"payment_intent.payment_failed\") {\n return {\n eventId,\n event: {\n kind: \"payment.updated\" as const,\n providerPaymentRef: object.id as string,\n status: \"failed\" as const,\n },\n };\n }\n\n if (payload.type === \"charge.refunded\") {\n return {\n eventId,\n event: {\n kind: \"payment.updated\" as const,\n providerPaymentRef: object.payment_intent as string,\n status: \"refunded\" as const,\n },\n };\n }\n\n if (\n payload.type === \"customer.subscription.updated\" ||\n payload.type === \"customer.subscription.created\"\n ) {\n return {\n eventId,\n event: {\n kind: \"subscription.updated\" as const,\n providerSubscriptionRef: object.id as string,\n status: object.status as string,\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 Stripe's REST API — raw `fetch()`\n * + `crypto.subtle`, no Stripe Node SDK. Implements the required\n * `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/\n * `verifyWebhookSignature`/`parseWebhookEvent`, plus the optional\n * `subscriptions` capability via Stripe's native Subscriptions API (a\n * better fit than the Square provider's loyalty/recurring-order model,\n * which omits this capability entirely). `catalogSync` is omitted, same\n * as the Square provider's first cut.\n */\nexport function createStripePaymentProvider(\n config: StripeProviderConfig,\n): PaymentProvider {\n return {\n name: \"stripe\",\n checkCatalogPrices: (refs) => checkCatalogPrices(config, refs),\n findOrCreateCustomer: (email, idempotencyKey) =>\n findOrCreateCustomer(config, email, idempotencyKey),\n checkout: (request) => checkout(config, request),\n verifyWebhookSignature: createVerifyWebhookSignature(config),\n parseWebhookEvent,\n subscriptions: {\n async create(args) {\n const subscription = await stripeFetch(config, \"/v1/subscriptions\", {\n method: \"POST\",\n body: {\n customer: args.customerRef,\n items: [{ price: args.planRef }],\n },\n idempotencyKey: args.idempotencyKey,\n });\n return {\n providerSubscriptionRef: subscription.id as string,\n status: subscription.status as string,\n };\n },\n async cancel(providerSubscriptionRef) {\n await stripeFetch(\n config,\n `/v1/subscriptions/${providerSubscriptionRef}`,\n { method: \"DELETE\" },\n );\n },\n },\n };\n}\n"],"mappings":";;AA0CA,MAAM,sBAAsB;AAC5B,MAAM,oCAAoC;AAC1C,MAAM,WAAW;AAEjB,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;AAMA,SAAS,WACP,QACA,QACiB;CACjB,MAAM,OAAO,IAAI,gBAAgB;CACjC,MAAM,UAAU,KAAa,UAAmB;EAC9C,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM;EAC3C,IAAI,MAAM,QAAQ,KAAK,GAAG;GACxB,MAAM,SAAS,MAAM,UAAU;IAC7B,KAAK,MAAM,CAAC,GAAG,MAAM,WACnB,GAAG,OAAO,KAAK,IAAI,KAAK,GACxB,GACF,CAAC,CAAC,QAAQ,GACR,KAAK,OAAO,GAAG,CAAC;GAEpB,CAAC;GACD;EACF;EACA,IAAI,OAAO,UAAU,UAAU;GAC7B,KAAK,MAAM,CAAC,GAAG,MAAM,WACnB,OACA,GACF,CAAC,CAAC,QAAQ,GACR,KAAK,OAAO,GAAG,CAAC;GAElB;EACF;EACA,KAAK,OAAO,KAAK,OAAO,KAAK,CAAC;CAChC;CACA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,OAAO,SAAS,GAAG,OAAO,GAAG,IAAI,KAAK,KAAK,KAAK;CAElD,OAAO;AACT;AAEA,eAAe,YACb,QACA,MACA,MAMkC;CAClC,MAAM,MAAM,IAAI,IAAI,GAAG,WAAW,MAAM;CACxC,IAAI,KAAK,OACP,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,KAAK,GAClD,IAAI,aAAa,IAAI,KAAK,KAAK;CAInC,MAAM,UAAkC;EACtC,eAAe,UAAU,OAAO;EAChC,kBAAkB,OAAO,cAAc;CACzC;CACA,IAAI,KAAK,gBAAgB,QAAQ,qBAAqB,KAAK;CAC3D,IAAI,KAAK,MAAM,QAAQ,kBAAkB;CAEzC,MAAM,WAAW,MAAM,MAAM,KAAK;EAChC,QAAQ,KAAK;EACb;EACA,MAAM,KAAK,OAAO,WAAW,KAAK,IAAI,IAAI,KAAA;CAC5C,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,mBACb,QACA,MAC8B;CAC9B,OAAO,QAAQ,IACb,KAAK,IAAI,OAAO,eAAe;EAC7B,MAAM,QAAQ,MAAM,YAAY,QAAQ,cAAc,cAAc,EAClE,QAAQ,MACV,CAAC;EACD,OAAO;GACL;GACA,iBAAiB;IACf,QAAS,MAAM,eAA0B;IACzC,WAAY,MAAM,YAAuB,MAAA,CAAO,YAAY;GAC9D;GAEA,mBAAmB,KAAA;EACrB;CACF,CAAC,CACH;AACF;AAEA,eAAe,qBACb,QACA,OACA,gBACiB;CAKjB,MAAM,YAAY,MAJC,YAAY,QAAQ,iBAAiB;EACtD,QAAQ;EACR,OAAO;GAAE;GAAO,OAAO;EAAI;CAC7B,CAAC,EAAA,CACsB,OAA6C;CACpE,IAAI,UAAU,OAAO,SAAS;CAO9B,QAAO,MALe,YAAY,QAAQ,iBAAiB;EACzD,QAAQ;EACR,MAAM,EAAE,MAAM;EACd;CACF,CAAC,EAAA,CACc;AACjB;AAEA,SAAS,6BACP,QAC0B;CAC1B,IAAI,WAAW,aAAa,OAAO;CACnC,IAAI,WAAW,qBAAqB,WAAW,yBAC7C,OAAO;CAET,OAAO;AACT;AAEA,eAAe,SACb,QACA,SACyB;CACzB,MAAM,SAAS,QAAQ,UAAU,QAC9B,KAAK,SAAS,MAAM,KAAK,gBAAgB,SAAS,KAAK,UACxD,CACF;CACA,MAAM,YACJ,QAAQ,UAAU,EAAE,EAAE,gBAAgB,YAAY,MAAA,CAClD,YAAY;CAKd,MAAM,gBAAgB,MAAM,YAAY,QAAQ,uBAAuB;EACrE,QAAQ;EACR,MAAM;GACJ;GACA;GACA,gBAAgB,QAAQ;GACxB,SAAS;GAIT,2BAA2B;IAAE,SAAS;IAAM,iBAAiB;GAAQ;GACrE,UAAU,QAAQ;EACpB;EACA,gBAAgB,QAAQ;CAC1B,CAAC;CAED,MAAM,KAAK,cAAc;CACzB,OAAO;EACL,kBAAkB;EAClB,oBAAoB;EACpB,QAAQ,6BAA6B,cAAc,MAAgB;EACnE,QAAQ;GACN,QAAQ,cAAc;GACtB,WAAY,cAAc,YAAuB,SAAA,CAAU,YAAY;EACzE;EACA,KAAK;CACP;AACF;AAEA,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,cAAc,SAAiB,QAAiC;CAC7E,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,MAAM,KAAK,IAAI,WAAW,SAAS,IAAI,MAC5C,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAChC,CAAC,CAAC,KAAK,EAAE;AACX;AAEA,SAAS,6BACP,QAC2C;CAC3C,OAAO,OAAO,EAAE,SAAS,SAAS,aAAa;EAC7C,MAAM,SAAS,QAAQ,IAAI,kBAAkB;EAC7C,IAAI,CAAC,QAAQ,OAAO;EAKpB,MAAM,wBAAQ,IAAI,IAAoB;EACtC,KAAK,MAAM,QAAQ,OAAO,MAAM,GAAG,GAAG;GACpC,MAAM,CAAC,KAAK,SAAS,KAAK,MAAM,GAAG;GACnC,IAAI,OAAO,OAAO,MAAM,IAAI,KAAK,KAAK;EACxC;EACA,MAAM,YAAY,MAAM,IAAI,GAAG;EAC/B,MAAM,YAAY,MAAM,IAAI,IAAI;EAChC,IAAI,CAAC,aAAa,CAAC,WAAW,OAAO;EAErC,MAAM,YACJ,OAAO,2BAA2B;EAEpC,IADY,KAAK,IAAI,KAAK,IAAI,IAAI,MAAO,OAAO,SAAS,WAAW,EAAE,CAChE,IAAI,WAAW,OAAO;EAG5B,OAAO,gBAAgB,MADA,cAAc,GAAG,UAAU,GAAG,WAAW,MAAM,GACrC,SAAS;CAC5C;AACF;AAQA,SAAS,kBAAkB,SAAiB;CAC1C,MAAM,UAAU,KAAK,MAAM,OAAO;CAClC,MAAM,UAAU,QAAQ;CACxB,MAAM,SAAS,QAAQ,KAAK;CAE5B,IAAI,QAAQ,SAAS,4BACnB,OAAO;EACL;EACA,OAAO;GACL,MAAM;GACN,oBAAoB,OAAO;GAC3B,QAAQ;EACV;CACF;CAGF,IAAI,QAAQ,SAAS,iCACnB,OAAO;EACL;EACA,OAAO;GACL,MAAM;GACN,oBAAoB,OAAO;GAC3B,QAAQ;EACV;CACF;CAGF,IAAI,QAAQ,SAAS,mBACnB,OAAO;EACL;EACA,OAAO;GACL,MAAM;GACN,oBAAoB,OAAO;GAC3B,QAAQ;EACV;CACF;CAGF,IACE,QAAQ,SAAS,mCACjB,QAAQ,SAAS,iCAEjB,OAAO;EACL;EACA,OAAO;GACL,MAAM;GACN,yBAAyB,OAAO;GAChC,QAAQ,OAAO;EACjB;CACF;CAGF,OAAO;EACL;EACA,OAAO;GAAE,MAAM;GAAsB,SAAS,QAAQ;EAAK;CAC7D;AACF;;;;;;;;;;;AAYA,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,wBAAwB,6BAA6B,MAAM;EAC3D;EACA,eAAe;GACb,MAAM,OAAO,MAAM;IACjB,MAAM,eAAe,MAAM,YAAY,QAAQ,qBAAqB;KAClE,QAAQ;KACR,MAAM;MACJ,UAAU,KAAK;MACf,OAAO,CAAC,EAAE,OAAO,KAAK,QAAQ,CAAC;KACjC;KACA,gBAAgB,KAAK;IACvB,CAAC;IACD,OAAO;KACL,yBAAyB,aAAa;KACtC,QAAQ,aAAa;IACvB;GACF;GACA,MAAM,OAAO,yBAAyB;IACpC,MAAM,YACJ,QACA,qBAAqB,2BACrB,EAAE,QAAQ,SAAS,CACrB;GACF;EACF;CACF;AACF"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PaymentProvider } from "@thebes/cadmea-plugin-ecommerce";
|
|
2
|
+
|
|
3
|
+
//#region src/provider.d.ts
|
|
4
|
+
interface StripeProviderConfig {
|
|
5
|
+
secretKey: string;
|
|
6
|
+
/** Stripe's pinned API version header (`Stripe-Version`). Default: a fixed, tested version — bump deliberately, not implicitly. */
|
|
7
|
+
apiVersion?: string;
|
|
8
|
+
/** Seconds of clock skew tolerated on a webhook's `t=` timestamp before it's rejected as stale. Default: 300 (Stripe's own recommended tolerance). */
|
|
9
|
+
webhookToleranceSeconds?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a `PaymentProvider` backed by Stripe's REST API — raw `fetch()`
|
|
13
|
+
* + `crypto.subtle`, no Stripe Node SDK. Implements the required
|
|
14
|
+
* `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/
|
|
15
|
+
* `verifyWebhookSignature`/`parseWebhookEvent`, plus the optional
|
|
16
|
+
* `subscriptions` capability via Stripe's native Subscriptions API (a
|
|
17
|
+
* better fit than the Square provider's loyalty/recurring-order model,
|
|
18
|
+
* which omits this capability entirely). `catalogSync` is omitted, same
|
|
19
|
+
* as the Square provider's first cut.
|
|
20
|
+
*/
|
|
21
|
+
declare function createStripePaymentProvider(config: StripeProviderConfig): PaymentProvider;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { StripeProviderConfig, createStripePaymentProvider };
|
|
24
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/provider.ts"],"mappings":";;;UAkCiB,oBAAA;EACf,SAAA;EADe;EAGf,UAAA;;EAEA,uBAAA;AAAA;;;;AAAuB;AAoUzB;;;;;;iBAAgB,2BAAA,CACd,MAAA,EAAQ,oBAAA,GACP,eAAe"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PaymentProvider } from "@thebes/cadmea-plugin-ecommerce";
|
|
2
|
+
|
|
3
|
+
//#region src/provider.d.ts
|
|
4
|
+
interface StripeProviderConfig {
|
|
5
|
+
secretKey: string;
|
|
6
|
+
/** Stripe's pinned API version header (`Stripe-Version`). Default: a fixed, tested version — bump deliberately, not implicitly. */
|
|
7
|
+
apiVersion?: string;
|
|
8
|
+
/** Seconds of clock skew tolerated on a webhook's `t=` timestamp before it's rejected as stale. Default: 300 (Stripe's own recommended tolerance). */
|
|
9
|
+
webhookToleranceSeconds?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a `PaymentProvider` backed by Stripe's REST API — raw `fetch()`
|
|
13
|
+
* + `crypto.subtle`, no Stripe Node SDK. Implements the required
|
|
14
|
+
* `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/
|
|
15
|
+
* `verifyWebhookSignature`/`parseWebhookEvent`, plus the optional
|
|
16
|
+
* `subscriptions` capability via Stripe's native Subscriptions API (a
|
|
17
|
+
* better fit than the Square provider's loyalty/recurring-order model,
|
|
18
|
+
* which omits this capability entirely). `catalogSync` is omitted, same
|
|
19
|
+
* as the Square provider's first cut.
|
|
20
|
+
*/
|
|
21
|
+
declare function createStripePaymentProvider(config: StripeProviderConfig): PaymentProvider;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { StripeProviderConfig, createStripePaymentProvider };
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/provider.ts"],"mappings":";;;UAkCiB,oBAAA;EACf,SAAA;EADe;EAGf,UAAA;;EAEA,uBAAA;AAAA;;;;AAAuB;AAoUzB;;;;;;iBAAgB,2BAAA,CACd,MAAA,EAAQ,oBAAA,GACP,eAAe"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
//#region src/provider.ts
|
|
2
|
+
const DEFAULT_API_VERSION = "2024-12-18.acacia";
|
|
3
|
+
const DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
|
|
4
|
+
const BASE_URL = "https://api.stripe.com";
|
|
5
|
+
var StripeApiError = class extends Error {
|
|
6
|
+
status;
|
|
7
|
+
body;
|
|
8
|
+
constructor(message, status, body) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.name = "StripeApiError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function toFormBody(params, prefix) {
|
|
16
|
+
const body = new URLSearchParams();
|
|
17
|
+
const append = (key, value) => {
|
|
18
|
+
if (value === void 0 || value === null) return;
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
value.forEach((item, index) => {
|
|
21
|
+
for (const [k, v] of toFormBody({ [String(index)]: item }, key).entries()) body.append(k, v);
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "object") {
|
|
26
|
+
for (const [k, v] of toFormBody(value, key).entries()) body.append(k, v);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
body.append(key, String(value));
|
|
30
|
+
};
|
|
31
|
+
for (const [key, value] of Object.entries(params)) append(prefix ? `${prefix}[${key}]` : key, value);
|
|
32
|
+
return body;
|
|
33
|
+
}
|
|
34
|
+
async function stripeFetch(config, path, init) {
|
|
35
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
36
|
+
if (init.query) for (const [key, value] of Object.entries(init.query)) url.searchParams.set(key, value);
|
|
37
|
+
const headers = {
|
|
38
|
+
Authorization: `Bearer ${config.secretKey}`,
|
|
39
|
+
"Stripe-Version": config.apiVersion ?? DEFAULT_API_VERSION
|
|
40
|
+
};
|
|
41
|
+
if (init.idempotencyKey) headers["Idempotency-Key"] = init.idempotencyKey;
|
|
42
|
+
if (init.body) headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
method: init.method,
|
|
45
|
+
headers,
|
|
46
|
+
body: init.body ? toFormBody(init.body) : void 0
|
|
47
|
+
});
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
50
|
+
if (!response.ok) throw new StripeApiError(`Stripe API request to "${path}" failed with status ${response.status}`, response.status, parsed);
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
async function checkCatalogPrices(config, refs) {
|
|
54
|
+
return Promise.all(refs.map(async (catalogRef) => {
|
|
55
|
+
const price = await stripeFetch(config, `/v1/prices/${catalogRef}`, { method: "GET" });
|
|
56
|
+
return {
|
|
57
|
+
catalogRef,
|
|
58
|
+
serverUnitPrice: {
|
|
59
|
+
amount: price.unit_amount ?? 0,
|
|
60
|
+
currency: (price.currency ?? "usd").toUpperCase()
|
|
61
|
+
},
|
|
62
|
+
availableQuantity: void 0
|
|
63
|
+
};
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
async function findOrCreateCustomer(config, email, idempotencyKey) {
|
|
67
|
+
const existing = (await stripeFetch(config, "/v1/customers", {
|
|
68
|
+
method: "GET",
|
|
69
|
+
query: {
|
|
70
|
+
email,
|
|
71
|
+
limit: "1"
|
|
72
|
+
}
|
|
73
|
+
})).data?.[0];
|
|
74
|
+
if (existing) return existing.id;
|
|
75
|
+
return (await stripeFetch(config, "/v1/customers", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
body: { email },
|
|
78
|
+
idempotencyKey
|
|
79
|
+
})).id;
|
|
80
|
+
}
|
|
81
|
+
function mapStripePaymentIntentStatus(status) {
|
|
82
|
+
if (status === "succeeded") return "succeeded";
|
|
83
|
+
if (status === "requires_action" || status === "requires_confirmation") return "requires_action";
|
|
84
|
+
return "failed";
|
|
85
|
+
}
|
|
86
|
+
async function checkout(config, request) {
|
|
87
|
+
const amount = request.lineItems.reduce((sum, item) => sum + item.clientUnitPrice.amount * item.quantity, 0);
|
|
88
|
+
const currency = (request.lineItems[0]?.clientUnitPrice.currency ?? "USD").toLowerCase();
|
|
89
|
+
const paymentIntent = await stripeFetch(config, "/v1/payment_intents", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: {
|
|
92
|
+
amount,
|
|
93
|
+
currency,
|
|
94
|
+
payment_method: request.paymentSourceToken,
|
|
95
|
+
confirm: true,
|
|
96
|
+
automatic_payment_methods: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
allow_redirects: "never"
|
|
99
|
+
},
|
|
100
|
+
metadata: request.metadata
|
|
101
|
+
},
|
|
102
|
+
idempotencyKey: request.idempotencyKey
|
|
103
|
+
});
|
|
104
|
+
const id = paymentIntent.id;
|
|
105
|
+
return {
|
|
106
|
+
providerOrderRef: id,
|
|
107
|
+
providerPaymentRef: id,
|
|
108
|
+
status: mapStripePaymentIntentStatus(paymentIntent.status),
|
|
109
|
+
amount: {
|
|
110
|
+
amount: paymentIntent.amount,
|
|
111
|
+
currency: (paymentIntent.currency ?? currency).toUpperCase()
|
|
112
|
+
},
|
|
113
|
+
raw: paymentIntent
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function timingSafeEqual(a, b) {
|
|
117
|
+
if (a.length !== b.length) return false;
|
|
118
|
+
let mismatch = 0;
|
|
119
|
+
for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
120
|
+
return mismatch === 0;
|
|
121
|
+
}
|
|
122
|
+
async function hmacSha256Hex(message, secret) {
|
|
123
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
|
|
124
|
+
name: "HMAC",
|
|
125
|
+
hash: "SHA-256"
|
|
126
|
+
}, false, ["sign"]);
|
|
127
|
+
const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
|
|
128
|
+
return Array.from(new Uint8Array(signature), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
129
|
+
}
|
|
130
|
+
function createVerifyWebhookSignature(config) {
|
|
131
|
+
return async ({ rawBody, headers, secret }) => {
|
|
132
|
+
const header = headers.get("stripe-signature");
|
|
133
|
+
if (!header) return false;
|
|
134
|
+
const parts = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const part of header.split(",")) {
|
|
136
|
+
const [key, value] = part.split("=");
|
|
137
|
+
if (key && value) parts.set(key, value);
|
|
138
|
+
}
|
|
139
|
+
const timestamp = parts.get("t");
|
|
140
|
+
const signature = parts.get("v1");
|
|
141
|
+
if (!timestamp || !signature) return false;
|
|
142
|
+
const tolerance = config.webhookToleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;
|
|
143
|
+
if (Math.abs(Date.now() / 1e3 - Number.parseInt(timestamp, 10)) > tolerance) return false;
|
|
144
|
+
return timingSafeEqual(await hmacSha256Hex(`${timestamp}.${rawBody}`, secret), signature);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function parseWebhookEvent(rawBody) {
|
|
148
|
+
const payload = JSON.parse(rawBody);
|
|
149
|
+
const eventId = payload.id;
|
|
150
|
+
const object = payload.data.object;
|
|
151
|
+
if (payload.type === "payment_intent.succeeded") return {
|
|
152
|
+
eventId,
|
|
153
|
+
event: {
|
|
154
|
+
kind: "payment.updated",
|
|
155
|
+
providerPaymentRef: object.id,
|
|
156
|
+
status: "succeeded"
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
if (payload.type === "payment_intent.payment_failed") return {
|
|
160
|
+
eventId,
|
|
161
|
+
event: {
|
|
162
|
+
kind: "payment.updated",
|
|
163
|
+
providerPaymentRef: object.id,
|
|
164
|
+
status: "failed"
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
if (payload.type === "charge.refunded") return {
|
|
168
|
+
eventId,
|
|
169
|
+
event: {
|
|
170
|
+
kind: "payment.updated",
|
|
171
|
+
providerPaymentRef: object.payment_intent,
|
|
172
|
+
status: "refunded"
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
if (payload.type === "customer.subscription.updated" || payload.type === "customer.subscription.created") return {
|
|
176
|
+
eventId,
|
|
177
|
+
event: {
|
|
178
|
+
kind: "subscription.updated",
|
|
179
|
+
providerSubscriptionRef: object.id,
|
|
180
|
+
status: object.status
|
|
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 Stripe's REST API — raw `fetch()`
|
|
193
|
+
* + `crypto.subtle`, no Stripe Node SDK. Implements the required
|
|
194
|
+
* `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/
|
|
195
|
+
* `verifyWebhookSignature`/`parseWebhookEvent`, plus the optional
|
|
196
|
+
* `subscriptions` capability via Stripe's native Subscriptions API (a
|
|
197
|
+
* better fit than the Square provider's loyalty/recurring-order model,
|
|
198
|
+
* which omits this capability entirely). `catalogSync` is omitted, same
|
|
199
|
+
* as the Square provider's first cut.
|
|
200
|
+
*/
|
|
201
|
+
function createStripePaymentProvider(config) {
|
|
202
|
+
return {
|
|
203
|
+
name: "stripe",
|
|
204
|
+
checkCatalogPrices: (refs) => checkCatalogPrices(config, refs),
|
|
205
|
+
findOrCreateCustomer: (email, idempotencyKey) => findOrCreateCustomer(config, email, idempotencyKey),
|
|
206
|
+
checkout: (request) => checkout(config, request),
|
|
207
|
+
verifyWebhookSignature: createVerifyWebhookSignature(config),
|
|
208
|
+
parseWebhookEvent,
|
|
209
|
+
subscriptions: {
|
|
210
|
+
async create(args) {
|
|
211
|
+
const subscription = await stripeFetch(config, "/v1/subscriptions", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
body: {
|
|
214
|
+
customer: args.customerRef,
|
|
215
|
+
items: [{ price: args.planRef }]
|
|
216
|
+
},
|
|
217
|
+
idempotencyKey: args.idempotencyKey
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
providerSubscriptionRef: subscription.id,
|
|
221
|
+
status: subscription.status
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
async cancel(providerSubscriptionRef) {
|
|
225
|
+
await stripeFetch(config, `/v1/subscriptions/${providerSubscriptionRef}`, { method: "DELETE" });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
export { createStripePaymentProvider };
|
|
232
|
+
|
|
233
|
+
//# 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// Stripe's REST API directly via fetch() — never the `stripe` npm SDK\n// (Node-targeted; relies on node:crypto/node:http internally for some\n// operations). Webhook signature verification uses crypto.subtle.\n//\n// Real interface friction vs. the Square provider (PaymentProvider was\n// pressure-tested against this on purpose, per the build plan — these\n// aren't bugs, they're the asymmetry the interface has to absorb):\n// 1. Stripe's API takes `application/x-www-form-urlencoded` bodies, not\n// JSON — see `toFormBody` below.\n// 2. Stripe's idempotency key is an `Idempotency-Key` HTTP header, not a\n// body field the way Square's `idempotency_key` is.\n// 3. Stripe has no object analogous to Square's separate Order — a\n// `PaymentIntent` is both \"the order\" and \"the payment\" in one. This\n// provider sets `providerOrderRef` and `providerPaymentRef` to the\n// same PaymentIntent id rather than inventing a fake second id.\n// 4. Stripe has no native inventory/catalog concept matching Square's\n// Catalog+Inventory APIs — `checkCatalogPrices` reads `Price` objects\n// (`unit_amount`/`currency`) and always returns `availableQuantity:\n// undefined` (Stripe doesn't track it, so there's nothing honest to\n// report).\n// 5. Stripe's native Subscriptions API is a better fit for the optional\n// `subscriptions` capability than Square's loyalty/recurring-order\n// model — implemented here, omitted in the Square provider.\n\nimport type {\n CatalogPriceCheck,\n CheckoutRequest,\n CheckoutResult,\n PaymentProvider,\n} from \"@thebes/cadmea-plugin-ecommerce\";\n\nexport interface StripeProviderConfig {\n secretKey: string;\n /** Stripe's pinned API version header (`Stripe-Version`). Default: a fixed, tested version — bump deliberately, not implicitly. */\n apiVersion?: string;\n /** Seconds of clock skew tolerated on a webhook's `t=` timestamp before it's rejected as stale. Default: 300 (Stripe's own recommended tolerance). */\n webhookToleranceSeconds?: number;\n}\n\nconst DEFAULT_API_VERSION = \"2024-12-18.acacia\";\nconst DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;\nconst BASE_URL = \"https://api.stripe.com\";\n\nclass StripeApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message);\n this.name = \"StripeApiError\";\n }\n}\n\n// Stripe's form-encoding for nested objects uses bracket notation\n// (`items[0][price]=...`) — this plugin's own usage is shallow enough\n// that a small recursive flattener covers it without needing a general\n// qs-style library dependency.\nfunction toFormBody(\n params: Record<string, unknown>,\n prefix?: string,\n): URLSearchParams {\n const body = new URLSearchParams();\n const append = (key: string, value: unknown) => {\n if (value === undefined || value === null) return;\n if (Array.isArray(value)) {\n value.forEach((item, index) => {\n for (const [k, v] of toFormBody(\n { [String(index)]: item },\n key,\n ).entries()) {\n body.append(k, v);\n }\n });\n return;\n }\n if (typeof value === \"object\") {\n for (const [k, v] of toFormBody(\n value as Record<string, unknown>,\n key,\n ).entries()) {\n body.append(k, v);\n }\n return;\n }\n body.append(key, String(value));\n };\n for (const [key, value] of Object.entries(params)) {\n append(prefix ? `${prefix}[${key}]` : key, value);\n }\n return body;\n}\n\nasync function stripeFetch(\n config: StripeProviderConfig,\n path: string,\n init: {\n method: string;\n body?: Record<string, unknown>;\n idempotencyKey?: string;\n query?: Record<string, string>;\n },\n): Promise<Record<string, unknown>> {\n const url = new URL(`${BASE_URL}${path}`);\n if (init.query) {\n for (const [key, value] of Object.entries(init.query)) {\n url.searchParams.set(key, value);\n }\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${config.secretKey}`,\n \"Stripe-Version\": config.apiVersion ?? DEFAULT_API_VERSION,\n };\n if (init.idempotencyKey) headers[\"Idempotency-Key\"] = init.idempotencyKey;\n if (init.body) headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\";\n\n const response = await fetch(url, {\n method: init.method,\n headers,\n body: init.body ? toFormBody(init.body) : undefined,\n });\n const text = await response.text();\n const parsed = text ? JSON.parse(text) : {};\n if (!response.ok) {\n throw new StripeApiError(\n `Stripe API request to \"${path}\" failed with status ${response.status}`,\n response.status,\n parsed,\n );\n }\n return parsed;\n}\n\nasync function checkCatalogPrices(\n config: StripeProviderConfig,\n refs: string[],\n): Promise<CatalogPriceCheck[]> {\n return Promise.all(\n refs.map(async (catalogRef) => {\n const price = await stripeFetch(config, `/v1/prices/${catalogRef}`, {\n method: \"GET\",\n });\n return {\n catalogRef,\n serverUnitPrice: {\n amount: (price.unit_amount as number) ?? 0,\n currency: ((price.currency as string) ?? \"usd\").toUpperCase(),\n },\n // Stripe has no native inventory concept — nothing honest to report.\n availableQuantity: undefined,\n };\n }),\n );\n}\n\nasync function findOrCreateCustomer(\n config: StripeProviderConfig,\n email: string,\n idempotencyKey: string,\n): Promise<string> {\n const list = await stripeFetch(config, \"/v1/customers\", {\n method: \"GET\",\n query: { email, limit: \"1\" },\n });\n const existing = (list.data as Array<{ id: string }> | undefined)?.[0];\n if (existing) return existing.id;\n\n const created = await stripeFetch(config, \"/v1/customers\", {\n method: \"POST\",\n body: { email },\n idempotencyKey,\n });\n return created.id as string;\n}\n\nfunction mapStripePaymentIntentStatus(\n status: string | undefined,\n): CheckoutResult[\"status\"] {\n if (status === \"succeeded\") return \"succeeded\";\n if (status === \"requires_action\" || status === \"requires_confirmation\") {\n return \"requires_action\";\n }\n return \"failed\";\n}\n\nasync function checkout(\n config: StripeProviderConfig,\n request: CheckoutRequest,\n): Promise<CheckoutResult> {\n const amount = request.lineItems.reduce(\n (sum, item) => sum + item.clientUnitPrice.amount * item.quantity,\n 0,\n );\n const currency = (\n request.lineItems[0]?.clientUnitPrice.currency ?? \"USD\"\n ).toLowerCase();\n\n // One call, confirmed immediately — Stripe's PaymentIntent is both \"the\n // order\" and \"the charge\" at once, unlike Square's separate Orders +\n // Payments calls.\n const paymentIntent = await stripeFetch(config, \"/v1/payment_intents\", {\n method: \"POST\",\n body: {\n amount,\n currency,\n payment_method: request.paymentSourceToken,\n confirm: true,\n // automatic_payment_methods.allow_redirects: \"never\" keeps this a\n // single synchronous confirm — redirect-based methods would need a\n // requires_action round trip this checkout flow doesn't implement.\n automatic_payment_methods: { enabled: true, allow_redirects: \"never\" },\n metadata: request.metadata,\n },\n idempotencyKey: request.idempotencyKey,\n });\n\n const id = paymentIntent.id as string;\n return {\n providerOrderRef: id,\n providerPaymentRef: id,\n status: mapStripePaymentIntentStatus(paymentIntent.status as string),\n amount: {\n amount: paymentIntent.amount as number,\n currency: ((paymentIntent.currency as string) ?? currency).toUpperCase(),\n },\n raw: paymentIntent,\n };\n}\n\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\nasync function hmacSha256Hex(message: string, secret: string): 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 Array.from(new Uint8Array(signature), (b) =>\n b.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n}\n\nfunction createVerifyWebhookSignature(\n config: StripeProviderConfig,\n): PaymentProvider[\"verifyWebhookSignature\"] {\n return async ({ rawBody, headers, secret }) => {\n const header = headers.get(\"stripe-signature\");\n if (!header) return false;\n\n // Header format: \"t=<timestamp>,v1=<signature>[,v0=<old_signature>]\" —\n // parse rather than regex-matching positionally, since Stripe doesn't\n // guarantee component order.\n const parts = new Map<string, string>();\n for (const part of header.split(\",\")) {\n const [key, value] = part.split(\"=\");\n if (key && value) parts.set(key, value);\n }\n const timestamp = parts.get(\"t\");\n const signature = parts.get(\"v1\");\n if (!timestamp || !signature) return false;\n\n const tolerance =\n config.webhookToleranceSeconds ?? DEFAULT_WEBHOOK_TOLERANCE_SECONDS;\n const age = Math.abs(Date.now() / 1000 - Number.parseInt(timestamp, 10));\n if (age > tolerance) return false;\n\n const expected = await hmacSha256Hex(`${timestamp}.${rawBody}`, secret);\n return timingSafeEqual(expected, signature);\n };\n}\n\ninterface StripeWebhookPayload {\n id: string;\n type: string;\n data: { object: Record<string, unknown> };\n}\n\nfunction parseWebhookEvent(rawBody: string) {\n const payload = JSON.parse(rawBody) as StripeWebhookPayload;\n const eventId = payload.id;\n const object = payload.data.object;\n\n if (payload.type === \"payment_intent.succeeded\") {\n return {\n eventId,\n event: {\n kind: \"payment.updated\" as const,\n providerPaymentRef: object.id as string,\n status: \"succeeded\" as const,\n },\n };\n }\n\n if (payload.type === \"payment_intent.payment_failed\") {\n return {\n eventId,\n event: {\n kind: \"payment.updated\" as const,\n providerPaymentRef: object.id as string,\n status: \"failed\" as const,\n },\n };\n }\n\n if (payload.type === \"charge.refunded\") {\n return {\n eventId,\n event: {\n kind: \"payment.updated\" as const,\n providerPaymentRef: object.payment_intent as string,\n status: \"refunded\" as const,\n },\n };\n }\n\n if (\n payload.type === \"customer.subscription.updated\" ||\n payload.type === \"customer.subscription.created\"\n ) {\n return {\n eventId,\n event: {\n kind: \"subscription.updated\" as const,\n providerSubscriptionRef: object.id as string,\n status: object.status as string,\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 Stripe's REST API — raw `fetch()`\n * + `crypto.subtle`, no Stripe Node SDK. Implements the required\n * `checkCatalogPrices`/`findOrCreateCustomer`/`checkout`/\n * `verifyWebhookSignature`/`parseWebhookEvent`, plus the optional\n * `subscriptions` capability via Stripe's native Subscriptions API (a\n * better fit than the Square provider's loyalty/recurring-order model,\n * which omits this capability entirely). `catalogSync` is omitted, same\n * as the Square provider's first cut.\n */\nexport function createStripePaymentProvider(\n config: StripeProviderConfig,\n): PaymentProvider {\n return {\n name: \"stripe\",\n checkCatalogPrices: (refs) => checkCatalogPrices(config, refs),\n findOrCreateCustomer: (email, idempotencyKey) =>\n findOrCreateCustomer(config, email, idempotencyKey),\n checkout: (request) => checkout(config, request),\n verifyWebhookSignature: createVerifyWebhookSignature(config),\n parseWebhookEvent,\n subscriptions: {\n async create(args) {\n const subscription = await stripeFetch(config, \"/v1/subscriptions\", {\n method: \"POST\",\n body: {\n customer: args.customerRef,\n items: [{ price: args.planRef }],\n },\n idempotencyKey: args.idempotencyKey,\n });\n return {\n providerSubscriptionRef: subscription.id as string,\n status: subscription.status as string,\n };\n },\n async cancel(providerSubscriptionRef) {\n await stripeFetch(\n config,\n `/v1/subscriptions/${providerSubscriptionRef}`,\n { method: \"DELETE\" },\n );\n },\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thebes/cadmea-plugin-ecommerce-stripe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Stripe PaymentProvider for @thebes/cadmea-plugin-ecommerce — raw fetch() + crypto.subtle, no Stripe 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-stripe"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/bowenlabs/project-thebes/tree/main/packages/cadmea-plugin-ecommerce-stripe#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/bowenlabs/project-thebes/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"cadmea",
|
|
18
|
+
"cadmus",
|
|
19
|
+
"cms",
|
|
20
|
+
"ecommerce",
|
|
21
|
+
"stripe",
|
|
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/cadmea-plugin-ecommerce": "^1.0.0",
|
|
45
|
+
"@thebes/cadmus": "^0.2.1"
|
|
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
|
+
}
|