{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/connectors",
  "slug": "engines/connectors",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/connectors",
  "name": "@leumas/connectors",
  "eyebrow": null,
  "chip": null,
  "summary": "Connectors — logged-in users link third-party accounts (databases, social/OAuth, SSH/SFTP). A per-provider connector registry + createConnectorsRouter (CRUD connections + test-connection). Absorbs...",
  "keywords": [
    "connectors",
    "createconnectorsrouter",
    "leumas-middleware",
    "database-connector",
    "databases",
    "connectors api",
    "leumas connectors",
    "how to use connectors"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# @leumas/connectors\n\nLogged-in users **link third-party accounts** — databases (Mongo, SQL, Redis, Firebase),\nmedia (Cloudinary), social/OAuth (Google, GitHub, Facebook, LinkedIn, YouTube, TikTok), and\nSSH/SFTP hosts. One connector registry powers the API, the connect form, and the Studio page.\n\n**Absorbs (DRY-consolidated from):** `leumas-middleware/database-connector` (Mongo/SQL/Redis/Firebase/\nCloudinary adapters) + `leumas-cursor-agent-mvp` (`databasesController`, `socialConnectionsController` =\nOAuth, `sshProfilesController` = SSH/SFTP).\n\n## The model\n\nA **Connector** describes one integration a user can link — its `provider` id, `family`\n(`database | media | social | ssh`), the form `fields`, which fields are `secret`, and a\n`test(config)` probe. Drivers (`mongodb`, `redis`, `ssh2`, …) are **lazy-loaded inside `test()`** so\nthis engine has no hard driver dependency.\n\nA **Connection** is a saved link: `{ id, owner, provider, name, config, lastTest }`. Secrets are\nnever returned to the client (masked as `••••••`).\n\n## Usage (in leumas-api)\n\n```js\nimport { createConnectorsRouter } from '@leumas/connectors';\nimport { requireAuth } from '@leumas/auth';\n\n// `store` persists the per-user `connections` collection (back it with @leumas/middleware).\napp.use('/api/connectors', createConnectorsRouter({ store, requireAuth }));\n```\n\n### The `store` contract\n\n```\nstore.list({ owner })                → Connection[]\nstore.get(id, { owner })             → Connection | null\nstore.create(connection, { owner })  → Connection\nstore.update(id, changes, { owner }) → Connection\nstore.remove(id, { owner })          → void\n```\n\n`createMemoryStore()` is a zero-dep default for dev/tests.\n\n## Routes\n\n| Method | Path | Purpose |\n| --- | --- | --- |\n| GET | `/providers` | connector manifests (fields, family, auth) |\n| GET | `/` | this user's connections (secrets masked) |\n| POST | `/` | create `{ provider, name, config }` |\n| GET/PUT/DELETE | `/:id` | read / update / remove a connection |\n| POST | `/:id/test` | test a saved connection |\n| POST | `/test` | test an unsaved `{ provider, config }` |\n| GET | `/:provider/authorize` | OAuth authorize URL (social connectors) |\n\n## Add a provider\n\n```js\nimport { defineConnector } from '@leumas/connectors';\n\nexport const stripe = defineConnector({\n  provider: 'stripe', family: 'media', name: 'Stripe',\n  fields: [{ name: 'secretKey', type: 'password', required: true }],\n  async test(config) { /* probe */ return { ok: true }; },\n});\n```\n\nPass it in `connectors: [...allConnectors, stripe]` to `createConnectorsRouter`, or add it to\n`src/providers/index.js`.\n\n## Operations — what a connection can actually DO\n\nA connector carries an **`.integration`** payload (`{ baseUrl, auth, operations }`) and that one\nproperty is what makes it callable: `executeOperation`, `createIntegrationRunner`, the\n`/:id/operations` route, the grid's `integration:` cell and `@leumas/invoke`'s `integration` kind all\nlook for it and nothing else.\n\n- `api`-family connectors get it from `defineApiIntegration` (`src/providers/api/catalog.js`).\n- **OAuth connectors get it from `src/providers/operations/<provider>.js`** — one pure-data file per\n  provider, assembled by `operations/index.js` and attached in `providers/social.js`. 156 operations\n  across 27 providers.\n- A provider with no catalog is listed in `UNCATALOGUED` with the **sentence saying why**, which the\n  Studio panel shows instead of an empty list. The rule: a provider gets a catalog when a bearer\n  token is enough to call it; when it is not, it gets a sentence.\n\nDeclare parameters with the compact spec in `operations/_shared.js`:\n\n```js\nparams: args({\n  '*id': 'string|Required — the star means required.',\n  part:  'string=snippet|A default, applied before the call.',\n  mine:  'boolean=true|Typed: this is a real boolean, not \"true\".',\n})\n```\n\nAn operation may also declare `scopes: [...]` (needs more than the read-ish defaults),\n`bodyStyle: 'json' | 'form' | 'multipart'`, and `uploadStyle: 'google-resumable'`.\n\n> [warning] **A declared scope must be in the provider descriptor's `scopes` or `optionalScopes`.** If it is\n> in neither, the authorize URL never asks for it — so it can never be granted and the operation can\n> never work, on any account, with no error anywhere. `pnpm smoke:connector-ops` asserts this.\n\n## Environment\n\n| Variable | What it does |\n| --- | --- |\n| `LEUMAS_SECRET_KEY` | Seals every credential at rest (aes-256-gcm, `lsx1:` prefix). **Without it tokens and database passwords are stored in clear text**, and the Connections page says so on the page rather than leaving you to infer it. |\n| `LEUMAS_OAUTH_<PROVIDER>_CLIENT_ID` / `_SECRET` | This deployment's registered OAuth app. `<PROVIDER>` is the provider id uppercased with dashes as underscores — `google-sheets` → `LEUMAS_OAUTH_GOOGLE_SHEETS_CLIENT_ID`. The Google family falls back to the plain `GOOGLE` pair, so one registered Google app serves Docs, Drive, Sheets, Gmail, Calendar and YouTube. |\n| `LEUMAS_OAUTH_SHARED_APPS` | `1` makes those env apps **shared defaults a tenant may override** with their own, instead of **pinned** (the default, where env wins and the UI locks the form). See below. |\n| `LEUMAS_OAUTH_REDIRECT_URI` | Pins the callback URI. Otherwise it is derived from the address the **browser** is at — env → `X-Forwarded-Host` → `Origin` → `Referer` → `Host` — never from the listening socket, because in dev the browser is on Vite and `127.0.0.1` and `localhost` are different origins to Google. |\n\n### Pinned vs shared OAuth apps\n\nTwo modes, opposite failure modes, both real — so it is a flag rather than a flip:\n\n- **Pinned (default).** Env wins outright and the provider shows as `locked`. An operator must not be\n  able to silently repoint a fleet's or a vendor's Google app by saving a row over it.\n- **Shared (`LEUMAS_OAUTH_SHARED_APPS=1`).** Env is *Leumas's own* registered app, offered so a new\n  Studio can connect an account with no OAuth paperwork — and a tenant that registers their own app\n  **beats it**. `status()` reports `shared: true` (works today, not locked) or `overrides: 'shared'`\n  (your own app is the one in use).\n\nA tenant's own app is the better one whenever it exists: their name on the consent screen, their\nquota, and no shared-app suspension taking them down with it.\n",
  "source": {
    "path": "shared/engines/connectors/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 6705,
    "hash": "af1ff00de8c03e03f3640ff6aa5333fdfbddff83"
  },
  "urls": {
    "html": "/p/engines/connectors",
    "json": "/docs/engines/connectors.json",
    "md": "/docs/engines/connectors.md"
  },
  "links": {
    "composes": [
      "pkg:@leumas/api-client",
      "pkg:@leumas/schemas",
      "pkg:@leumas/security"
    ],
    "usedBy": [
      "pkg:@leumas/variables"
    ],
    "product": [],
    "howTo": [],
    "skills": [
      "skill:leumas-lmx#lmq",
      "skill:leumas-studio#domains/protocols"
    ]
  },
  "exports": null
}
