# Forge Identity v1

Forge Identity is the optional app-scoped account and team service for
applications hosted by Forge Deploy. It lets an application recognize a
visitor without receiving that visitor's Forge control-plane session.

## Configuration

```ts
import { defineForge } from "@smolai/forge/config";

export default defineForge({
  version: 1,
  name: "team-dashboard",
  app: {
    assets: { directory: "dist", fallback: "index.html" },
    identity: {
      scopes: ["user:basic", "accounts:read", "teams:read"],
    },
  },
  routes: [{ pattern: "/*", to: "app.assets" }],
});
```

`user:basic` discloses an app-specific random user ID and the visitor's public
Forge profile. `accounts:read` adds the visitor's current accounts and roles.
`teams:read` adds only teams the visitor currently belongs to.

## Browser API

The versioned browser client is hosted by Forge:

```js
import { forge } from "/.forge/client.js";

const user = await forge.identity.currentUser();
if (!user) {
  forge.identity.login({ returnTo: location.pathname });
}

const accounts = await forge.identity.listAccounts();
const teams = await forge.identity.listTeams();
await forge.identity.logout();
```

For a complete sign-in and account control without adopting a UI framework,
mount Forge's web component:

```html
<script type="module">
  import { forge } from "/.forge/client.js";

  forge.identity.mountAccountControl("#account", {
    returnTo: location.pathname,
  });

  document.querySelector("#account").addEventListener(
    "forge-account-select",
    ({ detail }) => {
      // This is app-local context. It does not change Forge ownership or plans.
      console.log("Selected account", detail.account);
    },
  );
</script>

<div id="account"></div>
```

The same control is available declaratively as
`<forge-account-control return-to="/dashboard">`. It renders in Shadow DOM,
works without React, lists only the accounts and teams disclosed to this app,
and emits `forge-identity-change` when its session changes. Account selection
is deliberately an app event rather than an implicit platform mutation.

Dynamic Forge applications receive the same app-scoped principal through the
trusted wrapper:

```ts
export default {
  async fetch(_request, env) {
    const user = env.forge.identity.currentUser();
    if (!user) return new Response("Sign in", { status: 401 });
    const account = env.forge.identity.requireAccountRole(
      "acct_org_example",
      ["owner", "admin"],
    );
    return Response.json({ user, account });
  },
};
```

The visitor is embedded in Forge's trusted request environment. Native hosted
Workers receive it in the edge-signed, short-lived deployment context.
Connected Workers on an active custom domain receive the same API from a
Forge-published wrapper backed by a private, release-scoped service binding.
Repository code never sees or introspects the raw session cookie.

## Protocol and isolation

1. The trusted Sites edge or connected-Worker wrapper creates a PKCE verifier,
   state, and nonce in a five-minute
   host-only transaction cookie.
2. The browser authorizes on `forge.smol.ai`; the control-plane bearer token is
   used only on that origin.
3. Forge redirects to the exact
   `https://<site>/.forge/identity/callback` URI with a 60-second one-time code.
4. The trusted edge or wrapper exchanges the code through a private Forge
   service binding.
5. That platform layer stores the opaque app session in
   `__Host-forge-session` with `Secure`, `HttpOnly`, `SameSite=Lax`, and no
   `Domain`.

The platform layer removes both Forge Identity cookies before dispatching a
request to repository code. It also rejects any application response that
attempts to set a `__Host-forge-*` cookie. Connected code receives only the
frozen `env.forge.identity` facade; the private service binding and its
release-scoped props are removed from its environment.

For a connected Worker, every Identity call revalidates the exact HTTPS
hostname against an attached, observed-active custom-domain record with active
DNS and certificate evidence. The project, account authorization generation,
active production release and provider version, provider target and binding
generation, manifest digest, callback origin, and session grant must all agree.
Workers.dev previews, `*.sites.smol.ai` connected previews, path-only routes,
detached domains, stale releases, and cross-project or cross-host sessions fail
closed.

Sessions have a 12-hour hard lifetime and no refresh token in v1. Account
suspension, tier/entitlement changes, organization membership changes, and team
membership changes advance authorization generations. A session carrying stale
generation evidence fails closed on its next use.

Visitors can inspect connected hosted apps under Forge **Settings → Connected
Forge apps**. That surface shows the exact disclosed scope categories, active
session count, and last use. Ending sessions revokes every live session for that
visitor and app; it does not delete the stable app-specific identity, and the
visitor may authorize the app again later.

Hosted applications never receive Forge's control-plane cookie or bearer token,
the JWT signing secret, D1, Cloudflare credentials, or an unrestricted
organization directory. Product-specific permissions remain product-owned.

## Hosting constraints

- Identity is available only to deployments whose `forgeBuild.ts` explicitly
  requests it and whose owning account has `identity.enabled`.
- Native callback origins are derived from successful Forge deployment records
  on `*.sites.smol.ai`. Connected callback origins are derived only from exact,
  verified, active custom-domain and production-release records. Arbitrary
  redirect URLs are rejected.
- The shared preview domain is not a confidentiality boundary.
- Connected preview and Workers.dev hostnames never become Identity callback
  origins merely because a provider version exists.
- SSO, SCIM, custom branding, and payment-driven tier changes remain separate
  product capabilities.

Forge Identity remains provider-neutral. Forge owns pairwise app IDs, scopes,
consent, sessions, revocation, and the application-facing API. Cloudflare
Access, an enterprise OIDC provider, or another upstream login system may
authenticate a Forge user, but it is not exposed as the connected app's
identity contract and cannot mint Forge app sessions by itself.
