@volter/twin-polar 0.1.2 → 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 +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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-polar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Local Polar twin for customers, checkouts, subscriptions, meters, and usage events.",
|
|
5
5
|
"author": "Volter (https://github.com/volter-ai)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"src",
|
|
9
9
|
"README.md",
|
|
10
10
|
"LICENSE",
|
|
11
|
-
"!**/*.test.ts"
|
|
11
|
+
"!**/*.test.ts",
|
|
12
|
+
"dist"
|
|
12
13
|
],
|
|
13
14
|
"repository": {
|
|
14
15
|
"type": "git",
|
|
@@ -18,26 +19,32 @@
|
|
|
18
19
|
"homepage": "https://github.com/volter-ai/twin/tree/main/packages/twin/polar#readme",
|
|
19
20
|
"type": "module",
|
|
20
21
|
"exports": {
|
|
21
|
-
".":
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/src/index.d.ts",
|
|
24
|
+
"default": "./dist/src/index.js"
|
|
25
|
+
}
|
|
22
26
|
},
|
|
23
27
|
"bin": {
|
|
24
|
-
"world-polar": "src/cli.
|
|
28
|
+
"world-polar": "dist/src/cli.js"
|
|
25
29
|
},
|
|
26
30
|
"scripts": {
|
|
27
31
|
"test": "bun test src/*.test.ts",
|
|
28
|
-
"typecheck": "tsc --noEmit"
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"build": "node ../../../scripts/publish/build.mjs",
|
|
34
|
+
"prepack": "node ../../../scripts/publish/prepare-publish.mjs prepack",
|
|
35
|
+
"postpack": "node ../../../scripts/publish/prepare-publish.mjs postpack"
|
|
29
36
|
},
|
|
30
37
|
"peerDependencies": {
|
|
31
|
-
"@volter/
|
|
38
|
+
"@volter/world-core": "2.0.0"
|
|
32
39
|
},
|
|
33
40
|
"devDependencies": {
|
|
34
41
|
"@types/bun": "^1.2.20",
|
|
35
42
|
"@types/node": "^24.0.0",
|
|
36
|
-
"@volter/
|
|
37
|
-
"@volter/
|
|
43
|
+
"@volter/world-core": "2.0.0",
|
|
44
|
+
"@volter/world-tooling": "0.1.0",
|
|
38
45
|
"typescript": "^5.9.0"
|
|
39
46
|
},
|
|
40
47
|
"engines": {
|
|
41
|
-
"
|
|
48
|
+
"node": ">=22.3"
|
|
42
49
|
}
|
|
43
50
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
import { keepProcessAlive } from '@volter/
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { keepProcessAlive } from '@volter/world-core/lifecycle';
|
|
3
3
|
// world-polar CLI: serve the KERNEL-BACKED Polar API twin, or run conformance. State lives in
|
|
4
|
-
// the @volter/
|
|
5
|
-
// so the bin runs without @volter/
|
|
6
|
-
import { hasFlag, optionValue } from '@volter/
|
|
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
7
|
import { createPolarTwinServer } from './polar-server.ts';
|
|
8
8
|
|
|
9
9
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
@@ -12,7 +12,7 @@ const root = optionValue(rest, '--root') || undefined;
|
|
|
12
12
|
const readOnly = hasFlag(rest, '--read-only'); // a twin accepts writes unless started read-only
|
|
13
13
|
|
|
14
14
|
if (cmd === 'serve' || cmd === undefined) {
|
|
15
|
-
const s = createPolarTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
|
|
15
|
+
const s = await createPolarTwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}) });
|
|
16
16
|
process.stdout.write(`polar twin (merchant-of-record billing API)${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:${s.port}\n`);
|
|
17
17
|
await keepProcessAlive();
|
|
18
18
|
} else if (cmd === 'conformance') {
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @volter/twin-polar — the Polar (polar.sh) merchant-of-record billing API twin, built on the
|
|
2
|
-
// shared @volter/
|
|
2
|
+
// shared @volter/world-core kernel. REST transport over api.polar.sh shapes; state lives entirely in
|
|
3
3
|
// the kernel action log (no side-store). Customers, products, subscriptions, orders, checkouts,
|
|
4
4
|
// benefits, discounts, meters, usage events, customer sessions, and webhook endpoints.
|
|
5
|
-
// (Conformance/capability tooling lives in @volter/
|
|
5
|
+
// (Conformance/capability tooling lives in @volter/world-tooling, a dev dependency — NOT shipped.)
|
|
6
6
|
export { handlePolarTwinRequest, polarTwinSnapshot, POLAR_RESOURCE_TYPES } from './polar-twin.ts';
|
|
7
7
|
export type { PolarRequest, PolarResponse, PolarResourceType, PolarTwinSnapshot } from './polar-twin.ts';
|
|
8
8
|
export { createPolarTwinFetch, createPolarTwinServer, type PolarTwinFetchOptions } from './polar-server.ts';
|
|
@@ -15,9 +15,11 @@ export {
|
|
|
15
15
|
pullPolarOrders,
|
|
16
16
|
pullPolarProducts,
|
|
17
17
|
pullPolarSubscriptions,
|
|
18
|
-
pushPendingPolarActions,
|
|
19
18
|
pushPolarAction,
|
|
20
19
|
syncPolarFromReal,
|
|
20
|
+
polarClientOver,
|
|
21
|
+
syncPolarFromRemote,
|
|
22
|
+
performPolarAction,
|
|
21
23
|
} from './polar-connector.ts';
|
|
22
24
|
export type { PolarCustomer, PolarLikeClient, PolarOrder, PolarProduct, PolarSubscription, PolarBudgetedOptions } from './polar-connector.ts';
|
|
23
25
|
// The client-side rate budget — the fail-closed backstop every live call goes through. The
|
|
@@ -43,9 +45,21 @@ export {
|
|
|
43
45
|
export type { PolarBudgetErrorKind, PolarBudgetOptions, PolarBudgetReservation, PolarBudgetSnapshot } from './polar-budget.ts';
|
|
44
46
|
|
|
45
47
|
// Registry descriptor: the pack self-describes so tooling can discover it.
|
|
46
|
-
import type
|
|
48
|
+
import { registerPack, type TwinPack } from '@volter/world-core';
|
|
47
49
|
import { POLAR_RATE_BUDGET as RATE_BUDGET } from './polar-budget.ts';
|
|
50
|
+
import { performPolarAction as perform, syncPolarFromRemote as refresh } from './polar-connector.ts';
|
|
48
51
|
export const pack: TwinPack = {
|
|
52
|
+
// 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
|
|
53
|
+
// state system: perform one entry against Polar, refresh the root from it. Moved 2026-09-08.
|
|
54
|
+
protocol: '2',
|
|
55
|
+
refresh: { every: '5m', webhook: true, onDemand: { atMost: '30s' } },
|
|
56
|
+
stateSystem: { perform, refresh },
|
|
57
|
+
roundTrip: { method: 'POST', path: '/v1/products', body: { name: 'round trip', recurring_interval: 'month' }, headers: { authorization: 'Bearer polar_oat_round_trip' } },
|
|
58
|
+
// rule 5: a checkout names its product; when the head adopts Polar's id for the product, the kernel resolves it first
|
|
59
|
+
referenceTrip: { method: 'POST', path: '/v1/checkouts', body: { products: ['{{id}}'] }, headers: { authorization: 'Bearer polar_oat_round_trip' } },
|
|
60
|
+
references: [{ type: 'checkout', to: 'product', key: (f: Record<string, unknown>) => (typeof f.product_id === 'string' ? `product:${f.product_id}` : undefined), adopt: (_f: Record<string, unknown>, vendorId: string) => ({ product_id: vendorId.replace(/^product:/, '') }) }],
|
|
61
|
+
parityOrigin: 'http://twin',
|
|
62
|
+
shapeParity: 'held',
|
|
49
63
|
// The SAME object polar-budget.ts declares at module load — one source of truth, so registering
|
|
50
64
|
// the pack and importing the connector can never arm two different ceilings.
|
|
51
65
|
rateBudget: RATE_BUDGET,
|
|
@@ -56,8 +70,8 @@ export const pack: TwinPack = {
|
|
|
56
70
|
resources: ['customer', 'product', 'subscription', 'order', 'checkout', 'benefit', 'discount', 'meter', 'event', 'customer_session', 'webhook_endpoint'],
|
|
57
71
|
specSource: 'polar-conformance.ts (endpoint/resource inventory from the Polar API docs at docs.polar.sh)',
|
|
58
72
|
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.',
|
|
59
|
-
// Adoption + interception, moved off the central maps unchanged (
|
|
60
|
-
//
|
|
73
|
+
// Adoption + interception, moved off the central maps unchanged (descriptor-first back-migration, adding-a-twin.md §3,
|
|
74
|
+
// 2026-08-31). Both the production API host and Polar's own sandbox host are
|
|
61
75
|
// claimed — an integration picks one by configuration, and claiming only production would let
|
|
62
76
|
// sandbox traffic escape the world.
|
|
63
77
|
adoption: {
|
|
@@ -68,3 +82,5 @@ export const pack: TwinPack = {
|
|
|
68
82
|
hosts: [{ host: 'api.polar.sh' }, { host: 'sandbox-api.polar.sh' }],
|
|
69
83
|
browserRouting: { apiPathPrefix: '/v1', loaderHost: 'https://api.polar.sh' },
|
|
70
84
|
};
|
|
85
|
+
// registered at import: the kernel learns the pack's state system and its references (protocol 2)
|
|
86
|
+
registerPack(pack);
|
package/src/polar-budget.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Polar's CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus `guardPolarClient`,
|
|
2
2
|
// the choke point every live Polar call goes through. The MECHANISM — the durable token-keyed
|
|
3
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/
|
|
4
|
+
// corrupt ledger — lives ONCE in the vendor-agnostic kernel (`@volter/world-core` → `rateBudget.ts`).
|
|
5
5
|
// Read that module's header for the full rationale AND for the honest list of what the guard does
|
|
6
6
|
// NOT guarantee (an injected clock or ledger path still defeats it — it guards carelessness, not
|
|
7
7
|
// malice). This module is modeled on notion-budget.ts, the reference injected-client decorator.
|
|
@@ -47,11 +47,12 @@ import {
|
|
|
47
47
|
rateBudgetPath,
|
|
48
48
|
rateBudgetWeight,
|
|
49
49
|
RateBudget,
|
|
50
|
+
RateBudgetError,
|
|
50
51
|
type RateBudgetDeclaration,
|
|
51
52
|
type RateBudgetOptions,
|
|
52
53
|
type RateBudgetReservation,
|
|
53
54
|
type RateBudgetSnapshot,
|
|
54
|
-
} from '@volter/
|
|
55
|
+
} from '@volter/world-core';
|
|
55
56
|
import type { PolarLikeClient } from './polar-connector.ts';
|
|
56
57
|
|
|
57
58
|
const VENDOR = 'polar';
|
|
@@ -153,8 +154,8 @@ export class PolarBudget extends RateBudget {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
export type { RateBudgetErrorKind as PolarBudgetErrorKind } from '@volter/
|
|
157
|
-
export { RateBudgetError as PolarBudgetError } from '@volter/
|
|
157
|
+
export type { RateBudgetErrorKind as PolarBudgetErrorKind } from '@volter/world-core';
|
|
158
|
+
export { RateBudgetError as PolarBudgetError } from '@volter/world-core';
|
|
158
159
|
export type PolarBudgetReservation = RateBudgetReservation;
|
|
159
160
|
export type PolarBudgetSnapshot = RateBudgetSnapshot;
|
|
160
161
|
|
|
@@ -253,6 +254,21 @@ export function guardPolarClient(client: PolarLikeClient, opts: PolarBudgetedOpt
|
|
|
253
254
|
else budget.recordCall(weight, undefined, { reservation });
|
|
254
255
|
};
|
|
255
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Settle once the call RESOLVED: the vendor has answered. recordCall arms any cooldown before
|
|
259
|
+
* it throws (a back-off beyond the cap), so that refusal is swallowed and the answer kept — a
|
|
260
|
+
* write the vendor accepted is never reported failed and performed again on retry. A resolved
|
|
261
|
+
* answer that carries a non-2xx status still lets the louder refusal win.
|
|
262
|
+
*/
|
|
263
|
+
const settleAnswered = (weight: number, reservation: RateBudgetReservation, v: unknown): void => {
|
|
264
|
+
try {
|
|
265
|
+
settle(weight, reservation, v);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
const status = responseStatus(v);
|
|
268
|
+
if (!(e instanceof RateBudgetError) || (status !== undefined && (status < 200 || status >= 300))) throw e;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
256
272
|
/**
|
|
257
273
|
* Charge, call, settle — for a MODELED method, whose interface declares it `async`. Refusing
|
|
258
274
|
* REJECTS rather than throwing synchronously, so `client.x().catch(…)` behaves exactly as it does
|
|
@@ -265,7 +281,7 @@ export function guardPolarClient(client: PolarLikeClient, opts: PolarBudgetedOpt
|
|
|
265
281
|
const reservation = budget.checkBudget(weight);
|
|
266
282
|
try {
|
|
267
283
|
const res = await invoke(...args);
|
|
268
|
-
|
|
284
|
+
settleAnswered(weight, reservation, res);
|
|
269
285
|
return res;
|
|
270
286
|
} catch (e) {
|
|
271
287
|
settle(weight, reservation, e); // may throw its own louder refusal, which wins
|
|
@@ -304,7 +320,7 @@ export function guardPolarClient(client: PolarLikeClient, opts: PolarBudgetedOpt
|
|
|
304
320
|
: out;
|
|
305
321
|
}
|
|
306
322
|
return out.then(
|
|
307
|
-
(res) => {
|
|
323
|
+
(res) => { settleAnswered(weight, reservation, res); return res; },
|
|
308
324
|
(e: unknown) => { settle(weight, reservation, e); throw e; },
|
|
309
325
|
);
|
|
310
326
|
};
|
|
@@ -7,17 +7,15 @@
|
|
|
7
7
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { checkCapabilities, type CapabilityReport, type CapabilitySpec, verifyBoundary } from '@volter/
|
|
10
|
+
import { checkCapabilities, type CapabilityReport, type CapabilitySpec, verifyBoundary } from '@volter/world-tooling';
|
|
11
11
|
import { checkPolarConformance } from './polar-conformance.ts';
|
|
12
|
-
import { syncPolarFromReal,
|
|
12
|
+
import { syncPolarFromReal, performPolarAction, type PolarLikeClient } from './polar-connector.ts';
|
|
13
13
|
import { handlePolarTwinRequest, type PolarResponse } from './polar-twin.ts';
|
|
14
14
|
|
|
15
15
|
const done = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], verify: CapabilitySpec['verify']): CapabilitySpec =>
|
|
16
16
|
({ id, area, title, dimension, tier, expected: 'done', verify });
|
|
17
17
|
const todo = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier']): CapabilitySpec =>
|
|
18
18
|
({ id, area, title, dimension, tier, expected: 'todo' });
|
|
19
|
-
const outOfScope = (id: string, area: string, title: string, dimension: CapabilitySpec['dimension'], tier: CapabilitySpec['tier'], reason: string): CapabilitySpec =>
|
|
20
|
-
({ id, area, title, dimension, tier, expected: 'todo', outOfScope: reason });
|
|
21
19
|
|
|
22
20
|
// ── API verify: drive REAL requests against a fresh isolated root, then assert status/shape ──
|
|
23
21
|
type Step = { m: string; p: string; b?: unknown };
|
|
@@ -39,7 +37,7 @@ const id = (r: PolarResponse) => (r.body as Body)?.id as string;
|
|
|
39
37
|
const field = (r: PolarResponse, k: string) => (r.body as Body)?.[k];
|
|
40
38
|
const items = (r: PolarResponse) => (r.body as Body)?.items as Body[];
|
|
41
39
|
/** A Polar ResourceNotFound 404 (the real error envelope). */
|
|
42
|
-
const isNotFound = (r: PolarResponse) => r.status === 404 && (r.body as Body)?.
|
|
40
|
+
const isNotFound = (r: PolarResponse) => r.status === 404 && (r.body as Body)?.error === 'ResourceNotFound'; // Polar's envelope: { error, detail }
|
|
43
41
|
/** A FastAPI/Polar 422 validation envelope. */
|
|
44
42
|
const isValidation = (r: PolarResponse) => r.status === 422 && Array.isArray((r.body as Body)?.detail);
|
|
45
43
|
|
|
@@ -241,6 +239,47 @@ export const POLAR_CAPABILITIES: CapabilitySpec[] = [
|
|
|
241
239
|
return (field(s, 'active_meters') as Body[])[0]!.balance === 3;
|
|
242
240
|
})),
|
|
243
241
|
|
|
242
|
+
// ── The customer by YOUR id, meters as Polar keeps them (2026-09-07, the platform's billing) ──
|
|
243
|
+
done('polar.customers.get_external_state', 'customers', 'GET /customers/external/{id} and its /state: the customer by external id; unknown → 404', 'api', 'core', () =>
|
|
244
|
+
withRoot(async (h) => {
|
|
245
|
+
await h({ m: 'POST', p: '/v1/customers/', b: { email: 'ext@example.test', external_id: 'org_1' } });
|
|
246
|
+
const g = await h({ m: 'GET', p: '/v1/customers/external/org_1' });
|
|
247
|
+
const s = await h({ m: 'GET', p: '/v1/customers/external/org_1/state' });
|
|
248
|
+
const miss = await h({ m: 'GET', p: '/v1/customers/external/nobody/state' });
|
|
249
|
+
return ok(g) && field(g, 'external_id') === 'org_1' && ok(s) && Array.isArray(field(s, 'active_subscriptions')) && isNotFound(miss);
|
|
250
|
+
})),
|
|
251
|
+
done('polar.checkouts.external_customer_id', 'checkouts', 'A checkout created with external_customer_id confirms onto the customer that id names (created if new)', 'api', 'core', () =>
|
|
252
|
+
withRoot(async (h) => {
|
|
253
|
+
const p = await h({ m: 'POST', p: '/v1/products/', b: { name: 'Team', recurring_interval: 'month', prices: [{ price_amount: 2000, price_currency: 'usd' }] } });
|
|
254
|
+
const c = await h({ m: 'POST', p: '/v1/checkouts/', b: { products: [id(p)], external_customer_id: 'org_42', customer_email: 'pay@example.test' } });
|
|
255
|
+
const done = await h({ m: 'POST', p: `/v1/checkouts/${id(c)}/confirm`, b: {} });
|
|
256
|
+
const s = await h({ m: 'GET', p: '/v1/customers/external/org_42/state' });
|
|
257
|
+
const subs = field(s, 'active_subscriptions') as Body[];
|
|
258
|
+
return c.status === 201 && ok(done) && ok(s) && subs.length === 1 && subs[0]!.status === 'active';
|
|
259
|
+
})),
|
|
260
|
+
done('polar.events.ingest_dedupes_external_id', 'events', 'Ingest counts a repeated external_id as a duplicate and resolves external_customer_id to the customer', 'api', 'core', () =>
|
|
261
|
+
withRoot(async (h) => {
|
|
262
|
+
const first = await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ name: 'world.hour', external_customer_id: 'org_7', external_id: 'org_7:h:1', metadata: { units: 1 } }] } });
|
|
263
|
+
const again = await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [{ name: 'world.hour', external_customer_id: 'org_7', external_id: 'org_7:h:1', metadata: { units: 1 } }] } });
|
|
264
|
+
const list = await h({ m: 'GET', p: '/v1/events/?external_customer_id=org_7' });
|
|
265
|
+
const cust = await h({ m: 'GET', p: '/v1/customers/external/org_7' });
|
|
266
|
+
return field(first, 'inserted') === 1 && field(again, 'duplicates') === 1 && field(again, 'inserted') === 0 && (field(list, 'items') as unknown[]).length === 1 && ok(cust);
|
|
267
|
+
})),
|
|
268
|
+
done('polar.customers.state_per_meter_aggregation', 'customers', 'With meters defined, state carries one entry per meter aggregated by its filter and function', 'api', 'core', () =>
|
|
269
|
+
withRoot(async (h) => {
|
|
270
|
+
const m = await h({ m: 'POST', p: '/v1/meters/', b: { name: 'world_hours', filter: { conjunction: 'and', clauses: [{ property: 'name', operator: 'eq', value: 'world.hour' }] }, aggregation: { func: 'sum', property: 'units' } } });
|
|
271
|
+
await h({ m: 'POST', p: '/v1/meters/', b: { name: 'pushes', filter: { conjunction: 'and', clauses: [{ property: 'name', operator: 'eq', value: 'world.push' }] }, aggregation: { func: 'count' } } });
|
|
272
|
+
await h({ m: 'POST', p: '/v1/events/ingest', b: { events: [
|
|
273
|
+
{ name: 'world.hour', external_customer_id: 'org_9', external_id: 'a', metadata: { units: 2 } },
|
|
274
|
+
{ name: 'world.hour', external_customer_id: 'org_9', external_id: 'b', metadata: { units: 3 } },
|
|
275
|
+
{ name: 'world.push', external_customer_id: 'org_9', external_id: 'c' },
|
|
276
|
+
] } });
|
|
277
|
+
const s = await h({ m: 'GET', p: '/v1/customers/external/org_9/state' });
|
|
278
|
+
const meters = field(s, 'active_meters') as Body[];
|
|
279
|
+
const hours = meters.find((x) => x.meter_id === id(m)); const pushes = meters.find((x) => x.meter_id !== id(m));
|
|
280
|
+
return meters.length === 2 && hours?.consumed_units === 5 && pushes?.consumed_units === 1;
|
|
281
|
+
})),
|
|
282
|
+
|
|
244
283
|
// ── Customer sessions ───────────────────────────────────────────────────────────────
|
|
245
284
|
done('polar.customer_sessions.create', 'customer-sessions', 'Create a customer portal session; unknown customer → 404', 'api', 'common', () =>
|
|
246
285
|
withRoot(async (h) => {
|
|
@@ -298,18 +337,17 @@ export const POLAR_CAPABILITIES: CapabilitySpec[] = [
|
|
|
298
337
|
rmSync(root, { recursive: true, force: true });
|
|
299
338
|
}
|
|
300
339
|
})),
|
|
301
|
-
done('polar.connector.push.entrypoint', 'connector', '
|
|
340
|
+
done('polar.connector.push.entrypoint', 'connector', 'performPolarAction crosses a local customer.create to Polar through the executor (protocol 2)', 'connector', 'core', () =>
|
|
302
341
|
withRoot(async () => {
|
|
303
342
|
const root = mkdtempSync(join(tmpdir(), 'polar-push-'));
|
|
304
343
|
try {
|
|
305
|
-
await handlePolarTwinRequest({ method: 'POST', path: '/v1/customers/', body: JSON.stringify({ email: 'push@example.test', name: 'P' }), root });
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return res.pushed === 1 && pushedEmail === 'push@example.test' && again.pushed === 0;
|
|
344
|
+
const made = await handlePolarTwinRequest({ method: 'POST', path: '/v1/customers/', body: JSON.stringify({ email: 'push@example.test', name: 'P' }), root });
|
|
345
|
+
const wire: Array<{ method: string; path: string; body?: string | Uint8Array }> = [];
|
|
346
|
+
const execute = async (r: { method: string; path: string; body?: string | Uint8Array }) => { wire.push(r); return { status: 201, headers: {}, body: JSON.stringify({ id: 'cus_pushed' }) }; };
|
|
347
|
+
const out = await performPolarAction(execute, { operation: 'customer.create', subject: { type: 'customer', id: `customer:${String(field(made, 'id'))}` }, fields: { email: 'push@example.test', name: 'P' } } as never, { resolve: (_t, l) => l, service: 'polar', root });
|
|
348
|
+
const sent = JSON.parse(typeof wire[0]?.body === 'string' ? wire[0].body : Buffer.from(wire[0]?.body ?? '{}').toString()) as Body;
|
|
349
|
+
// one entry, one wire write, the vendor's id back; re-performing is the kernel's to refuse
|
|
350
|
+
return out.externalId === 'cus_pushed' && wire.length === 1 && wire[0]!.path === '/v1/customers' && sent.email === 'push@example.test';
|
|
313
351
|
} finally {
|
|
314
352
|
rmSync(root, { recursive: true, force: true });
|
|
315
353
|
}
|
|
@@ -320,9 +358,6 @@ export const POLAR_CAPABILITIES: CapabilitySpec[] = [
|
|
|
320
358
|
return report.ok && report.endpointsChecked >= 30 && report.resourceTypesChecked >= 10;
|
|
321
359
|
}),
|
|
322
360
|
|
|
323
|
-
// ── out of scope (a local twin cannot honestly do these) ──────────────────────────────
|
|
324
|
-
outOfScope('polar.payments.real_processing', 'payments', 'Real payment processor authorization, capture, disputes, and settlement', 'api', 'core', 'A local twin must not contact or emulate real card networks; it returns deterministic checkout/order shapes and "confirms" payment offline.'),
|
|
325
|
-
outOfScope('polar.tax.real_calculation', 'tax', 'Hosted tax/VAT calculation, nexus, remittance, and jurisdiction updates', 'api', 'common', 'Tax authority integrations and live jurisdiction rules are outside a deterministic local twin.'),
|
|
326
361
|
|
|
327
362
|
// ── the remaining real Polar surface (the honest TODO denominator) ─────────────────────
|
|
328
363
|
...[
|
|
@@ -380,9 +415,9 @@ export const POLAR_CAPABILITIES: CapabilitySpec[] = [
|
|
|
380
415
|
|
|
381
416
|
// ── Missing-area sweep (TWIN-87 / F1) ────────────────────────────────────────────────────
|
|
382
417
|
// /v1/metrics and /v1/customer-meters had NO manifest entry of any status, and the
|
|
383
|
-
// `payments` area held only
|
|
418
|
+
// `payments` area held only a real-processing phantom (list/read was unrepresented).
|
|
384
419
|
// Filed as honest todos (all genuinely buildable — reading back recorded payment metadata
|
|
385
|
-
// is not the same as real card processing, which
|
|
420
|
+
// is not the same as real card processing, which is what the real root does). See
|
|
386
421
|
// POLAR_AREAS below + polar-capabilities.test.ts's area-census meta-test, gate-wired so a
|
|
387
422
|
// future whole-area omission fails here instead of waiting for another review pass.
|
|
388
423
|
todo('polar.metrics.query', 'metrics', 'Metrics: aggregated revenue/orders/subscribers time-series (GET /v1/metrics/)', 'api', 'niche'),
|
package/src/polar-connector.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
//
|
|
13
13
|
// The vendor I/O is an INJECTED client interface (`PolarLikeClient`): a fake in tests, a real
|
|
14
14
|
// `new Polar({ accessToken })` in prod. The pack imports NO SDK and holds NO key.
|
|
15
|
-
import {
|
|
16
|
-
import type { SyncResource, TwinAction } from '@volter/
|
|
15
|
+
import { observeResources } from '@volter/world-core';
|
|
16
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction } from '@volter/world-core';
|
|
17
17
|
//
|
|
18
18
|
// ── The client-side RATE BUDGET is not optional here ────────────────────────────────────────
|
|
19
19
|
// Polar publishes 500 requests/minute in production and 100/minute in sandbox per organization, and
|
|
@@ -29,7 +29,7 @@ const SERVICE = 'polar';
|
|
|
29
29
|
export type { PolarBudgetedOptions };
|
|
30
30
|
|
|
31
31
|
export type PolarCustomer = { id: string; email?: string | null; name?: string | null; external_id?: string | null; created_at?: string; metadata?: Record<string, unknown> };
|
|
32
|
-
export type PolarProduct = { id: string; name?: string | null; description?: string | null; recurring_interval?: string | null; is_archived?: boolean; created_at?: string };
|
|
32
|
+
export type PolarProduct = { id: string; name?: string | null; description?: string | null; recurring_interval?: string | null; is_archived?: boolean; created_at?: string; prices?: unknown[]; trial_interval?: string | null; trial_interval_count?: number | null };
|
|
33
33
|
export type PolarSubscription = { id: string; status?: string; amount?: number; currency?: string; recurring_interval?: string; customer_id?: string | null; product_id?: string | null; created_at?: string; current_period_end?: string | null; cancel_at_period_end?: boolean };
|
|
34
34
|
export type PolarOrder = { id: string; status?: string; amount?: number; total_amount?: number; currency?: string; customer_id?: string | null; product_id?: string | null; subscription_id?: string | null; created_at?: string };
|
|
35
35
|
|
|
@@ -53,7 +53,7 @@ export function mapCustomer(c: PolarCustomer): SyncResource {
|
|
|
53
53
|
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 ?? {} } };
|
|
54
54
|
}
|
|
55
55
|
export function mapProduct(p: PolarProduct): SyncResource {
|
|
56
|
-
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, prices: [], benefits: [], metadata: {} } };
|
|
56
|
+
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: {} } };
|
|
57
57
|
}
|
|
58
58
|
export function mapSubscription(s: PolarSubscription): SyncResource {
|
|
59
59
|
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: {} } };
|
|
@@ -101,8 +101,43 @@ export async function syncPolarFromReal(
|
|
|
101
101
|
...(await pullPolarSubscriptions(client)),
|
|
102
102
|
...(await pullPolarOrders(client)),
|
|
103
103
|
];
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// protocol 2: an observation lands on the head through the kernel's fold — one batch, one instant
|
|
105
|
+
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}` });
|
|
106
|
+
return { observed: report.observed, deltasAppended: report.appended };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── PROTOCOL 2: the state system's two adapters, over the kernel's executor ──────────
|
|
110
|
+
/** A PolarLikeClient over a RemoteExecute: the calls the SDK makes, as wire requests the executor carries. */
|
|
111
|
+
export function polarClientOver(execute: RemoteExecute): PolarLikeClient {
|
|
112
|
+
const call = async <T>(method: string, path: string, body?: Record<string, unknown>): Promise<T> => {
|
|
113
|
+
const res = await execute({ method, path, headers: { accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
114
|
+
if (res.status >= 300) throw new Error(`polar: ${method} ${path} answered ${res.status}: ${res.body.slice(0, 200)}`);
|
|
115
|
+
return (res.body === '' ? {} : JSON.parse(res.body)) as T;
|
|
116
|
+
};
|
|
117
|
+
const list = <T>(path: string) => async (): Promise<{ items: T[] }> => { const page = await call<{ items?: T[] }>('GET', `${path}?limit=100`); return { items: page.items ?? [] }; };
|
|
118
|
+
return {
|
|
119
|
+
customers: { list: list<PolarCustomer>('/v1/customers'), create: (params) => call<{ id: string }>('POST', '/v1/customers', params) },
|
|
120
|
+
products: { list: list<PolarProduct>('/v1/products'), create: (params) => call<{ id: string }>('POST', '/v1/products', params) },
|
|
121
|
+
subscriptions: { list: list<PolarSubscription>('/v1/subscriptions') },
|
|
122
|
+
orders: { list: list<PolarOrder>('/v1/orders') },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** The refresh adapter: pull Polar's customers, products, subscriptions and orders through the executor and fold them into the root. */
|
|
126
|
+
export async function syncPolarFromRemote(execute: RemoteExecute, opts: { root?: string; origin?: string; occurredAt?: string } & PolarBudgetedOptions = {}): Promise<{ observed: number; deltasAppended: number }> {
|
|
127
|
+
// guarded HERE too (idempotent), so the source tooth sees every entrypoint hold the budget
|
|
128
|
+
const budget = { ...(opts.budgetOptions ?? {}), ...(opts.root !== undefined && !opts.budgetOptions?.root ? { root: opts.root } : {}) };
|
|
129
|
+
const client = guardPolarClient(polarClientOver(execute), polarBudgetOf({ ...opts, budgetOptions: budget }));
|
|
130
|
+
return syncPolarFromReal(client, { ...opts, budgetOptions: budget });
|
|
131
|
+
}
|
|
132
|
+
/** The perform adapter: one entry crosses to Polar through the executor; a customer or product it names by a local id resolves first. */
|
|
133
|
+
export async function performPolarAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome> {
|
|
134
|
+
const fields = { ...(action.fields ?? {}) };
|
|
135
|
+
for (const [field, type] of [['customer_id', 'customer'], ['product_id', 'product']] as const) {
|
|
136
|
+
if (typeof fields[field] === 'string') fields[field] = ctx.resolve(type, kid(type, String(fields[field]).replace(new RegExp(`^${type}:`), ''))).replace(new RegExp(`^${type}:`), '');
|
|
137
|
+
}
|
|
138
|
+
const budget = { budgetOptions: { ...(ctx.root !== undefined ? { root: ctx.root } : {}) } };
|
|
139
|
+
const { externalId } = await pushPolarAction(guardPolarClient(polarClientOver(execute), polarBudgetOf(budget)), { operation: action.operation, subject: action.subject, fields }, budget);
|
|
140
|
+
return { externalId };
|
|
106
141
|
}
|
|
107
142
|
|
|
108
143
|
// ── PUSH ──────────────────────────────────────────────────────────────────────────
|
|
@@ -133,23 +168,5 @@ export async function pushPolarAction(
|
|
|
133
168
|
* Push the twin's PENDING local actions to real Polar and CONFIRM each. A confirmed action is
|
|
134
169
|
* no longer pending, so a re-push enacts NOTHING. Unpushable ops are skipped.
|
|
135
170
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
opts: { root?: string; occurredAt?: string } & PolarBudgetedOptions = {},
|
|
139
|
-
): Promise<{ pushed: number; confirmed: string[]; externalIds: Record<string, string> }> {
|
|
140
|
-
// Guard here as well as in pushPolarAction: guarding is idempotent, so the inner call is a no-op
|
|
141
|
-
// and the ledger/clock the caller chose is what the whole loop accounts against.
|
|
142
|
-
const client = guardPolarClient(rawClient, polarBudgetOf(opts));
|
|
143
|
-
const occurredAt = opts.occurredAt ?? new Date().toISOString();
|
|
144
|
-
const confirmed: string[] = [];
|
|
145
|
-
const externalIds: Record<string, string> = {};
|
|
146
|
-
for (const action of pendingActions(SERVICE, opts.root)) {
|
|
147
|
-
const op = action.operation ?? `${action.subject.type}.update`;
|
|
148
|
-
if (!PUSHABLE.has(op)) continue;
|
|
149
|
-
const { externalId } = await pushPolarAction(client, action, polarBudgetOf(opts));
|
|
150
|
-
confirmAction({ service: SERVICE, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
|
|
151
|
-
confirmed.push(action.id);
|
|
152
|
-
externalIds[action.id] = externalId;
|
|
153
|
-
}
|
|
154
|
-
return { pushed: confirmed.length, confirmed, externalIds };
|
|
155
|
-
}
|
|
171
|
+
// protocol 2: the pending-actions loop is the kernel's (the head performs each entry through `performPolarAction`);
|
|
172
|
+
// the v1 `pushPendingPolarActions` is gone with the log it read.
|
package/src/polar-server.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// kernel's ONE adaptation (`createTwinFetchFromHandler`); this file contributes only VALUES. The
|
|
8
8
|
// vendor's 204 deletes return `{ status: 204, body: null }`, which the adapter's null-body rule
|
|
9
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';
|
|
10
11
|
import { handlePolarTwinRequest } from './polar-twin.ts';
|
|
11
|
-
import { createTwinFetchFromHandler, statefulTwinManifest } from '@volter/
|
|
12
|
+
import { createTwinFetchFromHandler, statefulTwinManifest, twinPublicBase } from '@volter/world-core';
|
|
12
13
|
|
|
13
14
|
/** Options every Polar-twin HTTP surface needs, independent of who owns the socket. */
|
|
14
15
|
export interface PolarTwinFetchOptions {
|
|
@@ -17,14 +18,37 @@ export interface PolarTwinFetchOptions {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function createPolarTwinFetch(options: PolarTwinFetchOptions = {}): (request: Request) => Promise<Response> {
|
|
20
|
-
|
|
21
|
+
const api = createTwinFetchFromHandler(handlePolarTwinRequest, {
|
|
21
22
|
...options,
|
|
22
23
|
manifest: statefulTwinManifest({ vendor: 'polar', twinOf: 'the Polar billing API', stores: 'customers, checkouts, subscriptions, meters and usage events' }),
|
|
24
|
+
// where this twin is reached (origin plus any served-World mount path): the checkout's hosted page is minted there
|
|
25
|
+
extras: (request) => ({ publicBase: twinPublicBase(request) }),
|
|
23
26
|
});
|
|
27
|
+
// the hosted checkout page, at the twin's own address: what a person sees after "continue to checkout at
|
|
28
|
+
// Polar" in a walk — the product, the amount, a Pay button that confirms the checkout and returns to the
|
|
29
|
+
// app's success_url (in production this page is Polar's; the twin stands in for it, plainly labelled)
|
|
30
|
+
return async function polarFetch(request: Request): Promise<Response> {
|
|
31
|
+
const url = new URL(request.url);
|
|
32
|
+
const page = /^\/twin\/checkout\/([a-zA-Z0-9-]+)(\/pay)?$/.exec(url.pathname);
|
|
33
|
+
if (!page) return api(request);
|
|
34
|
+
const id = page[1]!;
|
|
35
|
+
const read = await api(new Request(`${url.origin}/v1/checkouts/${id}`, { headers: { authorization: request.headers.get('authorization') ?? 'Bearer polar_oat_page' } }));
|
|
36
|
+
if (!read.ok) return new Response('no such checkout', { status: 404 });
|
|
37
|
+
const c = (await read.json()) as { status: string; amount: number; currency: string; success_url: string | null; product?: { name?: string } | null; customer_email?: string | null };
|
|
38
|
+
if (page[2] && request.method === 'POST') {
|
|
39
|
+
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: '{}' }));
|
|
40
|
+
if (!done.ok) return new Response(`the checkout could not be confirmed (${done.status})`, { status: 502 });
|
|
41
|
+
return new Response(null, { status: 303, headers: { location: c.success_url ?? `${twinPublicBase(request)}/twin/checkout/${id}` } });
|
|
42
|
+
}
|
|
43
|
+
const money = `$${(Number(c.amount ?? 0) / 100).toFixed(2)} ${String(c.currency ?? 'usd').toUpperCase()}`;
|
|
44
|
+
const esc = (v: unknown): string => String(v ?? '').replace(/[&<>"]/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[ch]!);
|
|
45
|
+
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>`;
|
|
46
|
+
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
47
|
+
};
|
|
24
48
|
}
|
|
25
49
|
|
|
26
|
-
export function createPolarTwinServer(options: { root?: string; port?: number; readOnly?: boolean } = {}): { port: number; stop: () => void } {
|
|
27
|
-
const server =
|
|
50
|
+
export async function createPolarTwinServer(options: { root?: string; port?: number; readOnly?: boolean } = {}): Promise<{ port: number; stop: () => void }> {
|
|
51
|
+
const server = await serveHttp({
|
|
28
52
|
port: options.port ?? 0,
|
|
29
53
|
idleTimeout: 60,
|
|
30
54
|
fetch: createPolarTwinFetch(options),
|