We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/kuro-tomo/shopify-agentic-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
/**
* AP2 MandateSigner Tests
*
* Tests the MandateSigner class with REAL ES256 cryptographic keys.
* No mocks for crypto — every signature is generated and verified
* using actual ECDSA P-256 key pairs.
*/
import { generateKeyPair, exportJWK, jwtVerify, importJWK } from 'jose';
import type { JWK } from 'jose';
import { MandateSigner } from '../../src/ap2/signer.js';
import type { CheckoutSession } from '../../src/types.js';
// ──────────────────────────────────────────────
// Test fixtures
// ──────────────────────────────────────────────
function makeCheckoutSession(overrides?: Partial<CheckoutSession>): CheckoutSession {
return {
id: 'checkout-sign-test',
status: 'ready_for_complete',
currency: 'USD',
line_items: [
{
id: 'li-1',
product_id: 'p-1',
variant_id: 'v-1',
title: 'Test Widget',
quantity: 2,
unit_amount: 1500,
total_amount: 3000,
type: 'product',
},
],
totals: {
subtotal: 3000,
tax: 240,
shipping: 0,
discount: 0,
fee: 15,
total: 3255,
currency: 'USD',
},
buyer: { email: 'buyer@test.com' },
shipping_address: {
line1: '123 Test',
city: 'NYC',
postal_code: '10001',
country: 'US',
},
payment_instruments: [{ handler_id: 'ap2', type: 'mandate' }],
messages: [],
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...overrides,
};
}
// ──────────────────────────────────────────────
// MandateSigner.generateKeyPair (static)
// ──────────────────────────────────────────────
describe('MandateSigner.generateKeyPair', () => {
it('returns publicKey, privateKey, and kid', async () => {
const pair = await MandateSigner.generateKeyPair();
expect(pair).toHaveProperty('publicKey');
expect(pair).toHaveProperty('privateKey');
expect(pair).toHaveProperty('kid');
});
it('publicKey has kty=EC, crv=P-256, and no d parameter', async () => {
const pair = await MandateSigner.generateKeyPair();
expect(pair.publicKey.kty).toBe('EC');
expect(pair.publicKey.crv).toBe('P-256');
expect(pair.publicKey).not.toHaveProperty('d');
});
it('privateKey has kty=EC, crv=P-256, and includes d parameter', async () => {
const pair = await MandateSigner.generateKeyPair();
expect(pair.privateKey.kty).toBe('EC');
expect(pair.privateKey.crv).toBe('P-256');
expect(pair.privateKey.d).toBeDefined();
expect(typeof pair.privateKey.d).toBe('string');
});
it('kid starts with ap2_', async () => {
const pair = await MandateSigner.generateKeyPair();
expect(pair.kid).toMatch(/^ap2_/);
});
it('kid is embedded in both publicKey.kid and privateKey.kid', async () => {
const pair = await MandateSigner.generateKeyPair();
expect(pair.publicKey.kid).toBe(pair.kid);
expect(pair.privateKey.kid).toBe(pair.kid);
});
it('two calls produce different kids', async () => {
const pair1 = await MandateSigner.generateKeyPair();
const pair2 = await MandateSigner.generateKeyPair();
expect(pair1.kid).not.toBe(pair2.kid);
});
});
// ──────────────────────────────────────────────
// MandateSigner.signCartMandate
// ──────────────────────────────────────────────
describe('MandateSigner.signCartMandate', () => {
let publicJwk: JWK;
let privateJwk: JWK;
beforeAll(async () => {
// Use jose's generateKeyPair with extractable: true to avoid the
// extractability bug that may exist in MandateSigner.generateKeyPair.
const keys = await generateKeyPair('ES256', { extractable: true });
publicJwk = await exportJWK(keys.publicKey);
privateJwk = await exportJWK(keys.privateKey);
privateJwk.kid = 'test-kid-sign';
publicJwk.kid = 'test-kid-sign';
});
it('returns a JWS compact serialization (3 dot-separated parts)', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
expect(typeof token).toBe('string');
const parts = token.split('.');
expect(parts).toHaveLength(3);
// Each part should be non-empty base64url
for (const part of parts) {
expect(part.length).toBeGreaterThan(0);
}
});
it('protected header has alg=ES256 and typ=mandate+jwt', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
// Decode protected header (first dot-separated segment)
const headerB64 = token.split('.')[0]!;
const header = JSON.parse(Buffer.from(headerB64, 'base64url').toString());
expect(header.alg).toBe('ES256');
expect(header.typ).toBe('mandate+jwt');
});
it('payload has type=cart and status=active', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.type).toBe('cart');
expect(payload.status).toBe('active');
});
it('payload checkout_id matches session id', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.payload.checkout_id).toBe(session.id);
});
it('payload contains a non-empty merchant_signature', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.payload.merchant_signature).toBeDefined();
expect(typeof payload.payload.merchant_signature).toBe('string');
expect(payload.payload.merchant_signature.length).toBeGreaterThan(0);
});
it('signature is verifiable with the corresponding public key', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
// Import the public JWK to a CryptoKey for verification
const publicKey = await importJWK(publicJwk, 'ES256');
const result = await jwtVerify(token, publicKey, { algorithms: ['ES256'] });
expect(result.payload).toBeDefined();
expect(result.protectedHeader.alg).toBe('ES256');
});
it('subject is the buyer email', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession({ buyer: { email: 'alice@example.com' } });
const token = await signer.signCartMandate(session);
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.subject).toBe('alice@example.com');
});
it('issuer is the checkout session id', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession({ id: 'checkout-issuer-test' });
const token = await signer.signCartMandate(session);
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.issuer).toBe('checkout-issuer-test');
});
it('uses anonymous as subject when buyer email is absent', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession({ buyer: undefined });
const token = await signer.signCartMandate(session);
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.subject).toBe('anonymous');
});
it('respects custom merchantSecret parameter', async () => {
const signerA = new MandateSigner(privateJwk, 'secret-alpha');
const signerB = new MandateSigner(privateJwk, 'secret-beta');
const session = makeCheckoutSession();
const tokenA = await signerA.signCartMandate(session);
const tokenB = await signerB.signCartMandate(session);
// Different merchant secrets produce different merchant_signatures
const payloadA = JSON.parse(Buffer.from(tokenA.split('.')[1]!, 'base64url').toString());
const payloadB = JSON.parse(Buffer.from(tokenB.split('.')[1]!, 'base64url').toString());
expect(payloadA.payload.merchant_signature).not.toBe(payloadB.payload.merchant_signature);
});
it('mandate id starts with mandate_cart_', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.id).toMatch(/^mandate_cart_/);
});
it('includes line_items and totals in cart payload', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.payload.line_items).toHaveLength(1);
expect(payload.payload.line_items[0].title).toBe('Test Widget');
expect(payload.payload.totals.total).toBe(3255);
expect(payload.payload.totals.currency).toBe('USD');
});
it('sets issued_at and expires_at timestamps', async () => {
const before = Date.now();
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
const after = Date.now();
const payloadB64 = token.split('.')[1]!;
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
const issuedAt = new Date(payload.issued_at).getTime();
const expiresAt = new Date(payload.expires_at).getTime();
// issued_at should be within the test window
expect(issuedAt).toBeGreaterThanOrEqual(before - 1000);
expect(issuedAt).toBeLessThanOrEqual(after + 1000);
// expires_at should be ~30 minutes after issued_at
const diff = expiresAt - issuedAt;
expect(diff).toBeGreaterThanOrEqual(29 * 60 * 1000);
expect(diff).toBeLessThanOrEqual(31 * 60 * 1000);
});
it('fails verification with a wrong key', async () => {
const signer = new MandateSigner(privateJwk);
const session = makeCheckoutSession();
const token = await signer.signCartMandate(session);
// Generate a completely different key pair
const wrongKeys = await generateKeyPair('ES256', { extractable: true });
const wrongPublicJwk = await exportJWK(wrongKeys.publicKey);
const wrongPublicKey = await importJWK(wrongPublicJwk, 'ES256');
await expect(
jwtVerify(token, wrongPublicKey, { algorithms: ['ES256'] }),
).rejects.toThrow();
});
});