{
 "generated": "tools/build_docs.py from docs/catalog/interfaces.yaml; do not edit",
 "product": "MainSDK",
 "site": "https://mainsdk.dev",
 "interfaces": [
  {
   "id": "auth",
   "name": "Sign-in and sessions",
   "category": "identity",
   "summary": "Sign people in with GitHub, passkeys, company SSO or WhatsApp, and keep their sessions safe.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "auth",
   "calls": [
    "auth.session(req)",
    "auth.require(req, { role, aal, fresh })",
    "auth.passkeys.register()",
    "auth.passkeys.signIn()",
    "auth.whatsapp.send()",
    "auth.sso.enable()",
    "auth.scim.enable()",
    "auth.challenge.start({ method })",
    "auth.challenge.verify({ message, signature })",
    "auth.device.start({ scope })",
    "auth.devices(user)",
    "auth.devices.revoke(user, deviceId)",
    "auth.orgs.create({ name, slug?, region })",
    "auth.orgs.list()",
    "auth.orgs.switch(req, tenantId)",
    "auth.members.list({ cursor?, limit? })",
    "auth.members.setRoles(userId, roles)",
    "auth.members.remove(userId)",
    "auth.teams.create(name)",
    "auth.teams.list()",
    "auth.teams.add(teamId, userId)",
    "auth.teams.remove(teamId, userId)",
    "auth.invites.create({ email, roles, teams?, expiresIn? })",
    "auth.invites.accept(req, token)",
    "auth.invites.revoke(inviteId)",
    "auth.domains.add(domain)",
    "auth.domains.verify(domain)",
    "auth.domains.autoJoin(domain, { role } | false)",
    "auth.sso.require(tenantId, { except? })",
    "new Auth(opts)",
    "new StepUpRequired(o)",
    "assessRisk(input, cfg)",
    "createAuth(opts)",
    "isPhishingResistantMethod(method)",
    "maskPhone(e164)",
    "normalizePhone(input)",
    "auth.handle(req, fn)",
    "auth.passwords.register(req, { email, password, name? })",
    "auth.passwords.signIn(req, { email, password })",
    "auth.link.issue({ email } | { phone }, { bind?, req? })",
    "auth.link.verify(req, token)",
    "auth.oauth.start(provider, { redirectUri, returnTo? })",
    "auth.oauth.callback(req)",
    "auth.stepUp.start({ pending, challenge, method })",
    "auth.stepUp.complete(req, { pending, challenge, method, ... })",
    "auth.recover(user, { via, req? })",
    "auth.recover.confirm(challengeId, code)",
    "auth.recover.cancel(session)",
    "auth.recover.complete(req, recovery)",
    "auth.recover.withCode(req, user, code)",
    "auth.recover.withPasskey(req, user, assertion)",
    "auth.recover.generateCodes(session)",
    "auth.recover.codesLeft(user)",
    "auth.sso.get()",
    "auth.sso.signIn({ email } | { org }, { redirectUri, returnTo? })",
    "auth.sso.callback(req)",
    "auth.sso.saml.metadata(org, { acsUrl })",
    "auth.sso.saml.acs(req)",
    "auth.risk(req, { userId })",
    "auth.signOut(req)",
    "auth.signOutEverywhere(req)",
    "auth.onRisk(fn)",
    "auth.raw()"
   ],
   "yaml": "auth:\n  driver: github                       # owner default; the auth spec's example uses clerk\n  methods: [passkeys, whatsapp, sso]\n  signup: open                         # open | closed\n  oauth:\n    google: { preset: oidc, issuer: https://accounts.google.com, client-id: \"...\", key: secret:GOOGLE_SECRET }\n  whatsapp: { driver: whatsapp-cloud }\n  sso: { protocols: [saml, oidc], scim: true }\n  session: { ttl: 15m, refresh: 7d, bind: dbsc }\n  orgs:\n    invites: { expires: 7d }\n    domains: { auto-join: true }\n  routes: { base: /_mainsdk/auth }     # where every adapter mounts the browser sign-in routes\n",
   "example": "import { auth } from \"mainsdk\";\n\nexport async function POST(req: Request) {\n  // 401 or 403 with a fix unless an admin signed in with a fresh, phishing-resistant method\n  const session = await auth.require(req, { role: \"admin\", aal: \"aal2\", fresh: \"5m\" });\n  return Response.json({ ok: true });\n}\n",
   "drivers": [
    {
     "id": "github",
     "name": "GitHub",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/github.html"
    },
    {
     "id": "clerk",
     "name": "Clerk",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/clerk.html"
    },
    {
     "id": "auth0",
     "name": "Auth0",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/auth0.html"
    },
    {
     "id": "better-auth",
     "name": "Better Auth",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/better-auth.html"
    },
    {
     "id": "workos",
     "name": "WorkOS",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/workos.html"
    },
    {
     "id": "supabase",
     "name": "Supabase Auth",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/supabase.html"
    },
    {
     "id": "local",
     "name": "Local (development only)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Short-lived, HttpOnly, Secure, SameSite sessions with rotating refresh tokens",
    "Passkeys by default, phishing-resistant",
    "Fails closed when the provider cannot be reached",
    "WhatsApp codes single use, expiring and rate limited per phone and per IP",
    "Every recovery audited, never weaker than sign-in"
   ],
   "checks": [
    "MAIN-AUTH-0053",
    "MAIN-AUTH-0055"
   ],
   "runtime_codes": [
    "MAIN-AUTH-0001",
    "MAIN-AUTH-0006",
    "MAIN-AUTH-0007",
    "MAIN-AUTH-0008",
    "MAIN-AUTH-0041",
    "MAIN-AUTH-0042",
    "MAIN-AUTH-0043",
    "MAIN-AUTH-0044",
    "MAIN-AUTH-0045",
    "MAIN-AUTH-0002",
    "MAIN-AUTH-0003",
    "MAIN-AUTH-0004",
    "MAIN-AUTH-0005",
    "MAIN-AUTH-0010",
    "MAIN-AUTH-0011",
    "MAIN-AUTH-0012",
    "MAIN-AUTH-0013",
    "MAIN-AUTH-0014",
    "MAIN-AUTH-0015",
    "MAIN-AUTH-0016",
    "MAIN-AUTH-0017",
    "MAIN-AUTH-0018",
    "MAIN-AUTH-0019",
    "MAIN-AUTH-0020",
    "MAIN-AUTH-0021",
    "MAIN-AUTH-0022",
    "MAIN-AUTH-0023",
    "MAIN-AUTH-0024",
    "MAIN-AUTH-0025",
    "MAIN-AUTH-0026",
    "MAIN-AUTH-0027",
    "MAIN-AUTH-0050",
    "MAIN-AUTH-0051",
    "MAIN-AUTH-0052",
    "MAIN-AUTH-0054",
    "MAIN-AUTH-0056",
    "MAIN-AUTH-0060",
    "MAIN-AUTH-0061",
    "MAIN-AUTH-0080",
    "MAIN-AUTH-0081",
    "MAIN-AUTH-0082",
    "MAIN-AUTH-0083",
    "MAIN-AUTH-0084",
    "MAIN-AUTH-0040",
    "MAIN-AUTH-0046",
    "MAIN-AUTH-0090",
    "MAIN-AUTH-0091",
    "MAIN-AUTH-0100",
    "MAIN-AUTH-0101",
    "MAIN-AUTH-0102",
    "MAIN-AUTH-0103",
    "MAIN-AUTH-0104",
    "MAIN-AUTH-0105",
    "MAIN-AUTH-0106",
    "MAIN-AUTH-0200",
    "MAIN-AUTH-0201",
    "MAIN-AUTH-0202",
    "MAIN-AUTH-0203",
    "MAIN-AUTH-0204",
    "MAIN-AUTH-0205",
    "MAIN-AUTH-0206"
   ],
   "features": [
    {
     "name": "Wallet sign-in by challenge signature",
     "phase": 2,
     "milestone": "0.7",
     "ticket": null
    },
    {
     "name": "Sessions and require",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#session-require"
    },
    {
     "name": "Web passkeys and browser QR",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#passkeys-web"
    },
    {
     "name": "CLI device sign-in",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#device-flow"
    },
    {
     "name": "WhatsApp one-time codes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#whatsapp-codes"
    },
    {
     "name": "OIDC single sign-on",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#sso-oidc"
    },
    {
     "name": "SAML single sign-on",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#sso-saml"
    },
    {
     "name": "SAML sign-in request in the redirect binding",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#sso-saml-request"
    },
    {
     "name": "SCIM provisioning",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#scim"
    },
    {
     "name": "Native passkeys and app-to-desktop QR",
     "phase": 3,
     "milestone": "1.1",
     "ticket": null
    },
    {
     "name": "Organisations",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#orgs-members"
    },
    {
     "name": "Email-bound",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#invites"
    },
    {
     "name": "Teams usable as access subjects",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#teams"
    },
    {
     "name": "Verified email domains and auto-join",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#domains"
    },
    {
     "name": "Require SSO per organisation",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "auth#sso-per-tenant"
    },
    {
     "name": "Sign-in routes mounted by every adapter",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "auth-client#routes"
    },
    {
     "name": "Browser client for passkeys with autofill",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "auth-client#client-passkeys"
    },
    {
     "name": "Sign-in links and WhatsApp codes from the page",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "auth-client#link-whatsapp"
    },
    {
     "name": "Step-up",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "auth-client#step-up"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/auth.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/auth.md"
  },
  {
   "id": "connections",
   "name": "Connections to other services",
   "category": "identity",
   "summary": "Let a person connect Gmail, GitHub or Slack once, so an app agent can act for them within the scopes they agreed to.",
   "status": "building",
   "stability": "experimental",
   "phase": 1,
   "milestone": "0.3",
   "spec": "connections",
   "calls": [
    "connections.connect(req, provider, opts)",
    "connections.callback(req)",
    "connections.fetch(provider, url, init)",
    "connections.list(opts?)",
    "connections.revoke(id, opts?)",
    "connections.describe(provider, scopes?)"
   ],
   "yaml": "connections:\n  store: db\n  callback: /api/connections/callback\n  providers:\n    github:\n      preset: github\n      client-id: Iv1.4f…\n      client-secret: secret:GITHUB_OAUTH_SECRET\n      scopes: [read:user, repo]\n      step-up: [repo]\nagents:\n  support-bot:\n    connections:\n      github: [read:user]\n",
   "example": "import { connections } from \"mainsdk/experimental/connections\";\n\nconst res = await connections.fetch(\"github\", \"https://api.github.com/user\", {\n  scope: \"read:user\", agent: \"support-bot\", user,\n});\n",
   "drivers": [
    {
     "id": "google",
     "name": "Google (Gmail",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/google.html"
    },
    {
     "id": "github",
     "name": "GitHub",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/github.html"
    },
    {
     "id": "slack",
     "name": "Slack",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/slack.html"
    },
    {
     "id": "microsoft",
     "name": "Microsoft 365",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/microsoft.html"
    },
    {
     "id": "notion",
     "name": "Notion",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/notion.html"
    },
    {
     "id": "atlassian",
     "name": "Atlassian",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/atlassian.html"
    },
    {
     "id": "airtable",
     "name": "Airtable",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/airtable.html"
    },
    {
     "id": "partner",
     "name": "Any OAuth provider (declare every endpoint)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/partner.html"
    },
    {
     "id": "local",
     "name": "Local (tests and local mode",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Tokens are sent only to the provider's hosts, over https",
    "An agent uses only the scopes main.yaml gives it",
    "Risky scopes wait for a person's yes",
    "Every use of a connection is logged"
   ],
   "checks": [
    "MAIN-CONN-0020",
    "MAIN-CONN-0021"
   ],
   "runtime_codes": [
    "MAIN-CONN-0001",
    "MAIN-CONN-0002",
    "MAIN-CONN-0003",
    "MAIN-CONN-0004",
    "MAIN-CONN-0005",
    "MAIN-CONN-0006",
    "MAIN-CONN-0007",
    "MAIN-CONN-0008",
    "MAIN-CONN-0009",
    "MAIN-CONN-0010",
    "MAIN-MCP-0007",
    "MAIN-MCP-0008",
    "MAIN-MCP-0009",
    "MAIN-MCP-0010"
   ],
   "features": [
    {
     "name": "OAuth connect flow with one consent screen",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#flow"
    },
    {
     "name": "Sealed token store in the tenant's region",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#store"
    },
    {
     "name": "Fetch with a scope and host check",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#fetch"
    },
    {
     "name": "Refresh and rotation",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#refresh"
    },
    {
     "name": "List, manage and revoke",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#manage"
    },
    {
     "name": "Per-agent scopes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#agent-scopes"
    },
    {
     "name": "Step-up approval for risky scopes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#step-up"
    },
    {
     "name": "Audit of every use",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#audit"
    },
    {
     "name": "MCP client authorization",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#mcp-client-auth"
    },
    {
     "name": "MCP server authorization",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#mcp-server-auth"
    },
    {
     "name": "Erase a connection on request",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#erase"
    },
    {
     "name": "Checks for undeclared scopes and providers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "connections#checks"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/connections.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/connections.md"
  },
  {
   "id": "verify",
   "name": "Phone, email and identity checks",
   "category": "identity",
   "summary": "Confirm a phone number, an email address or a person's identity at the level a market requires, with no answer that tells who has an account.",
   "status": "building",
   "stability": "beta",
   "phase": 2,
   "milestone": "0.7",
   "spec": "verify",
   "calls": [
    "verify.phone({ number, via?, for?, locale?, req? })",
    "verify.phone.check({ id, code })",
    "verify.email({ address, for?, req? })",
    "verify.email.confirm(token)",
    "verify.identity({ level, market, claims, need?, purpose, user, consent, redirect? })",
    "verify.identity.complete({ id, code? })",
    "createVerify(options)",
    "parseVerify(config)",
    "verifyHosts(config)",
    "normalizeNumber(input)",
    "normalizeAddress(input)",
    "maskNumber(e164)",
    "reaches(driver, level)",
    "twilioVerify(options)",
    "bird(options)",
    "localPhone()",
    "phoneFromConfig(id, { settings, secret, fetch })",
    "failure(provider, { retryable, cause })",
    "onfido(options)",
    "persona(options)",
    "digilockerIdentity(wallet)",
    "localIdentity(fixture?, options?)",
    "deriveClaims(attrs, { asked, nowMs })",
    "createIdentity(options)"
   ],
   "yaml": "verify:\n  driver: twilio-verify\n  key: secret:VERIFY_KEY\n  code: { ttl: 10m, tries: 5 }\n  limits: { per-number: 3, per-ip: 10, window: 10m }\n  email: { base: https://app.example.com, path: /verify/email }\n  identity: { in: digilocker, eu: onfido }\n  require: { in: substantial, eu: high }\n  policy: privacy-2026-10\n  drivers:\n    twilio-verify: { account: secret:TWILIO_ACCOUNT_SID, token: secret:TWILIO_AUTH_TOKEN, service: VAxxxxxxxx }\n    onfido: { token: secret:ONFIDO_TOKEN, workflow: wf_123, region: eu }\n",
   "example": "import { verify } from \"mainsdk/verify\";\n\nconst started = await verify.phone({ number: input.phone, for: user.id });\n// later, with the code the person typed\nconst check = await verify.phone.check({ id: started.id, code: input.code });\nif (check.ok) await db.users.for(user).update(user.id, { phone: check.number, phoneVerified: true });\n",
   "drivers": [
    {
     "id": "twilio-verify",
     "name": "Twilio Verify",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/twilio.html"
    },
    {
     "id": "bird",
     "name": "Bird",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/bird.html"
    },
    {
     "id": "local",
     "name": "Local outbox for development and tests",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "digilocker",
     "name": "DigiLocker",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/digilocker.html"
    },
    {
     "id": "onfido",
     "name": "Onfido",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/onfido.html"
    },
    {
     "id": "persona",
     "name": "Persona",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/persona.html"
    }
   ],
   "guarantees": [
    "A six-digit code that ends after ten minutes or five tries, kept only as a keyed digest",
    "Sends limited per number and per IP, behind the bot-defence door",
    "Email checks sent through email, with a getqr.id link",
    "The same answer, in about the same time, whether or not an address has an account",
    "Numbers normalised to E.164, labelled personal and audited masked",
    "Changing the driver changes no code",
    "Identity checks keep only the claims asked for, with a consent record"
   ],
   "checks": [
    "MAIN-VERIFY-0004",
    "MAIN-VERIFY-0006"
   ],
   "runtime_codes": [
    "MAIN-VERIFY-0001",
    "MAIN-VERIFY-0002",
    "MAIN-VERIFY-0003",
    "MAIN-VERIFY-0005",
    "MAIN-VERIFY-0007",
    "MAIN-VERIFY-0008"
   ],
   "features": [
    {
     "name": "Phone code with expiry",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "verify#phone"
    },
    {
     "name": "Behind the bot-defence door",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "verify#door"
    },
    {
     "name": "Email check with a getqr.id link",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "verify#email"
    },
    {
     "name": "Same answer whatever the input",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "verify#same-answer"
    },
    {
     "name": "Twilio Verify",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "verify#drivers"
    },
    {
     "name": "Identity checks by level and market",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "verify#identity"
    },
    {
     "name": "Check for a level a market's driver cannot reach",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "verify#checks"
    },
    {
     "name": "Digital credentials through wallets and DigiLocker",
     "phase": 5,
     "milestone": "1.6",
     "ticket": "digital-ids#wallets-digilocker"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/verify.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/verify.md"
  },
  {
   "id": "tenant",
   "name": "Tenants",
   "category": "identity",
   "summary": "Give every request a tenant and keep each customer's records, files, keys and logs apart, in the region they chose.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "tenancy",
   "calls": [
    "tenant.current()",
    "tenant.maybe()",
    "tenant.run(id, fn)",
    "tenant.system(reason, fn)",
    "tenant.anonymous(fn)",
    "tenant.create({ name, kind?, region, slug?, plan? })",
    "tenant.suspend(id, { reason })",
    "tenant.resume(id)",
    "tenant.export(id)",
    "tenant.erase(id, { reason })",
    "tenant.cancelErase(id)",
    "new MemoryDirectory()",
    "globMatch(globs, path)",
    "overrideLoosens(direction, base, { value })",
    "regionOfMarket(market)",
    "uuidv7(now)",
    "tenant.active()",
    "tenant.actor()",
    "tenant.as(who, fn)",
    "tenant.audit(action, id, attributes?)",
    "tenant.base",
    "tenant.carry()",
    "tenant.configValue(key)",
    "tenant.forgetPerson(user)",
    "tenant.handle(req, own, fn)",
    "tenant.hold(id)",
    "tenant.isGlobal(model)",
    "tenant.isolationTarget()",
    "tenant.loadOverrides(raw, base, direction)",
    "tenant.personHash(user)",
    "tenant.personalFor(person)",
    "tenant.prefix(key)",
    "tenant.principal()",
    "tenant.register(driver)",
    "tenant.release(id)",
    "tenant.resolve(req, own?)",
    "tenant.roles()",
    "tenant.runJob(id, fn)",
    "tenant.scope(call)",
    "tenant.setPlan(id, plan)",
    "tenant.store(stores)",
    "tenant.subject()",
    "tenant.sweep()",
    "tenant.tag()",
    "tenant.tenantOf(id)",
    "tenant.verify(key, data, signature)"
   ],
   "yaml": "tenancy:\n  isolation: shared                  # shared | schema | database\n  resolve: [apikey, session, subdomain]\n  host: acme.app\n  personal: true\n  directory: eu\n  roles: [owner, admin, member, viewer]\n  system: { allow: [src/admin/**, scripts/**] }\n  suspended: { allow: [billing.*, tenant.export] }\n  erase: { wait: 7d }\n  global: [Plan, Country]              # models shared by every tenant\n  databases:                           # isolation: database only\n    tnt_acme: secret:ACME_DATABASE_URL\n",
   "example": "import { db, tenant } from \"mainsdk\";\n\n// the adapter set the tenant from the API key, the session or the subdomain\nconst acme = tenant.current();\nconst orders = await db.orders.for(user).list({ limit: 20 });   // only acme's rows\n\n// a job has no request, so it names the tenant once\nawait tenant.run(job.tenantId, () => sendDigest());\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (core",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Every request has a tenant, and every query, key and file path is scoped to it",
    "A record, file or team of another tenant reads as not found",
    "Each tenant is pinned to one region and nothing is written elsewhere",
    "tenant.system() runs only in listed admin files and is audited with its reason",
    "Log records carry the tenant, and each tenant has its own exportable audit chain"
   ],
   "checks": [
    "MAIN-TENANT-0010",
    "MAIN-TENANT-0011",
    "MAIN-TENANT-0012",
    "MAIN-TENANT-0013"
   ],
   "runtime_codes": [
    "MAIN-TENANT-0001",
    "MAIN-TENANT-0002",
    "MAIN-TENANT-0003",
    "MAIN-TENANT-0004",
    "MAIN-TENANT-0005",
    "MAIN-TENANT-0006",
    "MAIN-TENANT-0007",
    "MAIN-TENANT-0008"
   ],
   "features": [
    {
     "name": "tenant@1 contract and the implicit default tenant",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#contract"
    },
    {
     "name": "Tenant in context with tenant.current() and tenant.maybe()",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#context"
    },
    {
     "name": "Tenant resolved from API key",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#resolvers"
    },
    {
     "name": "tenant.run() for jobs and audited tenant.system()",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#run-system"
    },
    {
     "name": "Tenant directory, tenant.create() and personal tenants",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#directory"
    },
    {
     "name": "Rows isolated by tenant_id with row-level security",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#db-shared"
    },
    {
     "name": "Schema-per-tenant and database-per-tenant isolation",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#db-schema-database"
    },
    {
     "name": "kv",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#kv-files-limits"
    },
    {
     "name": "Logs tagged and one audit chain per tenant",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#log-audit"
    },
    {
     "name": "A key per tenant for crypto-erasure",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#keys-tenant"
    },
    {
     "name": "Tenant in every access subject and resource",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#access-tenant"
    },
    {
     "name": "Suspend, export and erase a tenant",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#lifecycle"
    },
    {
     "name": "Flags by tenant and per-tenant config that only tightens",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#config-flags"
    },
    {
     "name": "Check rules for unfiltered queries and tenant.system() outside admin code",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#checks"
    },
    {
     "name": "Two-tenant test helper",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#two-tenants"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/tenant.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/tenant.md"
  },
  {
   "id": "apikeys",
   "name": "API keys for your customers",
   "category": "identity",
   "summary": "Issue each customer API keys with scopes, rotation without downtime, instant revoke and a rate limit, and verify them on every call.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "apikeys",
   "calls": [
    "apikeys.create({ tenant?, name, scopes, expiresAt?, rate? })",
    "apikeys.verify(req)",
    "apikeys.list({ limit?, cursor? })",
    "apikeys.rotate(keyId, { overlap?, expiresAt? })",
    "apikeys.revoke(keyId, { reason })",
    "API_KEY_DDL(const)",
    "new ApiKeys(o)",
    "dbApiKeyStore(o)",
    "isValidKey(text, prefix)",
    "keyRedactor(prefix)",
    "localApiKeyStore()",
    "parseApiKeys(raw, project?)"
   ],
   "yaml": "apikeys:\n  driver: db\n  prefix: acme\n  header: x-api-key\n  scopes: [orders.read, orders.write, reports.export]\n  max-ttl: 365d\n  role: member                         # the role a key's principal carries\n  limits:\n    default: 600/min\n",
   "example": "import { apikeys } from \"mainsdk\";\n\n// a signed-in admin issues a key; the secret is shown once\nconst { key, secret } = await apikeys.create({ name: \"CI\", scopes: [\"orders.read\"] });\n\n// in the customer-facing API\nconst { tenant, scopes } = await apikeys.verify(req);\n",
   "drivers": [
    {
     "id": "db",
     "name": "Your database (the tenant's region store)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/db.html"
    },
    {
     "id": "local",
     "name": "Local (tests and local mode)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "The secret is shown once; only its SHA-256 digest is stored",
    "A bad key fails offline, from its prefix and checksum, with no store lookup",
    "A key never widens what the tenant's policy allows",
    "Revoke takes effect within 5 seconds on every instance",
    "Keys are redacted in logs and caught in source by mainsdk check"
   ],
   "checks": [
    "MAIN-APIKEY-0010",
    "MAIN-APIKEY-0011"
   ],
   "runtime_codes": [
    "MAIN-APIKEY-0001",
    "MAIN-APIKEY-0002",
    "MAIN-APIKEY-0003",
    "MAIN-APIKEY-0004",
    "MAIN-APIKEY-0005",
    "MAIN-APIKEY-0006",
    "MAIN-APIKEY-0007",
    "MAIN-APIKEY-0008",
    "MAIN-APIKEY-0009",
    "MAIN-APIKEY-0012"
   ],
   "features": [
    {
     "name": "apikeys@1 contract and key format",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#contract"
    },
    {
     "name": "Keys with prefix",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#format"
    },
    {
     "name": "Verify with a digest lookup and the tenant set in context",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#verify"
    },
    {
     "name": "Scopes that are access permissions",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#scopes"
    },
    {
     "name": "List",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#lifecycle"
    },
    {
     "name": "Last use written in batches",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#last-used"
    },
    {
     "name": "Per-key rate limits and audit records",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#rate-audit"
    },
    {
     "name": "Key format redacted in logs",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#redaction"
    },
    {
     "name": "Check for committed keys and undefined scopes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#check"
    },
    {
     "name": "Recently verified keys served when the store is down",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#fail-static"
    },
    {
     "name": "db and memory drivers with a conformance suite",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#drivers"
    },
    {
     "name": "apikeys helpers in the testing kit",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#testing"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/apikeys.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/apikeys.md"
  },
  {
   "id": "access",
   "name": "Permissions",
   "category": "identity",
   "summary": "Decide who can do what to which record, with roles, attributes or relations on any engine.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "access",
   "calls": [
    "access.check({ subject, action, resource, context })",
    "access.decide({ subject, action, resource, context })",
    "access.require(subject, action, resource)",
    "access.explain(decisionId)",
    "access.can()",
    "access.role().can()",
    "access.list()",
    "access.relate(team, \"editor\", folder)",
    "access.rule(name, fn)",
    "access.who(action, resource)",
    "access.attribute(name, { subject, asset })",
    "access.attributes.register(name, source)",
    "access.checkWith(request, needs)",
    "access.setGroups(fn)"
   ],
   "yaml": "access:\n  driver: cedar            # built in, in process\n  models: [rbac, abac]\n  fresh-after-write: true\n  unreachable: { deny: true, cache: 30s }\n",
   "example": "import { access } from \"mainsdk\";\n\nconst decision = await access.check({\n  subject: user,\n  action: \"orders.update\",\n  resource: order,\n});\nif (!decision.allow) return new Response(\"Forbidden\", { status: 403 });\n",
   "drivers": [
    {
     "id": "cedar",
     "name": "Cedar (built in)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "opa",
     "name": "Open Policy Agent",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/open-policy-agent.html"
    },
    {
     "id": "openfga",
     "name": "OpenFGA",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/openfga.html"
    },
    {
     "id": "spicedb",
     "name": "SpiceDB",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/spicedb.html"
    },
    {
     "id": "cerbos",
     "name": "Cerbos",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/cerbos.html"
    },
    {
     "id": "permit",
     "name": "Permit.io",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/permit-io.html"
    },
    {
     "id": "oso",
     "name": "Oso",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/oso.html"
    },
    {
     "id": "avp",
     "name": "AWS Verified Permissions",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/aws-verified-permissions.html"
    }
   ],
   "guarantees": [
    "Deny by default, with no bypass path",
    "Denies when a remote engine is unreachable",
    "Lists filter at the database, not after fetching rows",
    "Every decision written to the audit log with the policy version"
   ],
   "checks": [
    "MAIN-ACCESS-0004",
    "MAIN-ACCESS-0011"
   ],
   "runtime_codes": [
    "MAIN-ACCESS-0007",
    "MAIN-ACCESS-0030",
    "MAIN-ACCESS-0031",
    "MAIN-ACCESS-0001",
    "MAIN-ACCESS-0020"
   ],
   "features": [
    {
     "name": "Token gates through external attributes",
     "phase": 2,
     "milestone": "0.7",
     "ticket": null
    },
    {
     "name": "Tenant in every subject and resource",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "tenancy#access-tenant"
    },
    {
     "name": "access@1 contract in AuthZEN shape",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#contract"
    },
    {
     "name": "In-process Cedar with RBAC and ABAC",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#cedar"
    },
    {
     "name": "Lists filtered in the database",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#listing"
    },
    {
     "name": "OpenFGA driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#openfga"
    },
    {
     "name": "SpiceDB driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#spicedb"
    },
    {
     "name": "OPA and Cerbos drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#opa-cerbos"
    },
    {
     "name": "Fail closed with a short cache",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#fail-closed"
    },
    {
     "name": "Capability gap and engine version checks",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#gap-check"
    },
    {
     "name": "Decision audit",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#audit"
    },
    {
     "name": "mainsdk test access",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "access#test-cmd"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/access.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/access.md"
  },
  {
   "id": "policy",
   "name": "Policy decisions",
   "category": "identity",
   "summary": "Decide every sensitive call in process from policies kept in git, and explain each decision.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.2",
   "spec": "policy",
   "calls": [
    "policy.decide()",
    "policy.explain()",
    "policy.test()",
    "policy.rollout()",
    "policy.decideRemote(input)"
   ],
   "yaml": "policy:\n  engine: cedar\n  placement: server\n  dir: policies/\n  mode: observe                # the default for new policies\n",
   "example": "import { policy } from \"mainsdk\";\n\nconst d = policy.decide({\n  subject: { type: \"user\", id: user.id },\n  action: { name: \"export.data\" },\n  resource: { type: \"report\", id: report.id },\n  context: { country: \"IN\" },\n});\nif (d.decision === \"deny\") console.log(policy.explain(d.id));\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Cedar (built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "opa",
     "name": "Open Policy Agent",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/open-policy-agent.html"
    }
   ],
   "guarantees": [
    "Deny when no policy permits",
    "In process, no network round trip per decision",
    "Lower layers only tighten, never loosen",
    "Keeps the last good policy set when a new one fails to load"
   ],
   "checks": [
    "MAIN-POLICY-0004"
   ],
   "runtime_codes": [
    "MAIN-POLICY-0001",
    "MAIN-POLICY-0002",
    "MAIN-POLICY-0003",
    "MAIN-POLICY-0005",
    "MAIN-POLICY-0006",
    "MAIN-POLICY-0009",
    "MAIN-POLICY-0011",
    "MAIN-POLICY-0012",
    "MAIN-PLAY-0001",
    "MAIN-PLAY-0002",
    "MAIN-PLAY-0003",
    "MAIN-PLAY-0004",
    "MAIN-PLAY-0005",
    "MAIN-POLICY-0007"
   ],
   "features": [
    {
     "name": "Deny-by-default decisions",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#decide"
    },
    {
     "name": "Policies folder with a content-hashed version",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#policies-folder"
    },
    {
     "name": "Observe, warn and enforce modes",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#modes"
    },
    {
     "name": "Org, repo, overlay and Fleet layers",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#layering"
    },
    {
     "name": "Explain a decision",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#explain"
    },
    {
     "name": "Policy tests and recorded replay",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#test"
    },
    {
     "name": "Staged rollout with automatic rollback",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#rollout"
    },
    {
     "name": "OPA driver for Rego",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#opa-driver"
    },
    {
     "name": "Last good policy set",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#fail-static"
    },
    {
     "name": "Server, edge or browser placement",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "policy#placement"
    },
    {
     "name": "Policy playground",
     "phase": 2,
     "milestone": "0.7",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/policy.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/policy.md"
  },
  {
   "id": "context",
   "name": "Context and shared context",
   "category": "identity",
   "summary": "One request context every interface reads, and consent-gated, first-party shared context (settings and progress) between peer apps of one owner on one machine.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "context",
   "calls": [
    "context.current()",
    "context.run(patch, fn)",
    "context.capture()",
    "context.restore(snap, fn)",
    "context.shared.put({ purpose, attributes?, events? })",
    "context.shared.get(purpose)",
    "context.shared.list({ purpose?, cursor?, limit? })",
    "context.shared.erase({ user?, purpose? })",
    "context.identity.reset()",
    "parseContext(raw, opts?)",
    "contextJsonSchema(extras?)",
    "deriveContextId(groupKey, { issuer, tenant, subject, epoch })"
   ],
   "yaml": "context:\n  group: acme-learning\n  identity: auth\n  purposes:\n    personalise:\n      description: \"Use the same language and theme in every Acme app\"\n      attributes: [locale, theme]\n      retention: 30d\n      share-with: [prj_01JD8X]\n  fields:\n    attributes:\n      favourite-subject: { type: string, enum: [maths, science] }\n    events: [lesson.completed]\n  sinks: []\n",
   "example": "import { context } from \"mainsdk\";\n\n// sent only to peers named under the purpose, and only with the person's consent on both sides\nawait context.shared.put({ purpose: \"personalise\", attributes: { locale: \"hi-IN\", theme: \"dark\" } });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (the app link)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Nothing flows without a recorded purpose and consent on both sides",
    "Never an advertising or device id; the person can reset the id",
    "Stays on the machine unless a sink inside the app is declared"
   ],
   "checks": [
    "MAIN-CONTEXT-0001",
    "MAIN-CONTEXT-0006"
   ],
   "runtime_codes": [
    "MAIN-CONTEXT-0002",
    "MAIN-CONTEXT-0003",
    "MAIN-CONTEXT-0004",
    "MAIN-CONTEXT-0005",
    "MAIN-CONTEXT-0007"
   ],
   "features": [
    {
     "name": "The mainsdk-context@1 schema and purposes",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "context#schema"
    },
    {
     "name": "Consent gating",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "context#consent"
    },
    {
     "name": "Shared identity from auth or a first-party id",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "context#identity"
    },
    {
     "name": "Context over the app link",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "context#bus"
    },
    {
     "name": "Minimisation, retention and tenant isolation",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "context#minimise"
    },
    {
     "name": "Export and erasure with the person",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "context#lifecycle"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/context.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/context.md"
  },
  {
   "id": "db",
   "name": "Data and records",
   "category": "data",
   "summary": "Read and write records that are private to their owner, stored in the owner's region, with labelled fields.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "db",
   "calls": [
    "db.<model>.for(user).list()",
    "db.<model>.for(user).get()",
    "db.<model>.for(user).create()",
    "db.<model>.for(user).update()",
    "db.<model>.for(user).delete()",
    "db.<model>.for(user).restore()",
    "db.<model>.for(user).trashed().list()",
    "db.<model>.public()",
    "db.transaction()",
    "model(schema, options)",
    "etag(version)",
    "ifMatch(request)",
    "preconditionFailed(error)",
    "new DbCollection(sql, tenancy, name, fields)",
    "new FileCollection(name, file)",
    "new LocalCollection(name)",
    "openCollection(backend, name, fields)",
    "new RuntimeDb(opts)",
    "new Outbox(opts)",
    "new VersionConflict(currentVersion, fromIfMatch?)",
    "spanMs(v)",
    "models(defs)",
    "fingerprint(sql)",
    "queryScope(kind, fn, opts?)",
    "trackQueries(fn)",
    "parseDbSettings(cfg, opts?)"
   ],
   "yaml": "data: { store: postgres, residency: { default: eu, by-market: { in: in, us: us } } }\ndb:\n  driver: postgres                        # owner default; the db spec's example uses neon\n  regions: { us: secret:DB_US, eu: secret:DB_EU, in: secret:DB_IN }\n  fields:\n    user.email:    { label: personal }\n    patient.notes: { label: phi, encrypt: true }\n",
   "example": "import { auth, db } from \"mainsdk\";\n\nexport async function GET(req: Request) {\n  const session = await auth.require(req);\n  const orders = await db.orders.for(session.user).list({ limit: 20 });  // no .for(), no compile\n  return Response.json(orders);\n}\n",
   "drivers": [
    {
     "id": "postgres",
     "name": "Postgres",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/postgres.html"
    },
    {
     "id": "neon",
     "name": "Neon",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/neon.html"
    },
    {
     "id": "supabase",
     "name": "Supabase",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/supabase.html"
    },
    {
     "id": "sqlite",
     "name": "SQLite (local only)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/sqlite.html"
    }
   ],
   "guarantees": [
    "No query without an owner scope",
    "Each record stored in its owner's region",
    "Personal fields carry a label; card numbers are never stored",
    "Repeated idempotency keys never write twice"
   ],
   "checks": [
    "MAIN-DATA-0013",
    "MAIN-DATA-0020",
    "MAIN-DATA-0021",
    "MAIN-DATA-0030",
    "MAIN-DATA-0040",
    "MAIN-DATA-0041",
    "MAIN-DATA-0042",
    "MAIN-DATA-0043",
    "MAIN-DATA-0044",
    "MAIN-DATA-0045",
    "MAIN-DATA-0051",
    "MAIN-DATA-0055",
    "MAIN-DATA-0056",
    "MAIN-DATA-0061",
    "MAIN-DATA-0062",
    "MAIN-DATA-0065",
    "MAIN-DATA-0070",
    "MAIN-DATA-0071",
    "MAIN-DATA-0180",
    "MAIN-DATA-0181",
    "MAIN-DATA-0182"
   ],
   "runtime_codes": [
    "MAIN-DATA-0007",
    "MAIN-DATA-0008",
    "MAIN-DATA-0009",
    "MAIN-DATA-0031",
    "MAIN-DATA-0032",
    "MAIN-DATA-0050",
    "MAIN-DATA-0052",
    "MAIN-DATA-0053",
    "MAIN-DATA-0054",
    "MAIN-DATA-0060",
    "MAIN-DATA-0063",
    "MAIN-DATA-0066",
    "MAIN-DATA-0064"
   ],
   "features": [
    {
     "name": "db@1 contract and field labels",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db#contract"
    },
    {
     "name": "Owner scope enforced by types",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db#query-scope"
    },
    {
     "name": "Region stores for US, EU and India",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db#regions"
    },
    {
     "name": "Cross-region join check",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db#join-check"
    },
    {
     "name": "Field labels enforced",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db#labels"
    },
    {
     "name": "Field encryption through keys",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db#field-encryption"
    },
    {
     "name": "Idempotent writes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db#idempotent-writes"
    },
    {
     "name": "Postgres, Neon and local SQLite drivers with Prisma and Drizzle",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db#drivers"
    },
    {
     "name": "Residency vaults and tokenisation",
     "phase": 3,
     "milestone": "1.2",
     "ticket": null
    },
    {
     "name": "Pages from list calls with keyset cursors",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "pagination#db-list"
    },
    {
     "name": "Delete",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db-writes#trash"
    },
    {
     "name": "Row versions with ETag and If-Match",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "db-writes#versions"
    },
    {
     "name": "Query timeouts",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Slow-query and repeated-query detection",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Safe migrations and mainsdk db migrate",
     "phase": 1,
     "milestone": "0.4",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/db.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/db.md"
  },
  {
   "id": "calendar",
   "name": "Calendars",
   "category": "data",
   "summary": "Store events with recurrence and time zones, import and export iCalendar, and show free/busy without showing details.",
   "status": "building",
   "stability": "experimental",
   "phase": 3,
   "milestone": "1.2",
   "spec": "calendar",
   "calls": [
    "calendar.createCalendar({ id?, name, owner? })",
    "calendar.calendars()",
    "calendar.create(cal, event)",
    "calendar.get(id)",
    "calendar.update(id, patch, { scope, occurrence?, version })",
    "calendar.delete(id, { scope, occurrence? })",
    "calendar.list({ calendars, from, to, cursor?, limit? })",
    "calendar.freeBusy({ calendars?, members?, from, to })",
    "calendar.import(cal, source)",
    "calendar.export(cal, { from?, to? })",
    "calendar.subscribe(cal)"
   ],
   "yaml": "calendar:\n  store: db\n  max-occurrences: 5000\n  max-window: 366d\n",
   "example": "import { calendar } from \"mainsdk\";\n\nawait calendar.create(\"team\", {\n  title: \"Stand-up\", start: \"2026-11-02T09:00\", end: \"2026-11-02T09:15\",\n  timeZone: \"America/New_York\", rrule: \"FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR\",\n});\n",
   "drivers": [
    {
     "id": "db",
     "name": "Your database (the tenant's region store)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/db.html"
    },
    {
     "id": "local",
     "name": "Local (tests and mainsdk dev)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "caldav",
     "name": "CalDAV",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/caldav.html"
    }
   ],
   "guarantees": [
    "Wall-clock times stay put across daylight-saving changes",
    "Recurrence expansion is bounded",
    "Free/busy never shows titles, places or attendees to someone who can't read the calendar"
   ],
   "checks": [
    "MAIN-CAL-0001",
    "MAIN-CAL-0002"
   ],
   "runtime_codes": [
    "MAIN-CAL-0003",
    "MAIN-CAL-0004",
    "MAIN-CAL-0005",
    "MAIN-CAL-0006",
    "MAIN-CAL-0007",
    "MAIN-CAL-0008"
   ],
   "features": [
    {
     "name": "calendar@1 contract",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "calendar#contract"
    },
    {
     "name": "Events in IANA time zones",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "calendar#events"
    },
    {
     "name": "RFC 5545 recurrence",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "calendar#rrule"
    },
    {
     "name": "Edit one, following or all occurrences",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "calendar#edit-scope"
    },
    {
     "name": "iCalendar import and export",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "calendar#ical"
    },
    {
     "name": "Free/busy",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "calendar#freebusy"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/calendar.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/calendar.md"
  },
  {
   "id": "imports",
   "name": "Import files and pull data",
   "category": "data",
   "summary": "Read CSV, XLSX, JSON and NDJSON files, map their columns with a confirmed mapping, and pull from outside sources on a schedule.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "imports",
   "calls": [
    "imports.preview(file, opts)",
    "imports.file(file, opts)",
    "imports.get(runId)",
    "imports.list(opts?)",
    "imports.cancel(runId, opts)",
    "imports.source(name, opts)",
    "imports.enable(name, opts?)",
    "imports.disable(name)",
    "imports.pull(name, opts?)",
    "imports.conflicts(name, opts?)"
   ],
   "yaml": "imports:\n  inline-max: 5MB\n  batch: 500\n  max-rows: 1000000\n  max-columns: 500\n  json-max: 20MB\n  sources:\n    crm-sheet: { driver: google-sheets, spreadsheet: \"1AbC…\", range: \"Contacts!A:F\", auth: connection:google, every: 15m }\n    partner-api:\n      driver: rest\n      url: https://api.partner.com/v1/items\n      auth: secret:PARTNER_TOKEN\n      every: 5m\n      full-every: 24h\n      deletes-guard: 50%\n",
   "example": "import { imports } from \"mainsdk/imports\";\nimport { Contact } from \"./schema\";\n\nconst preview = await imports.preview(file, { schema: Contact, key: \"email\" });\nconst run = await imports.file(file, {\n  schema: Contact, key: \"email\", into: \"contacts\",\n  mapping: { \"E-mail\": \"email\", \"Full name\": \"name\" },\n});\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in parsers for CSV, XLSX, JSON and NDJSON",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "google-sheets",
     "name": "Google Sheets",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/googlesheets.html"
    },
    {
     "id": "airtable",
     "name": "Airtable",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/airtable.html"
    },
    {
     "id": "rest",
     "name": "REST API with a cursor",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/rest.html"
    }
   ],
   "guarantees": [
    "Rows are written only after the person confirms the mapping",
    "A repeated run with the same file, target, key and mapping writes nothing twice",
    "Deletions from a source happen only on a full pass, behind a guard",
    "An unsafe workbook is refused before its rows are read"
   ],
   "checks": [
    "MAIN-IMPORT-0020"
   ],
   "runtime_codes": [
    "MAIN-IMPORT-0001",
    "MAIN-IMPORT-0002",
    "MAIN-IMPORT-0003",
    "MAIN-IMPORT-0004",
    "MAIN-IMPORT-0005",
    "MAIN-IMPORT-0006",
    "MAIN-IMPORT-0010",
    "MAIN-IMPORT-0011",
    "MAIN-IMPORT-0012"
   ],
   "features": [
    {
     "name": "Read CSV, XLSX, JSON and NDJSON",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#read"
    },
    {
     "name": "Map columns with a confirmed mapping",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#mapping"
    },
    {
     "name": "Dry run with a row error report",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#dry-run-report"
    },
    {
     "name": "Resumable batched writes",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#upsert-resume"
    },
    {
     "name": "Large files run as a workflow with progress",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#workflow-progress"
    },
    {
     "name": "Excel workbooks with safety limits",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#xlsx"
    },
    {
     "name": "Scheduled pulls from sources",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#pull"
    },
    {
     "name": "Conflict rules and guarded deletes",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#conflicts-deletes"
    },
    {
     "name": "Google Sheets and Airtable sources",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#drivers-sheets-airtable"
    },
    {
     "name": "Checks for undeclared sources",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "imports#checks"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/imports.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/imports.md"
  },
  {
   "id": "kv",
   "name": "Key-value store and cache",
   "category": "data",
   "summary": "Keep small values and counters under a key with an optional expiry, scoped to the tenant.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "kv",
   "calls": [
    "kv.get(key)",
    "kv.set(key, value, { ttl })",
    "kv.delete(key)",
    "kv.incr(key, { by, ttl })",
    "new RespClient(opts)",
    "new RespError(message)",
    "new RespSubscriber(opts, backoff)",
    "redisKv(o)",
    "localKv(o?)"
   ],
   "yaml": "kv:\n  driver: redis\n  redis: { url: secret:REDIS_URL, prefix: myapp }\n",
   "example": "import { kv } from \"mainsdk\";\n\nconst hits = await kv.incr(`rate:${ip}`, { ttl: \"1m\" });   // a fixed one-minute window\nawait kv.set(`draft:${user.id}`, { step: 2 }, { ttl: \"1d\" });\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local (memory or one file)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "redis",
     "name": "Redis",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/redis.html"
    },
    {
     "id": "upstash",
     "name": "Upstash Redis",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/upstash.html"
    },
    {
     "id": "vercel-runtime-cache",
     "name": "Vercel Runtime Cache",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/vercel.html"
    },
    {
     "id": "cloudflare-kv",
     "name": "Cloudflare KV",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/cloudflare-kv.html"
    }
   ],
   "guarantees": [
    "Keys scoped per tenant",
    "A ttl is a positive duration, or the write fails",
    "Redis connections pass the network allow list first"
   ],
   "checks": [],
   "runtime_codes": [
    "MAIN-KV-0001",
    "MAIN-KV-0002",
    "MAIN-KV-0003"
   ],
   "features": [
    {
     "name": "Get",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Counters with a fixed window",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Cache wrap and tag invalidation (the cache interface)",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "http-cache#wrap"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/kv.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/kv.md"
  },
  {
   "id": "cache",
   "name": "Cache",
   "category": "data",
   "summary": "Memoise any call with keys that always carry the tenant and region, and keep personal responses out of shared caches.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "http-cache",
   "calls": [
    "cache.wrap(key, fn, { ttl, swr?, tags?, scope? })",
    "cache.get(key, { scope? })",
    "cache.set(key, value, { ttl, tags?, scope? })",
    "cache.delete(key | { tag }, { scope? })",
    "new Cache(opts)",
    "parseCache(raw, hasKv?)",
    "cacheIssues(raw)",
    "serveRead(action, req, opts)",
    "etagMatches(header, etag)",
    "weakEtag(output)"
   ],
   "yaml": "cache:\n  driver: kv\n  timeout: 50ms\n  max-value: 256KB\n  memory: { max-entries: 1000, max-ttl: 30s }\n",
   "example": "import { cache } from \"mainsdk/cache\";\n\nconst top = await cache.wrap(\"top-products\", () => loadTopProducts(), { ttl: \"5m\", tags: [\"products\"] });\nawait cache.delete({ tag: \"products\" });   // every instance drops it\n",
   "drivers": [
    {
     "id": "kv",
     "name": "The kv store with an in-process tier",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/kv.html"
    },
    {
     "id": "local",
     "name": "In-process only",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Keys carry the tenant and region; no call reads another tenant's entry",
    "Personal responses go out private, no-store; shared caching only when declared",
    "A slow or failed store never fails or blocks a request",
    "One call per key per instance under load"
   ],
   "checks": [
    "MAIN-CACHE-0011",
    "MAIN-CACHE-0012"
   ],
   "runtime_codes": [
    "MAIN-CACHE-0001",
    "MAIN-CACHE-0002",
    "MAIN-CACHE-0003",
    "MAIN-CACHE-0004",
    "MAIN-CACHE-0005"
   ],
   "features": [
    {
     "name": "Private by default on credentialed responses",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "http-cache#headers"
    },
    {
     "name": "Declared public caching on read actions",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "http-cache#public-actions"
    },
    {
     "name": "ETags and 304 after the access check",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "http-cache#etag"
    },
    {
     "name": "Wrap with single flight and stale-while-revalidate",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "http-cache#wrap"
    },
    {
     "name": "Fail-static reads",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "http-cache#fail-static"
    },
    {
     "name": "Key and tag invalidation across instances",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "http-cache#invalidation"
    },
    {
     "name": "No personal data in app-scoped entries; erased with the tenant",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "http-cache#privacy"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/cache.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/cache.md"
  },
  {
   "id": "search",
   "name": "Search",
   "category": "data",
   "summary": "Index records and search them with facets, highlighting and typo tolerance, filtered by tenant and access inside the query.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "search",
   "calls": [
    "search.index()",
    "search.query({ for: user })",
    "search.remove(index, ids)",
    "search.status(index)",
    "search.reindex(index, { region?, tenant? })"
   ],
   "yaml": "search:\n  driver: db\n  timeout: 2s\n  max-depth: 1000\n  # used when the driver is typesense or meilisearch\n  typesense: { key: \"secret:TYPESENSE_KEY\", regions: { eu: \"https://eu.search.example.com\" } }\n  meilisearch: { key: \"secret:MEILI_KEY\", regions: { eu: \"https://eu.meili.example.com\" } }\n  indexes:\n    posts:\n      model: posts\n      access: posts.read\n      fields:\n        text: { search: true, weight: 2 }\n        tags: { facet: true, filter: true }\n        createdAt: { sort: true, filter: true }\n",
   "example": "import { search } from \"mainsdk\";\n\nconst page = await search.query({ index: \"posts\", q: \"launch\", for: user, facets: [\"tags\"], highlight: true });\n",
   "drivers": [
    {
     "id": "db",
     "name": "Built in on db (Postgres full-text search, SQLite FTS5 locally)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/postgresql.html"
    },
    {
     "id": "typesense",
     "name": "Typesense",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/typesense.html"
    },
    {
     "id": "meilisearch",
     "name": "Meilisearch",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/meilisearch.html"
    },
    {
     "id": "local",
     "name": "Local (in process",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "algolia",
     "name": "Algolia",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/algolia.html"
    },
    {
     "id": "pgvector",
     "name": "pgvector",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/postgres.html"
    }
   ],
   "guarantees": [
    "Every query is filtered by the tenant and by access inside the engine's query, never after it",
    "Facet counts and totals count only what the searcher may read",
    "Index updates go through the job outbox, never the request path",
    "One index per region; a document is indexed only in its tenant's region",
    "Labelled fields stay out of indexes unless allowed; erasure reaches every index"
   ],
   "checks": [
    "MAIN-SEARCH-0010",
    "MAIN-SEARCH-0011",
    "MAIN-SEARCH-0012",
    "MAIN-SEARCH-0013",
    "MAIN-SEARCH-0014"
   ],
   "runtime_codes": [
    "MAIN-SEARCH-0001",
    "MAIN-SEARCH-0002",
    "MAIN-SEARCH-0003",
    "MAIN-SEARCH-0004",
    "MAIN-SEARCH-0005",
    "MAIN-SEARCH-0006",
    "MAIN-SEARCH-0007",
    "MAIN-SEARCH-0008",
    "MAIN-SEARCH-0009"
   ],
   "features": [
    {
     "name": "search@1 contract and index declarations",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#contract"
    },
    {
     "name": "db driver on Postgres full-text search and SQLite FTS5",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#db-driver"
    },
    {
     "name": "Typo tolerance",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#typo"
    },
    {
     "name": "Facets",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#facets"
    },
    {
     "name": "Cursor pages up to max-depth",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#paging"
    },
    {
     "name": "Tenant and access filter inside the query",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#access-filter"
    },
    {
     "name": "Index updates through the outbox",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#outbox"
    },
    {
     "name": "Fail fast when the engine is down",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#fail-static"
    },
    {
     "name": "One index per region",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#regions"
    },
    {
     "name": "mainsdk search reindex",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#reindex"
    },
    {
     "name": "Labels and erasure",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#privacy"
    },
    {
     "name": "Typesense and Meilisearch drivers",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#engines"
    },
    {
     "name": "Search checks",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "search#checks"
    },
    {
     "name": "Vector search",
     "phase": 2,
     "milestone": "0.6",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/search.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/search.md"
  },
  {
   "id": "feeds",
   "name": "Feeds and timelines",
   "category": "data",
   "summary": "Follows, blocks and mutes, home timelines that fan out on write and on read, and trending per region, with a feed that has an end.",
   "status": "building",
   "stability": "experimental",
   "phase": 1,
   "milestone": "0.4",
   "spec": "feeds",
   "calls": [
    "feeds.follow(follower, target)",
    "feeds.unfollow(follower, target)",
    "feeds.followers(user)",
    "feeds.following(user)",
    "feeds.counts(user)",
    "feeds.requests(user)",
    "feeds.accept(user, from)",
    "feeds.decline(user, from)",
    "feeds.block(user, target)",
    "feeds.unblock(user, target)",
    "feeds.mute(user, target, { until? })",
    "feeds.unmute(user, target)",
    "feeds.relation(a, b)",
    "feeds.publish(author, { id, visibility })",
    "feeds.edit(author, id)",
    "feeds.remove(by, id)",
    "feeds.timeline(user, { order?, cursor?, continue? })",
    "feeds.profile(viewer, author)",
    "feeds.ranker(fn)",
    "feeds.count(key, { by })",
    "feeds.trending({ window?, k? })"
   ],
   "yaml": "feeds:\n  driver: db\n  items: posts\n  fanout: { threshold: 10000, batch: 500 }\n  timeline: { order: chronological }\n  page: { end: true, pause_after: 200 }\n  trending: { window: 1h, k: 20, min-count: 25 }\n",
   "example": "import { feeds } from \"mainsdk\";\n\nawait feeds.publish(user, { id: post.id, visibility: \"public\" });\nconst home = await feeds.timeline(user, { limit: 20 });   // ends with a caught-up or pause entry\n",
   "drivers": [
    {
     "id": "db",
     "name": "Built in on db",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/db.html"
    },
    {
     "id": "local",
     "name": "Local (in process",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "New items never written to timelines in the request; authors see their own items at once",
    "Edits, deletes and moderation apply on the next read of every timeline",
    "Chronological by default; a ranked timeline explains each item and can always be switched off",
    "Every timeline has an end and a pause, and new items arrive as a count, never pushed into the list",
    "Trending stores counts only, never a person"
   ],
   "checks": [
    "MAIN-FEED-0010",
    "MAIN-FEED-0011",
    "MAIN-FEED-0012",
    "MAIN-FEED-0013"
   ],
   "runtime_codes": [
    "MAIN-FEED-0001",
    "MAIN-FEED-0002",
    "MAIN-FEED-0003",
    "MAIN-FEED-0004",
    "MAIN-FEED-0005",
    "MAIN-FEED-0006",
    "MAIN-FEED-0007",
    "MAIN-FEED-0008",
    "MAIN-FEED-0009"
   ],
   "features": [
    {
     "name": "feeds@1 contract",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#contract"
    },
    {
     "name": "Follow graph with requests and counts",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#graph"
    },
    {
     "name": "Block and mute",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#block-mute"
    },
    {
     "name": "Fan-out on write",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#fanout-write"
    },
    {
     "name": "Fan-out on read above a threshold and read-your-writes",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#fanout-read"
    },
    {
     "name": "Edits and deletes shown at once",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#edits-deletes"
    },
    {
     "name": "Chronological and ranked timelines",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#order"
    },
    {
     "name": "Caught-up end, pause and children's rules",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#ethics"
    },
    {
     "name": "Trending per region",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#trending"
    },
    {
     "name": "Tenants and regions",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#tenancy-regions"
    },
    {
     "name": "Erasure",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#erase"
    },
    {
     "name": "Feed checks",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "feeds#checks"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/feeds.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/feeds.md"
  },
  {
   "id": "sync",
   "name": "Offline-first replication",
   "category": "data",
   "summary": "Keep a local copy of a person's rows on their device, work offline, and merge changes back to the server without losing edits.",
   "status": "building",
   "stability": "experimental",
   "phase": 3,
   "milestone": "1.1",
   "spec": "sync",
   "calls": [
    "sync.open({ tables, for, transport, storage?, now?, ids? })",
    "replica.status()",
    "replica.flush({ signal })",
    "replica.conflicts()",
    "replica.close()",
    "table.insert(values, { id? })",
    "table.update(id, patch)",
    "table.delete(id)",
    "table.get(id)",
    "table.list()",
    "server.push(who, request)",
    "server.pull(who, request)",
    "server.erase(subject, { tenant })",
    "server.transport(who)",
    "server.handler(who)",
    "httpTransport(url, { fetch })",
    "memoryStorage()"
   ],
   "yaml": "sync:\n  driver: local\n  tables:\n    notes: { version: 1, conflicts: last-writer-wins }\n  quota: { rows: 100000, bytes: 50MB, batch: 500 }\n  max-clock-drift: 5m\n  page: 500\n",
   "example": "import { sync } from \"mainsdk/experimental/sync\";\n\nconst replica = await sync.open({ tables: [\"notes\"], for: user, transport });   // local copy, scoped to the user's tenant\nawait replica.notes.insert({ text: \"works offline\" });                          // merged when the device is back online\nawait replica.flush();\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local (in memory",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "electric",
     "name": "ElectricSQL",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/electric.html"
    },
    {
     "id": "powersync",
     "name": "PowerSync",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/powersync.html"
    }
   ],
   "guarantees": [
    "A device holds only rows its person may read in their tenant, checked on every pushed and pulled row",
    "Offline edits are never dropped silently; two edits to different fields both survive and every clash is recorded",
    "Deletes travel as tombstones, and erasing a person reaches every device replica",
    "A device on another schema version is refused and keeps its queue"
   ],
   "checks": [
    "MAIN-SYNC-0007"
   ],
   "runtime_codes": [
    "MAIN-SYNC-0001",
    "MAIN-SYNC-0002",
    "MAIN-SYNC-0003",
    "MAIN-SYNC-0004",
    "MAIN-SYNC-0005",
    "MAIN-SYNC-0006"
   ],
   "features": [
    {
     "name": "sync@1 contract and the sync main.yaml section",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#contract"
    },
    {
     "name": "Field-level merge on hybrid logical clocks",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#merge"
    },
    {
     "name": "Conflict rules per table",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#merge"
    },
    {
     "name": "Tenant and policy checked on every row",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#policy"
    },
    {
     "name": "Deletes as tombstones",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#tombstones"
    },
    {
     "name": "Schema version refusal",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#schema-version"
    },
    {
     "name": "Erasure on every replica",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#erase"
    },
    {
     "name": "Per-tenant quotas",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#quotas"
    },
    {
     "name": "Local replica store and testing rig",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#local"
    },
    {
     "name": "HTTP handler and transport",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#http"
    },
    {
     "name": "Sync checks",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "sync#checks"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/sync.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/sync.md"
  },
  {
   "id": "ai",
   "name": "AI calls",
   "category": "ai",
   "summary": "Call any model through one interface, with keys out of your code, limits per user and every call audited.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "ai",
   "calls": [
    "ai.generate({ model, prompt, feature, user })",
    "ai.embed({ model, input, feature })",
    "ai.tool(action)",
    "ai.stream({ model, prompt | messages, feature })",
    "ai.extract({ input, schema, model, feature })",
    "ai.classify({ input, labels, model, feature })",
    "ai.summarize({ input, length, model, feature })",
    "ai.disclose(generation)",
    "ai.extractor(schema, { model, feature })",
    "ai.image.generate({ model, prompt, feature })",
    "ai.image.edit({ model, image, prompt, feature })",
    "ai.image.inpaint({ model, image, mask, prompt, feature })",
    "ai.image.variations({ model, image, count, feature })",
    "ai.credentials.verify(file, { user })",
    "ai.run({ model, feature, agent, goal, tools, maxSteps, budget })",
    "ai.run.stream(opts)",
    "ai.run.resume(runId)",
    "ai.run.get(runId)",
    "ai.batch.submit({ model, feature, items })",
    "ai.batch.get(id)",
    "ai.batch.cancel(id, { reason })",
    "ai.video.generate({ model, prompt, feature, seconds })",
    "ai.video.get(id)",
    "ai.video.cancel(id, { reason })",
    "ai.voice.session({ model, voice, instructions, feature, agent })",
    "ai.voice.calls.handle(req, session)",
    "ai.voice.calls.dial({ to, from, session })",
    "ai.voice.calls.upgrade({ req, socket, head })",
    "ai.voice.rooms.join({ room, session })",
    "ai.voice.rooms.token({ room, user })",
    "ai.speech({ model, voice, text | input, format, feature })",
    "ai.transcribe({ model, audio, feature })",
    "ai.transcribe.stream({ model, audio, format, sampleRate, feature })",
    "ai.voices.list()",
    "ai.voices.clone({ name, samples, statement, feature })",
    "ai.voices.delete(id, { reason })",
    "ai.voices.share(id, share)",
    "createAi(opts)",
    "new ProviderError(code, message, opts?)",
    "fakeImageDriver(opts?)",
    "fileStore(dir)",
    "gatewayImageDriver(opts)",
    "inlineWorkflows(timers?)",
    "localAiDriver(opts?)",
    "localImageDriver(opts?)",
    "localSigner(opts)",
    "looksLikeInjection(text)",
    "meetsData(declared, required)",
    "memoryStore()",
    "openaiImageDriver(opts)",
    "parseSse(body)",
    "signAsset(asset, mime, { req, signer })",
    "verifyAsset(asset, mime, opts?)",
    "ai.html(text, opts?)"
   ],
   "yaml": "ai:\n  driver: anthropic                 # anthropic | openai | local | fake; no driver means fake; bedrock is not built\n  models: [anthropic/claude-*]      # allowlist\n  data: { training: never, retention: zero }\n  redact: [personal, sensitive, child, phi]\n  limits: { per-user: $1/day, per-feature: { order-summary: $200/month }, total: $500/month }\n",
   "example": "import { ai } from \"mainsdk\";\n\nconst summary = await ai.generate({\n  model: \"anthropic/claude-sonnet-4-5\",\n  prompt: `Summarise order ${order.id}`,\n  feature: \"order-summary\",   // spend limits and audit per feature\n  user,                       // and per user\n});\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local (no account",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "anthropic",
     "name": "Anthropic",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/anthropic.html"
    },
    {
     "id": "openai",
     "name": "OpenAI",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/openai.html"
    },
    {
     "id": "gateway",
     "name": "AI gateway",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/ai-gateway.html"
    },
    {
     "id": "bedrock",
     "name": "Amazon Bedrock",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/amazon-bedrock.html"
    },
    {
     "id": "ollama",
     "name": "Ollama",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/ollama.html"
    },
    {
     "id": "llama-cpp",
     "name": "llama.cpp server",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/llama-cpp.html"
    },
    {
     "id": "openai-compatible",
     "name": "OpenAI-compatible server",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/openai-compatible.html"
    }
   ],
   "guarantees": [
    "Provider keys never in code, only secret references",
    "Providers that train on or keep prompts are refused",
    "Personal data redacted before the model sees it",
    "Spend limits per user, per feature and per month",
    "Every call audited with feature, model, tokens and cost, never the prompt",
    "Typed output checked against its schema; invalid output is repaired, then refused, never returned"
   ],
   "checks": [
    "MAIN-AI-0001",
    "MAIN-AI-0044",
    "MAIN-AI-0049",
    "MAIN-AI-0068",
    "MAIN-AI-0070",
    "MAIN-AI-0079",
    "MAIN-VOICE-0010",
    "MAIN-VOICE-0011",
    "MAIN-EVAL-0002",
    "MAIN-EVAL-0003",
    "MAIN-EVAL-0004"
   ],
   "runtime_codes": [
    "MAIN-AI-0005",
    "MAIN-AI-0007",
    "MAIN-AI-0010",
    "MAIN-AI-0011",
    "MAIN-AI-0014",
    "MAIN-AI-0040",
    "MAIN-AI-0041",
    "MAIN-AI-0042",
    "MAIN-AI-0043",
    "MAIN-AI-0045",
    "MAIN-AI-0046",
    "MAIN-AI-0047",
    "MAIN-AI-0048",
    "MAIN-AI-0060",
    "MAIN-AI-0061",
    "MAIN-AI-0062",
    "MAIN-AI-0063",
    "MAIN-AI-0064",
    "MAIN-AI-0065",
    "MAIN-AI-0066",
    "MAIN-AI-0067",
    "MAIN-AI-0071",
    "MAIN-AI-0072",
    "MAIN-AI-0073",
    "MAIN-AI-0074",
    "MAIN-AI-0075",
    "MAIN-AI-0078",
    "MAIN-AI-0080",
    "MAIN-AI-0050",
    "MAIN-AI-0051",
    "MAIN-AI-0052",
    "MAIN-AI-0053",
    "MAIN-VOICE-0001",
    "MAIN-VOICE-0002",
    "MAIN-VOICE-0003",
    "MAIN-VOICE-0004",
    "MAIN-VOICE-0005",
    "MAIN-AI-0069",
    "MAIN-AI-0002",
    "MAIN-AI-0003",
    "MAIN-AI-0004",
    "MAIN-AI-0006",
    "MAIN-AI-0008",
    "MAIN-AI-0012",
    "MAIN-AI-0013",
    "MAIN-AI-0015",
    "MAIN-AI-0016",
    "MAIN-AI-0017",
    "MAIN-AI-0018",
    "MAIN-AI-0019",
    "MAIN-AI-0020",
    "MAIN-AI-0021",
    "MAIN-AI-0022",
    "MAIN-AI-0023",
    "MAIN-AI-0076",
    "MAIN-EVAL-0001",
    "MAIN-EVAL-0005",
    "MAIN-VOICE-0006"
   ],
   "features": [
    {
     "name": "Streaming and schema-checked typed output",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "ai#stream-structured"
    },
    {
     "name": "Generate and embed",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Tools that are MainSDK actions",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "ai#tools"
    },
    {
     "name": "Extract typed data from text with a schema",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "ai#extract"
    },
    {
     "name": "Classify into one of your labels",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "ai#classify-summarize"
    },
    {
     "name": "Summaries in short",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "ai#classify-summarize"
    },
    {
     "name": "Model parser for parse.first",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "ai#extractor"
    },
    {
     "name": "Schema and repairs in generate",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "ai#generate-schema"
    },
    {
     "name": "Every model verb through redaction",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "ai#verb-guards"
    },
    {
     "name": "Images, PDFs, audio and video frames as prompt parts through files",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ai-multimodal#verbs"
    },
    {
     "name": "Image generate, edit, inpaint and variations, output through files",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ai-image#edit"
    },
    {
     "name": "Disclosure metadata and C2PA content credentials on AI images",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ai-image#c2pa"
    },
    {
     "name": "Agent loop with ai.run: action tools, bounds, approval, handoff and delegation",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ai-run#loop"
    },
    {
     "name": "Fallback routes that never weaken data use or region",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ai-routing#fallback"
    },
    {
     "name": "Prompt caching and a tenant-safe response cache",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ai-routing#response-cache"
    },
    {
     "name": "Batch jobs at batch prices",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ai-routing#batch"
    },
    {
     "name": "Local models through Ollama, llama.cpp and OpenAI-compatible servers",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "ai-local#drivers"
    },
    {
     "name": "Eval sets with a CI regression gate and the result in the seal",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "ai-evals#gate"
    },
    {
     "name": "AI video as a durable long job",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "ai-video#job"
    },
    {
     "name": "Realtime voice agents on phone lines and WebRTC rooms",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "ai-voice#session"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/ai.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/ai.md"
  },
  {
   "id": "action",
   "name": "Actions and agent tools",
   "category": "ai",
   "summary": "Write an action once and get an API endpoint, a server action, a CLI command and an MCP tool with the same checks.",
   "status": "preview",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "mcp-actions",
   "calls": [
    "action({ name, intent, input, risk, run })",
    "mcpServer({ actions })",
    "mcpClient({ servers })",
    "new Idempotency(opts)",
    "idempotencyKey(action, input)",
    "idempotency(object: once, key, current)",
    "signTools(actions, key)",
    "new TokenExchange({ key, scopes })",
    "toolHash(tool)",
    "canonicalJson(value)",
    "inputHash(value)",
    "createMcpServer({ actions })",
    "createMcpClient({ servers })"
   ],
   "yaml": "mcp:\n  expose: [orders.refund, orders.list]\n  confirm: { write: true, destructive: true }\n  auth: { issuer: auth, scopes: [orders:read, orders:refund], token-ttl: 10m }\n",
   "example": "import { action, db, pay } from \"mainsdk\";\nimport { s } from \"mainsdk/schema\";\n\nexport const refundOrder = action({\n  name: \"orders.refund\",\n  intent: \"Refund an order the signed-in user owns\",\n  input: s.object({ orderId: s.string(), reason: s.string() }),\n  risk: \"destructive\",          // agents must get a person's yes\n  mcp: true,\n  run: async ({ input, user }) => {\n    const order = await db.orders.for(user).get(input.orderId);\n    return pay.refund(order.paymentId, { reason: input.reason });\n  },\n});\n",
   "drivers": [
    {
     "id": "mcp",
     "name": "MCP",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "http",
     "name": "HTTP API",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "server-action",
     "name": "Server actions",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "cli",
     "name": "CLI",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "One door, the same checks for people and agents",
    "Destructive actions always get a person's yes",
    "Tool descriptions signed and verified",
    "Agents act with narrow, short-lived tokens tied to the person who approved them"
   ],
   "checks": [],
   "runtime_codes": [
    "MAIN-MCP-0003",
    "MAIN-MCP-0011",
    "MAIN-MCP-0001",
    "MAIN-MCP-0002",
    "MAIN-MCP-0004",
    "MAIN-MCP-0005",
    "MAIN-MCP-0006"
   ],
   "features": [
    {
     "name": "MCP client with allowlist",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Agent identity",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "One action becomes endpoint, server action, CLI command and audit event",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "mcp-actions#action"
    },
    {
     "name": "Opt-in MCP tools and resources",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "mcp-actions#mcp-optin"
    },
    {
     "name": "Confirmation through MCP elicitation",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "mcp-actions#confirm"
    },
    {
     "name": "Signed tool descriptions",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "mcp-actions#signed-tools"
    },
    {
     "name": "OAuth 2.1 on the MCP endpoint",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "mcp-actions#oauth"
    },
    {
     "name": "Agent tokens through token exchange",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "mcp-actions#token-exchange"
    },
    {
     "name": "Signed approvals for confirmed actions",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "signed-approvals#action-risk-hook"
    },
    {
     "name": "Agent-to-agent calls over A2A",
     "phase": 5,
     "milestone": "1.6",
     "ticket": "platforms#a2a"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/action.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/action.md"
  },
  {
   "id": "agents",
   "name": "App agents",
   "category": "ai",
   "summary": "Give each agent your app runs an identity in the audit log, a budget, a person's approval for risky steps and a kill switch.",
   "status": "building",
   "stability": "experimental",
   "phase": 1,
   "milestone": "0.3",
   "spec": "agent-governance",
   "calls": [
    "agents.reserve(call)",
    "agents.settle(hold, actual)",
    "agents.release(hold)",
    "agents.record(call)",
    "agents.check(call)",
    "agents.ask(req)",
    "agents.remaining(name)",
    "agents.counters()",
    "agents.attach(budget)",
    "agents.admit(call)",
    "agents.charge(call)",
    "agents.run(call, fn)",
    "agents.approve(approvalId, by)",
    "agents.isApproved(approvalId)",
    "agents.approvals.list()",
    "agents.get(name)",
    "agents.reload(config)",
    "agents.close(approvalId, why)",
    "agents.decline(approvalId, by)",
    "agents.isDeclined(approvalId)",
    "agents.isPending(approvalId)",
    "agents.onApprove(fn)",
    "agents.remainingSpend(name)",
    "agents.request(req)",
    "agents.pendingApprovals()",
    "agents.onBudgetLow(fn)"
   ],
   "yaml": "agents:\n  support-bot:\n    budget: { spend: $5/day, calls: 500/day }\n    approve: { risk: destructive, spend-over: $1 }\n    enabled: true\n",
   "example": "import { agents } from \"mainsdk\";\n\nconst out = await agents.run(\n  { agent: \"support-bot\", action: \"orders.refund\", user: user.id, spend: \"$5.00\", risk: \"destructive\" },\n  () => refundOrder(input),\n);\nif (out.status === \"pending\") return Response.json({ approval: out.approval.id }, { status: 202 });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Every step is audited with the agent as actor and the person it acts for",
    "A budget is checked before the call; at the budget the call is refused",
    "A destructive or costly step waits for a person, and an agent never approves its own",
    "enabled false stops an agent on its next call, with no deploy",
    "Works offline from local config"
   ],
   "checks": [],
   "runtime_codes": [
    "MAIN-AGENT-0001",
    "MAIN-AGENT-0002",
    "MAIN-AGENT-0003",
    "MAIN-AGENT-0004",
    "MAIN-AGENT-0006",
    "MAIN-AGENT-0007",
    "MAIN-AGENT-0005"
   ],
   "features": [
    {
     "name": "Agent and person in every audit event",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "agent-governance#actor"
    },
    {
     "name": "Spend and call budgets per agent",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "agent-governance#budgets"
    },
    {
     "name": "Approval above a risk or spend threshold",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "agent-governance#approval"
    },
    {
     "name": "Calls bound to a named agent",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "agent-governance#bind-spend"
    },
    {
     "name": "Kill switch without a deploy",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "agent-governance#kill"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/agents.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/agents.md"
  },
  {
   "id": "memory",
   "name": "Agent memory",
   "category": "ai",
   "summary": "Let app agents remember the people they serve, inside the tenant and the app, with untrusted recall, versions and forgetting you can prove.",
   "status": "building",
   "stability": "experimental",
   "phase": 1,
   "milestone": "0.4",
   "spec": "memory",
   "calls": [
    "memory.remember(input, { about, kind, ttl, label, source, key, session })",
    "memory.recall(query, { about, kinds, k, asOf, halfLife, hops, session })",
    "memory.context({ about, query, budget })",
    "memory.revise(id, next, { reason })",
    "memory.forget(target, { reason, dryRun })",
    "memory.pin(id)",
    "memory.unpin(id)",
    "memory.history(id)",
    "memory.learn(messages, { about, kinds, maxCost })",
    "memory.relate({ from, predicate, to })",
    "memory.consolidate({ about, reflect })",
    "memory.export({ about, kinds })",
    "memory.import(lines, { about, session })",
    "memory.mcp({ compat })",
    "memory.anthropicTool()"
   ],
   "yaml": "memory:\n  driver: db                       # db | local | mem0 | zep | letta | pinecone | mcp\n  embed: { model: openai/text-embedding-3-small, feature: memory-embed }\n  extract: { model: anthropic/claude-haiku-4-5, feature: memory-learn }\n  ttl: { event: 90d }\n  from-tools: quarantine           # quarantine | allow | refuse\n  children: off                    # off | guardian\nagents:\n  support-bot:\n    memory: { read: own, write: true, forget: approve, recalls: 500/day, writes: 100/day }\n",
   "example": "import { ai } from \"mainsdk\";\nimport { memory } from \"mainsdk/experimental/memory\";\n\nawait memory.remember(\"Prefers WhatsApp over email\", { kind: \"preference\" });   // about the person the agent acts for\n\nconst ctx = await memory.context({ query: message, budget: 800 });   // pinned first, in one <memory> data block\nconst reply = await ai.generate({\n  model: \"anthropic/claude-sonnet-4-5\",\n  messages: [{ role: \"user\", content: ctx.text }, { role: \"user\", content: message }],   // data, never system text\n  feature: \"support-chat\",\n  agent: \"support-bot\",\n  user,\n});\n\nconst receipt = await memory.forget({ about: user.id }, { reason: \"user request\" });   // signed, hard, cascades\n",
   "drivers": [
    {
     "id": "db",
     "name": "Your database (Postgres with pgvector",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/db.html"
    },
    {
     "id": "local",
     "name": "Local (SQLite or JSONL)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "mem0",
     "name": "Mem0",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/mem0.html"
    },
    {
     "id": "zep",
     "name": "Zep",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/zep.html"
    },
    {
     "id": "letta",
     "name": "Letta",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/letta.html"
    },
    {
     "id": "pinecone",
     "name": "Pinecone",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/pinecone.html"
    },
    {
     "id": "mcp",
     "name": "MCP memory server",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/mcp.html"
    }
   ],
   "guarantees": [
    "Memories never cross tenants or apps; the tenant comes from tenant.current()",
    "Recalled text reaches the model only as escaped data, never as system text",
    "The injection check runs on every write and every recall",
    "Tool, MCP, web and import writes stay in their session until a person confirms them",
    "Revisions make new versions; nothing is overwritten",
    "Forget is a hard delete that cascades and returns a signed receipt",
    "Labelled personal by default, sealed with the person's key, kept only as long as retention allows",
    "Every write, recall and forget audited with ids only, never the text",
    "remember, recall and forget never call a generative model"
   ],
   "checks": [
    "MAIN-MEM-0020",
    "MAIN-MEM-0021",
    "MAIN-MEM-0022",
    "MAIN-MEM-0023",
    "MAIN-MEM-0024",
    "MAIN-MEM-0025"
   ],
   "runtime_codes": [
    "MAIN-MEM-0001",
    "MAIN-MEM-0002",
    "MAIN-MEM-0003",
    "MAIN-MEM-0004",
    "MAIN-MEM-0005",
    "MAIN-MEM-0006",
    "MAIN-MEM-0007",
    "MAIN-MEM-0008",
    "MAIN-MEM-0009",
    "MAIN-MEM-0010",
    "MAIN-MEM-0011",
    "MAIN-MEM-0012",
    "MAIN-MEM-0013",
    "MAIN-MEM-0014",
    "MAIN-MEM-0015",
    "MAIN-MEM-0016"
   ],
   "features": [
    {
     "name": "Remember and recall by tenant",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#remember-recall"
    },
    {
     "name": "Model context with pinned memories in one escaped data block",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#context-pin"
    },
    {
     "name": "Versions with revise",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#revise-history"
    },
    {
     "name": "Hard forget with cascade and a signed receipt",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#forget-receipt"
    },
    {
     "name": "Injection check and quarantine for tool and import writes",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#guards"
    },
    {
     "name": "Agent grants",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#governance"
    },
    {
     "name": "Personal label",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#privacy-keys"
    },
    {
     "name": "Audit of every write",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#audit"
    },
    {
     "name": "db driver with row-level security and pgvector",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#db-driver"
    },
    {
     "name": "Local driver with BM25",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#local-driver"
    },
    {
     "name": "Memory conformance suite",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#conformance"
    },
    {
     "name": "MCP tools for remember",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#mcp-tools"
    },
    {
     "name": "Anthropic memory tool compatibility",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#anthropic-tool"
    },
    {
     "name": "Children off by default",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "memory#children"
    },
    {
     "name": "Learn memories from a conversation",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "memory-advanced#learn"
    },
    {
     "name": "Consolidate and reflect as a workflow",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "memory-advanced#consolidate"
    },
    {
     "name": "Relations and recall hops",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "memory-advanced#relate"
    },
    {
     "name": "Lossless JSONL export and import",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "memory-advanced#export-import"
    },
    {
     "name": "Knowledge-graph MCP server compatibility",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "memory-advanced#kg-compat"
    },
    {
     "name": "Mem0",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "memory-advanced#provider-drivers"
    },
    {
     "name": "Pinecone and MCP drivers",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "memory-advanced#provider-drivers-vector-mcp"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/memory.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/memory.md"
  },
  {
   "id": "advisor",
   "name": "Playbook advisor",
   "category": "ai",
   "summary": "Optionally let a model read signals and norms and propose one of the app's declared playbooks with a reason a person approves; off by default.",
   "status": "building",
   "stability": "experimental",
   "phase": 2,
   "milestone": "0.6",
   "spec": "advisor",
   "calls": [
    "advisor.propose({ signals? })",
    "advisor.get(id)",
    "advisor.list({ cursor?, limit? })"
   ],
   "yaml": "advisor:\n  on: [attack.suspected, anomaly.detected]\n  may: [attack.contain, pressure.slim]\n  run: approve\n  model: anthropic/claude-sonnet-4-5   # optional; the first of ai.models by default\n  feature: advisor                     # the ai feature its calls are made under\n",
   "example": "import { advisor } from \"mainsdk/experimental/advisor\";\n\nconst p = await advisor.propose();\nconsole.log(p.choice, p.reason, p.evidence);\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (calls the ai interface)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Chooses only from declared playbooks, or none",
    "Never approves, and approval is the default",
    "Sees signals and norms only, redacted"
   ],
   "checks": [
    "MAIN-ADVISOR-0001",
    "MAIN-OPS-0001"
   ],
   "runtime_codes": [
    "MAIN-ADVISOR-0002",
    "MAIN-ADVISOR-0003",
    "MAIN-ADVISOR-0004",
    "MAIN-OPS-0002",
    "MAIN-OPS-0003",
    "MAIN-OPS-0004",
    "MAIN-OPS-0006"
   ],
   "features": [
    {
     "name": "Proposals limited to declared playbooks",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "advisor#propose"
    },
    {
     "name": "Offline local driver for tests",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "advisor#offline"
    },
    {
     "name": "The operate surface over MCP",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ops-mcp#tools-read"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/advisor.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/advisor.md"
  },
  {
   "id": "pay",
   "name": "Payments",
   "category": "payments",
   "summary": "Take payments and subscriptions on the provider's hosted checkout, routed by market, with no card data in your app.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "pay",
   "calls": [
    "pay.checkout()",
    "pay.subscribe()",
    "pay.refund()",
    "pay.on()",
    "pay.agent({ agent, amount, to })",
    "createPay(opts)",
    "fromMicros(micros)",
    "toMicros(value)",
    "parseRate(value)",
    "pay.handleWebhook(rawBody, headers)",
    "pay.reportUsage(event)",
    "pay.providerFor(market)",
    "razorpay(opts)"
   ],
   "yaml": "# main.yaml\npayments: { driver: stripe }\npay:\n  stripe:   { key: secret:STRIPE_SECRET_KEY, webhook: secret:STRIPE_WEBHOOK_SECRET }\n  razorpay: { key: secret:RAZORPAY_KEY, webhook: secret:RAZORPAY_WEBHOOK_SECRET }\n# main.in.yaml: the India overlay changes only the provider\npayments: { driver: razorpay, methods: [upi, cards] }\n",
   "example": "import { pay } from \"mainsdk\";\n\nconst session = await pay.checkout({\n  order: order.id,\n  amount: 125000,               // minor units\n  currency: \"INR\",\n  successUrl: \"/orders/done\",\n  cancelUrl: \"/cart\",\n});\n",
   "drivers": [
    {
     "id": "stripe",
     "name": "Stripe",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/stripe.html"
    },
    {
     "id": "razorpay",
     "name": "Razorpay",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/razorpay.html"
    },
    {
     "id": "paddle",
     "name": "Paddle",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/paddle.html"
    },
    {
     "id": "local",
     "name": "Local test cards",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Card data never touches the app",
    "Provider webhooks verified before handlers run",
    "Idempotency keys on checkout, subscribe and refund",
    "Amounts in integer minor units with an ISO 4217 currency"
   ],
   "checks": [
    "MAIN-PAY-0001"
   ],
   "runtime_codes": [
    "MAIN-PAY-0002",
    "MAIN-PAY-0010",
    "MAIN-PAY-0020",
    "MAIN-PAY-0030",
    "MAIN-PAY-0031",
    "MAIN-PAY-0032",
    "MAIN-PAY-0033"
   ],
   "features": [
    {
     "name": "Agent payments inside an agent budget",
     "phase": 2,
     "milestone": "0.7",
     "ticket": null
    },
    {
     "name": "pay@1 contract with market routing",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "pay#contract"
    },
    {
     "name": "Hosted checkout and subscriptions",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "pay#hosted"
    },
    {
     "name": "Card data check",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "pay#card-check"
    },
    {
     "name": "Verified provider webhooks",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "pay#webhooks"
    },
    {
     "name": "Idempotency keys",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "pay#idempotency"
    },
    {
     "name": "Provider by market",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "pay#routing"
    },
    {
     "name": "Stripe and Razorpay drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "pay#drivers"
    },
    {
     "name": "Signed payment approvals (PSD2, RBI)",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "signed-approvals#dynamic-linking"
    },
    {
     "name": "Subscription events set the tenant's plan",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#pay-source"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/pay.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/pay.md"
  },
  {
   "id": "meter",
   "name": "Usage metering",
   "category": "payments",
   "summary": "Record what each tenant uses, idempotently and without slowing a request, read it back by window, and export it to pay for usage billing.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "meter",
   "calls": [
    "meter.record(metric, value?, { feature?, apikey?, user?, at?, idempotencyKey? })",
    "meter.usage({ metric, window, tenant?, groupBy? })",
    "meter.flush()",
    "localMeter(o)",
    "meterIssues(raw, opts?)",
    "parseMeter(raw)",
    "meter.close()",
    "meter.erase(user)",
    "meter.exportDue(tenants)",
    "meter.exportUser(user)",
    "meter.pseudonym(user)",
    "meter.startExport(list)"
   ],
   "yaml": "meter:\n  driver: db\n  metrics:\n    api_calls: { unit: call, aggregate: sum }\n    active_users: { unit: person, aggregate: unique, of: user }\n  export:\n    to: pay\n    every: 1h\n    metrics: { api_calls: { meter: api_calls } }\n",
   "example": "import { meter } from \"mainsdk\";\n\n// counted once even if this step is retried\nmeter.record(\"audio_minutes\", 2.5, { feature: \"speech\" });\n\nconst month = await meter.usage({ metric: \"audio_minutes\", window: \"month\" });\n",
   "drivers": [
    {
     "id": "db",
     "name": "Your database (the tenant's region store)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/db.html"
    },
    {
     "id": "redis",
     "name": "Redis (built-in RESP client)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/redis.html"
    },
    {
     "id": "local",
     "name": "Local (tests and mainsdk dev)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "stripe-meters",
     "name": "Stripe Billing meters (export through pay)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/stripe.html"
    },
    {
     "id": "lago",
     "name": "Lago",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/lago.html"
    }
   ],
   "guarantees": [
    "A retried step, job or webhook records once",
    "Recording never waits on the store and never throws because it is down",
    "Records stay in the tenant's region; only hourly totals go to the billing provider",
    "A person's id in usage data is pseudonymised when they are erased"
   ],
   "checks": [
    "MAIN-METER-0001",
    "MAIN-METER-0006"
   ],
   "runtime_codes": [
    "MAIN-METER-0002",
    "MAIN-METER-0003",
    "MAIN-METER-0004",
    "MAIN-METER-0005",
    "MAIN-METER-0007",
    "MAIN-METER-0008"
   ],
   "features": [
    {
     "name": "meter@1 contract and declared metrics",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "meter#contract"
    },
    {
     "name": "Idempotent records per tenant",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "meter#record"
    },
    {
     "name": "Usage by hour, day, month and range",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "meter#usage"
    },
    {
     "name": "Local, db and redis drivers by region",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "meter#drivers"
    },
    {
     "name": "Buffered records and stale usage when the store is down",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "meter#fail-static"
    },
    {
     "name": "Export to pay for usage billing",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "meter#export"
    },
    {
     "name": "Pseudonymised user ids on erasure",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "meter#privacy"
    },
    {
     "name": "Usage source for plan limits",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "meter#entitlements-source"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/meter.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/meter.md"
  },
  {
   "id": "entitlements",
   "name": "Plans and entitlements",
   "category": "payments",
   "summary": "Say what each plan includes, check it in one line, and set a tenant's plan from its subscription.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "entitlements",
   "calls": [
    "entitlements.has(feature)",
    "entitlements.require(feature)",
    "entitlements.limit(name)",
    "entitlements.within(name, add?, fn?)",
    "entitlements.plan()",
    "entitlements.set(tenantId, plan, { reason, trial?, until? })",
    "entitlements.grant(tenantId, { features?, limits? }, { reason, until? })",
    "entitlements.revoke(tenantId, grantId, { reason })",
    "entitlements.ungrant(tenantId, grantId, { reason })",
    "new Entitlements(o)",
    "PLAN_DDL(const)",
    "dbPlanStore(o)",
    "localPlanStore()",
    "parsePlans(plans, ent, { meter? })",
    "planIssues(plans, ent, { meter? })"
   ],
   "yaml": "plans:\n  free: { features: [basic], limits: { seats: 3, projects: 5, ai_spend: $5/month } }\n  pro:\n    features: [basic, export, sso]\n    limits: { seats: 50, requests: 100/s }\n    prices: [price_pro_monthly, price_pro_yearly]\nentitlements:\n  driver: db\n  default: free\n  grace: 7d\n  upgrade: /settings/billing\n  usage: { seats: members, ai_spend: ai, projects: \"model:projects\" }\n",
   "example": "import { entitlements } from \"mainsdk\";\n\n// 403 with an upgrade hint unless the tenant's plan includes export\nentitlements.require(\"export\");\n\n// 403 once the tenant has used all its projects\nawait entitlements.within(\"projects\", 1, () => createProject(input));\n",
   "drivers": [
    {
     "id": "db",
     "name": "Your database (the tenant's region store)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/db.html"
    },
    {
     "id": "local",
     "name": "Local (tests and local mode)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "has reads memory only; no network call per check",
    "Every refusal carries an upgrade hint that names the plans that include the feature",
    "The plan comes from verified subscription events, never from the browser",
    "Per-tenant overrides can only add features or raise limits, and are audited",
    "The last known plan is served when the store is down"
   ],
   "checks": [
    "MAIN-PLAN-0006",
    "MAIN-PLAN-0010"
   ],
   "runtime_codes": [
    "MAIN-PLAN-0001",
    "MAIN-PLAN-0002",
    "MAIN-PLAN-0003",
    "MAIN-PLAN-0004",
    "MAIN-PLAN-0005",
    "MAIN-PLAN-0007",
    "MAIN-PLAN-0008",
    "MAIN-PLAN-0009",
    "MAIN-PLAN-0011"
   ],
   "features": [
    {
     "name": "plans and entitlements in main.yaml",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#schema"
    },
    {
     "name": "has, require, limit and within for the current tenant",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#checks"
    },
    {
     "name": "Plan state stored in the tenant's region",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#store"
    },
    {
     "name": "Plan set from pay subscription events",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#pay-source"
    },
    {
     "name": "Audited per-tenant overrides that only add",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#overrides"
    },
    {
     "name": "Last known plan served when the store is down",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#fail-static"
    },
    {
     "name": "Usage from members, records, AI spend and meter",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#usage"
    },
    {
     "name": "Plan request rates and plan in flag rules",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#limits-flags"
    },
    {
     "name": "Check for features and limits no plan declares",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#check"
    },
    {
     "name": "Plan helpers in the testing kit",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#testing"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/entitlements.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/entitlements.md"
  },
  {
   "id": "licence",
   "name": "App licensing",
   "category": "payments",
   "summary": "Sell your app with signed licence files that switch features on, verify offline against a pinned key, and keep serving through a grace period.",
   "status": "building",
   "stability": "experimental",
   "phase": 1,
   "milestone": "0.4",
   "spec": "licence",
   "calls": [
    "licence.state()",
    "licence.has(feature)",
    "licence.require(feature)",
    "licence.limit(name)",
    "licence.get()",
    "licence.update(token, { scope? })",
    "licence.refresh()",
    "licence.machine()",
    "licence.watch(scope, fn)",
    "new AppLicence(opts)",
    "storeKv(store)",
    "parseLicenceConfig(raw)",
    "licenceIssues(raw)",
    "createTestLicenceCA(opts?)"
   ],
   "yaml": "licence:\n  driver: http\n  product: acme-desktop\n  roots: [\"ed25519:CvBKNOIhJSGvXg9chN9qV8-z6byK3-is6-12mXaidWQ\"]  # the well-known test root; pin your own in production\n  file: ./main.licence\n  features: [sso, audit-export]\n  url: https://api.mainsdk.dev\n  refresh: 6h\n  trust: ./main.licence-trust          # CA certificates and revocation lists\n  machine: os                          # os | env\n  over: enforce                        # enforce | report\n  skew: 5m                             # clock skew tolerated, 0 to 1h\n",
   "example": "import { licence } from \"mainsdk\";\n\n// 403 unless the licence in scope includes sso and is valid or in grace\nlicence.require(\"sso\");\n\n// \"valid\", \"grace\", \"expired\", \"revoked\", \"suspended\", \"tampered\" or \"missing\"\nconst { status, graceEndsAt } = licence.state();\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local (a dev licence from the test root)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "offline",
     "name": "Offline (licence and revocation files only)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/offline.html"
    },
    {
     "id": "http",
     "name": "HTTP refresh (MainSDK Cloud or your own issuer)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Verified offline against pinned root keys; no call home on the request path",
    "A licence can't be edited, forged or rolled back to an older one through anything the SDK reads",
    "An unreachable revocation list or refresh endpoint never blocks a valid licence",
    "Expiry plus grace is the only stop, and it turns off only licensed features; the app always starts",
    "A licence only narrows plans and limits, never widens them"
   ],
   "checks": [
    "MAIN-LICENCE-0011",
    "MAIN-LICENCE-0012",
    "MAIN-LICENCE-0014"
   ],
   "runtime_codes": [
    "MAIN-LICENCE-0001",
    "MAIN-LICENCE-0002",
    "MAIN-LICENCE-0003",
    "MAIN-LICENCE-0004",
    "MAIN-LICENCE-0005",
    "MAIN-LICENCE-0006",
    "MAIN-LICENCE-0007",
    "MAIN-LICENCE-0008",
    "MAIN-LICENCE-0009",
    "MAIN-LICENCE-0010",
    "MAIN-LICENCE-0013",
    "MAIN-LICENCE-0015",
    "MAIN-LICENCE-0016",
    "MAIN-LICENCE-0020",
    "MAIN-LICENCE-0021",
    "MAIN-LICENCE-0022",
    "MAIN-LICENCE-0023",
    "MAIN-LICENCE-0024",
    "MAIN-LICENCE-0025",
    "MAIN-LICENCE-0026"
   ],
   "features": [
    {
     "name": "DSSE over JCS, Ed25519, root to CA to licence",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#verify"
    },
    {
     "name": "States: valid, grace, expired, revoked, suspended, tampered, missing",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#state"
    },
    {
     "name": "require, has, limit and state reading memory only",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#checks"
    },
    {
     "name": "Signed revocation lists with per-entry grace",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#revocation"
    },
    {
     "name": "Anti-rollback of licences",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#anti-rollback"
    },
    {
     "name": "Licensed features and limits narrow entitlements",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#entitlements"
    },
    {
     "name": "Licence limits lower limits quotas",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#limits"
    },
    {
     "name": "Tenant licences",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#tenant"
    },
    {
     "name": "Background refresh over HTTP",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#refresh"
    },
    {
     "name": "mainsdk licence verify and show",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#cli"
    },
    {
     "name": "Test licence CA and golden vectors",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "licence#testing"
    },
    {
     "name": "Issue, renew, suspend, revoke and rotate in MainSDK Cloud",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "licence-cloud#issue"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/licence.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/licence.md"
  },
  {
   "id": "notify",
   "name": "Notifications",
   "category": "messaging",
   "summary": "Reach a person on push, WhatsApp, SMS or email with one call that respects their preferences and quiet hours.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "email-notify",
   "calls": [
    "notify(to, { template, data, category, channels?, urgent?, idempotencyKey? })",
    "notify.preferences(user)",
    "notify.preferences(user).set(patch)",
    "notify.contact.get(user)",
    "notify.contact.update(user, patch)",
    "notify.consent.create(user, channel, opts?)",
    "notify.consent.get(user, channel)",
    "notify.consent.revoke(user, channel)",
    "notify.release(id)",
    "notify.broadcast({ id, template, data, category, to })",
    "notify.rollout.get(id)",
    "notify.rollout.resume(id, opts?)",
    "notify.rollout.advance(id)",
    "notify.raw()",
    "notify.info()",
    "notify.push.register()",
    "createNotify(deps)",
    "parseNotify(raw)",
    "twilioSms(o)",
    "msg91Sms(o)",
    "msg91WhatsApp(o)",
    "birdWhatsApp(o)",
    "whatsappCloudChannel(o)",
    "localChannel(id, o?)"
   ],
   "yaml": "notify:\n  channels: [whatsapp, sms, email]\n  sms: msg91\n  whatsapp: whatsapp-cloud\n  quiet: { from: \"22:00\", to: \"08:00\", tz: Asia/Kolkata }\n  rollout: { stages: [\"1%\", \"10%\", \"100%\"], pause-if: { bounces: \"2%\", complaints: \"0.1%\", unsubscribes: \"1%\" } }\n  templates:\n    order.shipped:\n      sms: { text: \"Order {{order}} shipped\", dlt-template-id: \"1007000000000001\" }\n      whatsapp: { name: order_shipped, language: en, params: [order] }\n  whatsapp-cloud: { phone-number-id: \"1234567890\", token: secret:WHATSAPP_TOKEN }\n  msg91: { key: secret:MSG91_KEY, sender: ACMEIN }\n  limits: { sms-per-day: 3, whatsapp-per-day: 5 }\n",
   "example": "import { notify } from \"mainsdk\";\n\nawait notify(user, {\n  template: \"order.shipped\",\n  data: { orderId: order.id },\n  category: \"orders\",\n});\n",
   "drivers": [
    {
     "id": "whatsapp-cloud",
     "name": "WhatsApp Cloud API",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/whatsapp.html"
    },
    {
     "id": "bird",
     "name": "Bird",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/bird.html"
    },
    {
     "id": "msg91",
     "name": "MSG91",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/msg91.html"
    },
    {
     "id": "twilio",
     "name": "Twilio",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/twilio.html"
    },
    {
     "id": "local",
     "name": "Local terminal channel",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "fcm",
     "name": "Firebase Cloud Messaging",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/firebase-cloud-messaging.html"
    },
    {
     "id": "apns",
     "name": "Apple Push Notification service",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/apns.html"
    },
    {
     "id": "expo",
     "name": "Expo",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/expo.html"
    },
    {
     "id": "resend",
     "name": "Resend",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/resend.html"
    },
    {
     "id": "ses",
     "name": "Amazon SES",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/amazon-ses.html"
    },
    {
     "id": "onesignal",
     "name": "OneSignal",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/onesignal.html"
    },
    {
     "id": "slack",
     "name": "Slack (alerts)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/slack.html"
    },
    {
     "id": "pagerduty",
     "name": "PagerDuty (alerts)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/pagerduty.html"
    },
    {
     "id": "opsgenie",
     "name": "Opsgenie (alerts)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/opsgenie.html"
    },
    {
     "id": "webhook",
     "name": "Signed webhook (alerts)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/webhook.html"
    }
   ],
   "guarantees": [
    "Opt-outs and quiet hours respected",
    "Every SMS recipient has a recorded opt-in",
    "WhatsApp only with recorded consent and approved templates",
    "No personal data in push payloads"
   ],
   "checks": [
    "MAIN-NOTIFY-0001",
    "MAIN-NOTIFY-0006",
    "MAIN-CHECK-0026"
   ],
   "runtime_codes": [
    "MAIN-NOTIFY-0002",
    "MAIN-NOTIFY-0003",
    "MAIN-NOTIFY-0004",
    "MAIN-NOTIFY-0005",
    "MAIN-NOTIFY-0007",
    "MAIN-NOTIFY-0008",
    "MAIN-NOTIFY-0009",
    "MAIN-NOTIFY-0010",
    "MAIN-NOTIFY-0011"
   ],
   "features": [
    {
     "name": "notify@1 contract",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#contract"
    },
    {
     "name": "Channel choice, preferences and opt-outs",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#notify-routing"
    },
    {
     "name": "Quiet hours and digests",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#quiet-hours"
    },
    {
     "name": "Staged rollout with auto-pause",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#rollout"
    },
    {
     "name": "Idempotent sends",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#idempotency"
    },
    {
     "name": "Twilio and MSG91 SMS drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#drivers-notify"
    },
    {
     "name": "WhatsApp Cloud API, Bird and MSG91 channel drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#drivers-whatsapp"
    },
    {
     "name": "Push through FCM, APNs and Expo",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "expo-adapter#push-through-notify"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/notify.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/notify.md"
  },
  {
   "id": "engagement",
   "name": "Engagement journeys",
   "category": "messaging",
   "summary": "Bring people back with reminder ladders that obey a weekly limit, quiet hours and a stop after ignored messages, with strict rules for children and no dark patterns.",
   "status": "building",
   "stability": "experimental",
   "phase": 1,
   "milestone": "0.4",
   "spec": "engagement",
   "calls": [
    "journey(name, { version?, start, category }, fn)",
    "ladder(user, rungs, { sendAt? })",
    "engagement.send(user, { template, data?, journey?, rung? })",
    "engagement.sendTime(user)",
    "engagement.status(user)",
    "engagement.results(flag, range)",
    "engagement.copy(template, { locale, variant? })",
    "engagement.guardian(child)",
    "engagement.optOut(user, { channel, category?, via? })",
    "engagement.stopByHandle(handle, { channel, category?, via? })",
    "engagement.keyword(user, text, { channel? })",
    "engagement.preferences(user).set({ maxPerWeek? })",
    "engagement.apply(pushed)",
    "engagement.register(journey)",
    "engagement.start(journey, user)",
    "engagement.attach()",
    "engagement.erase(userId)",
    "createEngagement(deps)",
    "parseEngagement(raw)"
   ],
   "yaml": "notify:\n  engagement:\n    max-per-week: 4\n    quiet-hours: \"21:00-08:00\"\n    stop-after-ignored: 5\n    audience: { children: strict }\n    guardian: { controls: [pause, frequency, quiet-hours, view] }\n    categories: [reminders]\n    active: user.active\n    ignored-after: 48h\n    send-time: { default: \"17:00\" }\n    copy: { ai: true, limit: $2/month }\n    proof:\n      finished-today: { kind: count, event: lesson.finished, window: day }\n    links:\n      offer: https://app.example.com/offer\n    measure: { return-within: 48h, unsubscribe-within: 7d }\n",
   "example": "import { journey, ladder } from \"mainsdk/notify\";\n\nexport const comeBack = journey(\"come-back\", { version: 1, start: \"user.inactive\", category: \"reminders\" },\n  async ({ user }) => ladder(user, [\n    { after: \"1d\", template: \"reminder.gentle\" },\n    { after: \"3d\", template: \"reminder.gentle-2\" },\n    { after: \"7d\", template: \"reminder.last\" },\n  ], { sendAt: \"usual\" }));\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in on notify and workflows",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "A weekly limit, quiet hours and a stop after ignored messages on every engagement message, tighten-only when pushed",
    "Every message carries a one-step stop: one-click unsubscribe on email, a stop action on push, a stop keyword on WhatsApp and SMS",
    "Children get no streak-loss, guilt or comparison copy, and a guardian can pause or limit reminders",
    "Social proof comes only from declared analytics queries, with small groups hidden",
    "Links in messages are one share per journey, rung and variant, never per person"
   ],
   "checks": [
    "MAIN-ENGAGE-0010",
    "MAIN-ENGAGE-0011",
    "MAIN-ENGAGE-0012",
    "MAIN-ENGAGE-0013",
    "MAIN-ENGAGE-0014"
   ],
   "runtime_codes": [
    "MAIN-ENGAGE-0001",
    "MAIN-ENGAGE-0002",
    "MAIN-ENGAGE-0003",
    "MAIN-ENGAGE-0004",
    "MAIN-ENGAGE-0005",
    "MAIN-ENGAGE-0006",
    "MAIN-ENGAGE-0007",
    "MAIN-ENGAGE-0008"
   ],
   "features": [
    {
     "name": "engagement contract and main.yaml keys",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#contract"
    },
    {
     "name": "Journeys and ladders as durable workflows",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#journeys"
    },
    {
     "name": "Weekly limit, quiet hours and stop after ignored",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#guardrails"
    },
    {
     "name": "Send time from the person's own activity",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#send-time"
    },
    {
     "name": "Variants through flags with return and unsubscribe rates",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#variants"
    },
    {
     "name": "Copy from ai within a tenant limit",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#ai-copy"
    },
    {
     "name": "Strict rules and guardian controls for children",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#children"
    },
    {
     "name": "Social proof from declared queries",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#proof"
    },
    {
     "name": "Engagement checks",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#checks"
    },
    {
     "name": "Manifest and privacy label line",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "engagement#label"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/engagement.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/engagement.md"
  },
  {
   "id": "email",
   "name": "Email",
   "category": "messaging",
   "summary": "Send transactional and marketing email, with one-click unsubscribe and no tracking pixels by default.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "email-notify",
   "calls": [
    "email.send({ to, template, data, idempotencyKey? })",
    "email.marketing({ to, template, data, unsubscribe, idempotencyKey? })",
    "email.raw()",
    "email.templates.define(name, template)",
    "email.templates.has(name)",
    "email.suppression.create(address, opts?)",
    "email.suppression.get(address)",
    "email.suppression.delete(address)",
    "email.unsubscribe.create(address, opts?)",
    "email.unsubscribe.accept(token)",
    "email.unsubscribe.handle(req)",
    "email.ingest(body, provider?)",
    "email.on(listener)",
    "email.info()",
    "createEmail(deps)",
    "parseEmail(raw)"
   ],
   "yaml": "email:\n  driver: resend\n  key: secret:RESEND_API_KEY\n  from: \"Shop <shop@example.com>\"\n  tracking: { opens: false, clicks: false }\n  unsubscribe-url: https://app.example.com/unsubscribe\n  webhook: resend-events\n  limits: { per-minute: 60, per-day: 5000 }\n  fallback: [ses]\n  templates:\n    receipt: { subject: \"Receipt {{orderId}}\", text: \"Thanks for order {{orderId}}.\" }\n",
   "example": "import { email } from \"mainsdk\";\n\nawait email.send({\n  to: user.email,\n  template: \"receipt\",\n  data: { orderId: order.id },\n});\n",
   "drivers": [
    {
     "id": "resend",
     "name": "Resend",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/resend.html"
    },
    {
     "id": "ses",
     "name": "Amazon SES",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/amazon-ses.html"
    },
    {
     "id": "local",
     "name": "Local inbox",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "postmark",
     "name": "Postmark",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/postmark.html"
    },
    {
     "id": "bird",
     "name": "Bird",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/bird.html"
    },
    {
     "id": "smtp",
     "name": "SMTP",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/smtp.html"
    }
   ],
   "guarantees": [
    "Marketing email does not compile without an unsubscribe",
    "Unsubscribes, bounces and complaints suppressed for every later send",
    "No open-tracking pixels or rewritten links by default",
    "A retry never sends twice"
   ],
   "checks": [
    "MAIN-EMAIL-0006",
    "MAIN-EMAIL-0009",
    "MAIN-EMAIL-0010",
    "MAIN-EMAIL-0011",
    "MAIN-CHECK-0026"
   ],
   "runtime_codes": [
    "MAIN-EMAIL-0001",
    "MAIN-EMAIL-0002",
    "MAIN-EMAIL-0003",
    "MAIN-EMAIL-0004",
    "MAIN-EMAIL-0005",
    "MAIN-EMAIL-0007",
    "MAIN-EMAIL-0008"
   ],
   "features": [
    {
     "name": "email@1 contract",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#contract"
    },
    {
     "name": "Send and marketing with the unsubscribe rule",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#email-core"
    },
    {
     "name": "Suppression list",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#suppression"
    },
    {
     "name": "No pixels or link rewriting",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#no-pixels"
    },
    {
     "name": "Idempotent sends",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#idempotency"
    },
    {
     "name": "Resend and Amazon SES drivers and the local inbox",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "email-notify#drivers-email"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/email.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/email.md"
  },
  {
   "id": "mail",
   "name": "Inbound mail and mailboxes",
   "category": "messaging",
   "summary": "Receive email through your mail provider's webhook, keep it in mailboxes with threads and attachments, and pull people's existing mailboxes over IMAP or JMAP.",
   "status": "building",
   "stability": "experimental",
   "phase": 3,
   "milestone": "1.2",
   "spec": "mail",
   "calls": [
    "mail.on(route, fn)",
    "mail.receive(req)",
    "mail.list({ mailbox, folder?, thread?, unread?, cursor?, limit? })",
    "mail.get(id)",
    "mail.update(id, { folder?, read?, flagged?, labels? })",
    "mail.thread(id)",
    "mail.replyAddress({ thread, ttl? })",
    "mail.headers({ replyTo })",
    "mail.allowImages(id)",
    "mail.pull(mailbox)",
    "mail.connect(req, { provider, mailbox? })",
    "mail.mailboxes()"
   ],
   "yaml": "mail:\n  domains: [in.acme.app]\n  inbound: { provider: ses, path: /mainsdk/mail/inbound, secret: secret:MAIL_INBOUND_SECRET }\n  mailboxes:\n    support: { address: support@in.acme.app, owner: tenant }\n  reject: { dmarc: fail, virus: infected }\n",
   "example": "import { mail } from \"mainsdk\";\n\nmail.on(\"support@\", async (m) => {\n  await tickets.open({ from: m.from.address, subject: m.subject, body: m.replyText ?? m.text });\n});\n",
   "drivers": [
    {
     "id": "ses",
     "name": "Amazon SES inbound",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/amazonsimpleemailservice.html"
    },
    {
     "id": "postmark",
     "name": "Postmark inbound",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/postmark.html"
    },
    {
     "id": "mailgun",
     "name": "Mailgun routes",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/mailgun.html"
    },
    {
     "id": "cloudflare",
     "name": "Cloudflare Email Routing",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/cloudflare.html"
    },
    {
     "id": "imap",
     "name": "IMAP (with a connection)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/imap.html"
    },
    {
     "id": "jmap",
     "name": "JMAP (with a connection)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/jmap.html"
    },
    {
     "id": "local",
     "name": "Local (tests and mainsdk dev)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "No SMTP server; every inbound message is verified before it is parsed",
    "Remote images in mail are blocked until the reader allows them",
    "Attachments are scanned and stored through files",
    "Mailbox tokens never reach app code"
   ],
   "checks": [
    "MAIN-MAIL-0008",
    "MAIN-MAIL-0009"
   ],
   "runtime_codes": [
    "MAIN-MAIL-0001",
    "MAIN-MAIL-0002",
    "MAIN-MAIL-0003",
    "MAIN-MAIL-0004",
    "MAIN-MAIL-0005",
    "MAIN-MAIL-0006",
    "MAIN-MAIL-0007",
    "MAIN-MAIL-0010"
   ],
   "features": [
    {
     "name": "mail@1 contract",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#contract"
    },
    {
     "name": "Inbound from provider webhooks",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#inbound"
    },
    {
     "name": "MIME parsing in the standard library",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#mime"
    },
    {
     "name": "SPF, DKIM, DMARC and spam verdicts",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#verdicts"
    },
    {
     "name": "Attachments into files",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#attachments"
    },
    {
     "name": "Mailboxes, folders and flags",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#store"
    },
    {
     "name": "Reply-by-email threading",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#threads"
    },
    {
     "name": "Safe display of message HTML",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#display"
    },
    {
     "name": "IMAP and JMAP mailbox pulls",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "mail#pull"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/mail.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/mail.md"
  },
  {
   "id": "realtime",
   "name": "Realtime updates",
   "category": "messaging",
   "summary": "Push live updates from your server to browsers over server-sent events, checked per channel and tenant.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "realtime",
   "calls": [
    "realtime.publish(channel, event, data)",
    "realtime.pipe(channel, tokens, { event? })",
    "realtime.handler()",
    "realtime.on(prefix, event, opts, fn)",
    "realtime.onStream(prefix, fn)",
    "realtime.ticket()",
    "realtime.subscribe(channels, { onEvent, onReset? })",
    "realtime.tokens(sub, channel, { onText, onDone })",
    "http.websocket(url, options)",
    "new SocketServer(hub)",
    "new Realtime(ports)",
    "parseRealtime(raw)",
    "publishProgress(hub, source)",
    "publishRecords(hub, source)",
    "new Broadcaster(opts)",
    "new MemoryBus()",
    "redisBroadcast(o)",
    "events(object: broadcast, onBroadcast)"
   ],
   "yaml": "realtime:\n  driver: broadcast\n  path: /mainsdk/realtime\n  transports: [sse, websocket]\n  channels:\n    order: { action: orders.read }\n    chat: { action: chats.read, send: chats.write }\n  replay: { size: 500, ttl: 5m }\n  max-event: 32KB\n  streams:\n    call: { types: [\"audio/opus\"], max-per-socket: 4, budget: 64MB }\n  socket: { rate: 50/s, buffer: 1MB }\n",
   "example": "import { realtime } from \"mainsdk\";\n\nawait realtime.publish(`order:${order.id}`, \"status\", { status: \"shipped\" });\n",
   "drivers": [
    {
     "id": "broadcast",
     "name": "Instance broadcast (events.broadcast)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/broadcast.html"
    },
    {
     "id": "local",
     "name": "Local (one instance)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "pusher",
     "name": "Pusher",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/pusher.html"
    },
    {
     "id": "ably",
     "name": "Ably",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/ably.html"
    },
    {
     "id": "supabase-realtime",
     "name": "Supabase Realtime",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/supabase.html"
    }
   ],
   "guarantees": [
    "Every subscribe is checked through the session and access, per declared channel prefix",
    "Channels are keyed by the current tenant",
    "A reader that falls behind is disconnected, never buffered without bound",
    "A stream resumes from Last-Event-ID, or is told to reset"
   ],
   "checks": [
    "MAIN-REALTIME-0003",
    "MAIN-REALTIME-0006",
    "MAIN-REALTIME-0007",
    "MAIN-REALTIME-0012",
    "MAIN-REALTIME-0015",
    "MAIN-REALTIME-0016"
   ],
   "runtime_codes": [
    "MAIN-REALTIME-0001",
    "MAIN-REALTIME-0002",
    "MAIN-REALTIME-0004",
    "MAIN-REALTIME-0005",
    "MAIN-REALTIME-0008",
    "MAIN-REALTIME-0009",
    "MAIN-REALTIME-0010",
    "MAIN-REALTIME-0011",
    "MAIN-REALTIME-0013",
    "MAIN-REALTIME-0014",
    "MAIN-REALTIME-0017",
    "MAIN-REALTIME-0018"
   ],
   "features": [
    {
     "name": "realtime@1 contract and channel names",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#contract"
    },
    {
     "name": "Publish and the SSE endpoint on the local driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#sse-local"
    },
    {
     "name": "Subscribe checked per channel prefix",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#subscribe-auth"
    },
    {
     "name": "Channels keyed by tenant",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#tenant"
    },
    {
     "name": "Replay buffer and resume",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#resume"
    },
    {
     "name": "Back-pressure and per-user connection cap",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#backpressure"
    },
    {
     "name": "Fan-out across instances",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#fanout"
    },
    {
     "name": "Model token streams with pipe",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#ai-pipe"
    },
    {
     "name": "Workflow progress and record-change events",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#progress-records"
    },
    {
     "name": "Reconnect hint on shutdown",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#shutdown"
    },
    {
     "name": "Browser client",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#client"
    },
    {
     "name": "Checks for undeclared channels and oversized events",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "realtime#checks"
    },
    {
     "name": "RFC 6455 WebSocket server on node:http",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#rfc6455"
    },
    {
     "name": "Upgrade checked by origin",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#upgrade-auth"
    },
    {
     "name": "Subscribe",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#socket-channels"
    },
    {
     "name": "Client messages handled with realtime.on",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#client-messages"
    },
    {
     "name": "Numbered binary streams with realtime.onStream",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#binary-streams"
    },
    {
     "name": "Bounded socket and stream queues",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#backpressure"
    },
    {
     "name": "Outbound http.websocket with the rules of http.fetch",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#outbound-parity"
    },
    {
     "name": "Close every socket with a reconnect code on drain",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#drain"
    },
    {
     "name": "Socket transport in the browser client",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#client"
    },
    {
     "name": "Checks for sockets without upgrade and undeclared send and stream prefixes",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "realtime-sockets#checks"
    },
    {
     "name": "Real-time collaboration and presence",
     "phase": 5,
     "milestone": "1.6",
     "ticket": "platforms#collab-presence"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/realtime.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/realtime.md"
  },
  {
   "id": "files",
   "name": "File uploads",
   "category": "files",
   "summary": "Upload files to your own bucket, Uploadthing, Vercel Blob or getqr.id, scanned and private to their owner, with short-lived links to read them.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "files-share",
   "calls": [
    "files.upload()",
    "files.uploadUrl()",
    "files.complete(id, { owner })",
    "files.url()",
    "files.get(id)",
    "handleFilesRoute(files, req)",
    "signatureScan(bytes)",
    "files.accept(idOrKey, token, { bytes, type, signal? })",
    "files.checkReadUrl(url)",
    "files.read(path)",
    "files.scanResult(id, verdict)",
    "files.sign(s)",
    "files.verify(s, sig)",
    "files.delete(ref, { owner })",
    "files.driver"
   ],
   "yaml": "files:\n  driver: s3\n  bucket: acme-uploads\n  region: eu-west-1\n  key: secret:S3_SECRET_ACCESS_KEY\n  scan: true\n  maxSize: 25MB\n",
   "example": "import { files } from \"mainsdk\";\n\nconst ref = await files.upload(file, { owner: user, label: \"personal\" });\nconst url = await files.url(ref, { for: user, expiresIn: \"1h\" });\n",
   "drivers": [
    {
     "id": "getqr",
     "name": "getqr.id",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/getqr-id.html"
    },
    {
     "id": "s3",
     "name": "Amazon S3 and S3-compatible stores (R2, MinIO, B2)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/amazon-s3.html"
    },
    {
     "id": "uploadthing",
     "name": "Uploadthing",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/uploadthing.html"
    },
    {
     "id": "vercel-blob",
     "name": "Vercel Blob",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/vercel-blob.html"
    },
    {
     "id": "local",
     "name": "Local files on disk",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Files readable only by their owner or whom access allows",
    "Scanned before they become readable",
    "Resumable, idempotent uploads",
    "Browser uploads go straight to the store on a signed, single-use URL",
    "Every upload goes through files, never a storage SDK in your route code"
   ],
   "checks": [
    "MAIN-FILES-0010",
    "MAIN-FILES-0016"
   ],
   "runtime_codes": [
    "MAIN-FILES-0002",
    "MAIN-FILES-0005",
    "MAIN-FILES-0003",
    "MAIN-FILES-0006",
    "MAIN-FILES-0004",
    "MAIN-FILES-0007",
    "MAIN-FILES-0008",
    "MAIN-FILES-0011",
    "MAIN-FILES-0012",
    "MAIN-FILES-0013",
    "MAIN-FILES-0014",
    "MAIN-FILES-0015"
   ],
   "features": [
    {
     "name": "files@1 contract on the getqr.id OpenAPI",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#contract"
    },
    {
     "name": "Upload and signed URLs with owner scope and scanning",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#upload"
    },
    {
     "name": "Resumable, idempotent uploads",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#resumable"
    },
    {
     "name": "Upload component from @getqr/react",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#react"
    },
    {
     "name": "Check against storage SDKs used directly",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#check-rule"
    },
    {
     "name": "Local driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#local-driver"
    },
    {
     "name": "Direct browser uploads on signed URLs",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#direct-upload"
    },
    {
     "name": "S3 and S3-compatible driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#driver-s3"
    },
    {
     "name": "Uploadthing driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#driver-uploadthing"
    },
    {
     "name": "Vercel Blob driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#driver-vercel-blob"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/files.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/files.md"
  },
  {
   "id": "share",
   "name": "Share links and QR codes",
   "category": "files",
   "summary": "Share any record, file or screen as a private, expiring getqr.id link and QR code, run offline QR placements, and get first-party open counts with consent; the only way an app hands out a link.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "files-share",
   "calls": [
    "share(resource, { expiresIn })",
    "share.get(id)",
    "share.list()",
    "share.update(id, { title?, url?, uploadId?, extend? })",
    "share.revoke()",
    "share.qr(id, { format?, size? })",
    "share.story(id)",
    "share.stats()",
    "share.usage()",
    "share.campaigns.create()",
    "share.campaigns.get(id)",
    "share.placements.create(campaignId, rows)",
    "share.placements.list(campaignId, { consent })",
    "share.placements.get(id, { consent })",
    "share.placements.update(id)",
    "share.on(type, fn)",
    "share.webhooks.list()",
    "share.webhooks.create(url, events)",
    "GETQR_EVENT_TYPES(const)",
    "GETQR_OPERATIONS(const)",
    "new LocalGetqr(o?)",
    "httpTransport(o)",
    "localGetqrFetch(local)",
    "createShare(o)",
    "handleShareRoute(share, req)",
    "verifyGetqrSignature(raw, header, { secret, nowMs })",
    "withShareMembers(routed, base)"
   ],
   "yaml": "share:\n  driver: getqr\n  key: secret:GETQR_API_KEY\n  default-expiry: 7d\n  max-expiry: 90d\napp:\n  url: https://acme.dev                # public origin; default share.origin of the local driver\n",
   "example": "import { share } from \"mainsdk\";\n\nconst { url, qr } = await share(order, { expiresIn: \"7d\", access: \"read\" });\n",
   "drivers": [
    {
     "id": "getqr",
     "name": "getqr.id",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/getqr-id.html"
    },
    {
     "id": "local",
     "name": "Local links on the dev server",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "The only egress for links, QR codes and share pages (ADR-0056)",
    "Links private by default, expiring and read-only",
    "No personal data in the URL, title or metadata",
    "Open and scan counts first-party and only with consent",
    "Every share sheet shows \"Powered by getqr.id\"",
    "Types generated from getqr.id's own OpenAPI, pinned and diffed (ADR-0057)"
   ],
   "checks": [
    "MAIN-SHARE-0001",
    "MAIN-SHARE-0007",
    "MAIN-CONTRACT-0001",
    "MAIN-CONTRACT-0003",
    "MAIN-CONTRACT-0004",
    "MAIN-CONTRACT-0021",
    "MAIN-CONTRACT-0030",
    "MAIN-CONTRACT-0031"
   ],
   "runtime_codes": [
    "MAIN-SHARE-0002",
    "MAIN-SHARE-0003",
    "MAIN-SHARE-0004",
    "MAIN-SHARE-0005",
    "MAIN-SHARE-0006",
    "MAIN-SHARE-0008",
    "MAIN-SHARE-0009",
    "MAIN-SHARE-0010",
    "MAIN-SHARE-0011",
    "MAIN-SHARE-0012",
    "MAIN-SHARE-0013"
   ],
   "features": [
    {
     "name": "share@1 contract",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#contract"
    },
    {
     "name": "Private, expiring, read-only links and QR codes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#share"
    },
    {
     "name": "ShareBar and QRCode from @getqr/react",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#react"
    },
    {
     "name": "Check against other sharers and shorteners",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#check-rule"
    },
    {
     "name": "Vendored getqr.id contract with contract diff",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#vendor-contract"
    },
    {
     "name": "Sandbox contract test in stage",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#sandbox-contract"
    },
    {
     "name": "Offline contract replay of recorded calls and contract record",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#contract-replay"
    },
    {
     "name": "Get, list and update shares",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#share-manage"
    },
    {
     "name": "QR in SVG or PNG and story images",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#share-qr-story"
    },
    {
     "name": "Usage against the getqr.id plan",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#share-usage"
    },
    {
     "name": "Campaigns and offline QR placements with consent-gated counts",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#campaigns"
    },
    {
     "name": "Inbound getqr.id webhooks preset and share.on",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#webhook-preset"
    },
    {
     "name": "Counts into files status and analytics with consent",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#webhook-feeds"
    },
    {
     "name": "Local share driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#share-local"
    },
    {
     "name": "Check against shortener and QR libraries and hosts",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "files-share#check-sharers"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/share.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/share.md"
  },
  {
   "id": "links",
   "name": "Invite, deep and sign-in links",
   "category": "files",
   "summary": "Send invite links, deep links that open the right screen on the web, Android and iOS, and sign-in links that hand off to auth, all minted by getqr.id through share.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "links",
   "calls": [
    "links.create(route)",
    "links.invite(route, { email?, uses?, expiresIn? })",
    "links.accept(req, token)",
    "links.signIn({ email } | { phone }, { next? })",
    "links.resolve(url)",
    "links.revoke(id)",
    "createLinks(o)",
    "parseLinks(raw)",
    "links.route(req)"
   ],
   "yaml": "links:\n  base: https://app.example.com\n  routes:\n    order: { web: \"/orders/:id\", app: \"order/:id\", params: [id] }\n",
   "example": "import { links } from \"mainsdk\";\n\nconst link = await links.create({ screen: \"order\", params: { id: order.id } });\n",
   "drivers": [
    {
     "id": "getqr",
     "name": "getqr.id",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/getqr-id.html"
    },
    {
     "id": "local",
     "name": "Local links on the dev server",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Every link minted by share on getqr.id, expiring and revocable",
    "Nothing from the route in the getqr.id URL",
    "Invites single use by default, optionally bound to an email",
    "Sign-in links never return to the caller and never reveal who has an account",
    "Universal Links and App Links association files generated from main.yaml"
   ],
   "checks": [
    "MAIN-LINK-0003",
    "MAIN-LINK-0004",
    "MAIN-LINK-0005"
   ],
   "runtime_codes": [
    "MAIN-LINK-0001",
    "MAIN-LINK-0002"
   ],
   "features": [
    {
     "name": "links@1 contract",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "links#contract"
    },
    {
     "name": "Deep links and resolve",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "links#deep"
    },
    {
     "name": "applinks association files generated from main.yaml",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "links#association"
    },
    {
     "name": "Invite links",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "links#invite"
    },
    {
     "name": "Sign-in links that hand off to auth",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "links#sign-in"
    },
    {
     "name": "Local links with no network",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "links#local"
    },
    {
     "name": "Check rules for routes and app identifiers",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "links#checks"
    },
    {
     "name": "Egress conformance for every producer",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "links#egress"
    },
    {
     "name": "Passkey association in the same files",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "mobile-auth#association-files"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/links.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/links.md"
  },
  {
   "id": "media",
   "name": "Images, video and audio",
   "category": "files",
   "summary": "Resize images to AVIF and WebP and transcode video and audio to HLS as workflows, with output through files and metadata stripped by default.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "media",
   "calls": [
    "media.image(file, { widths?, formats? })",
    "media.video(file, { renditions? })",
    "media.audio(file, { bitrates? })",
    "media.get(jobId)",
    "media.cancel(jobId, { reason })",
    "media.url(output, { for })",
    "media.playlist(job, output, { for })"
   ],
   "yaml": "media:\n  driver: getqr\n  image: { widths: [320, 640, 1280], formats: [avif, webp], max-pixels: 40MP }\n  video: { renditions: [360p, 720p], max-duration: 10m, max-size: 2GB }\n",
   "example": "import { media } from \"mainsdk/media\";\n\nconst job = await media.image(photo, { widths: [320, 1280] });   // returns at once; a workflow does the work\n",
   "drivers": [
    {
     "id": "getqr",
     "name": "getqr.id",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/getqr-id.html"
    },
    {
     "id": "mux",
     "name": "Mux",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/mux.html"
    },
    {
     "id": "cloudinary",
     "name": "Cloudinary",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/cloudinary.html"
    },
    {
     "id": "ffmpeg",
     "name": "ffmpeg (development",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/ffmpeg.html"
    },
    {
     "id": "local",
     "name": "Local placeholder driver (right structure",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Never processed in the request path; every call is a resumable workflow",
    "Every output goes through files, owned by the source's owner, in the tenant's region",
    "EXIF, XMP, GPS and container metadata stripped from every output; location can't be kept",
    "Type, size, duration and pixel limits checked before any transform",
    "ffmpeg runs only as a declared program through system.run"
   ],
   "checks": [
    "MAIN-MEDIA-0007",
    "MAIN-MEDIA-0010",
    "MAIN-MEDIA-0011",
    "MAIN-MEDIA-0012",
    "MAIN-MEDIA-0013"
   ],
   "runtime_codes": [
    "MAIN-MEDIA-0001",
    "MAIN-MEDIA-0002",
    "MAIN-MEDIA-0003",
    "MAIN-MEDIA-0004",
    "MAIN-MEDIA-0005",
    "MAIN-MEDIA-0006"
   ],
   "features": [
    {
     "name": "media@1 contract",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#contract"
    },
    {
     "name": "Processing as a workflow",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#workflow"
    },
    {
     "name": "Metadata stripped by default",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#strip"
    },
    {
     "name": "Video and audio to HLS with thumbnails",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#video-audio"
    },
    {
     "name": "Limits before work",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#limits"
    },
    {
     "name": "ffmpeg development driver",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#driver-ffmpeg"
    },
    {
     "name": "getqr.id driver",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#driver-getqr"
    },
    {
     "name": "Mux and Cloudinary drivers",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#drivers-mux-cloudinary"
    },
    {
     "name": "Moderation hold",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#hold"
    },
    {
     "name": "Tenants",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#tenancy-access"
    },
    {
     "name": "Media checks",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "media#checks"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/media.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/media.md"
  },
  {
   "id": "moderate",
   "name": "Moderation",
   "category": "security",
   "summary": "Take reports, review them in a queue, decide and take appeals with a statement of reasons, and hold uploads until a pre-screen clears them.",
   "status": "building",
   "stability": "experimental",
   "phase": 1,
   "milestone": "0.4",
   "spec": "moderate",
   "calls": [
    "moderate.report({ target, reason, by })",
    "moderate.screen({ target, author, text?, files? })",
    "moderate.state(target)",
    "moderate.queue.list()",
    "moderate.queue.assign(caseId, to)",
    "moderate.decide(caseId, { action, ground, facts, by })",
    "moderate.appeal(decisionId, { by, note })",
    "moderate.resolve(appealId, { outcome, note, by })",
    "moderate.statement(decisionId)",
    "moderate.transparency({ from, to })",
    "moderate.watch(\"decision\", fn)",
    "moderate.watch(\"match\", fn)",
    "sanitize(object: html, markdown, text, attr, url)"
   ],
   "yaml": "moderate:\n  reasons: [spam, harassment, hate]\n  terms: terms\n  hold: { files: true, media: true, max: 24h }\n  prescreen:\n    classify: { labels: [ok, spam, sexual, violence, hate], hold-on: [sexual, violence, hate], remove-on: [spam] }\n    hash: { driver: none }\n  reports: { per-user: 20/hour }\n  appeals: { window: 6mo }\n",
   "example": "import { moderate } from \"mainsdk/experimental/moderate\";\n\nawait moderate.report({ target: { kind: \"post\", id: post.id }, reason: \"harassment\", by: user });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in on db",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "ai-classify",
     "name": "Pre-screen through ai.classify",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/ai-classify.html"
    },
    {
     "id": "pdq",
     "name": "PDQ hash matching (lists from your provider)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/pdq.html"
    },
    {
     "id": "photodna",
     "name": "PhotoDNA (lists from Microsoft)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/photodna.html"
    }
   ],
   "guarantees": [
    "Uploads and media outputs are held until the pre-screen clears them",
    "No hash list ships in the repository; lists come from your provider at run time",
    "Every decision has a ground and a statement of reasons, and can be appealed to a different reviewer",
    "Reports are rate-limited and every step is in the audit log"
   ],
   "checks": [
    "MAIN-MOD-0010",
    "MAIN-MOD-0011",
    "MAIN-MOD-0012",
    "MAIN-MOD-0013",
    "MAIN-SANITIZE-0003"
   ],
   "runtime_codes": [
    "MAIN-MOD-0001",
    "MAIN-MOD-0002",
    "MAIN-MOD-0003",
    "MAIN-MOD-0004",
    "MAIN-MOD-0005",
    "MAIN-MOD-0006",
    "MAIN-MOD-0007",
    "MAIN-MOD-0008",
    "MAIN-MOD-0009",
    "MAIN-SANITIZE-0001",
    "MAIN-SANITIZE-0002"
   ],
   "features": [
    {
     "name": "moderate@1 contract",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#contract"
    },
    {
     "name": "Reports and cases",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#reports"
    },
    {
     "name": "Review queue and assignment",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#queue"
    },
    {
     "name": "Decisions: remove, label, restrict, warn",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#decisions"
    },
    {
     "name": "Appeals",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#appeals"
    },
    {
     "name": "Hold before publish",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#hold"
    },
    {
     "name": "Hash matching hook",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#hash-hook"
    },
    {
     "name": "Pre-screen with ai.classify",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#classify"
    },
    {
     "name": "Statements of reasons and DSA export",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#statements"
    },
    {
     "name": "Transparency report",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#transparency"
    },
    {
     "name": "Audit",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#audit-tenancy"
    },
    {
     "name": "Moderation checks",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "moderate#checks"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/moderate.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/moderate.md"
  },
  {
   "id": "pdf",
   "name": "PDF documents",
   "category": "files",
   "summary": "Render PDFs from templates in the reader's language; the rendering half of documents.",
   "status": "preview",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "documents",
   "calls": [
    "pdf.render(template, data)"
   ],
   "yaml": "pdf:\n  driver: builtin\n",
   "example": "import { pdf } from \"mainsdk\";\n\nconst invoice = await pdf.render(\"invoice\", { order, locale: user.locale });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "docraptor",
     "name": "DocRaptor",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/docraptor.html"
    },
    {
     "id": "gotenberg",
     "name": "Gotenberg",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/gotenberg.html"
    },
    {
     "id": "documenso",
     "name": "Documenso",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/documenso.html"
    }
   ],
   "guarantees": [
    "Localised through i18n",
    "Rendered without a browser by the built-in renderer",
    "Output stored through files in the tenant's region"
   ],
   "checks": [
    "MAIN-DOC-0011"
   ],
   "runtime_codes": [
    "MAIN-DOC-0007"
   ],
   "features": [
    {
     "name": "Render from a template",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "documents#render"
    },
    {
     "name": "HTML to PDF through Gotenberg or DocRaptor",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "documents#render-drivers-checks"
    },
    {
     "name": "Sign a document",
     "phase": 3,
     "milestone": "1.2",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/pdf.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/pdf.md"
  },
  {
   "id": "documents",
   "name": "Document reading",
   "category": "files",
   "summary": "Read PDFs, Office files and scans into text, tables and layout, with OCR from the driver you pick and active content never run.",
   "status": "preview",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "documents",
   "calls": [
    "documents.parse(file, opts)",
    "documents.start(file, opts)",
    "documents.get(jobId)",
    "documents.render(layout, data, opts)",
    "new Documents(opts)",
    "aiOcrDriver(opts)",
    "azureDriver(o)",
    "fakeOcrDriver(opts)"
   ],
   "yaml": "documents:\n  driver: builtin\n  ocr: { driver: textract }\n  limits: { max-size: 50MB, max-pages: 500, inline-pages: 20 }\n",
   "example": "import { documents } from \"mainsdk\";\n\nconst doc = await documents.parse(upload, { tables: true });   // text layer read offline\nconst firstPage = doc.pages[0].text;                            // untrusted: never instructions\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "textract",
     "name": "Amazon Textract",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/amazonwebservices.html"
    },
    {
     "id": "azure-document",
     "name": "Azure AI Document Intelligence",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/azure-document.html"
    },
    {
     "id": "google-document",
     "name": "Google Document AI",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/googlecloud.html"
    },
    {
     "id": "unstructured",
     "name": "Unstructured",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/unstructured.html"
    },
    {
     "id": "ai",
     "name": "A model through ai",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/ai.html"
    }
   ],
   "guarantees": [
    "Text-layer PDFs and Office files read offline with no account",
    "Scanned pages are reported, never returned empty",
    "JavaScript, macros, embedded files and links are never run or followed",
    "Labelled documents go only to drivers in the tenant's region that meet the data rules",
    "Parsed text is untrusted data"
   ],
   "checks": [
    "MAIN-DOC-0010"
   ],
   "runtime_codes": [
    "MAIN-DOC-0001",
    "MAIN-DOC-0002",
    "MAIN-DOC-0003",
    "MAIN-DOC-0004",
    "MAIN-DOC-0005",
    "MAIN-DOC-0006",
    "MAIN-DOC-0008",
    "MAIN-DOC-0009"
   ],
   "features": [
    {
     "name": "PDF text layer read natively",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "documents#pdf-text"
    },
    {
     "name": "DOCX, XLSX and PPTX read natively",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "documents#office"
    },
    {
     "name": "Limits and active content checks before parsing",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "documents#limits-active"
    },
    {
     "name": "Large documents as a workflow",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "documents#jobs"
    },
    {
     "name": "OCR, layout and tables through Textract, Azure, Google and Unstructured",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "documents#ocr-drivers"
    },
    {
     "name": "OCR through a model with every ai guard",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "documents#ai-driver"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/documents.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/documents.md"
  },
  {
   "id": "webhooks",
   "name": "Webhooks",
   "category": "events",
   "summary": "Send signed webhooks with retries and verify incoming ones before your handler runs.",
   "status": "preview",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "webhooks-jobs",
   "calls": [
    "webhooks.emit()",
    "webhooks.receive()",
    "webhooks.delivery(id)",
    "new Webhooks(inbound, outbound)",
    "PRESETS(const)",
    "devPaths(dir)",
    "newSecret()",
    "readDeliveries(dir, now)",
    "replayKey(dir, create)",
    "replaySignature(key, deliveryId, { body })",
    "signHeaders(secrets, id, { timestampSeconds, body })",
    "webhooks.handle(req, { remote? })",
    "webhooks.send(type, data, { idempotencyKey? })"
   ],
   "yaml": "webhooks:\n  driver: builtin\n  secret: secret:WEBHOOK_SIGNING_KEY\n  retries: { max: 8, window: 24h }\n",
   "example": "import { webhooks } from \"mainsdk\";\n\nexport const POST = webhooks.receive(\"github\", async (event) => {\n  await handlePush(event.data);      // runs only after the signature is checked\n});\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "svix",
     "name": "Svix",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/svix.html"
    }
   ],
   "guarantees": [
    "Outbound webhooks signed per Standard Webhooks",
    "Inbound signatures and timestamps checked before the handler",
    "Replays older than 5 minutes refused",
    "Duplicates call the handler once"
   ],
   "checks": [
    "MAIN-HOOK-0011"
   ],
   "runtime_codes": [
    "MAIN-HOOK-0002",
    "MAIN-HOOK-0001",
    "MAIN-HOOK-0003",
    "MAIN-HOOK-0004",
    "MAIN-HOOK-0005",
    "MAIN-HOOK-0006",
    "MAIN-HOOK-0007",
    "MAIN-HOOK-0008",
    "MAIN-HOOK-0009",
    "MAIN-HOOK-0010",
    "MAIN-HOOK-0012"
   ],
   "features": [
    {
     "name": "webhooks@1 contract",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#contract"
    },
    {
     "name": "Shared idempotency record",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#idempotency-store"
    },
    {
     "name": "Signed delivery with retries and a delivery log",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#emit"
    },
    {
     "name": "Verified receive",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#receive"
    },
    {
     "name": "Built-in and Svix drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#drivers-webhooks"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/webhooks.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/webhooks.md"
  },
  {
   "id": "workflows",
   "name": "Durable workflows",
   "category": "events",
   "summary": "Write a multi-step flow as one function that waits for days and resumes after a crash, with each finished step journaled and never run twice.",
   "status": "preview",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "workflows",
   "calls": [
    "workflow(name, opts, fn)",
    "step(name, fn, opts?)",
    "sleep(duration)",
    "waitFor(type, opts)",
    "approve(req)",
    "workflows.start(wf, input, opts?)",
    "workflows.get(runId)",
    "workflows.list(opts?)",
    "workflows.cancel(runId, opts)",
    "new Workflows(opts)",
    "workflows.register(workflow)"
   ],
   "yaml": "workflows:\n  driver: db\n  retries: { max: 3, backoff: exponential, initial: 1s }\n  max-step-result: 256KB\n  lease: 30s\n  keep: 30d\n",
   "example": "import { workflow, step, sleep } from \"mainsdk/workflows\";\n\nexport const welcome = workflow(\"welcome\", { version: 1 }, async ({ userId }: { userId: string }) => {\n  await step(\"send\", () => email.send({ to: userId, template: \"welcome\" }));\n  await sleep(\"3d\");\n  await step(\"nudge\", () => notify(userId, { template: \"come-back\" }));\n});\n",
   "drivers": [
    {
     "id": "db",
     "name": "Built on the app's database",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/db.html"
    },
    {
     "id": "local",
     "name": "Local (in memory in tests, a journal folder in development)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "inngest",
     "name": "Inngest",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/inngest.html"
    },
    {
     "id": "temporal",
     "name": "Temporal",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/temporal.html"
    },
    {
     "id": "vercel-workflow",
     "name": "Vercel Workflow",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/vercel.html"
    }
   ],
   "guarantees": [
    "A finished step never runs again on resume",
    "A run resumes on the version it started on",
    "Step results and workflow inputs never appear in run history"
   ],
   "checks": [
    "MAIN-WORKFLOW-0009",
    "MAIN-WORKFLOW-0010",
    "MAIN-WORKFLOW-0011",
    "MAIN-WORKFLOW-0012"
   ],
   "runtime_codes": [
    "MAIN-WORKFLOW-0001",
    "MAIN-WORKFLOW-0002",
    "MAIN-WORKFLOW-0003",
    "MAIN-WORKFLOW-0004",
    "MAIN-WORKFLOW-0005",
    "MAIN-WORKFLOW-0006",
    "MAIN-WORKFLOW-0007",
    "MAIN-WORKFLOW-0008",
    "MAIN-WORKFLOW-0013"
   ],
   "features": [
    {
     "name": "workflow() and step() with a journal",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#contract"
    },
    {
     "name": "Resume from the journal after a crash or deploy",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#replay"
    },
    {
     "name": "Finished steps return their journaled result",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#idempotent-steps"
    },
    {
     "name": "Per-step retries with backoff",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#retries"
    },
    {
     "name": "sleep and waitFor without holding a worker",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#timers-events"
    },
    {
     "name": "approve for a person's yes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#approve"
    },
    {
     "name": "Versioned runs and stuck detection",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#versions"
    },
    {
     "name": "Cancel and inspect runs",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#cancel"
    },
    {
     "name": "Run history in mainsdk logs",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#visibility"
    },
    {
     "name": "Tenant isolation for runs",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#tenant"
    },
    {
     "name": "Database driver, with local and file for development",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#drivers"
    },
    {
     "name": "Checks for clocks, randomness and network calls outside steps",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "workflows#checks"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/workflows.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/workflows.md"
  },
  {
   "id": "compute",
   "name": "Remote GPU and long jobs",
   "category": "ai",
   "summary": "Run GPU and long jobs on your own Modal, Replicate or RunPod account, or a local program, as workflows with progress, cancel, cost limits and results in files.",
   "status": "building",
   "stability": "experimental",
   "phase": 2,
   "milestone": "0.6",
   "spec": "compute",
   "calls": [
    "compute.run(target, input, { files?, maxCost?, timeout?, agent?, idempotencyKey? })",
    "compute.get(jobId)",
    "compute.list({ target?, state?, cursor?, limit? })",
    "compute.cancel(jobId, { reason })",
    "compute.step(name, target, input, opts?)",
    "compute.callback(call, request)"
   ],
   "yaml": "compute:\n  heartbeat: 60s\n  poll: 15s\n  results: { label: personal, max-size: 2GB, types: [\"image/*\"] }\n  limits: { per-tenant: $50/month }\n  targets:\n    upscale:\n      driver: replicate\n      model: nightmareai/real-esrgan:f121d640bd286e1fdc67f9799164c1d5be36ff74576ee11c803ae5b665dd46aa\n      region: us\n      timeout: 30m\n      price: $0.0014/s\n      max-cost: $2\n",
   "example": "import { compute } from \"mainsdk/experimental/compute\";\n\n// refused before submit if the worst case could pass $2\nconst { jobId } = await compute.run(\"upscale\", { scale: 4 }, { files: [image], maxCost: \"$2\" });\nconst job = await compute.get(jobId);\n",
   "drivers": [
    {
     "id": "modal",
     "name": "Modal",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/modal.html"
    },
    {
     "id": "replicate",
     "name": "Replicate",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/replicate.html"
    },
    {
     "id": "runpod",
     "name": "RunPod",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/runpod.html"
    },
    {
     "id": "local",
     "name": "Local program (system.run)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "MainSDK never hosts models; jobs run on your provider account",
    "A job never costs more than its limit; the worst case is checked before submit",
    "Jobs survive restarts and can be cancelled at the provider",
    "Labelled data goes only to a target in the tenant's region"
   ],
   "checks": [
    "MAIN-COMPUTE-0001",
    "MAIN-COMPUTE-0008",
    "MAIN-COMPUTE-0013"
   ],
   "runtime_codes": [
    "MAIN-COMPUTE-0002",
    "MAIN-COMPUTE-0003",
    "MAIN-COMPUTE-0004",
    "MAIN-COMPUTE-0005",
    "MAIN-COMPUTE-0006",
    "MAIN-COMPUTE-0007",
    "MAIN-COMPUTE-0009",
    "MAIN-COMPUTE-0010",
    "MAIN-COMPUTE-0011",
    "MAIN-COMPUTE-0012"
   ],
   "features": [
    {
     "name": "compute@1 contract and pinned targets",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#contract"
    },
    {
     "name": "Jobs as workflow runs",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#run"
    },
    {
     "name": "Wait on a job as one workflow step",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#step"
    },
    {
     "name": "Cost limits before and during a job",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#cost"
    },
    {
     "name": "Heartbeat, progress and lost jobs",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#heartbeat"
    },
    {
     "name": "Results into files",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#results"
    },
    {
     "name": "Region check for labelled input",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#region"
    },
    {
     "name": "Verified provider callbacks",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#callbacks"
    },
    {
     "name": "Modal, Replicate, RunPod and local drivers",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "compute#drivers"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/compute.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/compute.md"
  },
  {
   "id": "jobs",
   "name": "Events and background jobs",
   "category": "events",
   "summary": "Publish events, fan small messages out to every instance, and run background jobs that retry, survive restarts and never run twice for the same key.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.3",
   "spec": "webhooks-jobs",
   "calls": [
    "events.publish()",
    "events.on()",
    "events.broadcast(topic, payload)",
    "events.onBroadcast(topic, fn)",
    "jobs.run()",
    "jobs.schedule()",
    "new Events(opts)",
    "typeMatches(pattern, type)",
    "new Jobs(opts)",
    "new WorkGate(pressure?)",
    "nextCron(c, after)",
    "parseCron(expr)",
    "jobs.on(name, handler)",
    "jobs.get(id)",
    "jobs.dead.list({ cursor?, limit? })",
    "jobs.dead.retry(id)",
    "jobs.idle()"
   ],
   "yaml": "jobs:\n  driver: db\n  dead-letter: true\nidempotency: { store: db, ttl: 24h }\nevents:\n  broadcast: { driver: redis, redis: { url: secret:REDIS_URL }, max-payload: 64KB }\n  schemas:\n    order.shipped: { version: 1, compat: backward, fields: { orderId: string, carrier: \"string?\" } }\n",
   "example": "import { events, jobs } from \"mainsdk\";\n\nawait events.publish(\"order.shipped\", { orderId: order.id });\nawait jobs.schedule(\"reports.nightly\", \"0 3 * * *\", { region: \"in\" });\n",
   "drivers": [
    {
     "id": "db",
     "name": "The app's database",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/db.html"
    },
    {
     "id": "local",
     "name": "In-process local queue",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "inngest",
     "name": "Inngest",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/inngest.html"
    },
    {
     "id": "vercel-queues",
     "name": "Vercel Queues",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/vercel.html"
    },
    {
     "id": "trigger-dev",
     "name": "Trigger.dev",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/trigger-dev.html"
    },
    {
     "id": "sqs",
     "name": "Amazon SQS",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/amazon-sqs.html"
    }
   ],
   "guarantees": [
    "CloudEvents envelope with versioned schemas",
    "Retries with backoff and a dead-letter queue",
    "A duplicate key never runs the side effect twice",
    "Broadcast is best effort between instances of one app, and never blocks a request"
   ],
   "checks": [
    "MAIN-EVENT-0004",
    "MAIN-EVENT-0010",
    "MAIN-EVENT-0011"
   ],
   "runtime_codes": [
    "MAIN-EVENT-0012",
    "MAIN-EVENT-0013",
    "MAIN-EVENT-0001",
    "MAIN-EVENT-0014",
    "MAIN-IDEM-0001",
    "MAIN-IDEM-0002",
    "MAIN-JOB-0001",
    "MAIN-JOB-0002",
    "MAIN-JOB-0003",
    "MAIN-JOB-0004",
    "MAIN-JOB-0005",
    "MAIN-JOB-0006"
   ],
   "features": [
    {
     "name": "events@1 and jobs@1 contracts",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#contract"
    },
    {
     "name": "Shared idempotency record",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#idempotency-store"
    },
    {
     "name": "Publish and subscribe with a schema registry",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#events"
    },
    {
     "name": "Run and schedule with retries and dead letters",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#jobs"
    },
    {
     "name": "Inngest, Vercel Queues and local queue drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#drivers-jobs"
    },
    {
     "name": "Node worker adapter",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#worker-adapter"
    },
    {
     "name": "Instance broadcast on the memory driver, with topic and payload limits",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#broadcast-api"
    },
    {
     "name": "Instance broadcast on the redis driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#broadcast-redis"
    },
    {
     "name": "Production check for the memory broadcast driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "webhooks-jobs#broadcast-prod-check"
    },
    {
     "name": "Schedules on serverless platforms through the signed tick route",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#tick"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/jobs.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/jobs.md"
  },
  {
   "id": "secrets",
   "name": "Secrets",
   "category": "security",
   "summary": "Read provider keys on the server from your secrets manager, never from code or the browser.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.2",
   "spec": "config-secrets",
   "calls": [
    "secrets.get()",
    "secrets.raw()",
    "resolveSecret(ref, source?)",
    "secrets.all(name)",
    "secrets.age(name)",
    "secrets.watch(name, fn)"
   ],
   "yaml": "secrets:\n  driver: vault\n  vault: { address: https://vault.pinecart.internal, auth: workload-identity }\nemail:\n  key: secret:RESEND_API_KEY\n",
   "example": "import { secrets } from \"mainsdk\";   // server only: a client import fails the build\n\nconst token = await secrets.get(\"PARTNER_API_TOKEN\");\n",
   "drivers": [
    {
     "id": "vault",
     "name": "HashiCorp Vault",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/hashicorp-vault.html"
    },
    {
     "id": "aws-secrets-manager",
     "name": "AWS Secrets Manager",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/aws-secrets-manager.html"
    },
    {
     "id": "gcp-secret-manager",
     "name": "Google Secret Manager",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/google-secret-manager.html"
    },
    {
     "id": "azure-key-vault",
     "name": "Azure Key Vault",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/azure-key-vault.html"
    },
    {
     "id": "doppler",
     "name": "Doppler",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/doppler.html"
    },
    {
     "id": "infisical",
     "name": "Infisical",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/infisical.html"
    },
    {
     "id": "env",
     "name": "Environment variables",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "local",
     "name": "Local file",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Importable only in server code",
    "Values redacted from logs, errors and telemetry",
    "Last good value served when the provider is unreachable, never written to disk",
    "A key in code, a committed env file or the browser bundle fails the check"
   ],
   "checks": [
    "MAIN-SECRET-0001",
    "MAIN-SECRET-0002",
    "MAIN-SECRET-0003",
    "MAIN-SECRET-0004",
    "MAIN-SECRET-0011",
    "MAIN-SECRET-0014",
    "MAIN-CONFIG-0004",
    "MAIN-CONFIG-0005",
    "MAIN-CONFIG-0010"
   ],
   "runtime_codes": [
    "MAIN-SECRET-0005",
    "MAIN-SECRET-0006",
    "MAIN-SECRET-0007",
    "MAIN-SECRET-0008",
    "MAIN-SECRET-0009",
    "MAIN-SECRET-0010",
    "MAIN-SECRET-0012",
    "MAIN-SECRET-0013",
    "MAIN-CONFIG-0001",
    "MAIN-CONFIG-0002",
    "MAIN-CONFIG-0003"
   ],
   "features": [
    {
     "name": "Server-only secrets.get with last-good caching",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#secrets-api"
    },
    {
     "name": "secret references in main.yaml",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#secret-refs"
    },
    {
     "name": "AWS, Google and Azure drivers",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#secrets-cloud-drivers"
    },
    {
     "name": "Vault, Doppler and Infisical drivers",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#secrets-vault-drivers"
    },
    {
     "name": "Secret leak check",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#secret-gate"
    },
    {
     "name": "Required env var names checked in the platform build",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#preflight"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/secrets.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/secrets.md"
  },
  {
   "id": "keys",
   "name": "Encryption keys",
   "category": "security",
   "summary": "Encrypt records with their own data keys, wrapped by a regional KMS key, with per-user keys for erasure.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "keys",
   "calls": [
    "keys.encrypt()",
    "keys.decrypt()",
    "keys.forUser()",
    "keys.destroyUser()",
    "keys.rotate()",
    "keys.sign({ profile })",
    "keys.verify({ profile, publicKey, data, signature })",
    "new Keys(opts)",
    "new LocalKms(master)",
    "new MemoryKeyStore()",
    "keysFromConfig(cfg, deps)",
    "keys.forTenant(tenant, { region? })",
    "keys.destroyTenant(tenant)",
    "keys.rewrap(sealed, { purpose })",
    "keys.raw()"
   ],
   "yaml": "keys:\n  driver: aws-kms\n  key: { us: arn:aws:kms:us-east-1:…, eu: arn:aws:kms:eu-west-1:…, in: arn:aws:kms:ap-south-1:… }\n  per-user: true\n  rotate: 365d\n",
   "example": "import { keys } from \"mainsdk\";\n\nconst sealed = await keys.encrypt(note, { owner: user, region: \"eu\", label: \"phi\" });\nconst receipt = await keys.destroyUser(user);   // crypto-erasure\n",
   "drivers": [
    {
     "id": "aws-kms",
     "name": "AWS KMS",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/aws-kms.html"
    },
    {
     "id": "gcp-kms",
     "name": "Google Cloud KMS",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/google-cloud-kms.html"
    },
    {
     "id": "azure-keyvault",
     "name": "Azure Key Vault",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/azure-key-vault.html"
    },
    {
     "id": "vault-transit",
     "name": "Vault Transit",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/hashicorp-vault.html"
    },
    {
     "id": "local",
     "name": "Local (development only)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "A data key per record, wrapped by a key that never leaves the KMS",
    "One wrapping key per region",
    "Every key use audited, never the key material",
    "Automatic rotation with old data still readable"
   ],
   "checks": [
    "MAIN-KEYS-0001",
    "MAIN-KEYS-0021",
    "MAIN-KEYS-0023"
   ],
   "runtime_codes": [
    "MAIN-KEYS-0002",
    "MAIN-KEYS-0003",
    "MAIN-KEYS-0004",
    "MAIN-KEYS-0005",
    "MAIN-KEYS-0006",
    "MAIN-KEYS-0020",
    "MAIN-KEYS-0022",
    "MAIN-KEYS-0024",
    "MAIN-KEYS-0025"
   ],
   "features": [
    {
     "name": "Chain signing profiles (secp256k1",
     "phase": 2,
     "milestone": "0.7",
     "ticket": null
    },
    {
     "name": "keys@1 contract",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "keys#contract"
    },
    {
     "name": "Envelope encryption with AES-256-GCM",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "keys#envelope"
    },
    {
     "name": "Per-user keys and crypto-erasure",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "keys#per-user"
    },
    {
     "name": "Key use audit",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "keys#use-log"
    },
    {
     "name": "Automatic rotation",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "keys#rotation"
    },
    {
     "name": "AWS KMS, Vault Transit and local drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "keys#drivers-aws-vault"
    },
    {
     "name": "Google Cloud KMS and Azure Key Vault drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "keys#drivers-gcp-azure"
    },
    {
     "name": "HSM drivers through PKCS#11 and KMIP",
     "phase": 3,
     "milestone": "1.2",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/keys.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/keys.md"
  },
  {
   "id": "crypto",
   "name": "Cryptography",
   "category": "security",
   "summary": "Encrypt fields, sign and verify, hash passwords and make tokens with modern algorithms only.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "keys",
   "calls": [
    "crypto.keys.customer(org, { kms })",
    "cryptoApi(object: every crypto helper under one name)",
    "crypto.token(bytes?)",
    "crypto.sign(privateKey, data)",
    "crypto.verify(publicKey, { data, signature })",
    "crypto.seal(key, data, { aad? })",
    "crypto.open(key, sealed, { aad? })",
    "crypto.generateKey()",
    "crypto.deriveKey(secret, info)",
    "crypto.randomBytes(n)",
    "crypto.hash(data, alg?)",
    "crypto.hashHex(data, alg?)",
    "crypto.hmac(key, data, alg?)",
    "crypto.verifyHmac(key, data, tag, alg?)",
    "crypto.generateSigningKeyPair()",
    "crypto.equal(a, b)",
    "crypto.b64u(bytes)",
    "crypto.fromB64u(text)",
    "crypto.toHex(bytes)"
   ],
   "yaml": "crypto:\n  fips: false\n",
   "example": "import { crypto } from \"mainsdk\";\n\nconst resetToken = crypto.token();                 // 32 random bytes, URL-safe\nconst stored = await crypto.hashHex(resetToken);   // keep the hash, never the token\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "WebCrypto",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "cloud-kms",
     "name": "Cloud key services",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "vault-transit",
     "name": "Vault Transit",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/hashicorp-vault.html"
    },
    {
     "id": "libsodium",
     "name": "libsodium",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/libsodium.html"
    }
   ],
   "guarantees": [
    "Modern algorithms only",
    "Keys never in code"
   ],
   "checks": [
    "MAIN-CRYPTO-0002",
    "MAIN-CRYPTO-0013"
   ],
   "runtime_codes": [
    "MAIN-CRYPTO-0001",
    "MAIN-CRYPTO-0003",
    "MAIN-CRYPTO-0010",
    "MAIN-CRYPTO-0011",
    "MAIN-CRYPTO-0012"
   ],
   "features": [
    {
     "name": "Field encryption, signing and tokens",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Customer-managed keys",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "hsm-keys#customer-keys"
    },
    {
     "name": "Post-quantum signatures",
     "phase": 5,
     "milestone": "1.6",
     "ticket": "post-quantum#pq-signatures"
    },
    {
     "name": "Hybrid post-quantum key exchange",
     "phase": 5,
     "milestone": "1.6",
     "ticket": "post-quantum#hybrid-kex"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/crypto.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/crypto.md"
  },
  {
   "id": "e2e",
   "name": "End-to-end encryption",
   "category": "security",
   "summary": "Encrypt messages and records on people's devices with keys only they hold, using MLS for groups, so the server can't read them.",
   "status": "building",
   "stability": "experimental",
   "phase": 3,
   "milestone": "1.2",
   "spec": "e2e",
   "calls": [
    "e2e.join()",
    "e2e.group.create({ members })",
    "e2e.group.add(groupId, member)",
    "e2e.group.remove(groupId, member)",
    "e2e.group.get(groupId)",
    "e2e.encrypt(groupId, data)",
    "e2e.decrypt(groupId, message)",
    "e2e.recovery.enrol(prf)",
    "e2e.recovery.restore(prf)"
   ],
   "yaml": "e2e:\n  driver: mls\n  suite: classic\n  recovery: passkey\n  fields:\n    notes: [title, body]\n  accept: []\n",
   "example": "import { configureE2e, e2e, toWire } from \"mainsdk/experimental/e2e\";\nimport { localE2eDriver, memoryE2eRelay } from \"@mainsdk/testing\";\n\nconst relay = memoryE2eRelay();                                    // the server side; development only, with the local driver (a fake)\nawait configureE2e({ self: \"bob\", driver: localE2eDriver, relay }).join();    // bob's device publishes its key package\nconfigureE2e({ self: \"alice\", driver: localE2eDriver, relay });\nconst group = await e2e.group.create({ members: [\"bob\"] });        // keys stay on the devices\nconst sealed = await e2e.encrypt(group.id, \"meet at six\");         // the server stores ciphertext only\nconst note = toWire(sealed);                                       // a string that starts e2e1., safe in a column\n",
   "drivers": [
    {
     "id": "mls",
     "name": "MLS (RFC 9420)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/mls.html"
    },
    {
     "id": "local",
     "name": "Local fake (not for production)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Private keys never leave the device",
    "The server stores and relays ciphertext only",
    "Adding or removing a member changes the epoch, and a removed member can't read anything new",
    "Group membership changes are signed and audited",
    "Recovery uses the passkey PRF extension, with nothing the server can read"
   ],
   "checks": [
    "MAIN-E2E-0007",
    "MAIN-E2E-0008",
    "MAIN-E2E-0009"
   ],
   "runtime_codes": [
    "MAIN-E2E-0001",
    "MAIN-E2E-0002",
    "MAIN-E2E-0003",
    "MAIN-E2E-0004",
    "MAIN-E2E-0005",
    "MAIN-E2E-0006",
    "MAIN-E2E-0010",
    "MAIN-E2E-0011"
   ],
   "features": [
    {
     "name": "Contract",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "e2e#groups"
    },
    {
     "name": "Sealed fields skipped by search",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "e2e#skip"
    },
    {
     "name": "Key recovery with passkeys",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "e2e#recovery"
    },
    {
     "name": "MLS groups for messages and records (RFC 9420 driver)",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "e2e#mls-driver"
    },
    {
     "name": "Post-quantum hybrid ciphersuite",
     "phase": 5,
     "milestone": "1.6",
     "ticket": "post-quantum#hybrid-kex"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/e2e.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/e2e.md"
  },
  {
   "id": "network",
   "name": "Network permissions",
   "category": "platform",
   "summary": "Allow outbound calls only to the hosts you and your drivers declare, and block everything else.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "network",
   "calls": [
    "http.fetch(url, init)",
    "http.get(url, init)",
    "http.post(url, body, init)",
    "http.json(url, init)",
    "http.events(url, init)",
    "http.websocket(url, protocols)",
    "http.assert(url)",
    "network.destinations()",
    "exportAwsSg(allow, opts)",
    "exportK8s(allow, opts)",
    "isPrivateIp(ip)",
    "parseRule(text)",
    "createHttp(net)",
    "network.assert(target)",
    "network.mode",
    "network.rules",
    "network.decisions",
    "network.entries",
    "network.breakerSlots()",
    "network.breakerStates()",
    "network.decide(url, reason?, caller?)",
    "network.decidePublic(url, caller?)",
    "network.fetch(input, init?)",
    "network.fetchPublic(input, init?)",
    "network.matchHost(host)",
    "network.matchIp(ip)",
    "network.reload(opts)",
    "network.settingsFor(host, call?)",
    "network.trim()"
   ],
   "yaml": "network:\n  allow:\n    - api.partner.com\n    - \"*.pinecart.dev\"\n  default: deny\n  mode: enforce\n  http:\n    timeout: 10s\n    connect: 3s\n    retries: { attempts: 3, backoff: 200ms, max: 5s, deadline: 30s }\n    breaker: { failures: 50%, window: 30s, min: 10, cooldown: 15s }\n    hosts:\n      api.partner.com: { timeout: 3s, retries: { attempts: 5 } }\nexceptions:\n  - { rule: net.default_deny, reason: \"partner VPN\", owner: \"@pinecart/platform\", expires: 2026-11-30 }\n",
   "example": "import { http } from \"mainsdk\";\n\n// allowed only if api.partner.com is in network.allow\nconst res = await http.fetch(\"https://api.partner.com/v1/rates\");\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Deny by default; undeclared hosts blocked and reported",
    "DNS rebinding and redirects to undeclared hosts refused",
    "Every helper (get, post, json, events, websocket) passes the same check as http.fetch",
    "http.json and http.events refuse non-2xx answers and non-JSON bodies with MAIN-NET-0008",
    "Every outbound call has a timeout; retries and a per-host breaker are built in, with no hand-rolled loops",
    "Free on every plan"
   ],
   "checks": [
    "MAIN-NET-0001",
    "MAIN-NET-0004",
    "MAIN-NET-0007",
    "MAIN-NET-0040",
    "MAIN-NET-0041",
    "MAIN-NET-0050",
    "MAIN-NET-0051"
   ],
   "runtime_codes": [
    "MAIN-NET-0002",
    "MAIN-NET-0003",
    "MAIN-NET-0005",
    "MAIN-NET-0006",
    "MAIN-NET-0008",
    "MAIN-NET-0009",
    "MAIN-NET-0010",
    "MAIN-NET-0011",
    "MAIN-NET-0012"
   ],
   "features": [
    {
     "name": "Allowlist in main.yaml with driver-declared hosts",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#schema"
    },
    {
     "name": "Enforcement in the SDK network layer",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#chokepoint"
    },
    {
     "name": "DNS and redirect checks",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#dns-redirects"
    },
    {
     "name": "Blocked calls reported",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#report"
    },
    {
     "name": "Check against raw sockets and other HTTP clients",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#build-check"
    },
    {
     "name": "Export as platform firewall rules",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#export"
    },
    {
     "name": "Helpers for GET, POST, JSON, server-sent events and WebSockets",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#helpers"
    },
    {
     "name": "http.assert for database URLs and git remotes before connecting",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#helpers"
    },
    {
     "name": "Discovery of every host the code uses (mainsdk discover)",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#discover"
    },
    {
     "name": "Discovered hosts written as a build-time manifest (discover --out)",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#discover"
    },
    {
     "name": "Timeouts per attempt and per call, with a connect timeout",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#timeouts"
    },
    {
     "name": "network.http settings per host, with field-by-field overrides",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#http-config"
    },
    {
     "name": "Retries for idempotent calls, with backoff and Retry-After",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#retries"
    },
    {
     "name": "Per-host circuit breaker",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#breaker"
    },
    {
     "name": "Every attempt and breaker change in the decision log",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#attempt-log"
    },
    {
     "name": "Driver defaults for timeouts and idempotency keys",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#driver-defaults"
    },
    {
     "name": "Measured overhead against the performance budget",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "network#reliability-overhead"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/network.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/network.md"
  },
  {
   "id": "disk",
   "name": "Local files",
   "category": "platform",
   "summary": "Read and write only the folders your server app declares; user uploads go through files, not here.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "app-permissions",
   "calls": [
    "disk.read(path)",
    "disk.readText(path)",
    "disk.write(path, data)",
    "disk.list(path)",
    "disk.exists(path)",
    "disk.delete(path)",
    "createDisk(perm)",
    "validateDisk(disk)",
    "disk.temp(fn, { prefix? })",
    "disk.remove(path)",
    "disk.space(path)",
    "disk.usage(path)"
   ],
   "yaml": "permissions:\n  mode: enforce\n  disk:\n    read: [content]\n    write: [.cache]\n",
   "example": "import { disk } from \"mainsdk\";\n\n// allowed only if .cache is under permissions.disk.write\nawait disk.write(\".cache/rates.json\", JSON.stringify(rates));\nconst cached = await disk.readText(\".cache/rates.json\");\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Paths outside the declared folders are refused, including through symlinks",
    "A folder declared for write can also be read; nothing else is reachable",
    "Every use is recorded with the calling file and line",
    "Server side only; the browser has no disk"
   ],
   "checks": [
    "MAIN-PERM-0001",
    "MAIN-PERM-0003",
    "MAIN-PERM-0008"
   ],
   "runtime_codes": [
    "MAIN-PERM-0002",
    "MAIN-PERM-0006",
    "MAIN-PERM-0007"
   ],
   "features": [
    {
     "name": "Declared read and write folders under permissions.disk",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Enforce and observe modes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Undeclared folder use found by mainsdk check",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Decisions shown by mainsdk info",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/disk.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/disk.md"
  },
  {
   "id": "system",
   "name": "Server facts and programs",
   "category": "platform",
   "summary": "Read the facts about the server and run the programs you declare, with every URL they are given checked against the network list.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "app-permissions",
   "calls": [
    "system.user()",
    "system.hostname()",
    "system.platform()",
    "system.networkInterfaces()",
    "system.run(cmd, args, opts)",
    "createSystem(perm)",
    "SYSTEM_FACTS(const)"
   ],
   "yaml": "permissions:\n  mode: enforce\n  system: [hostname, platform]\n  processes: [git]\n",
   "example": "import { system } from \"mainsdk\";\n\n// allowed only if git is under permissions.processes and the URL is in network.allow\nconst out = await system.run(\"git\", [\"ls-remote\", \"https://github.com/acme/app.git\"]);\nconst os = system.platform();\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Only the facts and programs declared under permissions are readable or runnable",
    "Programs run without a shell, so arguments are never interpreted",
    "Every URL in a program's arguments is checked against network.allow",
    "Every use is recorded with the calling file and line",
    "Server side only"
   ],
   "checks": [
    "MAIN-PERM-0001",
    "MAIN-PERM-0003",
    "MAIN-NET-0001"
   ],
   "runtime_codes": [
    "MAIN-PERM-0002",
    "MAIN-PERM-0004",
    "MAIN-PERM-0005"
   ],
   "features": [
    {
     "name": "Declared operating system facts under permissions.system",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Declared programs under permissions.processes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "URL arguments checked against network.allow",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    },
    {
     "name": "Undeclared facts and programs found by mainsdk check",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/system.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/system.md"
  },
  {
   "id": "net",
   "name": "TCP, UDP and unix sockets",
   "category": "platform",
   "summary": "Open TCP connections, run TCP servers, send UDP datagrams and use unix domain sockets, each checked against the protocols, ports and directions main.yaml declares.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "net-sockets",
   "calls": [
    "net.connect(host, port, opts)",
    "net.listen(port, handler, opts)",
    "net.udp(opts)",
    "net.connectUnix(path, opts)",
    "net.listenUnix(path, handler, opts)",
    "createNet(get)"
   ],
   "yaml": "network:\n  allow:\n    - api.partner.com           # HTTP, HTTPS and WebSocket only\n    - tcp://db.acme.dev:5432    # raw TCP to this host and port\n    - udp://8.8.8.8:53          # UDP to this peer and port\n    - unix:///run/app/redis.sock   # a unix domain socket path\n  listen:\n    - tcp:7000                  # loopback only\n    - unix:///run/app/ctl.sock  # serve this unix socket path\n    - { protocol: tcp, port: 9000-9010, bind: 0.0.0.0, from: [10.0.0.0/8] }\n  udp:\n    multicast: [\"239.255.0.1:5000\"]\n  tcp:\n    timeout: 3s\n    idle: 5m\n    tls: { min-version: TLSv1.2 }\n",
   "example": "import { net } from \"mainsdk\";\n\n// allowed only if network.allow has tcp://db.acme.dev:5432\nconst conn = await net.connect(\"db.acme.dev\", 5432, { tls: true });\nconst writer = conn.writable.getWriter();\nawait writer.write(new TextEncoder().encode(\"PING\\r\\n\"));\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Plain network.allow hosts never open raw sockets; TCP and UDP need an entry with protocol and port",
    "Listening needs a network.listen entry and binds loopback unless the entry says otherwise",
    "The socket connects to the address that passed the DNS-rebinding check",
    "TLS verifies by default, TLS 1.2 minimum, certificates only from secret references",
    "Connects retry only before the first byte; open streams and UDP are never retried",
    "Every queue is bounded, with backpressure through Web streams",
    "Broadcast and multicast are off unless declared",
    "A unix socket needs a unix:// entry and is never opened by network.default; serving one needs a private 0700 directory and makes the file 0600",
    "Free on every plan"
   ],
   "checks": [
    "MAIN-NET-0001",
    "MAIN-NET-0007",
    "MAIN-NET-0020",
    "MAIN-NET-0025",
    "MAIN-NET-0028",
    "MAIN-NET-0029",
    "MAIN-NET-0030"
   ],
   "runtime_codes": [
    "MAIN-NET-0002",
    "MAIN-NET-0009",
    "MAIN-NET-0010",
    "MAIN-NET-0011",
    "MAIN-NET-0021",
    "MAIN-NET-0022",
    "MAIN-NET-0023",
    "MAIN-NET-0024",
    "MAIN-NET-0026",
    "MAIN-NET-0027",
    "MAIN-NET-0031",
    "MAIN-NET-0032",
    "MAIN-NET-0033"
   ],
   "features": [
    {
     "name": "tcp:// and udp:// entries, network.listen and network.udp",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#grammar"
    },
    {
     "name": "Unix domain sockets with unix:// entries, a private directory and a 0600 socket file",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#unix"
    },
    {
     "name": "net.connect over Web streams to the checked address",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#connect"
    },
    {
     "name": "Connect timeout, connect retries and a breaker per host and port",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#connect-reliability"
    },
    {
     "name": "Backpressure and half-open handling",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#streams"
    },
    {
     "name": "TLS verified by default, mTLS and startTls",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#tls"
    },
    {
     "name": "net.listen with bind and from limits, shedding and the drain",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#listen"
    },
    {
     "name": "net.udp with send, receive and bind",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#udp"
    },
    {
     "name": "Declared broadcast and multicast",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#udp-groups"
    },
    {
     "name": "Sockets discovered and checked",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#discover-check"
    },
    {
     "name": "Serverless and edge targets reported in check",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#runtime-check"
    },
    {
     "name": "Socket rules exported as platform firewall rules",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#export"
    },
    {
     "name": "In-process loopback network for tests",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "net-sockets#loopback-fake"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/net.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/net.md"
  },
  {
   "id": "workers",
   "name": "Worker threads and cluster",
   "category": "platform",
   "summary": "Run CPU-heavy work on a pool of worker threads or processes, and run one server instance per core, declared in main.yaml and carrying the tenant.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "parallelism",
   "calls": [
    "workers.run(module, input, opts)",
    "workers.snapshot()",
    "worker(fn)",
    "cluster.run(main)",
    "cluster.restart(opts)",
    "cluster.snapshot()",
    "cluster.role()",
    "cluster(object: one instance per core)",
    "parseWorkers(section, ctx)",
    "validateWorkers(config, ctx)"
   ],
   "yaml": "workers:\n  size: auto\n  queue: 1000\n  timeout: 30s\n  memory: 256MB\n  idle: 30s\n  env: [TZ]\n  modules: [\"src/workers/*.worker.ts\"]\n  builtin: on\n  pools:\n    pdf: { size: 2, isolation: process, memory: 1GB, modules: [src/workers/pdf.worker.ts] }\ncluster:\n  processes: auto\n  start-timeout: 30s\n  respawn: { backoff: 1s, max: 30s, crashes: 5, window: 60s }\n",
   "example": "import { workers } from \"mainsdk\";\n\n// runs on a worker thread; the module must match workers.modules\nconst pages = await workers.run(\"./src/workers/parse.worker.ts\", { fileId }, { timeout: \"20s\" });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Only declared worker modules run",
    "Pools are sized from the cores the process may use, cgroup quota included",
    "Every task has a timeout and a signal; a stuck thread is terminated and replaced",
    "The tenant, trace and audit trail cross into every task; secrets and the environment don't",
    "Cluster instances share nothing but the port, the local store and broadcasts, so limits stay correct",
    "Rolling restarts and shutdown go through the health drain",
    "Free on every plan"
   ],
   "checks": [
    "MAIN-WORKER-0001",
    "MAIN-WORKER-0009",
    "MAIN-WORKER-0010",
    "MAIN-WORKER-0016"
   ],
   "runtime_codes": [
    "MAIN-WORKER-0002",
    "MAIN-WORKER-0003",
    "MAIN-WORKER-0004",
    "MAIN-WORKER-0005",
    "MAIN-WORKER-0006",
    "MAIN-WORKER-0007",
    "MAIN-WORKER-0008",
    "MAIN-WORKER-0011",
    "MAIN-WORKER-0012",
    "MAIN-WORKER-0013",
    "MAIN-WORKER-0014",
    "MAIN-WORKER-0015"
   ],
   "features": [
    {
     "name": "workers and cluster sections with core-based sizing",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#contract"
    },
    {
     "name": "workers.run on a lazy thread pool with a bounded queue",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#pool"
    },
    {
     "name": "Structured clone and transfer lists",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#data"
    },
    {
     "name": "Timeouts, deadlines and signals that terminate stuck threads",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#stop"
    },
    {
     "name": "Context in, secrets out, logs through redaction",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#boundary"
    },
    {
     "name": "Process pools for code that may crash",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#process-pool"
    },
    {
     "name": "Inline fallback and inline test mode",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#fallbacks"
    },
    {
     "name": "Documents, imports and the seal boot check on the built-in pool",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#builtin"
    },
    {
     "name": "cluster.run with one instance per core on a shared port",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#cluster-run"
    },
    {
     "name": "Rolling restarts through the drain",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#rolling"
    },
    {
     "name": "Crash respawn with backoff and crash-loop stop",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#supervise"
    },
    {
     "name": "Local store and broadcasts relayed by the primary",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#shared-state"
    },
    {
     "name": "Cluster-wide resource pressure",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "parallelism#pressure"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/workers.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/workers.md"
  },
  {
   "id": "sandbox",
   "name": "Code sandboxes",
   "category": "security",
   "summary": "Run untrusted or AI-written code in a sandbox with time, memory and cost limits, no secrets, and only the hosts you allow.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "sandbox",
   "calls": [
    "sandbox.run({ code, language, files, network, feature })",
    "sandbox.create(opts)",
    "sandbox.get(id)",
    "sandbox.action({ name, intent, languages })",
    "new Sandbox(deps)",
    "createSandbox(deps)",
    "parseSandboxConfig(raw)",
    "secret(name)",
    "localSandboxDriver(opts)",
    "e2bNetworkPolicy(hosts)",
    "vercelNetworkPolicy(hosts)"
   ],
   "yaml": "sandbox:\n  driver: vercel-sandbox\n  limits: { timeout: 30s, memory: 512MB, output: 1MB, max-cost: $0.05 }\n  network: [pypi.org]\n",
   "example": "import { sandbox } from \"mainsdk\";\n\nconst out = await sandbox.run({\n  code: \"print(sum(range(10)))\",\n  language: \"python\",\n  feature: \"analysis\",        // no network, no secrets, killed at its limits\n});\n",
   "drivers": [
    {
     "id": "vercel-sandbox",
     "name": "Vercel Sandbox",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/vercel.html"
    },
    {
     "id": "e2b",
     "name": "E2B",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/e2b.html"
    },
    {
     "id": "local",
     "name": "Local subprocess (development)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Code never runs in the app's process",
    "No network unless allowed, and never wider than network.allow",
    "No secrets or app values unless named",
    "Killed at its time, memory, output and cost limits",
    "Every run audited with the code's hash, never the code",
    "The local driver is refused in production"
   ],
   "checks": [
    "MAIN-SANDBOX-0010",
    "MAIN-SANDBOX-0011",
    "MAIN-SANDBOX-0012"
   ],
   "runtime_codes": [
    "MAIN-SANDBOX-0001",
    "MAIN-SANDBOX-0002",
    "MAIN-SANDBOX-0003",
    "MAIN-SANDBOX-0004",
    "MAIN-SANDBOX-0005",
    "MAIN-SANDBOX-0006",
    "MAIN-SANDBOX-0007",
    "MAIN-SANDBOX-0008",
    "MAIN-SANDBOX-0009"
   ],
   "features": [
    {
     "name": "Local driver with time and memory limits, an empty environment and no network",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "sandbox#local-driver"
    },
    {
     "name": "Network allowlist parity with network.allow",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "sandbox#network-parity"
    },
    {
     "name": "No secrets unless named; files through files",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "sandbox#secrets-files"
    },
    {
     "name": "Sessions with run",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "sandbox#sessions"
    },
    {
     "name": "Vercel Sandbox and E2B drivers",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "sandbox#provider-drivers"
    },
    {
     "name": "A code-runner action for agents",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "sandbox#action"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/sandbox.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/sandbox.md"
  },
  {
   "id": "limits",
   "name": "Rate limits and quotas",
   "category": "security",
   "summary": "Rate-limit requests, enforce quotas and take locks from named policies, with standard 429 answers and a declared mode for when the store is down.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "limits",
   "calls": [
    "limits.rate(key, policy, { cost? })",
    "limits.enforce(policy, { key?, cost? })",
    "limits.quota(name, { key?, add? })",
    "limits.release(name, { key?, add })",
    "limits.lock(name, fn, { ttl?, wait? })",
    "limits.concurrency(name, fn, { key?, wait? })",
    "limits.headers(decision)",
    "new LimitError(code, message, retryAfter, headers, decision?)",
    "asKeyLimits(limiter)",
    "limitsIssues(raw)",
    "localLimits(o?)",
    "parseLimits(raw)",
    "new StoreHub(o)",
    "dbStore(o)",
    "localStore(o?)",
    "redisStore(o)",
    "limits.hubs()",
    "limits.info()",
    "limits.switchStore(down)",
    "limits.declaredRate(policy)"
   ],
   "yaml": "limits:\n  driver: redis\n  redis: { url: secret:REDIS_URL }\n  policies:\n    login: { max: 5, per: 15m, algorithm: token-bucket, key: ip, on-store-down: closed }\n    export: { max: 5, per: 1m, key: user }\n  quotas:\n    exports-daily: { max: 50, per: day, key: user }\n  concurrency:\n    transcode: { max: 2, key: tenant, ttl: 10m }\n",
   "example": "import { limits } from \"mainsdk\";\n\n// 429 with Retry-After and RateLimit headers past 5 a minute per person\nawait limits.enforce(\"export\");\n\nawait limits.concurrency(\"transcode\", (signal) => transcode(file, signal));\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local (in process)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "redis",
     "name": "Redis",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/redis.html"
    },
    {
     "id": "arcjet",
     "name": "Arcjet",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/arcjet.html"
    }
   ],
   "guarantees": [
    "Every refusal is a 429 problem with Retry-After and the RateLimit headers",
    "Each policy declares what happens when the store is down; nothing fails by surprise",
    "Counters are per tenant and the store holds no raw keys or personal data",
    "A decision never waits longer than the store timeout"
   ],
   "checks": [
    "MAIN-LIMIT-0006",
    "MAIN-LIMIT-0007",
    "MAIN-LIMIT-0008"
   ],
   "runtime_codes": [
    "MAIN-LIMIT-0001",
    "MAIN-LIMIT-0002",
    "MAIN-LIMIT-0003",
    "MAIN-LIMIT-0004",
    "MAIN-LIMIT-0005",
    "MAIN-LIMIT-0009"
   ],
   "features": [
    {
     "name": "limits@1 contract and named policies",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "limits#contract"
    },
    {
     "name": "Token bucket and sliding window rate limits",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "limits#rate"
    },
    {
     "name": "429 with Retry-After and RateLimit headers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "limits#enforce"
    },
    {
     "name": "Quotas per window",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "limits#quota"
    },
    {
     "name": "Locks and concurrency slots with leases",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "limits#lock"
    },
    {
     "name": "Redis driver with atomic decisions",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "limits#redis"
    },
    {
     "name": "Open, closed or local when the store is down",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "limits#fail-policy"
    },
    {
     "name": "Counters in the tenant's region",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "limits#regions"
    },
    {
     "name": "Per-key limits for apikeys",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "apikeys#rate-audit"
    },
    {
     "name": "Plan request rates",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#limits-flags"
    },
    {
     "name": "IP allowlist per organisation",
     "phase": 1,
     "milestone": "0.3",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/limits.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/limits.md"
  },
  {
   "id": "protect",
   "name": "Protected files",
   "category": "security",
   "summary": "Protect a file so it opens only under its policy, at its release time, can be revoked in seconds and shows who opened it.",
   "status": "building",
   "stability": "experimental",
   "phase": 1,
   "milestone": "0.4",
   "spec": "protect-taps",
   "calls": [
    "protect(file, { label, policy })",
    "protect.get(asset)",
    "protect.list({ cursor })",
    "protect.update(asset, { policy, label })",
    "protect.versions.create(asset, file)",
    "protect.revoke(asset, { reason, person })",
    "protect.erase(asset, { reason, approvals })",
    "protect.transfer(asset, { to })",
    "protect.activity(asset)",
    "protect.usage(asset)",
    "protect.open(asset, { purpose })",
    "protect.policy.check(asset, { as, agent, purpose })",
    "protect.policy.simulate(asset, policy)",
    "protect.policy.lint(policy)",
    "protect.zones.list()",
    "protect.zones.update(zones, { approvals })",
    "protect.threshold.get(asset)",
    "protect.share(asset, { to })",
    "protect.share.get(link)",
    "protect.share.list(asset)",
    "protect.share.update(link, change)",
    "protect.share.revoke(link)",
    "protect.share.stats(link, { consent })",
    "protect.people.create(asset, { person })",
    "protect.people.list(asset)",
    "protect.people.remove(asset, person)",
    "protect.agents(asset, { allowed })",
    "protect.delegate.create({ agent, assets, actions, purposes, expiresIn })",
    "protect.delegate.get(id)",
    "protect.delegate.list()",
    "protect.delegate.revoke(id)",
    "protect.approvals.list({ state })",
    "protect.approvals.get(id)",
    "protect.approvals.update(id, { decision })",
    "protect.query(assets, { question, purpose })",
    "protect.mcp.tools()",
    "protect.mcp.manifest()",
    "protect.mcp.verify(manifest, tools)",
    "protect.attest.agent(agent)",
    "protect.attest.verify(evidence, { kind })",
    "protect.inspect(asset, { function })",
    "protect.environment.get()",
    "protect.ledger.list({ asset })",
    "protect.ledger.proof(asset)",
    "protect.ledger.verify()",
    "protect.ledger.export()",
    "protect.leaks.create({ asset, evidence })",
    "protect.leaks.trace(caseId, { approvals })",
    "protect.lockdown.create({ reason, approvals })",
    "protect.lockdown.delete(id, { approvals })",
    "protect.channels.list()",
    "protect.channels.events(pattern)",
    "protect.channels.watch(pattern, fn)",
    "protect.channels.security()",
    "createProtect(deps)",
    "new ProtectError(code, message, opts?)",
    "PROTECT_CHANNELS(const)",
    "PROTECT_DRIVERS(const)",
    "localProtect(deps)",
    "lintPolicy(policy)",
    "parseProtectSettings(raw)",
    "protectHosts(settings)",
    "routeShare(base, built, { protect })"
   ],
   "yaml": "protect:\n  driver: taps-node          # local in dev and tests\n  url: https://taps.acme.dev\n  share-domain: open.acme.dev\n  key: secret:MAINSDK_PROTECT_KEY\n  mode: viewer               # viewer | wrapper\n  suite: TAPS-CS1            # TAPS-CS1 | TAPS-CS2\n  renew: 10s                 # licence renewal, 5s to 5m\n  mcp: false                 # true adds the taps_* tools to the MCP server\n",
   "example": "import { protect, share } from \"mainsdk\";\n\nconst asset = await protect(paper, { label: \"sensitive\", policy: { releaseAt: \"2027-03-01T09:00:00Z\" } });\nconst { url } = await share(asset, { to: examiners });   // a getqr.id link that opens the TAPS Web Reader\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local fake TAPS Node",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "taps-node",
     "name": "Self-hosted TAPS Node",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/taps.html"
    },
    {
     "id": "taps-hosted",
     "name": "Hosted TAPS Node",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/taps.html"
    },
    {
     "id": "sovedocs",
     "name": "SoveDocs",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/taps.html"
    }
   ],
   "guarantees": [
    "Ciphertext without a licence",
    "No licence before releaseAt, or before t of n parties release",
    "Revocation takes effect within one renew interval (10 s by default)",
    "Opens, denials and revocations in the MainSDK decision log and the TAPS ledger",
    "Agents act only inside a delegation from the person they act for",
    "Share links and QR codes through getqr.id",
    "No post-quantum claim until TAPS-CS3 is registered"
   ],
   "checks": [
    "MAIN-PROTECT-0020",
    "MAIN-PROTECT-0021",
    "MAIN-PROTECT-0022"
   ],
   "runtime_codes": [
    "MAIN-PROTECT-0001",
    "MAIN-PROTECT-0002",
    "MAIN-PROTECT-0003",
    "MAIN-PROTECT-0004",
    "MAIN-PROTECT-0005",
    "MAIN-PROTECT-0006",
    "MAIN-PROTECT-0007",
    "MAIN-PROTECT-0008",
    "MAIN-PROTECT-0009",
    "MAIN-PROTECT-0010",
    "MAIN-PROTECT-0011",
    "MAIN-PROTECT-0012",
    "MAIN-PROTECT-0013",
    "MAIN-PROTECT-0014",
    "MAIN-PROTECT-0015",
    "MAIN-PROTECT-0016",
    "MAIN-PROTECT-0017",
    "MAIN-PROTECT-0018",
    "MAIN-PROTECT-0019"
   ],
   "features": [
    {
     "name": "Protect, get, list, update, versions, revoke, erase, transfer, activity and usage",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#local-core"
    },
    {
     "name": "Policy by person, device, network zone, geo, time and purpose",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#policy"
    },
    {
     "name": "Release on time and threshold release",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#release"
    },
    {
     "name": "Share through getqr.id into the TAPS Web Reader",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#share"
    },
    {
     "name": "Agent delegation and human approval",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#agents"
    },
    {
     "name": "Compute-only query",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#query"
    },
    {
     "name": "Signed taps_* MCP tools",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#mcp"
    },
    {
     "name": "Attestation, inspection, holdings and assistive access",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#attest-inspect"
    },
    {
     "name": "Channels, decision log, ledger and security events",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#rails"
    },
    {
     "name": "Post-quantum claim gate",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#pq-gate"
    },
    {
     "name": "TAPS Node, hosted and SoveDocs drivers in @mainsdk/protect-taps",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "protect-taps#driver-package"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/protect.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/protect.md"
  },
  {
   "id": "privacy",
   "name": "Privacy and data rights",
   "category": "security",
   "summary": "Export or erase a person's data across every driver with one call, and record their consent.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "privacy-lifecycle",
   "calls": [
    "privacy.export(user)",
    "privacy.erase(user, { reason })",
    "privacy.consent(user).set({ purpose, granted })",
    "privacy.consent(user).get(purpose)",
    "privacy.hold(user, { until, reason })",
    "privacy.release(user)",
    "privacy.consent(user).signal({ tcf } | { gpp })",
    "privacy.register(name, erase, { export?, labels?, region? })",
    "privacy.retain()",
    "privacy.resume()",
    "privacy.pending()",
    "privacy.verify(record)",
    "privacy.guard(user, { purpose, send })",
    "privacy.registerRetainer(key, prune)"
   ],
   "yaml": "privacy:\n  retention:\n    personal: 365d\n    child: 90d\n    logs: 30d\n    analytics: 13m\n  erase: { confirm-within: 30d, retries: 3, retry-delay: 1s }\n  consent:\n    tcf: { analytics: [1], personalisation: [1, 3, 4] }\n",
   "example": "import { privacy } from \"mainsdk\";\n\nawait privacy.consent(user).set({ purpose: \"analytics\", granted: false });\nconst record = await privacy.erase(user, { reason: \"user request\" });\n// signed record listing every driver and its confirmation\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Retention declared per label and enforced everywhere",
    "Erasure confirmed by every driver, with a signed record",
    "Backups erased by deleting the person's key",
    "Consent recorded per purpose, and a consent platform's TCF or GPP signal read with no second banner",
    "A driver that cannot confirm leaves a partial signed record and a pending erasure, never a failed request"
   ],
   "checks": [
    "MAIN-PRIV-0001",
    "MAIN-PRIV-0004"
   ],
   "runtime_codes": [
    "MAIN-PRIV-0002",
    "MAIN-PRIV-0003",
    "MAIN-PRIV-0005",
    "MAIN-PRIV-0006"
   ],
   "features": [
    {
     "name": "Retention per data label",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#retention"
    },
    {
     "name": "Erase and export hooks in drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#driver-hooks"
    },
    {
     "name": "Erase across every driver",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#erase"
    },
    {
     "name": "Signed erasure record and backup crypto-erasure",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#erasure-record"
    },
    {
     "name": "One export bundle",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#export"
    },
    {
     "name": "Consent records with TCF and GPP signals",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#consent"
    },
    {
     "name": "Generated privacy labels and data map",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#labels-gen"
    },
    {
     "name": "Legal hold",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#hold"
    },
    {
     "name": "Partial erasure that retries and stays audited",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#partial"
    },
    {
     "name": "Context",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "privacy-lifecycle#wiring"
    },
    {
     "name": "Industry packs for payments",
     "phase": 3,
     "milestone": "1.2",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/privacy.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/privacy.md"
  },
  {
   "id": "backup",
   "name": "Backups and restore drills",
   "category": "reliability",
   "summary": "Declare how much data you can lose and how fast you must restore, and prove it with timed restore drills.",
   "status": "building",
   "stability": "beta",
   "phase": 3,
   "milestone": "1.2",
   "spec": "backup-restore-evidence",
   "calls": [
    "runDrill({ driver, target, profile, at })",
    "signDrill(result, key)",
    "verifyDrill(evidence, trusted)",
    "backupIssues(profile, declared, drill)",
    "drillControls({ standard, evidence, now })",
    "backupBadge({ evidence, now })"
   ],
   "yaml": "backup:\n  rpo: 15m\n  rto: 1h\n  keep: 35d\n  drill: { every: 30d, to: scratch }\n",
   "example": "import { runDrill, signDrill, drillKey } from \"mainsdk/backup\";\n\nconst result = await runDrill({ driver, target, profile, declared, at });   // restores into scratch, never production\nconst evidence = await signDrill(result, await drillKey());\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local (tests and drills without a provider)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "neon-pitr",
     "name": "Neon point-in-time recovery",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/neon.html"
    },
    {
     "id": "supabase-pitr",
     "name": "Supabase point-in-time recovery",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/supabase.html"
    },
    {
     "id": "pgbackrest",
     "name": "pgBackRest",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/pgbackrest.html"
    },
    {
     "id": "aws-backup",
     "name": "AWS Backup",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/aws-backup.html"
    },
    {
     "id": "object-lock",
     "name": "Object storage with object lock",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Backups encrypted, copied off-site and immutable for the retention period",
    "The badge turns amber when no restore drill passed in 30 days",
    "Restore per tenant, not only all or nothing",
    "MainSDK never runs or stores backups itself; the provider does"
   ],
   "checks": [
    "MAIN-BACKUP-0001",
    "MAIN-BACKUP-0002",
    "MAIN-BACKUP-0003",
    "MAIN-BACKUP-0006"
   ],
   "runtime_codes": [
    "MAIN-BACKUP-0004",
    "MAIN-BACKUP-0005"
   ],
   "features": [
    {
     "name": "Backup and point-in-time restore in the db and files contracts",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "backup-restore-evidence#backup-capability"
    },
    {
     "name": "Provider backup settings read",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "backup-restore-evidence#provider-readers"
    },
    {
     "name": "RPO, RTO and retention check",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "backup-restore-evidence#rpo-rto-check"
    },
    {
     "name": "Timed restore drill into scratch",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "backup-restore-evidence#drill-restore"
    },
    {
     "name": "Signed drill evidence",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "backup-restore-evidence#drill-evidence"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/backup.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/backup.md"
  },
  {
   "id": "deploy",
   "name": "Deploy config and serverless schedules",
   "category": "reliability",
   "summary": "Write the platform's own deploy files from main.yaml for Vercel, Cloudflare Workers, Fly.io and Docker, wake schedules through one tick route, check the build before it ships and diff two environments.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "deploy",
   "calls": [
    "createTick(app)",
    "tick.handle(req)",
    "tick.scheduled(event)",
    "deploySection(config)",
    "staticSchedules(config)",
    "slotOf(expr, now)"
   ],
   "yaml": "deploy:\n  target: vercel\n  regions: [fra1]\n  port: 3000\n  tick:\n    secret: secret:CRON_SECRET\n    late: 10m\n  vercel: { plan: pro }\n  docker: { base: node:24-slim }\njobs:\n  schedules:\n    nightly-report: \"0 3 * * *\"\nseal:\n  mode: warn\n  exclude: [\"**/*.map\"]\n  publish: { sbom: false }\n",
   "example": "// Hono on Vercel: the platform cron calls this route with the bearer secret\nimport { Hono } from \"hono\";\nimport { createTick } from \"mainsdk/deploy\";\nimport { app } from \"mainsdk\";\n\nconst tick = createTick(app());\nconst router = new Hono();\nrouter.all(\"/_mainsdk/jobs/tick\", (c) => tick.handle(c.req.raw));\n\n// Cloudflare Workers: the same code, in process, for a cron trigger\nexport default { fetch: router.fetch, scheduled: (e) => tick.scheduled(e) };\n",
   "drivers": [
    {
     "id": "vercel",
     "name": "Vercel",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/vercel.html"
    },
    {
     "id": "cloudflare",
     "name": "Cloudflare Workers",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/cloudflare.html"
    },
    {
     "id": "fly",
     "name": "Fly.io",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/fly.html"
    },
    {
     "id": "docker",
     "name": "Docker",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/docker.html"
    }
   ],
   "guarantees": [
    "MainSDK writes and checks deploy files; it never pushes a deploy, holds a platform token or hosts the app",
    "Generated blocks are owned and checked; every other key and line of the file is kept byte for byte",
    "Regions follow the data regions, and a region outside them is a finding",
    "A schedule runs once per slot, however many times the platform calls the tick",
    "The build fails before anything is served when a required env var name is missing",
    "deploy config, preflight and diff make no network call and read no platform credential"
   ],
   "checks": [
    "MAIN-DEPLOY-0001",
    "MAIN-DEPLOY-0002",
    "MAIN-DEPLOY-0003",
    "MAIN-DEPLOY-0004",
    "MAIN-DEPLOY-0005",
    "MAIN-DEPLOY-0010",
    "MAIN-DEPLOY-0011",
    "MAIN-DEPLOY-0012"
   ],
   "runtime_codes": [
    "MAIN-DEPLOY-0006",
    "MAIN-DEPLOY-0007",
    "MAIN-DEPLOY-0008"
   ],
   "features": [
    {
     "name": "Deploy section and jobs.schedules in main.yaml with region and plan-limit rules",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#contract"
    },
    {
     "name": "Schedules from jobs.schedules and literal jobs.schedule calls",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#schedules"
    },
    {
     "name": "vercel.json regions",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#vercel"
    },
    {
     "name": "wrangler.toml with cron triggers and the in-process scheduled handler",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#cloudflare"
    },
    {
     "name": "fly.toml and a non-root Dockerfile with probes and a shutdown deadline",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#fly-docker"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/deploy.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/deploy.md"
  },
  {
   "id": "status",
   "name": "Health, resources and status page",
   "category": "reliability",
   "summary": "Serve health probes, drain cleanly on shutdown, track SLO burn, watch CPU, event-loop delay and memory. An app status page with incidents is planned for phase 2 and not built yet.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "health",
   "calls": [
    "status.check(name, fn, { every?, timeout?, critical? })",
    "resources.snapshot()",
    "resources.state()",
    "resources.onPressure(fn)",
    "cgroupCores(read)",
    "cgroupMemory(read)",
    "isServerless(env)",
    "nodeSources(opts)",
    "parseResources(sec)",
    "validateResources(sec)",
    "parseHealth(sec, env)",
    "validateHealth(sec, env, { known })",
    "new Slo(opts)",
    "parseSlo(sec)",
    "validateSlo(sec)",
    "resources(object: snapshot, state, onPressure)",
    "realTimers(const)"
   ],
   "yaml": "status:\n  driver: builtin\nhealth:\n  live: /healthz\n  ready: /readyz\n  critical: [db]                       # dependencies that make the app unready\n  token: secret:HEALTH_TOKEN           # optional: guards the detailed report\n  shutdown:\n    deadline: 25s\n    drain-delay: 5s\nslo:\n  availability: 99.9%\n  exclude: [\"GET /healthz\"]            # routes left out of the readings\n  alert: { to: [log, telemetry] }\nresources:\n  critical: [\"POST /api/checkout/**\"]  # never paused or shed\n  shed: { loop: 500ms }\n  nice: 10                             # 0 to 19\n  retry-after: 5s\n  max-pause: 5m\n",
   "example": "import { status } from \"mainsdk\";\n\nstatus.check(\"payments\", async () => (await pingProvider()).ok);\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "provider",
     "name": "Your status page provider",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Health checks per dependency",
    "Probes read cached state only and never hang on a slow dependency"
   ],
   "checks": [
    "MAIN-HEALTH-0005",
    "MAIN-HEALTH-0006",
    "MAIN-RESOURCE-0003"
   ],
   "runtime_codes": [
    "MAIN-HEALTH-0001",
    "MAIN-HEALTH-0002",
    "MAIN-HEALTH-0003",
    "MAIN-HEALTH-0004",
    "MAIN-HEALTH-0010",
    "MAIN-HEALTH-0011",
    "MAIN-RESOURCE-0001",
    "MAIN-RESOURCE-0002",
    "MAIN-RESOURCE-0004",
    "MAIN-RESOURCE-0005",
    "MAIN-RESOURCE-0006"
   ],
   "features": [
    {
     "name": "App status page and status.incident (not built; the incident controls are the separate incident interface)",
     "phase": 2,
     "milestone": "0.6",
     "ticket": null
    },
    {
     "name": "Remote dependency state through health()",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "fail-static#dependency-status"
    },
    {
     "name": "health and slo sections in main.yaml",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "health#contract"
    },
    {
     "name": "/healthz and /readyz from every adapter",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "health#probes"
    },
    {
     "name": "Custom readiness probes with status.check",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "health#status-check"
    },
    {
     "name": "Graceful drain on SIGTERM",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "health#drain"
    },
    {
     "name": "Shutdown deadline and release of cut work",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "health#deadline"
    },
    {
     "name": "SLO windows from the request log",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "health#slo-windows"
    },
    {
     "name": "Error-budget burn alerts",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "health#slo-alerts"
    },
    {
     "name": "Probe state and burn in mainsdk info",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "health#info"
    },
    {
     "name": "CPU, event-loop delay and memory sampled against the container's limits",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "resources#sampler"
    },
    {
     "name": "Background work paused and caches trimmed under pressure",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "resources#pause-background"
    },
    {
     "name": "Non-critical requests shed with 503 and Retry-After",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "resources#shed"
    },
    {
     "name": "Burn",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#sources"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/status-page.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/status-page.md"
  },
  {
   "id": "security",
   "name": "Security events",
   "category": "security",
   "summary": "Write security events in OCSF with ATT&CK tags, detect brute force, stuffing and probing in the app, and stream them to your own SIEM.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "security-events",
   "calls": [
    "security.emit({ type, severity, message, attrs?, attack? })",
    "security.on(type, fn)"
   ],
   "yaml": "security:\n  events:\n    to: [log, otel]\n    min: low\n    regions:\n      eu: { endpoint: https://otel.eu.acme.dev:4318, headers: { authorization: secret:OTEL_EU_TOKEN } }\n  detect:\n    brute-force: { failures: 10, window: 5m }\n    stuffing: { accounts: 20, window: 5m }\n    probing: { denials: 20, window: 1m }\n",
   "example": "import { security } from \"mainsdk/security\";\n\n// the app's own event: only app.* types; the SDK writes the catalogue events itself\nsecurity.emit({ type: \"app.refund_spike\", severity: \"medium\", message: \"Refunds above the daily norm\", attrs: { count: 41 } });\n\n// read every sign-in refusal after it is written\nconst stop = security.on(\"auth.*\", (e) => console.log(e.type, e.severity));\nstop();\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (log, OTLP and events sinks)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Every event is OCSF with ATT&CK tags and no personal data (keyed user hash, trimmed IP)",
    "Never on the request path; export is buffered and drops events below high first",
    "Each event goes to the endpoint of its tenant's region",
    "Detectors run in the app with bounded counters"
   ],
   "checks": [
    "MAIN-SECURITY-0001",
    "MAIN-SECURITY-0002"
   ],
   "runtime_codes": [
    "MAIN-SECURITY-0003",
    "MAIN-SECURITY-0004",
    "MAIN-SECURITY-0005",
    "MAIN-SECURITY-0010",
    "MAIN-SECURITY-0011",
    "MAIN-SECURITY-0012"
   ],
   "features": [
    {
     "name": "Event type",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#contract"
    },
    {
     "name": "security.emit and security.on",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#emit"
    },
    {
     "name": "Producers: sign-in refusals, access denials and refused egress (others pending)",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#producers"
    },
    {
     "name": "Brute force, credential stuffing and access probing detectors",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#detectors"
    },
    {
     "name": "Log, OTLP and events sinks by region",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#sinks"
    },
    {
     "name": "Bounded export buffer",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#buffer"
    },
    {
     "name": "mainsdk logs --security and the security line in mainsdk info",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#cli"
    },
    {
     "name": "Overhead budget",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#budget"
    },
    {
     "name": "Sigma rules for the detections",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "security-events#sigma"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/security.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/security.md"
  },
  {
   "id": "incident",
   "name": "Incident controls",
   "category": "security",
   "summary": "Switch off one action, route, interface, driver or agent, put the app in read-only mode, or sign everyone out, in seconds and without a deploy.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "incident-controls",
   "calls": [
    "incident.status()",
    "incident.set(req, change, { reason, expires? })"
   ],
   "yaml": "incident:\n  kill: []\n  read-only: false\n  read-only-allow: [/api/auth/*, /api/webhooks/*]\n  sessions-after: null\n  tenants: {}\n  expires: {}\n",
   "example": "import { incident } from \"mainsdk\";\n\n// what is switched off right now, from memory\nconst state = incident.status();\nconsole.log(state.controls.length);\n\n// from an admin screen: needs incident.manage and a fresh passkey sign-in\nawait incident.set(req, { kill: [\"action:orders.refund\"] }, { reason: \"refund abuse, ticket 812\", expires: \"4h\" });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (pushes through the config driver)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "A control only tightens what main.yaml allows; a push cannot open anything",
    "Reaches every instance through the config store and survives restarts",
    "Audited with who, why and when; cleared by deleting it",
    "Never reads the store in a request; fail static"
   ],
   "checks": [
    "MAIN-INCIDENT-0004",
    "MAIN-INCIDENT-0007",
    "MAIN-CONFIG-0020"
   ],
   "runtime_codes": [
    "MAIN-INCIDENT-0001",
    "MAIN-INCIDENT-0002",
    "MAIN-INCIDENT-0003",
    "MAIN-INCIDENT-0005",
    "MAIN-INCIDENT-0006",
    "MAIN-CONFIG-0001",
    "MAIN-CONFIG-0002",
    "MAIN-CONFIG-0003"
   ],
   "features": [
    {
     "name": "incident section in main.yaml and the tighter-only push rule",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#schema"
    },
    {
     "name": "Kill switch for actions",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#kill"
    },
    {
     "name": "Read-only mode",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#read-only"
    },
    {
     "name": "Sign-out cut-off for the app or one tenant",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#sessions-after"
    },
    {
     "name": "Config driver write",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#driver-write"
    },
    {
     "name": "mainsdk incident on",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#cli"
    },
    {
     "name": "incident.set for an admin screen",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#in-app"
    },
    {
     "name": "Controls in the status header and mainsdk info",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#visibility"
    },
    {
     "name": "Expiry and the 24 hour warning",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "incident-controls#expiry"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/incident.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/incident.md"
  },
  {
   "id": "canary",
   "name": "Exfiltration guard and canaries",
   "category": "security",
   "summary": "Stop a stolen secret or a bulk copy of personal data leaving your app, and plant records and keys that raise an alarm when an intruder touches them.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "exfiltration-guard",
   "calls": [
    "canary.plant(model, { count? })",
    "canary.plantAll()",
    "canary.status()"
   ],
   "yaml": "exfiltration:\n  mode: enforce\n  outbound:\n    secrets: block\n    receivers: { api.partner-crm.com: [personal] }\n  responses:\n    pan: block\n    bulk: { personal: 500 }\n    actor-window: { window: 1h, personal: 5000 }\ncanaries:\n  records: { users: 2 }\n  keys: true\n  on-touch: alert\n",
   "example": "import { canary } from \"mainsdk/canary\";\n\n// plant two canary users in each region's canary tenant, once; counts only, never ids\nawait canary.plant(\"users\", { count: 2 });\nconst { records } = await canary.status();   // { users: 4 }\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (an Aho-Corasick matcher on the standard library)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "A resolved secret leaves only for its own provider, in any common encoding, and the value is never logged",
    "A response carrying a secret or a card number becomes a 500 with no value in it",
    "A canary touch raises one critical security event and the reader is not told",
    "Never in the request path beyond one in-memory scan; fail static"
   ],
   "checks": [
    "MAIN-EXFIL-0006",
    "MAIN-CANARY-0003"
   ],
   "runtime_codes": [
    "MAIN-EXFIL-0001",
    "MAIN-EXFIL-0002",
    "MAIN-EXFIL-0003",
    "MAIN-EXFIL-0004",
    "MAIN-EXFIL-0005",
    "MAIN-CANARY-0001",
    "MAIN-CANARY-0002"
   ],
   "features": [
    {
     "name": "exfiltration and canaries sections in main.yaml with their check rules",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "exfiltration-guard#schema"
    },
    {
     "name": "Known-value matcher with chunked scanning",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "exfiltration-guard#matcher"
    },
    {
     "name": "Outbound secret and labelled-value check",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "exfiltration-guard#outbound"
    },
    {
     "name": "Response check",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "exfiltration-guard#responses"
    },
    {
     "name": "mainsdk canary plant and status",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "exfiltration-guard#canary-plant"
    },
    {
     "name": "canary.touched event",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "exfiltration-guard#canary-touch"
    },
    {
     "name": "Canary API key and secret",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "exfiltration-guard#canary-keys"
    },
    {
     "name": "observe",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "exfiltration-guard#modes"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/canary.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/canary.md"
  },
  {
   "id": "alerts",
   "name": "Alerts",
   "category": "reliability",
   "summary": "Route SLO burn, downed dependencies, security detections and your own alerts to email, Slack, PagerDuty, Opsgenie or a webhook, once.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "alerts",
   "calls": [
    "alerts.raise({ key, severity, title, attrs?, runbook? })",
    "alerts.resolve(key)",
    "alerts.ack(id)",
    "alerts.get(id)",
    "alerts.list({ state?, source?, cursor?, limit? })"
   ],
   "yaml": "alerts:\n  min: warning\n  recipients:\n    oncall:\n      - { driver: pagerduty, key: secret:PAGERDUTY_ROUTING_KEY }\n      - { driver: slack, webhook: secret:SLACK_ALERTS_WEBHOOK }\n  routes:\n    - { match: { severity: critical }, to: [oncall], escalate: [{ after: 15m, to: [oncall] }] }\n    - { to: [oncall] }\n  quiet: { from: \"22:00\", to: \"08:00\", tz: Asia/Kolkata }\n",
   "example": "import { alerts } from \"mainsdk\";\n\n// one message per open key, however often it is raised; routes in main.yaml pick who hears it\nawait alerts.raise({ key: \"import-failed\", severity: \"warning\", title: \"Nightly import failed\", runbook: \"https://acme.dev/runbooks/import\" });\n\nawait alerts.resolve(\"import-failed\");\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (sends through notify and email drivers)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "One message per open alert, however many instances raise it",
    "Critical alerts go at once; others wait out quiet hours as a digest",
    "Never on the request path; sends run as jobs with retries",
    "No personal data in an alert; provider dedupe keys are keyed hashes"
   ],
   "checks": [
    "MAIN-ALERT-0001",
    "MAIN-ALERT-0002",
    "MAIN-ALERT-0003"
   ],
   "runtime_codes": [
    "MAIN-ALERT-0010",
    "MAIN-ALERT-0011",
    "MAIN-ALERT-0012",
    "MAIN-ALERT-0013",
    "MAIN-ALERT-0014",
    "MAIN-ALERT-0015"
   ],
   "features": [
    {
     "name": "alerts section in main.yaml with its checks",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#contract"
    },
    {
     "name": "raise, resolve, ack and list with dedupe by key",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#core"
    },
    {
     "name": "Routes, grouping, repeat and storm limits",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#routing"
    },
    {
     "name": "Quiet hours and escalation",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#quiet-escalation"
    },
    {
     "name": "Slack, PagerDuty, Opsgenie, webhook and email recipients",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#drivers"
    },
    {
     "name": "Delivery as jobs off the request path",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#delivery"
    },
    {
     "name": "Health, security, resources, jobs and error sources (the backup source is planned)",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#sources"
    },
    {
     "name": "Routes in mainsdk info and mainsdk alerts test",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "alerts#cli"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/alerts.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/alerts.md"
  },
  {
   "id": "signals",
   "name": "Signals",
   "category": "reliability",
   "summary": "Say what the app noticed (an attack, CPU pressure, a downed dependency, a rotated key, an anomaly) as one typed, signed, short-lived signal that the app, its other instances and its neighbour apps hear.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "signals",
   "calls": [
    "signals.emit(name, { severity, subject?, data?, ttl?, scope? })",
    "signals.on(pattern, handler, { min?, from? })",
    "signals.list({ name?, min?, live?, cursor?, limit? })",
    "new Signals(opts)",
    "parseSignals(section)",
    "signalsIssues(section)",
    "signalEnvelopeSchema(const: the JSON Schema of a signal)"
   ],
   "yaml": "signals:\n  keep: 1000\n  max-per-minute: 600\n  key: secret:MAINSDK_SIGNAL_KEY\n",
   "example": "import { signals } from \"mainsdk\";\n\n// one signal per condition, however often it is emitted while live\nawait signals.emit(\"app.import_failed\", { severity: \"high\", subject: \"import:nightly\", data: { rows: 3 } });\n\nsignals.on(\"pressure.*\", (s) => console.log(s.name, s.severity, s.trust));\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (diagnostics channels, events.broadcast and the host bus)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Signed, versioned envelopes with severity, TTL and dedupe",
    "No personal data in a signal; data is redacted like a log record",
    "Never on the request path"
   ],
   "checks": [
    "MAIN-SIGNAL-0006",
    "MAIN-SIGNAL-0007"
   ],
   "runtime_codes": [
    "MAIN-SIGNAL-0001",
    "MAIN-SIGNAL-0002",
    "MAIN-SIGNAL-0003",
    "MAIN-SIGNAL-0004",
    "MAIN-SIGNAL-0005",
    "MAIN-SIGNAL-0008"
   ],
   "features": [
    {
     "name": "The mainsdk-signal@1 envelope and the built-in catalogue",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "signals#envelope"
    },
    {
     "name": "emit, on and list with dedupe and TTL",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "signals#bus"
    },
    {
     "name": "App-scope signals across instances",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "signals#app-scope"
    },
    {
     "name": "Built-in emitters from resources and health",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "signals#emitters"
    },
    {
     "name": "Emitters for security events",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "signals#emitters-security"
    },
    {
     "name": "mainsdk signals",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "signals#cli"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/signals.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/signals.md"
  },
  {
   "id": "playbooks",
   "name": "Playbooks",
   "category": "reliability",
   "summary": "Declare in main.yaml what the app does when a signal arrives, with dry-run, approval, an audit trail and automatic rollback; every step only tightens and expires.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "playbooks",
   "calls": [
    "playbooks.list()",
    "playbooks.plan(rule, { signals? })",
    "playbooks.run(rule, { signals?, reason? })",
    "playbooks.get(runId)",
    "playbooks.revert(runId, { reason })",
    "posture.get()",
    "posture(object: the reaction posture)",
    "new Playbooks(opts)",
    "parseReaction(main, ctx)",
    "reactionIssues(main, ctx)"
   ],
   "yaml": "playbooks:\n  use: [attack, pressure, key-rotation]\n  approve-timeout: 15m                 # how long an approve step waits\n  presets:\n    attack: { limits: [sign-in], posture: high, notify: oncall }\n    pressure: { flags: [recommendations] }\n  rules:\n    report-flood:                      # a rule of your own\n      when: { signal: anomaly.detected, min: medium }\n      run: approve                     # auto | approve | dry-run\n      do:\n        - limits.tighten: { policy: report-export, to: 10/hour }\nposture:\n  levels:\n    high: [alerts.raise]               # steps added to the level below\nguard:\n  max-active: 5                        # runs active at once, 1 to 20\n  max-per-hour: 20\n  max-hops: 2\n  quarantine: 1h                       # how long a blocked source stays held\n  limit-floor: 10%\n  tenant-quorum: 3\n  block:\n    max-ranges: 256                    # ranges a playbook may add in total\n    never: [10.0.0.0/8]                # ranges never blocked\n",
   "example": "import { playbooks } from \"mainsdk\";\n\n// what would attack.contain change right now? nothing is applied\nconst plan = await playbooks.plan(\"attack.contain\");\nconsole.log(plan.why, plan.steps.map((s) => s.step));\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Every step tightens or reduces work and expires within 24 hours",
    "Approval by default; steps that touch people always need a person",
    "Rolled back when the condition clears or things get worse",
    "One guard for every source, with a never-a-reaction list"
   ],
   "checks": [
    "MAIN-PLAYBOOK-0001",
    "MAIN-PLAYBOOK-0002",
    "MAIN-PLAYBOOK-0010",
    "MAIN-GUARD-0008"
   ],
   "runtime_codes": [
    "MAIN-PLAYBOOK-0004",
    "MAIN-PLAYBOOK-0005",
    "MAIN-PLAYBOOK-0006",
    "MAIN-PLAYBOOK-0007",
    "MAIN-PLAYBOOK-0008",
    "MAIN-PLAYBOOK-0009",
    "MAIN-GUARD-0003",
    "MAIN-GUARD-0004",
    "MAIN-GUARD-0006",
    "MAIN-GUARD-0007",
    "MAIN-GUARD-0001",
    "MAIN-GUARD-0002",
    "MAIN-GUARD-0005",
    "MAIN-PLAYBOOK-0003"
   ],
   "features": [
    {
     "name": "Rules and matching",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "playbooks#match"
    },
    {
     "name": "The executor with dry-run, approve and auto",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "playbooks#executor"
    },
    {
     "name": "The reaction overlay",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "playbooks#overlay"
    },
    {
     "name": "Approval on the agents queue",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "playbooks#approval"
    },
    {
     "name": "Automatic rollback",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "playbooks#rollback"
    },
    {
     "name": "Attack, pressure and key-rotation presets",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "playbooks#presets"
    },
    {
     "name": "App actions marked reaction: true as steps, with undo as the rollback",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "playbooks#action-step"
    },
    {
     "name": "Blast radius",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "reaction-safety#never"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/playbooks.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/playbooks.md"
  },
  {
   "id": "log",
   "name": "Logs, audit and telemetry",
   "category": "observability",
   "summary": "Log with redaction built in, keep a tamper-evident audit log and export telemetry to any OpenTelemetry backend.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.2",
   "spec": "log-telemetry",
   "calls": [
    "log.info()",
    "log.error()",
    "log.audit()",
    "log.audit.verify()",
    "new MainError(code, { fix })",
    "problem(err)",
    "log.debug(msg, attrs?)",
    "log.warn(msg, attrs?)",
    "log.verifyAudit({ from? })",
    "verifyChain(records)",
    "currentTrace()",
    "newTraceId()",
    "withTrace(id, fn)",
    "fileSink(path)",
    "log.records",
    "log.auditRecords",
    "readAuditFile(path)",
    "instance(object: id)",
    "log.addRedactor(fn)",
    "log.addSecret(value)",
    "log.auditFor(tenant)",
    "log.reload(opts)",
    "log.holdsSecret(text)",
    "log.redactData(attrs)",
    "log.redactString(text)",
    "log.secretValues()",
    "log.errors.list(query?: { since?, release?, cursor?, limit? })",
    "log.errors.get(fingerprint)",
    "auditEvent(input)",
    "parseErrorReportConfig(logging)",
    "errorConfigIssues(logging, ctx)",
    "reportErrors(opts?: { endpoint?, sample? })"
   ],
   "yaml": "logging: { audit: true, redact: [personal, sensitive, child, phi] }\ntelemetry:\n  to: [terminal, otel]\n  otel: { endpoint: https://otel.pinecart.dev:4318, headers: { authorization: secret:OTEL_TOKEN } }\n  sample: 0.2\nlogging:\n  errors:\n    driver: otlp            # otlp | sentry | local\n    release: auto           # the build hash, else MAINSDK_RELEASE, else the package version\n    process: true           # capture uncaught errors, flush, exit 1\n    browser: false          # serve POST /_mainsdk/errors for reportErrors()\n    ignore: [AbortError]\n    alert: { new: warning, spike: { over: 50, per: 5m, severity: critical } }\n",
   "example": "import { log, MainError } from \"mainsdk\";\n\nlog.info(\"order shipped\", { orderId: order.id });\nthrow new MainError(\"MAIN-DATA-0007\", { fix: \"Query with db.orders.for(user)\" });\n",
   "drivers": [
    {
     "id": "terminal",
     "name": "Terminal",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "otel",
     "name": "Any OpenTelemetry backend",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/opentelemetry.html"
    },
    {
     "id": "mainsdk",
     "name": "MainSDK Cloud",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "sentry",
     "name": "Sentry (error events)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/sentry.html"
    }
   ],
   "guarantees": [
    "Personal data and secrets redacted in process before any write",
    "Append-only, hash-chained audit log in OCSF, never sampled",
    "Export never blocks a request",
    "Nothing sent to MainSDK Cloud without login and opt-in"
   ],
   "checks": [
    "MAIN-REPORT-0005",
    "MAIN-LOG-0020"
   ],
   "runtime_codes": [
    "MAIN-LOG-0003",
    "MAIN-LOG-0005",
    "MAIN-LOG-0010",
    "MAIN-REPORT-0001",
    "MAIN-REPORT-0002",
    "MAIN-REPORT-0003",
    "MAIN-REPORT-0004",
    "MAIN-REPORT-0006",
    "MAIN-REPORT-0007"
   ],
   "features": [
    {
     "name": "Logs on the OpenTelemetry data model",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "log-telemetry#log-api"
    },
    {
     "name": "Local event store for 7 days",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "log-telemetry#local-store"
    },
    {
     "name": "In-process redaction",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "log-telemetry#redactor"
    },
    {
     "name": "Hash-chained audit log in OCSF",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "log-telemetry#audit-log"
    },
    {
     "name": "telemetry@1 field contract",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "log-telemetry#telemetry-contract"
    },
    {
     "name": "OTLP export",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "log-telemetry#otlp-export"
    },
    {
     "name": "Opt-in sending to MainSDK Cloud",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "log-telemetry#opt-in"
    },
    {
     "name": "MainError with problem details",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "errors#main-error"
    },
    {
     "name": "Unhandled errors and crashes captured with a stable fingerprint",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "error-reporting#fingerprint"
    },
    {
     "name": "Alerts on new and spiking error groups",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "error-reporting#groups-alerts"
    },
    {
     "name": "Sentry driver with no SDK",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "error-reporting#sentry"
    },
    {
     "name": "Opt-in browser error reports",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "error-reporting#browser"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/log.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/log.md"
  },
  {
   "id": "observe",
   "name": "Observe the SDK",
   "category": "observability",
   "summary": "Subscribe to every decision and side effect the SDK makes on node:diagnostics_channel, with a bounded queue, redacted payloads and no cost when nobody listens.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "observe",
   "calls": [
    "observe(pattern, fn, opts?)",
    "observe.channels()",
    "observe.schema(name)",
    "observe.flush()",
    "observe.stats()",
    "observe.otel(api?)",
    "registerChannel(def)",
    "removeChannel(name, version)",
    "detailScope(opts, fn)",
    "tracedDriver(driver, { publisher, interface, ops })",
    "app.observe(pattern, fn, opts?)"
   ],
   "yaml": "# Observers are code, not configuration. Debug output is an environment variable:\n#   MAINSDK_DEBUG=observe                         print every event, with detail fields (dev only)\n#   MAINSDK_DEBUG=mainsdk:access.*,mainsdk:network.*   only matching channels\nschema: 1\n",
   "example": "import { observe } from \"mainsdk\";\n\nconst stop = observe(\"mainsdk:network.*\", (e) => {\n  console.log(e.channel, e.data.host, e.data.decision);\n});\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (core",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "An observer that throws or stalls never changes a request",
    "Payloads are redacted and frozen before any observer sees them",
    "A channel with no subscriber costs one property read"
   ],
   "checks": [],
   "runtime_codes": [
    "MAIN-OBS-0001",
    "MAIN-OBS-0002",
    "MAIN-OBS-0003",
    "MAIN-OBS-0004",
    "MAIN-OBS-0005"
   ],
   "features": [
    {
     "name": "Diagnostics channels with a versioned registry",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "observe#registry"
    },
    {
     "name": "Bounded observer queues with drop reports",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "observe#core"
    },
    {
     "name": "Spans for fetch",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "observe#tracing"
    },
    {
     "name": "Test kit app.events",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "observe#testing-events"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/observe.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/observe.md"
  },
  {
   "id": "analytics",
   "name": "Product analytics",
   "category": "observability",
   "summary": "Track product events, metrics and usage, private by default, with nothing recorded before consent where it is required.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "analytics",
   "calls": [
    "analytics.track(event, props?)",
    "analytics.page(path, props?)",
    "analytics.counter(name).add(n?, attrs?)",
    "analytics.gauge(name).set(value, attrs?)",
    "analytics.histogram(name).record(value, attrs?)",
    "analytics.query(q)"
   ],
   "yaml": "analytics:\n  to: [builtin]\n  without-consent: drop\n  buffer: 10000\n  posthog: { host: eu.i.posthog.com, key: \"secret:POSTHOG_KEY\", regions: [eu] }\n  min-group: 10\n  events:\n    checkout.started:\n      purpose: analytics\n      props: { plan: \"enum(free, pro)\", items: int }\n  metrics:\n    checkout.duration: { type: histogram, unit: ms, attrs: [plan] }\n",
   "example": "import { analytics } from \"mainsdk\";\n\nanalytics.track(\"checkout.started\", { plan: \"pro\", items: 3 });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (on db)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "posthog",
     "name": "PostHog",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/posthog.html"
    },
    {
     "id": "otel",
     "name": "Any OpenTelemetry backend",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/opentelemetry.html"
    },
    {
     "id": "plausible",
     "name": "Plausible",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/plausible.html"
    }
   ],
   "guarantees": [
    "Events are recorded only with consent for their purpose",
    "Only declared events and typed props are accepted",
    "Children's audiences are aggregate-only",
    "Small groups are hidden in query results"
   ],
   "checks": [
    "MAIN-ANALYTICS-0010",
    "MAIN-ANALYTICS-0011",
    "MAIN-ANALYTICS-0012",
    "MAIN-ANALYTICS-0013"
   ],
   "runtime_codes": [
    "MAIN-ANALYTICS-0001",
    "MAIN-ANALYTICS-0002",
    "MAIN-ANALYTICS-0003",
    "MAIN-ANALYTICS-0004",
    "MAIN-ANALYTICS-0005",
    "MAIN-ANALYTICS-0006",
    "MAIN-ANALYTICS-0007",
    "MAIN-ANALYTICS-0008",
    "MAIN-ANALYTICS-0009"
   ],
   "features": [
    {
     "name": "analytics@1 contract",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#contract"
    },
    {
     "name": "Track and page with typed props",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#track"
    },
    {
     "name": "Consent and per-app keyed ids",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#consent-ids"
    },
    {
     "name": "Children aggregate-only",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#children"
    },
    {
     "name": "Built-in store in each tenant's region",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#builtin-store"
    },
    {
     "name": "Counters",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#metrics"
    },
    {
     "name": "Queries: counts, active users, retention and funnels",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#query"
    },
    {
     "name": "PostHog and OpenTelemetry drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#drivers"
    },
    {
     "name": "Checks for undeclared events and identifying props",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#checks"
    },
    {
     "name": "Consent decided on the device",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "expo-adapter#on-device-policy"
    },
    {
     "name": "Active users by country, region and version",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "analytics#query"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/analytics.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/analytics.md"
  },
  {
   "id": "norms",
   "name": "Behaviour norms",
   "category": "observability",
   "summary": "Learn what normal looks like for the app (traffic, errors, latency, CPU, loop delay, memory) by hour of week, deterministically and with no model, and emit an anomaly signal when it leaves the range.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "norms",
   "calls": [
    "norms.get(metric, { at? })",
    "norms.list({ cursor?, limit? })",
    "norms.delete(metric, { reason })",
    "new Norms(opts)",
    "parseNorms(section?)",
    "normsIssues(section)"
   ],
   "yaml": "norms:\n  season: hour-of-week\n  tz: Asia/Kolkata\n  learn: 7d\n  min-samples: 20                      # observations a bucket needs before it speaks\n  persist: 5m                          # how often the model is stored, 1s to 24h\n",
   "example": "import { norms } from \"mainsdk\";\n\nconst n = await norms.get(\"requests\");\nconsole.log(n.bucket, n.p50, n.p99, n.learning);\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "The same input gives the same model and the same signals",
    "No dependency, no model and no service",
    "Holds numbers only, never a person"
   ],
   "checks": [
    "MAIN-NORMS-0001"
   ],
   "runtime_codes": [
    "MAIN-NORMS-0002",
    "MAIN-NORMS-0003",
    "MAIN-NORMS-0004"
   ],
   "features": [
    {
     "name": "EWMA and decayed histogram model",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "norms#model"
    },
    {
     "name": "Seasonal buckets",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "norms#season"
    },
    {
     "name": "Auth failures, access denials and refused egress as inputs",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "norms#inputs-security"
    },
    {
     "name": "Anomaly signals",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "norms#anomaly"
    },
    {
     "name": "Model persisted in the runtime store",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "norms#persist"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/norms.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/norms.md"
  },
  {
   "id": "i18n",
   "name": "Languages and formats",
   "category": "platform",
   "summary": "Translate messages and format money, dates and numbers for each locale, with right-to-left and Indian scripts.",
   "status": "preview",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "i18n",
   "calls": [
    "t(key, values)",
    "i18n.locale()",
    "i18n.negotiate(req, { user })",
    "i18n.with(locale, fn)",
    "i18n.format.number(n, opts)",
    "i18n.format.money(amount, currency)",
    "i18n.format.date(d, opts)",
    "i18n.format.list(items, opts)",
    "i18n.format.relative(d)",
    "i18n.dir(locale)",
    "i18n.drafts.list({ locale })",
    "i18n.drafts.approve(id, { text })",
    "i18n.drafts.reject(id, { reason })",
    "new Catalog(cfg, messages?)",
    "new Drafts(o)",
    "FORMATS(const)",
    "I18N_DRIVERS(const)",
    "new I18n(o)",
    "NEGOTIATE_SOURCES(const)",
    "PSEUDO_LOCALES(const)",
    "checkMessages(o)",
    "compileMessage(src)",
    "createI18n(o)",
    "crowdinSource(o)",
    "detectSyntax(src)",
    "exportBundle(format, b, opts?)",
    "extractKeys(source, file)",
    "fileCache(file)",
    "fileDraftStore(dir)",
    "formatMessage(m, ctx)",
    "formatsFor(locale)",
    "icuToMf2(src)",
    "importBundle(format, text)",
    "lastGood(source, cache, { log })",
    "localeDir(locale)",
    "localeSupported(locale)",
    "lokaliseSource(o)",
    "matchLocale(tag, locales)",
    "memoryDraftStore()",
    "negotiate(o, req, ctx?)",
    "parseAcceptLanguage(header)",
    "parseI18nConfig(raw)",
    "parseIcu(src)",
    "parseMf2(src)",
    "printMf2(m)",
    "pseudoMessage(src, locale)",
    "pseudoMessages(messages, locale)",
    "readGlossary(file)",
    "readLocales(dir)",
    "splitPlural(src)",
    "tolgeeSource(o)",
    "variablesOf(m)",
    "writeLocale(dir, locale, { messages })",
    "i18n.handle(req, next, { user? })"
   ],
   "yaml": "i18n:\n  driver: files\n  locales: [en-IN, hi-IN, ta-IN]\n",
   "example": "import { t, i18n } from \"mainsdk\";\n\nconst title = t(\"orders.count\", { count: orders.length });\nconst total = i18n.format.money(125000, \"INR\");   // ₹1,25,000 in en-IN\n",
   "drivers": [
    {
     "id": "files",
     "name": "Local YAML files",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "tolgee",
     "name": "Tolgee",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/tolgee.html"
    },
    {
     "id": "crowdin",
     "name": "Crowdin",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/crowdin.html"
    },
    {
     "id": "lokalise",
     "name": "Lokalise",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/lokalise.html"
    },
    {
     "id": "ai-draft",
     "name": "AI draft with human review",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "A missing translation fails the check, not the user",
    "Formats from Unicode CLDR through the runtime's Intl, with no locale data shipped",
    "Locale from the user's choice first, never guessed from IP alone",
    "A production page never shows a message key",
    "AI translations reach users only after a person approves them"
   ],
   "checks": [
    "MAIN-I18N-0001",
    "MAIN-I18N-0002",
    "MAIN-I18N-0003",
    "MAIN-I18N-0006",
    "MAIN-I18N-0008"
   ],
   "runtime_codes": [
    "MAIN-I18N-0004",
    "MAIN-I18N-0005",
    "MAIN-I18N-0007",
    "MAIN-I18N-0009",
    "MAIN-I18N-0010",
    "MAIN-I18N-0011",
    "MAIN-I18N-0012"
   ],
   "features": [
    {
     "name": "Messages in MessageFormat 2 with ICU fallback",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "i18n#mf2"
    },
    {
     "name": "Numbers, money, dates, lists and relative times from Intl",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "i18n#formats"
    },
    {
     "name": "Locale negotiated per request",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "i18n#negotiate"
    },
    {
     "name": "Fallback chains",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "i18n#fallback"
    },
    {
     "name": "Missing keys and placeholder mismatches in check",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "i18n#checks"
    },
    {
     "name": "Pseudo-localization",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "i18n#pseudo"
    },
    {
     "name": "AI drafts with a glossary and a person's review",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "i18n#ai-draft"
    },
    {
     "name": "XLIFF, xcstrings, Android strings and ICU JSON import and export",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "i18n#interchange-drivers"
    },
    {
     "name": "Android and iOS string files from the same locales",
     "phase": 3,
     "milestone": "1.1",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/i18n.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/i18n.md"
  },
  {
   "id": "geo",
   "name": "Location",
   "category": "platform",
   "summary": "Get a coarse location hint from the declared platform's headers and geocode addresses through network, never for access decisions.",
   "status": "building",
   "stability": "beta",
   "phase": 2,
   "milestone": "0.7",
   "spec": "geo",
   "calls": [
    "geo.of(req)",
    "geo.geocode(address, { country? })",
    "createGeo(settings)",
    "parseGeo(raw)",
    "geoHosts(config)",
    "geoEdge(config)"
   ],
   "yaml": "geo:\n  driver: platform-headers\n  edge: cloudfront\n  key: secret:MAPBOX_TOKEN\n  cache-ttl: 30d\n  timeout: 2s\n",
   "example": "import { geo } from \"mainsdk/geo\";\n\nconst hint = geo.of(request);                      // a hint for defaults, never for access\nconst place = await geo.geocode(address);          // a Place, or null when the provider is down\n",
   "drivers": [
    {
     "id": "platform-headers",
     "name": "Platform headers",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "mapbox",
     "name": "Mapbox",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/mapbox.html"
    },
    {
     "id": "google-maps",
     "name": "Google Maps Platform",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/google-maps.html"
    },
    {
     "id": "local",
     "name": "Local table in the testing kit",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Location is a hint, never used for access",
    "The hint never holds an IP address and reads only the declared platform's headers",
    "An address is personal data, sent only to a host that serves the tenant's region and cached per tenant under a digest",
    "A geocoding provider that is down gives null and the app serves on"
   ],
   "checks": [
    "MAIN-GEO-0002",
    "MAIN-GEO-0005"
   ],
   "runtime_codes": [
    "MAIN-GEO-0001",
    "MAIN-GEO-0003",
    "MAIN-GEO-0004",
    "MAIN-GEO-0006"
   ],
   "features": [
    {
     "name": "geo@1 contract",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "geo#contract"
    },
    {
     "name": "Request location hint from the declared platform",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "geo#hint"
    },
    {
     "name": "Guard that keeps a hint out of policy",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "geo#guard"
    },
    {
     "name": "Geocoding through network",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "geo#geocode"
    },
    {
     "name": "Platform headers",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "geo#drivers"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/geo.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/geo.md"
  },
  {
   "id": "updates",
   "name": "App updates and version blocking",
   "category": "platform",
   "summary": "Tell users an update is ready, block old versions on the server as well as in the app, and roll releases out by percentage and market.",
   "status": "building",
   "stability": "beta",
   "phase": 2,
   "milestone": "0.7",
   "spec": "updates",
   "calls": [
    "updates.minVersion()",
    "updates.check({ version, channel?, user?, market? })",
    "updates.channel({ user?, market?, channel? })",
    "updates.build()",
    "updates.announce(build)",
    "createUpdates(deps)",
    "parseUpdates(raw)",
    "updatesIssues(raw)",
    "versionHeaders(version, platform?)",
    "refusal(res)",
    "applyUpdate(status, driver)",
    "watchBuild({ build, onUpdate, banner? })",
    "expoUpdates(module)",
    "googlePlay(module)",
    "appStore({ url, open })",
    "tauri(module)"
   ],
   "yaml": "updates:\n  driver: local\n  min-version: 2.4.0\n  build: 2026.10.27-1\n  exempt: [/api/me/export]\n  channels:\n    stable: { version: 2.6.0, rollout: 50, markets: { IN: 100, EU: 10 } }\n    beta: { version: 2.7.0-beta.1, rollout: 100 }\n  api:\n    deprecated:\n      - { path: /api/v1, since: 2026-10-01, sunset: 2027-06-30, successor: /api/v2 }\n",
   "example": "import { app } from \"mainsdk\";\n\nconst updates = app().updates;\nconst status = updates.check({ version: \"2.3.9\", user: user.id });\nif (status.required) showUpdateScreen(status.minVersion);\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local fake for tests and development",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "expo-updates",
     "name": "Expo Updates",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/expo.html"
    },
    {
     "id": "google-play",
     "name": "Google Play",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/google-play.html"
    },
    {
     "id": "app-store",
     "name": "App Store",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/app-store.html"
    },
    {
     "id": "tauri",
     "name": "Tauri updater",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/tauri.html"
    }
   ],
   "guarantees": [
    "Blocking enforced by the server as well as the app",
    "The minimum version can only rise through a push, and every change is audited",
    "Blocked users can still sign out and export their data",
    "Open web tabs hear about a new build over server-sent events",
    "The same user always gets the same answer in a staged rollout",
    "API clients get Deprecation and Sunset headers before a route goes away",
    "Reads memory only, so a config or store outage never changes an answer"
   ],
   "checks": [
    "MAIN-UPDATE-0003",
    "MAIN-UPDATE-0004"
   ],
   "runtime_codes": [
    "MAIN-UPDATE-0001",
    "MAIN-UPDATE-0002"
   ],
   "features": [
    {
     "name": "updates@1 contract and the updates main.yaml keys",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "updates#contract"
    },
    {
     "name": "Minimum version that only tightens",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "updates#min-version"
    },
    {
     "name": "Server gate with 426 MAIN-UPDATE-0001",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "updates#server-gate"
    },
    {
     "name": "Web version skew banner over SSE",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "updates#web-skew"
    },
    {
     "name": "Stable and beta channels with a staged rollout by percentage and market",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "updates#channels"
    },
    {
     "name": "Deprecation and Sunset headers for API clients",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "updates#api-sunset"
    },
    {
     "name": "Update checks",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "updates#checks"
    },
    {
     "name": "Size budgets and no network in the request path",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "updates#budgets"
    },
    {
     "name": "Store drivers for Expo Updates",
     "phase": 3,
     "milestone": "1.1",
     "ticket": "updates#drivers"
    },
    {
     "name": "Update metadata signed and delivered through TUF",
     "phase": 2,
     "milestone": "0.7",
     "ticket": null
    },
    {
     "name": "Automatic halt of a staged rollout on a crash or error spike",
     "phase": 2,
     "milestone": "0.7",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/updates.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/updates.md"
  },
  {
   "id": "feedback",
   "name": "Feedback and support",
   "category": "messaging",
   "summary": "Let users report a problem or open a ticket, with the trace attached and screenshots redacted.",
   "status": "building",
   "stability": "beta",
   "phase": 2,
   "milestone": "0.6",
   "spec": "debug-loop",
   "calls": [
    "feedback.open({ category? })",
    "feedback.onCrash({ code? })",
    "feedback.onFailedAction({ code })",
    "support.ticket({ subject, body })"
   ],
   "yaml": "feedback:\n  tracker: linear\n  prompt: { onCrash: true, onFailedAction: true }\n",
   "example": "import { createFeedback, StepRecorder } from \"mainsdk/feedback\";\n\nconst { feedback } = createFeedback({ version, device, steps: new StepRecorder(), trace, ask, upload, send });\nawait feedback.open({ category: \"checkout\" });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "intercom",
     "name": "Intercom",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/intercom.html"
    },
    {
     "id": "zendesk",
     "name": "Zendesk",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/zendesk.html"
    },
    {
     "id": "plain",
     "name": "Plain",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/plain.html"
    }
   ],
   "guarantees": [
    "Screenshots redacted",
    "Support access audited"
   ],
   "checks": [],
   "runtime_codes": [],
   "features": [
    {
     "name": "Report capture with trace, version and steps",
     "phase": 2,
     "milestone": "0.6",
     "ticket": "debug-loop#feedback-capture"
    },
    {
     "name": "Support tickets",
     "phase": 2,
     "milestone": "0.6",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/feedback.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/feedback.md"
  },
  {
   "id": "neighbours",
   "name": "Neighbour apps",
   "category": "platform",
   "summary": "Let MainSDK apps on one machine hear each other's signals on user-only sockets and, between declared peers, ask for status, a raised posture or shed load, and nothing else.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "neighbours",
   "calls": [
    "neighbours.ask(peer, verb, { to?, for?, reason? })",
    "new Neighbours(opts)",
    "parseNeighbours(section)",
    "neighboursIssues(section, ctx)",
    "rotateHostKey(appsDir)"
   ],
   "yaml": "neighbours:\n  host: on\n  listen: [attack.*, pressure.*]\n  share: [attack.suspected, pressure.high]\n  asks: { accept: [status, posture.raise, load.shed] }\n  priority: 5                          # 1 to 9\n  ports: 47400-47499                   # loopback range for the host bus\n",
   "example": "import { neighbours } from \"mainsdk\";\n\nconst s = await neighbours.ask(\"prj_01JD8X\", \"status\");\nconsole.log(s.resources.state, s.posture);\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (user-only sockets and the app link)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Nothing leaves the machine",
    "Only three asks between declared peers; never code, data or a lowered guard",
    "Apps never list the machine's apps"
   ],
   "checks": [
    "MAIN-NEIGHBOUR-0001",
    "MAIN-NEIGHBOUR-0005"
   ],
   "runtime_codes": [
    "MAIN-NEIGHBOUR-0002",
    "MAIN-NEIGHBOUR-0003",
    "MAIN-NEIGHBOUR-0004",
    "MAIN-NEIGHBOUR-0006",
    "MAIN-NEIGHBOUR-0007"
   ],
   "features": [
    {
     "name": "Per-host key",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "neighbours#host-key"
    },
    {
     "name": "Host signal bus",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "neighbours#host-bus"
    },
    {
     "name": "status, posture.raise and load.shed asks",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "neighbours#asks"
    },
    {
     "name": "Priority between neighbours",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "neighbours#priority"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/neighbours.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/neighbours.md"
  },
  {
   "id": "apps",
   "name": "App registry and link",
   "category": "platform",
   "summary": "See every MainSDK app on a machine from one local folder, and let two apps that declare each other prove who they are and call each other's exposed actions.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "app-registry",
   "calls": [
    "apps.peer(id)",
    "apps.call(id, action, { input?, signal? })",
    "apps.provide(actions)",
    "new AppRegistry(opts)",
    "parseApps(section)",
    "readRegistry(dir)",
    "readSeal(doc)"
   ],
   "yaml": "apps:\n  link: on\n  identity: \"ed25519:MCowBQYDK2VwAyEA\"\n  peers: [prj_01JD8X]\n  expose: [orders.status]\n",
   "example": "import { apps } from \"mainsdk\";\n\nconst order = await apps.call(\"prj_01JD8X\", \"orders.status\", { input: { id: 1 } });\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (a local folder and a user-only socket)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Nothing leaves the machine",
    "A record holds apps and their seals, never a person",
    "Apps see only the peers they declare, and a call needs both sides to agree"
   ],
   "checks": [
    "MAIN-APPS-0004",
    "MAIN-APPS-0005"
   ],
   "runtime_codes": [
    "MAIN-APPS-0001",
    "MAIN-APPS-0002",
    "MAIN-APPS-0003",
    "MAIN-APPS-0006",
    "MAIN-APPS-0007",
    "MAIN-APPS-0008",
    "MAIN-APPS-0009",
    "MAIN-APPS-0010",
    "MAIN-APPS-0011"
   ],
   "features": [
    {
     "name": "Per-machine registry",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "app-registry#write"
    },
    {
     "name": "mainsdk apps",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "app-registry#apps-cmd"
    },
    {
     "name": "Peer calls on a mutual challenge",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "app-registry#link"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/apps.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/apps.md"
  },
  {
   "id": "ops",
   "name": "Ops server",
   "category": "ai",
   "summary": "Let a coding agent ask a running app how it is, what it has seen and what a playbook would do, through nine MCP tools on a user-only socket, with every change waiting for a person.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.4",
   "spec": "ops-mcp",
   "calls": [
    "new OpsServer(opts)",
    "parseOps(section, env)",
    "opsIssues(section, ctx)",
    "OPS_TOOL_NAMES(const: the nine tool names)"
   ],
   "yaml": "ops:\n  mcp: { write: approve }\n  agent: ops\n",
   "example": "// An agent starts the stdio server; the tools are listed by the MCP client.\n// npx mainsdk ops mcp\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (a user-only socket and a token file)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Nothing leaves the machine",
    "No tool calls an app action, changes config or reads app data",
    "A run or revert always waits for a person; an agent never approves"
   ],
   "checks": [
    "MAIN-OPS-0001"
   ],
   "runtime_codes": [
    "MAIN-OPS-0002",
    "MAIN-OPS-0004",
    "MAIN-OPS-0005",
    "MAIN-OPS-0006"
   ],
   "features": [
    {
     "name": "Read tools",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ops-mcp#tools-read"
    },
    {
     "name": "Approval-only run and revert",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ops-mcp#tools-write"
    },
    {
     "name": "mainsdk ops mcp",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "ops-mcp#stdio"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/ops.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/ops.md"
  },
  {
   "id": "device",
   "name": "Connected devices",
   "category": "platform",
   "summary": "Optional plumbing for any fleet of devices or agents your app manages - enrolment, device identity and attestation, signed telemetry, policy sync, approved commands and health - with nothing loaded until you declare it.",
   "status": "building",
   "stability": "beta",
   "phase": 4,
   "milestone": "1.3",
   "spec": "device-identity",
   "calls": [
    "device.enrolments.create({ group, uses, expiresIn })",
    "device.enrolments.list({ cursor })",
    "device.enrolments.revoke(id, { reason })",
    "device.get(id)",
    "device.list({ group, state, cursor })",
    "device.update(id, { name, group, labels })",
    "device.revoke(id, { reason })",
    "device.handler()",
    "device.telemetry.on(stream, handler, { schema })",
    "device.summary.get({ group })",
    "device.policy.define(schema, { tighten })",
    "device.policy.publish(content, { target, reason, rollout })",
    "device.policy.get(version, { target })",
    "device.policy.list({ target, cursor })",
    "device.policy.rollback({ target, to, reason })",
    "device.policy.tighten({ target, path, value, expiresIn, reason })",
    "device.commands.define(type, { input, risk, approve, undo })",
    "device.commands.create(type, { to, input, reason })",
    "device.commands.get(id)",
    "device.commands.list({ device, state, cursor })",
    "device.commands.cancel(id, { reason })",
    "device.commands.revert(id, { reason })",
    "device.mcp.tools()",
    "deviceClient({ url, keys, dir })",
    "fileKeyStore(dir, { alg })",
    "memoryKeyStore({ alg })",
    "webCryptoKeyStore({ db })",
    "softwareEvidence({ software })",
    "simulateDevices(app, { count })",
    "deviceConformance({ driver })"
   ],
   "yaml": "device:\n  driver: local                 # local | taps (from @mainsdk/device-taps)\n  path: /mainsdk/device\n  authority: secret:MAINSDK_DEVICE_KEY\n  enrol: { proofs: [token, code] }\n  attest: { require: none }\n  telemetry: { streams: { app.status: {} }, max-batch: 1MB }\n  heartbeat: { every: 60s }\n  policy:\n    rollout: { stages: [\"5%\", \"25%\", \"100%\"], hold: 1h }\n    tighten: { report.interval: higher }\n  commands:\n    ttl: 15m\n    max-targets: 100\n    reaction-max: 50\n",
   "example": "import { device } from \"mainsdk/device\";             // not exported from \"mainsdk\"\nimport { deviceClient, fileKeyStore } from \"mainsdk/device/client\";\n\nconst { token } = await device.enrolments.create({ group: \"lab\", uses: 10 });\ndevice.telemetry.on(\"app.status\", async (records) => { await store(records); });\n\n// on the device\nconst client = deviceClient({ url: \"https://app.example.com/mainsdk/device\", keys: fileKeyStore(\"/var/lib/acme\") });\nawait client.enrol({ token });\nawait client.start();\nclient.send(\"app.status\", { ok: true });\n",
   "drivers": [
    {
     "id": "local",
     "name": "Local (did:key, Ed25519 or P-256, no dependency)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "taps",
     "name": "TAPS credentials, attestation and delegation",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/taps.html"
    }
   ],
   "guarantees": [
    "Off until declared in main.yaml and imported; no route, table, timer or cost otherwise, and never re-exported from mainsdk",
    "Device keys are made on the device and never leave it",
    "Every device message is signed, fresh and bound to one tenant",
    "Telemetry is delivered once and in order, with redaction on the device and on the server",
    "A device keeps its last good policy when the app is unreachable and never accepts an older one",
    "Commands run only registered handlers, expire, and need people's approval when risky; an agent never approves",
    "No detection rules, endpoint event schemas or code delivery, now or later (ADR-0076)"
   ],
   "checks": [
    "MAIN-DEVICE-0001",
    "MAIN-DEVICE-0002",
    "MAIN-DEVICE-0003",
    "MAIN-DEVICE-0004",
    "MAIN-DEVICE-0016",
    "MAIN-DEVICE-0022",
    "MAIN-DEVICE-0038",
    "MAIN-DEVICE-0039",
    "MAIN-DEVICE-0050",
    "MAIN-DEVICE-0066",
    "MAIN-DEVICE-0067",
    "MAIN-DEVICE-0071",
    "MAIN-DEVICE-0082",
    "MAIN-DEVICE-0092"
   ],
   "runtime_codes": [
    "MAIN-DEVICE-0005",
    "MAIN-DEVICE-0006",
    "MAIN-DEVICE-0007",
    "MAIN-DEVICE-0008",
    "MAIN-DEVICE-0009",
    "MAIN-DEVICE-0010",
    "MAIN-DEVICE-0011",
    "MAIN-DEVICE-0012",
    "MAIN-DEVICE-0013",
    "MAIN-DEVICE-0014",
    "MAIN-DEVICE-0015",
    "MAIN-DEVICE-0017",
    "MAIN-DEVICE-0018",
    "MAIN-DEVICE-0020",
    "MAIN-DEVICE-0021",
    "MAIN-DEVICE-0023",
    "MAIN-DEVICE-0024",
    "MAIN-DEVICE-0030",
    "MAIN-DEVICE-0031",
    "MAIN-DEVICE-0032",
    "MAIN-DEVICE-0033",
    "MAIN-DEVICE-0034",
    "MAIN-DEVICE-0035",
    "MAIN-DEVICE-0036",
    "MAIN-DEVICE-0037",
    "MAIN-DEVICE-0045",
    "MAIN-DEVICE-0046",
    "MAIN-DEVICE-0047",
    "MAIN-DEVICE-0048",
    "MAIN-DEVICE-0049",
    "MAIN-DEVICE-0051",
    "MAIN-DEVICE-0060",
    "MAIN-DEVICE-0061",
    "MAIN-DEVICE-0062",
    "MAIN-DEVICE-0063",
    "MAIN-DEVICE-0064",
    "MAIN-DEVICE-0065",
    "MAIN-DEVICE-0068",
    "MAIN-DEVICE-0069",
    "MAIN-DEVICE-0070",
    "MAIN-DEVICE-0080",
    "MAIN-DEVICE-0081",
    "MAIN-DEVICE-0083",
    "MAIN-DEVICE-0090",
    "MAIN-DEVICE-0091",
    "MAIN-DEVICE-0093"
   ],
   "features": [
    {
     "name": "Opt-in subpaths and half-declaration checks",
     "phase": 4,
     "milestone": "1.3",
     "ticket": "device-identity#opt-in"
    },
    {
     "name": "Enrolment by token, approved code and workload OIDC",
     "phase": 4,
     "milestone": "1.3",
     "ticket": "device-identity#enrol-token"
    },
    {
     "name": "Renewal, rotation and revocation",
     "phase": 4,
     "milestone": "1.3",
     "ticket": "device-identity#renew-rotate"
    },
    {
     "name": "Device client and key stores",
     "phase": 4,
     "milestone": "1.3",
     "ticket": "device-identity#client"
    },
    {
     "name": "Simulated devices and the conformance suite",
     "phase": 4,
     "milestone": "1.3",
     "ticket": "device-identity#sim"
    },
    {
     "name": "Attestation levels, nonces and appraisal policy",
     "phase": 4,
     "milestone": "1.3",
     "ticket": "device-attest#policy"
    },
    {
     "name": "Signed ordered telemetry with an offline buffer",
     "phase": 4,
     "milestone": "1.3",
     "ticket": "device-telemetry#ingest"
    },
    {
     "name": "Heartbeat",
     "phase": 4,
     "milestone": "1.3",
     "ticket": "device-health#states"
    },
    {
     "name": "Policy bundles with staged rollout and rollback",
     "phase": 4,
     "milestone": "1.4",
     "ticket": "device-policy#rollout"
    },
    {
     "name": "Tighten-only policy overlays as playbook steps",
     "phase": 4,
     "milestone": "1.4",
     "ticket": "device-policy#overlays"
    },
    {
     "name": "Signed commands, approvals, receipts and undo",
     "phase": 4,
     "milestone": "1.4",
     "ticket": "device-commands#deliver"
    },
    {
     "name": "TAPS credentials, status lists, evidence and delegation",
     "phase": 4,
     "milestone": "1.4",
     "ticket": "device-taps#credentials"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/device.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/device.md"
  },
  {
   "id": "config",
   "name": "Runtime settings",
   "category": "dx",
   "summary": "Change typed runtime settings without a deploy, with every change audited.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.2",
   "spec": "config-secrets",
   "calls": [
    "config.get(key)",
    "config.watch(key, fn)",
    "config.watch(prefix, fn)",
    "config.refresh()",
    "config.ready(opts)",
    "config.raw()",
    "redisConfig(o)",
    "filesConfig(o)",
    "new MemoryConfigStore(initial?)",
    "loadMain(dir, env?)",
    "parseMain(text, overlays?)",
    "parseYaml(src)",
    "schemaNode(schema, key)",
    "config.close()"
   ],
   "yaml": "config:\n  driver: redis                  # memory | files | redis\n  schema: ./config.schema.json\n  refresh: 30s                   # poll interval without a change stream\n  values: { checkout.currency: INR }\n  files: { dir: config/ }        # files driver\n  redis: { url: secret:REDIS_URL, prefix: \"mainsdk:prj_01JC8Q…\" }\n",
   "example": "import { config, log } from \"mainsdk\";\n\nconst maxItems = config.get(\"cart.maxItems\");     // typed from the schema; synchronous, from memory\nconst stop = config.watch(\"cart.maxItems\", (v) => log.info(\"changed\", { v }));\n",
   "drivers": [
    {
     "id": "files",
     "name": "Files",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "local",
     "name": "Local (in process)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "redis",
     "name": "Redis",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/redis.html"
    },
    {
     "id": "postgres",
     "name": "Postgres",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/postgresql.html"
    },
    {
     "id": "vercel-global-config",
     "name": "Vercel Global Config",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/vercel.html"
    },
    {
     "id": "aws-appconfig",
     "name": "AWS AppConfig",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/aws-appconfig.html"
    },
    {
     "id": "cloudflare-kv",
     "name": "Cloudflare KV",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/cloudflare-kv.html"
    }
   ],
   "guarantees": [
    "Typed against a schema; a bad value keeps the last good one",
    "Reads come from memory, so the app never waits on the store",
    "Changes reach every running instance without a deploy",
    "A push can only tighten what main.yaml allows",
    "Every applied or refused change is audited by key and hash, never by value"
   ],
   "checks": [
    "MAIN-CONFIG-0030",
    "MAIN-CONFIG-0031",
    "MAIN-CONFIG-0032",
    "MAIN-CONFIG-0033"
   ],
   "runtime_codes": [
    "MAIN-CONFIG-0004",
    "MAIN-CONFIG-0020",
    "MAIN-CONFIG-0021",
    "MAIN-CONFIG-0022",
    "MAIN-CONFIG-0023",
    "MAIN-CONFIG-0001",
    "MAIN-CONFIG-0002",
    "MAIN-CONFIG-0003",
    "MAIN-CONFIG-0005",
    "MAIN-CONFIG-0010"
   ],
   "features": [
    {
     "name": "Typed get and watch",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#config-api"
    },
    {
     "name": "File driver",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#config-drivers"
    },
    {
     "name": "Background refresh by change stream or poll",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#config-runtime"
    },
    {
     "name": "Last good snapshot kept on disk for a cold start",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#config-runtime"
    },
    {
     "name": "Tighten-only pushes of main.yaml values",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#config-tighten"
    },
    {
     "name": "Audit of every applied and refused change",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#config-audit"
    },
    {
     "name": "Memory driver and fs.watch on the files driver",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#config-memory-files"
    },
    {
     "name": "Redis driver on the built-in RESP client",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "config-secrets#config-redis"
    },
    {
     "name": "Deploy files for Vercel",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#writer"
    },
    {
     "name": "Environment diff for a reviewed promotion",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "deploy#diff"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/config.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/config.md"
  },
  {
   "id": "schema",
   "name": "Schema and parsers",
   "category": "dx",
   "summary": "Describe app data once with s, get a strict check, a type and a JSON Schema, and turn raw text into checked values without a model.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.2",
   "spec": "schema",
   "calls": [
    "s.object(shape)",
    "schema.parse(value)",
    "schema.safeParse(value)",
    "schema.toJSONSchema()",
    "parse.text(raw)",
    "parse.match(raw, list)",
    "parse.regex(raw, re, schema)",
    "parse.json(raw, schema)",
    "parse.csv(raw, { schema })",
    "parse.lines(raw)",
    "parse.number(raw, { locale })",
    "parse.date(raw)",
    "parse.first(...parsers)",
    "check(schema, value)",
    "checkAsync(schema, value)",
    "jsonSchemaOf(schema)",
    "parse(object: text, match, regex, json, csv, lines, number, date, first, exact, pattern)",
    "s(object: schema builders)",
    "isUnsafeRegex(source)",
    "parseDuration(v, key, { allowZero? })",
    "parseSize(v)"
   ],
   "yaml": null,
   "example": "import { s, type Infer } from \"mainsdk/schema\";\nimport { parse } from \"mainsdk/parse\";\n\nconst Invoice = s.object({\n  number: s.string().max(40),\n  total: s.number().min(0),\n  due: s.date(),\n});\ntype Invoice = Infer<typeof Invoice>;\n\nconst invoice = Invoice.parse(body);                        // MAIN-SCHEMA-0001 lists every issue, never the value\nconst total = parse.number(\"1.234,50\", { locale: \"de-DE\" }); // 1234.5\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Strict by default; an unknown key is refused, not passed on",
    "Issues carry a path, a code and a message, never the value",
    "No coercion unless you ask for it",
    "Parsers refuse oversized input and nested-quantifier patterns before any match runs",
    "Standard library only; runs in the app with no network call",
    "Free on every plan"
   ],
   "checks": [],
   "runtime_codes": [
    "MAIN-SCHEMA-0001",
    "MAIN-SCHEMA-0002",
    "MAIN-SCHEMA-0003",
    "MAIN-SCHEMA-0004"
   ],
   "features": [
    {
     "name": "Strict object",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#core"
    },
    {
     "name": "JSON Schema 2020-12 from every schema",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#json-schema"
    },
    {
     "name": "Standard Schema v1",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#standard"
    },
    {
     "name": "Opt-in coercion for forms and environment values",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#coerce"
    },
    {
     "name": "Text",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#parse-text"
    },
    {
     "name": "Regex parser with a size guard and named groups",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#parse-regex"
    },
    {
     "name": "Locale numbers and ISO 8601 dates",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#parse-locale"
    },
    {
     "name": "parse.first",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#parse-first"
    },
    {
     "name": "Schema issues in problem details and action input errors",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#problem-errors"
    },
    {
     "name": "MCP tool inputs as JSON Schema",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#mcp-schema"
    },
    {
     "name": "http.json replies checked against a schema",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "schema#http-schema"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/schema.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/schema.md"
  },
  {
   "id": "flags",
   "name": "Feature flags",
   "category": "dx",
   "summary": "Turn features on for some users, a market or a share of users, and change them while the app runs.",
   "status": "preview",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "flags",
   "calls": [
    "flags.enabled(name, ctx)",
    "flags.value(name, fallback, ctx)"
   ],
   "yaml": "flags:\n  driver: config\n",
   "example": "import { flags } from \"mainsdk\";\n\nif (flags.enabled(\"new-checkout\", { user: user.id })) {   // synchronous, from memory\n  return renderNewCheckout();\n}\n",
   "drivers": [
    {
     "id": "config",
     "name": "Config store",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/config.html"
    },
    {
     "id": "openfeature",
     "name": "Any OpenFeature provider",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/openfeature.html"
    },
    {
     "id": "posthog",
     "name": "PostHog",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/posthog.html"
    },
    {
     "id": "vercel-flags",
     "name": "Vercel Flags",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/vercel.html"
    }
   ],
   "guarantees": [
    "The same answer for the same user on every instance",
    "Reads memory only; no network call or disk read per check",
    "A change applies with no restart and is audited like any config change"
   ],
   "checks": [],
   "runtime_codes": [
    "MAIN-FLAGS-0001",
    "MAIN-FLAGS-0002",
    "MAIN-FLAGS-0003"
   ],
   "features": [
    {
     "name": "Enabled and value from the config store",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "flags#flags-eval"
    },
    {
     "name": "Stable percentage rollout by user",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "flags#flags-rollout"
    },
    {
     "name": "Live updates and refusal of invalid definitions",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "flags#flags-live"
    },
    {
     "name": "Target by plan",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "entitlements#limits-flags"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/flags.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/flags.md"
  },
  {
   "id": "api",
   "name": "Typed API and OpenAPI document",
   "category": "dx",
   "summary": "Turn the app's actions into REST routes, one OpenAPI document and a typed client, with idempotent writes and breaking changes caught before release.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "app-api",
   "calls": [
    "api.openapi(actions, opts?)",
    "api.routes(actions)",
    "api.handler(options)",
    "api.client(doc)",
    "api.diff(before, after)",
    "api.react(options)",
    "createClient({ baseUrl, auth, fetch?, retries? })",
    "page(rows, init)",
    "pageLimit(limit, max?)",
    "paginate(list, opts?)",
    "inbound(object: handle)",
    "inboundIssues(config, env, { today? })",
    "originAllowed(origin, origins)"
   ],
   "yaml": "api:\n  base: /api\n  actions: src/actions.ts\n  openapi:\n    serve: false\n    include: [\"*\"]\n    title: Acme API\n  snapshot: api/openapi.json\n  idempotency:\n    require: [destructive]\npagination:\n  key: PAGINATION_KEY                  # secret that signs list cursors\n",
   "example": "import { api } from \"mainsdk/api\";\nimport { refundOrder } from \"./actions/orders\";\n\nconst doc = api.openapi([refundOrder], { include: [\"orders.*\"] });   // OpenAPI 3.1\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in core module",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "The same actions always produce the same document",
    "A value that fails its output schema is never sent",
    "A breaking change in a shipped major version fails mainsdk api diff"
   ],
   "checks": [
    "MAIN-API-0010",
    "MAIN-API-0011",
    "MAIN-API-0012",
    "MAIN-API-0020"
   ],
   "runtime_codes": [
    "MAIN-API-0001",
    "MAIN-API-0002",
    "MAIN-API-0003",
    "MAIN-API-0004",
    "MAIN-API-0005",
    "MAIN-API-0006",
    "MAIN-API-0007",
    "MAIN-PAGE-0001",
    "MAIN-PAGE-0002"
   ],
   "features": [
    {
     "name": "Output schema checked before anything is sent",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#output"
    },
    {
     "name": "Versioned REST routes from the actions",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#routes"
    },
    {
     "name": "OpenAPI 3.1 document",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#openapi"
    },
    {
     "name": "Idempotency-Key for writes",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#idempotency"
    },
    {
     "name": "Typed client, one file, global fetch only",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#client"
    },
    {
     "name": "Paged and all-items helpers in the client",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#client-pages"
    },
    {
     "name": "Breaking-change diff against the snapshot",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#diff"
    },
    {
     "name": "One schema for actions, documents and the client",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#one-schema"
    },
    {
     "name": "Tenant and auth on every public route",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "app-api#tenant-auth"
    },
    {
     "name": "React hooks and TanStack Query options for the generated client",
     "phase": 2,
     "milestone": "0.7",
     "ticket": "client-react#generator"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/api.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/api.md"
  },
  {
   "id": "testing",
   "name": "Testing kit",
   "category": "dx",
   "summary": "Test against in-memory fakes of every interface, control time and inject provider outages.",
   "status": "building",
   "stability": "stable",
   "phase": 1,
   "milestone": "0.4",
   "spec": "testing-kit",
   "calls": [
    "createTestApp({ main })",
    "createSim({ apps, start, seed })",
    "sim.run(duration, { step })",
    "sim.inject.cpu(app, curve)",
    "sim.expect.signal(app, name)",
    "sim.report({ golden })",
    "app.fixtures(opts?)",
    "mailbox(app).toHaveSent()",
    "clock.advance()",
    "chaos.on()",
    "expect(app.pay).toHaveCharged()",
    "assertFakesCover(ids)",
    "runDriverKit(driver)",
    "policyTest()",
    "createApp(options)",
    "configure(app)",
    "app()",
    "fakeAiDriver()",
    "testCards()",
    "new App(opts?)"
   ],
   "yaml": null,
   "example": "import { createTestApp, expect, clock, chaos } from \"@mainsdk/testing\";\n\nconst app = createTestApp({ main: \"./main.yaml\" });   // every interface on its in-memory fake\nawait signUp(app, \"a@pinecart.dev\");\nmailbox(app).toHaveSent({ to: \"a@pinecart.dev\", template: \"welcome\" });\n\nclock.advance(\"25h\");                                 // expiring links, TTLs, retries\nchaos.on(\"email\", { outage: true });\n",
   "drivers": [
    {
     "id": "local",
     "name": "The local drivers with recording and chaos",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "A fake for every interface, generated from its contract"
   ],
   "checks": [],
   "runtime_codes": [],
   "features": [
    {
     "name": "In-memory fakes for every interface",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "testing-kit#fakes-core"
    },
    {
     "name": "Assertions and a controllable clock",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "testing-kit#assertions-time"
    },
    {
     "name": "Policy test helpers",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "testing-kit#policy-helpers"
    },
    {
     "name": "Chaos drills",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "testing-kit#chaos-drills"
    },
    {
     "name": "Conformance suite",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "testing-kit#conformance-suite"
    },
    {
     "name": "Parity vectors for the other language SDKs",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "testing-kit#parity-vectors"
    },
    {
     "name": "Driver kit",
     "phase": 1,
     "milestone": "0.4",
     "ticket": "testing-kit#driver-kit"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/testing.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/testing.md"
  },
  {
   "id": "adapters",
   "name": "Framework adapters",
   "category": "dx",
   "summary": "One import wires MainSDK into Next.js, Hono or Express, with a request context, a trace, ordered hooks and problem-detail errors.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.2",
   "spec": "adapters",
   "calls": [
    "mainsdk.proxy()",
    "withMainSDK(handler)",
    "mainsdkAction(handler)",
    "mainsdk()",
    "mainsdk.errors()",
    "worker({ fetch })",
    "worker(handler)",
    "withPrisma(client)",
    "withDrizzle(db)"
   ],
   "yaml": null,
   "example": "import { Hono } from \"hono\";\nimport { mainsdk } from \"mainsdk/hono\";\n\nconst app = new Hono();\napp.use(\"*\", mainsdk());          // trace, context, hooks, problem details, sign-in routes\n",
   "drivers": [
    {
     "id": "next",
     "name": "Next.js App Router",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/nextdotjs.html"
    },
    {
     "id": "hono",
     "name": "Hono",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/hono.html"
    },
    {
     "id": "express",
     "name": "Express",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/express.html"
    },
    {
     "id": "cloudflare",
     "name": "Cloudflare Workers",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/cloudflare.html"
    }
   ],
   "guarantees": [
    "Every request carries a trace, tenant, user, agent and region through AsyncLocalStorage",
    "Hooks run in a fixed order and a failing hook never changes the answer",
    "Errors become RFC 9457 problems and unknown errors never leak detail",
    "Health probes, shutdown draining and inbound protection stay in front of your handlers",
    "No peer dependency on the framework and no network call to MainSDK Cloud",
    "A market overlay answers config.get for the market of the request, with no redeploy",
    "A Prisma or Drizzle query is decided by the access policy and audited before it runs"
   ],
   "checks": [
    "MAIN-DATA-0001"
   ],
   "runtime_codes": [],
   "features": [
    {
     "name": "Request context and trace propagation",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#request-context"
    },
    {
     "name": "Ordered request hooks",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#lifecycle"
    },
    {
     "name": "Next.js adapter",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#next-adapter"
    },
    {
     "name": "Hono adapter",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#hono-adapter"
    },
    {
     "name": "Express adapter",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#express-adapter"
    },
    {
     "name": "Node worker adapter",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#worker-adapter"
    },
    {
     "name": "Schedule tick route for serverless deploys",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#schedule-tick"
    },
    {
     "name": "Cloudflare Workers entry",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#cloudflare-adapter"
    },
    {
     "name": "Prisma and Drizzle integration",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#orm-integration"
    },
    {
     "name": "Adapter overhead budgets",
     "phase": 1,
     "milestone": "0.2",
     "ticket": "adapters#adapter-budgets"
    },
    {
     "name": "Driver marketplace with conformance",
     "phase": 3,
     "milestone": "1.2",
     "ticket": null
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/adapters.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/adapters.md"
  },
  {
   "id": "deps",
   "name": "Dependency checks",
   "category": "security",
   "summary": "Catch made-up, brand-new and look-alike packages, known advisories, denied licences and unreviewed install scripts in your dependencies when you run mainsdk check, offline-friendly and never in the app.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.5",
   "spec": "dependency-check",
   "calls": [
    "mainsdk check"
   ],
   "yaml": "deps:\n  min-age: 7d                    # a direct dependency first published more recently is reported\n  licences: { deny: [AGPL-3.0-only, SSPL-1.0], allow: [], unknown: warn }\n  advisories:\n    fail-on: high                # critical | high | moderate | low | none\n    ignore:\n      - { id: GHSA-xxxx-xxxx-xxxx, until: 2027-01-31, reason: \"Not reachable: only used in tests\" }\n  scripts: { allow: [esbuild, sharp] }\n  offline: info                  # info | fail\n",
   "example": "# mainsdk check runs the deps gate with the others; the CLI asks the registry and OSV, then keeps the answers for 24 hours\nnpx mainsdk check --json\n",
   "drivers": [
    {
     "id": "osv",
     "name": "OSV.dev advisories",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/osv.html"
    },
    {
     "id": "npm-registry",
     "name": "npm registry and private registries from .npmrc",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/npm.html"
    }
   ],
   "guarantees": [
    "Runs only in the CLI; nothing loads in the app or its request path",
    "Reads pnpm, npm, yarn (classic and Berry) and bun lockfiles into one component list",
    "A registry token goes only to the host it is configured for, never to another registry or to OSV",
    "Offline is an info finding, not a failure, and answers are cached for 24 hours"
   ],
   "checks": [
    "MAIN-DEPS-0001",
    "MAIN-DEPS-0002",
    "MAIN-DEPS-0003",
    "MAIN-DEPS-0004",
    "MAIN-DEPS-0005",
    "MAIN-DEPS-0006",
    "MAIN-DEPS-0007",
    "MAIN-DEPS-0008"
   ],
   "runtime_codes": [],
   "features": [
    {
     "name": "Lockfile readers for pnpm, npm, yarn and bun",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "dependency-check#lockfiles"
    },
    {
     "name": "Unknown, brand-new and look-alike direct dependencies",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "dependency-check#registry-rules"
    },
    {
     "name": "Advisories from OSV with severity, fixed version and path",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "dependency-check#advisories"
    },
    {
     "name": "Licence policy over SPDX expressions",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "dependency-check#licences"
    },
    {
     "name": "Install scripts and the offline cache",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "dependency-check#scripts-offline"
    },
    {
     "name": "Licences in the seal's SBOM",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "dependency-check#seal"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/deps.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/deps.md"
  },
  {
   "id": "upgrade",
   "name": "Upgrades",
   "category": "dx",
   "summary": "Move every MainSDK package to a new release together with one command that applies the release's codemods and main.yaml moves and opens a pull request, and learn when a security release is out.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.5",
   "spec": "upgrade",
   "calls": [
    "mainsdk upgrade",
    "mainsdk upgrade --check"
   ],
   "yaml": null,
   "example": "npx mainsdk upgrade --to latest --plan    # what would change, and nothing written\nnpx mainsdk upgrade --to latest           # branch, bump, install, codemods, sync, check, tests, pull request\nnpx mainsdk upgrade --check               # exit 1 when a security release is not installed\n",
   "drivers": [
    {
     "id": "npm-registry",
     "name": "npm registry and private registries from .npmrc",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/npm.html"
    },
    {
     "id": "git",
     "name": "Git and the gh CLI",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/git.html"
    }
   ],
   "guarantees": [
    "Dry run by default for planning; applying needs a clean git tree and works on a new branch",
    "Every MainSDK package moves together to one exact version",
    "Codemods run on your own TypeScript compiler, are idempotent and never touch node_modules",
    "Steps a person has to do are listed with file and line",
    "The default branch never moves"
   ],
   "checks": [
    "MAIN-UPGRADE-0002",
    "MAIN-UPGRADE-0005",
    "MAIN-UPGRADE-0010"
   ],
   "runtime_codes": [
    "MAIN-UPGRADE-0001",
    "MAIN-UPGRADE-0003",
    "MAIN-UPGRADE-0004",
    "MAIN-UPGRADE-0006"
   ],
   "features": [
    {
     "name": "upgrade --plan from the registry and the release notes",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "upgrade#plan"
    },
    {
     "name": "Branch",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "upgrade#apply"
    },
    {
     "name": "Idempotent codemods and main.yaml key moves",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "upgrade#codemods"
    },
    {
     "name": "Sync",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "upgrade#verify-pr"
    },
    {
     "name": "upgrade --check for security releases",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "upgrade#security-check"
    },
    {
     "name": "Release gate for notes and codemod fixtures",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "upgrade#notes-gate"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/upgrade.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/upgrade.md"
  },
  {
   "id": "adopt",
   "name": "Adopt an existing app",
   "category": "dx",
   "summary": "Bring an existing Next.js or Node app, including one from Lovable, Bolt or v0, onto MainSDK one module and one pull request at a time, with its behaviour locked first and its coding agent driving the steps.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.5",
   "spec": "migration-skill",
   "calls": [
    "mainsdk adopt scan",
    "mainsdk adopt report",
    "mainsdk adopt lock",
    "mainsdk adopt next",
    "mainsdk adopt finish"
   ],
   "yaml": null,
   "example": "npx mainsdk adopt scan      # main.yaml as the app is, no code change\nnpx mainsdk adopt report    # the gaps against Main 27: secrets, access, data, the rest\nnpx mainsdk adopt lock      # record sign-in and checkout as locked flows\nnpx mainsdk adopt next      # one module, one branch, gated by check and the locked flows\nnpx mainsdk adopt finish    # Main 27 met, or exactly what remains\n",
   "drivers": [
    {
     "id": "builtin",
     "name": "Built in (reads files, runs git)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "The scan is deterministic and changes no code; it writes main.yaml only",
    "What the app already runs is kept and tagged, never replaced",
    "Nothing changes until the key flows are locked",
    "Each module is one branch that passes check and every locked flow; a failed step keeps its branch and leaves the default branch untouched",
    "Runs fully locally and sends nothing"
   ],
   "checks": [
    "MAIN-ADOPT-0002",
    "MAIN-ADOPT-0003",
    "MAIN-ADOPT-0004",
    "MAIN-ADOPT-0007"
   ],
   "runtime_codes": [
    "MAIN-ADOPT-0001",
    "MAIN-ADOPT-0006",
    "MAIN-ADOPT-0008",
    "MAIN-ADOPT-0009",
    "MAIN-ADOPT-0010",
    "MAIN-ADOPT-0012",
    "MAIN-ADOPT-0013"
   ],
   "features": [
    {
     "name": "Deterministic scan that keeps what you already run",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "migration-skill#scan"
    },
    {
     "name": "Gap report in plain language",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "migration-skill#gap-report"
    },
    {
     "name": "Locked flows before any change",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "migration-skill#lock-flows"
    },
    {
     "name": "One module per branch",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "migration-skill#module-step"
    },
    {
     "name": "Launch report",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "migration-skill#finish-report"
    },
    {
     "name": "Agent skill and the adopt MCP tools",
     "phase": 1,
     "milestone": "0.5",
     "ticket": "migration-skill#agent-skill"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/adopt.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/adopt.md"
  },
  {
   "id": "bots",
   "name": "Bot defence",
   "category": "security",
   "summary": "Score the sign-in, sign-up, reset and invite doors in process, challenge or refuse bots with a proof-of-work, and stop SMS and WhatsApp pumping before a message is paid for.",
   "status": "building",
   "stability": "beta",
   "phase": 1,
   "milestone": "0.3",
   "spec": "bot-defence",
   "calls": [
    "bots.formToken()",
    "bots.formFields()",
    "bots.check(req, { door? })",
    "bots.guardSend(phone)",
    "bots.challenge({ score? })"
   ],
   "yaml": "bots:\n  mode: enforce\n  doors:\n    sign-up: { challenge: 30, deny: 80, disposable-email: deny }\n    contact: { path: /api/contact, form: true }\n  challenge: { driver: pow }\n  pumping: { per-prefix: { max: 10, per: 1h }, countries: [IN] }\n  crawlers: [googlebot]\n  block: [198.51.100.0/24]\n",
   "example": "import { bots } from \"mainsdk/bots\";\n\n// a hidden token and a honeypot field for a contact form; the door is declared in main.yaml\nconst { html } = bots.formFields();\n\n// before you send a code yourself\nawait bots.guardSend(\"+91 98765 43210\");   // throws MAIN-BOT-0007 (429) when refused\n",
   "drivers": [
    {
     "id": "pow",
     "name": "Proof-of-work (built in, no third party)",
     "default": true,
     "integration": "https://mainsdk.dev/docs/integrations/pow.html"
    },
    {
     "id": "local",
     "name": "In-process counters (the default store; no third party)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    },
    {
     "id": "turnstile",
     "name": "Cloudflare Turnstile",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/turnstile.html"
    },
    {
     "id": "hcaptcha",
     "name": "hCaptcha",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/hcaptcha.html"
    }
   ],
   "guarantees": [
    "A request is scored from its headers, a form token and counters in process, with no cookie, no script and no fingerprint",
    "A refusal never says why; the reasons are in the log and the security event",
    "No raw address, email or phone number is stored, and each counter holds at most 10,000 keys",
    "A store outage never blocks a sign-in; the counters fall back to local"
   ],
   "checks": [
    "MAIN-BOT-0005"
   ],
   "runtime_codes": [
    "MAIN-BOT-0001",
    "MAIN-BOT-0002",
    "MAIN-BOT-0003",
    "MAIN-BOT-0004",
    "MAIN-BOT-0006",
    "MAIN-BOT-0007"
   ],
   "features": [
    {
     "name": "bots section in main.yaml with its check rule",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "bot-defence#schema"
    },
    {
     "name": "Door scoring and velocity counters",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "bot-defence#score"
    },
    {
     "name": "Form token and honeypot",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "bot-defence#forms"
    },
    {
     "name": "Proof-of-work challenge and browser client",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "bot-defence#pow"
    },
    {
     "name": "Message pumping guard",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "bot-defence#pumping"
    },
    {
     "name": "Crawler verification",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "bot-defence#crawlers"
    },
    {
     "name": "Turnstile and hCaptcha drivers",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "bot-defence#captcha-drivers"
    },
    {
     "name": "Pushed block list",
     "phase": 1,
     "milestone": "0.3",
     "ticket": "bot-defence#signals"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/bots.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/bots.md"
  },
  {
   "id": "connectors",
   "name": "Connectors to GRC, privacy, vulnerability and ticket tools",
   "category": "security",
   "summary": "Push assure evidence to your GRC tool, run privacy requests from Transcend or OneTrust, send SBOM and VEX to your vulnerability tool, and open one ticket per failure.",
   "status": "building",
   "stability": "beta",
   "phase": 3,
   "milestone": "1.2",
   "spec": "connectors",
   "calls": [
    "connectorFor({ kind, settings }, ports)",
    "connectorsFrom(config, ports)",
    "pushAssure(connectors, run, options)",
    "failuresOf(run, { owner, now })",
    "syncTickets(tickets, run, options)",
    "pushRelease(vulns, release, options)",
    "importFindings(vulns, options)",
    "processPrivacy(privacy, { steps, sign }, options)",
    "testConnectors(connectors, options)"
   ],
   "yaml": "connectors:\n  grc:     { driver: servicenow-irm, instance: https://acme.service-now.com, auth: secret:SN_TOKEN }\n  privacy: { driver: transcend, auth: secret:TRANSCEND_KEY }\n  vulns:   { driver: dependency-track, url: https://dt.acme.internal, auth: secret:DT_KEY }\n  tickets: { driver: github }\n",
   "example": "import { connectorsFrom, pushAssure } from \"mainsdk/connectors\";\n\nconst connectors = connectorsFrom(config.connectors, { http, secrets, log });\nawait pushAssure(connectors.grc, run);   // one control result per failure, evidence attached\n",
   "drivers": [
    {
     "id": "servicenow-irm",
     "name": "ServiceNow IRM",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/servicenow.html"
    },
    {
     "id": "archer",
     "name": "Archer",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/archer.html"
    },
    {
     "id": "onetrust-grc",
     "name": "OneTrust GRC",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/onetrust-grc.html"
    },
    {
     "id": "transcend",
     "name": "Transcend",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/transcend.html"
    },
    {
     "id": "onetrust-privacy",
     "name": "OneTrust privacy requests",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/onetrust-privacy.html"
    },
    {
     "id": "dependency-track",
     "name": "Dependency-Track",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/dependency-track.html"
    },
    {
     "id": "snyk",
     "name": "Snyk",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/snyk.html"
    },
    {
     "id": "wiz",
     "name": "Wiz",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/wiz.html"
    },
    {
     "id": "defectdojo",
     "name": "DefectDojo",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/defectdojo.html"
    },
    {
     "id": "github",
     "name": "GitHub Issues",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/github.html"
    },
    {
     "id": "jira",
     "name": "Jira",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/jira.html"
    },
    {
     "id": "local",
     "name": "Local mock (tests and mainsdk dev)",
     "default": false,
     "integration": "https://mainsdk.dev/docs/integrations/built-in.html"
    }
   ],
   "guarantees": [
    "Credentials come from secrets and a literal token fails the check",
    "Every connector call writes one audit line with no credential in it",
    "A connector is never in the request path",
    "A privacy request is complete only after every driver confirms"
   ],
   "checks": [
    "MAIN-CONNECTOR-0001"
   ],
   "runtime_codes": [
    "MAIN-CONNECTOR-0002"
   ],
   "features": [
    {
     "name": "connector@1 contract and conformance suite",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "connectors#connector-contract"
    },
    {
     "name": "GRC connectors",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "connectors#grc-connectors"
    },
    {
     "name": "Privacy-request connectors",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "connectors#privacy-connectors"
    },
    {
     "name": "Vulnerability-tool connectors",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "connectors#vuln-connectors"
    },
    {
     "name": "Ticket connectors",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "connectors#ticket-connectors"
    },
    {
     "name": "Secrets and audit for every call",
     "phase": 3,
     "milestone": "1.2",
     "ticket": "connectors#connector-auth"
    }
   ],
   "docs": "https://mainsdk.dev/docs/interfaces/connectors.html",
   "markdown": "https://mainsdk.dev/docs/interfaces/connectors.md"
  }
 ]
}
