@volter/twin-polar 0.1.2 → 2.0.1
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 +2 -2
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +26 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +55 -0
- package/dist/src/polar-budget.d.ts +83 -0
- package/dist/src/polar-budget.js +413 -0
- package/dist/src/polar-capabilities.d.ts +4 -0
- package/dist/src/polar-capabilities.js +389 -0
- package/dist/src/polar-conformance.d.ts +7 -0
- package/dist/src/polar-conformance.js +25 -0
- package/dist/src/polar-connector.d.ts +113 -0
- package/dist/src/polar-connector.js +146 -0
- package/dist/src/polar-server.d.ts +14 -0
- package/dist/src/polar-server.js +52 -0
- package/dist/src/polar-twin.d.ts +24 -0
- package/dist/src/polar-twin.js +941 -0
- package/package.json +16 -9
- package/src/cli.ts +6 -6
- package/src/index.ts +22 -6
- package/src/polar-budget.ts +22 -6
- package/src/polar-capabilities.ts +54 -19
- package/src/polar-connector.ts +43 -26
- package/src/polar-server.ts +28 -4
- package/src/polar-twin.ts +159 -49
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Polar CONNECTOR — the live-vendor pull/push path that gives the Polar twin the full
|
|
2
|
+
// "git for SaaS" lifecycle over an INJECTED client (the auth boundary).
|
|
3
|
+
//
|
|
4
|
+
// PULL (real → twin): fetch real Polar objects via the injected client (customers, products,
|
|
5
|
+
// subscriptions, orders), map them to SyncResource[], and fold them into
|
|
6
|
+
// the event log via syncPull (shadow-diff dedup, so a re-pull of identical
|
|
7
|
+
// state is a no-op). Kernel subject ids are TYPE-PREFIXED to stay
|
|
8
|
+
// collision-safe (mirrors the twin handler).
|
|
9
|
+
// PUSH (twin → real): for every PENDING local action, call the injected client and
|
|
10
|
+
// confirmAction on success — recording the confirmed fields as observed
|
|
11
|
+
// and suppressing the local projection.
|
|
12
|
+
//
|
|
13
|
+
// The vendor I/O is an INJECTED client interface (`PolarLikeClient`): a fake in tests, a real
|
|
14
|
+
// `new Polar({ accessToken })` in prod. The pack imports NO SDK and holds NO key.
|
|
15
|
+
import { observeResources } from '@volter/world-core';
|
|
16
|
+
//
|
|
17
|
+
// ── The client-side RATE BUDGET is not optional here ────────────────────────────────────────
|
|
18
|
+
// Polar publishes 500 requests/minute in production and 100/minute in sandbox per organization, and
|
|
19
|
+
// is the one vendor in this batch that documents a Retry-After header — so the guard's cooldown is
|
|
20
|
+
// armed by the vendor itself rather than inferred.
|
|
21
|
+
// Every entrypoint below GUARDS the injected client before touching it (`guardPolarClient`, which is
|
|
22
|
+
// idempotent — a caller who already wrapped is not double-charged, a caller who forgot is protected
|
|
23
|
+
// anyway); there is deliberately no option that turns the budget off. See polar-budget.ts.
|
|
24
|
+
import { polarBudgetOf, guardPolarClient } from "./polar-budget.js";
|
|
25
|
+
const SERVICE = 'polar';
|
|
26
|
+
// Kernel subject id (type-prefixed → collision-safe across types; mirrors polar-twin.ts).
|
|
27
|
+
function kid(type, id) {
|
|
28
|
+
return `${type}:${id}`;
|
|
29
|
+
}
|
|
30
|
+
// ── PULL mappers (real Polar object → SyncResource) ──────────────────────────────────
|
|
31
|
+
export function mapCustomer(c) {
|
|
32
|
+
return { type: 'customer', id: kid('customer', String(c.id)), fields: { created_at: c.created_at ?? null, modified_at: null, email: c.email ?? null, name: c.name ?? null, external_id: c.external_id ?? null, customer_type: 'individual', email_verified: false, locale: 'en', metadata: c.metadata ?? {} } };
|
|
33
|
+
}
|
|
34
|
+
export function mapProduct(p) {
|
|
35
|
+
return { type: 'product', id: kid('product', String(p.id)), fields: { created_at: p.created_at ?? null, modified_at: null, name: p.name ?? null, description: p.description ?? null, recurring_interval: p.recurring_interval ?? null, is_archived: p.is_archived ?? false, trial_interval: p.trial_interval ?? null, trial_interval_count: p.trial_interval_count ?? null, prices: p.prices ?? [], benefits: [], metadata: {} } };
|
|
36
|
+
}
|
|
37
|
+
export function mapSubscription(s) {
|
|
38
|
+
return { type: 'subscription', id: kid('subscription', String(s.id)), fields: { created_at: s.created_at ?? null, modified_at: null, status: s.status ?? 'active', amount: s.amount ?? 0, currency: s.currency ?? 'usd', recurring_interval: s.recurring_interval ?? 'month', customer_id: s.customer_id ?? null, product_id: s.product_id ?? null, current_period_end: s.current_period_end ?? null, cancel_at_period_end: s.cancel_at_period_end ?? false, metadata: {} } };
|
|
39
|
+
}
|
|
40
|
+
export function mapOrder(o) {
|
|
41
|
+
return { type: 'order', id: kid('order', String(o.id)), fields: { created_at: o.created_at ?? null, modified_at: null, status: o.status ?? 'paid', paid: o.status !== 'refunded', amount: o.amount ?? o.total_amount ?? 0, total_amount: o.total_amount ?? o.amount ?? 0, currency: o.currency ?? 'usd', customer_id: o.customer_id ?? null, product_id: o.product_id ?? null, subscription_id: o.subscription_id ?? null, metadata: {} } };
|
|
42
|
+
}
|
|
43
|
+
export async function pullPolarCustomers(rawClient, opts = {}) {
|
|
44
|
+
const client = guardPolarClient(rawClient, opts);
|
|
45
|
+
if (!client.customers)
|
|
46
|
+
return [];
|
|
47
|
+
return (await client.customers.list()).items.map(mapCustomer);
|
|
48
|
+
}
|
|
49
|
+
export async function pullPolarProducts(rawClient, opts = {}) {
|
|
50
|
+
const client = guardPolarClient(rawClient, opts);
|
|
51
|
+
if (!client.products)
|
|
52
|
+
return [];
|
|
53
|
+
return (await client.products.list()).items.map(mapProduct);
|
|
54
|
+
}
|
|
55
|
+
export async function pullPolarSubscriptions(rawClient, opts = {}) {
|
|
56
|
+
const client = guardPolarClient(rawClient, opts);
|
|
57
|
+
if (!client.subscriptions)
|
|
58
|
+
return [];
|
|
59
|
+
return (await client.subscriptions.list()).items.map(mapSubscription);
|
|
60
|
+
}
|
|
61
|
+
export async function pullPolarOrders(rawClient, opts = {}) {
|
|
62
|
+
const client = guardPolarClient(rawClient, opts);
|
|
63
|
+
if (!client.orders)
|
|
64
|
+
return [];
|
|
65
|
+
return (await client.orders.list()).items.map(mapOrder);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* D7 entry point: pull from real Polar (customers + products + subscriptions + orders) and fold
|
|
69
|
+
* EVERY domain into the twin via ONE syncPull (shadow-diff dedup). Returns { observed,
|
|
70
|
+
* deltasAppended } — callers need not know vendor-specific pull mechanics.
|
|
71
|
+
*/
|
|
72
|
+
export async function syncPolarFromReal(rawClient, opts = {}) {
|
|
73
|
+
// Guard ONCE here and hand the guarded client down, so no domain pull can run unbudgeted.
|
|
74
|
+
const client = guardPolarClient(rawClient, polarBudgetOf(opts));
|
|
75
|
+
const occurredAt = opts.occurredAt ?? new Date().toISOString();
|
|
76
|
+
const resources = [
|
|
77
|
+
...(await pullPolarCustomers(client)),
|
|
78
|
+
...(await pullPolarProducts(client)),
|
|
79
|
+
...(await pullPolarSubscriptions(client)),
|
|
80
|
+
...(await pullPolarOrders(client)),
|
|
81
|
+
];
|
|
82
|
+
// protocol 2: an observation lands on the head through the kernel's fold — one batch, one instant
|
|
83
|
+
const report = observeResources(SERVICE, resources.map((r) => ({ type: r.type, id: r.id, fields: r.fields })), { ...(opts.root !== undefined ? { root: opts.root } : {}), at: occurredAt, batch: `obs:${SERVICE}:${occurredAt}` });
|
|
84
|
+
return { observed: report.observed, deltasAppended: report.appended };
|
|
85
|
+
}
|
|
86
|
+
// ── PROTOCOL 2: the state system's two adapters, over the kernel's executor ──────────
|
|
87
|
+
/** A PolarLikeClient over a RemoteExecute: the calls the SDK makes, as wire requests the executor carries. */
|
|
88
|
+
export function polarClientOver(execute) {
|
|
89
|
+
const call = async (method, path, body) => {
|
|
90
|
+
const res = await execute({ method, path, headers: { accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
91
|
+
if (res.status >= 300)
|
|
92
|
+
throw new Error(`polar: ${method} ${path} answered ${res.status}: ${res.body.slice(0, 200)}`);
|
|
93
|
+
return (res.body === '' ? {} : JSON.parse(res.body));
|
|
94
|
+
};
|
|
95
|
+
const list = (path) => async () => { const page = await call('GET', `${path}?limit=100`); return { items: page.items ?? [] }; };
|
|
96
|
+
return {
|
|
97
|
+
customers: { list: list('/v1/customers'), create: (params) => call('POST', '/v1/customers', params) },
|
|
98
|
+
products: { list: list('/v1/products'), create: (params) => call('POST', '/v1/products', params) },
|
|
99
|
+
subscriptions: { list: list('/v1/subscriptions') },
|
|
100
|
+
orders: { list: list('/v1/orders') },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** The refresh adapter: pull Polar's customers, products, subscriptions and orders through the executor and fold them into the root. */
|
|
104
|
+
export async function syncPolarFromRemote(execute, opts = {}) {
|
|
105
|
+
// guarded HERE too (idempotent), so the source tooth sees every entrypoint hold the budget
|
|
106
|
+
const budget = { ...(opts.budgetOptions ?? {}), ...(opts.root !== undefined && !opts.budgetOptions?.root ? { root: opts.root } : {}) };
|
|
107
|
+
const client = guardPolarClient(polarClientOver(execute), polarBudgetOf({ ...opts, budgetOptions: budget }));
|
|
108
|
+
return syncPolarFromReal(client, { ...opts, budgetOptions: budget });
|
|
109
|
+
}
|
|
110
|
+
/** The perform adapter: one entry crosses to Polar through the executor; a customer or product it names by a local id resolves first. */
|
|
111
|
+
export async function performPolarAction(execute, action, ctx) {
|
|
112
|
+
const fields = { ...(action.fields ?? {}) };
|
|
113
|
+
for (const [field, type] of [['customer_id', 'customer'], ['product_id', 'product']]) {
|
|
114
|
+
if (typeof fields[field] === 'string')
|
|
115
|
+
fields[field] = ctx.resolve(type, kid(type, String(fields[field]).replace(new RegExp(`^${type}:`), ''))).replace(new RegExp(`^${type}:`), '');
|
|
116
|
+
}
|
|
117
|
+
const budget = { budgetOptions: { ...(ctx.root !== undefined ? { root: ctx.root } : {}) } };
|
|
118
|
+
const { externalId } = await pushPolarAction(guardPolarClient(polarClientOver(execute), polarBudgetOf(budget)), { operation: action.operation, subject: action.subject, fields }, budget);
|
|
119
|
+
return { externalId };
|
|
120
|
+
}
|
|
121
|
+
// ── PUSH ──────────────────────────────────────────────────────────────────────────
|
|
122
|
+
// The twin operations this connector knows how to push to real Polar. Anything else FAILS
|
|
123
|
+
// LOUDLY rather than being silently dropped.
|
|
124
|
+
const PUSHABLE = new Set(['customer.create', 'product.create']);
|
|
125
|
+
export async function pushPolarAction(rawClient, action, opts = {}) {
|
|
126
|
+
// Writes count against the SAME per-minute budget reads do, so the push path is budgeted too.
|
|
127
|
+
const client = guardPolarClient(rawClient, opts);
|
|
128
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
129
|
+
if (!PUSHABLE.has(op))
|
|
130
|
+
throw new Error(`polar push: unsupported operation '${op}' — refusing to silently drop a local write`);
|
|
131
|
+
const fields = action.fields ?? {};
|
|
132
|
+
let externalId = action.subject.id;
|
|
133
|
+
if (op === 'customer.create' && client.customers?.create) {
|
|
134
|
+
externalId = (await client.customers.create({ email: fields.email, name: fields.name, external_id: fields.external_id })).id;
|
|
135
|
+
}
|
|
136
|
+
else if (op === 'product.create' && client.products?.create) {
|
|
137
|
+
externalId = (await client.products.create({ name: fields.name, recurring_interval: fields.recurring_interval })).id;
|
|
138
|
+
}
|
|
139
|
+
return { externalId: externalId || action.subject.id };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Push the twin's PENDING local actions to real Polar and CONFIRM each. A confirmed action is
|
|
143
|
+
* no longer pending, so a re-push enacts NOTHING. Unpushable ops are skipped.
|
|
144
|
+
*/
|
|
145
|
+
// protocol 2: the pending-actions loop is the kernel's (the head performs each entry through `performPolarAction`);
|
|
146
|
+
// the v1 `pushPendingPolarActions` is gone with the log it read.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Options every Polar-twin HTTP surface needs, independent of who owns the socket. */
|
|
2
|
+
export interface PolarTwinFetchOptions {
|
|
3
|
+
root?: string;
|
|
4
|
+
readOnly?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function createPolarTwinFetch(options?: PolarTwinFetchOptions): (request: Request) => Promise<Response>;
|
|
7
|
+
export declare function createPolarTwinServer(options?: {
|
|
8
|
+
root?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
port: number;
|
|
13
|
+
stop: () => void;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Polar twin HTTP server — serve the full Polar API twin handler over HTTP so the real
|
|
2
|
+
// `@polar-sh/sdk` (pointed at this base URL) works unmodified. JSON bodies pass through to the
|
|
3
|
+
// handler. Writable by default; pass readOnly to reject writes with 405 (D3). State is the
|
|
4
|
+
// kernel projection (no side-store) — see polar-twin.ts.
|
|
5
|
+
//
|
|
6
|
+
// FETCH-FIRST (runtime contract R12b): the serve path is the plain fetch below, built from the
|
|
7
|
+
// kernel's ONE adaptation (`createTwinFetchFromHandler`); this file contributes only VALUES. The
|
|
8
|
+
// vendor's 204 deletes return `{ status: 204, body: null }`, which the adapter's null-body rule
|
|
9
|
+
// serves as a genuinely empty response. The server is one line of Bun.serve around that closure.
|
|
10
|
+
import { serveHttp } from '@volter/world-core';
|
|
11
|
+
import { handlePolarTwinRequest } from "./polar-twin.js";
|
|
12
|
+
import { createTwinFetchFromHandler, statefulTwinManifest, twinPublicBase } from '@volter/world-core';
|
|
13
|
+
export function createPolarTwinFetch(options = {}) {
|
|
14
|
+
const api = createTwinFetchFromHandler(handlePolarTwinRequest, {
|
|
15
|
+
...options,
|
|
16
|
+
manifest: statefulTwinManifest({ vendor: 'polar', twinOf: 'the Polar billing API', stores: 'customers, checkouts, subscriptions, meters and usage events' }),
|
|
17
|
+
// where this twin is reached (origin plus any served-World mount path): the checkout's hosted page is minted there
|
|
18
|
+
extras: (request) => ({ publicBase: twinPublicBase(request) }),
|
|
19
|
+
});
|
|
20
|
+
// the hosted checkout page, at the twin's own address: what a person sees after "continue to checkout at
|
|
21
|
+
// Polar" in a walk — the product, the amount, a Pay button that confirms the checkout and returns to the
|
|
22
|
+
// app's success_url (in production this page is Polar's; the twin stands in for it, plainly labelled)
|
|
23
|
+
return async function polarFetch(request) {
|
|
24
|
+
const url = new URL(request.url);
|
|
25
|
+
const page = /^\/twin\/checkout\/([a-zA-Z0-9-]+)(\/pay)?$/.exec(url.pathname);
|
|
26
|
+
if (!page)
|
|
27
|
+
return api(request);
|
|
28
|
+
const id = page[1];
|
|
29
|
+
const read = await api(new Request(`${url.origin}/v1/checkouts/${id}`, { headers: { authorization: request.headers.get('authorization') ?? 'Bearer polar_oat_page' } }));
|
|
30
|
+
if (!read.ok)
|
|
31
|
+
return new Response('no such checkout', { status: 404 });
|
|
32
|
+
const c = (await read.json());
|
|
33
|
+
if (page[2] && request.method === 'POST') {
|
|
34
|
+
const done = await api(new Request(`${url.origin}/v1/checkouts/${id}/confirm`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: 'Bearer polar_oat_page' }, body: '{}' }));
|
|
35
|
+
if (!done.ok)
|
|
36
|
+
return new Response(`the checkout could not be confirmed (${done.status})`, { status: 502 });
|
|
37
|
+
return new Response(null, { status: 303, headers: { location: c.success_url ?? `${twinPublicBase(request)}/twin/checkout/${id}` } });
|
|
38
|
+
}
|
|
39
|
+
const money = `$${(Number(c.amount ?? 0) / 100).toFixed(2)} ${String(c.currency ?? 'usd').toUpperCase()}`;
|
|
40
|
+
const esc = (v) => String(v ?? '').replace(/[&<>"]/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[ch]);
|
|
41
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Checkout — Polar (twin)</title><style>body{font:16px/1.5 -apple-system,system-ui,sans-serif;margin:0;background:#f6f6f7;color:#111}main{max-width:480px;margin:48px auto;background:#fff;border:1px solid #e3e3e6;border-radius:12px;padding:28px 32px}h1{font-size:20px;margin:0 0 4px}.mode{color:#666;font-size:13px}.price{font-size:32px;font-weight:650;margin:12px 0}button{font:inherit;padding:10px 18px;border-radius:8px;border:0;background:#111;color:#fff;cursor:pointer}label{display:block;margin:12px 0 4px;font-size:13px;color:#555}input{width:100%;padding:8px 10px;border:1px solid #ccc;border-radius:6px;font:inherit}</style></head><body><main><p class="mode">Polar checkout · this page is the polar twin's stand-in for Polar's hosted checkout</p><h1>${esc(c.product?.name ?? 'Subscription')}</h1><p class="price">${esc(money)} <span class="mode">/ month</span></p>${c.status !== 'open' ? `<p>This checkout is ${esc(c.status)}.</p>` : `<form method="post" action="${esc(twinPublicBase(request))}/twin/checkout/${esc(id)}/pay"><label>Email</label><input name="email" value="${esc(c.customer_email ?? '')}" readonly /><label>Card</label><input name="card" value="4242 4242 4242 4242 · any date · any code" readonly /><p class="mode">A test card: nothing is charged here.</p><button type="submit">Pay ${esc(money)}</button></form>`}</main></body></html>`;
|
|
42
|
+
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function createPolarTwinServer(options = {}) {
|
|
46
|
+
const server = await serveHttp({
|
|
47
|
+
port: options.port ?? 0,
|
|
48
|
+
idleTimeout: 60,
|
|
49
|
+
fetch: createPolarTwinFetch(options),
|
|
50
|
+
});
|
|
51
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type PolarRequest = {
|
|
2
|
+
method: string;
|
|
3
|
+
path: string;
|
|
4
|
+
body?: string;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
/** where the twin is reached when served over HTTP (`twinPublicBase`): the checkout's hosted page is there */
|
|
7
|
+
publicBase?: string;
|
|
8
|
+
occurredAt?: string;
|
|
9
|
+
root?: string;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type PolarResponse = {
|
|
13
|
+
status: number;
|
|
14
|
+
body: unknown;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
export declare const POLAR_RESOURCE_TYPES: readonly ["customer", "product", "subscription", "order", "checkout", "benefit", "discount", "customer_session", "event", "meter", "webhook_endpoint"];
|
|
18
|
+
export type PolarResourceType = typeof POLAR_RESOURCE_TYPES[number];
|
|
19
|
+
export declare function handlePolarTwinRequest(req: PolarRequest): Promise<PolarResponse>;
|
|
20
|
+
export type PolarTwinSnapshot = {
|
|
21
|
+
resourceTypes: readonly PolarResourceType[];
|
|
22
|
+
implementedEndpoints: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
export declare function polarTwinSnapshot(): PolarTwinSnapshot;
|