diff --git a/.changeset/quiet-aep-mpp-example.md b/.changeset/quiet-aep-mpp-example.md new file mode 100644 index 0000000..226a308 --- /dev/null +++ b/.changeset/quiet-aep-mpp-example.md @@ -0,0 +1,4 @@ +--- +--- + +Add an Express example that applies AEP API-key authentication before MPP payment enforcement. diff --git a/README.md b/README.md index 77642b6..6eb3bb3 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,8 @@ Runnable end-to-end examples live in [`examples/`](./examples). Start a seller, **MPP:** +- [`mpp-aep-seller-express`](./examples/mpp-aep-seller-express) — Express server applying AEP API-key authentication + before MPP payment enforcement on the same protected routes. - [`mpp-seller-express`](./examples/mpp-seller-express) — Express server accepting MPP payments via `mppx`'s Express adapter + InFlow's `inflow` seller method, plus a multi-currency `/api/checkout` route via `inflowChargesNodeListener`. diff --git a/examples/mpp-aep-seller-express/.env.example b/examples/mpp-aep-seller-express/.env.example new file mode 100644 index 0000000..1a3dadb --- /dev/null +++ b/examples/mpp-aep-seller-express/.env.example @@ -0,0 +1,6 @@ +INFLOW_API_KEY= +INFLOW_BASE_URL=https://sandbox.inflowpay.ai +MPP_SECRET_KEY= +SERVICE_DID=did:web:127.0.0.1%3A4100:services:example-service +HOST=127.0.0.1 +PORT=3000 diff --git a/examples/mpp-aep-seller-express/README.md b/examples/mpp-aep-seller-express/README.md new file mode 100644 index 0000000..678c545 --- /dev/null +++ b/examples/mpp-aep-seller-express/README.md @@ -0,0 +1,74 @@ +# Example — AEP plus MPP seller on Express + +This Express Service applies Agent Enrollment Protocol (AEP) authentication before Machine Payments Protocol (MPP) +payment enforcement. Its API-key credential uses `x-aep-api-key`, leaving `Authorization: Payment` available for the MPP +credential. + +## Run + +Start the local AEP Platform example first. It serves the Service DID used by this example. + +```bash +cd /Users/nxkavian/Drive/Source/AEP/aep-node +pnpm --filter @aep-foundation/example-aep-platform-ephemeral start +``` + +Build the AEP packages, then use the existing unified local-link script in the InFlow command-line interface checkout +when exercising the command-line scenarios below. It links the local AEP SDK packages without adding an example-specific +linker. + +```bash +cd /Users/nxkavian/Drive/Source/AEP/aep-node +pnpm --filter @aep-foundation/core build +pnpm --filter @aep-foundation/service build +pnpm --filter @aep-foundation/express build + +cd /Users/nxkavian/Drive/Source/InFlow/inflow-cli +node scripts/link-local-inflow-node.mjs +``` + +Configure and start this example: + +```bash +cd /Users/nxkavian/Drive/Source/InFlow/inflow-node/examples/mpp-aep-seller-express +cp .env.example .env +# Set INFLOW_API_KEY, INFLOW_BASE_URL, and MPP_SECRET_KEY. +pnpm install +pnpm start +``` + +`SERVICE_DID` defaults in `.env.example` to the local Platform example's Service DID. `HOST` and `PORT` default to +`127.0.0.1` and `3000`. `INFLOW_BASE_URL` selects the InFlow environment that issued `INFLOW_API_KEY` and defaults in +`.env.example` to `https://sandbox.inflowpay.ai`. + +## Routes + +| Route | Enforcement | +| ----------------------------------------------------- | --------------------------------------------------------------- | +| `GET /api/widgets` | AEP API key, then 0.01 USDC MPP charge | +| `POST /api/upload` | AEP API key, then 0.10 USDC MPP charge; echoes the request body | +| `GET /free` | No AEP or MPP enforcement | +| `GET /.well-known/aep`, `/aep/*`, `GET /openapi.json` | AEP discovery, lifecycle, and OpenAPI documents | + +For a protected route, an anonymous request receives only the AEP `401` challenge. A request with `x-aep-api-key` but no +payment receives only the MPP `402` challenge. A completed payment replay carries both `x-aep-api-key` and +`Authorization: Payment …`. + +## Command-line scenarios + +Use the built command-line interface from `/Users/nxkavian/Drive/Source/InFlow/inflow-cli` with the local Platform and +this Service running: + +```bash +node packages/cli/dist/cli.js inspect http://127.0.0.1:3000/api/widgets --format json +node packages/cli/dist/cli.js aep inspect http://127.0.0.1:3000 --format json +node packages/cli/dist/cli.js aep fetch http://127.0.0.1:3000/api/widgets --format json +node packages/cli/dist/cli.js aep grant http://127.0.0.1:3000 --grant-type api-key --format json +node packages/cli/dist/cli.js aep fetch http://127.0.0.1:3000/api/widgets --format json +node packages/cli/dist/cli.js mpp pay http://127.0.0.1:3000/api/widgets --format json +node packages/cli/dist/cli.js mpp pay http://127.0.0.1:3000/api/upload --method POST --data '{"widget":"one"}' --header 'X-Caller-Header: retained' --format json +``` + +The first `aep fetch` uses the API-key Grant path and stops with the downstream payment-required result. Re-running it +after explicit Grant reuses the stored key. `mpp pay` performs AEP authentication before payment creation; the returned +payment identifier can be completed with `mpp fetch` when approval is asynchronous. diff --git a/examples/mpp-aep-seller-express/package.json b/examples/mpp-aep-seller-express/package.json new file mode 100644 index 0000000..91cdbb0 --- /dev/null +++ b/examples/mpp-aep-seller-express/package.json @@ -0,0 +1,30 @@ +{ + "name": "@inflowpayai/example-mpp-aep-seller-express", + "version": "0.0.0", + "private": true, + "description": "Example: sequential AEP authentication and MPP payments via Express.", + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "start": "tsx src/index.ts", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@aep-foundation/core": "^0.2.0", + "@aep-foundation/express": "^0.2.0", + "@aep-foundation/service": "^0.2.0", + "@inflowpayai/mpp-seller": "workspace:^", + "dotenv": "^16.4.0", + "express": "^5.0.0", + "mppx": "^0.6.28" + }, + "devDependencies": { + "@inflowpayai/mpp": "workspace:^", + "@types/express": "^5.0.0", + "@types/node": "^24.0.0", + "tsx": "^4.0.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } +} diff --git a/examples/mpp-aep-seller-express/src/app.ts b/examples/mpp-aep-seller-express/src/app.ts new file mode 100644 index 0000000..5ee712d --- /dev/null +++ b/examples/mpp-aep-seller-express/src/app.ts @@ -0,0 +1,119 @@ +import { randomUUID } from 'node:crypto'; + +import { AEP_GRANT_TYPE_API_KEY } from '@aep-foundation/core'; +import type { ApiKeyGrantResponse } from '@aep-foundation/core'; +import { createExpressAepProtectedResourceHandler, registerExpressAepRoutes } from '@aep-foundation/express'; +import { + createAepService, + createDidWebClientAssertionVerifier, + createInMemoryClientAssertionReplayStore, + createInMemoryCommandIdempotencyStore, + createInMemoryEnrollmentStore, + createInMemoryServiceCredentialStore, + createStaticEnrollmentPolicy, + didWebIdentityMethod, + storedApiKeyGrantType, +} from '@aep-foundation/service'; +import type { AepServiceCredentialStore } from '@aep-foundation/service'; +import { inflow } from '@inflowpayai/mpp-seller'; +import express from 'express'; +import type { Request, RequestHandler } from 'express'; +import { Mppx } from 'mppx/express'; + +export interface CreateMppAepSellerAppOptions { + apiKey: string; + baseUrl?: string; + listenUrl: string; + mppSecretKey: string; + onAepPassed?: () => void; + onProtectedHandler?: (request: Request) => void; + serviceDid: string; + credentialStore?: AepServiceCredentialStore; +} + +export function createMppAepSellerApp(options: CreateMppAepSellerAppOptions) { + const credentialStore = options.credentialStore ?? createInMemoryServiceCredentialStore(); + const service = createAepService({ + authenticationMethods: [AEP_GRANT_TYPE_API_KEY], + clientAssertionVerifier: createDidWebClientAssertionVerifier(), + commandIdempotencyStore: createInMemoryCommandIdempotencyStore(), + enrollmentPolicy: createStaticEnrollmentPolicy(), + enrollmentStore: createInMemoryEnrollmentStore(), + grantTypes: [ + storedApiKeyGrantType({ + issue: (): ApiKeyGrantResponse => ({ + api_key: randomUUID(), + credential_id: randomUUID(), + expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), + header: 'x-aep-api-key', + scopes: ['read:widgets', 'write:uploads'], + }), + store: credentialStore, + }), + ], + identityMethods: [didWebIdentityMethod()], + openapi: { url: '/openapi.json', pathMatching: { trailingSlash: 'strict' } }, + replayStore: createInMemoryClientAssertionReplayStore(), + serviceDid: options.serviceDid, + }); + const method = inflow({ + apiKey: options.apiKey, + ...(options.baseUrl === undefined ? { environment: 'sandbox' } : { baseUrl: options.baseUrl }), + }); + const mppx = Mppx.create({ methods: [method], secretKey: options.mppSecretKey }); + const authenticateAep = createExpressAepProtectedResourceHandler(service, options.listenUrl); + const requireAep: RequestHandler = (request, response, next) => + authenticateAep(request, response, () => { + options.onAepPassed?.(); + next(); + }); + const app = express(); + + app.use(express.json({ type: ['application/json', 'application/aep+json'] })); + app.use((request, response, next) => { + response.on('finish', () => { + console.log(`request method=${request.method} path=${request.path} status=${response.statusCode}`); + }); + next(); + }); + registerExpressAepRoutes(app, service); + app.get('/openapi.json', (_request, response) => response.json(openApiDocument())); + app.get('/api/widgets', requireAep, mppx.charge({ amount: '0.01', currency: 'USDC' }), (request, response) => { + options.onProtectedHandler?.(request); + response.json({ widgets: [1, 2, 3] }); + }); + app.post('/api/upload', requireAep, mppx.charge({ amount: '0.10', currency: 'USDC' }), (request, response) => { + options.onProtectedHandler?.(request); + response.json({ received: request.body }); + }); + app.get('/free', (_request, response) => { + response.json({ ok: true, note: 'no AEP authentication or payment required' }); + }); + + return { app, credentialStore, service }; +} + +function openApiDocument(): Record { + return { + openapi: '3.1.0', + info: { title: 'AEP and MPP Express example', version: '1.0.0' }, + components: { + securitySchemes: { + aepApiKey: { + type: 'apiKey', + in: 'header', + name: 'x-aep-api-key', + 'x-aep-authentication-method': AEP_GRANT_TYPE_API_KEY, + }, + }, + }, + paths: { + '/api/widgets': { + get: { security: [{ aepApiKey: [] }], responses: { '200': { description: 'Paid widgets' } } }, + }, + '/api/upload': { + post: { security: [{ aepApiKey: [] }], responses: { '200': { description: 'Paid upload' } } }, + }, + }, + }; +} diff --git a/examples/mpp-aep-seller-express/src/index.ts b/examples/mpp-aep-seller-express/src/index.ts new file mode 100644 index 0000000..57ad22f --- /dev/null +++ b/examples/mpp-aep-seller-express/src/index.ts @@ -0,0 +1,49 @@ +import 'dotenv/config'; +import type { Server } from 'node:http'; + +import { createMppAepSellerApp } from './app.js'; + +const apiKey = requiredEnvironment('INFLOW_API_KEY'); +const mppSecretKey = requiredEnvironment('MPP_SECRET_KEY'); +const serviceDid = requiredEnvironment('SERVICE_DID'); +const baseUrl = process.env['INFLOW_BASE_URL']; +const host = process.env['HOST'] ?? '127.0.0.1'; +const port = parsePort(process.env['PORT'] ?? '3000'); +const listenUrl = `http://${host}:${port.toString()}`; +const { app } = createMppAepSellerApp({ + apiKey, + ...(baseUrl === undefined ? {} : { baseUrl }), + listenUrl, + mppSecretKey, + serviceDid, +}); +const server: Server = app.listen(port, host); + +server.once('error', (error) => { + console.error(`Unable to listen on ${listenUrl}:`, error); + process.exitCode = 1; +}); +server.once('listening', () => { + console.log(`AEP and MPP seller listening on ${listenUrl}`); + console.log(` GET ${listenUrl}/.well-known/aep`); + console.log(` POST ${listenUrl}/aep/enroll`); + console.log(` POST ${listenUrl}/aep/grant`); + console.log(` GET ${listenUrl}/aep/status`); + console.log(` POST ${listenUrl}/aep/revoke`); + console.log(` GET ${listenUrl}/openapi.json`); + console.log(` GET ${listenUrl}/api/widgets`); + console.log(` POST ${listenUrl}/api/upload`); + console.log(` GET ${listenUrl}/free`); +}); + +function requiredEnvironment(name: string): string { + const value = process.env[name]; + if (value === undefined || value.length === 0) throw new Error(`Missing required environment variable: ${name}`); + return value; +} + +function parsePort(value: string): number { + const port = Number(value); + if (!Number.isInteger(port) || port <= 0 || port > 65_535) throw new TypeError(`Invalid PORT: ${value}`); + return port; +} diff --git a/examples/mpp-aep-seller-express/test/sequential.test.ts b/examples/mpp-aep-seller-express/test/sequential.test.ts new file mode 100644 index 0000000..eeb9e1c --- /dev/null +++ b/examples/mpp-aep-seller-express/test/sequential.test.ts @@ -0,0 +1,231 @@ +import { once } from 'node:events'; +import type { Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; + +import { AEP_GRANT_TYPE_API_KEY } from '@aep-foundation/core'; +import { createInMemoryServiceCredentialStore } from '@aep-foundation/service'; +import { decode, parseChallengeHeader } from '@inflowpayai/mpp'; +import express from 'express'; +import { Credential } from 'mppx'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { createMppAepSellerApp } from '../src/app.js'; + +const servers: Server[] = []; +const apiKey = 'aep-api-key'; + +afterEach(async () => { + await Promise.all(servers.splice(0).map(closeServer)); +}); + +describe('sequential AEP and MPP enforcement', () => { + it('enforces AEP before MPP, completes GET and POST with both credentials, and keeps credentials out of logs', async () => { + const requestLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); + const fixture = await startFixture(); + try { + const anonymous = await fetch(`${fixture.url}/api/widgets`); + expect(anonymous.status).toBe(401); + expect(anonymous.headers.get('www-authenticate')).toMatch(/^AEP /); + expect(anonymous.headers.get('www-authenticate')).not.toContain('Payment'); + expect(fixture.redeemCalls).toBe(0); + expect(fixture.aepPassed).toBe(0); + expect(fixture.handlerRequests).toHaveLength(0); + + const free = await fetch(`${fixture.url}/free`); + expect(free.status).toBe(200); + expect(fixture.redeemCalls).toBe(0); + expect(fixture.aepPassed).toBe(0); + + const paymentRequired = await fetch(`${fixture.url}/api/widgets`, { + headers: { 'x-aep-api-key': apiKey }, + }); + expect(paymentRequired.status).toBe(402); + expect(paymentRequired.headers.get('www-authenticate')).toMatch(/^Payment /); + expect(paymentRequired.headers.get('www-authenticate')).not.toContain('AEP '); + expect(fixture.aepPassed).toBe(1); + expect(fixture.handlerRequests).toHaveLength(0); + + const rejectedPayment = await fetch(`${fixture.url}/api/widgets`, { + headers: { + authorization: paymentAuthorization(paymentRequired), + 'x-aep-api-key': apiKey, + }, + }); + expect(rejectedPayment.status).toBe(402); + expect(fixture.redeemCalls).toBe(1); + expect(fixture.handlerRequests).toHaveLength(0); + + fixture.redeemOutcome = 'success'; + const getChallenge = await fetch(`${fixture.url}/api/widgets`, { headers: { 'x-aep-api-key': apiKey } }); + const getAuthorization = paymentAuthorization(getChallenge); + const getResponse = await fetch(`${fixture.url}/api/widgets`, { + headers: { authorization: getAuthorization, 'x-aep-api-key': apiKey }, + }); + expect(getResponse.status).toBe(200); + expect(await getResponse.json()).toEqual({ widgets: [1, 2, 3] }); + + const body = '{"widget":"one","nested":{"preserved":true}}'; + const callerHeader = 'retained-through-payment-replay'; + const postChallenge = await fetch(`${fixture.url}/api/upload`, { + body, + headers: { 'content-type': 'application/json', 'x-aep-api-key': apiKey, 'x-caller-header': callerHeader }, + method: 'POST', + }); + const postResponse = await fetch(`${fixture.url}/api/upload`, { + body, + headers: { + authorization: paymentAuthorization(postChallenge), + 'content-type': 'application/json', + 'x-aep-api-key': apiKey, + 'x-caller-header': callerHeader, + }, + method: 'POST', + }); + expect(postResponse.status).toBe(200); + expect(await postResponse.json()).toEqual({ received: JSON.parse(body) }); + expect(fixture.handlerRequests).toHaveLength(2); + expect(fixture.handlerRequests[1]).toMatchObject({ + authorization: expect.stringMatching(/^Payment /), + 'x-aep-api-key': apiKey, + 'x-caller-header': callerHeader, + }); + expect(fixture.configCalls).toBeGreaterThan(0); + expect(fixture.redeemCalls).toBe(3); + expect(requestLog.mock.calls.flat().join(' ')).not.toContain(apiKey); + expect(requestLog.mock.calls.flat().join(' ')).not.toContain(getAuthorization); + } finally { + requestLog.mockRestore(); + } + }); +}); + +async function startFixture() { + let configCalls = 0; + let redeemCalls = 0; + let redeemOutcome: 'problem' | 'success' = 'problem'; + let aepPassed = 0; + const handlerRequests: Record[] = []; + const credentialStore = createInMemoryServiceCredentialStore(); + await credentialStore.saveCredential({ + agentDid: 'did:web:agent.example', + createdAt: new Date().toISOString(), + credential: { + api_key: apiKey, + credential_id: 'credential-1', + expires_at: new Date(Date.now() + 60_000).toISOString(), + header: 'x-aep-api-key', + scopes: ['read:widgets', 'write:uploads'], + }, + credentialId: 'credential-1', + expiresAt: new Date(Date.now() + 60_000).toISOString(), + grantType: AEP_GRANT_TYPE_API_KEY, + }); + const configApp = express(); + configApp.use(express.json()); + configApp.get('/v1/mpp/config', (_request, response) => { + configCalls += 1; + response.json(configResponse()); + }); + configApp.post('/v1/mpp/redeem', (_request, response) => { + redeemCalls += 1; + if (redeemOutcome === 'problem') { + response.json({ + problem: { + detail: 'Balance too low.', + status: 402, + title: 'Payment Insufficient', + type: 'https://paymentauth.org/problems/payment-insufficient', + }, + }); + return; + } + response.json({ + receipt: { + challengeId: 'challenge-1', + method: 'inflow', + reference: 'settlement-1', + status: 'success', + timestamp: '2026-07-16T00:00:00.000Z', + }, + receiptHeader: 'ignored-by-mppx', + }); + }); + const configServer = configApp.listen(0, '127.0.0.1'); + servers.push(configServer); + await once(configServer, 'listening'); + const configAddress = configServer.address() as AddressInfo; + const { app } = createMppAepSellerApp({ + apiKey: 'seller-api-key', + baseUrl: `http://127.0.0.1:${configAddress.port.toString()}`, + credentialStore, + listenUrl: 'http://127.0.0.1:3000', + mppSecretKey: 'test-secret-key-with-at-least-thirty-two-characters', + onAepPassed: () => { + aepPassed += 1; + }, + onProtectedHandler: (request) => { + handlerRequests.push({ + authorization: request.get('authorization'), + 'x-aep-api-key': request.get('x-aep-api-key'), + 'x-caller-header': request.get('x-caller-header'), + }); + }, + serviceDid: 'did:web:127.0.0.1%3A4100:services:example-service', + }); + const server = app.listen(0, '127.0.0.1'); + servers.push(server); + await once(server, 'listening'); + const address = server.address() as AddressInfo; + + return { + get aepPassed() { + return aepPassed; + }, + get configCalls() { + return configCalls; + }, + get redeemCalls() { + return redeemCalls; + }, + get redeemOutcome() { + return redeemOutcome; + }, + set redeemOutcome(value: 'problem' | 'success') { + redeemOutcome = value; + }, + handlerRequests, + url: `http://127.0.0.1:${address.port.toString()}`, + }; +} + +function configResponse() { + return { + featureFlags: { idempotencyKeyEnabled: true }, + replayPolicy: { managedBy: 'psp' }, + sellerId: '22222222-2222-2222-2222-222222222222', + supportedMethods: [ + { + id: 'inflow', + label: 'InFlow', + methodDetails: { currencyRails: { USDC: { rail: 'balance' } } }, + supportedCurrencies: ['USDC'], + supportedIntents: ['charge'], + }, + ], + }; +} + +function paymentAuthorization(response: Response): string { + const header = response.headers.get('www-authenticate'); + if (header === null) throw new Error('missing MPP challenge'); + const challenge = parseChallengeHeader(header); + return Credential.serialize({ + challenge: { ...challenge, request: decode(challenge.request) }, + payload: { transactionId: `transaction-${Math.random().toString()}`, type: 'balance' }, + source: 'did:inflow:payer', + }); +} + +function closeServer(server: Server): Promise { + return new Promise((resolve) => server.close(() => resolve())); +} diff --git a/examples/mpp-aep-seller-express/tsconfig.json b/examples/mpp-aep-seller-express/tsconfig.json new file mode 100644 index 0000000..7379636 --- /dev/null +++ b/examples/mpp-aep-seller-express/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e57fae..5302459 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,49 @@ importers: specifier: ^2.1.0 version: 2.1.9(@types/node@24.12.4)(msw@2.14.6(@types/node@24.12.4)(typescript@5.9.3)) + examples/mpp-aep-seller-express: + dependencies: + '@aep-foundation/core': + specifier: ^0.2.0 + version: 0.2.0 + '@aep-foundation/express': + specifier: ^0.2.0 + version: 0.2.0(express@5.2.1) + '@aep-foundation/service': + specifier: ^0.2.0 + version: 0.2.0 + '@inflowpayai/mpp-seller': + specifier: workspace:^ + version: link:../../packages/mpp-seller + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + express: + specifier: ^5.0.0 + version: 5.2.1 + mppx: + specifier: ^0.6.28 + version: 0.6.28(express@5.2.1)(hono@4.12.19)(typescript@5.9.3)(viem@2.50.4(typescript@5.9.3)(zod@4.4.3)) + devDependencies: + '@inflowpayai/mpp': + specifier: workspace:^ + version: link:../../packages/mpp + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/node': + specifier: ^24.0.0 + version: 24.12.4 + tsx: + specifier: ^4.0.0 + version: 4.22.2 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@24.12.4)(msw@2.14.6(@types/node@24.12.4)(typescript@5.9.3)) + examples/mpp-buyer-fetch: dependencies: '@inflowpayai/mpp-buyer': @@ -547,6 +590,20 @@ packages: '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + '@aep-foundation/core@0.2.0': + resolution: {integrity: sha512-Zc6Tyzi/A3z2YOPoND89JVD0xZYi4XzxMEcotCmMWD8epsfOYZ3EmgCVtgadfGYcAkxkzE6MlKQN0yQtG+CfHA==} + engines: {node: '>=22.0.0'} + + '@aep-foundation/express@0.2.0': + resolution: {integrity: sha512-eUEEhNlaGu55fCxkGy/mS7Grp7WI7hgTzLzNjKOy5/hsMdo1GccvSZ9WL6eMujj5EfYM++lnO5m2muvEzaydhA==} + engines: {node: '>=22.0.0'} + peerDependencies: + express: '>=5' + + '@aep-foundation/service@0.2.0': + resolution: {integrity: sha512-fliagmVmYe025srVgCw7LUUn23mSEDTbe7+Bu4ocfTekMSePiZ2LlimVt2kxmZ8peksBML5ttCeaa8kw5VICaA==} + engines: {node: '>=22.0.0'} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -3116,6 +3173,9 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -4224,6 +4284,20 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} + '@aep-foundation/core@0.2.0': + dependencies: + jose: 6.2.3 + + '@aep-foundation/express@0.2.0(express@5.2.1)': + dependencies: + '@aep-foundation/core': 0.2.0 + '@aep-foundation/service': 0.2.0 + express: 5.2.1 + + '@aep-foundation/service@0.2.0': + dependencies: + '@aep-foundation/core': 0.2.0 + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -6764,6 +6838,8 @@ snapshots: jose@5.10.0: {} + jose@6.2.3: {} + joycon@3.1.1: {} js-yaml@3.14.2: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0605598..f624a0e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,3 +13,8 @@ allowBuilds: esbuild: true msw: true sharp: true + +minimumReleaseAgeExclude: + - '@aep-foundation/core@0.2.0' + - '@aep-foundation/express@0.2.0' + - '@aep-foundation/service@0.2.0'