@apiosk/checkout-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/index.d.ts +172 -0
- package/index.mjs +330 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# `@apiosk/checkout-core`
|
|
2
|
+
|
|
3
|
+
Shared checkout contract for all Apiosk checkout surfaces.
|
|
4
|
+
|
|
5
|
+
This package owns the schema and helper layer that the other checkout modules
|
|
6
|
+
should agree on:
|
|
7
|
+
|
|
8
|
+
- rail metadata
|
|
9
|
+
- normalized checkout config
|
|
10
|
+
- checkout intent payload shape
|
|
11
|
+
- checkout status path helpers
|
|
12
|
+
- webhook event names
|
|
13
|
+
- webhook signature helpers
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
After publish:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @apiosk/checkout-core
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Before publish:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install ./subs/checkout-core
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What belongs here
|
|
30
|
+
|
|
31
|
+
`checkout-core` is not a UI package. It should stay framework-neutral and avoid
|
|
32
|
+
React, Vue, DOM, and CSS concerns.
|
|
33
|
+
|
|
34
|
+
Use it when you need to:
|
|
35
|
+
|
|
36
|
+
- build a merchant checkout intent payload
|
|
37
|
+
- normalize rail configuration before rendering a button
|
|
38
|
+
- keep webhook naming and signature handling consistent
|
|
39
|
+
- share the same checkout schema between web, React, Vue, and server code
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import {
|
|
45
|
+
createCheckoutIntentPayload,
|
|
46
|
+
normalizeCheckoutConfig,
|
|
47
|
+
selectDefaultRail,
|
|
48
|
+
} from "@apiosk/checkout-core";
|
|
49
|
+
|
|
50
|
+
const config = normalizeCheckoutConfig({
|
|
51
|
+
merchantName: "Northstar Marketplace",
|
|
52
|
+
productName: "Automation bundle",
|
|
53
|
+
amountLabel: "24.95 EUR",
|
|
54
|
+
orderReference: "ord_2048",
|
|
55
|
+
rails: [
|
|
56
|
+
{ id: "credits", status: "live" },
|
|
57
|
+
{ id: "x402", status: "live" },
|
|
58
|
+
{ id: "agent", status: "pilot" },
|
|
59
|
+
],
|
|
60
|
+
callbacks: {
|
|
61
|
+
successUrl: "https://merchant.example/success",
|
|
62
|
+
cancelUrl: "https://merchant.example/cancel",
|
|
63
|
+
webhookUrl: "https://merchant.example/webhooks/apiosk",
|
|
64
|
+
statusUrl: "https://merchant.example/api/orders/ord_2048",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const payload = createCheckoutIntentPayload(config);
|
|
69
|
+
const defaultRail = selectDefaultRail(config.rails);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Normalized config
|
|
73
|
+
|
|
74
|
+
The normalizer accepts both:
|
|
75
|
+
|
|
76
|
+
- a flat button-friendly shape like `merchantName`, `productName`, `amountLabel`
|
|
77
|
+
- a structured config with `merchant`, `order`, `appearance`, and `callbacks`
|
|
78
|
+
|
|
79
|
+
That lets internal dashboard code migrate incrementally while external SDKs use
|
|
80
|
+
the structured version from day one.
|
|
81
|
+
|
|
82
|
+
## Intent contract
|
|
83
|
+
|
|
84
|
+
`createCheckoutIntentPayload()` produces the payload shape that the future
|
|
85
|
+
checkout API should accept at:
|
|
86
|
+
|
|
87
|
+
- `POST /v1/checkout/intents`
|
|
88
|
+
- `GET /v1/checkout/intents/:id`
|
|
89
|
+
|
|
90
|
+
This package is where that contract should evolve first.
|
|
91
|
+
|
|
92
|
+
## Webhook helpers
|
|
93
|
+
|
|
94
|
+
Use `createWebhookSignature()` and `verifyWebhookSignature()` for server-side
|
|
95
|
+
signature handling:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
import { verifyWebhookSignature } from "@apiosk/checkout-core";
|
|
99
|
+
|
|
100
|
+
const rawBody = '{"event":"checkout.intent.paid"}';
|
|
101
|
+
const signature = request.headers["x-apiosk-signature"];
|
|
102
|
+
|
|
103
|
+
const isValid = await verifyWebhookSignature(
|
|
104
|
+
rawBody,
|
|
105
|
+
signature,
|
|
106
|
+
process.env.APIOSK_WEBHOOK_SECRET,
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Pass the raw request body string whenever possible.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export type CheckoutRailId = "credits" | "x402" | "card" | "agent";
|
|
2
|
+
export type CheckoutRailStatus = "live" | "pilot" | "coming-soon";
|
|
3
|
+
export type CheckoutTheme = "light" | "dark" | "system";
|
|
4
|
+
|
|
5
|
+
export interface CheckoutRailInput {
|
|
6
|
+
id: CheckoutRailId;
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
status?: CheckoutRailStatus;
|
|
10
|
+
badge?: string;
|
|
11
|
+
launchUrl?: string;
|
|
12
|
+
checkoutUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CheckoutAmountInput {
|
|
16
|
+
label?: string;
|
|
17
|
+
value?: string | number;
|
|
18
|
+
currency?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CheckoutCallbacks {
|
|
22
|
+
successUrl?: string;
|
|
23
|
+
cancelUrl?: string;
|
|
24
|
+
webhookUrl?: string;
|
|
25
|
+
statusUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CheckoutAppearance {
|
|
29
|
+
buttonLabel?: string;
|
|
30
|
+
theme?: CheckoutTheme;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CheckoutStructuredConfigInput {
|
|
34
|
+
merchant?: {
|
|
35
|
+
name?: string;
|
|
36
|
+
id?: string;
|
|
37
|
+
};
|
|
38
|
+
order?: {
|
|
39
|
+
reference?: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
amount?: CheckoutAmountInput;
|
|
43
|
+
amountLabel?: string;
|
|
44
|
+
};
|
|
45
|
+
rails?: CheckoutRailInput[];
|
|
46
|
+
callbacks?: CheckoutCallbacks;
|
|
47
|
+
trustSignals?: string[];
|
|
48
|
+
integrationNotes?: string[];
|
|
49
|
+
appearance?: CheckoutAppearance;
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CheckoutFlatConfigInput {
|
|
54
|
+
merchantName?: string;
|
|
55
|
+
merchantId?: string;
|
|
56
|
+
productName?: string;
|
|
57
|
+
amountLabel?: string;
|
|
58
|
+
amountValue?: string | number;
|
|
59
|
+
currency?: string;
|
|
60
|
+
orderReference?: string;
|
|
61
|
+
subtitle?: string;
|
|
62
|
+
rails?: CheckoutRailInput[];
|
|
63
|
+
trustSignals?: string[];
|
|
64
|
+
integrationNotes?: string[];
|
|
65
|
+
buttonLabel?: string;
|
|
66
|
+
theme?: CheckoutTheme;
|
|
67
|
+
callbacks?: CheckoutCallbacks;
|
|
68
|
+
metadata?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type CheckoutConfigInput = CheckoutStructuredConfigInput | CheckoutFlatConfigInput;
|
|
72
|
+
|
|
73
|
+
export interface NormalizedCheckoutRail {
|
|
74
|
+
id: CheckoutRailId;
|
|
75
|
+
title: string;
|
|
76
|
+
description: string;
|
|
77
|
+
status: CheckoutRailStatus;
|
|
78
|
+
badge?: string;
|
|
79
|
+
launchUrl?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface NormalizedCheckoutConfig {
|
|
83
|
+
merchant: {
|
|
84
|
+
name: string;
|
|
85
|
+
id?: string;
|
|
86
|
+
};
|
|
87
|
+
order: {
|
|
88
|
+
reference: string;
|
|
89
|
+
title: string;
|
|
90
|
+
description: string;
|
|
91
|
+
amount: {
|
|
92
|
+
label: string;
|
|
93
|
+
value?: string;
|
|
94
|
+
currency?: string;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
rails: NormalizedCheckoutRail[];
|
|
98
|
+
callbacks: CheckoutCallbacks;
|
|
99
|
+
trustSignals: string[];
|
|
100
|
+
integrationNotes: string[];
|
|
101
|
+
appearance: {
|
|
102
|
+
buttonLabel: string;
|
|
103
|
+
theme: CheckoutTheme;
|
|
104
|
+
};
|
|
105
|
+
metadata: Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface CheckoutIntentPayload {
|
|
109
|
+
merchant_id: string | null;
|
|
110
|
+
merchant_name: string;
|
|
111
|
+
order_reference: string;
|
|
112
|
+
title: string;
|
|
113
|
+
description: string;
|
|
114
|
+
amount: {
|
|
115
|
+
label: string;
|
|
116
|
+
value: string | null;
|
|
117
|
+
currency: string | null;
|
|
118
|
+
};
|
|
119
|
+
allowed_rails: CheckoutRailId[];
|
|
120
|
+
callbacks: {
|
|
121
|
+
success_url: string | null;
|
|
122
|
+
cancel_url: string | null;
|
|
123
|
+
webhook_url: string | null;
|
|
124
|
+
status_url: string | null;
|
|
125
|
+
};
|
|
126
|
+
ui: {
|
|
127
|
+
button_label: string;
|
|
128
|
+
theme: CheckoutTheme;
|
|
129
|
+
};
|
|
130
|
+
trust_signals: string[];
|
|
131
|
+
integration_notes: string[];
|
|
132
|
+
metadata: Record<string, unknown>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export declare const CHECKOUT_CORE_VERSION: string;
|
|
136
|
+
export declare const DEFAULT_WEBHOOK_SIGNATURE_HEADER: string;
|
|
137
|
+
export declare const DEFAULT_WEBHOOK_SIGNATURE_PREFIX: string;
|
|
138
|
+
export declare const DEFAULT_BUTTON_LABEL: string;
|
|
139
|
+
export declare const DEFAULT_CHECKOUT_THEME: CheckoutTheme;
|
|
140
|
+
export declare const DEFAULT_RAIL_ORDER: CheckoutRailId[];
|
|
141
|
+
export declare const CHECKOUT_INTENT_PATH: string;
|
|
142
|
+
export declare const CHECKOUT_INTENT_STATUSES: string[];
|
|
143
|
+
export declare const CHECKOUT_EVENT_TYPES: string[];
|
|
144
|
+
export declare const RAIL_METADATA: Record<CheckoutRailId, {
|
|
145
|
+
id: CheckoutRailId;
|
|
146
|
+
title: string;
|
|
147
|
+
description: string;
|
|
148
|
+
status: CheckoutRailStatus;
|
|
149
|
+
}>;
|
|
150
|
+
|
|
151
|
+
export declare function selectDefaultRail(
|
|
152
|
+
rails: CheckoutRailInput[] | NormalizedCheckoutRail[],
|
|
153
|
+
): CheckoutRailId;
|
|
154
|
+
export declare function normalizeCheckoutConfig(input?: CheckoutConfigInput): NormalizedCheckoutConfig;
|
|
155
|
+
export declare function createCheckoutIntentPayload(input?: CheckoutConfigInput): CheckoutIntentPayload;
|
|
156
|
+
export declare function buildCheckoutIntentPath(): string;
|
|
157
|
+
export declare function buildCheckoutStatusPath(intentId: string): string;
|
|
158
|
+
export declare function resolveRailLaunchTarget(
|
|
159
|
+
input: CheckoutConfigInput,
|
|
160
|
+
railId: CheckoutRailId,
|
|
161
|
+
): string | undefined;
|
|
162
|
+
export declare function serializeCheckoutPayload(payload: unknown): string;
|
|
163
|
+
export declare function createWebhookSignature(
|
|
164
|
+
payload: unknown,
|
|
165
|
+
secret: string,
|
|
166
|
+
options?: { prefix?: string },
|
|
167
|
+
): Promise<string>;
|
|
168
|
+
export declare function verifyWebhookSignature(
|
|
169
|
+
payload: unknown,
|
|
170
|
+
signature: string,
|
|
171
|
+
secret: string,
|
|
172
|
+
): Promise<boolean>;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export const CHECKOUT_CORE_VERSION = "0.1.0";
|
|
2
|
+
export const DEFAULT_WEBHOOK_SIGNATURE_HEADER = "x-apiosk-signature";
|
|
3
|
+
export const DEFAULT_WEBHOOK_SIGNATURE_PREFIX = "sha256";
|
|
4
|
+
export const DEFAULT_BUTTON_LABEL = "Pay with Apiosk";
|
|
5
|
+
export const DEFAULT_CHECKOUT_THEME = "light";
|
|
6
|
+
export const DEFAULT_RAIL_ORDER = ["credits", "x402", "card", "agent"];
|
|
7
|
+
export const CHECKOUT_INTENT_PATH = "/v1/checkout/intents";
|
|
8
|
+
export const CHECKOUT_INTENT_STATUSES = [
|
|
9
|
+
"draft",
|
|
10
|
+
"pending",
|
|
11
|
+
"authorized",
|
|
12
|
+
"paid",
|
|
13
|
+
"failed",
|
|
14
|
+
"expired",
|
|
15
|
+
"refunded",
|
|
16
|
+
];
|
|
17
|
+
export const CHECKOUT_EVENT_TYPES = [
|
|
18
|
+
"checkout.intent.created",
|
|
19
|
+
"checkout.intent.updated",
|
|
20
|
+
"checkout.intent.authorized",
|
|
21
|
+
"checkout.intent.paid",
|
|
22
|
+
"checkout.intent.failed",
|
|
23
|
+
"checkout.intent.expired",
|
|
24
|
+
"checkout.intent.refunded",
|
|
25
|
+
];
|
|
26
|
+
export const RAIL_METADATA = Object.freeze({
|
|
27
|
+
credits: Object.freeze({
|
|
28
|
+
id: "credits",
|
|
29
|
+
title: "Apiosk credits",
|
|
30
|
+
description: "Use a pre-funded Apiosk balance and recover through top-up when needed.",
|
|
31
|
+
status: "live",
|
|
32
|
+
}),
|
|
33
|
+
x402: Object.freeze({
|
|
34
|
+
id: "x402",
|
|
35
|
+
title: "x402 wallet",
|
|
36
|
+
description: "Use proof-based programmable payment from a browser wallet or agent wallet.",
|
|
37
|
+
status: "live",
|
|
38
|
+
}),
|
|
39
|
+
card: Object.freeze({
|
|
40
|
+
id: "card",
|
|
41
|
+
title: "Card checkout",
|
|
42
|
+
description: "Accept familiar fiat checkout where the merchant still needs card coverage.",
|
|
43
|
+
status: "pilot",
|
|
44
|
+
}),
|
|
45
|
+
agent: Object.freeze({
|
|
46
|
+
id: "agent",
|
|
47
|
+
title: "Agent budget",
|
|
48
|
+
description: "Authorize autonomous purchases inside merchant-defined categories and caps.",
|
|
49
|
+
status: "coming-soon",
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function isPlainObject(value) {
|
|
54
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toStringValue(value, fallback = "") {
|
|
58
|
+
if (value === null || value === undefined) {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compactList(values) {
|
|
66
|
+
return Array.from(
|
|
67
|
+
new Set(
|
|
68
|
+
(Array.isArray(values) ? values : [])
|
|
69
|
+
.map((value) => toStringValue(value).trim())
|
|
70
|
+
.filter(Boolean),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeTheme(theme) {
|
|
76
|
+
if (theme === "dark" || theme === "system") {
|
|
77
|
+
return theme;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return DEFAULT_CHECKOUT_THEME;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeRail(rawRail) {
|
|
84
|
+
const source = isPlainObject(rawRail) ? rawRail : {};
|
|
85
|
+
const id = DEFAULT_RAIL_ORDER.includes(source.id) ? source.id : "credits";
|
|
86
|
+
const metadata = RAIL_METADATA[id];
|
|
87
|
+
const status =
|
|
88
|
+
source.status === "live" || source.status === "pilot" || source.status === "coming-soon"
|
|
89
|
+
? source.status
|
|
90
|
+
: metadata.status;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
title: toStringValue(source.title, metadata.title).trim() || metadata.title,
|
|
95
|
+
description:
|
|
96
|
+
toStringValue(source.description, metadata.description).trim() || metadata.description,
|
|
97
|
+
status,
|
|
98
|
+
badge: toStringValue(source.badge).trim() || undefined,
|
|
99
|
+
launchUrl: toStringValue(source.launchUrl || source.checkoutUrl).trim() || undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeAmount(rawAmount, fallbackLabel) {
|
|
104
|
+
const source = isPlainObject(rawAmount) ? rawAmount : {};
|
|
105
|
+
const label = toStringValue(source.label, fallbackLabel).trim() || fallbackLabel;
|
|
106
|
+
const currency = toStringValue(source.currency).trim() || undefined;
|
|
107
|
+
|
|
108
|
+
let value = source.value;
|
|
109
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
110
|
+
value = String(value);
|
|
111
|
+
} else if (typeof value === "string" && value.trim()) {
|
|
112
|
+
value = value.trim();
|
|
113
|
+
} else {
|
|
114
|
+
value = undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
label,
|
|
119
|
+
value,
|
|
120
|
+
currency,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeCallbacks(callbacks) {
|
|
125
|
+
const source = isPlainObject(callbacks) ? callbacks : {};
|
|
126
|
+
return {
|
|
127
|
+
successUrl: toStringValue(source.successUrl).trim() || undefined,
|
|
128
|
+
cancelUrl: toStringValue(source.cancelUrl).trim() || undefined,
|
|
129
|
+
webhookUrl: toStringValue(source.webhookUrl).trim() || undefined,
|
|
130
|
+
statusUrl: toStringValue(source.statusUrl).trim() || undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toStructuredConfig(input) {
|
|
135
|
+
if (isPlainObject(input) && (isPlainObject(input.merchant) || isPlainObject(input.order))) {
|
|
136
|
+
return input;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const source = isPlainObject(input) ? input : {};
|
|
140
|
+
return {
|
|
141
|
+
merchant: {
|
|
142
|
+
name: source.merchantName,
|
|
143
|
+
id: source.merchantId,
|
|
144
|
+
},
|
|
145
|
+
order: {
|
|
146
|
+
reference: source.orderReference,
|
|
147
|
+
title: source.productName,
|
|
148
|
+
description: source.subtitle,
|
|
149
|
+
amount: {
|
|
150
|
+
label: source.amountLabel,
|
|
151
|
+
value: source.amountValue,
|
|
152
|
+
currency: source.currency,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
rails: source.rails,
|
|
156
|
+
callbacks: source.callbacks,
|
|
157
|
+
trustSignals: source.trustSignals,
|
|
158
|
+
integrationNotes: source.integrationNotes,
|
|
159
|
+
appearance: {
|
|
160
|
+
buttonLabel: source.buttonLabel,
|
|
161
|
+
theme: source.theme,
|
|
162
|
+
},
|
|
163
|
+
metadata: source.metadata,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function selectDefaultRail(rails) {
|
|
168
|
+
const normalizedRails = (Array.isArray(rails) ? rails : []).map(normalizeRail);
|
|
169
|
+
return (
|
|
170
|
+
normalizedRails.find((rail) => rail.status === "live")?.id ||
|
|
171
|
+
normalizedRails[0]?.id ||
|
|
172
|
+
DEFAULT_RAIL_ORDER[0]
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function normalizeCheckoutConfig(input = {}) {
|
|
177
|
+
const structured = toStructuredConfig(input);
|
|
178
|
+
const merchantSource = isPlainObject(structured.merchant) ? structured.merchant : {};
|
|
179
|
+
const orderSource = isPlainObject(structured.order) ? structured.order : {};
|
|
180
|
+
const appearanceSource = isPlainObject(structured.appearance) ? structured.appearance : {};
|
|
181
|
+
const railSource = Array.isArray(structured.rails) ? structured.rails : [];
|
|
182
|
+
const normalizedRails = DEFAULT_RAIL_ORDER
|
|
183
|
+
.map((railId) => railSource.find((rail) => rail?.id === railId))
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.concat(railSource.filter((rail) => rail && !DEFAULT_RAIL_ORDER.includes(rail.id)))
|
|
186
|
+
.map(normalizeRail);
|
|
187
|
+
const rails = normalizedRails.length > 0 ? normalizedRails : [normalizeRail({ id: "credits" })];
|
|
188
|
+
const title = toStringValue(orderSource.title, "Untitled order").trim() || "Untitled order";
|
|
189
|
+
const description =
|
|
190
|
+
toStringValue(orderSource.description, "Complete this request through Apiosk checkout.").trim() ||
|
|
191
|
+
"Complete this request through Apiosk checkout.";
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
merchant: {
|
|
195
|
+
name: toStringValue(merchantSource.name, "Apiosk merchant").trim() || "Apiosk merchant",
|
|
196
|
+
id: toStringValue(merchantSource.id).trim() || undefined,
|
|
197
|
+
},
|
|
198
|
+
order: {
|
|
199
|
+
reference: toStringValue(orderSource.reference, "intent_demo").trim() || "intent_demo",
|
|
200
|
+
title,
|
|
201
|
+
description,
|
|
202
|
+
amount: normalizeAmount(orderSource.amount, toStringValue(orderSource.amountLabel, "")),
|
|
203
|
+
},
|
|
204
|
+
rails,
|
|
205
|
+
callbacks: normalizeCallbacks(structured.callbacks),
|
|
206
|
+
trustSignals: compactList(structured.trustSignals),
|
|
207
|
+
integrationNotes: compactList(structured.integrationNotes),
|
|
208
|
+
appearance: {
|
|
209
|
+
buttonLabel:
|
|
210
|
+
toStringValue(appearanceSource.buttonLabel, DEFAULT_BUTTON_LABEL).trim() ||
|
|
211
|
+
DEFAULT_BUTTON_LABEL,
|
|
212
|
+
theme: normalizeTheme(appearanceSource.theme),
|
|
213
|
+
},
|
|
214
|
+
metadata: isPlainObject(structured.metadata) ? { ...structured.metadata } : {},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function createCheckoutIntentPayload(input = {}) {
|
|
219
|
+
const config = normalizeCheckoutConfig(input);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
merchant_id: config.merchant.id || null,
|
|
223
|
+
merchant_name: config.merchant.name,
|
|
224
|
+
order_reference: config.order.reference,
|
|
225
|
+
title: config.order.title,
|
|
226
|
+
description: config.order.description,
|
|
227
|
+
amount: {
|
|
228
|
+
label: config.order.amount.label,
|
|
229
|
+
value: config.order.amount.value || null,
|
|
230
|
+
currency: config.order.amount.currency || null,
|
|
231
|
+
},
|
|
232
|
+
allowed_rails: config.rails.map((rail) => rail.id),
|
|
233
|
+
callbacks: {
|
|
234
|
+
success_url: config.callbacks.successUrl || null,
|
|
235
|
+
cancel_url: config.callbacks.cancelUrl || null,
|
|
236
|
+
webhook_url: config.callbacks.webhookUrl || null,
|
|
237
|
+
status_url: config.callbacks.statusUrl || null,
|
|
238
|
+
},
|
|
239
|
+
ui: {
|
|
240
|
+
button_label: config.appearance.buttonLabel,
|
|
241
|
+
theme: config.appearance.theme,
|
|
242
|
+
},
|
|
243
|
+
trust_signals: config.trustSignals,
|
|
244
|
+
integration_notes: config.integrationNotes,
|
|
245
|
+
metadata: config.metadata,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function buildCheckoutIntentPath() {
|
|
250
|
+
return CHECKOUT_INTENT_PATH;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function buildCheckoutStatusPath(intentId) {
|
|
254
|
+
return `${CHECKOUT_INTENT_PATH}/${encodeURIComponent(toStringValue(intentId).trim())}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function resolveRailLaunchTarget(input, railId) {
|
|
258
|
+
const config = normalizeCheckoutConfig(input);
|
|
259
|
+
const rail = config.rails.find((candidate) => candidate.id === railId);
|
|
260
|
+
return rail?.launchUrl;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function serializeCheckoutPayload(payload) {
|
|
264
|
+
if (typeof payload === "string") {
|
|
265
|
+
return payload;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return JSON.stringify(payload);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function bytesToHex(bytes) {
|
|
272
|
+
return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function hmacSha256Hex(message, secret) {
|
|
276
|
+
if (globalThis.crypto?.subtle) {
|
|
277
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
278
|
+
"raw",
|
|
279
|
+
new TextEncoder().encode(secret),
|
|
280
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
281
|
+
false,
|
|
282
|
+
["sign"],
|
|
283
|
+
);
|
|
284
|
+
const signature = await globalThis.crypto.subtle.sign(
|
|
285
|
+
"HMAC",
|
|
286
|
+
key,
|
|
287
|
+
new TextEncoder().encode(message),
|
|
288
|
+
);
|
|
289
|
+
return bytesToHex(new Uint8Array(signature));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const { createHmac } = await import("node:crypto");
|
|
293
|
+
return createHmac("sha256", secret).update(message).digest("hex");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeSignature(signature) {
|
|
297
|
+
const value = toStringValue(signature).trim();
|
|
298
|
+
const separatorIndex = value.indexOf("=");
|
|
299
|
+
return separatorIndex === -1 ? value : value.slice(separatorIndex + 1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function createWebhookSignature(
|
|
303
|
+
payload,
|
|
304
|
+
secret,
|
|
305
|
+
options = {},
|
|
306
|
+
) {
|
|
307
|
+
const prefix = toStringValue(options.prefix, DEFAULT_WEBHOOK_SIGNATURE_PREFIX).trim();
|
|
308
|
+
const digest = await hmacSha256Hex(serializeCheckoutPayload(payload), toStringValue(secret));
|
|
309
|
+
return `${prefix}=${digest}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function verifyWebhookSignature(payload, signature, secret) {
|
|
313
|
+
const expected = normalizeSignature(
|
|
314
|
+
await createWebhookSignature(payload, secret, {
|
|
315
|
+
prefix: DEFAULT_WEBHOOK_SIGNATURE_PREFIX,
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
const received = normalizeSignature(signature);
|
|
319
|
+
|
|
320
|
+
if (expected.length !== received.length) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let mismatch = 0;
|
|
325
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
326
|
+
mismatch |= expected.charCodeAt(index) ^ received.charCodeAt(index);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return mismatch === 0;
|
|
330
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apiosk/checkout-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared checkout schema, rails, intent payload helpers, and webhook utilities for Apiosk checkout packages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.mjs",
|
|
10
|
+
"index.d.ts",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"default": "./index.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"check": "node --check index.mjs"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"apiosk",
|
|
24
|
+
"checkout",
|
|
25
|
+
"payments",
|
|
26
|
+
"x402",
|
|
27
|
+
"credits"
|
|
28
|
+
],
|
|
29
|
+
"author": "Apiosk",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"homepage": "https://docs.apiosk.com",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|