1import { Database } from "bun:sqlite";2import { createHash, randomUUID } from "node:crypto";3import {4 providerCapabilitiesSchema,5 COMMERCE_EVENT_VERSION,6 HTTP_BINDING,7} from "openiap-commerce-protocol";8import { protocolError, validate } from "./contract.mjs";9import { createErasureLedger } from "./erasure.mjs";10 11export const FIXTURE = Object.freeze({12 store: "fixture",13 evidence: "local-purchase-alice",14 userId: "demo_alice",15 productId: "premium.monthly",16 startsAt: Date.UTC(2026, 8, 7, 9),17 expiresAt: Date.UTC(2026, 9, 7, 9),18});19export const CREDENTIALS = Object.freeze({20 verification: "Bearer local-demo-verification",21 server: "Bearer local-demo-server",22});23const fingerprint = (value) => createHash("sha256").update(value).digest("hex");24 25export function isEntitled(state, expiresAt, now) {26 return (27 (state === "Active" || state === "InGracePeriod") &&28 (expiresAt == null || now < expiresAt)29 );30}31 32// This adapter recognizes one fictional purchase; it never contacts a store.33function fixtureEvidence(input) {34 switch (input.store) {35 case "apple":36 return input.apple?.jws;37 case "google":38 return input.google?.purchaseToken;39 case "amazon":40 return (41 input.amazon &&42 JSON.stringify([43 input.amazon.userId,44 input.amazon.receiptId,45 input.amazon.sandbox === true,46 ])47 );48 case "horizon":49 return (50 input.horizon &&51 JSON.stringify([input.horizon.userId, input.horizon.sku])52 );53 default:54 return input.evidence;55 }56}57 58function verifyFixture(input, fixture) {59 if (input.store !== fixture.store) return { error: "UNSUPPORTED_STORE" };60 const evidence = fixtureEvidence(input);61 if (typeof evidence !== "string") return { error: "INVALID_REQUEST" };62 if (evidence === "local-upstream-outage") {63 return { error: "VERIFICATION_FAILED" };64 }65 const current = fixture.currentVerdict?.();66 if (current === "outage") return { error: "VERIFICATION_FAILED" };67 return { accepted: evidence === fixture.evidence && current !== false };68}69 70export function createProvider(path, now, fixture = FIXTURE) {71 const db = new Database(path, { create: true });72 db.exec(`73 PRAGMA journal_mode = WAL;74 CREATE TABLE IF NOT EXISTS purchases (75 fingerprint TEXT PRIMARY KEY, user_id TEXT, product_id TEXT NOT NULL,76 state TEXT NOT NULL, expires_at INTEGER NOT NULL, will_renew INTEGER NOT NULL,77 observed_at INTEGER NOT NULL, entitlement_granted INTEGER NOT NULL DEFAULT 078 );79 CREATE TABLE IF NOT EXISTS observations (id TEXT PRIMARY KEY);80 CREATE TABLE IF NOT EXISTS outbox (81 event_id TEXT PRIMARY KEY, delivery_id TEXT NOT NULL, body TEXT NOT NULL,82 attempts INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending',83 next_at INTEGER NOT NULL DEFAULT 084 );85 `);86 const erasures = createErasureLedger(db);87 if (88 !db89 .query("PRAGMA table_info(purchases)")90 .all()91 .some((column) => column.name === "erased")92 )93 db.exec(94 "ALTER TABLE purchases ADD COLUMN erased INTEGER NOT NULL DEFAULT 0",95 );96 97 function snapshot(row) {98 return {99 productId: row.product_id,100 state: row.state,101 active: isEntitled(row.state, row.expires_at, now()),102 store: fixture.store,103 expiresAt: row.expires_at,104 willRenew: Boolean(row.will_renew),105 };106 }107 108 function rowsFor(userId) {109 return db.query("SELECT * FROM purchases WHERE user_id = ?").all(userId);110 }111 112 function entitlements(userId) {113 if (fixture.pointInTime) {114 const current = fixture.currentVerdict?.();115 if (current === "outage") return { error: "VERIFICATION_FAILED" };116 return {117 userId,118 productIds:119 current === false120 ? []121 : [...new Set(rowsFor(userId).map((row) => row.product_id))],122 subscriptions: [],123 };124 }125 const subscriptions = rowsFor(userId)126 .map(snapshot)127 .filter((row) => row.active);128 return {129 userId,130 productIds: [...new Set(subscriptions.map((row) => row.productId))],131 subscriptions,132 };133 }134 135 const eventTypes = [136 "entitlement.granted",137 "subscription.canceled",138 "subscription.expired",139 "entitlement.revoked",140 ];141 const capabilityNames = Object.keys(142 providerCapabilitiesSchema.$defs.StoreCapabilities.properties,143 );144 const supported = new Set([145 "initialValidation",146 "subscriptions",147 "entitlements",148 "serverNotifications",149 "expiration",150 ]);151 const capabilities = {152 specVersion: HTTP_BINDING.protocolVersion,153 implementation: {154 name: "Commerce Protocol Example — fictional fixture store",155 },156 eventTypes,157 stores: {158 [fixture.store]: Object.fromEntries(159 capabilityNames.map((key) => [160 key,161 {162 provider:163 supported.has(key) &&164 (!fixture.pointInTime ||165 ["initialValidation", "entitlements"].includes(key)),166 implementation:167 supported.has(key) &&168 (!fixture.pointInTime ||169 ["initialValidation", "entitlements"].includes(key)),170 notes:171 "Local fixture demonstration only; no real store integration or profile conformance claim.",172 },173 ]),174 ),175 },176 };177 178 function enqueue(eventType, row, occurredAt, sourceStoreEventId) {179 const body = {180 eventId: randomUUID(),181 eventType,182 eventVersion: COMMERCE_EVENT_VERSION,183 occurredAt,184 processedAt: now(),185 store: fixture.store,186 environment: "local-fixture",187 projectId: "commerce_example",188 productId: row.product_id,189 ...(row.user_id ? { userId: row.user_id } : {}),190 subscription: snapshot(row),191 ...(sourceStoreEventId ? { sourceStoreEventId } : {}),192 };193 if (!validate("#/$defs/CommerceEvent", body))194 throw new Error("Invalid event");195 db.query(196 "INSERT INTO outbox (event_id, delivery_id, body) VALUES (?, ?, ?)",197 ).run(body.eventId, randomUUID(), JSON.stringify(body));198 }199 200 const handlers = {201 providerCapabilities: () => capabilities,202 verifyPurchase(input) {203 const verdict = verifyFixture(input, fixture);204 if (verdict.error) return verdict;205 if (verdict.accepted) {206 db.query(207 `INSERT OR IGNORE INTO purchases (fingerprint, user_id, product_id, state, expires_at, will_renew, observed_at) VALUES (?, NULL, ?, 'Active', ?, 1, ?)`,208 ).run(209 fingerprint(fixtureEvidence(input)),210 fixture.productId,211 fixture.expiresAt,212 fixture.startsAt,213 );214 }215 const expired = !fixture.pointInTime && now() >= fixture.expiresAt;216 return {217 store: fixture.store,218 isValid: verdict.accepted && !expired,219 state: !verdict.accepted220 ? "INAUTHENTIC"221 : expired222 ? "EXPIRED"223 : "ENTITLED",224 ...(verdict.accepted ? { productId: fixture.productId } : {}),225 environment: "local-fixture",226 };227 },228 bindPurchase(input) {229 if (input.store !== fixture.store) return { error: "UNSUPPORTED_STORE" };230 if (typeof fixtureEvidence(input) !== "string")231 return { error: "INVALID_REQUEST" };232 return db.transaction(() => {233 if (erasures.has(input.userId)) return { bound: false };234 const key = fingerprint(fixtureEvidence(input));235 const updated = db236 .query(237 "UPDATE purchases SET user_id = ? WHERE fingerprint = ? AND user_id IS NULL AND erased = 0",238 )239 .run(input.userId, key);240 const row = db241 .query("SELECT * FROM purchases WHERE fingerprint = ?")242 .get(key);243 if (244 updated.changes &&245 !fixture.pointInTime &&246 isEntitled(row.state, row.expires_at, now())247 ) {248 enqueue("entitlement.granted", row, row.observed_at);249 db.query(250 "UPDATE purchases SET entitlement_granted = 1 WHERE fingerprint = ?",251 ).run(key);252 }253 return { bound: row?.user_id === input.userId };254 })();255 },256 entitlements: (input) => entitlements(input.userId),257 eraseUser(input) {258 return db.transaction(() => {259 const jobId = erasures.remember(input.userId);260 db.query(261 "UPDATE purchases SET user_id = NULL, erased = 1, entitlement_granted = 0 WHERE user_id = ?",262 ).run(input.userId);263 // A claimed delivery may already be in flight; the receiver erases its own copy.264 db.query(265 "DELETE FROM outbox WHERE json_extract(body, '$.userId') = ?",266 ).run(input.userId);267 return { accepted: true, jobId, status: "completed" };268 })();269 },270 subscriptionStatus(input) {271 if (fixture.pointInTime) return { active: false };272 const snapshots = rowsFor(input.userId).map(snapshot);273 const subscription = snapshots.find((row) => row.active) ?? snapshots[0];274 return {275 active: snapshots.some((row) => row.active),276 ...(subscription ? { subscription } : {}),277 };278 },279 };280 281 async function fetch(request) {282 const url = new URL(request.url);283 const spec = HTTP_BINDING.operations.find(284 (entry) => entry.path === url.pathname && entry.method === request.method,285 );286 if (!spec) return protocolError("NOT_FOUND");287 const auth = request.headers.get("authorization");288 if (spec.auth !== "none") {289 if (!Object.values(CREDENTIALS).includes(auth))290 return protocolError("UNAUTHORIZED");291 if (spec.auth === "server" && auth !== CREDENTIALS.server)292 return protocolError("FORBIDDEN");293 }294 if (!handlers[spec.name]) return protocolError("UNSUPPORTED_PROFILE");295 let input = null;296 if (spec.input) {297 try {298 input =299 request.method === "GET"300 ? Object.fromEntries(url.searchParams)301 : await request.json();302 } catch {303 return protocolError("INVALID_REQUEST");304 }305 if (!validate(spec.input, input)) return protocolError("INVALID_REQUEST");306 }307 try {308 const result = handlers[spec.name](input);309 if (result.error) return protocolError(result.error);310 if (!validate(spec.result, result))311 throw new Error("Invalid protocol response");312 return Response.json(result, { status: spec.successStatus });313 } catch {314 return protocolError("INTERNAL_ERROR");315 }316 }317 318 function observe({ id, kind, occurredAt }) {319 if (320 !["cancel", "expire"].includes(kind) ||321 !Number.isSafeInteger(occurredAt) ||322 occurredAt > now()323 ) {324 throw new Error("Invalid fixture observation");325 }326 return db.transaction(() => {327 if (db.query("SELECT id FROM observations WHERE id = ?").get(id))328 return false;329 const row = db330 .query("SELECT * FROM purchases WHERE fingerprint = ?")331 .get(fingerprint(fixture.evidence));332 if (!row) throw new Error("Verify the fixture purchase first");333 if (kind === "expire" && occurredAt < row.expires_at) {334 throw new Error("Premature expiry requires store reconciliation");335 }336 db.query("INSERT INTO observations VALUES (?)").run(id);337 if (occurredAt < row.observed_at) return false;338 if (339 (kind === "cancel" && (!row.will_renew || row.state !== "Active")) ||340 (kind === "expire" && row.state === "Expired")341 )342 return false;343 const next = {344 ...row,345 will_renew: 0,346 state: kind === "expire" ? "Expired" : row.state,347 };348 db.query(349 "UPDATE purchases SET state = ?, will_renew = 0, observed_at = ? WHERE fingerprint = ?",350 ).run(next.state, occurredAt, row.fingerprint);351 const types = [352 kind === "cancel" ? "subscription.canceled" : "subscription.expired",353 ];354 if (355 row.entitlement_granted &&356 !isEntitled(next.state, next.expires_at, now())357 ) {358 types.push("entitlement.revoked");359 db.query(360 "UPDATE purchases SET entitlement_granted = 0 WHERE fingerprint = ?",361 ).run(row.fingerprint);362 }363 for (const eventType of types) enqueue(eventType, next, occurredAt, id);364 return true;365 })();366 }367 368 function inspect() {369 return {370 purchases: db371 .query(372 "SELECT user_id AS userId, product_id AS productId, state, will_renew AS willRenew FROM purchases",373 )374 .all(),375 access: entitlements(fixture.userId),376 deliveries: db377 .query(378 "SELECT event_id AS eventId, delivery_id AS deliveryId, attempts, status, body FROM outbox ORDER BY rowid",379 )380 .all()381 .map(({ body, ...row }) => ({382 ...row,383 eventType: JSON.parse(body).eventType,384 })),385 };386 }387 388 return { db, fetch, observe, inspect, close: () => db.close() };389}390