← all branches

feat/enphase

needs attentionviewing older commit
9704c80 · fullPR #282reviewed 2026-07-09 23:52 UTC8H · 14M · 9L · 3I
The branch
Purpose
Port the Enphase 2-device legacy integration into the v2 metrics engine — replacing the secret write-back OAuth pattern with a DDB token store, and achieving billing-grade 15m native parity with the legacy DDB source.
Goal
Enphase Enlighten v4 integration — metrics worker (15m + 1d), single-use-refresh-token rotation via DDB (no write-back), CDK stacks, live-validated with exact parity
Sub-goals
  • SG-1: Probe + worker + token-rotation design + 47 unit tests
  • SG-2: CDK (AuthCache + Lambda stacks) + coordinator grant
  • SG-3: Preview data plane + sentinel grant-protection
  • SG-4: Live SFN validation + Tinybird + exact legacy parity
  • SG-N: Framework learnings fold-back (learnings #27-#32)
  • SG-6: Rebase onto feat/int-base (multi-gran engine)
  • SG-7: Multi-granularity worker (15m + 1d, 1M graceful skip, 53 tests)
The changes (whole branch)
What
New Enphase metrics worker (15m/1d, OAuth rotation via DDB), CDK stacks (auth-cache + lambda), 11 integration manifest stubs for future brand branches (abb, apsystems, fronius, hoymiles, huawei-portal, powerradar, shelly-cloud, sma, solark, solis, victron), migration scripts for legacy registry/secrets, enhancements to site-metrics transforms/compute for multi-gran support
Why
Legacy Enphase integration used secret write-back for OAuth rotation (brittle, IAM-write required). New design retires write-back: secret is read-only bootstrap, live token pair lives in DDB with CAS-protected rotation chain. Batch 3 port sibling of #280 (Victron), stacked on #268 (multi-gran engine).
Areas
services/metrics+313014scripts/metrics+17190packages/integration-manifests+10834infra/cdk+5131domains/metrics+59134packages/database+25714domains/cross-domain+838packages/api+82infra/tinybird+110domains/core+161
Blast
82 files, +7862/-213 across the enphase integration, CDK stacks, 11 manifest stubs, domains/metrics transforms, and scripts/metrics migration helpers
oauth-token-rotation single-use-refresh-tokens production-grant-safety billing-grade-data ddb-no-ttl-by-design
unit-tests· 47->53 tests (13 rotation state machine, 18 translation, 16+ handler); typecheck/lint greenlive-sfn-validation· 3 runs SUCCEEDED, 288 Tinybird rows, 0 dupes, exact parity 3/3 days (delta=0.000000)ci· No CI check results availablecoderabbit· No .coderabbit.yaml present

Findings · 21

correctness6

high

isInvalidGrantBody regex /refresh[ _]token/i too broad — false-positive dead-grant on benign 4xx bodies

services/metrics/integrations/enphase/src/engine/enlighten-client.ts:46

Matches any 4xx body mentioning 'refresh_token' as a field name or grant type — not only terminal invalid-grant responses. A mislabelled transient body triggers the CS re-authorize runbook.

high

rotationSpent=true after concurrent-rotation blocks valid 401-retry on adopted token

services/metrics/integrations/enphase/src/handlers/metrics.lambda.ts:195

Adopted token could already be stale; guard prevents a second rotation attempt, surfacing misleading UpstreamAuthFailed instead of attempting rotation.

high

1d MAX_WINDOW_DAYS=366 silently skips >366d backfill windows

services/metrics/integrations/enphase/src/handlers/metrics.lambda.ts:92

Lifetime endpoint serves all history (442d proven) in one call. The 366d cap applies 15m telemetry reasoning to an endpoint that needs no chunking.

medium

releaseLock unconditional — can stomp concurrent holder's lock on failure paths

services/metrics/integrations/enphase/src/lib/token-store.ts:85

No ConditionExpression — a crashed rotation can zero out a newly-acquired lock, opening a window for simultaneous rotation and potential double-spend.

medium

casWritePair does not verify lockUntil — stale writer can clobber in-flight lock holder

services/metrics/integrations/enphase/src/lib/token-store.ts:117

CAS guards on exchangedRefreshToken but not lock ownership; stale writer can succeed and zero lockUntil, breaking the new holder's rotation.

medium

projectLifetime wh/24 not calendar-aware — off by ~4% on DST transition days

services/metrics/integrations/enphase/src/translation/points-to-batu.ts:306

Spring-forward (23h) over-estimates by ~4.35%, fall-back (25h) under-estimates by ~4.17%. billing-grade data.

security5

high

refresh_token exposed as query param in OAuth fallback URL — log/proxy leak risk

services/metrics/integrations/enphase/src/engine/enlighten-client.ts:68

Second-attempt fallback builds URL with ?refresh_token=... . AWS X-Ray, proxy logs, or Node error messages can capture the full URL. Single-use token in URL is a higher-leak channel than POST body.

medium

IAM glob enphase/* covers portal human-login secrets

infra/cdk/src/stacks/services/metrics/integrations/enphase/iam.ts:45

Lambda can read portal credential secrets. Narrow to oid=* pattern.

medium

secretConfigArn not validated — arbitrary-ARN GetSecretValue injection risk

services/metrics/integrations/enphase/src/handlers/metrics.lambda.ts:184

ARN comes from Invocation payload with no whitelist check. Should validate against expected enphase ARN prefix.

medium

DDB token store has no customer-managed KMS encryption

infra/cdk/src/stacks/services/metrics/integrations/enphase/auth-cache.stack.ts:70

Table tagged confidential, holds live OAuth tokens, RETAIN in prod. AWS-owned key precludes KMS decrypt audit trail.

medium

casWritePair DDB exception leaves lock stuck for 90s — no finally-releaseLock

services/metrics/integrations/enphase/src/lib/token-manager.ts:212

Transient DDB errors propagate without releasing the lock. Add try/finally around casWritePair.

conventions4

high

shelly-cloud credentialFields (user/password) contradict JSDoc (api_key/access_token)

packages/integration-manifests/src/manifests/shelly-cloud.ts:21

One of them is wrong. Brand branch will fail D2 secret copy or runtime reads if fields don't match the real secret payload.

high

5m declared for unproven vendors — MetricSource.granularity is immutable once provisioned

packages/integration-manifests/src/manifests/apsystems.ts:18

fronius, huawei-portal, solark, solis also affected. A later probe finding no 5m support requires deactivate+recreate of sources.

medium

DDB errors throw rather than returning Result<T,E> — FCIS violation

services/metrics/integrations/enphase/src/lib/token-store.ts:80

tryAcquireLock and casWritePair re-throw DDB errors; callers are not in try/catch. Inconsistent with the Result pattern.

medium

SMA manifest missing 1d fallback — all other integrations with 5m declare 1d

packages/integration-manifests/src/manifests/sma.ts:47

Legacy SMA lambda had daily totals. Omitting 1d prevents daily-grain MetricSource creation.

tests4

high

DST fall-back (clock set back) not tested for localMidnightUtcMs

services/metrics/integrations/enphase/src/__tests__/points-to-batu.test.ts:280

Spring-forward covered, fall-back creates ambiguous local midnight. Two-pass algorithm behavior on fall-back is undocumented and untested.

high

Partial-serve guard not tested for grid channels (nested intervals path)

services/metrics/integrations/enphase/src/__tests__/metrics-handler.test.ts:427

Guard for grid uses .intervals.flat().at(-1).end_at — different from meter path. Regression in the nested-flat branch would go undetected.

medium

CAS-loss with no replacement pair (postRow.accessToken absent) not tested

services/metrics/integrations/enphase/src/__tests__/token-manager.test.ts:281

Existing CAS-loss test plants a winner row WITH an accessToken. The failure branch is untested.

medium

Post-lock concurrent-winner adoption in rotateAsClaimHolder not tested as distinct scenario

services/metrics/integrations/enphase/src/__tests__/token-manager.test.ts:179

Existing test covers PRE-lock re-read adoption. Post-lock adopt path is untested.

improvement2

medium

Timezone helpers copied verbatim from victron — extract to shared lib

services/metrics/integrations/enphase/src/translation/points-to-batu.ts:63

Will proliferate to every new brand branch. Extract to services/metrics/integrations/_shared/.

medium

No-op .map(inner => inner) in mergeTelemetryChunks — identity map

services/metrics/integrations/enphase/src/handlers/metrics.lambda.ts:421

gridChunks.flatMap(c => c.intervals ?? []) is equivalent and clearer.

History · 3 commits

  1. 27da24fneeds attentionincremental0H · 3M · 7L2026-07-10 00:23
  2. 9704c80needs attentionfull8H · 14M · 9L2026-07-09 23:52current
  3. f6e795dblockedfull4H · 14M · 12L2026-07-08 03:42