# Entitlements

```js
resolveEntitlements(client, now)
```

The single authority for what a client holds at a given instant. Nothing else in
the platform is allowed to decide it.

## Why only one implementation

Two places deciding expiry drift apart. One gets a bug fix, the other does not;
one learns about `SUSPENDED`, the other keeps serving. The failure is silent and
it is a billing failure: a client either gets what they did not pay for, or
loses what they did.

So `resolveEntitlements` is the only code that may decide:

- whether a product is active,
- when it expires,
- whether a feature is gated by its parent product,
- whether a status overrides a subscription.

The gate asserts this mechanically: it scans the module tree for a second
implementation and fails if it finds one. The migration tool is exempt from the
scan but held to a stricter rule — it must never compare the clock against an
expiry, because it only reads fields to compare identity.

## Output

```js
{
  status: 'ACTIVE',
  products: {
    market_engine:     { active: false, expires_at: null, reason: 'EXPIRED' },
    live_intelligence: { active: true,  expires_at: '2026-11-15T00:00:00.000Z', reason: null }
  },
  features: {
    premium_markets: false, pricing: false, canonical_mapping: false,
    live_match: true, momentum: true, advanced_stats: false
  }
}
```

`reason` is `null` exactly when `active` is `true`.

## Reasons

| reason | meaning |
|---|---|
| `NO_SUBSCRIPTION` | the client has never held this product |
| `NOT_STARTED` | a subscription exists but begins later |
| `EXPIRED` | every period for this product has ended |
| `SUSPENDED` | the client is suspended; subscriptions are intact |
| `REVOKED` | the client is revoked; final |

`NOT_STARTED` is separate from `NO_SUBSCRIPTION` deliberately. "You have not
bought this" and "your access begins on Monday" are different answers, and a
client panel that conflates them produces a support ticket.

## Resolution order

```
1. status != ACTIVE            -> everything off, reason = SUSPENDED | REVOKED
2. for each subscription:
     now <  starts_at          -> skip, reason = NOT_STARTED
     now >= expires_at         -> skip, reason = EXPIRED
     otherwise                 -> active, expires_at = max(active periods)
                                  grant the product's base features
3. additional_features:        -> granted only if the parent product is active
```

Step 1 runs first and returns early: a human decision is not negotiated against
dates.

Step 3 runs last for the same reason in reverse: an add-on cannot resurrect a
product. A client who bought `advanced_stats` and let Live Intelligence lapse
holds neither — `advanced_stats` is reported `false`, not "active but
unreachable".

## Features and their parent

```
market_engine      premium_markets · pricing · canonical_mapping   (all base)
live_intelligence  live_match · momentum                           (base)
                   advanced_stats                                  (add-on)
```

Base features come with the subscription. `additional_features` is a per-client
map of add-ons, and the parent check is what makes it safe to set one ahead of
time: enabling `advanced_stats` on a client without Live Intelligence stores the
intent and grants nothing until they subscribe.

## Using it

Server side, through the licensing gate:

```js
const result = licensing.authorize({ now: Date.now(), req, requireOrigin: true });
if (!result.ok) return deny(res);          // 403, empty body
```

For a specific product or feature:

```js
const client = clients.findByToken(token, Date.now());
if (!client)                                        return error('INVALID_API_TOKEN');
if (client.status !== 'ACTIVE')                     return error('INVALID_API_TOKEN');
if (!client.entitlements.products.market_engine.active)
                                                    return error('SUBSCRIPTION_REQUIRED');
if (!client.entitlements.features.advanced_stats)   return error('FEATURE_NOT_ENABLED');
```

A suspended client and an unknown token return the **same** error on purpose.
Distinguishing them would tell an attacker which tokens exist.

## The licensing union

Access is granted if **either** source allows it:

```
env license (single install)   OR   a registered client owning the origin
```

Default-deny stays whole: with neither, denial. A non-numeric `now`, a missing
origin, an empty allowlist and `*` in the allowlist are all denial.

The env license path exists so a single-machine install works without a
registry. Once clients are registered, the service stays up on their strength
alone — otherwise a multi-client install would sit dark with paying clients on
the books.
