@useairfoil/producer-polar 0.0.1 → 0.2.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 ADDED
@@ -0,0 +1,224 @@
1
+ # @useairfoil/producer-polar
2
+
3
+ Polar producer connector for Airfoil Connector Kit.
4
+
5
+ Current scope:
6
+
7
+ - entities: `customers`, `checkouts`, `orders`, `subscriptions`
8
+ - backfill source: Polar REST API
9
+ - live source: Polar webhooks on `/webhooks/polar`
10
+
11
+ ## Public Exports
12
+
13
+ - root: `PolarApiClient`, `PolarConnector`, entity schemas, webhook schema, and schema-derived types
14
+ - manifest subpath: `@useairfoil/producer-polar/manifest` exports browser-safe connector metadata
15
+
16
+ Typical imports:
17
+
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`.
28
+
29
+ ## Configuration
30
+
31
+ The sandbox reads connector values from the environment:
32
+
33
+ ```env
34
+ POLAR_ACCESS_TOKEN=polar_oat_xxx
35
+ POLAR_WEBHOOK_SECRET=polar_whs_xxx
36
+ ```
37
+
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`.
39
+
40
+ ```env
41
+ POLAR_API_BASE_URL=https://api.polar.sh/v1/
42
+ ```
43
+
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
+ ```
61
+
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
+ ```
78
+
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:
84
+
85
+ ```bash
86
+ pnpm --filter @useairfoil/producer-polar run sandbox
87
+ pnpm --filter @useairfoil/producer-polar run start
88
+ ```
89
+
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:
99
+
100
+ ```bash
101
+ pnpm nx run @useairfoil/producer-polar:docker:build
102
+ docker run --rm airfoil/producer-polar:local --help
103
+ ```
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.
@@ -0,0 +1,13 @@
1
+ //#region rolldown:runtime
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (all) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ return target;
10
+ };
11
+
12
+ //#endregion
13
+ export { __export as t };