@bubltec/mycota-payments 0.1.1-next.18
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/dist/fake.adapter.d.ts +22 -0
- package/dist/fake.adapter.d.ts.map +1 -0
- package/dist/fake.adapter.js +53 -0
- package/dist/fake.adapter.js.map +1 -0
- package/dist/fake.adapter.spec.d.ts +2 -0
- package/dist/fake.adapter.spec.d.ts.map +1 -0
- package/dist/fake.adapter.spec.js +39 -0
- package/dist/fake.adapter.spec.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/money.d.ts +7 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +13 -0
- package/dist/money.js.map +1 -0
- package/dist/money.spec.d.ts +2 -0
- package/dist/money.spec.d.ts.map +1 -0
- package/dist/money.spec.js +14 -0
- package/dist/money.spec.js.map +1 -0
- package/dist/ports.d.ts +61 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/stripe.adapter.d.ts +47 -0
- package/dist/stripe.adapter.d.ts.map +1 -0
- package/dist/stripe.adapter.js +96 -0
- package/dist/stripe.adapter.js.map +1 -0
- package/dist/stripe.adapter.spec.d.ts +2 -0
- package/dist/stripe.adapter.spec.d.ts.map +1 -0
- package/dist/stripe.adapter.spec.js +74 -0
- package/dist/stripe.adapter.spec.js.map +1 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Genome
|
|
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.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Money } from './money.js';
|
|
2
|
+
import type { CheckoutSession, CreateCheckoutInput, PaymentEvent, PaymentGateway, RefundInput, RefundResult } from './ports.js';
|
|
3
|
+
export interface FakePaymentGatewayOptions {
|
|
4
|
+
/** If set, `parseWebhook` treats this string as a valid signature. */
|
|
5
|
+
webhookSecret?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* In-memory gateway for local boot and tests. Checkout URLs are fake; the
|
|
9
|
+
* consuming app can complete a payment by posting a signed webhook body.
|
|
10
|
+
*/
|
|
11
|
+
export declare class FakePaymentGateway implements PaymentGateway {
|
|
12
|
+
private readonly options;
|
|
13
|
+
readonly provider = "fake";
|
|
14
|
+
private readonly sessions;
|
|
15
|
+
private seq;
|
|
16
|
+
constructor(options?: FakePaymentGatewayOptions);
|
|
17
|
+
createCheckout(input: CreateCheckoutInput): Promise<CheckoutSession>;
|
|
18
|
+
refund(input: RefundInput): Promise<RefundResult>;
|
|
19
|
+
parseWebhook(rawBody: string, signature: string): Promise<PaymentEvent | null>;
|
|
20
|
+
checkoutTotal(id: string): Money | undefined;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=fake.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake.adapter.d.ts","sourceRoot":"","sources":["../src/fake.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,cAAc,EACd,WAAW,EACX,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,yBAAyB;IACxC,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,cAAc;IAK3C,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,QAAQ,CAAC,QAAQ,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;IACnE,OAAO,CAAC,GAAG,CAAK;gBAEa,OAAO,GAAE,yBAA8B;IAE9D,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC;IAUpE,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAIjD,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA4BpF,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;CAS7C"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { addMoney, money } from './money.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory gateway for local boot and tests. Checkout URLs are fake; the
|
|
4
|
+
* consuming app can complete a payment by posting a signed webhook body.
|
|
5
|
+
*/
|
|
6
|
+
export class FakePaymentGateway {
|
|
7
|
+
options;
|
|
8
|
+
provider = 'fake';
|
|
9
|
+
sessions = new Map();
|
|
10
|
+
seq = 0;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
async createCheckout(input) {
|
|
15
|
+
if (input.lines.length === 0) {
|
|
16
|
+
throw new Error('checkout requires at least one line');
|
|
17
|
+
}
|
|
18
|
+
this.seq += 1;
|
|
19
|
+
const id = `cs_fake_${this.seq}`;
|
|
20
|
+
this.sessions.set(id, input);
|
|
21
|
+
return { id, url: `https://payments.local/checkout/${id}` };
|
|
22
|
+
}
|
|
23
|
+
async refund(input) {
|
|
24
|
+
return { id: `re_fake_${input.paymentId}`, status: 'succeeded' };
|
|
25
|
+
}
|
|
26
|
+
async parseWebhook(rawBody, signature) {
|
|
27
|
+
const expected = this.options.webhookSecret;
|
|
28
|
+
if (expected && signature !== expected) {
|
|
29
|
+
throw new Error('invalid webhook signature');
|
|
30
|
+
}
|
|
31
|
+
const body = JSON.parse(rawBody);
|
|
32
|
+
const paymentId = body.paymentId ?? '';
|
|
33
|
+
const metadata = body.metadata ?? {};
|
|
34
|
+
const amount = money(body.amountCents ?? 0, body.currency ?? 'usd');
|
|
35
|
+
if (body.type === 'checkout.completed') {
|
|
36
|
+
return { type: 'checkout.completed', paymentId, amount, metadata };
|
|
37
|
+
}
|
|
38
|
+
if (body.type === 'checkout.failed') {
|
|
39
|
+
return { type: 'checkout.failed', paymentId, metadata };
|
|
40
|
+
}
|
|
41
|
+
if (body.type === 'charge.refunded') {
|
|
42
|
+
return { type: 'charge.refunded', paymentId, amount };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
checkoutTotal(id) {
|
|
47
|
+
const session = this.sessions.get(id);
|
|
48
|
+
if (!session)
|
|
49
|
+
return undefined;
|
|
50
|
+
return session.lines.reduce((sum, line) => addMoney(sum, money(line.unitAmount.amountCents * line.quantity, line.unitAmount.currency)), money(0, session.lines[0]?.unitAmount.currency ?? 'usd'));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=fake.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake.adapter.js","sourceRoot":"","sources":["../src/fake.adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake.adapter.spec.d.ts","sourceRoot":"","sources":["../src/fake.adapter.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { FakePaymentGateway } from './fake.adapter.js';
|
|
3
|
+
import { money } from './money.js';
|
|
4
|
+
describe('FakePaymentGateway', () => {
|
|
5
|
+
it('creates a checkout and totals the lines', async () => {
|
|
6
|
+
const gateway = new FakePaymentGateway();
|
|
7
|
+
const session = await gateway.createCheckout({
|
|
8
|
+
successUrl: 'https://app.local/ok',
|
|
9
|
+
cancelUrl: 'https://app.local/no',
|
|
10
|
+
metadata: { orderId: 'ord_1' },
|
|
11
|
+
lines: [
|
|
12
|
+
{ name: 'GA', quantity: 2, unitAmount: money(3000) },
|
|
13
|
+
{ name: 'Tee', quantity: 1, unitAmount: money(2500) },
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
expect(session.id).toMatch(/^cs_fake_/);
|
|
17
|
+
expect(gateway.checkoutTotal(session.id)).toEqual(money(8500));
|
|
18
|
+
});
|
|
19
|
+
it('rejects unsigned webhooks when a secret is configured', async () => {
|
|
20
|
+
const gateway = new FakePaymentGateway({ webhookSecret: 'whsec_test' });
|
|
21
|
+
await expect(gateway.parseWebhook(JSON.stringify({ type: 'checkout.completed' }), 'nope')).rejects.toThrow(/invalid webhook signature/);
|
|
22
|
+
});
|
|
23
|
+
it('parses a completed checkout webhook', async () => {
|
|
24
|
+
const gateway = new FakePaymentGateway({ webhookSecret: 'whsec_test' });
|
|
25
|
+
const event = await gateway.parseWebhook(JSON.stringify({
|
|
26
|
+
type: 'checkout.completed',
|
|
27
|
+
paymentId: 'pi_1',
|
|
28
|
+
amountCents: 8500,
|
|
29
|
+
metadata: { orderId: 'ord_1' },
|
|
30
|
+
}), 'whsec_test');
|
|
31
|
+
expect(event).toEqual({
|
|
32
|
+
type: 'checkout.completed',
|
|
33
|
+
paymentId: 'pi_1',
|
|
34
|
+
amount: money(8500),
|
|
35
|
+
metadata: { orderId: 'ord_1' },
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=fake.adapter.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake.adapter.spec.js","sourceRoot":"","sources":["../src/fake.adapter.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,OAAO,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC;YAC3C,UAAU,EAAE,sBAAsB;YAClC,SAAS,EAAE,sBAAsB;YACjC,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE;YAC9B,KAAK,EAAE;gBACL,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE;gBACpD,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE;aACtD;SACF,CAAC,CAAC;QACH,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACxC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,OAAO,GAAG,IAAI,kBAAkB,CAAC,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC,CAAC;QACxE,MAAM,MAAM,CACV,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC,EAAE,MAAM,CAAC,CAC7E,CAAC,OAAO,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,MAAM,OAAO,GAAG,IAAI,kBAAkB,CAAC,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC,CAAC;QACxE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,YAAY,CACtC,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,oBAAoB;YAC1B,SAAS,EAAE,MAAM;YACjB,WAAW,EAAE,IAAI;YACjB,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE;SAC/B,CAAC,EACF,YAAY,CACb,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;YACpB,IAAI,EAAE,oBAAoB;YAC1B,SAAS,EAAE,MAAM;YACjB,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC;YACnB,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE;SAC/B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { addMoney, money, type Money } from './money.js';
|
|
2
|
+
export type { CheckoutLine, CheckoutSession, CreateCheckoutInput, PaymentEvent, PaymentGateway, PaymentGatewayConfig, RefundInput, RefundResult, } from './ports.js';
|
|
3
|
+
export { FakePaymentGateway, type FakePaymentGatewayOptions } from './fake.adapter.js';
|
|
4
|
+
export { StripePaymentGateway, type StripeCheckoutClient, type StripeWebhookEvent, } from './stripe.adapter.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EACV,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,cAAc,EACd,oBAAoB,EACpB,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EACL,oBAAoB,EACpB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,GACxB,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAc,MAAM,YAAY,CAAC;AAWzD,OAAO,EAAE,kBAAkB,EAAkC,MAAM,mBAAmB,CAAC;AACvF,OAAO,EACL,oBAAoB,GAGrB,MAAM,qBAAqB,CAAC"}
|
package/dist/money.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,SAAQ,GAAG,KAAK,CAKlE;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAKlD"}
|
package/dist/money.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function money(amountCents, currency = 'usd') {
|
|
2
|
+
if (!Number.isInteger(amountCents) || amountCents < 0) {
|
|
3
|
+
throw new Error('amountCents must be a non-negative integer');
|
|
4
|
+
}
|
|
5
|
+
return { amountCents, currency: currency.toLowerCase() };
|
|
6
|
+
}
|
|
7
|
+
export function addMoney(a, b) {
|
|
8
|
+
if (a.currency !== b.currency) {
|
|
9
|
+
throw new Error(`currency mismatch: ${a.currency} vs ${b.currency}`);
|
|
10
|
+
}
|
|
11
|
+
return money(a.amountCents + b.amountCents, a.currency);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=money.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.js","sourceRoot":"","sources":["../src/money.ts"],"names":[],"mappings":"AAKA,MAAM,UAAU,KAAK,CAAC,WAAmB,EAAE,QAAQ,GAAG,KAAK;IACzD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,CAAQ,EAAE,CAAQ;IACzC,IAAI,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC,QAAQ,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.spec.d.ts","sourceRoot":"","sources":["../src/money.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { addMoney, money } from './money.js';
|
|
3
|
+
describe('money', () => {
|
|
4
|
+
it('rejects fractional cents', () => {
|
|
5
|
+
expect(() => money(1.5)).toThrow(/non-negative integer/);
|
|
6
|
+
});
|
|
7
|
+
it('adds same-currency amounts', () => {
|
|
8
|
+
expect(addMoney(money(100, 'usd'), money(50, 'usd'))).toEqual(money(150, 'usd'));
|
|
9
|
+
});
|
|
10
|
+
it('refuses to mix currencies', () => {
|
|
11
|
+
expect(() => addMoney(money(100, 'usd'), money(50, 'eur'))).toThrow(/currency mismatch/);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
//# sourceMappingURL=money.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.spec.js","sourceRoot":"","sources":["../src/money.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE7C,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;IACrB,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC3F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Money } from './money.js';
|
|
2
|
+
export interface CheckoutLine {
|
|
3
|
+
name: string;
|
|
4
|
+
quantity: number;
|
|
5
|
+
unitAmount: Money;
|
|
6
|
+
}
|
|
7
|
+
export interface CreateCheckoutInput {
|
|
8
|
+
/** Connected-account id when the platform is splitting funds (Stripe Connect, Adyen for Platforms). */
|
|
9
|
+
connectedAccountId?: string;
|
|
10
|
+
/** Platform take, in the same currency as the lines. */
|
|
11
|
+
applicationFee?: Money;
|
|
12
|
+
successUrl: string;
|
|
13
|
+
cancelUrl: string;
|
|
14
|
+
customerEmail?: string;
|
|
15
|
+
lines: CheckoutLine[];
|
|
16
|
+
metadata: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export interface CheckoutSession {
|
|
19
|
+
id: string;
|
|
20
|
+
url: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RefundInput {
|
|
23
|
+
paymentId: string;
|
|
24
|
+
amount?: Money;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RefundResult {
|
|
28
|
+
id: string;
|
|
29
|
+
status: 'pending' | 'succeeded' | 'failed';
|
|
30
|
+
}
|
|
31
|
+
export type PaymentEvent = {
|
|
32
|
+
type: 'checkout.completed';
|
|
33
|
+
paymentId: string;
|
|
34
|
+
amount: Money;
|
|
35
|
+
metadata: Record<string, string>;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'checkout.failed';
|
|
38
|
+
paymentId: string;
|
|
39
|
+
metadata: Record<string, string>;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'charge.refunded';
|
|
42
|
+
paymentId: string;
|
|
43
|
+
amount: Money;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Outbound port for taking money, refunding it, and translating provider
|
|
47
|
+
* webhooks into domain events. Inner layers depend on this, never on Stripe.
|
|
48
|
+
*/
|
|
49
|
+
export interface PaymentGateway {
|
|
50
|
+
readonly provider: string;
|
|
51
|
+
createCheckout(input: CreateCheckoutInput): Promise<CheckoutSession>;
|
|
52
|
+
refund(input: RefundInput): Promise<RefundResult>;
|
|
53
|
+
parseWebhook(rawBody: string, signature: string): Promise<PaymentEvent | null>;
|
|
54
|
+
}
|
|
55
|
+
export interface PaymentGatewayConfig {
|
|
56
|
+
secretKey: string;
|
|
57
|
+
webhookSecret: string;
|
|
58
|
+
/** Default currency when a Money value omits one. */
|
|
59
|
+
currency?: string;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,KAAK,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,uGAAuG;IACvG,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,cAAc,CAAC,EAAE,KAAK,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC5C;AAED,MAAM,MAAM,YAAY,GACpB;IACE,IAAI,EAAE,oBAAoB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,GACD;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,GACD;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,CAAC;CACf,CAAC;AAEN;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACrE,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAClD,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAChF;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CheckoutSession, CreateCheckoutInput, PaymentEvent, PaymentGateway, PaymentGatewayConfig, RefundInput, RefundResult } from './ports.js';
|
|
2
|
+
/**
|
|
3
|
+
* Narrow Stripe surface so this package does not take a hard dependency on
|
|
4
|
+
* the `stripe` SDK. The consuming app constructs the real client (or a test
|
|
5
|
+
* double) and passes it in.
|
|
6
|
+
*/
|
|
7
|
+
export interface StripeCheckoutClient {
|
|
8
|
+
checkout: {
|
|
9
|
+
sessions: {
|
|
10
|
+
create: (params: Record<string, unknown>) => Promise<{
|
|
11
|
+
id: string;
|
|
12
|
+
url: string | null;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
refunds: {
|
|
17
|
+
create: (params: Record<string, unknown>) => Promise<{
|
|
18
|
+
id: string;
|
|
19
|
+
status: string | null;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
webhooks: {
|
|
23
|
+
constructEvent: (body: string, signature: string, secret: string) => StripeWebhookEvent;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface StripeWebhookEvent {
|
|
27
|
+
type: string;
|
|
28
|
+
data: {
|
|
29
|
+
object: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Stripe adapter. Destination charges (platform is merchant of record, funds
|
|
34
|
+
* land on the connected account) are the default Connect shape for ticketing
|
|
35
|
+
* + merch marketplaces. Swap this class for an Adyen/Square adapter later
|
|
36
|
+
* without touching use cases.
|
|
37
|
+
*/
|
|
38
|
+
export declare class StripePaymentGateway implements PaymentGateway {
|
|
39
|
+
private readonly stripe;
|
|
40
|
+
private readonly config;
|
|
41
|
+
readonly provider = "stripe";
|
|
42
|
+
constructor(stripe: StripeCheckoutClient, config: PaymentGatewayConfig);
|
|
43
|
+
createCheckout(input: CreateCheckoutInput): Promise<CheckoutSession>;
|
|
44
|
+
refund(input: RefundInput): Promise<RefundResult>;
|
|
45
|
+
parseWebhook(rawBody: string, signature: string): Promise<PaymentEvent | null>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=stripe.adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.adapter.d.ts","sourceRoot":"","sources":["../src/stripe.adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,cAAc,EACd,oBAAoB,EACpB,WAAW,EACX,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE;QACR,QAAQ,EAAE;YACR,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;gBAAE,EAAE,EAAE,MAAM,CAAC;gBAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;aAAE,CAAC,CAAC;SAC1F,CAAC;KACH,CAAC;IACF,OAAO,EAAE;QACP,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC,CAAC;KAC7F,CAAC;IACF,QAAQ,EAAE;QACR,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,kBAAkB,CAAC;KACzF,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;CAC3C;AAMD;;;;;GAKG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IAIvD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJzB,QAAQ,CAAC,QAAQ,YAAY;gBAGV,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,oBAAoB;IAGzC,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC;IAuCpE,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAajD,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;CAgCrF"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
function toMajorUnit(amount) {
|
|
2
|
+
return amount.amountCents;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Stripe adapter. Destination charges (platform is merchant of record, funds
|
|
6
|
+
* land on the connected account) are the default Connect shape for ticketing
|
|
7
|
+
* + merch marketplaces. Swap this class for an Adyen/Square adapter later
|
|
8
|
+
* without touching use cases.
|
|
9
|
+
*/
|
|
10
|
+
export class StripePaymentGateway {
|
|
11
|
+
stripe;
|
|
12
|
+
config;
|
|
13
|
+
provider = 'stripe';
|
|
14
|
+
constructor(stripe, config) {
|
|
15
|
+
this.stripe = stripe;
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
async createCheckout(input) {
|
|
19
|
+
if (input.lines.length === 0) {
|
|
20
|
+
throw new Error('checkout requires at least one line');
|
|
21
|
+
}
|
|
22
|
+
const params = {
|
|
23
|
+
mode: 'payment',
|
|
24
|
+
success_url: input.successUrl,
|
|
25
|
+
cancel_url: input.cancelUrl,
|
|
26
|
+
customer_email: input.customerEmail,
|
|
27
|
+
metadata: input.metadata,
|
|
28
|
+
line_items: input.lines.map((line) => ({
|
|
29
|
+
quantity: line.quantity,
|
|
30
|
+
price_data: {
|
|
31
|
+
currency: line.unitAmount.currency,
|
|
32
|
+
unit_amount: toMajorUnit(line.unitAmount),
|
|
33
|
+
product_data: { name: line.name },
|
|
34
|
+
},
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
if (input.connectedAccountId) {
|
|
38
|
+
const paymentIntentData = {
|
|
39
|
+
transfer_data: { destination: input.connectedAccountId },
|
|
40
|
+
metadata: input.metadata,
|
|
41
|
+
};
|
|
42
|
+
if (input.applicationFee) {
|
|
43
|
+
paymentIntentData.application_fee_amount = input.applicationFee.amountCents;
|
|
44
|
+
}
|
|
45
|
+
params.payment_intent_data = paymentIntentData;
|
|
46
|
+
}
|
|
47
|
+
const session = await this.stripe.checkout.sessions.create(params);
|
|
48
|
+
if (!session.url) {
|
|
49
|
+
throw new Error('stripe checkout session missing url');
|
|
50
|
+
}
|
|
51
|
+
return { id: session.id, url: session.url };
|
|
52
|
+
}
|
|
53
|
+
async refund(input) {
|
|
54
|
+
const refund = await this.stripe.refunds.create({
|
|
55
|
+
payment_intent: input.paymentId,
|
|
56
|
+
amount: input.amount?.amountCents,
|
|
57
|
+
reason: input.reason,
|
|
58
|
+
});
|
|
59
|
+
const status = refund.status === 'succeeded' || refund.status === 'pending' || refund.status === 'failed'
|
|
60
|
+
? refund.status
|
|
61
|
+
: 'pending';
|
|
62
|
+
return { id: refund.id, status };
|
|
63
|
+
}
|
|
64
|
+
async parseWebhook(rawBody, signature) {
|
|
65
|
+
const event = this.stripe.webhooks.constructEvent(rawBody, signature, this.config.webhookSecret);
|
|
66
|
+
const object = event.data.object;
|
|
67
|
+
const metadata = object.metadata ?? {};
|
|
68
|
+
const currency = String(object.currency ?? this.config.currency ?? 'usd');
|
|
69
|
+
if (event.type === 'checkout.session.completed') {
|
|
70
|
+
const paymentId = String(object.payment_intent ?? object.id);
|
|
71
|
+
const amountCents = Number(object.amount_total ?? 0);
|
|
72
|
+
return {
|
|
73
|
+
type: 'checkout.completed',
|
|
74
|
+
paymentId,
|
|
75
|
+
amount: { amountCents, currency },
|
|
76
|
+
metadata,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (event.type === 'checkout.session.expired' || event.type === 'payment_intent.payment_failed') {
|
|
80
|
+
return {
|
|
81
|
+
type: 'checkout.failed',
|
|
82
|
+
paymentId: String(object.payment_intent ?? object.id),
|
|
83
|
+
metadata,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (event.type === 'charge.refunded') {
|
|
87
|
+
return {
|
|
88
|
+
type: 'charge.refunded',
|
|
89
|
+
paymentId: String(object.payment_intent ?? object.id),
|
|
90
|
+
amount: { amountCents: Number(object.amount_refunded ?? 0), currency },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=stripe.adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.adapter.js","sourceRoot":"","sources":["../src/stripe.adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.adapter.spec.d.ts","sourceRoot":"","sources":["../src/stripe.adapter.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { money } from './money.js';
|
|
3
|
+
import { StripePaymentGateway } from './stripe.adapter.js';
|
|
4
|
+
function stubClient(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
checkout: {
|
|
7
|
+
sessions: {
|
|
8
|
+
create: vi.fn(async () => ({ id: 'cs_test_1', url: 'https://checkout.stripe.com/c/pay/cs_test_1' })),
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
refunds: {
|
|
12
|
+
create: vi.fn(async () => ({ id: 're_test_1', status: 'succeeded' })),
|
|
13
|
+
},
|
|
14
|
+
webhooks: {
|
|
15
|
+
constructEvent: vi.fn(() => ({
|
|
16
|
+
type: 'checkout.session.completed',
|
|
17
|
+
data: {
|
|
18
|
+
object: {
|
|
19
|
+
id: 'cs_test_1',
|
|
20
|
+
payment_intent: 'pi_test_1',
|
|
21
|
+
amount_total: 3000,
|
|
22
|
+
currency: 'usd',
|
|
23
|
+
metadata: { orderId: 'ord_1' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
})),
|
|
27
|
+
},
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
describe('StripePaymentGateway', () => {
|
|
32
|
+
it('maps lines onto Stripe Checkout and destination-charges Connect accounts', async () => {
|
|
33
|
+
const client = stubClient();
|
|
34
|
+
const gateway = new StripePaymentGateway(client, {
|
|
35
|
+
secretKey: 'sk_test',
|
|
36
|
+
webhookSecret: 'whsec_test',
|
|
37
|
+
});
|
|
38
|
+
const session = await gateway.createCheckout({
|
|
39
|
+
connectedAccountId: 'acct_org',
|
|
40
|
+
applicationFee: money(150),
|
|
41
|
+
successUrl: 'https://app.local/ok',
|
|
42
|
+
cancelUrl: 'https://app.local/no',
|
|
43
|
+
metadata: { orderId: 'ord_1' },
|
|
44
|
+
lines: [{ name: 'GA', quantity: 1, unitAmount: money(3000) }],
|
|
45
|
+
});
|
|
46
|
+
expect(session).toEqual({
|
|
47
|
+
id: 'cs_test_1',
|
|
48
|
+
url: 'https://checkout.stripe.com/c/pay/cs_test_1',
|
|
49
|
+
});
|
|
50
|
+
expect(client.checkout.sessions.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
51
|
+
mode: 'payment',
|
|
52
|
+
payment_intent_data: {
|
|
53
|
+
transfer_data: { destination: 'acct_org' },
|
|
54
|
+
application_fee_amount: 150,
|
|
55
|
+
metadata: { orderId: 'ord_1' },
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
});
|
|
59
|
+
it('translates checkout.session.completed into a domain event', async () => {
|
|
60
|
+
const client = stubClient();
|
|
61
|
+
const gateway = new StripePaymentGateway(client, {
|
|
62
|
+
secretKey: 'sk_test',
|
|
63
|
+
webhookSecret: 'whsec_test',
|
|
64
|
+
});
|
|
65
|
+
const event = await gateway.parseWebhook('{}', 'sig');
|
|
66
|
+
expect(event).toEqual({
|
|
67
|
+
type: 'checkout.completed',
|
|
68
|
+
paymentId: 'pi_test_1',
|
|
69
|
+
amount: money(3000),
|
|
70
|
+
metadata: { orderId: 'ord_1' },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=stripe.adapter.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.adapter.spec.js","sourceRoot":"","sources":["../src/stripe.adapter.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bubltec/mycota-payments",
|
|
3
|
+
"version": "0.1.1-next.18",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.10.0",
|
|
21
|
+
"typescript": "^5.8.0",
|
|
22
|
+
"vitest": "^2.1.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -b",
|
|
26
|
+
"typecheck": "tsc -b --noEmit",
|
|
27
|
+
"lint": "oxlint -c ../../.oxlintrc.json src",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
}
|
|
30
|
+
}
|