@useairfoil/producer-polar 0.2.2 → 0.3.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.
Files changed (48) hide show
  1. package/README.md +43 -202
  2. package/dist/index.d.mts +4449 -0
  3. package/dist/index.mjs +3 -0
  4. package/dist/main.d.mts +1 -0
  5. package/dist/main.mjs +616 -0
  6. package/dist/main.mjs.map +1 -0
  7. package/dist/manifest.d.mts +38 -0
  8. package/dist/{manifest-B9GtCBa3.js → manifest.mjs} +15 -31
  9. package/dist/manifest.mjs.map +1 -0
  10. package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
  11. package/dist/src-6sB3UUzt.mjs +807 -0
  12. package/dist/src-6sB3UUzt.mjs.map +1 -0
  13. package/package.json +19 -19
  14. package/src/api.ts +83 -136
  15. package/src/connector.ts +143 -86
  16. package/src/create-table.ts +5 -0
  17. package/src/demo/customers.ts +385 -0
  18. package/src/demo/index.ts +37 -0
  19. package/src/demo/setup.ts +79 -0
  20. package/src/demo/shared.ts +70 -0
  21. package/src/index.ts +18 -21
  22. package/src/main.ts +8 -8
  23. package/src/manifest.ts +3 -29
  24. package/src/resources/checkouts.ts +99 -0
  25. package/src/resources/customers.ts +53 -0
  26. package/src/resources/discounts.ts +70 -0
  27. package/src/resources/orders.ts +94 -0
  28. package/src/resources/products.ts +143 -0
  29. package/src/resources/refunds.ts +48 -0
  30. package/src/resources/shared.ts +88 -0
  31. package/src/resources/subscriptions.ts +91 -0
  32. package/src/sandbox.ts +11 -10
  33. package/src/start.ts +5 -37
  34. package/src/tables.ts +19 -0
  35. package/src/webhooks.ts +121 -0
  36. package/dist/index.d.ts +0 -1821
  37. package/dist/index.js +0 -4
  38. package/dist/main.d.ts +0 -1
  39. package/dist/main.js +0 -86
  40. package/dist/main.js.map +0 -1
  41. package/dist/manifest-B9GtCBa3.js.map +0 -1
  42. package/dist/manifest-CcMsQBSa.d.ts +0 -37
  43. package/dist/manifest.d.ts +0 -2
  44. package/dist/manifest.js +0 -3
  45. package/dist/src-CmGCpsxx.js +0 -634
  46. package/dist/src-CmGCpsxx.js.map +0 -1
  47. package/src/constants.ts +0 -8
  48. package/src/schemas.ts +0 -338
package/README.md CHANGED
@@ -1,224 +1,65 @@
1
- # @useairfoil/producer-polar
1
+ # Polar producer
2
2
 
3
- Polar producer connector for Airfoil Connector Kit.
3
+ This connector backfills Polar customers, checkouts, orders, subscriptions,
4
+ refunds, products, and discounts.
5
+ It gets live events at `POST /webhooks/polar`.
4
6
 
5
- Current scope:
7
+ ## Config
6
8
 
7
- - entities: `customers`, `checkouts`, `orders`, `subscriptions`
8
- - backfill source: Polar REST API
9
- - live source: Polar webhooks on `/webhooks/polar`
9
+ | Variable | Required | Default |
10
+ | ----------------------- | ----------- | ------- |
11
+ | `POLAR_ACCESS_TOKEN` | yes | none |
12
+ | `POLAR_WEBHOOK_SECRET` | yes | none |
13
+ | `POLAR_API_BASE_URL` | hosted only | none |
14
+ | `POLAR_ORGANIZATION_ID` | no | none |
10
15
 
11
- ## Public Exports
16
+ ## Setup
12
17
 
13
- - root: `PolarApiClient`, `PolarConnector`, entity schemas, webhook schema, and schema-derived types
14
- - manifest subpath: `@useairfoil/producer-polar/manifest` exports browser-safe connector metadata
18
+ Create a [Polar organization access token](https://docs.polar.sh/integrate/oat)
19
+ with `customers:read`, `checkouts:read`, `orders:read`, `subscriptions:read`,
20
+ `refunds:read`, `products:read`, and `discounts:read`. Use it as
21
+ `POLAR_ACCESS_TOKEN`.
15
22
 
16
- Typical imports:
23
+ Create a webhook endpoint for `/webhooks/polar` and use its signing secret as
24
+ `POLAR_WEBHOOK_SECRET`. Sandbox tokens and webhooks must come from the Polar
25
+ sandbox. Subscribe it to the `customer.*`, `checkout.*`, `order.*`,
26
+ `subscription.*`, `refund.*`, `product.*`, and `discount.*` events. The
27
+ connector acknowledges other events without storing them.
17
28
 
18
- ```ts
19
- import { CustomerSchema, PolarConnector, type Customer } from "@useairfoil/producer-polar";
20
- import { PolarApiClient, WebhookPayloadSchema } from "@useairfoil/producer-polar";
21
- ```
22
-
23
- Connector config and runtime types are exported from the `PolarConnector` namespace.
24
-
25
- ## ConnectorApp Shape
26
-
27
- `PolarConnector` is a typed Effect service with `layer(config)` and `layerConfig(config)`. Runtime and dashboard validation both use `layerConfig(PolarConfigDef.config)`. Local sandbox runs use `layerConfig(...)` with a `Config.succeed("https://sandbox-api.polar.sh/v1/")` override for `apiBaseUrl`.
29
+ ## API version
28
30
 
29
- ## Configuration
31
+ The connector pins Polar API version `2026-10` with the `Polar-Version` header.
32
+ Set the webhook endpoint API version to `2026-10`.
30
33
 
31
- The sandbox reads connector values from the environment:
34
+ ## Limits
32
35
 
33
- ```env
34
- POLAR_ACCESS_TOKEN=polar_oat_xxx
35
- POLAR_WEBHOOK_SECRET=polar_whs_xxx
36
- ```
36
+ Deleted products stay in the `products` table because Polar has no
37
+ `product.deleted` webhook. Subscriptions without `started_at` can be missed
38
+ during a backfill because Polar has no stable sort for them. After backfill,
39
+ updates depend on webhooks. Missed webhook deliveries are not recovered yet.
37
40
 
38
- The sandbox injects the Polar sandbox API URL. Hosted `start` instead requires a read-only connector JSON file selected by `AIRFOIL_CONFIG_PATH`; matching environment values override file values per key. The file includes manifest runtime keys such as `POLAR_ACCESS_TOKEN` and `POLAR_API_BASE_URL`.
41
+ ## Demo data
39
42
 
40
- ```env
41
- POLAR_API_BASE_URL=https://api.polar.sh/v1/
42
- ```
43
+ `pnpm demo --size 128MiB` writes made-up Polar data without a Polar token. See
44
+ the Connector Kit README for the flags and the variables it needs.
43
45
 
44
- Optional:
45
-
46
- ```env
47
- POLAR_ORGANIZATION_ID=org_xxx
48
- # Defaults to 100 for sandbox and 500 for production.
49
- # POLAR_RATE_LIMIT_PER_MINUTE=500
50
- # POLAR_TRANSIENT_MAX_RETRIES=5
51
- # POLAR_RETRY_BASE_DELAY_MS=200
52
- # POLAR_REQUEST_TIMEOUT_SECONDS=120
53
- POLAR_WEBHOOK_PORT=8080
54
- OTEL_ENABLED=false
55
- OTEL_SERVICE_NAME=producer-polar
56
- # OTEL_SERVICE_VERSION=0.1.0
57
- # OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,team=data
58
- OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
59
- OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer token,X-Axiom-Dataset=airfoil-traces
60
- ```
46
+ Each customer has checkouts, subscriptions, orders, and refunds that point to
47
+ each other and to a fixed catalog of 6 products and 3 discounts. `products` and
48
+ `discounts` don't grow with the size.
61
49
 
62
- The API policy is loaded with the rest of the connector config. It is applied per connector process, so separate instances using the same Polar token do not share one request budget.
63
-
64
- Production `start` also requires platform-owned Wings, table, and PostgreSQL state config:
65
-
66
- ```env
67
- WINGS_HOST=localhost:7777
68
- WINGS_NAMESPACE=namespaces/default
69
- POLAR_CUSTOMERS_TABLE=namespaces/default/tables/polar-customers
70
- POLAR_CHECKOUTS_TABLE=namespaces/default/tables/polar-checkouts
71
- POLAR_ORDERS_TABLE=namespaces/default/tables/polar-orders
72
- POLAR_SUBSCRIPTIONS_TABLE=namespaces/default/tables/polar-subscriptions
73
- AIRFOIL_CONFIG_PATH=/var/run/airfoil/config/config.json
74
- AIRFOIL_CONNECTOR_INSTANCE_ID=team-acme-polar-primary
75
- # AIRFOIL_STATE_TABLE=_airfoil_connectors_state
76
- POSTGRES_CONNECTION_STRING=postgresql://...
77
- ```
50
+ ## Local development
78
51
 
79
- The sandbox uses `Telemetry.layerOtlp()` and `Telemetry.layerMetricsConsoleDump()` from Connector Kit. Connector Kit reads `OTEL_ENABLED`, `OTEL_EXPORTER_OTLP_ENDPOINT`, and `OTEL_EXPORTER_OTLP_HEADERS` for OTLP trace/metric export. Effect reads `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, and `OTEL_RESOURCE_ATTRIBUTES` for resource metadata. HTTP runtimes expose shallow process health at `GET /health`, metrics at `GET /metrics`, and durable progress or source errors at `GET /status`.
80
-
81
- ## ConnectorApp Entrypoint
82
-
83
- The package scripts run the connector CLI from source:
52
+ The sandbox uses the Polar sandbox API.
84
53
 
85
54
  ```bash
86
- pnpm --filter @useairfoil/producer-polar run sandbox
87
- pnpm --filter @useairfoil/producer-polar run start
55
+ cd connectors/producer-polar
56
+ pnpm sandbox
88
57
  ```
89
58
 
90
- `sandbox` runs the real connector against Polar sandbox with `Publisher.layerConsole`. `start` runs against the configured `POLAR_API_BASE_URL` and passes the configured Wings table names to `Publisher.layerWings`.
91
-
92
- The CLI assembly lives in `src/main.ts`; connector-specific platform keys live in `src/constants.ts`; production runtime wiring lives in `src/start.ts`; sandbox runtime wiring lives in `src/sandbox.ts`.
93
-
94
- Before provisioning, the dashboard backend passes `PolarConnector.PolarConnector` and `PolarConnector.layerConfig(PolarConnector.PolarConfigDef.config)` to `ConnectorApp.check(...)`. Each selected entity performs a read-only one-item request against its corresponding Polar list endpoint; unselected entities are not contacted.
95
-
96
- ## Production Image
97
-
98
- The build emits a separate `dist/main.js` CLI while keeping it outside the package exports. Build and smoke-test the non-root Node 24 image through Nx:
59
+ A hosted run loads connector config from `AIRFOIL_CONFIG_PATH`. It also needs
60
+ the Wings and PostgreSQL variables from the Connector Kit README.
99
61
 
100
62
  ```bash
101
- pnpm nx run @useairfoil/producer-polar:docker:build
102
- docker run --rm airfoil/producer-polar:local --help
63
+ cd connectors/producer-polar
64
+ pnpm start
103
65
  ```
104
-
105
- The image runs `node dist/main.js start`, exposes port `8080`, and contains only the pruned production package. Mount the complete connector JSON file read-only at the path supplied by `AIRFOIL_CONFIG_PATH`; inject the platform-owned values listed above separately. Never bake `.env`, access tokens, webhook secrets, or database credentials into the image. The operator owns Kubernetes readiness and liveness probes against `/health`.
106
-
107
- ## Minimal ConnectorApp Wiring
108
-
109
- The following is local/example wiring. Hosted production uses `RuntimeConfig.layerHosted()`, `StateStore.layerSql()` over `PgClient`, and the Wings publisher as shown by `src/start.ts`; it must not fall back to memory state.
110
-
111
- ```ts
112
- import { Publisher, ConnectorApp, StateStore, Telemetry } from "@useairfoil/connector-kit";
113
- import { Effect, Layer } from "effect";
114
- import { FetchHttpClient } from "effect/unstable/http";
115
-
116
- import { PolarConnector } from "@useairfoil/producer-polar";
117
-
118
- const BootstrapLayer = FetchHttpClient.layer;
119
-
120
- const ConnectorLayer = PolarConnector.layerConfig(PolarConnector.PolarConfigDef.config).pipe(
121
- Layer.provide(BootstrapLayer),
122
- );
123
- const TelemetryLayer = Telemetry.layerOtlp().pipe(Layer.provide(BootstrapLayer));
124
-
125
- const program = Effect.gen(function* () {
126
- const connector = yield* PolarConnector.PolarConnector;
127
- return yield* ConnectorApp.start(connector, { port: 8080 });
128
- });
129
-
130
- const RuntimeLayer = Layer.mergeAll(
131
- StateStore.layerMemory,
132
- Publisher.layerConsole,
133
- ConnectorLayer,
134
- TelemetryLayer,
135
- );
136
-
137
- const runnable = Effect.scoped(program).pipe(Effect.provide(RuntimeLayer));
138
-
139
- Effect.runPromise(runnable);
140
- ```
141
-
142
- ## Webhook Behavior
143
-
144
- - webhook path: `POST /webhooks/polar`
145
- - route payloads are schema-validated through `Webhook.route(...)`
146
- - `POLAR_WEBHOOK_SECRET` is required and every signature is verified against the raw request body
147
- - webhook upserts use the event timestamp as their Wings version
148
- - `customer.deleted` emits a delete mutation
149
- - subscription pause and resume events are supported
150
-
151
- ## API Client Layer
152
-
153
- `PolarApiClient.layer(config)` builds `PolarApiClient.PolarApiClient` from a raw `PolarConfig` value.
154
-
155
- This is useful for focused API tests or custom runtimes that do not need the full connector service.
156
-
157
- ```ts
158
- import { Effect, Layer, Option, Redacted, Schema } from "effect";
159
- import { FetchHttpClient } from "effect/unstable/http";
160
-
161
- import { PolarApiClient } from "@useairfoil/producer-polar";
162
-
163
- const apiLayer = PolarApiClient.layer({
164
- accessToken: Redacted.make("test"),
165
- apiBaseUrl: "https://sandbox-api.polar.sh/v1/",
166
- organizationId: Option.none(),
167
- rateLimitPerMinute: Option.none(),
168
- transientMaxRetries: 5,
169
- retryBaseDelayMs: 200,
170
- requestTimeoutSeconds: 120,
171
- webhookSecret: Redacted.make("test-webhook-secret"),
172
- }).pipe(Layer.provide(FetchHttpClient.layer));
173
-
174
- const program = PolarApiClient.PolarApiClient.use((api) =>
175
- api.fetchList(Schema.Any, "customers/", {
176
- page: 1,
177
- limit: 100,
178
- sorting: "-created_at",
179
- }),
180
- ).pipe(Effect.provide(apiLayer));
181
-
182
- Effect.runPromise(program);
183
- ```
184
-
185
- ## Development Notes
186
-
187
- - Polar entity streams combine live webhook events with paginated backfill
188
- - backfill is bounded by the cutoff established from live webhooks or the initial runtime cutoff
189
- - backfill pagination stays on `created_at`; decoded rows use `modified_at ?? created_at` as their version
190
- - incoming events outside the current connector scope are ignored
191
-
192
- ## Testing
193
-
194
- - `test/api.vcr.test.ts`: VCR-backed API replay against a recorded cassette
195
- - `test/check.test.ts`, `test/schemas.test.ts`, and `test/rate-limit.test.ts`: config checks, provider schemas, and request policy
196
- - `test/webhook.test.ts`: in-memory webhook round-trip using `NodeHttpServer.layerTest`
197
-
198
- Run:
199
-
200
- ```bash
201
- pnpm --filter @useairfoil/producer-polar run test:ci
202
- ```
203
-
204
- ## Sandbox Tracing
205
-
206
- Set `OTEL_ENABLED=true` to export traces and metrics from the sandbox. Metrics are also logged locally by `Telemetry.layerMetricsConsoleDump()`.
207
-
208
- The sandbox uses `Telemetry.layerOtlp()` with the default Connector Kit sensitive-header redaction. See `@useairfoil/connector-kit` for the full telemetry env var list, metric names, and redaction defaults.
209
-
210
- For local Jaeger with persistent storage, start it from the traceview package:
211
-
212
- ```bash
213
- pnpm --filter @useairfoil/traceview run jaeger:up
214
- ```
215
-
216
- Then set `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318` and run the sandbox. After triggering a webhook or backfill, render the trace:
217
-
218
- ```bash
219
- traceview <trace-id> --source jaeger
220
- # or for Axiom:
221
- traceview <trace-id> --source axiom
222
- ```
223
-
224
- Webhook traces may include an external parent span from Polar's `traceparent` header — that parent is expected to be missing from your dataset unless Polar also exports there.