Skip to content

feat(connectors): callApi for metered connectors - #256

Open
ChenMachBase wants to merge 4 commits into
mainfrom
connectors-money
Open

feat(connectors): callApi for metered connectors#256
ChenMachBase wants to merge 4 commits into
mainfrom
connectors-money

Conversation

@ChenMachBase

Copy link
Copy Markdown
Contributor

Summary

Adds callApi for metered connectors — connectors backed by paid third-party APIs that charge Base44 per call.

For those, the OAuth token is deliberately unavailable to app code (getConnection rejects with a 403), because the Base44 proxy is the only place calls we pay for can be counted. callApi is the replacement:

const res = await base44.asServiceRole.connectors.callApi('x', {
  method: 'POST',
  path: '/2/tweets',
  body: { text: 'Shipped!' },
});
// -> { success, status, data, headers, creditsCharged }

Only platform connectors are affected. A workspace-registered or app user connector runs on your own OAuth app, so the provider invoices you directly and there is nothing for Base44 to meter — those keep normal token access via getWorkspaceConnection() / getCurrentAppUserConnection(). That is why there is one method here and not three.

Two shape decisions worth reviewing

An upstream 4xx/5xx resolves, it does not throw. A provider error is a normal outcome of a call Base44 completed and billed, so it comes back as success: false with the provider's own status and data. Only Base44-side failures — no connection, credits exhausted, a rejected request — reject the promise. Tests pin both sides of that split.

query is always forwarded, never dropped. The server prices the merged query string, so an SDK that accepted the field and then discarded it would make the quoted price and the real request disagree. (This is the client-side half of a billing bypass caught during review of the backend change.)

Responses carry creditsCharged, and the module docs gain a "Metered connectors" section noting that cost can vary by two orders of magnitude between endpoints on the same connector — an expensive call inside a loop being the failure mode worth warning about.

Testing

  • 9 new unit tests in tests/unit/connectors-proxy.test.ts: payload normalization, snake_casecamelCase mapping, query forwarding, upstream-error-resolves vs Base44-error-rejects, and the metered 403 surfacing an actionable message.
  • Full suite green: 210 tests, plus tsc --noEmit, npm run test:types, and eslint src.
  • Installed with npm ci (lockfile-exact). Up to date with main.

Notes

  • No version bumppackage.json is at main's 0.8.42. Publishing, and bumping the pinned @base44/sdk in the platform's builder prompts, is a separate release step; that pin touches several prompt files including system-prompt ones.
  • Backend counterpart: base44-dev/apper#19753. It ships inert (no connector is registered as metered yet), so nothing here is load-bearing until it is.
  • Supersedes feat(connectors): callApi for metered connectors #250, which was opened from a fork before I had write access here — fork PRs get no CI and cannot run preview-publish, so this replaces it.

ChenMachBase and others added 3 commits August 12, 2026 12:33
Some connectors are backed by paid third-party APIs that charge Base44 per call.
For those the OAuth token is not available to app code — getConnection and its
siblings reject with a 403 — because the Base44 proxy is the only place those
calls can be counted. Adds the three proxy methods that replace them:

- callApi(integrationType, request)          — shared platform connector
- callWorkspaceApi(connectorId, request)     — workspace-registered connector
- callCurrentAppUserApi(connectorId, request) — per-app-user connector

Each mirrors its getConnection counterpart, so the identifier you already use
carries over.

Two deliberate shape decisions:

- An upstream 4xx/5xx resolves with `success: false` and the provider's own
  `status`/`data` rather than throwing. It is a normal outcome of a call Base44
  completed and billed; only Base44-side failures (no connection, credits
  exhausted, a rejected request) reject.
- `query` is always sent, never dropped. The server prices the merged query
  string, so a client that accepted the field and then discarded it would make
  the quoted price and the real request disagree.

Responses carry `creditsCharged` so callers can see what a call actually cost,
and the module docs call out that cost varies sharply by endpoint — an expensive
call inside a loop is the failure mode worth warning about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drops callWorkspaceApi and callCurrentAppUserApi. They implied that
workspace-registered and app user connectors can be metered, and they can't: both
run on the workspace's *own* OAuth app, so the provider invoices the workspace
directly. Proxying them would have billed the customer credits on top of a vendor
bill they already pay.

Only a platform connector runs on Base44's OAuth app, so callApi is the only one
of the three that ever had something to meter. The backend's matching routes are
gone too (base44-dev/apper#19753).

The module docs now say which connectors this applies to and, more usefully, why
the other two don't — so the next person doesn't re-add the methods on the
assumption they were an oversight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ChenMachBase's task —— View job


Code Review: callApi for metered connectors

Thorough, well-tested PR. The two "shape decisions" (upstream errors resolve; query always forwarded) are correct and the tests pin both. Below is feedback organized by category.

Code quality — 👍

  • proxyCall is cleanly factored out and documented; the @internal JSDoc explaining why upstream 4xx resolves rather than throws is exactly the kind of comment that earns its keep (src/modules/connectors.ts:139-147).
  • New assertNonEmptyString helper reduces the repeated validation boilerplate. Nice.
  • camelCase mapping and the raw envelope type (ConnectorProxyRawResponse) match the codebase convention of casting the interceptor's unwrapped response.data via as unknown as — consistent with the sibling methods.

Potential bugs / issues

  1. method not validated / arbitrary strings pass through. request.method is typed as a union, but at runtime any string is accepted and uppercased (connectors.ts:159). The test even passes "post" lowercase. That's fine for the happy path, but an invalid method (e.g. "FETCH") is silently forwarded to the server. Minor — server will reject — but worth deciding if the SDK should guard.
  2. body on GET/HEAD/DELETE. The type doc says body is "Ignored for GET, HEAD, DELETE," but the client always forwards body: request.body ?? null (connectors.ts:163). So the server must be the one ignoring it. That's a reasonable division, but the doc phrasing ("Ignored") reads as if the SDK drops it. Consider "Ignored by the server for GET/HEAD/DELETE" to match actual behavior.
  3. Validation-order asymmetry. callApi validates integrationType before request is even checked for being an object; proxyCall then validates request and path. Given the test.each with ["x", ""] expects the path error, this ordering works, but the two-stage validation split across two functions is a little subtle. Not a bug — just flag for maintainers.

Performance — 👍

No concerns. Single POST per call; no extra allocations of note. The docs' warning about expensive calls inside loops is the right place to address the real cost lever, since it's server-priced.

Security

  • The design (token never leaves the server for metered connectors) is sound and is the whole point.
  • headers passthrough: the type doc says only connector-allowlisted headers are forwarded and the rest dropped server-side (connectors.types.ts:66). Good that the allowlist lives on the server rather than the client. No client-side secret handling here. ✅
  • No logging of request bodies/headers added in this path (the iframe postMessage logging in axios-client.ts is pre-existing and unrelated).

Test coverage — strong

9 tests cover: normalization, GET default, query forwarding, camelCase mapping, upstream-error-resolves, Base44-error-rejects (402), metered 403 on getConnection, and empty identifier/path. A couple of gaps worth considering (optional):

  • No test for missing/non-object requestproxyCall's "Request is required and must be an object" branch (connectors.ts:153-155) is uncovered. Easy to add: callApi("x", undefined as any).
  • No test asserting status/headers/creditsCharged defaults when the server omits them (the ?? null / ?? {} / ?? 0 fallbacks at connectors.ts:169-172). A response missing those fields would exercise the nullish-coalescing paths.
  • method uppercasing is implicitly tested via "post""POST", good.

Minor nits

  • getConnection/getWorkspaceConnection/getCurrentAppUserConnection still use inline if (!x || typeof x !== "string") checks while callApi uses the new assertNonEmptyString. Not required for this PR, but a follow-up could unify them.
  • ConnectorApiResponse.data is typed T but the proxy could return null/error bodies on success: false; callers relying on T without checking success may get a surprise. The docs cover this, so it's acceptable.

Overall: LGTM. The billing-bypass reasoning behind always forwarding query is well-justified, and error semantics are correct and tested. The only thing I'd actually add before merge is a test for the non-object request branch; everything else is optional polish.
• Branch: connectors-money

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/sdk@0.8.43-pr.256.e74a345

Prefer not to change any import paths? Install using npm alias so your code still imports @base44/sdk:

npm i "@base44/sdk@npm:@base44-preview/sdk@0.8.43-pr.256.e74a345"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "@base44/sdk": "npm:@base44-preview/sdk@0.8.43-pr.256.e74a345"
  }
}

Preview published to npm registry — try new features instantly!

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.

1 participant