Skip to content

feat(sdk): custom function sync helpers (DH crypto, Sombra session exchange) - #321

Open
ecton-transcend wants to merge 3 commits into
mainfrom
ecton-custom-functions-sdk
Open

feat(sdk): custom function sync helpers (DH crypto, Sombra session exchange)#321
ecton-transcend wants to merge 3 commits into
mainfrom
ecton-custom-functions-sdk

Conversation

@ecton-transcend

@ecton-transcend ecton-transcend commented Jul 20, 2026

Copy link
Copy Markdown

Related Issues

Internal Changelog

  • Added: custom-functions module in @transcend-io/sdk — a customer-ingress code signing helper (signCustomFunctionCode, calling Sombra's new /v1/custom/sign route with bearer authentication), a Sombra gateway URL resolver (resolveSombraCustomerUrl), typed GraphQL documents, and fetch/diff/sync helpers for custom functions.

Part 1 of 3. The CLI commands consuming these helpers (transcend custom-functions push / list) land in the stacked follow-up PR #322, and the composite GitHub Action in #323.

Implementation details

How a sync works

  1. syncCustomFunction sends the plaintext code + execution context (allowed hosts, timeout, env vars) to the Sombra customer-ingress /v1/custom/sign route, authenticated by the Transcend API key bearer header (plus the X-Sombra-Authorization internal key when self-hosting) via the existing createSombraGotInstance. Sombra returns HMAC-signed signedCodeJwt / signedCodeContextJwt.
  2. The pre-signed JWTs are saved via createCustomFunction / updateStandaloneCustomFunction (inside input), where the API key's ManageDataMap scope is enforced. New revisions land as drafts and are promoted via promoteCustomFunctionVersion unless promotion is disabled. updateStandaloneCustomFunction handles both GENERAL and DSR functions.
  3. Per the security review on the platform side (#46175), this replaces an earlier design that exchanged the API key for a Sombra employee session and encrypted payloads over a client-built Diffie-Hellman channel — the customer-ingress route is protected by the bearer token instead, and no DH crypto ships in the SDK.

Per-gateway signing

Each custom function belongs to a single Sombra gateway whose keys sign its code and encrypt its env values — signing against any other gateway would produce JWTs that fail verification at execution time. resolveEffectiveSombraId resolves the gateway per function (config sombraId → existing function's gateway → caller default → primary), errors when a config tries to move an existing function between gateways, and the customFunctions query fetches each function's sombraId so updates always re-sign against the owning gateway.

Matching and change detection

  • Entries are matched by id first when set (rename-safe; a nonexistent ID fails rather than creating a duplicate), falling back to exact name. Ambiguous names (multiple functions sharing a name) fail with an error listing candidate IDs, since custom function names are not unique.
  • Change detection decodes the payloads of the signed code/context JWTs returned by the customFunctions query and diffs code, allowed hosts, timeout, imports flag, and env variable names. Env values are encrypted at sign time and cannot be diffed — callers can force a re-sign for env-value rotations.

Key files

  • Signing client: packages/sdk/src/custom-functions/signCustomFunctionCode.ts
  • Sync/diff logic: packages/sdk/src/custom-functions/syncCustomFunction.ts, codeSigning.ts
  • Gateway URL resolution: packages/sdk/src/custom-functions/resolveSombraCustomerUrl.ts

Dependencies / rollout

  • Requires the Sombra customer-ingress /v1/custom/sign route and the signedCodeJwt / signedCodeContextJwt GraphQL input fields from the backend pivot (#46175). A 404 from an older Sombra produces a friendly "upgrade your gateway" error.

Testing

  • 21 unit tests: the signing client against a stubbed customer-ingress (payload shape, 404 feature detection, error passthrough), JWT diff logic, ID/name resolution rules, and per-gateway resolution rules (config/existing/default precedence, cross-gateway move rejection).

Security Implications

  • Custom function code and env values travel over TLS directly to the customer's own Sombra gateway and never reach Transcend's backend in plaintext — only the HMAC-signed JWTs (with env values encrypted at sign time) are stored via the API.
  • The customer-ingress route is authenticated by the same bearer credentials as every other internal Sombra route (API key, plus the internal key when self-hosting) — the internal-key holder is the customer themselves.
  • Saving the signed JWTs requires an API key with the existing ManageDataMap scope — no new privilege surface in this package.

@ecton-transcend ecton-transcend self-assigned this Jul 20, 2026
@linear-code

linear-code Bot commented Jul 20, 2026

Copy link
Copy Markdown

WAL-9270

@pkg-pr-new

pkg-pr-new Bot commented Jul 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@transcend-io/airgap.js-types

pnpm add https://pkg.pr.new/@transcend-io/airgap.js-types@321
yarn add https://pkg.pr.new/@transcend-io/airgap.js-types@321.tgz

@transcend-io/cli

pnpm add https://pkg.pr.new/@transcend-io/cli@321
yarn add https://pkg.pr.new/@transcend-io/cli@321.tgz

@transcend-io/design-tokens

pnpm add https://pkg.pr.new/@transcend-io/design-tokens@321
yarn add https://pkg.pr.new/@transcend-io/design-tokens@321.tgz

@transcend-io/internationalization

pnpm add https://pkg.pr.new/@transcend-io/internationalization@321
yarn add https://pkg.pr.new/@transcend-io/internationalization@321.tgz

@transcend-io/privacy-types

pnpm add https://pkg.pr.new/@transcend-io/privacy-types@321
yarn add https://pkg.pr.new/@transcend-io/privacy-types@321.tgz

@transcend-io/sdk

pnpm add https://pkg.pr.new/@transcend-io/sdk@321
yarn add https://pkg.pr.new/@transcend-io/sdk@321.tgz

@transcend-io/type-utils

pnpm add https://pkg.pr.new/@transcend-io/type-utils@321
yarn add https://pkg.pr.new/@transcend-io/type-utils@321.tgz

@transcend-io/utils

pnpm add https://pkg.pr.new/@transcend-io/utils@321
yarn add https://pkg.pr.new/@transcend-io/utils@321.tgz

@transcend-io/mcp

pnpm add https://pkg.pr.new/@transcend-io/mcp@321
yarn add https://pkg.pr.new/@transcend-io/mcp@321.tgz

@transcend-io/mcp-server-admin

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-admin@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-admin@321.tgz

@transcend-io/mcp-server-assessment

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-assessment@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-assessment@321.tgz

@transcend-io/mcp-server-base

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-base@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-base@321.tgz

@transcend-io/mcp-server-consent

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-consent@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-consent@321.tgz

@transcend-io/mcp-server-discovery

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-discovery@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-discovery@321.tgz

@transcend-io/mcp-server-docs

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-docs@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-docs@321.tgz

@transcend-io/mcp-server-dsr

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-dsr@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-dsr@321.tgz

@transcend-io/mcp-server-inventory

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-inventory@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-inventory@321.tgz

@transcend-io/mcp-server-preferences

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-preferences@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-preferences@321.tgz

@transcend-io/mcp-server-workflows

pnpm add https://pkg.pr.new/@transcend-io/mcp-server-workflows@321
yarn add https://pkg.pr.new/@transcend-io/mcp-server-workflows@321.tgz

commit: 4dc2a10

@ecton-transcend
ecton-transcend marked this pull request as ready for review July 20, 2026 04:27

@michaelfarrell76 michaelfarrell76 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see https://github.com/transcend-io/main/pull/46175#pullrequestreview-4732313911

will need to resolve this first and build a new sombra endpoint

@ecton-transcend

Copy link
Copy Markdown
Author

🤖 Posted by Cursor on behalf of @ecton-transcend

Automated Change: Auth pivot to customer-ingress signing

Per security review feedback on the platform side (#46175), the signing flow no longer exchanges the API key for a Sombra employee session over a Diffie-Hellman channel. The CLI now signs code directly against the Sombra customer-ingress /v1/custom/sign route (bearer token auth) and saves the pre-signed JWTs via GraphQL.

Implementation details

What changed: All DH crypto and the session-exchange helper were removed from the SDK; a signCustomFunctionCode customer-ingress client and resolveSombraCustomerUrl helper were added; the GraphQL mutations now receive signedCodeJwt / signedCodeContextJwt inside input instead of dhEncrypted; the CLI gained --sombraAuth (Sombra internal key, needed when self-hosting) and the action a matching sombra-auth input.
Why: The employee-session exchange hard-coded SombraEmployeeAuthMethod.Transcend, which is not enabled on all self-hosted gateways; the customer-ingress bearer token is the established auth model for internal routes.
Impact: Same trust model at rest (backend stores unverifiable HMAC JWTs either way); code/env plaintext now travels only over TLS to the customer's own gateway. Requires a Sombra version with the new route — older gateways get a friendly upgrade error.

@ecton-transcend

Copy link
Copy Markdown
Author

🤖 Posted by Cursor on behalf of @ecton-transcend

Automated Change: Per-gateway code signing

Custom functions can belong to different Sombra gateways, and a function's JWTs must be signed by its own gateway's keys or they fail verification at execution time. Previously the push signed every function against a single gateway (the --sombraId flag or the primary), which could silently produce runtime-broken revisions for functions on other gateways.

Implementation details

What changed: The customFunctions query now fetches each function's sombraId; new resolveEffectiveSombraId resolves the signing gateway per function (manifest sombra-id → existing function's gateway → --sombraId → primary) and rejects configs that try to move an existing function between gateways; the CLI caches one customer-ingress connection per distinct gateway and signs each function against its own.
Why: Signing against the wrong gateway succeeds at push time (the backend cannot verify the HMAC JWTs) and only fails at execution — the worst failure mode.
Impact: Single-gateway orgs are unaffected (everything resolves to the primary). Multi-gateway manifests now work in one push, with the documented limitation that one --sombraAuth key applies to all gateways in a run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants