@ai4b-team/fsaos-gateway-sdk

中文文档
4.3.0发布于 2026/11/05SEE LICENSE IN LICENSETypeScriptESM + CJS零依赖近 30 天下载 1700

FSAOS Gateway SDK — VFS hooks, session, realtime, and cache layer.

原始使用文档
文档目录26 节
  1. What This Is
  2. Architecture
  3. Location vs. Access — the two kinds of read
  4. Exports
  5. Error
  6. Session Management
  7. Gateway Call
  8. Public Page Forms
  9. Shared Instances
  10. Query Key Factory
  11. Channel Room Modes
  12. Chats, Threads and Breakouts
  13. Members, Seats and Agents
  14. VFS Operations (pure async functions)
  15. Cache Invalidation
  16. Realtime
  17. React Hooks (TanStack Query)
  18. Source Files
  19. Application bootstrap
  20. Build
  21. Dependencies
  22. Zero Kernel
  23. License
  24. Personal channel and project ordering
  25. Personal sidebar Sections
  26. Bound browser read traffic

@fsaos/gateway

The FSAOS Gateway SDK — reverse-engineered from the deployed production bundle into clean, maintainable TypeScript source.

What This Is

This is the source code for the 51 KB IIFE bundle that powers every FSAOS page today. Previously, only the minified bundle existed on R2 with no source repo. This package reconstructs the full TypeScript source with proper types, JSDoc comments, and a reproducible build pipeline.

Architecture

The SDK is loaded as a <script> tag by the dispatch worker and exposes window.__FSAOS_GATEWAY__ as a global. Components access it through the require() shim in dom-mount.js.

CDN Scripts (loaded before gateway.js):
  1. supabase-js@2        → window.supabase
  2. react@18 + react-dom → window.React / window.ReactDOM

Gateway SDK:
  3. gateway.js           → window.__FSAOS_GATEWAY__

Mount Runtime:
  4. dom-mount.js         → require() shim + mount()
  5. Component IIFE       → window.__FSAOS_COMPONENT__
  6. __FSAOS_MOUNT__()    → renders the component

Location vs. Access — the two kinds of read

Two questions look similar but are answered by different calls. Getting this right is the difference between a correct UI and one that shows a member items that aren't theirs.

  • Location — "what is at this path / of this type in this account?" → useItems, useList, useTree. These scope to the current account and return what's there. They do not know who you are within the account.
  • Access — "what can THIS actor reach in THIS context?" → useResolvedItems. Resolved server-side by the kernel: union of account / member / space / system / licensed tiers, frame-bounded to the acting principal, preference-cascade aware.

Rule of thumb: if the answer should differ depending on who is asking or which channel they are in — connectors, tools, or anything member-scoped — use useResolvedItems. The SDK never computes frames or permissions client-side; it forwards to the kernel and renders the answer.

Exports

Error

  • EnforcementDeniedError — Error class for enforcement denials (rules, access, entitlement)

Session Management

  • initSession() — Initialize domain-scoped session via GET /d/{hostname}/init
  • initializeFsaos() — Canonical one-call domain session + tracking bootstrap
  • clearSession() — Clear cached session
  • getSessionEntry() — Get current session entry (or null)
  • getAccessToken() — Get current auth token (waits for first auth event)
  • setCachedToken() — Manually set auth token (for embed tokens)
  • getScope() / setScope() / subscribeScope() — Active scope (account/switcher) accessors + subscription

Gateway Call

  • gatewayCall(method, params) — Core RPC dispatcher: POST /d/{hostname}

Public Page Forms

  • submitForm(capabilityId, fields, options?) — Submit through the public domain gateway to the form dataset bound to the current published page release. The browser supplies only the declared capability name, current page URL, and field values; it never receives a dataset identity or write credential. This works from the IIFE, ESM, or npm build.
  • FormSubmissionError — Includes the stable fillId, HTTP status, and error code so an uncertain submission can be retried without creating a second fill.
const receipt = await submitForm('hero-optin', { email });
if (receipt.next) window.location.assign(receipt.next);

Plain HTML can use the same contract without loading the SDK:

<form method="post" action="https://fsaos-mcp-gw-rust.fly.dev/d/launch.example.com/actions/hero-optin">
  <input type="hidden" name="_page_path" value="/offer">
  <input type="email" name="email" required>
  <button type="submit">Continue</button>
</form>

Shared Instances

  • supabase — Shared Supabase client instance
  • queryClient — Shared TanStack QueryClient instance

Query Key Factory

  • vfsKeys — Deterministic query keys for all VFS data

Channel Room Modes

Channels contain principals as members. AI is represented as a principal/member, not as a separate channel type. The SDK exposes the room mode through Channel.aiMode (Agent System Master Spec D17; kernel revision b7c4d2a9e1f3, 2026-10-29):

Channel.aiModeMeaning
"open"The floor decision runs on every message; whoever it names answers.
"active"Only the on-duty agents answer a person (Channel.activeAgentIds; absent = every agent seated in the channel). An agent's message wakes another agent by mention only.
"strict"Mention or named recipient only.
"hand_raise"The floor decision runs on a person's message but nobody is woken; its answer arrives as the conversation.hands_raised event (subscribeToEvents) and the person picks who speaks, an explicit-recipient send.

Mentions, a named recipient and trigger_ai answer in every mode. The kernel stores exactly these four values (CHANNEL_AI_MODES) and refuses any other at create-channel (CHANNEL_AI_MODE_INVALID); the legacy on/off rows were rewritten on → active, off → strict. A row the kernel has not rewritten is read the same way:

Legacy/backend valueSDK value
"on", "auto""active"
"off", "mention", missing or unknown"strict"

Chats, Threads and Breakouts

A channel is a chat (a DM or a group) and its members are its audience (Agent System Master Spec D21, 2026-10-30). Inside a chat every message is one thing, a message, with two flags the kernel computes on each top-level row:

FlagMeaning
ChannelMessage.reply_count > 0A thread: a side conversation inside the chat (useChannelMessages(...).threads).
ChannelMessage.member_count > 0A breakout: the thread root has members of its own, and only they (and the owners above it) read and post in it; the kernel hides it from everyone else (useChannelMessages(...).breakouts).

A chat created with member_ids is born auto_thread: false (messages happen in the channel feed); a member's own agent channel keeps auto_thread: true. Agents hand work to other agents in other chats with the todo tool's assign_to / report_to; the assignee is woken in that chat and the delegating agent is woken in the original conversation when the work completes, fails or needs input.

The SDK does not decide when AI should respond, spawn agents, or infer agent identity. It forwards gateway calls and normalizes read models; the kernel owns channel policy and message-trigger behavior.

Members, Seats and Agents

The typed surface of the membership doors (Agent System Master Spec D13–D21). Each function forwards one gateway call with the exact params the kernel reads and returns its receipt; the hooks (useInviteMember, useAcceptInvite, useSeatMember, useCreateAgent, useAnswerInput) wrap the same calls with the cache invalidation each needs.

CallKernel doorWhat it does
inviteMember({ email, target_scope_id, role? })invite-memberA member item, pending until accepted. role is their standing in the account (ladder role, default editor; ROLE_ABOVE_INVITER if above yours). Returns member_id.
seatMember({ chat_id, member_id, role? })link (has_member)Seats them on one chat (a channel) or breakout (a thread root). role goes in the edge context and is their role on that chat only; absent reads as editor. There is no metadata on an edge. Seat an invited member right after the invite: the roster shows the seat as pending and it opens on accept.
acceptInvite({ member_id })accept-inviteThe member turns active; can_do on every chat (and thread/breakout under it) they are seated on is false before this call and true after. No second seating step.
createAgent({ display_name, description?, role_path?, character?, upload?, instructions? })create-agentOne transaction: identity, home member item, instructions/ blocks, account-wide role, seats in every space you are in. role_path must already exist (ROLE_NOT_FOUND); omit it for the user-default role. Only a person may call it. Returns principal_id, handle, member_id, member_path, seats. It creates no chat: to talk to the agent, create-channel with member_ids: [principal_id] births a DM.
answerInput({ step_id, answer, input_request_id? | input_request? })todo-control answer_inputAnswers a step that is awaiting_input, with either the request id or the request object read from the step (TODO_INPUT_CHANGED if the step moved on). The kernel records input_response, returns the step to pending and wakes the assignee; the agent decides the next step. Nothing mechanical reads the answer as yes or no.

update with type_data merges at the top level: each key sent replaces that whole key, keys not sent are kept ({ type_data: { ai_mode: 'strict' } } changes the mode and nothing else; _remove_keys: ['x'] deletes a key). A channel's name (its path) is governed and refused; change type_data.display_name.

VFS Operations (pure async functions)

  • normalizeItem(raw) — Normalize raw gateway response to VFSItem
  • fetchVfsItem(path) — Fetch a single item
  • fetchVfsItemById(id) — Fetch a single item by UUID
  • fetchVfsChildren(path) — Fetch children (also warms item cache)
  • fetchItems(filter) — Fetch items with structured filters: one type, or any_of groups spanning types; fields, field_in, discovery, sort and offset
  • itemsFilterFromQueryFilter(typeData, { scope?, limit? }) — Turn a stored saved filter (a query_filter child of an arrangement) into the filter useInfiniteItems takes
  • fetchTypeDefinitions(scopeId?) — Fetch type definitions as Map<string, TypeDefinition>
  • fetchEdgesForItem(itemId) — Fetch edges (incoming + outgoing)
  • fetchVfsTree(path, depth?, types?, limit?) — Fetch recursive tree
  • fetchMemberFocus(scopeId, types?, limit?) — Fetch member-focus items
  • fetchResolvedItems(contextId, itemType) — Imperative form of useResolvedItems (access resolution)
  • inviteMember / acceptInvite / seatMember / createAgent / answerInput — see Members, Seats and Agents

Cache Invalidation

  • invalidateChildren(path) — Invalidate children query
  • invalidateItem(path) — Invalidate item query
  • invalidatePathAndParent(path) — Invalidate item + parent children
  • invalidateSubtree(path) — Invalidate all queries containing path prefix
  • invalidateAllVfs() — Nuclear: invalidate all VFS queries
  • invalidateTypes(scopeId?) — Invalidate type definitions

Realtime

  • initVfsRealtimeWs(scopeId) — Subscribe to VFS changes over WebSocket
  • disposeVfsRealtimeWs() — Tear down realtime connection
  • mountRealtimeScope(path) / unmountRealtimeScope(path) — Add/remove a scope from the realtime subscription
  • subscribeToPath(path, callback) — Subscribe to an item path and its descendants
  • waitForPathReady(path, timeoutMs?) — Wait for the gateway to acknowledge the current path subscription
  • resubscribePath(path) — Send a fresh wire-level subscription for an active path
  • getPathSubscriptionState(path) — Read pending, ready, disconnected, or unsubscribed
  • onConnectionStateChange(callback) — Observe transport connection state changes

React Hooks (TanStack Query)

Read hooks:

HookDescription
useItem(path)Single item by path
useItemById(id)Single item by UUID
useList(path) / useChildren(path)Children of a path (location)
useItems(filter)Items by type with filters (location; account-scoped, not frame-aware)
useInfiniteItems(filter, { realtime? })Kernel-filtered infinite pages; optional event-driven refresh, cached items, load more and refetch. No polling.
useIncrementalItems(filter, { shouldReadUpdate? })Live created-at feed: query pages once, point-read inserts and selected updates, reconcile after deletes/reconnect. No polling.
useInfiniteChildren(path)Infinite-scroll directory listing
useTree(path, depth?)Recursive tree
useSearch(query, types?)Keyword search
useResolve(params)Multi-signal search (exact + trigram + keyword + semantic)
useResolvedItems(contextId, itemType)Access resolution — what this actor can reach here (frame-bounded, pref-aware). Use for connectors, tools, any member-scoped type.
useOpen(path, options?)Open envelope — render directives + compatible components
useEdges(itemId)Edges for an item
useTypes(scopeId?) / useType(typeKey, scopeId?)Type definitions
useTypeHelpers(scopeId?)Derived type-system helper functions
useMemberFocus(scopeId, types?, limit?)Items relevant to the current user in a scope
useItemVersions(itemId, opts?)Version history (useItemHistory is deprecated)
useRecentActivity(scopePath, limit?, offset?)Audit log for a scope
useScope() / useScopeReady()Current scope session / readiness
useAuth()Reactive Supabase auth
useAccounts()Account memberships, switcher accounts, and the accounts mounted under each
usePrincipal() / useUpdatePrincipal()Current principal identity / update
useAsset(idOrPath)Resolve an asset URL
useComponent(path)Dynamically load a component from the edge
useTheme()Read theme CSS custom properties

useInfiniteItems(filter, { realtime: true }) uses the existing WebSocket event dispatch and reconnect recovery. As with the other realtime hooks, the host must hold an SDK account or path subscription (directly or via SDK bootstrap). It does not open a separate socket or subscribe once per component. The shared query cache stays fresh until a relevant item change or transport reconnect. Events with a different direct parent_id do not refresh a parent-filtered list; loaded items moving out still refresh their former list. Closed lists are marked stale without fetching, and unchanged lists reopen from cache. All consumers of the same filtered query should use the same realtime setting.

For a high-traffic message feed, useIncrementalItems avoids reloading every loaded page whenever a reply changes. It supports one item type, an explicit scope, sort_by: 'created_at', and optional parent_id, fields, or field_in. It returns the same list and pagination fields as useInfiniteItems. Inserts cost one item read; updates are ignored unless shouldReadUpdate(change, loadedItems) selects them; moves, deletes, and reconnects reconcile the pages. More complex filters fall back to the ordinary realtime listing. The host still needs its normal account or path WebSocket subscription.

A filter names one type, or any_of: 1-10 groups, each with an item_type, fields and field_in, so one listing spans item types with one total and one page order. discovery: 'catalog' keeps only what the kernel classes as a person's own standalone things; the kernel applies it before paging. A menu's stored sections are run without building a filter by hand:

const { resolvedChildren } = useArrangement({ key: 'library' });
const section = resolvedChildren.find(child => child.child_type === 'query_filter');
const list = useInfiniteItems(
  itemsFilterFromQueryFilter(section?.type_data, { scope: accountPath, limit: 50 }),
  { realtime: true },
);
// list.allItems, list.fetchNextPage(), list.isExhausted, list.totalHint

A relevant burst refreshes the loaded pages once, using the existing refreshQuery protection against overlapping loads. Multiple pages are read again together so offset pagination stays consistent after inserts/deletes. Current VFS notifications do not include arbitrary type_data fields, so a direct-child change may still require a listing read to evaluate a field filter (for example role=user). Missing old-parent information can only identify a move out when the changed item is already cached; reconnect or the next fetch reconciles the remaining count. No background polling is used.

Channel / notification hooks:

HookDescription
useChannelMessages(opts)Channel messages + bundled cards
useInfiniteChannelMessages(opts)Cursor-paginated messages (live tail)
useAllChannels() / useChannels() / useDmChannels()Channel lists (all / public / DM); archived channels only with { includeArchived: true }
usePersonalItemOrder('channel-order' | 'project-order', items)Apply and save the selected Space member's personal order through existing arrangements.
useNotifications(opts?)Notifications feed
useUnreadCounts(opts?)Aggregated unread counts

Realtime hooks:

HookDescription
useEventStream(...)Subscribe to SSE events by type
useRealtimeQuery(opts)useQuery that auto-refetches on matching events

Mutation hooks:

HookDescription
useCreate()Create item (invalidates parent children)
useUpdate()Update item (invalidates item + parent)
useDelete()Delete item (removes from cache + invalidates parent)
useMove()Move item (invalidates old parent, new parent, item)
useLink()Create edge (invalidates edges for both endpoints)
useMutation(method, opts?)Generic mutation for any gateway method

Source Files

src/
  types.ts             — All type definitions (VFSItem, TypeDefinition, VFSEdge, etc.)
  config.ts            — Reads __FSAOS_CONFIG__, creates Supabase client
  enforcement.ts       — EnforcementDeniedError class
  session.ts           — Auth token lifecycle, domain session init, scope accessors
  client.ts            — gatewayCall() — the core RPC dispatcher
  query-client.ts      — TanStack QueryClient with VFS-tuned defaults
  vfs-keys.ts          — Query key factory
  vfs.ts               — VFS fetch functions + cache invalidation helpers
  vfs-realtime-ws.ts   — WebSocket realtime subscriptions
  ws.ts                — WebSocket transport (events, scope, ping)
  hooks.ts             — All React hooks (useQuery/useMutation wrappers)
  resolved-items.ts    — useResolvedItems / fetchResolvedItems (access resolution)
  upload.ts            — File upload (imperative + hook)
  schema-utils.ts      — Schema interpretation utilities
  index.ts             — Barrel export

Application bootstrap

Set the registered application configuration before importing the SDK, then initialize it once. Tracking starts by default; custom business events remain explicit. Automatic page and declared-action capture runs for every published source that declares those adapters. A successful public form submission sends its receipt to each source that declares form_receipt; the default source continues to supply the durable form row's visitor and session attribution.

window.__FSAOS_CONFIG__ = {
  hostname: 'app.example.com',
  gatewayUrl: 'https://fsaos-mcp-gw-rust.fly.dev',
};

const { initializeFsaos } = await import('@ai4b-team/fsaos-gateway-sdk');
const fsaos = await initializeFsaos();

Use initializeFsaos({ tracking: false }) when consent or product policy requires tracking to remain disabled. Published FSAOS IIFE pages call the same bootstrap automatically.

Build

npm install
node build.mjs          # dist/gateway.js (minified IIFE, ~51 KB)
node build.mjs --dev    # dist/gateway.dev.js (readable, ~128 KB)

The build produces:

  • dist/gateway.js — Minified IIFE assigned to window.__FSAOS_GATEWAY__
  • dist/gateway.dev.js — Unminified for debugging
  • dist/*.d.ts — TypeScript declarations
  • dist/*.js.map — Source maps

Dependencies

Bundled inline (part of the IIFE):

  • @tanstack/react-query v5 — Query/mutation cache, deduplication, background refetch

External globals (loaded via CDN <script> before this bundle):

  • react — via window.React
  • @supabase/supabase-js — via window.supabase.createClient

Zero Kernel

This SDK has zero references to "kernel" in source, bundle, or type declarations. All naming uses gateway terminology. (The kernel owns resolution, frames, and enforcement; the SDK is the control surface that forwards to it.)

License

Proprietary and source-available — see LICENSE. You may install and use this package as a dependency in your own applications, but you may not copy, modify, or redistribute it as a standalone library. All rights reserved by AI4B Team.

Personal channel and project ordering

usePersonalItemOrder(key, items) reuses resolve-arrangement and the ordinary update primitive. Supply a complete collection of canonical IDs; render the returned items and call reorder(ids) once at drop. It returns contextKey for resetting in-progress UI gestures when the Space changes, plus ready, supported, isLoading, isSaving, error, saveError and refetch.

The hook reads only the selected Space member's provisioned channel-order or project-order arrangement. Account mounts are unsupported and make no requests. It never creates a missing arrangement or falls back to a shared blueprint. Channels without a saved position appear before manually ordered channels, newest creation first using Channel.createdAt from the existing query payload. Missing/equal creation times retain source order. With no saved order, this is also the channel list's default order. Existing manually placed channels keep their relative positions. Arrival/removal performs no arrangement write; the next explicit reorder saves the supplied order. Project ordering is unchanged. A save preserves unseen IDs, rejects stale Space callbacks and invalid lists, and reconciles through the existing coalesced arrangement refresh. No polling, item reads or parent changes are involved. For paged collections, apply ordering in the backend before pagination; this hook does not discover Project folders or provide that paged read contract.

Personal sidebar Sections

usePersonalSections(contextId, 'channels' | 'chats' | 'projects') reads personal reference groups. Use a Space ID for channels/projects or a channel ID for chats. The hook returns sections, ready, change, destination, and loading/error state. change supports create, rename, delete, assign, remove and reorder operations; it preserves canonical item locations. Supply a stable client UUID for section_id on create so a retry cannot create a duplicate Section.

Capture destination(sectionId) before starting a new channel or chat, then call assignPersonalSection(destination, savedItemId) once the canonical item has been saved. Retry that filing separately from creation/sending. The captured Space and context isolate late results after navigation. Versioned snapshots prevent late responses from replacing newer organization; refresh uses existing coalesced item-change events and reconnects, with no polling.

Requires the personal-sections kernel method from migration 0793523446ad. Projects are protocol-ready; a consumer must supply its Project UI separately.

Bound browser read traffic

Hosts sharing the SDK's QueryClient can opt into transport scheduling before mounting readers:

configureGatewayReads({ concurrency: 6, timeoutMs: 15_000, onTiming: recordTiming });
useInfiniteItems(filter, { priority: 'foreground' });
gatewayCall('query', params, { signal, priority: 'background' });

Equivalent in-flight reads share one transport request, scoped by credentials, session generation, hostname, and effective parameters. Each observer receives its own result object and may cancel independently. The last cancellation aborts the fetch; identity reset cancels queued and active reads. The deadline includes queue time. onTiming reports only method, priority, queue/request duration and outcome. Commands, writes, materializing queries and subscriptions bypass the queue. The scheduler never retries a request; retry/cooldown policy remains with the host's existing QueryClient. Without configuration, transport behavior is unchanged.

版本与发布记录

@ai4b-team/fsaos-gateway-sdk 共发布 71 个版本,首次发布于 2026/05/22;过去 12 个月发布了 71 个版本。

版本发布日期距今
4.3.0latest2026/11/055 天前
4.2.22026/11/055 天前
4.2.12026/11/055 天前
4.2.02026/10/319 天前
4.1.02026/10/3110 天前
4.0.02026/10/3011 天前

关于 @ai4b-team/fsaos-gateway-sdk 的常见问题

@ai4b-team/fsaos-gateway-sdk 是什么?

FSAOS Gateway SDK — VFS hooks, session, realtime, and cache layer.

如何安装 @ai4b-team/fsaos-gateway-sdk?

在项目目录运行 npm install @ai4b-team/fsaos-gateway-sdk,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@tanstack/react-query(^5.99.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ai4b-team/fsaos-gateway-sdk 支持 TypeScript 吗?

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

@ai4b-team/fsaos-gateway-sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@ai4b-team/fsaos-gateway-sdk 可以商用吗?

@ai4b-team/fsaos-gateway-sdk 使用 SEE LICENSE IN LICENSE 许可。许可条款写在包内的独立文件中,请阅读该文件确认可否商用。以上为概括,不构成法律意见,请以许可原文为准。

@ai4b-team/fsaos-gateway-sdk 的下载量有多少?

根据 npm 官方下载统计,@ai4b-team/fsaos-gateway-sdk 在 2026/10/05 – 2026/11/03 这 30 天内被下载 1,700 次,日均约 57 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ai4b-team/fsaos-gateway-sdk 还在维护吗?

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