@flow-industries/id

中文文档
0.36.0发布于 2026/10/29MITTypeScriptES Module近 30 天下载 3742

Passkey-first identity for Flow applications. One passkey bound to id.flow.industries, usable across Flow apps with audience-bound JWTs and optional Tempo signing.

原始使用文档
文档目录15 节
  1. Develop
  2. Security headers
  3. AT Protocol identity
  4. Connected accounts
  5. Installation
  6. Documentation
  7. Sign-in and account pages
  8. Public profile overlays
  9. License
  10. Migration compatibility checks
  11. Versioned consumer contracts
  12. Server tracing
  13. Daily check-in study activities
  14. Bun SSR integration
  15. User identity in request traces

Flow ID

Passkey-first identity for Flow applications. One passkey bound to id.flow.industries, usable across Flow apps with audience-bound JWTs and optional Tempo signing.

Develop

cp .env.example .env
bun install
bun run dev

bun run dev is the only supported local application launcher. It starts the API, dialog, playground, and landing page on the first free block of four consecutive ports. Configure DATABASE_URL and BETTER_AUTH_SECRET in .env first.

The launcher validates locally stored JWT signing keys before starting Auth. If BETTER_AUTH_SECRET changed, only keys that can no longer be decrypted are rotated so local session creation keeps working after environment changes. It also refuses to start when migrations fail instead of serving against a partially migrated schema.

Security headers

@flow-industries/id/start exports securityHeaders() and withSecurityHeaders(response, options). Both generate CSP, HSTS, nosniff, Referrer-Policy and Permissions-Policy. The default CSP permits same-origin resources and denies framing; each app supplies the directives its resources and embeds need.

import { withSecurityHeaders } from "@flow-industries/id/start";

const secured = withSecurityHeaders(response, {
  csp: {
    "connect-src": ["'self'", "https://id.flow.industries"],
    "frame-src": ["https://id.flow.industries"],
  },
});

Directive overrides replace one directive; null omits it. headers overrides generated defaults, and reportOnly: true emits the generated CSP under Content-Security-Policy-Report-Only. The response wrapper fills missing headers while preserving existing route policies, cookies, status and body stream. Compose a route's full CSP in csp before wrapping: an existing CSP is retained as-is. Explicit Permissions-Policy overrides can retain features such as microphone delegation on voice-enabled surfaces.

AT Protocol identity

Consumers discover the signed-in user's current PDS through await flow.atproto.resolve({ freshness: "cached" }). Use "refresh" after an endpoint or authorization failure, and call flow.atproto.invalidate() after identity or migration events. Flow account switches, logout and the first-party linking helpers invalidate automatically. Invalidation forces the next request to refresh. Discovery uses the configured Auth host, cookie or Flow bearer session, and canonical user ID; it never provisions an account.

The result includes the DID, verified display handle (or null), current PDS origin, hosting ownership, lifecycle and authorization status. DID documents are authoritative for the endpoint. No OAuth tokens or private keys are returned. connected indicates an unexpired stored grant whose audience and issuer match public resource metadata; it does not prove that a remote grant has not since been revoked. Writes must obtain fresh AT OAuth authorization, and must not forward old credentials to a migrated host or replay mutations with unknown outcomes. AtprotoRequestError.code distinguishes sign-in, missing identity, pending or failed provisioning, account changes, unsupported DID methods, invalid identity and unavailable resolution.

ATPROTO_MANAGED_PDS_ORIGINS is an optional comma-separated list of public HTTPS PDS origins owned by Flow, used only to classify current hosting. ATPROTO_RESOLUTION_TTL_MS enables a server metadata cache keyed by DID and mapping version, bounded to five minutes; unset or invalid values disable caching. Authorization status is checked separately on each request. A cached authorization failure forces one fresh DID resolution. Public discovery works without OAuth credential configuration and reports reconnect_required.

Bluesky and other AT Protocol accounts can sign in to Flow ID or connect to an existing account. Flow user IDs remain stable; the provider DID identifies the connection. New accounts do not need an email, passkey, or wallet to enroll.

Set ATPROTO_PUBLIC_URL, ATPROTO_PRIVATE_KEY (an ES256 private JWK with a stable kid), and ATPROTO_STORAGE_KEY (a base64-encoded 32-byte encryption key) to enable the integration. Leave all three unset to disable it. Production uses HTTPS client metadata at /api/auth/atproto/client-metadata.json and public keys at /api/auth/atproto/jwks.json. Provider tokens and DPoP keys are encrypted in Postgres and stay on the server. Back up the storage key with the database; changing it requires re-encrypting stored credentials.

For local OAuth, set ATPROTO_PUBLIC_URL=http://127.0.0.1:8183 and launch ./dev.sh 8180. The hosted sign-in page uses http://localhost:8183 and the launcher sets the WebAuthn relying party to localhost. Use the printed URLs; existing passkeys remain bound to their original relying party.

Without an email provider configured, the local launcher also prints sign-in codes in the browser console. This requires development mode, the launcher's FLOW_DEV_OTP_CONSOLE=1 opt-in, and a loopback issuer; the API then binds only to 127.0.0.1. Production responses never include these codes.

AT Protocol requires an IP loopback callback URI. The development landing server redirects loopback GET requests to its fixed localhost origin before handling them, retaining the callback query so OAuth resumes with the same browser cookies and session storage as sign-in. The SDK still exchanges the code using the registered IP callback URI. If that port block is occupied, update ATPROTO_PUBLIC_URL to the selected landing port and restart. Production clients use HTTPS metadata and authenticate with the configured signing key.

Provider sign-in resumes the bound /authorize transaction. Flow installs a first-party session, then returns the existing one-use PKCE code to the app. Account linking returns to /account; no provider or Flow tokens travel in redirect URLs or window messages.

The account connection view reads saved identities and credentials locally. It does not refresh provider tokens or resolve handles on page load; reconnect performs a new provider authorization when access needs to be restored.

Connected accounts

Better Auth owns the shared account model. External identities are keyed by providerId and accountId, and link to the canonical Flow userId. For AT Protocol these values are atproto and the verified DID. Handles are display metadata, never an identity key. A Flow account can connect different providers without adding provider-specific identity tables. Provider IDs identify a trusted provider configuration: separate OIDC issuers must use separate IDs, even if both run the same identity-server software.

Provider credentials and temporary authorization state live in the encrypted, provider-scoped oauth_session and oauth_state stores. The AT SDK uses an adapter over those stores; its DPoP and discovery behavior stays within the AT integration. Provider tokens never become Flow session tokens.

Better Auth supports built-in social providers and its Generic OAuth plugin. Flow currently uses version 1.4.17, whose default OAuth enrollment requires an email and creates its own sessions. A new provider must supply a verified stable subject to Flow's shared account enrollment, then use Flow's audience-bound session issuance. Register usable sign-in providers explicitly; arbitrary account rows do not count as recovery methods. No new identity table is needed.

Automatic email-based account linking is disabled. Disconnect through Flow's account controls so credential cleanup and last-sign-in-method checks run in one transaction; the stock Better Auth unlink endpoint is blocked. Its account listing remains available and excludes credentials.

Migration 0040 preserves existing AT links and encrypted sessions, then removes the earlier AT-only tables. It invalidates in-flight authorization state, so unfinished attempts must restart. Deploy the complete revised Auth stack before enabling AT production configuration. After this migration, the intermediate AT-only builds from PRs #170–#172 are not rollback targets; use the pre-AT main build or a compatible forward fix. Existing pre-AT Flow user/session writes remain supported.

npm: @flow-industries/id

Installation

bun add @flow-industries/id viem

For Wagmi apps:

bun add @flow-industries/id wagmi viem @tanstack/react-query

Documentation

  • Integration guide — server setup, canonical sign-in, React, and account navigation.
  • Guest sessions — silent guest creation and preserving identity during signup.
  • Action sessions — activity timing and the inline action widget.

The Auth pages in the separate documentation repository are archived. Use the guides here for the current SDK integration.

Sign-in and account pages

The SDK starts sign-in through your app's first-party /flow/session handler and navigates to Flow ID's /authorize page. Completion returns to the saved app path; read the new session after navigation instead of continuing after await flow.login().

For a signed-in account that needs a passkey, call flow.connectPasskey({ returnTo: "/", requirePrf: true }). It uses the same first-party redirect, stays bound to the app’s current account, and creates its first passkey on Flow ID. An existing registered passkey is reused. Set requirePrf for encrypted storage so unsupported new credentials are refused before attachment. After returning, the SDK refreshes the connected credential; the app must still evaluate actual PRF with a new user gesture before opening an encrypted vault. The method never resolves in the document that started it.

ProfileButton opens the shared account dropdown, with saved accounts, account switching, add-account, management, and sign-out actions. flow.openAccount() opens /account in a normal new tab. Both menus let signed-in users choose Online, Idle, Busy, or Invisible. The selected option is the saved preference; the avatar indicator shows effective availability, including automatic idle and derived offline. Busy does not alter notifications, and Invisible appears offline to other users. A failed save keeps the last confirmed preference and shows an inline retry. The profile button uses its existing Flow presence controller; the account page uses the same controller with its issuer cookie session. Practice can include a currently occupied Game room alongside its opt-in subject. Room visibility is projected independently; the combined activity expires at the earlier claim/room deadline and the next refresh can retain practice without a stale room. The source app stays loaded and refreshes its session on refocus. Sign-in and own-account management no longer use an embedded dialog. The remaining dialog supports signing, public profiles, and widgets. SDK ConnectResponse remains a deprecated type alias for source compatibility; sign-in itself returns through the first-party callback. See the integration guide for server wiring and migration.

Public profile overlays

import { closeProfile, openProfile } from "@flow-industries/id";
import { isValidUsername } from "@flow-industries/id/usernames";

if (isValidUsername(username)) {
  openProfile(username, { onClose: () => resumeInput() });
}

closeProfile();

@flow-industries/id/usernames also exports USERNAME_REGEX and MAX_USERNAME_LENGTH. It has no browser or server dependencies.

OpenProfileOptions.onClose?: () => void fires once after the overlay stops accepting input, before its exit animation finishes. It covers the close button, backdrop, Escape, closeProfile(), and sign-in/sign-out handoffs. The latest successful openProfile call owns the callback, even when switching profiles while open; an omitted callback clears the previous one. Invalid usernames and SSR return false without replacing it. Closing an already closed overlay is a no-op. A callback may open another profile.

React callers pass the same option to useOpenProfile({ onClose: () => resumeInput() }) from @flow-industries/id/react; the callback is captured when its returned opener is called. The overlay belongs to the page and survives component unmounts.

Open from the document that owns the full viewport. Canvas hosts must release pointer lock before opening, then use onClose to restore their input state.

License

MIT

Migration compatibility checks

bun run db:check regenerates into a temporary directory and refuses schema drift or edits to previously committed SQL. CI compares against the PR base or previous main commit. bun run test:migrations exercises the committed chain, the supported previous schema, current projections, older writes after upgrade, and deliberate missing-migration failures. MIGRATION_TEST_DATABASE_URL=postgres://... bun scripts/postgres-acceptance.ts additionally creates a unique disposable database on a test PostgreSQL server, runs the real migration entrypoint, rejects a changed applied hash, and proves a failing final migration rolls back both its schema changes and ledger entry. The test deletes only its own generated database.

The initial compatibility baseline is source 1bda372b69e2f66a3dc0cbb244bcbe7cb858f493 (schema through 0035). The tests freeze representative older SQL reads/writes; they do not boot every historical application version. Advance this baseline deliberately when retiring support, and extend the fixtures when changing a persisted contract. Migration failures block the image build; production's PreSync job also refuses an applied ledger that is not an exact committed prefix.

Application rollback keeps the expanded schema. Pin the latest verified migration image via Mesh's migrate.image while restoring the previous compatible application image. Do not run down migrations or point an older migration image at a newer ledger: that is refused. A destructive contract migration needs an explicit support-boundary change after older writers and browser clients have drained.

Versioned consumer contracts

bun run contracts:generate produces the published @flow-industries/id/contracts/v1/reports.json and openapi.json artifacts from the same schemas used by the XP, action, presence and position handlers. bun run contracts:check rejects stale output and incompatible changes to the existing v1 request schema against CONTRACT_BASE_REF (default origin/main). Keep existing v1 fields compatible; a breaking boundary needs a new version and an overlap plan for deployed consumers.

Trusted service consumers can call createReportsApi(host, serviceToken) from @flow-industries/id. The returned function accepts the operation name and its typed request, validates both the outgoing request and successful response, and throws ReportRequestError for non-success responses. Use the XP reporting credential for action/XP operations and the room credential for presence/position; never ship either service credential to a browser.

bun run test:sdk-package builds, packs and installs the actual SDK artifact in a disposable consumer, checks public imports and declarations, and proves a removed contract export fails. Producer tests execute the typed client against real report handlers and validate minted guest/full session and JWT shapes. CI gates both container and package publication on these checks.

The current OpenAPI artifact describes the service report boundary. The remaining account, room management, study, settings, session and passkey HTTP operations still need complete request/response descriptions and generated client coverage before the full HTTP contract work is complete.

Server deployments may set FLOW_ID_API_URL to a trusted internal HTTP(S) origin for API and JWKS requests, or pass apiUrl to server session/verification helpers. issuerUrl remains the public JWT issuer and browser navigation origin. The transport URL must contain no credentials, path, query, or fragment.

Server tracing

Register a NodeTracerProvider with W3CTraceContextPropagator before serving requests, and set OTEL_EXPORTER_OTLP_ENDPOINT. The helpers from @flow-industries/id/start then continue W3C trace context across HTTP calls:

import { traceRequest, tracedFetch } from "@flow-industries/id/start";

const fetch = (request: Request) => traceRequest(request, () =>
  tracedFetch("https://service.example/api/status"),
);

traceRequest starts a SERVER span from the inbound headers, excluding GET/HEAD /health probes. tracedFetch starts a CLIENT span and injects its traceparent and tracestate, preserving request bodies, credentials, signals and header overrides. Pass a custom transport as its third argument to retain transport-specific restrictions. Both helpers record only HTTP method and status; they never capture URLs, headers, bodies or exception messages. Without the endpoint environment variable they delegate without adding spans or headers.

The server session/JWKS helpers and typed JSON clients use this transport automatically. withTraceContext(headers, callback) continues inbound context when a nested session helper has no enclosing span; extractTraceContext(headers) provides an isolated parent for an application's existing request span. Browser clients retain their native fetch behavior and do not establish RUM trace roots.

PDS discovery requires explicit nonnegative integer budgets in ATPROTO_RESOLUTION_USER_HOURLY_CAP and ATPROTO_RESOLUTION_GLOBAL_HOURLY_CAP. Unset, invalid or zero budgets refuse outbound discovery with HTTP 429 and retryable RESOLUTION_UNAVAILABLE. Every discovery request, including cached reads and forced refreshes, consumes durable per-user and fleet-wide hourly admission before any provider request. Failed outbound attempts retain admission consumption; counters use the existing bounded verification retention and canonical account erasure mechanism. These caps bound request admission across replicas; operators must choose budgets appropriate to replica count, outbound latency and capacity.

Daily check-in study activities

Study subjects default to trackingMode: "duration"; existing rows and clients keep that default. Create and update accept "duration" | "check-in". The subjects response advertises supported trackingModes, including on empty accounts. Switching keeps the subject ID, finished durations and daily history. A running timer refuses a mode change with 409 active_timer; check-in subjects cannot start duration timers or become the game's duration fallback.

GET /api/study/check-ins?subject=<id>&from=YYYY-MM-DD&to=YYYY-MM-DD reads an inclusive UTC window of at most 366 days, including archived subjects. days contains only recorded { date, completed, revision } rows; an absent day is unchecked at revision zero. PUT /api/study/subjects/:id/check-ins/:date takes { completed, expectedRevision } and returns { subjectId, date, completed, revision }. Writes allow backfill before subject creation but refuse invalid or future UTC dates, archived subjects and duration mode. Stale revisions answer 409 revision_conflict; the immediately previous revision can replay the same completion state without advancing the revision. Every recorded row, including unchecked rows, counts as history for archive-on-delete and is included in account export. Ownership always comes from the authenticated caller. Check-ins award no duration or XP; finished duration summaries continue to include historical time across mode switches.

Release in dependency order: apply the additive migration and deploy Auth API first, publish the SDK second, then deploy Time's capability-gated UI. Generate and validate migrations locally; production migration follows the existing release approval procedure. Rollback may hide the UI or disable further check-in writes, while preserving tracking_mode, check-in rows and all action history. Do not drop or convert those records during rollback.

New daily rows, including initial unchecked days, use persisted hourly admission ceilings shared across activities and Auth replicas: STUDY_CHECK_IN_USER_HOURLY_CAP defaults to 600 per account and STUDY_CHECK_IN_GLOBAL_HOURLY_CAP to 20,000 fleet-wide. Zero disables new rows; exhausted admission returns 429 rate_limited. Existing-day edits and idempotent retries remain available at the ceiling. Buckets use server request time, so choosing older dates cannot bypass the quota; historical backfill remains allowed. These limits bound growth rate, not lifetime storage.

The PostgreSQL race acceptance check targets a disposable local database only: DATABASE_URL=<local-test-url> MIGRATION_TEST_DATABASE_URL=<local-test-url> bun scripts/time-checkins-postgres-acceptance.ts. It applies committed migrations, verifies old-client writes and races timers, switches, toggles and deletion. SDK version 0.28.0 prepares publication through the existing merge workflow; the migration-containing release remains gated before production rollout.

Bun SSR integration

@flow-industries/id/start/bun provides server-only static responses and a small request harness. It has no React or TanStack runtime imports. Applications choose which paths are files, their cache/security headers, and their SSR handler:

import {
  createSsrServer,
  installGracefulShutdown,
  safeRelativePath,
} from "@flow-industries/id/start/bun";

const app = createSsrServer({
  health: { readiness: async () => true },
  staticAsset: (_request, url) => {
    if (!url.pathname.startsWith("/assets/")) return null;
    return {
      directory: "./dist/client",
      path: safeRelativePath(url.pathname) ?? "",
      headers: { "Cache-Control": "public, max-age=31536000, immutable" },
    };
  },
  ssrHandler: (request) => handler.fetch(request),
});
const server = Bun.serve({
  port: 3000,
  async fetch(request, server) {
    const response = await app.fetch(request, server);
    if (!response) throw new Error("Expected an HTTP response");
    return response;
  },
});
installGracefulShutdown(server, { onShutdown: () => app.drain() });

Routing checks drain, optional health, before, selected static files, then SSR. before returns null to continue, a response to stop, or undefined after a WebSocket upgrade. Apps with different probe/origin ordering can omit health and handle it in before. transformResponse applies app policy to every HTTP response; observe wraps handling for the app's tracing and access logging. Neither hook receives implicit cache or security policy.

serveStaticFile(request, { directory, path, headers, contentType, conditional }) is available separately. It serves only GET/HEAD regular files confined to the selected directory, preserves caller headers and supports Last-Modified/304. Set conditional: false for unconditional responses. A missing selected file returns 404 from the harness; fallthrough: true instead continues to SSR. safeRelativePath decodes a URL path exactly once; pass its decoded result to serveStaticFile, which also refuses relative traversal and symlink escapes.

drain() is idempotent, rejects new requests and waits for admitted handlers and onDrain. Bun's server.stop(false) remains responsible for active response streams, and installGracefulShutdown supplies the shutdown deadline. The app continues to own WebSocket and worker cleanup.

For multi-origin session routes, pass audience: (request) => selectedOrigin to flowSessionHandlers and resolveFlowSessionRequest from start/server. Keep the request-host allowlist in the app, and wrap the resolver in an app-source server function when passing options. readSessionCookies(header, audience) from /server extracts the exact cookie names without verifying their values.

createFocusApi returns CompleteFocusApi, whose checkIns and setCheckIn methods are required. FocusApi retains optional methods for custom adapters. createMeditationApi(host, getToken) from the root or /focus reads finished Meditation summary(range?) and sessions({ before?, limit? }?) through the same JSON transport and StudyRequestError as study. Its cursor stays opaque, and it does not change study-only summaries or create a study subject.

rooms.mine() returns MyRoomSummary entries with the caller's effective viewerRole, including current global staff roles. The field is optional to represent older issuers; absence does not grant management permission. Mutations continue to check current server-side authorization. Public room and room-event wire types are exported from the package root.

User identity in request traces

Successful caller resolution, JWT verification, refresh and Better Auth session hooks record an auth.user.verified child span with only user.id. Separate identity spans preserve account switches and multiple verified users in one request. The ID comes from verified session/token state, never X-Flow-User-Id, a profile target, request body, or decoded unsigned JWT. Profile display metadata is resolved separately at read time; credentials and profile fields are not copied into these spans. No identity span is emitted without an active recording request span. Old traces and requests that do not resolve a user remain unrecorded. No SDK release or database migration is needed.

版本与发布记录

@flow-industries/id 共发布 66 个版本,首次发布于 2026/04/26;过去 12 个月发布了 66 个版本。

版本发布日期距今
0.36.0latest2026/10/2918 天前
0.35.02026/10/2918 天前
0.34.02026/10/2819 天前
0.33.12026/10/2620 天前
0.33.02026/10/2522 天前
0.32.12026/10/2323 天前

关于 @flow-industries/id 的常见问题

@flow-industries/id 是什么?

Passkey-first identity for Flow applications. One passkey bound to id.flow.industries, usable across Flow apps with audience-bound JWTs and optional Tempo signing.

如何安装 @flow-industries/id?

在项目目录运行 npm install @flow-industries/id,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:ox(>=0.14.0)、viem(>=2.47.10)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@flow-industries/id 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@flow-industries/id 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@flow-industries/id 可以商用吗?

@flow-industries/id 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@flow-industries/id 的下载量有多少?

根据 npm 官方下载统计,@flow-industries/id 在 2026/10/15 – 2026/11/13 这 30 天内被下载 3,742 次,日均约 125 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@flow-industries/id 还在维护吗?

最新版本 0.36.0 发布于 2026/10/29(18 天前);至今共发布 66 个版本,其中过去 12 个月发布了 66 个;首次发布于 2026/04/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。