# Purchase ownership verification

ForgeGuard can verify a BuiltByBit resource license through the buyer's authenticated OAuth account.
The adapter is implemented and tested against the official response shapes. **Seller credentials,
the real ForgeGuard resource ID and a live storefront test are still required.** An unconfigured
provider is displayed as unavailable; it does not grant a purchase.

## Seller setup

1. Use the seller's [BuiltByBit API credentials](https://builtbybit.com/account/api).
   Set the token's Project name and enable only the OAuth-permitted read scope needed for the
   buyer's license list. Retrieve the OAuth client ID and client secret from the token Details page.
   BuiltByBit documents its access requirements and scope permissions in its
   [OAuth2 guide](https://builtbybit.com/wiki/oauth2/) and [API V2 guide](https://builtbybit.com/wiki/api-v2/).
2. Set the following in Netlify's **Functions environment** and redeploy:

| Variable | Production value |
|---|---|
| `FORGEGUARD_PURCHASE_REQUIRED` | `true` |
| `FORGEGUARD_PUBLIC_ORIGIN` | Exact HTTPS site origin, such as `https://your-site.netlify.app`; no subpath, query or fragment |
| `FORGEGUARD_BUILTBYBIT_CLIENT_ID` | Seller's OAuth client ID |
| `FORGEGUARD_BUILTBYBIT_CLIENT_SECRET` | Seller's OAuth client secret; never put it in public JavaScript |
| `FORGEGUARD_BUILTBYBIT_RESOURCE_ID` | The real ForgeGuard resource's positive integer ID |
| `FORGEGUARD_BUILTBYBIT_SCOPES` | Exact required OAuth scope identifier(s) from the seller's scope list, separated by spaces |

3. Register/allow the exact return address required by the provider:
   `https://your-site.netlify.app/api/purchases/builtbybit/callback`.
   This adapter fixes the callback to the configured origin and never accepts a browser-provided
   redirect URL. Its authorization-code exchange includes the same `redirect_uri`.
4. Deploy the functions and durable Blobs store, then perform the live checks below.

The official OpenAPI specification leaves OAuth scope names unspecified. This project therefore
does not invent a scope value or assume an empty scope grants license access. Use the values exposed
by the seller's actual BuiltByBit account. The API permission must permit OAuth for
`GET /v2/resources/discover/licenses`. BuiltByBit's published API documentation describes an Ultimate
upgrade requirement; confirm the seller's current access before enabling the production gate.

For local work, explicitly set `FORGEGUARD_PURCHASE_REQUIRED=false`. This bypass is only for local
development; the reference environment file recommends `true` for the customer deployment.
An absent variable currently means the optional development mode, so set it deliberately in
production. The OAuth adapter always requires a configured HTTPS public origin, including when
testing a staging deployment. It does not send production OAuth credentials to a loopback callback.

## Buyer flow

1. Sign in to ForgeGuard and open `purchase.html`.
2. Choose BuiltByBit and authorize the seller's application on BuiltByBit itself.
3. Return to ForgeGuard and click **Continue verification**. This committed same-origin page lets
   the browser send ForgeGuard's existing `SameSite=Strict` session cookie without weakening it.
4. The callback verifies the exact account and session that started the request, exchanges the
   one-time code server-side, and reads that OAuth user's resource licenses.
5. The page retrieves the saved result from `/api/purchases`. A success query parameter alone never
   creates or proves an entitlement.

The website never accepts a claimed buyer ID, e-mail match, receipt screenshot or arbitrary license
number as proof. The buyer ID is read from the provider's authenticated license response. The V2
license schema has no standalone license ID, so uniqueness is enforced on **provider + resource ID
+ buyer ID** with an atomic create-only store operation. A purchase cannot be reused by another
ForgeGuard account. There is no automatic account-transfer endpoint; support must investigate a
deliberate transfer and its existing server links. Signing out does not release the purchase binding.
Deleting a ForgeGuard account also leaves this ownership binding in place, so recreating an account
cannot silently transfer its license. Relinking after account deletion currently requires an
operator-reviewed recovery; a self-service recovery flow is not implemented.

## Integration contract

`netlify/functions/purchases.mjs` declares its own Netlify routes; no additional redirect rule is
needed. The local development server must also route `/api/purchases` to this handler.

| Route | Behaviour |
|---|---|
| `GET /api/purchases` | Authenticated status: `{required, providers, entitlement, canCreateServers}` |
| `POST /api/purchases/builtbybit/start` | Empty JSON body, same-origin session and CSRF required; returns `{authorizationUrl, expiresAt}` |
| `GET /api/purchases/builtbybit/callback` | OAuth return; checks single-use state and original session; redirects to the purchase page |

The provider entry is `{id:'builtbybit', name:'BuiltByBit', configured:boolean}`. A public entitlement
contains `{provider, resourceId, memberId, status, verifiedAt, checkedAt, validUntil}`. Status is
`verified`, `inactive`, `reconnect` or `unavailable`. A missing entitlement is `null`. Timestamps are
milliseconds. Access/refresh tokens, client secrets and session/state credentials are never returned.

Successful callback: `/purchase.html?purchase=verified`. Failure:
`/purchase.html?purchase=failed&reason=<fixed-code>`, using only sanitized codes such as
`invalid_state`, `session_ended`, `cancelled`, `purchase_required`, `already_linked`, `reconnect`,
`configuration_unavailable` or `verification_unavailable`.

Backend integration uses:

```js
import { purchaseRequired, requirePurchase } from './purchases.mjs';

await requirePurchase(store, purchaserUserId, {
  env,
  now: Date.now(),
  fetch: globalThis.fetch, // injectable provider transport for tests
});
```

`requirePurchase` returns the verified public entitlement, or `null` when the gate is explicitly
optional. It throws `purchase_required` (403), `configuration_unavailable` (503) or
`verification_unavailable` (503). It also requires the purchaser's ForgeGuard account to remain
active. Apply it to server creation/pairing, and use the linked server's recorded purchaser for
connected control and dispatch checks. Invited staff use that server owner's entitlement rather
than buying a separate personal license. Purchase verification never grants staff membership or
additional Minecraft permissions.

## Rechecks and limits

An active permanent license requires `permanent:true` and `active:true`. A temporary license requires
the current time to lie between its `start_date` and `end_date`; its `active` field is ignored as the
official schema specifies. Only `content_type:'resource'` and the configured resource ID qualify.
The exact schema and routes are in BuiltByBit's
[official OpenAPI specification](https://raw.githubusercontent.com/BuiltByBit/api-openapi-spec/main/spec.yaml).

Verified results are cached for at most **15 minutes**, shortened to the end of a temporary license.
The next protected request after that point rechecks the buyer's license list and refreshes an
expired OAuth access token when possible. No active license invalidates access; an explicit new
verification that finds no license also invalidates an earlier positive cache immediately. An
unavailable provider cannot extend stale verified access. Requests back off after errors and honor
BuiltByBit V2's millisecond `Retry-After` convention. Revoked/denied OAuth authorization requires
reconnection. A short compare-and-set lease serializes refreshes for one account.

There is no refund webhook integration in this version. Access changes when the provider's license
record changes and the next bounded recheck observes it. This verifies an active resource license,
not a payment amount or payment receipt; a provider-granted license may also qualify. It does not
retroactively undo a Minecraft action already accepted by the server. The website gate is not a
Java JAR licensing or obfuscation mechanism.

Provider requests have fixed HTTPS destinations, no redirects, six-second request timeouts within
a twelve-second check budget, a 256 KiB response limit, and at most 25 license pages of 100 entries.
Unexpected or incomplete data fails closed. An account with a larger license list needs a reviewed
increase or another verified provider query; truncation never means success.

Private Blobs records retain the OAuth grant for revalidation, ownership binding and timestamps.
Only the server reads these tokens. State records use a hashed random nonce, expire after ten
minutes, bind to the account/session, and are atomically consumed before token exchange. Old states
are pruned when that account starts another attempt. Keep runtime Blobs, environment secrets,
OAuth codes and logs containing callback URLs out of source packages.

## Validation before production

Automated tests inject official-shaped provider responses and exercise active/expired licenses,
wrong resources, account/session state binding, replay, pagination, unique purchase binding,
refresh/revocation, stale-access refusal, rate limits, malformed data and Strict-cookie continuation.
Run `node --test tests/purchases.test.mjs` plus the server gate integration tests.

The live acceptance check still needs the seller configuration: verify a real licensed buyer,
reject a buyer without this product, reject reuse on another ForgeGuard account, confirm invited
staff can use the licensed server, and confirm a revoked/expired license stops newly protected work.
Do not describe this as a tested live storefront connection until those checks have been completed.
