{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/variables",
  "slug": "engines/variables",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/variables",
  "name": "@leumas/variables",
  "eyebrow": null,
  "chip": null,
  "summary": "Leumas Variables — one value, read everywhere. A tenant/user-scoped key-value store whose secrets are sealed at rest with @leumas/connectors' aes-256-gcm, expanded server-side as {{var.KEY}} in...",
  "keywords": [
    "variables",
    "route-local",
    "user-scoped",
    "received",
    "aes-256-gcm",
    "variables api",
    "leumas variables",
    "how to use variables"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# @leumas/variables\n\n**One value, set in one place, read everywhere.** Inside Leumas it is `{{var.KEY}}`; outside it is\n`/api/variables` with an `lk_` API key, opt-in per row.\n\n```js\nimport { createVariableResolver, createVariablesRouter } from '@leumas/variables';\n\nconst variables = createVariableResolver({ connector });          // 5s cached snapshot\napp.use('/api/variables', createVariablesRouter({ connector, gate, adminGate, isAdmin,\n  onChange: () => variables.invalidate() }));\n\nconst rules = createRuleEngine({ triggers, actions, variables }); // expands authored step props\n```\n\n## [critical] The one rule the whole feature rests on\n\n**Expand authored text, never received text.** A rule fires with `{ ...payload, ...step.props }`.\n`step.props` is what an operator typed into Studio; `payload` is whatever the trigger carried — and on\nthe webhook path that is a body a stranger chose. Expanding the *merged* object would let an inbound\nrequest containing the literal text `{{var.stripe.secret}}` resolve it, and the next action step — a\nmailer, an outbound call, a log line — would carry the plaintext straight back out. The attacker never\nreads the collection; they ask this instance to read it for them.\n\nSo every call site expands `step.props` **only**. `pnpm smoke:variables` fires a real rule with that\nexact string in both halves and asserts one expanded and the other did not.\n\nThe syntax is safe to add: `@leumas/actions`' interpolator matches `/\\{\\{(\\w+)\\}\\}/`, and `\\w`\nexcludes the dot, so `{{var.NAME}}` passes through it untouched.\n\n## Scopes, secrets, and the outside\n\n**Two scopes, one collection.** A `user` row beats the `instance` row for its owner. An `instance` row\nis policy for everybody, which is verbatim the `PLATFORM_COLLECTIONS` criterion — so writing one is\nadmin-only and this router is the legitimate path. **The owner is stamped from the session and never\nread off the body**; it used to be, and a member could create a user-scoped row owned by a victim,\nwhich the resolver then preferred over the admin's instance value for that person.\n\n**Secrets are sealed with `@leumas/connectors`**, not with crypto written here — `sealSecret` /\n`openSecret`, aes-256-gcm, keyed on `LEUMAS_SECRET_KEY`. It **no-ops when no key is configured**, by\ndesign, which is exactly why `sealingEnabled()` is surfaced in the Studio tab: an operator who ticks\n\"secret\" on a deployment with no key would otherwise believe a token is encrypted while it sits in the\ncollection in the clear. A secret's value is **omitted** from every list — omitted, not masked, because\na row of dots still tells a reader how long it is — and can never be published externally.\n\n**The external lane is route-local, and must stay that way.** `attachApiKeyUser()` is mounted inside\nthis router only. `@leumas/auth` forbids a global mount — it would make every authenticated route in\nthe API reachable with a long-lived static bearer that has none of a session's protections — and\n`ops/infra/scripts/check-mcp.mjs` asserts it. Note `hasScope` returns true for any *session* user, so\nit constrains only key-borne identities; the row's own `external` flag and the owner check are what do\nthe work. An API-key identity is `isAdmin: false` by construction, so **no key can ever write an\ninstance variable** — a property of the identity, not a policy someone has to remember.\n\n**A miss stays verbatim.** `{{var.typo}}` renders as `{{var.typo}}` rather than blanking, so the\nmistake is visible where it was made instead of three systems downstream.\n\n## Guards\n\n`pnpm smoke:variables` · `pnpm check:mcp` (proves the API-key mount is still route-local).\n",
  "source": {
    "path": "shared/engines/variables/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 3833,
    "hash": "38dc3eb86a8e23e67446c72e645d44bc08bbe748"
  },
  "urls": {
    "html": "/p/engines/variables",
    "json": "/docs/engines/variables.json",
    "md": "/docs/engines/variables.md"
  },
  "links": {
    "composes": [
      "pkg:@leumas/api-kit",
      "pkg:@leumas/auth",
      "pkg:@leumas/connectors"
    ],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  },
  "exports": null
}
