← Provider replacement walkthrough

IAPKit · scripts/docs/commerce-store-coverage.mjs

Source snapshot · 2026-09-09

SHA-256 e292a2dd490bfa7362d22456743456e508330be61150d98176cbe508682be345
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