Add six API-key vendor types
## Original requirement (verbatim, epic body)
> Add to Credential types:
>
> - Apify
> - Zappier
> - Make
> - N8n - just check that it has a default option to connect to their main cloud
> - Gumloop
> - IFTTT
> - Replit
> - Lovable
> - Heroku
>
> Make sure you reuse code, e.g. generic Oauth or something else generic. Improve the base connection. Each connector's code should be as slim as possible.
This child covers asks A1 ("Apify"), A3 ("Make"), A5 ("Gumloop"), A6 ("IFTTT"), A7 ("Replit") and A8 ("Lovable"), and A10–A12 for them ("reuse code… as slim as possible"). Heroku and Zapier (A2, A9) are a sibling child. It builds on the Foundation children: `KeyTestSpec` (declarative tester) and `allowed_hosts`.
## Settled requirements this child builds (PRD R-A, verbatim rows)
> Each appears in the console's credential Type dropdown with the label given, renders its fields from the registry, saves, and (where testable) tests via the existing Test button.
>
> | `apify` | Apify | API token (secret, req) | `https://api.apify.com` fixed | `GET /v2/users/me`, `Authorization: Bearer`; success names the username | — |
> | `make` | Make | Zone (text, default `us1`; allowed `us1`,`us2`,`eu1`,`eu2`), API token (secret, req) | `https://<zone>.make.com` | `GET /api/v2/users/me`, `Authorization: Token <t>`; names the user | Only if D3 verification says open |
> | `gumloop` | Gumloop | API key (secret, req), User ID (text, optional) | `https://api.gumloop.com` fixed | A read-only call that 401s on a bad key | — |
> | `ifttt` | IFTTT | Service key (secret, req) | `https://connect.ifttt.com` fixed | `GET /v2/me` with `IFTTT-Service-Key` | — |
> | `replit` | Replit | API key (secret, req) | — | Untestable; reason: Replit has no general public API to check a key against | — |
> | `lovable` | Lovable | API key (secret, req) | `https://api.lovable.dev` fixed | `GET /v1/workspaces` with `Lovable-API-Key`, `Accept: application/json` | — |
>
> **R-C.4 Slimness bar**: each new API-key connector = one registry entry (+ at most one provider entry when it has Connect) and **zero** tester functions, zero TypeScript, zero migrations.
Settled refinements (Architecture, answered by the operator):
- Make's zone is a required **url** field, "Zone URL", pre-filled with `https://us1.make.com` (Q1: a text zone cannot be the sealed host without a change to browser sealing). It is restricted to the four make.com zones (Q2).
- Make has NO Connect (D-A): Make issues OAuth clients only through a reviewed request form.
- Gumloop IS testable via `GET /api/v1/teams` (D-B).
## Approach (Architecture §2.5 rows — the whole per-connector code; pinned)
Add exactly these `CredentialTypeSpec` entries to `apps/api/app/credentials/registry.py`. Place them after `stripe` and before the generic-types banner (`registry.py:~491`), in the D-G alphabetical order: Apify, Gumloop, [Heroku], IFTTT, Lovable, Make, Replit, [Zapier]. The bracketed types come from the sibling child; keep the alphabetical order on rebase. `KT` = `KeyTestSpec`. Set `reject_names_vendor=True` on every testable type here.
- `apify` "Apify": `api_token` "API token" secret req; `default_base_url="https://api.apify.com"`; `KT(path="/v2/users/me", key_field="api_token", value_prefix="Bearer", identity=(("data","username"),), reject_names_vendor=True)`; `require_https_base_url=True`.
- `gumloop` "Gumloop": `api_key` "API key" secret req; `user_id` "User ID" text opt, placeholder "Needed for a personal API key"; `default_base_url="https://api.gumloop.com"`; `KT(path="/api/v1/teams", key_field="api_key", value_prefix="Bearer", field_headers=(("x-auth-key","user_id"),), list_field="teams", reject_names_vendor=True)`; `require_https_base_url=True`.
- `ifttt` "IFTTT": `service_key` "Service key" secret req; `default_base_url="https://connect.ifttt.com"`; `KT(path="/v2/me", key_field="service_key", header_name="IFTTT-Service-Key", identity=(("service_id",),), reject_names_vendor=True)`; `require_https_base_url=True`.
- `lovable` "Lovable": `api_key` "API key" secret req, placeholder "lov_..."; `default_base_url="https://api.lovable.dev"`; `KT(path="/v1/workspaces?limit=1", key_field="api_key", header_name="Lovable-API-Key", list_field="data", reject_names_vendor=True)`; `require_https_base_url=True`.
- `make` "Make": `base_url` "Zone URL" url req, `default="https://us1.make.com"`; `api_token` "API token" secret req; no `default_base_url` (guard `testers.py:~1861`); `KT(path="/api/v2/users/me", key_field="api_token", value_prefix="Token", identity=(("authUser","name"),("authUser","email")), reject_names_vendor=True)`; `allowed_hosts=("us1.make.com","us2.make.com","eu1.make.com","eu2.make.com")`; `require_https_base_url=True`.
- `replit` "Replit": `api_key` "API key" secret req; `default_base_url="https://replit.com"` (the guard needs it; it is never contacted); `testable=False`, `untestable_reason="Replit has no public API to check a key against, so any test here would be a guess."`.
Response fields the verdicts read (cited by the Architect): Apify `data.username`; Make `authUser.name`/`authUser.email`; Gumloop `{ "teams": [...] }`; IFTTT top-level `service_id`; Lovable `{ data: [...], pagination }`.
Tests (mocked HTTP, in the existing credential-tester test style):
- Every testable type:
- 200 → green; assert the exact request URL, the exact auth header name and value, and the success sentence (naming the identity where one is read)
- 401 → red with "{Label} rejected the {key label} - check it."
- Gumloop: `x-auth-key` is sent if and only if `user_id` is filled.
- Lovable and Gumloop: a 200 whose list key is not a list → the existing unexpected-body result.
- Make:
- the request URL is `https://<zone>.make.com/api/v2/users/me` for each of the 4 zones
- a zone not in the allowed set (e.g. `https://evil.example.com`, `https://us1.make.com.evil.com`) is refused at save with a 400 naming the allowed origins
- switching the zone on a saved row while keeping the stored token must 400 via `_refuse_kept_secrets_if_destination_moved` (the token must be re-entered)
- Replit: the route reports Test unavailable with exactly its reason.
Existing registry-wide tests this child MUST update (found at the critic gate), all in `apps/api/tests/test_credentials.py`:
- `_EXPECTED_TYPES` (`:~2022`, reused by `test_credentials_tenant_types.py:~24`): add the six full entries in D-G order after `stripe`. This is the AC1/AC2 unit oracle.
- `fixed_host` set (`:~2855`): add apify, gumloop, ifttt, lovable, replit.
- `require_https_base_url` set (`:~3749`): add apify, gumloop, ifttt, lovable, make.
- `TESTERS` set (`:~6001`): add apify, gumloop, ifttt, lovable, make.
- Untestable set (`:~6025`): add replit.
- Make fixture: `_config_for` (`:~3118-3123`) fills every url field with `https://zz-<slug>.example`, which `allowed_hosts` correctly refuses. Special-case `("make","base_url")` to `https://us1.make.com` so that the create sweep (`:~6122`) and the TESTERS sweep (`:~4814`) pass. NEVER weaken `allowed_hosts` to make a test pass.
## Acceptance criteria (PRD §6, verbatim — for these six types)
> AC1 — All 8 new types (Apify, Zapier, Make, Gumloop, IFTTT, Replit, Lovable, Heroku) appear in Credentials → New → Type, with exactly those labels (readable, correct brand spelling, no raw slugs), above the generic types. (This child: Apify, Make, Gumloop, IFTTT, Replit, Lovable.)
> AC2 — Each new type's form shows exactly the fields in R-A with readable labels; secret fields render as secrets and are never returned to the browser after save.
> AC3 — Testable types with a valid key go green with a sentence naming the vendor (and identity where the vendor returns one); an invalid key gives a clear red naming the vendor.
> AC4 — Replit shows Test disabled with its one-line reason; Gumloop does too only if no checkable endpoint was proven. (Gumloop's endpoint WAS proven → Gumloop is testable.)
> AC5 — Make: Zone defaults to `us1` on a new row; Test calls `https://<zone>.make.com/api/v2/...` for the chosen zone; an invalid zone is refused at save with a message naming the allowed values.
> AC8 — no new type in R-A that is a single-GET key check has a tester function.
> AC9 — Each new connector's code is one registry entry (+ one provider entry when it has Connect); no TypeScript, no migration changed for them.
> AC11 — Available to the operator: everything above is ON by default, behind no flag, on the dev control plane.
Quality bar: labels use the vendors' real brand spellings (IFTTT, not Ifttt). Every field label and placeholder reads naturally to an operator. The red sentence names the vendor, so a wrong key is obvious. The Make Zone URL pre-fill is a working value (US). It is a FAIL if the tests pass but the UI shows a raw slug, a confusing field label, or a generic "invalid" refusal for Make.
Credentials & reach: (Architecture §7, verbatim rows)
- Dev UI: AC1, AC2, AC4, AC5, AC11 — Credential `jaah/debug/main/operator-login` (operator login, works on dev) against `https://app.dev.jaah.ai` — **verified**: the roster fetch succeeded, and the dev console answered 200 from h2.
- AC3 garbage-key red for each new testable vendor — none (worktree + public vendor hosts) — **verified**: Apify, Make us1, Gumloop, Lovable and Heroku answered 401 to unauthenticated/bogus probes, and IFTTT answered 401 to a bogus key.
- AC3 real-key green for Apify, Make, Gumloop, IFTTT, Lovable — a vendor key per type — **deferred**: none in the roster; the operator chose to defer (Q5). Covered by mocked unit tests.
- AC8/AC9 — worktree only — **verified**: nothing beyond the worktree.
Out of scope:
- Heroku and Zapier types, the `OAUTH_CONNECT_PROVIDERS` entries and `_CONNECTABLE`; the sibling OAuth child owns them.
- Do NOT edit the `KeyTestSpec`, `_declared_tester`, `require_one_of` or `allowed_hosts` machinery, except for a bug fix proven by a failing test. The Foundation children own it; report any such fix in the PR body.
- "Do NOT add a per-type web component or touch the credential form beyond what generic rendering already does; no new field kind." "Do NOT add a DB migration." — nothing under `apps/web` or `apps/api/migrations`.
- "Do NOT change existing types' slugs, labels, fields, or test messages … do not change any `default_base_url` of an existing type."
- "Do NOT make a Test call that mutates vendor state (no IFTTT webhook trigger…)."
- "Do NOT commit any OAuth client id/secret or vendor key."
- No Make OAuth Connect (D-A). Deferred: Celonis Make zones, an IFTTT Webhooks (Maker) key shape, and Gumloop OAuth.
Done means:
- AC3/AC5 unit — `./dc check full` is green with new mocked tests asserting:
- for each testable type, the exact URL, the auth header and the green sentence on 200, and "{Label} rejected the {key label} - check it." on 401
- Make's URL for each of the 4 zones
- a Make zone outside the allowed set, including a look-alike, refused at save naming the allowed origins
- a zone change that keeps the stored token → 400
- AC8 — `grep -n "def test_" apps/api/app/credentials/testers.py` shows none for apify, gumloop, ifttt, lovable, make.
- AC9 — the PR diff touches nothing under `apps/web` or `apps/api/migrations`; for each type, the diff is one registry entry plus tests.
- AC1 — after this PR's Jaah Deploy succeeds on dev: Credentials → New → Type lists Apify, Gumloop, IFTTT, Lovable, Make and Replit with those exact labels, above the generic types, and `GET /api/credentials/types` lists the 6 slugs. Screenshot in the report.
- AC2 — on dev, for each of the 6: create a row with dummy values, save, and reload. Secret fields show "leave blank to keep", and their values are absent from the `GET /api/credentials/{id}` response. Delete the dummy rows afterwards.
- AC4 — on dev, a Replit row shows Test disabled with "Replit has no public API to check a key against, so any test here would be a guess."
- AC5 — on dev, a new Make row shows Zone URL pre-filled with `https://us1.make.com`, and saving `https://evil.example.com` is refused with a message naming the 4 allowed origins.
- AC3 red (live) — on dev, Test on an Apify, Gumloop, IFTTT, Lovable and Make row each holding a garbage key shows red naming the vendor.
- AC3 green (live) — (deferred — no vendor key in the roster; operator chose to defer, Q5). Report it as deferred; mocked tests cover it.
- AC11 — everything above is observed on dev with no flag toggled.
- AC1 (all 8) — whichever of the two vendor children deploys SECOND also screenshots the dropdown showing all 8 labels (Apify, Gumloop, Heroku, IFTTT, Lovable, Make, Replit, Zapier) above the generic types. The first to deploy notes in its report that the combined check falls to the sibling.
Outcome
Added Apify, Gumloop, IFTTT, Lovable, Make, Replit as registry-only types (no tester fns/TS/migrations). Full gate green; PR #5242 merged; dev deploy green, dev UI checks pass, live reds name vendor. Deferred: real-key green, 5 form UX fixes (in report).
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)