mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 12:54:03 +00:00
Add the AgentBBS M5 "AgentAd marketplace" spec and a working reference
implementation built on the existing @logicsrc/schemas AgentAd contracts.
- docs/agentad-marketplace.md: two-sided exchange PRD (buy/sell sides,
match -> auction -> pace -> serve -> meter -> settle, CoinPay settlement,
AgentBBS as reference publisher, milestones M5.0-M5.5).
- packages/agentad (@logicsrc/agentad): reference exchange
- builders that emit schema-valid, always-disclosed ad/campaign/placement docs
- HMAC-signed, single-use impression/click tracking tokens
- AgentAdExchange: targeting/format/category matching, second-price auction,
budget pacing + daily caps, frequency capping, token-driven metering
- pluggable settlement (InMemorySettlement) that can't overspend escrow
- runtime validation against the canonical agentad-*.schema.json
- 22 vitest cases (builders, tokens, full serve/meter/settle lifecycle).
- Wire package into root build; link the PRD from README + docs/agentad.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
3.1 KiB
TypeScript
72 lines
3.1 KiB
TypeScript
// Runtime validation against the canonical AgentAd JSON Schemas. Mirrors the
|
|
// approach in @logicsrc/validators: import the schema documents directly from
|
|
// @logicsrc/schemas and compile them with Ajv 2020. Keeping this local (rather
|
|
// than importing the built @logicsrc/validators dist) lets the exchange and its
|
|
// tests run straight from source with no cross-package build step.
|
|
|
|
import * as Ajv2020Module from "ajv/dist/2020.js";
|
|
import * as addFormatsModule from "ajv-formats";
|
|
import type { ErrorObject } from "ajv";
|
|
|
|
import adSchema from "../../schemas/schemas/agentad-ad.schema.json" with { type: "json" };
|
|
import placementSchema from "../../schemas/schemas/agentad-placement.schema.json" with { type: "json" };
|
|
import adRequestSchema from "../../schemas/schemas/agentad-ad-request.schema.json" with { type: "json" };
|
|
import adResponseSchema from "../../schemas/schemas/agentad-ad-response.schema.json" with { type: "json" };
|
|
import impressionSchema from "../../schemas/schemas/agentad-impression.schema.json" with { type: "json" };
|
|
import clickSchema from "../../schemas/schemas/agentad-click.schema.json" with { type: "json" };
|
|
import campaignSchema from "../../schemas/schemas/agentad-campaign.schema.json" with { type: "json" };
|
|
|
|
type CompiledValidator = { (data: unknown): boolean; errors?: ErrorObject[] | null };
|
|
const Ajv2020 = (Ajv2020Module as unknown as {
|
|
default: new (options: Record<string, unknown>) => { compile: (schema: unknown) => CompiledValidator };
|
|
}).default;
|
|
const addFormats = (addFormatsModule as unknown as { default: (ajv: unknown) => void }).default;
|
|
|
|
export const agentAdSchemas = {
|
|
"agentad-ad": adSchema,
|
|
"agentad-placement": placementSchema,
|
|
"agentad-ad-request": adRequestSchema,
|
|
"agentad-ad-response": adResponseSchema,
|
|
"agentad-impression": impressionSchema,
|
|
"agentad-click": clickSchema,
|
|
"agentad-campaign": campaignSchema
|
|
} as const;
|
|
|
|
export type AgentAdSchemaKind = keyof typeof agentAdSchemas;
|
|
|
|
export type ValidationResult =
|
|
| { ok: true; kind: AgentAdSchemaKind; data: unknown }
|
|
| { ok: false; kind: AgentAdSchemaKind; errors: ErrorObject[] };
|
|
|
|
const cache = new Map<AgentAdSchemaKind, CompiledValidator>();
|
|
|
|
function compiled(kind: AgentAdSchemaKind): CompiledValidator {
|
|
const existing = cache.get(kind);
|
|
if (existing) return existing;
|
|
|
|
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
addFormats(ajv);
|
|
const fn = ajv.compile(agentAdSchemas[kind]);
|
|
cache.set(kind, fn);
|
|
return fn;
|
|
}
|
|
|
|
export function validate(kind: AgentAdSchemaKind, data: unknown): ValidationResult {
|
|
const fn = compiled(kind);
|
|
const ok = fn(data);
|
|
return ok
|
|
? { ok: true, kind, data }
|
|
: { ok: false, kind, errors: fn.errors ?? [] };
|
|
}
|
|
|
|
/** Validate or throw with a readable message. Returns the value narrowed to T. */
|
|
export function assertValid<T>(kind: AgentAdSchemaKind, data: T): T {
|
|
const result = validate(kind, data);
|
|
if (!result.ok) {
|
|
const detail = result.errors
|
|
.map((e) => `${e.instancePath || "/"} ${e.message ?? "is invalid"}`)
|
|
.join("; ");
|
|
throw new Error(`Invalid ${kind} document: ${detail}`);
|
|
}
|
|
return data;
|
|
}
|