1import assert from "node:assert/strict";2import { join } from "node:path";3 4// Invoked only inside run-commerce-interop's isolated deployment.5export async function runStoreCoverage({6 example,7 output,8 key,9 kitUrl,10 receiver,11 appleJws,12 storeState,13 inspect,14 check,15 trace,16}) {17 const { createProvider, CREDENTIALS } = await import(18 join(example, "provider.mjs")19 );20 const { createCommerceClient } = await import(21 join(example, "composition/commerce-client.mjs")22 );23 const { startAppBackend } = await import(24 join(example, "composition/app-backend.mjs")25 );26 const kit = createCommerceClient({27 baseUrl: kitUrl,28 credential: `Bearer ${key}`,29 });30 let sqlite;31 const providerServer = Bun.serve({32 hostname: "127.0.0.1",33 port: 0,34 fetch: (request) => sqlite.fetch(request),35 });36 const exampleUrl = `http://127.0.0.1:${providerServer.port}`;37 const fixtureClient = createCommerceClient({38 baseUrl: exampleUrl,39 credential: CREDENTIALS.server,40 });41 const app = startAppBackend({42 path: join(output, "all-stores-app.sqlite"),43 receiver,44 providers: {45 example: { baseUrl: exampleUrl, credential: CREDENTIALS.server },46 iapkit: { baseUrl: kitUrl, credential: `Bearer ${key}` },47 },48 resolveSession: (request) =>49 request.headers.get("authorization")?.replace("Bearer ", ""),50 resolveStoreUser: (request, store) =>51 request.headers.get("authorization") === `Bearer store_${store}`52 ? `store-user-${store}`53 : null,54 });55 const endpoints = [app.url, receiver.url];56 const results = {};57 async function request(58 store,59 path,60 method = "GET",61 input,62 session = `store_${store}`,63 ) {64 const response = await fetch(app.url + path, {65 method,66 headers: {67 authorization: `Bearer ${session}`,68 "content-type": "application/json",69 },70 ...(input ? { body: JSON.stringify(input) } : {}),71 });72 const result = await response.json().catch(() => null);73 trace.push({ store, path, method, status: response.status, result });74 return { status: response.status, result };75 }76 try {77 for (const store of ["apple", "amazon", "horizon"]) {78 const input =79 store === "apple"80 ? { store, apple: { jws: appleJws } }81 : store === "amazon"82 ? {83 store,84 amazon: {85 userId: "store-user-amazon",86 receiptId: "local-amazon-receipt",87 sandbox: true,88 },89 }90 : {91 store,92 horizon: {93 userId: "store-user-horizon",94 sku: "premium.monthly",95 },96 };97 const userId = `store_${store}`;98 const evidence =99 store === "apple"100 ? appleJws101 : store === "amazon"102 ? JSON.stringify([103 input.amazon.userId,104 input.amazon.receiptId,105 true,106 ])107 : JSON.stringify([input.horizon.userId, input.horizon.sku]);108 const fixture = {109 store,110 evidence,111 userId,112 productId: "premium.monthly",113 startsAt: Date.now(),114 expiresAt: Date.now() + 3600000,115 pointInTime: store !== "apple",116 currentVerdict: () => storeState[store],117 };118 sqlite = createProvider(119 join(output, `store-${store}.sqlite`),120 Date.now,121 fixture,122 );123 results[store] = {};124 for (const [name, client] of [125 ["example", fixtureClient],126 ["iapkit", kit],127 ]) {128 app.select(name);129 check(130 `${store}/${name}: keeps the same app and receiver`,131 [app.url, receiver.url],132 endpoints,133 );134 const before = await client.call("entitlements", { userId });135 check(`${store}/${name}: starts without access`, before.productIds, []);136 check(137 `${store}/${name}: unverified evidence cannot bind`,138 (await client.call("bindPurchase", { ...input, userId })).bound,139 false,140 );141 const verified = await client.call("verifyPurchase", input);142 check(143 `${store}/${name}: verifies the store-specific input`,144 verified.isValid,145 true,146 );147 check(148 `${store}/${name}: verification alone grants no account access`,149 (await client.call("entitlements", { userId })).productIds,150 [],151 );152 const bought = await request(store, "/purchase", "POST", input);153 check(`${store}/${name}: app purchase succeeds`, bought.status, 200);154 check(155 `${store}/${name}: app receives access`,156 bought.result?.productIds,157 ["premium.monthly"],158 );159 check(160 `${store}/${name}: binding is repeatable`,161 (await client.call("bindPurchase", { ...input, userId })).bound,162 true,163 );164 check(165 `${store}/${name}: another account cannot claim the purchase`,166 (await client.call("bindPurchase", { ...input, userId: "store_bob" }))167 .bound,168 false,169 );170 const access = await client.call("entitlements", { userId });171 check(172 `${store}/${name}: access includes only verified products`,173 access.productIds,174 ["premium.monthly"],175 );176 check(177 `${store}/${name}: tokenless access does not leak store evidence`,178 JSON.stringify(access).includes("store-user-") ||179 JSON.stringify(access).includes("local-amazon-receipt"),180 false,181 );182 if (store !== "apple") {183 check(184 `${store}/${name}: does not invent subscription lifecycle records`,185 access.subscriptions,186 [],187 );188 const forged = {189 store,190 [store]: { ...input[store], userId: "someone-else" },191 };192 check(193 `${store}/${name}: rejects another store account in the app session`,194 (await request(store, "/purchase", "POST", forged)).status,195 403,196 );197 storeState[store] = "outage";198 check(199 `${store}/${name}: an upstream outage fails the access read`,200 (await request(store, "/access")).status,201 503,202 );203 storeState[store] = false;204 check(205 `${store}/${name}: a negative store recheck removes access`,206 (await client.call("entitlements", { userId })).productIds,207 [],208 );209 storeState[store] = true;210 check(211 `${store}/${name}: rechecked ownership restores access`,212 (await client.call("entitlements", { userId })).productIds,213 ["premium.monthly"],214 );215 if (name === "iapkit") {216 const updatedAt = async () =>217 (await inspect()).purchases.find(218 (row) => row.appUserId === userId,219 )?.updatedAt;220 const before = await updatedAt();221 await client.call("entitlements", { userId });222 check(223 `${store}/iapkit: an unchanged recheck verdict is not written back`,224 await updatedAt(),225 before,226 );227 }228 }229 results[store][name] = {230 verified: verified.isValid,231 productIds: access.productIds,232 subscriptions: access.subscriptions.length,233 storeRecheck: store !== "apple",234 };235 }236 app.select("example");237 check(238 `${store}: switching back needs no consumer edit`,239 (await request(store, "/access")).result.productIds,240 ["premium.monthly"],241 );242 const deleted = await request(store, "/account", "DELETE");243 check(244 `${store}: the app accepts account erasure`,245 deleted.result?.accepted,246 true,247 );248 let pending = 1;249 for (let attempt = 0; attempt < 100 && pending; attempt++) {250 pending = await app.drainErasure();251 if (pending) await Bun.sleep(100);252 }253 check(`${store}: both providers complete erasure`, pending, 0);254 for (const [name, client] of [255 ["example", fixtureClient],256 ["iapkit", kit],257 ]) {258 check(259 `${store}/${name}: erased account has no access`,260 (await client.call("entitlements", { userId })).productIds,261 [],262 );263 check(264 `${store}/${name}: erased user id cannot bind again`,265 (await client.call("bindPurchase", { ...input, userId })).bound,266 false,267 );268 }269 // The erased user id stays refused while its job is retained; IAPKit's270 // Amazon/Horizon evidence does not, unlike the example's permanent mark.271 if (["amazon", "horizon"].includes(store)) {272 const next = `store_next_${store}`;273 check(274 `${store}/example: erased evidence cannot bind to another account`,275 (await fixtureClient.call("bindPurchase", { ...input, userId: next }))276 .bound,277 false,278 );279 check(280 `${store}/iapkit: another account can bind the erased evidence`,281 (await kit.call("bindPurchase", { ...input, userId: next })).bound,282 true,283 );284 check(285 `${store}/iapkit: the new account receives the rechecked access`,286 (await kit.call("entitlements", { userId: next })).productIds,287 ["premium.monthly"],288 );289 }290 check(291 `${store}: an erased session cannot submit another purchase`,292 (await request(store, "/purchase", "POST", input)).status,293 401,294 );295 const state = await inspect();296 check(297 `${store}: IAPKit removes account identity from persisted purchases`,298 state.purchases.some((row) => row.appUserId === userId),299 false,300 );301 sqlite.close();302 sqlite = null;303 }304 assert.equal(Object.keys(results).length, 3);305 return results;306 } finally {307 await app.close();308 await providerServer.stop(true);309 sqlite?.close();310 }311}312