@commercelayer/agent-cli 0.0.0-stage → 0.1.1
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 +98 -2
- package/dist/api/client.js +39 -0
- package/dist/api/server.js +26 -0
- package/dist/api/stub/db.js +37 -0
- package/dist/api/stub/http.js +18 -0
- package/dist/api/stub/index.js +27 -0
- package/dist/api/stub/management.js +86 -0
- package/dist/api/stub/shopping.js +543 -0
- package/dist/api/stub/ucp.js +350 -0
- package/dist/api/transport.js +37 -0
- package/dist/api/types.js +4 -0
- package/dist/bin.js +83 -0
- package/dist/commands.js +201 -0
- package/dist/files.js +47 -0
- package/dist/output.js +18 -0
- package/dist/products.js +128 -0
- package/dist/store.js +172 -0
- package/dist/storefront.js +73 -0
- package/dist/ucp/buy.js +88 -0
- package/dist/ucp/check.js +355 -0
- package/dist/ucp/client.js +129 -0
- package/dist/ucp/platform.js +90 -0
- package/dist/ucp/signing.js +67 -0
- package/package.json +43 -4
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// What this CLI is on the UCP side: a platform (a buyer's agent). Its profile is
|
|
2
|
+
// served from commercelayer.ai so stores can fetch it; the stub knows it inline.
|
|
3
|
+
// Shared constants of the store side live here too, so the client, the stub and
|
|
4
|
+
// the conformance check agree on them.
|
|
5
|
+
export const UCP_VERSION = '2026-08-25';
|
|
6
|
+
export const UCP = `https://ucp.dev/${UCP_VERSION}`;
|
|
7
|
+
export const AGENT_PROFILE_URL = process.env.C11R_AGENT_PROFILE_URL ?? 'https://commercelayer.ai/ucp/agent-cli/profile.json';
|
|
8
|
+
export const AGENT_WEBHOOK_URL = 'https://commercelayer.ai/ucp/agent-cli/webhook';
|
|
9
|
+
/** The sandbox payment handler every sandbox store accepts. No money moves. */
|
|
10
|
+
export const SANDBOX_HANDLER = 'ai.commercelayer.sandbox';
|
|
11
|
+
export const SANDBOX_HANDLER_ID = 'cl_sandbox';
|
|
12
|
+
export const SANDBOX_HANDLER_SPEC = 'https://commercelayer.ai/ucp/handlers/sandbox';
|
|
13
|
+
export const SANDBOX_HANDLER_SCHEMA = 'https://commercelayer.ai/ucp/handlers/sandbox.json';
|
|
14
|
+
export const SANDBOX_CREDENTIAL_TYPE = 'sandbox_token';
|
|
15
|
+
/** Tokens the handler spec publishes: the first approves, the second declines. */
|
|
16
|
+
export const SANDBOX_TOKEN_SUCCESS = 'sbx_tok_success';
|
|
17
|
+
export const SANDBOX_TOKEN_DECLINED = 'sbx_tok_declined';
|
|
18
|
+
export const SHOPPING_CAPABILITIES = ['dev.ucp.shopping.catalog.search', 'dev.ucp.shopping.catalog.lookup', 'dev.ucp.shopping.cart', 'dev.ucp.shopping.checkout', 'dev.ucp.shopping.order'];
|
|
19
|
+
export const EXTENSIONS = {
|
|
20
|
+
'dev.ucp.shopping.fulfillment': ['dev.ucp.shopping.checkout', 'dev.ucp.shopping.catalog.search', 'dev.ucp.shopping.catalog.lookup'],
|
|
21
|
+
'dev.ucp.shopping.discount': ['dev.ucp.shopping.cart', 'dev.ucp.shopping.checkout'],
|
|
22
|
+
'dev.ucp.shopping.buyer_consent': ['dev.ucp.shopping.cart', 'dev.ucp.shopping.checkout'],
|
|
23
|
+
'dev.ucp.shopping.policy.return': ['dev.ucp.shopping.catalog.search', 'dev.ucp.shopping.catalog.lookup', 'dev.ucp.shopping.cart', 'dev.ucp.shopping.checkout', 'dev.ucp.shopping.order'],
|
|
24
|
+
};
|
|
25
|
+
const specOf = {
|
|
26
|
+
'dev.ucp.shopping.catalog.search': 'specification/shopping/catalog/search',
|
|
27
|
+
'dev.ucp.shopping.catalog.lookup': 'specification/shopping/catalog/lookup',
|
|
28
|
+
'dev.ucp.shopping.cart': 'specification/shopping/cart',
|
|
29
|
+
'dev.ucp.shopping.checkout': 'specification/shopping/checkout',
|
|
30
|
+
'dev.ucp.shopping.order': 'specification/shopping/order',
|
|
31
|
+
'dev.ucp.common.ask': 'specification/common/ask',
|
|
32
|
+
'dev.ucp.shopping.fulfillment': 'specification/shopping/extensions/fulfillment',
|
|
33
|
+
'dev.ucp.shopping.discount': 'specification/shopping/extensions/discount',
|
|
34
|
+
'dev.ucp.shopping.buyer_consent': 'specification/shopping/extensions/buyer-consent',
|
|
35
|
+
'dev.ucp.shopping.policy.return': 'specification/shopping/extensions/return-policy',
|
|
36
|
+
'dev.ucp.common.identity_linking': 'specification/common/identity-linking/',
|
|
37
|
+
'dev.ucp.shopping.permalink': 'specification/permalink',
|
|
38
|
+
};
|
|
39
|
+
const schemaOf = {
|
|
40
|
+
'dev.ucp.shopping.catalog.search': 'schemas/shopping/catalog_search.json',
|
|
41
|
+
'dev.ucp.shopping.catalog.lookup': 'schemas/shopping/catalog_lookup.json',
|
|
42
|
+
'dev.ucp.shopping.cart': 'schemas/shopping/cart.json',
|
|
43
|
+
'dev.ucp.shopping.checkout': 'schemas/shopping/checkout.json',
|
|
44
|
+
'dev.ucp.shopping.order': 'schemas/shopping/order.json',
|
|
45
|
+
'dev.ucp.common.ask': 'schemas/common/ask.json',
|
|
46
|
+
'dev.ucp.shopping.fulfillment': 'schemas/shopping/fulfillment.json',
|
|
47
|
+
'dev.ucp.shopping.discount': 'schemas/shopping/discount.json',
|
|
48
|
+
'dev.ucp.shopping.buyer_consent': 'schemas/shopping/buyer_consent.json',
|
|
49
|
+
'dev.ucp.shopping.policy.return': 'schemas/shopping/policy_return.json',
|
|
50
|
+
'dev.ucp.common.identity_linking': 'schemas/common/identity_linking.json',
|
|
51
|
+
'dev.ucp.shopping.permalink': 'schemas/shopping/permalink.json',
|
|
52
|
+
};
|
|
53
|
+
export function capabilityEntry(name, extra = {}) {
|
|
54
|
+
const entry = { version: UCP_VERSION, spec: `${UCP}/${specOf[name]}`, schema: `${UCP}/${schemaOf[name]}` };
|
|
55
|
+
if (EXTENSIONS[name])
|
|
56
|
+
entry.extends = EXTENSIONS[name];
|
|
57
|
+
return { ...entry, ...extra };
|
|
58
|
+
}
|
|
59
|
+
/** The platform profile this CLI presents in UCP-Agent. Published by the site. */
|
|
60
|
+
export function agentProfile() {
|
|
61
|
+
const capabilities = {};
|
|
62
|
+
for (const name of [...SHOPPING_CAPABILITIES, 'dev.ucp.common.ask', ...Object.keys(EXTENSIONS), 'dev.ucp.common.identity_linking']) {
|
|
63
|
+
capabilities[name] = [capabilityEntry(name, name === 'dev.ucp.shopping.order' ? { config: { webhook_url: AGENT_WEBHOOK_URL } } : {})];
|
|
64
|
+
}
|
|
65
|
+
// The fulfillment extension on the platform side: one group per method is enough.
|
|
66
|
+
capabilities['dev.ucp.shopping.fulfillment'] = [capabilityEntry('dev.ucp.shopping.fulfillment', { config: { supports_multi_group: false } })];
|
|
67
|
+
return {
|
|
68
|
+
ucp: {
|
|
69
|
+
version: UCP_VERSION,
|
|
70
|
+
services: {},
|
|
71
|
+
capabilities,
|
|
72
|
+
payment_handlers: {
|
|
73
|
+
[SANDBOX_HANDLER]: [{ id: SANDBOX_HANDLER_ID, version: UCP_VERSION, spec: SANDBOX_HANDLER_SPEC, schema: SANDBOX_HANDLER_SCHEMA, available_instruments: [{ type: 'card' }] }],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
name: 'Commerce Layer agent CLI',
|
|
77
|
+
description: 'c11r, the Commerce Layer CLI for coding agents, acting as a shopping platform: it places test orders and checks a store speaks UCP.',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Minor units per currency unit (ISO 4217 exponent). */
|
|
81
|
+
export function minorUnits(currency) {
|
|
82
|
+
if (['JPY', 'KRW', 'VND', 'CLP', 'ISK', 'HUF', 'TWD', 'UGX', 'PYG', 'RWF', 'XAF', 'XOF'].includes(currency))
|
|
83
|
+
return 1;
|
|
84
|
+
if (['BHD', 'KWD', 'JOD', 'OMR', 'TND', 'IQD', 'LYD'].includes(currency))
|
|
85
|
+
return 1000;
|
|
86
|
+
return 100;
|
|
87
|
+
}
|
|
88
|
+
export function toMinor(amount, currency) {
|
|
89
|
+
return Math.round(amount * minorUnits(currency));
|
|
90
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// HTTP message signatures (RFC 9421) and content digests (RFC 9530) as UCP uses
|
|
2
|
+
// them: ES256 over a signature base, raw r||s encoding, keys published as JWKs.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
export function contentDigest(body) {
|
|
5
|
+
return `sha-256=:${crypto.createHash('sha256').update(body).digest('base64')}:`;
|
|
6
|
+
}
|
|
7
|
+
/** The inner-list serialization that goes in Signature-Input and in the base. */
|
|
8
|
+
export function serializeParams(p) {
|
|
9
|
+
let s = `(${p.components.map((c) => `"${c}"`).join(' ')})`;
|
|
10
|
+
if (p.created !== undefined)
|
|
11
|
+
s += `;created=${p.created}`;
|
|
12
|
+
s += `;keyid="${p.keyid}"`;
|
|
13
|
+
return s;
|
|
14
|
+
}
|
|
15
|
+
/** The signature base: one line per covered component, then @signature-params. */
|
|
16
|
+
export function signatureBase(values, p) {
|
|
17
|
+
const lines = p.components.map((c) => `"${c}": ${values[c] ?? ''}`);
|
|
18
|
+
lines.push(`"@signature-params": ${serializeParams(p)}`);
|
|
19
|
+
return lines.join('\n');
|
|
20
|
+
}
|
|
21
|
+
export function generateKey(kid) {
|
|
22
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
23
|
+
const privateJwk = privateKey.export({ format: 'jwk' });
|
|
24
|
+
const publicJwk = { ...publicKey.export({ format: 'jwk' }), kid, use: 'sig', alg: 'ES256' };
|
|
25
|
+
return { privateJwk, publicJwk };
|
|
26
|
+
}
|
|
27
|
+
export function sign(base, privateJwk) {
|
|
28
|
+
const key = crypto.createPrivateKey({ key: privateJwk, format: 'jwk' });
|
|
29
|
+
return crypto.sign('sha256', Buffer.from(base), { key, dsaEncoding: 'ieee-p1363' }).toString('base64');
|
|
30
|
+
}
|
|
31
|
+
export function verify(base, signatureBase64, publicJwk) {
|
|
32
|
+
try {
|
|
33
|
+
const key = crypto.createPublicKey({ key: publicJwk, format: 'jwk' });
|
|
34
|
+
return crypto.verify('sha256', Buffer.from(base), { key, dsaEncoding: 'ieee-p1363' }, Buffer.from(signatureBase64, 'base64'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Build Signature-Input and Signature headers for a message. */
|
|
41
|
+
export function signHeaders(label, values, p, privateJwk) {
|
|
42
|
+
return {
|
|
43
|
+
'signature-input': `${label}=${serializeParams(p)}`,
|
|
44
|
+
signature: `${label}=:${sign(signatureBase(values, p), privateJwk)}:`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Parse one signature out of Signature-Input / Signature headers (the first label). */
|
|
48
|
+
export function parseSignature(signatureInput, signature) {
|
|
49
|
+
if (!signatureInput || !signature)
|
|
50
|
+
return null;
|
|
51
|
+
const input = signatureInput.match(/^\s*([a-z0-9_-]+)=\(([^)]*)\)((?:;[a-z]+=[^;]*)*)\s*$/i);
|
|
52
|
+
if (!input)
|
|
53
|
+
return null;
|
|
54
|
+
const [, label, list, paramStr] = input;
|
|
55
|
+
const components = [...list.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
56
|
+
const params = { components, keyid: '' };
|
|
57
|
+
for (const [, k, v] of paramStr.matchAll(/;([a-z]+)=("[^"]*"|[^;]*)/g)) {
|
|
58
|
+
if (k === 'keyid')
|
|
59
|
+
params.keyid = v.replace(/^"|"$/g, '');
|
|
60
|
+
if (k === 'created')
|
|
61
|
+
params.created = Number(v);
|
|
62
|
+
}
|
|
63
|
+
const sig = signature.match(new RegExp(`${label}=:([A-Za-z0-9+/=]+):`));
|
|
64
|
+
if (!sig)
|
|
65
|
+
return null;
|
|
66
|
+
return { label, params, signature: sig[1] };
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commercelayer/agent-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "c11r: the Commerce Layer CLI for coding agents. Builds a sandbox store that people and shopping agents can buy from.",
|
|
5
|
+
"homepage": "https://commercelayer.ai",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"commercelayer",
|
|
8
|
+
"commerce",
|
|
9
|
+
"cli",
|
|
10
|
+
"agent",
|
|
11
|
+
"ucp",
|
|
12
|
+
"sandbox"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"c11r": "dist/bin.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/bin.js dist/api/server.js",
|
|
28
|
+
"dev": "tsx src/bin.ts",
|
|
29
|
+
"stub-api": "tsx src/api/server.ts",
|
|
30
|
+
"test": "npm run build && vitest run",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"yaml": "^2.6.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"tsx": "^4.19.0",
|
|
39
|
+
"typescript": "^5.6.0",
|
|
40
|
+
"vitest": "^3.0.0"
|
|
41
|
+
},
|
|
42
|
+
"allowScripts": {
|
|
43
|
+
"esbuild@0.28.2": true
|
|
44
|
+
}
|
|
45
|
+
}
|