Skip to content

[FEAT] /platform module — own the SSO, hub-proxy, and billing glue apps currently hand-roll #22

@vutuanlinh2k2

Description

@vutuanlinh2k2

Summary

Every consumer app (gtm-agent, creative-agent, legal-agent) hand-rolls the same Tangle-platform integration glue: the cross-site SSO login flow, the integrations-hub proxy routes, and the billing HTTP/guard layer. agent-app should own this protocol behind typed seams — apps should configure it, not reimplement it.

Problem (audit findings)

A cross-repo audit of the three consumer apps found:

  • ~1,100 lines of platform glue in gtm-agent alone, of which roughly three-quarters is generic protocol any Tangle app needs verbatim (src/routes/auth.tangle.{start,callback}.ts, src/lib/.server/billing/platform.ts, src/lib/.server/auth-utils.ts, 5 near-identical api.integrations.hub.* proxy routes).
  • Three different answers to the same four problems:
    • SSO state signing: gtm = HMAC without TTL (node:crypto), legal = HMAC + timestamp + TTL (crypto.subtle), creative = unsigned random nonce with exact-match only.
    • Key storage: gtm = dedicated tangle_link table; legal/creative = sessions.tangleApiKey column.
    • Execution-key resolution: only gtm adopts resolveUserTangleExecutionKeyForUser from agent-app/runtime; creative re-implements it standalone; legal doesn't wire it at all.
    • Session creation / exchange error handling: near-identical copy-paste blocks in all three callbacks.
  • Near-identical proxy scaffolding repeated 5× in gtm (~35 lines each, ~90% identical).

The package already proves the right pattern elsewhere (WorkspaceKeyStore/KeyCrypto seams + preset-cloudflare reference impls); the SSO/hub/billing area just never got the same treatment.

Proposed approach

One new subpath export @tangle-network/agent-app/platform with four modules, structural seams only (no imports of agent-runtime, better-auth, drizzle, or consumer code — same rule as BrokerTokenMinter in src/tangle/index.ts):

  • platform/sso.ts — hardened signed state (WebCrypto HMAC-SHA256, TTL in the signed payload, constant-time compare) + createTangleSsoHandlers start/callback orchestration; TangleSsoAccountStore seam covers both the link-table and session-column storage styles.
  • platform/hub.tscreateHubProxyRoutes factory with the existing error contract preserved verbatim (412 tangle_link_required, hub-error status/code passthrough); structural error guards so bundler module duplication can't break instanceof.
  • platform/billing.ts — fetch-backed platform billing transport (user-bearer reads, service-token deduct), tier policy/state with fail-closed null-key handling, and a concrete implementation of /billing's PlatformBillingClient seam (type-only import).
  • platform/guards.ts — auth guard (302 page / 401 JSON), admin allowlist guard (404), assertBillableBalance (402 billing.balance_required) wired to isTangleBillingEnforcementDisabled.

Plus cookie helpers (serializeCookie / clearCookieHeader / readCookieValue) in /web.

Acceptance criteria

  • ./platform subpath ships the four modules above with vitest coverage (state round-trip/tamper/expiry, all handler error redirects, proxy route shapes + error mapping, billing header/body contracts, guard paths)
  • gtm-agent adopts it end-to-end with its existing behavior byte-compatible (cookie names, error codes, 412/402 semantics, tangle_link schema) — measured net deletion of its hand-rolled glue
  • No new runtime deps; agent-runtime stays an optional peer; ./platform not re-exported from the root barrel (collides with /billing's Platform* names)
  • Version bumped (0.5.0, additive) — publish via v0.5.0 tag after merge

Context

  • Implementation PR: feat(platform): SSO login handlers, hub proxy routes, and billing HTTP + guards #21 (branch feat/platform-glue)
  • gtm-agent adoption verified on a local branch (feat/platform-module-adoption): −453 net lines, 783 tests green, live smoke-tested against id.tangle.tools
  • Follow-ups (separate issues/PRs): adopt /platform in creative-agent (replaces its unsigned SSO state) and legal-agent (also needs its LLM path wired to the session-bound key)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

Status
Completed ✅

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions