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 if (store === "amazon" && name === "iapkit") {184 check(185 "amazon/iapkit: returns the RVS-verified subscription",186 access.subscriptions.map(187 ({ productId, store, state, active }) => ({188 productId,189 store,190 state,191 active,192 }),193 ),194 [195 {196 productId: "premium.monthly",197 store,198 state: "Active",199 active: true,200 },201 ],202 );203 check(204 "amazon/iapkit: does not invent expiry or renewal status",205 access.subscriptions.map(({ expiresAt, willRenew }) => ({206 expiresAt,207 willRenew,208 })),209 [{ expiresAt: undefined, willRenew: undefined }],210 );211 } else {212 check(213 `${store}/${name}: does not invent subscription lifecycle records`,214 access.subscriptions,215 [],216 );217 }218 const forged = {219 store,220 [store]: { ...input[store], userId: "someone-else" },221 };222 check(223 `${store}/${name}: rejects another store account in the app session`,224 (await request(store, "/purchase", "POST", forged)).status,225 403,226 );227 storeState[store] = "outage";228 check(229 `${store}/${name}: an upstream outage fails the access read`,230 (await request(store, "/access")).status,231 503,232 );233 storeState[store] = false;234 const rejected = await client.call("entitlements", { userId });235 check(236 `${store}/${name}: a negative store recheck removes access`,237 rejected.productIds,238 [],239 );240 check(241 `${store}/${name}: a negative recheck leaves no entitled subscription`,242 rejected.subscriptions,243 [],244 );245 storeState[store] = true;246 check(247 `${store}/${name}: rechecked ownership restores access`,248 (await client.call("entitlements", { userId })).productIds,249 ["premium.monthly"],250 );251 if (name === "iapkit") {252 const before = await inspect();253 await client.call("entitlements", { userId });254 const after = await inspect();255 if (store === "amazon") {256 const canonicalState = (snapshot) => ({257 purchases: snapshot.purchases258 .filter((row) => row.appUserId === userId)259 .map(({ _id, state, isValid, productId }) => ({260 _id,261 state,262 isValid,263 productId,264 })),265 subscriptions: snapshot.subscriptions266 .filter((row) => row.userId === userId)267 .map(({ _id, state, productId, store }) => ({268 _id,269 state,270 productId,271 store,272 })),273 events: snapshot.events.map(({ _id }) => _id),274 });275 check(276 "amazon/iapkit: unchanged rechecks preserve canonical rows and events",277 canonicalState(after),278 canonicalState(before),279 );280 } else {281 check(282 `${store}/iapkit: an unchanged recheck verdict is not written back`,283 after.purchases.find((row) => row.appUserId === userId)284 ?.updatedAt,285 before.purchases.find((row) => row.appUserId === userId)286 ?.updatedAt,287 );288 }289 }290 }291 results[store][name] = {292 verified: verified.isValid,293 productIds: access.productIds,294 subscriptions: access.subscriptions.length,295 storeRecheck: store !== "apple",296 };297 }298 app.select("example");299 check(300 `${store}: switching back needs no consumer edit`,301 (await request(store, "/access")).result.productIds,302 ["premium.monthly"],303 );304 const deleted = await request(store, "/account", "DELETE");305 check(306 `${store}: the app accepts account erasure`,307 deleted.result?.accepted,308 true,309 );310 let pending = 1;311 for (let attempt = 0; attempt < 100 && pending; attempt++) {312 pending = await app.drainErasure();313 if (pending) await Bun.sleep(100);314 }315 check(`${store}: both providers complete erasure`, pending, 0);316 for (const [name, client] of [317 ["example", fixtureClient],318 ["iapkit", kit],319 ]) {320 check(321 `${store}/${name}: erased account has no access`,322 (await client.call("entitlements", { userId })).productIds,323 [],324 );325 check(326 `${store}/${name}: erased user id cannot bind again`,327 (await client.call("bindPurchase", { ...input, userId })).bound,328 false,329 );330 }331 // The erased user id stays refused while its job is retained; IAPKit's332 // Amazon/Horizon evidence does not, unlike the example's permanent mark.333 if (["amazon", "horizon"].includes(store)) {334 const next = `store_next_${store}`;335 check(336 `${store}/example: erased evidence cannot bind to another account`,337 (await fixtureClient.call("bindPurchase", { ...input, userId: next }))338 .bound,339 false,340 );341 check(342 `${store}/iapkit: another account can bind the erased evidence`,343 (await kit.call("bindPurchase", { ...input, userId: next })).bound,344 true,345 );346 check(347 `${store}/iapkit: the new account receives the rechecked access`,348 (await kit.call("entitlements", { userId: next })).productIds,349 ["premium.monthly"],350 );351 }352 check(353 `${store}: an erased session cannot submit another purchase`,354 (await request(store, "/purchase", "POST", input)).status,355 401,356 );357 const state = await inspect();358 check(359 `${store}: IAPKit removes account identity from persisted purchases`,360 state.purchases.some((row) => row.appUserId === userId),361 false,362 );363 sqlite.close();364 sqlite = null;365 }366 assert.equal(Object.keys(results).length, 3);367 return results;368 } finally {369 await app.close();370 await providerServer.stop(true);371 sqlite?.close();372 }373}374