mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #161] [MERGED] feat!: resolve COMFY_API_KEY in new Comfy(), matching the Python client #163
Labels
No labels
audit:ai-readiness
audit:community
audit:github
audit:health-scoring
audit:policy
audit:release
audit:supply-chain
bug
cursor-review-pin
effort:medium
effort:small
effort:trivial
priority:nice-to-have
priority:recommended
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-typescript-sdk#163
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-typescript-sdk/pull/161
Author: @mattmillerai
Created: 9/19/2026
Status: ✅ Merged
Merged: 10/9/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-15897-client-api-key-env📝 Commits (10+)
04ffa1afeat!: resolve COMFY_API_KEY in new Comfy(), matching the Python clientac30b28docs(changelog): order the Unreleased sections Added before Changed466bcd7fix(sdk): strip every trailing slash when matching the Comfy Cloud base URL9529278Merge origin/main into matt/be-15897-client-api-key-env72b5fbetest(parity): find the credential tuple's close by depth, and derive the winner from the declared order3895303fix(sdk): normalize COMFY_BASE_URL's trailing slashes, and bind the parity raise to its cloud guard93b8090fix(sdk): reject a bare ? or # in COMFY_BASE_URL, and harden the credential extractor0ab5996test(parity): only a cloud guard at the resolver's own indentation countsbd3da1bMerge remote-tracking branch 'origin/main' into matt/be-15897-client-api-key-env22e86fcdocs(parity): name the discovery-methods lead as what blocks the snapshot refresh📊 Changes
12 files changed (+1063 additions, -33 deletions)
View changed files
📝
CHANGELOG.md(+22 -0)📝
README.md(+47 -8)📝
parity/README.md(+8 -2)📝
parity/python-surface.json(+13 -1)📝
scripts/python-surface.mjs(+243 -1)📝
src/index.test.ts(+3 -2)➕
src/sdk/apiKeyEnv.test.ts(+195 -0)📝
src/sdk/baseUrlEnv.test.ts(+38 -3)📝
src/sdk/client.test.ts(+5 -1)📝
src/sdk/client.ts(+137 -6)📝
src/sdk/credentials.ts(+8 -3)📝
src/sdk/surface-parity.test.ts(+344 -6)📄 Description
ELI-5
new Comfy()did not readCOMFY_API_KEY, butcomfy.models.*did. So the same environment variable configured half the SDK, and the other half sent noAuthorizationheader at all and came back with a bare401from the server. The README had to ship a workaround for it. Now both halves read the same variable, in the same order the Python client uses, and a missing key against Comfy Cloud is a clear local error thrown at construction instead of a server 401 on your first call.What changed
src/sdk/client.tsgainedresolveApiKey(explicit, baseUrl), mirroringcomfy_sdk's_resolve_api_key:options.apiKeywhen it is a non-blank string.COMFY_API_KEY, trimmed — blank counts as unset (soCOMFY_API_KEY=in a shell profile is not an error), and a runtime with noprocesssimply never sees it. Guarded exactly the wayresolveCredentials()guards it.MissingCredentials, thrown at construction when the resolved base URL is Comfy Cloud, naming both ways to supply one and how to target a deployment that needs none. WhenCOMFY_BASE_URLnames another deployment and neither source holds a key, resolution isundefinedand no credential is sent, so the keyless local-dev flow still works. ACOMFY_API_KEYthat is in the environment gets sent to that deployment, as the Python client does. The README and CHANGELOG say so, and say to leave it unset for a keyless target.Resolved per construction, like
resolveBaseUrl()already is, so a process can build successive clients under different credentials and a test can stub the environment. Comfy Cloud is matched by normalized origin and path, not by string —https://cloud.comfy.org:443is still Comfy Cloud (reading it as some other deployment would hand back a keyless client and the very 401 this replaces), whilehttps://cloud.comfy.org/self-hostedis a different target and stays keyless. That is the same_same_deploymentrule the Python client applies. Trailing slashes are stripped the way Python'srstrip("/")strips them — all of them, not just the last: a first cut removed one, sohttps://cloud.comfy.org//(what concatenating a base URL with a/-prefixed path produces) read as a different deployment, took the keyless carve-out and handed back a credential-less client aimed at Cloud. Fixed in466bcd7, with two cases in the spelling table that fail without it.The base URL is resolved first, deliberately: it decides whether a missing key is an error at all, and a malformed
COMFY_BASE_URLis the more actionable of the two failures.The module-level
comfy.config({ credentials })binding is not routed into the class client. The two surfaces stay separate — that is what keeps a process-global credential out of a multi-tenant server's per-request clients — they just read the same variable now.Judgment calls
apiKeynow throws aTypeErrornaming the type (never the value, the ruleconfig()already follows). Untyped JS callers get no compile error, and falling through to the environment would authenticate them as whateverCOMFY_API_KEYnames while they believe they supplied a key. Not asked for by the issue; it is error handling for a branch this change newly creates.feat!, notfeat. A keyless client against Comfy Cloud now throws at construction rather than deferring to the server. No capability is lost (see below), but the observable moment of failure moves, so the CHANGELOG entry says so under "Note for callers who relied on the old behaviour".code: "missing_credentials"rather than adopting Python'smissing_api_key, so the two TypeScript surfaces agree with each other. The class-name difference (MissingApiKey/MissingCredentials) is already a declared cross-SDK asymmetry.Capability-denial falsification
This diff adds a deny path, so the denied capability was attempted for real before shipping it — non-mutating reads only, against the host the ticket names:
GET https://cloud.comfy.org/api/v2/jobs?limit=1, noAuthorization401 unauthorized— "Missing or invalid API key."GET https://cloud.comfy.org/api/v2/jobs/<unknown-id>, noAuthorization401 unauthorized— sameGET https://cloud.comfy.org/api/v2/jobs?limit=1,Authorization: Bearer(empty)401 unauthorized— sameThere is no keyless path to Comfy Cloud's v2 API to redirect a caller to, so the throw denies nothing that worked; it moves a guaranteed remote failure to the call site. The one path that does work without a credential — a non-Cloud
COMFY_BASE_URL— is explicitly preserved, and two tests pin it (http://127.0.0.1:8189and a deployment mounted under the Cloud host both stay keyless and do not throw).Tests
New
src/sdk/apiKeyEnv.test.ts(23 cases): precedence in both directions, blank and whitespace at either source, trimming, per-construction reads, the no-network assertion (thefetchstub counts attempts and must see zero), the message namingapiKey/COMFY_API_KEY/COMFY_BASE_URL/ the Cloud URL, the errorcode, the five Comfy Cloud spellings (including the doubled trailing slash below), the path-mounted carve-out, and base-URL-before-credential ordering.Every test in that file stubs both environment variables, and the three existing files that built keyless clients now do the same (
baseUrlEnv.test.tsconstructs with an explicit key,index.test.tslikewise,client.test.tsclearsCOMFY_API_KEYsince its stub server needs none) — the ambient developer key must never reach a test client, and a base-URL test must not be decided by a credential.Cross-SDK parity
Partially landed; see Residual. What is here:
scripts/python-surface.mjsreadssrc/comfy_sdk/client.pyand extracts the credential resolution as a behaviour — the two variable names, the Cloud base URL, the order off the candidate tuple (read, not assumed, so a Python-side swap is visible), the raised class, and the cloud-guarded raise plus the trailingreturn None. Every shape it cannot read is a hard failure, matching the file's existing rule that a broken extraction must never read as parity. Seven extractor unit tests cover it: the two refusals; a candidate holding more than one nested call, whose tuple close is found by a depth scan rather than a regex; the candidate loop, which is read only from a statement line (a commented-out loop does not count), with the environment source matched on the exactAPI_KEY_ENV_VARconstant and a repeated source rejected; and the missing-key raise, which is read from directly inside theif _same_deployment(..., COMFY_CLOUD_BASE_URL):block. That guard is read through nested calls and a trailing comment or pragma, and comment lines inside it are skipped. These do not count: an earlier type-check raise, a raise nested under a further condition, a guard that is itself nested under a further condition, a negated guard, a guard narrowed withand ..., and a guard that only appears in a comment.surface-parity.test.tsrewrites thecredential-resolutionasymmetry entry, which claimed the class client resolved only at module level and is now wrong, and adds asharedCredentialOrderrot guard that checks the declared order against real constructions, deriving each expected winner from that declaration (the same "derive by calling, not by reading the table" rule the status-fallback comparison uses).Provenance
22e86fc(main at8096086merged in),pnpm testreports 731 passed, 5 skipped, 0 failed (29 files).pnpm lint,pnpm typecheck,pnpm buildandpnpm check:spec-driftwere clean onbd3da1b; the only change since is a one-lineparity/README.mdcorrection, after whichpnpm format:checkandpnpm lintare clean.extractCredentialResolutionreads the livesrc/comfy_sdk/client.pyfrom the Python default branch cleanly.pnpm check:sdk-parityis not green: the Python SDK has since gainedmodels.schema/models.list, so the snapshot is stale onmaintoo (same failure there). That refresh is open separately as #201.Residual
The Python-vs-TypeScript comparison of credential resolution is extracted but not asserted.
extractPythonSurfacenow emits acredentialResolutionsection, butparity/python-surface.jsondoes not carry it and no test compares it. The snapshot is not refreshed on this branch becausepnpm check:sdk-parity(thesdk-parityCI job) is already red onmainfor an unrelated reason: the Python SDK now shipsmodels.schemaandmodels.list, so themodelsMethodsAheadOfPythonlead for those two has rotted and its guard fires once the snapshot is refreshed. That refresh and the lead deletion are #201. Refreshing locally with this branch's extractor, the only failing parity assertion is that rot guard, and the newcredentialResolutionsection readsCOMFY_API_KEY/https://cloud.comfy.org/["explicit", "environment"]/MissingApiKey.What is left to do: once #201 lands, refresh the snapshot here so it carries
credentialResolution, then add that section toloadPythonSurface's non-empty section list and compareapiKeyEnvVar/cloudBaseUrl/order/missingKeyError(throughRENAMES) against the valuessurface-parity.test.tsalready derives from live constructions.parity/README.mdcarries these two lines as a note so they are findable without this PR open. Until then the TypeScript half is pinned byapiKeyEnv.test.tsand thesharedCredentialOrderrot guard, and the Python half by the extractor's unit tests. That is strictly more coverage than before, but it is not yet the two-sided comparison the issue asked for.Unexercised artifacts. The issue references two sibling tracker items by identifier only (the Python-side change this mirrors, and the cross-SDK parity check). Their content was not fetched and is not reachable from this environment, so neither was read. The Python behaviour was taken instead from
src/comfy_sdk/client.pyon that SDK's public default branch, which I read directly — that file, not the tracker item, is what this implementation was matched against, and it is also what the new extractor parses.The reporter's own artifact was exercised: published
@comfyorg/sdk@0.4.0(the version they named, pulled from the npm registry) carriesthis.low = new ComfyLow(resolveBaseUrl(), options.apiKey, ...)atdist/sdk/client.js:103, andCOMFY_API_KEYappears nowhere in that file — only indist/sdk/credentials.js, thecomfy.models.*surface. Their report is accurate on the exact build they ran. Note the CHANGELOG has no0.4.0section (it was last cut at0.3.0) even though0.4.0is the currentlatesttag on npm; that gap is not something this change touches.Summary by CodeRabbit
COMFY_API_KEY. Values are trimmed, and credentials are resolved when the client is created.MissingCredentialserror; other deployments can be used without credentials.TypeError.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.