@volter/twin-polar 0.1.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,12 +24,12 @@ capability):
24
24
  - `polar.tax.real_calculation` — tax-authority integrations and live jurisdiction rules are
25
25
  outside a deterministic local twin.
26
26
 
27
- ### UI mirror — a tracked gap, not a carve-out
27
+ ### UI mirror — a tracked gap
28
28
 
29
29
  This pack ships **no mirror UI, and that is an unfinished gap rather than a decision.** Polar is a
30
30
  merchant-of-record billing product with a real operator dashboard — the same shape as `stripe`,
31
31
  which does mirror — so under the rule in
32
- [docs/ADDING_A_TWIN.md](../../../docs/ADDING_A_TWIN.md) ("Does this vendor get a mirror?") it
32
+ [../../../docs/contributing/adding-a-twin.md](../../../docs/contributing/adding-a-twin.md) ("Does this vendor get a mirror?") it
33
33
  should have one. The work is tracked in the manifest as `polar.mirror.ui`
34
34
  (operator mirror for customers/subscriptions/orders/events).
35
35
 
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { keepProcessAlive } from '@volter/world-core/lifecycle';
3
+ // world-polar CLI: serve the KERNEL-BACKED Polar API twin, or run conformance. State lives in
4
+ // the @volter/world-core action log (no in-memory side-store). Conformance is dev-only + lazy-imported
5
+ // so the bin runs without @volter/world-tooling (E2).
6
+ import { hasFlag, optionValue } from '@volter/world-core/args';
7
+ import { createPolarTwinServer } from "./polar-server.js";
8
+ const [cmd, ...rest] = process.argv.slice(2);
9
+ const port = Number(optionValue(rest, '--port', String(process.env.PORT ?? '0'))) || undefined;
10
+ const root = optionValue(rest, '--root') || undefined;
11
+ const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
12
+ if (cmd === 'serve' || cmd === undefined) {
13
+ const s = await createPolarTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
14
+ process.stdout.write(`polar twin (merchant-of-record billing API)${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${s.port}\n`);
15
+ await keepProcessAlive();
16
+ }
17
+ else if (cmd === 'conformance') {
18
+ const { checkPolarConformance } = await import("./polar-conformance.js");
19
+ const report = checkPolarConformance();
20
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
21
+ if (!report.ok)
22
+ process.exitCode = 1;
23
+ }
24
+ else {
25
+ process.stdout.write('Usage: world-polar serve|conformance [--port N] [--root DIR] [--read-only]\n');
26
+ }
@@ -0,0 +1,9 @@
1
+ export { handlePolarTwinRequest, polarTwinSnapshot, POLAR_RESOURCE_TYPES } from './polar-twin.js';
2
+ export type { PolarRequest, PolarResponse, PolarResourceType, PolarTwinSnapshot } from './polar-twin.js';
3
+ export { createPolarTwinFetch, createPolarTwinServer, type PolarTwinFetchOptions } from './polar-server.js';
4
+ export { mapCustomer, mapOrder, mapProduct, mapSubscription, pullPolarCustomers, pullPolarOrders, pullPolarProducts, pullPolarSubscriptions, pushPolarAction, syncPolarFromReal, polarClientOver, syncPolarFromRemote, performPolarAction, } from './polar-connector.js';
5
+ export type { PolarCustomer, PolarLikeClient, PolarOrder, PolarProduct, PolarSubscription, PolarBudgetedOptions } from './polar-connector.js';
6
+ export { POLAR_BUDGETED_METHODS, POLAR_BUDGET_CEILING, POLAR_BUDGET_MAX_RETRY_AFTER_S, POLAR_BUDGET_WINDOW_MS, POLAR_CALL_WEIGHTS, POLAR_RATE_BUDGET, PolarBudget, PolarBudgetError, polarBudgetPath, polarCallWeight, polarClientBudget, guardPolarClient, } from './polar-budget.js';
7
+ export type { PolarBudgetErrorKind, PolarBudgetOptions, PolarBudgetReservation, PolarBudgetSnapshot } from './polar-budget.js';
8
+ import { type TwinPack } from '@volter/world-core';
9
+ export declare const pack: TwinPack;
@@ -0,0 +1,55 @@
1
+ // @volter/twin-polar — the Polar (polar.sh) merchant-of-record billing API twin, built on the
2
+ // shared @volter/world-core kernel. REST transport over api.polar.sh shapes; state lives entirely in
3
+ // the kernel action log (no side-store). Customers, products, subscriptions, orders, checkouts,
4
+ // benefits, discounts, meters, usage events, customer sessions, and webhook endpoints.
5
+ // (Conformance/capability tooling lives in @volter/world-tooling, a dev dependency — NOT shipped.)
6
+ export { handlePolarTwinRequest, polarTwinSnapshot, POLAR_RESOURCE_TYPES } from "./polar-twin.js";
7
+ export { createPolarTwinFetch, createPolarTwinServer } from "./polar-server.js";
8
+ export { mapCustomer, mapOrder, mapProduct, mapSubscription, pullPolarCustomers, pullPolarOrders, pullPolarProducts, pullPolarSubscriptions, pushPolarAction, syncPolarFromReal, polarClientOver, syncPolarFromRemote, performPolarAction, } from "./polar-connector.js";
9
+ // The client-side rate budget — the fail-closed backstop every live call goes through. The
10
+ // MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives here is this vendor's
11
+ // DECLARATION (window/ceiling/per-method weights) plus `guardPolarClient`, the choke point the
12
+ // connector entrypoints apply unconditionally. Exported so an operator can inspect spend
13
+ // (`snapshot`) and a caller can catch `PolarBudgetError` by type; there is deliberately no export
14
+ // that disables the guard.
15
+ export { POLAR_BUDGETED_METHODS, POLAR_BUDGET_CEILING, POLAR_BUDGET_MAX_RETRY_AFTER_S, POLAR_BUDGET_WINDOW_MS, POLAR_CALL_WEIGHTS, POLAR_RATE_BUDGET, PolarBudget, PolarBudgetError, polarBudgetPath, polarCallWeight, polarClientBudget, guardPolarClient, } from "./polar-budget.js";
16
+ // Registry descriptor: the pack self-describes so tooling can discover it.
17
+ import { registerPack } from '@volter/world-core';
18
+ import { POLAR_RATE_BUDGET as RATE_BUDGET } from "./polar-budget.js";
19
+ import { performPolarAction as perform, syncPolarFromRemote as refresh } from "./polar-connector.js";
20
+ export const pack = {
21
+ // PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of the real
22
+ // state system: perform one entry against Polar, refresh the root from it. Moved 2026-09-08.
23
+ protocol: '2',
24
+ refresh: { every: '5m', webhook: true, onDemand: { atMost: '30s' } },
25
+ stateSystem: { perform, refresh },
26
+ roundTrip: { method: 'POST', path: '/v1/products', body: { name: 'round trip', recurring_interval: 'month' }, headers: { authorization: 'Bearer polar_oat_round_trip' } },
27
+ // rule 5: a checkout names its product; when the head adopts Polar's id for the product, the kernel resolves it first
28
+ referenceTrip: { method: 'POST', path: '/v1/checkouts', body: { products: ['{{id}}'] }, headers: { authorization: 'Bearer polar_oat_round_trip' } },
29
+ references: [{ type: 'checkout', to: 'product', key: (f) => (typeof f.product_id === 'string' ? `product:${f.product_id}` : undefined), adopt: (_f, vendorId) => ({ product_id: vendorId.replace(/^product:/, '') }) }],
30
+ parityOrigin: 'http://twin',
31
+ shapeParity: 'held',
32
+ // The SAME object polar-budget.ts declares at module load — one source of truth, so registering
33
+ // the pack and importing the connector can never arm two different ceilings.
34
+ rateBudget: RATE_BUDGET,
35
+ vendor: 'polar',
36
+ transport: 'rest',
37
+ archetype: 'crud',
38
+ bin: 'world-polar',
39
+ resources: ['customer', 'product', 'subscription', 'order', 'checkout', 'benefit', 'discount', 'meter', 'event', 'customer_session', 'webhook_endpoint'],
40
+ specSource: 'polar-conformance.ts (endpoint/resource inventory from the Polar API docs at docs.polar.sh)',
41
+ description: 'Polar merchant-of-record billing twin — customers, products, subscriptions, orders, checkouts (with confirm → subscription/order), benefits, discounts, meters, usage events, customer sessions, and webhook endpoints. Kernel-backed.',
42
+ // Adoption + interception, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
43
+ // 2026-08-31). Both the production API host and Polar's own sandbox host are
44
+ // claimed — an integration picks one by configuration, and claiming only production would let
45
+ // sandbox traffic escape the world.
46
+ adoption: {
47
+ // Polar's official Python SDK (polarsource/polar-python).
48
+ pypi: ['polar-sdk'],
49
+ sdks: ['@polar-sh/sdk'], scopes: ['@polar-sh/'], envStems: ['POLAR'],
50
+ },
51
+ hosts: [{ host: 'api.polar.sh' }, { host: 'sandbox-api.polar.sh' }],
52
+ browserRouting: { apiPathPrefix: '/v1', loaderHost: 'https://api.polar.sh' },
53
+ };
54
+ // registered at import: the kernel learns the pack's state system and its references (protocol 2)
55
+ registerPack(pack);
@@ -0,0 +1,83 @@
1
+ import { RateBudget, type RateBudgetDeclaration, type RateBudgetOptions, type RateBudgetReservation, type RateBudgetSnapshot } from '@volter/world-core';
2
+ import type { PolarLikeClient } from './polar-connector.js';
3
+ /** Rolling window, in ms. Spend older than this is pruned. */
4
+ export declare const POLAR_BUDGET_WINDOW_MS = 60000;
5
+ /** Weighted units allowed inside one window. See the header for where this number comes from. */
6
+ export declare const POLAR_BUDGET_CEILING = 60;
7
+ /** Seconds. A `Retry-After` above this means the credential is throttled hard — fail loudly. */
8
+ export declare const POLAR_BUDGET_MAX_RETRY_AFTER_S = 300;
9
+ /** Per-call cost, keyed by the client method the guard is about to invoke. See the header. */
10
+ export declare const POLAR_CALL_WEIGHTS: {
11
+ /** Every modeled call. Polar meters one per-minute budget across the API. */
12
+ readonly other: 1;
13
+ };
14
+ /**
15
+ * The client methods this pack PRICES BY NAME, as dotted paths into the injected client.
16
+ *
17
+ * Two kinds of entry: (a) every method this connector actually calls, and (b) an endpoint the
18
+ * VENDOR documents in a distinct tier which a consumer can reach through the guarded client even
19
+ * though this connector never calls it (see the weights section of the header). Used to build the
20
+ * guarded surface — and, in the pack's own suite, the counting fake.
21
+ *
22
+ * NOT a closed list, and not a claim about the injected client's shape. A path the real client does
23
+ * not have is SKIPPED (this connector's members are optional; inventing one would turn "observe
24
+ * nothing" into "call something that isn't there"), and a method absent from this list is still
25
+ * PRICED at `defaultWeight` when a caller reaches for it — an unmodeled endpoint must never be
26
+ * free, and dropping one would be worse than free because it would be invisible.
27
+ */
28
+ export declare const POLAR_BUDGETED_METHODS: readonly ["customers.list", "customers.create", "products.list", "products.create", "subscriptions.list", "orders.list"];
29
+ /** THE PACK'S DECLARATION — pure data, the only Polar-specific thing in the whole budget. */
30
+ export declare const POLAR_RATE_BUDGET: RateBudgetDeclaration;
31
+ /** Price one Polar call by its client method key (a dotted path, e.g. `customers.list`). */
32
+ export declare function polarCallWeight(method: string): number;
33
+ /** Where Polar's ledger lives. Token-keyed and cwd-independent by default (the vendor limits per
34
+ * credential, so a cwd-scoped ledger would hand the same credential a fresh allowance in every
35
+ * checkout, worktree and CI matrix leg); pass `root` to opt into world-scoped accounting. */
36
+ export declare function polarBudgetPath(opts?: {
37
+ root?: string;
38
+ token?: string;
39
+ } | string): string;
40
+ /** Construction options for Polar's budget. The vendor is fixed; everything else may only TIGHTEN. */
41
+ export type PolarBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
42
+ /**
43
+ * Polar's budget — the shared kernel guard bound to this vendor's declaration. A real subclass,
44
+ * not an alias, so `budget instanceof PolarBudget` means "a budget that accounts against this
45
+ * vendor's ledger under this vendor's ceiling": another vendor's `RateBudget` (with its own,
46
+ * possibly larger, ceiling) is NOT assignable where one of these is required.
47
+ */
48
+ export declare class PolarBudget extends RateBudget {
49
+ constructor(opts?: PolarBudgetOptions);
50
+ }
51
+ export type { RateBudgetErrorKind as PolarBudgetErrorKind } from '@volter/world-core';
52
+ export { RateBudgetError as PolarBudgetError } from '@volter/world-core';
53
+ export type PolarBudgetReservation = RateBudgetReservation;
54
+ export type PolarBudgetSnapshot = RateBudgetSnapshot;
55
+ /** What every budgeted connector entrypoint accepts. There is deliberately no option that turns the
56
+ * guard OFF — only ones that say WHICH ledger and clock to account against. */
57
+ export type PolarBudgetedOptions = {
58
+ /** An existing budget to share across calls. Omit and one is constructed. Cannot be null. */
59
+ budget?: PolarBudget;
60
+ /** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
61
+ budgetOptions?: PolarBudgetOptions;
62
+ };
63
+ /** Pull the budget wiring out of a caller's opts bag, so an entrypoint can forward it verbatim. */
64
+ export declare function polarBudgetOf(opts: PolarBudgetedOptions): PolarBudgetedOptions;
65
+ /** Is this client already behind a budget? Returns the budget it is behind, if so. */
66
+ export declare function polarClientBudget(client: unknown): RateBudget | undefined;
67
+ /**
68
+ * Wrap an INJECTED Polar client so EVERY call it makes is charged against the shared budget
69
+ * BEFORE the request goes out. This pack's connector never constructs the transport itself (the
70
+ * consumer injects a client that satisfies `PolarLikeClient` structurally), so the guard is a
71
+ * DECORATOR rather than a factory — which is exactly why every connector entrypoint applies it
72
+ * UNCONDITIONALLY instead of trusting the caller to have done it.
73
+ *
74
+ * IDEMPOTENT: wrapping an already-guarded client returns it unchanged, so a caller who forgot is
75
+ * protected and a caller who wrapped deliberately is not double-charged.
76
+ *
77
+ * A method that THROWS is still inspected: an SDK typically RAISES on a 429 rather than returning
78
+ * it, and that error's back-off is exactly the signal that must become a persisted cooldown. Losing
79
+ * it would leave the ledger cheerfully spending into a throttled credential. The original error is
80
+ * always re-raised afterwards — the budget never swallows a vendor failure — EXCEPT when the
81
+ * back-off is beyond the cap, where the budget's own louder "stop calling" error takes precedence.
82
+ */
83
+ export declare function guardPolarClient(client: PolarLikeClient, opts?: PolarBudgetedOptions): PolarLikeClient;
@@ -0,0 +1,413 @@
1
+ // Polar's CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus `guardPolarClient`,
2
+ // the choke point every live Polar call goes through. The MECHANISM — the durable token-keyed
3
+ // ledger, the rolling window, reserve-under-lock, the `Retry-After`/429 cooldown, fail-CLOSED on a
4
+ // corrupt ledger — lives ONCE in the vendor-agnostic kernel (`@volter/world-core` → `rateBudget.ts`).
5
+ // Read that module's header for the full rationale AND for the honest list of what the guard does
6
+ // NOT guarantee (an injected clock or ledger path still defeats it — it guards carelessness, not
7
+ // malice). This module is modeled on notion-budget.ts, the reference injected-client decorator.
8
+ //
9
+ // ── WHY POLAR NEEDS ONE ─────────────────────────────────────────────────────────────
10
+ // Polar publishes its limits cleanly and is the only vendor in this batch that documents a
11
+ // `Retry-After` header (polar.sh/docs/api-reference/introduction): "500 requests per minute" in
12
+ // production and "100 requests per minute" in sandbox, in both cases per organization/customer or
13
+ // OAuth2 client, plus a separate "3 requests per second" budget for the unauthenticated license-key
14
+ // validation/activation/deactivation endpoints. Its 429 "includes a Retry-After header indicating how
15
+ // long you should wait" — which is exactly the signal the kernel's cooldown is built to consume, so
16
+ // on this vendor the guard's second line of defence is genuinely armed rather than inferred.
17
+ //
18
+ // ── HOW THE CEILING WAS CHOSEN ──────────────────────────────────────────────────────────────
19
+ // THIS PACK IS DELIBERATELY MORE PERMISSIVE THAN THE KERNEL FALLBACK, and the documented limits are
20
+ // why. The ceiling is derived from the SANDBOX figure, not production: 100 requests/minute is the
21
+ // tighter of the two published numbers, and a twin consumer is at least as likely to be pointed at
22
+ // sandbox as at production, so budgeting against 500/min would be the wrong default. 60 weighted
23
+ // units / 60s at weight 1 admits 60 calls/minute — 60% of the documented sandbox limit and 12% of
24
+ // production, against the fallback's 30/min. A pull of all four domains is 4 calls, so this leaves a
25
+ // pull-heavy caller real headroom while still stopping a loop well inside the vendor's own limit.
26
+ //
27
+ // ── WHAT THIS DOES NOT DO: PACE ─────────────────────────────────────────────────────────────
28
+ // It bounds the 60s AVERAGE; it does NOT bound the instantaneous rate. The window has no
29
+ // spacing, so a tight `await` loop can legitimately fire the whole allowance in a fraction of a
30
+ // second. In that shape the vendor's own 429 can arrive BEFORE this ceiling does, and the backstop
31
+ // is then the COOLDOWN: the guard reads the back-off off the thrown error (or the response's
32
+ // exhaustion headers) and refuses every later call without touching the vendor. So the honest claim
33
+ // is "bounds the 60s average, and converts the vendor's first 429 into a hard stop" — never
34
+ // "refuses before the vendor ever 429s". Pacing is the CALLER's job; this module REFUSES, it never
35
+ // sleeps (see the kernel header: it is deliberately not a scheduler). A tighter window would not
36
+ // close the gap, it would only turn every legitimate multi-page pull into a cascade of refusals.
37
+ //
38
+ // ── HOW THE WEIGHTS WERE CHOSEN (and what is a judgement call) ──────────────────────────────
39
+ // FLAT: every call costs 1. Polar meters ONE per-minute budget across the authenticated API — its
40
+ // docs give a single figure per environment rather than a per-endpoint table — so pricing one list
41
+ // above another would be invention. The one genuinely separate budget Polar publishes (3 requests/
42
+ // second for the unauthenticated license-key endpoints) is not part of this connector's surface and
43
+ // is per-second rather than per-minute, so it is recorded here rather than modeled as a rule. An
44
+ // unmodeled method reached through the guarded client is charged the same 1, never free.
45
+ import { declareRateBudget, rateBudgetPath, rateBudgetWeight, RateBudget, RateBudgetError, } from '@volter/world-core';
46
+ const VENDOR = 'polar';
47
+ /** Rolling window, in ms. Spend older than this is pruned. */
48
+ export const POLAR_BUDGET_WINDOW_MS = 60_000;
49
+ /** Weighted units allowed inside one window. See the header for where this number comes from. */
50
+ export const POLAR_BUDGET_CEILING = 60;
51
+ /** Seconds. A `Retry-After` above this means the credential is throttled hard — fail loudly. */
52
+ export const POLAR_BUDGET_MAX_RETRY_AFTER_S = 300;
53
+ /** Per-call cost, keyed by the client method the guard is about to invoke. See the header. */
54
+ export const POLAR_CALL_WEIGHTS = {
55
+ /** Every modeled call. Polar meters one per-minute budget across the API. */
56
+ other: 1,
57
+ };
58
+ /**
59
+ * The client methods this pack PRICES BY NAME, as dotted paths into the injected client.
60
+ *
61
+ * Two kinds of entry: (a) every method this connector actually calls, and (b) an endpoint the
62
+ * VENDOR documents in a distinct tier which a consumer can reach through the guarded client even
63
+ * though this connector never calls it (see the weights section of the header). Used to build the
64
+ * guarded surface — and, in the pack's own suite, the counting fake.
65
+ *
66
+ * NOT a closed list, and not a claim about the injected client's shape. A path the real client does
67
+ * not have is SKIPPED (this connector's members are optional; inventing one would turn "observe
68
+ * nothing" into "call something that isn't there"), and a method absent from this list is still
69
+ * PRICED at `defaultWeight` when a caller reaches for it — an unmodeled endpoint must never be
70
+ * free, and dropping one would be worse than free because it would be invisible.
71
+ */
72
+ export const POLAR_BUDGETED_METHODS = [
73
+ 'customers.list',
74
+ 'customers.create',
75
+ 'products.list',
76
+ 'products.create',
77
+ 'subscriptions.list',
78
+ 'orders.list',
79
+ ];
80
+ /** THE PACK'S DECLARATION — pure data, the only Polar-specific thing in the whole budget. */
81
+ export const POLAR_RATE_BUDGET = {
82
+ windowMs: POLAR_BUDGET_WINDOW_MS,
83
+ ceiling: POLAR_BUDGET_CEILING,
84
+ defaultWeight: POLAR_CALL_WEIGHTS.other,
85
+ maxRetryAfterSeconds: POLAR_BUDGET_MAX_RETRY_AFTER_S,
86
+ // Ordered: the kernel prices FIRST-MATCH-WINS, so the tightest tier is listed first.
87
+ rules: [],
88
+ reason: "Polar publishes \"500 requests per minute\" in production and \"100 requests per minute\" in sandbox, per " +
89
+ "organization/customer or OAuth2 client, plus a separate 3 requests/second budget for the unauthenticated " +
90
+ "license-key endpoints; its 429 explicitly \"includes a Retry-After header\". THIS DECLARATION IS MORE " +
91
+ "PERMISSIVE THAN the kernel's DEFAULT_RATE_BUDGET (30 calls/min) and the documented limits are why: 60 " +
92
+ "units / 60s at weight 1 admits 60 calls/min, derived from the tighter SANDBOX figure (60% of 100/min, 12% " +
93
+ "of production's 500/min) because a twin consumer is at least as likely to be pointed at sandbox. Weights " +
94
+ "are flat because Polar meters one per-minute budget across the authenticated API rather than publishing a " +
95
+ "per-endpoint table; the separate 3/s license-key budget is not in this connector's surface. Polar is the " +
96
+ "one vendor in this batch that documents Retry-After, so the cooldown path is armed by the vendor itself.",
97
+ };
98
+ // Declared at module load, so merely importing this module (which `polar-connector.ts` does) is
99
+ // enough to arm the real ceiling. A budget constructed BEFORE this runs falls back to the kernel's
100
+ // DEFAULT_RATE_BUDGET — which is tighter in call COUNT but prices every call at 2, so for a vendor
101
+ // with an expensive endpoint the fallback is CHEAPER there, not safer. `RateBudget` reads its policy
102
+ // LIVE precisely so this declaration takes effect the moment it lands, and constructing through the
103
+ // subclass below (whose module IS this one) makes the ordering a non-issue in practice.
104
+ declareRateBudget(VENDOR, POLAR_RATE_BUDGET);
105
+ /** Price one Polar call by its client method key (a dotted path, e.g. `customers.list`). */
106
+ export function polarCallWeight(method) {
107
+ return rateBudgetWeight(VENDOR, method);
108
+ }
109
+ /** Where Polar's ledger lives. Token-keyed and cwd-independent by default (the vendor limits per
110
+ * credential, so a cwd-scoped ledger would hand the same credential a fresh allowance in every
111
+ * checkout, worktree and CI matrix leg); pass `root` to opt into world-scoped accounting. */
112
+ export function polarBudgetPath(opts = {}) {
113
+ const o = typeof opts === 'string' ? { root: opts } : opts;
114
+ // VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
115
+ // excess-property check only catches object literals) must not redirect this pack's ledger to
116
+ // another vendor's file.
117
+ return rateBudgetPath({ ...o, vendor: VENDOR });
118
+ }
119
+ /**
120
+ * Polar's budget — the shared kernel guard bound to this vendor's declaration. A real subclass,
121
+ * not an alias, so `budget instanceof PolarBudget` means "a budget that accounts against this
122
+ * vendor's ledger under this vendor's ceiling": another vendor's `RateBudget` (with its own,
123
+ * possibly larger, ceiling) is NOT assignable where one of these is required.
124
+ */
125
+ export class PolarBudget extends RateBudget {
126
+ constructor(opts = {}) {
127
+ super({ ...opts, vendor: VENDOR });
128
+ }
129
+ }
130
+ export { RateBudgetError as PolarBudgetError } from '@volter/world-core';
131
+ /** Pull the budget wiring out of a caller's opts bag, so an entrypoint can forward it verbatim. */
132
+ export function polarBudgetOf(opts) {
133
+ return {
134
+ ...(opts.budget !== undefined ? { budget: opts.budget } : {}),
135
+ ...(opts.budgetOptions !== undefined ? { budgetOptions: opts.budgetOptions } : {}),
136
+ };
137
+ }
138
+ // ── the choke point ─────────────────────────────────────────────────────────────────────────
139
+ /**
140
+ * Marks a client this module has already wrapped, so guarding twice cannot charge twice.
141
+ *
142
+ * A MODULE-PRIVATE `Symbol()`, deliberately not `Symbol.for()`: a global-registry symbol is
143
+ * reachable BY NAME, so any caller could stamp `client[Symbol.for(…)] = anything` on a RAW client and
144
+ * the guard would hand it straight back UNGUARDED — a one-line bypass of the whole budget. With a
145
+ * private symbol the only way to be branded is to have been wrapped by this function. The cost is
146
+ * that two copies of this module in one dependency tree would each wrap (double-charging a call);
147
+ * that is the SAFE direction, and over-charging is the tradeoff this module takes everywhere else.
148
+ */
149
+ const GUARDED = Symbol('@volter/twin-polar.budget.guarded');
150
+ /** Is this client already behind a budget? Returns the budget it is behind, if so. */
151
+ export function polarClientBudget(client) {
152
+ const mark = client?.[GUARDED];
153
+ // Belt and braces: only a REAL budget counts as "already guarded". A non-RateBudget value here
154
+ // could only come from a forged brand, and the answer to a forgery is to wrap anyway.
155
+ return mark instanceof RateBudget ? mark : undefined;
156
+ }
157
+ /**
158
+ * Wrap an INJECTED Polar client so EVERY call it makes is charged against the shared budget
159
+ * BEFORE the request goes out. This pack's connector never constructs the transport itself (the
160
+ * consumer injects a client that satisfies `PolarLikeClient` structurally), so the guard is a
161
+ * DECORATOR rather than a factory — which is exactly why every connector entrypoint applies it
162
+ * UNCONDITIONALLY instead of trusting the caller to have done it.
163
+ *
164
+ * IDEMPOTENT: wrapping an already-guarded client returns it unchanged, so a caller who forgot is
165
+ * protected and a caller who wrapped deliberately is not double-charged.
166
+ *
167
+ * A method that THROWS is still inspected: an SDK typically RAISES on a 429 rather than returning
168
+ * it, and that error's back-off is exactly the signal that must become a persisted cooldown. Losing
169
+ * it would leave the ledger cheerfully spending into a throttled credential. The original error is
170
+ * always re-raised afterwards — the budget never swallows a vendor failure — EXCEPT when the
171
+ * back-off is beyond the cap, where the budget's own louder "stop calling" error takes precedence.
172
+ */
173
+ export function guardPolarClient(client, opts = {}) {
174
+ // There is no value a caller can pass to end up with an UNGUARDED client. `null`/`undefined` (or
175
+ // omitting it) build the default budget; anything that is not a REAL `PolarBudget` is refused
176
+ // loudly rather than trusted — a duck-typed stand-in with a no-op `checkBudget` would otherwise be
177
+ // the one clean way around the guard. Validated BEFORE the already-guarded early return, so
178
+ // `guardPolarClient(alreadyGuarded, { budget: impostor })` is refused too rather than silently
179
+ // ignoring the impostor.
180
+ if (opts.budget !== undefined && opts.budget !== null && !(opts.budget instanceof PolarBudget)) {
181
+ throw new Error('guardPolarClient: `budget` must be a PolarBudget — refusing to guard a Polar client with an unverified rate guard');
182
+ }
183
+ if (polarClientBudget(client))
184
+ return client;
185
+ const budget = opts.budget instanceof PolarBudget
186
+ ? opts.budget
187
+ : new PolarBudget({
188
+ // The default ledger is keyed by a hash of the credential — the vendor limits per credential,
189
+ // so a cwd-scoped ledger would hand it a fresh allowance per worktree/CI leg.
190
+ //
191
+ // HONESTLY: this pack does NOT hold the credential — the consumer's injected client does — so
192
+ // `POLAR_ACCESS_TOKEN` is a BEST-EFFORT stand-in for it, not the real thing. If that env var names a
193
+ // different account than the injected client, spend is booked against the wrong ledger; if it
194
+ // is unset, every unattributed Polar credential on the machine shares one (over-tight,
195
+ // which is the safe direction). A caller who knows the credential should say so:
196
+ // `budgetOptions: { token }` overrides this, and does so deliberately last in the spread.
197
+ ...(process.env.POLAR_ACCESS_TOKEN !== undefined ? { token: process.env.POLAR_ACCESS_TOKEN } : {}),
198
+ ...(opts.budgetOptions ?? {}),
199
+ });
200
+ /** Settle a reservation from whatever the call produced. May THROW (a back-off past the cap). */
201
+ const settle = (weight, reservation, v) => {
202
+ const status = responseStatus(v);
203
+ const headers = responseHeaders(v);
204
+ if (status !== undefined || headers !== undefined)
205
+ budget.recordCall(weight, headers, { status, reservation });
206
+ else
207
+ budget.recordCall(weight, undefined, { reservation });
208
+ };
209
+ /**
210
+ * Settle once the call RESOLVED: the vendor has answered. recordCall arms any cooldown before
211
+ * it throws (a back-off beyond the cap), so that refusal is swallowed and the answer kept — a
212
+ * write the vendor accepted is never reported failed and performed again on retry. A resolved
213
+ * answer that carries a non-2xx status still lets the louder refusal win.
214
+ */
215
+ const settleAnswered = (weight, reservation, v) => {
216
+ try {
217
+ settle(weight, reservation, v);
218
+ }
219
+ catch (e) {
220
+ const status = responseStatus(v);
221
+ if (!(e instanceof RateBudgetError) || (status !== undefined && (status < 200 || status >= 300)))
222
+ throw e;
223
+ }
224
+ };
225
+ /**
226
+ * Charge, call, settle — for a MODELED method, whose interface declares it `async`. Refusing
227
+ * REJECTS rather than throwing synchronously, so `client.x().catch(…)` behaves exactly as it does
228
+ * on an unguarded client. `checkBudget` RESERVES under lock, so nothing after its line runs when
229
+ * the budget refuses: the request is never made.
230
+ */
231
+ const chargeAsync = (method, invoke) => async (...args) => {
232
+ const weight = budget.weightFor(method);
233
+ const reservation = budget.checkBudget(weight);
234
+ try {
235
+ const res = await invoke(...args);
236
+ settleAnswered(weight, reservation, res);
237
+ return res;
238
+ }
239
+ catch (e) {
240
+ settle(weight, reservation, e); // may throw its own louder refusal, which wins
241
+ throw e;
242
+ }
243
+ };
244
+ /**
245
+ * Charge, call, settle — for an UNMODELED member reached through the passthrough Proxy, where the
246
+ * shape is unknown.
247
+ *
248
+ * Deliberately NOT `async`. Some vendor SDKs (twilio-shaped ones) build requests through
249
+ * SYNCHRONOUS chained accessors — `client.a.b('sid').c.create()` — where the intermediate calls
250
+ * return a resource context, not a promise. An `async` wrapper would turn every one of those into
251
+ * a `Promise` and `.c` would come back `undefined`: the guard would BREAK the client instead of
252
+ * guarding it. So a non-thenable return is treated as an accessor — still charged (we cannot know
253
+ * before calling, and over-charging is the safe direction) and wrapped, so the eventual async leaf
254
+ * is charged too rather than escaping the budget. The cost of the sync shape is that a REFUSAL on
255
+ * this path throws synchronously instead of rejecting; that is the honest signal for an accessor,
256
+ * and the modeled surface above (every method this connector actually calls) does not have it.
257
+ */
258
+ const charge = (method, invoke) => (...args) => {
259
+ const weight = budget.weightFor(method);
260
+ const reservation = budget.checkBudget(weight);
261
+ let out;
262
+ try {
263
+ out = invoke(...args);
264
+ }
265
+ catch (e) {
266
+ settle(weight, reservation, e); // may throw its own louder refusal, which wins
267
+ throw e;
268
+ }
269
+ if (!isThenable(out)) {
270
+ settle(weight, reservation, undefined);
271
+ return out !== null && (typeof out === 'object' || typeof out === 'function')
272
+ ? proxyThrough({}, out, method, charge)
273
+ : out;
274
+ }
275
+ return out.then((res) => { settleAnswered(weight, reservation, res); return res; }, (e) => { settle(weight, reservation, e); throw e; });
276
+ };
277
+ // Build the charged surface from POLAR_BUDGETED_METHODS (see its docstring for why an absent path is
278
+ // SKIPPED rather than stubbed).
279
+ const guarded = {};
280
+ for (const path of POLAR_BUDGETED_METHODS) {
281
+ const segs = path.split('.');
282
+ const leaf = segs[segs.length - 1];
283
+ let owner = client;
284
+ for (const seg of segs.slice(0, -1))
285
+ owner = owner?.[seg];
286
+ if (typeof owner?.[leaf] !== 'function')
287
+ continue;
288
+ const realOwner = owner;
289
+ let node = guarded;
290
+ for (const seg of segs.slice(0, -1))
291
+ node = (node[seg] ??= {});
292
+ // The method is resolved at CALL time, not here, so a client whose method is swapped later is
293
+ // still charged for whatever it actually runs.
294
+ node[leaf] = chargeAsync(path, (...args) => realOwner[leaf].apply(realOwner, args));
295
+ }
296
+ Object.defineProperty(guarded, GUARDED, { value: budget, enumerable: false });
297
+ // The surface above is what this connector calls. A real client has MORE — and a consumer who
298
+ // needs any of it must not be forced to keep the RAW client alongside, because every call through
299
+ // that would be unbudgeted. So the guarded object is a Proxy: known members come from the map
300
+ // above, anything else is taken from the real client and PRICED at `defaultWeight`.
301
+ return proxyThrough(guarded, client, '', charge);
302
+ }
303
+ /**
304
+ * Members JavaScript itself asks for. Wrapping any of these turns the object into something that
305
+ * looks thenable / mis-reports its own identity, which breaks `await`, `instanceof` and logging —
306
+ * so they always come from the target untouched, never priced.
307
+ */
308
+ const NEVER_WRAP = new Set(['then', 'catch', 'finally', 'constructor', 'prototype', 'toJSON', 'toString', 'valueOf', 'inspect']);
309
+ /** Does this look like a promise? (Only a thenable gets the settle-on-resolution treatment.) */
310
+ function isThenable(v) {
311
+ return v !== null && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function';
312
+ }
313
+ /**
314
+ * Symbols that name an ITERATION PROTOCOL. Reaching for one of these on a vendor object is how a
315
+ * paginator is driven (`for await (const page of client.things.list())`), i.e. it is the doorway to
316
+ * an unbounded sequence of REQUESTS — exactly what this budget exists to bound — so they are charged
317
+ * and their result is kept behind the proxy. Every other symbol (`Symbol.toStringTag`,
318
+ * `nodejs.util.inspect.custom`, …) is metadata rather than a request and passes through untouched.
319
+ */
320
+ const ITERATOR_SYMBOLS = new Set([Symbol.asyncIterator, Symbol.iterator]);
321
+ /**
322
+ * Serve `known` where it has the member; otherwise price a passthrough to `real`. Applied
323
+ * recursively, so a namespace member this pack never modeled is charged at `defaultWeight` rather
324
+ * than coming back `undefined`.
325
+ *
326
+ * CALLABLE NAMESPACES (§9 finding): a member can be BOTH a function and a namespace — twilio's
327
+ * `client.messages` is called as `client.messages(sid)` to get one message's context AND read as
328
+ * `client.messages.list()`. An earlier version returned the modeled node as a plain object whenever
329
+ * this pack modeled any child of it, which silently DROPPED the call signature and every unmodeled
330
+ * sibling — the module's own docstring calls a dropped member "worse than free, because it is
331
+ * invisible", and that is what it was doing. So when `real` is callable the proxy target is a
332
+ * function and an `apply` trap charges the call, while `known` stays the source of modeled members.
333
+ */
334
+ function proxyThrough(known, real, prefix, charge, owner) {
335
+ const callable = typeof real === 'function';
336
+ // The target must itself be callable for the `apply` trap to exist at all. `known` stays the
337
+ // source of modeled members, read explicitly below rather than through the target.
338
+ const target = callable ? function proxied() { } : known;
339
+ return new Proxy(target, {
340
+ apply(_t, _thisArg, args) {
341
+ // Calling the namespace is itself a request-builder hop: charge it, and keep the result
342
+ // behind the proxy (charge() re-proxies a non-thenable) so the eventual leaf is charged too.
343
+ // `owner` is the object the function was read from — dropping it would silently break every
344
+ // method that relies on `this`.
345
+ return charge(prefix || 'call', (...a) => real.apply(owner, a))(...args);
346
+ },
347
+ get(_t, prop, receiver) {
348
+ if (typeof prop === 'symbol') {
349
+ const ownSym = Reflect.get(known, prop, receiver);
350
+ if (ownSym !== undefined)
351
+ return ownSym; // the GUARDED brand, and anything we model
352
+ const fromSym = real?.[prop];
353
+ if (ITERATOR_SYMBOLS.has(prop) && typeof fromSym === 'function') {
354
+ return charge(`${prefix}[${prop.description ?? 'iterator'}]`, (...args) => fromSym.apply(real, args));
355
+ }
356
+ return fromSym;
357
+ }
358
+ const name = String(prop);
359
+ if (NEVER_WRAP.has(name))
360
+ return Reflect.get(known, prop, receiver);
361
+ const key = prefix ? `${prefix}.${name}` : name;
362
+ const own = Reflect.get(known, prop, receiver);
363
+ const from = real?.[name];
364
+ // A member we model: the charged wrapper (a function) or a namespace we must keep descending
365
+ // into, so an unmodeled sibling is still priced rather than dropped.
366
+ if (typeof own === 'function')
367
+ return own;
368
+ if (own && typeof own === 'object') {
369
+ // `from` may be an object OR a CALLABLE namespace — both keep descending, which is what
370
+ // preserves `client.messages(sid)` alongside the modeled `client.messages.list()`.
371
+ return from && (typeof from === 'object' || typeof from === 'function')
372
+ ? proxyThrough(own, from, key, charge, real)
373
+ : own;
374
+ }
375
+ if (own !== undefined)
376
+ return own;
377
+ // A member only the real client has. Functions go through proxyThrough too, so one that also
378
+ // carries members (a callable namespace) keeps both its call signature and its siblings.
379
+ if (typeof from === 'function')
380
+ return proxyThrough({}, from, key, charge, real);
381
+ if (from && typeof from === 'object')
382
+ return proxyThrough({}, from, key, charge, real);
383
+ return from;
384
+ },
385
+ has(_t, prop) {
386
+ return Reflect.has(known, prop) || (real !== null && Reflect.has(real, prop));
387
+ },
388
+ });
389
+ }
390
+ /** The HTTP status a value carries, if it looks like one (an SDK error, or a raw response). */
391
+ function responseStatus(v) {
392
+ const s = v?.status;
393
+ return typeof s === 'number' && Number.isFinite(s) ? s : undefined;
394
+ }
395
+ /**
396
+ * Response/error headers as a plain lower-cased record, or `undefined` when there are none. Accepts
397
+ * a `Headers` instance, a `Map`, or a plain object — an SDK's error type changes shape across
398
+ * versions and none of the three is worth depending on.
399
+ */
400
+ function responseHeaders(v) {
401
+ const h = v?.headers;
402
+ if (!h || typeof h !== 'object')
403
+ return undefined;
404
+ const out = {};
405
+ if (typeof h.forEach === 'function') {
406
+ h.forEach((value, key) => { out[String(key).toLowerCase()] = String(value); });
407
+ }
408
+ else {
409
+ for (const [k, value] of Object.entries(h))
410
+ out[k.toLowerCase()] = String(value);
411
+ }
412
+ return Object.keys(out).length > 0 ? out : undefined;
413
+ }
@@ -0,0 +1,4 @@
1
+ import { type CapabilityReport, type CapabilitySpec } from '@volter/world-tooling';
2
+ export declare const POLAR_CAPABILITIES: CapabilitySpec[];
3
+ export declare const POLAR_AREAS: readonly ["auth", "benefits", "checkouts", "conformance", "connector", "custom-fields", "customer-meters", "customer-portal", "customer-sessions", "customers", "discounts", "errors", "events", "files", "fixtures", "idempotency", "license-keys", "limits", "metadata", "meters", "metrics", "oauth", "orders", "organizations", "pagination", "payments", "products", "refunds", "safety", "subscriptions", "tax", "ui", "webhooks"];
4
+ export declare function polarCapabilities(): Promise<CapabilityReport>;