@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 +224 -0
- package/dist/chunk-Bp6m_JJh.js +13 -0
- package/dist/index.d.ts +1821 -0
- package/dist/index.js +4 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +86 -0
- package/dist/main.js.map +1 -0
- package/dist/manifest-B9GtCBa3.js +75 -0
- package/dist/manifest-B9GtCBa3.js.map +1 -0
- package/dist/manifest-CcMsQBSa.d.ts +37 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +3 -0
- package/dist/src-CmGCpsxx.js +634 -0
- package/dist/src-CmGCpsxx.js.map +1 -0
- package/package.json +46 -7
- package/src/api.ts +219 -0
- package/src/connector.ts +256 -0
- package/src/constants.ts +8 -0
- package/src/index.ts +24 -0
- package/src/main.ts +22 -0
- package/src/manifest.ts +65 -0
- package/src/sandbox.ts +38 -0
- package/src/schemas.ts +338 -0
- package/src/start.ts +79 -0
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 };
|