Skip to content

feat(sdk): custom function sync helpers - #321

Merged
ecton-transcend merged 14 commits into
mainfrom
ecton-custom-functions-sdk
Aug 13, 2026
Merged

feat(sdk): custom function sync helpers#321
ecton-transcend merged 14 commits into
mainfrom
ecton-custom-functions-sdk

Conversation

@ecton-transcend

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

Copy link
Copy Markdown
Contributor

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.

Test-before-promote (added in review)

  • Added: runCustomFunctionTest — test-runs freshly signed (unsaved) code via the runCustomFunction mutation using the pre-signed signedCodeJwt/signedCodeContextJwt pair as a test run (isCustomFunctionTestRun: true), returning the execution result (error, logs, exit code, timing) plus a passed boolean that mirrors the dashboard's pass logic (no error and exit code ≤ 0).
  • syncCustomFunction accepts optional testPayload / testPayloadType: when a payload is provided, the code is tested after signing and before any create/update. A failing test returns the new test-failed outcome with the full testResult attached, and nothing is pushed; passing runs continue and include testResult.
  • Requires backend support for pre-signed JWTs on runCustomFunction (main #47383). On an older backend the mutation rejects the JWT input fields and the helper fails with a friendly upgrade error (callers can skip testing).

Auto-created DSR integrations (added in review)

  • New DSR functions no longer require a dataSiloId: when a DSR config has no dataSiloId and no existing function matches, syncCustomFunction creates the DSR integration automatically — a customFunction-catalog data silo shell (createCustomFunctionDataSilo, titled after the function, on the entry's gateway or the org's primary Sombra), created before the test run because the backend resolves the execution gateway from the payload's extras.dataSilo.id.
  • On a passing test the function is created linked to the silo (flipping it to Connected); on a failing test the silo is rolled back (deleteDataSilo) and the sync returns test-failed with createdDataSilo: true.
  • All DSR test payloads get extras.dataSilo.{id,title} injected from the resolved silo (injectDataSiloIntoDsrTestPayload), so payload files never hardcode silo IDs.
  • Sync results now report dataSiloId / createdDataSilo, and the CUSTOM_FUNCTIONS query selects dataSiloId, so callers can write silo IDs back to their manifests.

Review follow-ups (Michael)

  • Enums — the custom function type / lifecycle-state / payload-type string unions moved to @transcend-io/privacy-types as CustomFunctionType, CustomFunctionLifecycleState, CustomFunctionVersionLifecycleState, and CustomFunctionPayloadType (wire values unchanged); the SDK and CLI now import them from there.
  • JWT decodingdecodeJwtPayload is now a thin wrapper over jsonwebtoken's jwt.decode (unsigned decode is intentional: change detection over JWTs fetched via the authenticated API).
  • Sombra query/URL dedup — deleted the one-off ORGANIZATION_SOMBRAS query and resolveSombraCustomerUrl; the existing ORGANIZATION query now selects sombra { id customerUrl } + sombras { id customerUrl }, and createSombraGotInstance gained a sombraId option to connect to a specific gateway. The CLI push flow passes sombraId directly instead of pre-resolving a URL.
  • File sizesyncCustomFunction.ts was split: resolveExistingCustomFunction.ts, resolveEffectiveSombraId.ts (incl. resolvePrimarySombraId), buildCustomFunctionSignPayload.ts, and injectDataSiloIntoDsrTestPayload.ts are now their own modules; the orchestrator and result types stay.

@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: 047a3ff

@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
Contributor 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
Contributor 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.

@ecton-transcend ecton-transcend changed the title feat(sdk): custom function sync helpers (DH crypto, Sombra session exchange) feat(sdk): custom function sync helpers Aug 12, 2026
@ecton-transcend
ecton-transcend requested a review from a team as a code owner August 12, 2026 20:35
Comment thread .github/workflows/ci.yml

- if: github.event_name == 'pull_request'
name: Check changeset
env:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hard coding the SHA causes long-open PRs to fail these check at the base_sha becomes stale. IMO safe to remove here as the fallback in code checks against main anyway.

If that was added for a specific reason I'm not aware of I'll add back in.

@ecton-transcend

Copy link
Copy Markdown
Contributor Author

@michaelfarrell76 This is Branch 1 of a 3 part stacked PR sequence to introduce custom function management to the CLI:

  1. SDK updates
  2. CLI updates
  3. Github actions package

Included in here the revised sombra-ingress route changes to avoid employee authentication sessions

@@ -0,0 +1,16 @@
import { gql } from 'graphql-request';

export const ORGANIZATION_SOMBRAS = gql`

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.

do we need this? isnt there an existing query for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — removed

return undefined;
}
try {
return JSON.parse(Buffer.from(segments[1], 'base64url').toString('utf-8')) as T;

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.

why not use the jsonwebtoken lib?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

/**
* The product-facing type of a custom function.
*/
export type CustomFunctionType = 'DSR' | 'GENERAL';

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.

nit: these should probably be enums in privacy-types

logger?: Logger;
} = {},
): Promise<string> {
const { logger = NOOP_LOGGER } = options;

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.

pretty sure a function for this exists already!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh yup, nice!

* `enricher` export. GENERAL functions are always run as Maestro payloads and
* ignore this setting.
*/
export type CustomFunctionTestPayloadType = 'DATA_POINT' | 'REQUEST_ENRICHER';

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.

nit: enum in privacy-types

});
}

return {

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.

nit: files a bit too big lets break it down

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, split the helpers up

@michaelfarrell76

Copy link
Copy Markdown
Member

@ecton-transcend terribly sorry for the delayed review!

@michaelfarrell76

Copy link
Copy Markdown
Member

@ecton-transcend ya youre gtg, i could approve but as soon as you push another commit it will clear out the approval since this is a public package. so would just address the comments and ask for re-approval from @giacaglia

this is close enough to merge but would do at least one round of cleanup befoer!

@ecton-transcend

Copy link
Copy Markdown
Contributor Author

@dmattia @dipack95 can I get a stamp from both of you on this? It's requiring code owners due to the change to ci.yml and privacy-types enum

let offset = 0;

let shouldContinue = false;
do {

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.

nit: do while isn't quite stylistically in line, would prefer either plain while or a for loop

draftVersion?: CustomFunctionVersionPreview | null;
}

const PAGE_SIZE = 20;

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.

could/should this be larger? what's the max page size if we're fetching all? what's the volume per org? do we want to always fetch all attributes on the CFs?

* functions with the same name) is an error that the caller should resolve by
* adding an `id` to the config.
*
* @param existing - All existing custom functions in the organization

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.

does anything ensure that this is all of them? don't see any calling code, wondering the usefulness of this?

} catch (err) {
const statusCode = (err as { response?: { statusCode?: number } })?.response?.statusCode;
if (statusCode === 404) {
throw new Error(

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.

do we have a NotFoundError or anything like that in the SDK?

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.

or i guess whatever nnot supported is? 401?

} = options;
const type: CustomFunctionType = input.type ?? CustomFunctionType.General;

const existing = resolveExistingCustomFunction(allExisting, input);

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.

oh this is where it's called, it was folded up by github

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.

should be in a helpers folder probs

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.

if we're just using the allExisting to see if one exists. Can we just call an api with the id/name to get it back from the backend? This seems like a heavy way to go about it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. It’s not just an existence check: we need the matched function’s signedCodeJwt / signedCodeContextJwt (to diff/skip), plus sombraId and dataSiloId.

The CLI fetches once per push and reuses that list across every manifest entry, so for a typical multi-function push one list call is cheaper than N lookups. Name matching also needs the full set so we can error when two functions share a name and the manifest has no id.

That said, customFunctions(filterBy: { id }) already exists on the backend, and for an id-keyed sync we could fetch just that one. Name-only still can’t be a clean “get by name” today: the API only has fuzzy text on name/description, not exact name. Happy to follow up with a targeted filterBy.id path if you want that cleaned up in this PR or a follow-up.

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.

yea I think if we're uniq-ing on name too then we should probably have a fetchByName api or a flag in the search filters for exact: true or something. I think it's significant enough that we shouldn't ship with a strategy of fetching all current functions to check for existence even though it's probably annoying to block on that?

if it's a multi-push as well you could check for conflicts within the set that you're pushing and then do a no-attribute fetch with filtered ids/names before pushing if you don't want to fail on submit. But yea overall pulling the full list in a loop is something we want to avoid for this

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.

signedCodeJwt / signedCodeContextJwt (to diff/skip), plus sombraId and dataSiloId.

should this happen in the backend?

@eajoyce eajoyce Aug 13, 2026

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.

so for a typical multi-function push one list call is cheaper than N lookups

potentially but it does also loop over the pages to fetch them all which could add up in a CI situation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The code is encrypted when fetched, so I think it's safe to pull down and run comparisons over what the user has locally. As the local code is encrypted at that point as well. Just checking if the hashes match.

I agree looping isn't the best mechanism here. I'll put together the preferred approach before merge!

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.

Yep no vuln concern :) just wanting to be a bit more optimal in how we hit typically pretty low-powered sombras lol. And maybe that is fetching, I'll approve and pass the ball back for your implementation 🙏

ty!!

const existingCode =
codePayload === undefined
? undefined
: Buffer.from(codePayload.base64Code, 'base64').toString('utf-8');

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.

naive question: is this just a node sdk or does it support browser too?

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.

also would this throw on empty string? or invalid base64?

* @param token - The JWT string
* @returns The decoded payload object, or undefined if the JWT is malformed
*/
export function decodeJwtPayload<T extends object>(token: string): T | undefined {

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.

nit: might not need to be own function, no real need to convert null to undefined here

logger,
});

const draft = updated.draftVersion;

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.

nit: mixing various explicit undefined and truthy checks

const draft = updated.draftVersion;
if (!draft) {
throw new Error(
`Expected a draft version to be created for custom function "${input.name}" but none was returned.`,

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.

error could be more specific for an sdk, maybe throw earlier for more specific issues

...(input.allowThirdPartyImports !== undefined
? { allowThirdPartyImports: input.allowThirdPartyImports }
: {}),
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),

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.

I think this could just be a default object with a spread on the input

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.

export function buildCustomFunctionSignPayload({
  code,
  context: contextOverrides,
}: CustomFunctionConfigInput): CustomFunctionSignPayload {
  return {
    code,
    context: {
      userDefinedEnv: {},
      allowedHosts: [],
      ...contextOverrides,
    },
  };
}

@eajoyce

eajoyce commented Aug 13, 2026

Copy link
Copy Markdown
Member

The PR description notes "never in plaintext" for the code, but also states that just the env vars are encrypted. Are we expecting the code itself to also be encrypted? Being signed into a token is just it being signed which could be fine but I want to make sure I understand the description note on that one.

@ecton-transcend

Copy link
Copy Markdown
Contributor Author

The PR description notes "never in plaintext" for the code, but also states that just the env vars are encrypted. Are we expecting the code itself to also be encrypted? Being signed into a token is just it being signed which could be fine but I want to make sure I understand the description note on that one.

The code bundles are passed into sombra via a custom-ingress route for encryption before being sent to the backend. So that comment is more saying the code/vars never reach transcend in plaintext. Which is the same security position we hold for AD implementation of the same feaures!

@eajoyce eajoyce 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.

had a lingering thread about checking existing CFs, discussed options and leaving to ecton to take it forward. ty!

@ecton-transcend
ecton-transcend added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit 98eeb1d Aug 13, 2026
4 checks passed
@ecton-transcend
ecton-transcend deleted the ecton-custom-functions-sdk branch August 13, 2026 18:35
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.

5 participants