docs: resolve product surface audit

This commit is contained in:
2026-07-10 12:42:34 +08:00
parent 599a2f6e66
commit 913a7ae5c0
18 changed files with 918 additions and 7 deletions
@@ -0,0 +1,191 @@
# Accepted product-surface code inventory
Audit date: 2026-07-10
Scope: source-level inventory for the accepted SaaS product boundary in ADR-0020,
ADR-0021 and ADR-0022. This is not a claim that tests were executed during this
inventory. “Test evidence” below means a repository test exercises the named
path; “no direct test found” means a search of `hub/test/**/*.ts` found no request
to the named route or corresponding service call.
## Contract baseline
The destination is not a shared-bot, single-credential Hub. The repository's
current shared language says:
- an Organization owns its memberships, projects, teams, external connections
and settings (`CONTEXT.md:7-9`);
- a Platform Administrator creates and operates Organizations without becoming
their member, while an Organization Administrator is only an OWNER/ADMIN of
that Organization (`CONTEXT.md:11-17`);
- the Admin Surface is a private web control plane requiring the corresponding
authenticated administrator authority (`CONTEXT.md:19-21`);
- each Organization has its own Feishu application for login, bot messaging and
directory integration (`CONTEXT.md:23-25`);
- provider connections are Organization-scoped BYOK or an Organization-distinct
platform-managed connection (`CONTEXT.md:27-37`);
- capacity uses non-optional platform ceilings, optional lower Organization
limits, durable Organization-fair admission and audited emergency brakes
(`CONTEXT.md:39-77`).
ADR-0021 makes `/admin/platform` and `/admin/org/:orgSlug` separate guarded web
areas (`docs/adr/0021-org-admin-project-onboarding.md:23-40`), requires a
customer-owned Feishu app with encrypted Organization-scoped secrets
(`docs/adr/0021-org-admin-project-onboarding.md:42-47`), and pins both provider
credential modes as Organization-scoped (`docs/adr/0021-org-admin-project-onboarding.md:49-60`).
ADR-0022 makes its safety controls release requirements, not optional future UI
(`docs/adr/0022-layered-capacity-admission.md:22-77`).
Status vocabulary in this inventory:
- **HTTP-backed** / **Feishu-backed**: a production route/event handler is wired.
- **Service/test only**: code can be invoked directly, but there is no production
product entry point.
- **Partial**: part of the journey exists but the accepted end-to-end product
surface cannot complete it.
- **Missing**: no production entry and no suitable persistence/control model.
- **Contract divergence**: reachable behavior actively uses the wrong tenancy or
credential shape.
## Journey matrix
| Target journey | Reachable entry | Authorization | Persistence | Test evidence | Verdict and exact gap |
|---|---|---|---|---|---|
| Platform Administrator signs in | None. The admin plugin registers only Feishu auth and Organization routes (`hub/src/admin/plugin.ts:7-8`, `hub/src/admin/plugin.ts:31-46`). The only HTML is the Org Admin login page (`hub/src/admin/routes/authRoutes.ts:171-207`). | `PlatformRoleAssignment` and `PlatformRole.ADMIN` exist in the schema (`hub/prisma/schema.prisma:111-129`), but the HTTP guard explicitly says the platform control plane is not modeled (`hub/src/admin/auth/guards.ts:1-5`). | Role rows can be stored, but there is no platform session/identity binding, invite/bootstrap flow or platform-admin audit surface. | No platform-auth route test exists. | **Missing.** `/admin/platform` from ADR-0021 is not mounted, so no authenticated operator can reach any platform workflow. |
| Platform Administrator creates/lists/updates/operates Organizations | None. No `/api/platform/*` or Organization CRUD route is registered; the route composition is auth + `/api/org/:orgSlug/*` only (`hub/src/admin/plugin.ts:31-46`, `hub/src/admin/routes/orgRoutes.ts:24-103`). | No `requirePlatformAdmin` implementation; only `requireSession` and `requireOrgRole` exist (`hub/src/admin/auth/guards.ts:43-113`). | `Organization` has slug, name and ACTIVE/SUSPENDED/ARCHIVED status (`hub/prisma/schema.prisma:28-52`). | Organizations are created directly only by test setup, e.g. `prisma.organization.upsert` (`hub/test/integration/helpers.ts:60-77`) and a direct cross-org fixture create (`hub/test/integration/admin-explorer.test.ts:198-210`). | **Service/test only at the database layer.** Manual pilot onboarding has no safe production command, HTTP API or UI; status changes are also unreachable. |
| Platform Administrator configures an Organization's customer-owned Feishu app and checks readiness | None. Runtime accepts exactly one `feishuAppId`/`feishuAppSecret` pair in `AdminPluginConfig` (`hub/src/admin/plugin.ts:10-19`), and server startup reads one process-global app id, secret and bot open id (`hub/src/server.ts:47-52`). | No platform guard or Organization connection ownership check. | There is no Feishu application/secret model. `ExternalDirectoryConnection` stores provider/tenant/source metadata but no app id, bot id or secret reference (`hub/prisma/schema.prisma:196-214`). | No provisioning/readiness test exists. | **Missing + contract divergence.** There is no Organization-scoped encrypted Feishu connection or readiness workflow; production can operate only the one env-configured app. |
| Platform Administrator configures an Organization-distinct platform-managed provider key/base URL | None. No platform provider route or UI is registered. | No platform guard. | The accepted Lean type exists only in the semantic contract (`spec/Spec/System/Organization.lean:52-66`); Prisma has no provider-connection or provider-mode model (`hub/prisma/schema.prisma:26-214`). | Runtime tests prove env values are returned, not Organization values (`hub/test/unit/runtime-settings.test.ts:18-55`). | **Missing + contract divergence.** `EnvRuntimeSettings.provider` ignores its project scope and reads one process-global `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` (`hub/src/settings/runtime.ts:20-21`, `hub/src/settings/runtime.ts:46-67`). |
| Platform Administrator applies Organization/platform `DRAIN` or `STOP_NOW` and later resumes | None. | No platform guard, reason capture or scope check. | No workload-brake model exists; only the Lean contract defines `open`/`drain`/`stopNow` semantics (`spec/Spec/System/Capacity.lean:71-87`). | No workload-brake test exists. | **Missing.** The required audited, reason-bearing, reversible emergency control from ADR-0022 (`docs/adr/0022-layered-capacity-admission.md:69-72`) has no product or persistence surface. |
| Organization user logs in with that Organization's Feishu app | `GET /auth/feishu` and callback are HTTP-backed (`hub/src/admin/routes/authRoutes.ts:49-128`). The callback upserts a user and signs a cookie (`hub/src/admin/routes/authRoutes.ts:99-123`). | Login itself is public OAuth; Organization APIs later enforce session + active OWNER/ADMIN membership (`hub/src/admin/auth/guards.ts:43-113`). | `User.feishuOpenId` is one process-global unique identifier (`hub/prisma/schema.prisma:89-109`); the signed cookie carries user id/open id but no Organization or Feishu-connection identity (`hub/src/admin/auth/session.ts:18-23`). | Redirect and mocked callback are covered (`hub/test/integration/admin-auth.test.ts:164-230`); OAuth request construction/exchange is covered (`hub/test/unit/feishu-oauth.test.ts:8-81`). | **HTTP-backed but contract-divergent.** OAuth is built once from the process-global app credentials (`hub/src/admin/routes/authRoutes.ts:38-47`), and the implementation explicitly documents this deferral (`hub/src/admin/auth/feishuOAuth.ts:1-10`). There is no org-selecting login route, connection resolver or issuer/app identity in the user key. |
| Each Organization operates its own Feishu Bot | One WebSocket listener is production-wired (`hub/src/server.ts:76-90`). | Message actions are gated by project grants once a binding resolves (`hub/src/feishu/trigger.ts:715-755`), but there is no initial Organization/connection ownership gate for the inbound app. | Binding stores only `chatId -> projectId`, with no Feishu connection/application id (`hub/prisma/schema.prisma:287-304`). Event receipts store globally unique `eventId`, likewise without connection id (`hub/src/feishu/trigger.ts:687-707`). | Trigger, onboarding and event dedup are integration-tested (`hub/test/integration/trigger.test.ts:211-330`, `hub/test/integration/trigger.test.ts:878-928`). | **Feishu-backed for one bot, contract-divergent for SaaS.** Startup constructs one client and one WS client from one config (`hub/src/feishu/client.ts:854-901`). It cannot operate or route multiple Organization-owned apps. |
| Suspended/archived Organization is prevented from bot work | No platform status-change entry exists. Org HTTP APIs do reject non-ACTIVE Organizations (`hub/src/admin/auth/guards.ts:79-88`). | The Feishu trigger resolves an active chat binding directly and then project permissions; it does not load or check `Project.organization.status` (`hub/src/feishu/trigger.ts:715-755`). | Organization status exists (`hub/prisma/schema.prisma:30-52`). | No suspended-Organization trigger test exists. | **Partial and unsafe.** Even a database-side status change only blocks web admin; an already-bound chat remains triggerable through the bot path. |
| Logged-in Organization OWNER/ADMIN reaches the management panel | OAuth success redirects to `/admin/org/:slug` (`hub/src/admin/routes/authRoutes.ts:237-267`). | All Organization APIs default to OWNER/ADMIN through `requireOrgRole` (`hub/src/admin/auth/guards.ts:66-113`), and tests prove MEMBER is denied while ADMIN succeeds (`hub/test/integration/admin-auth.test.ts:95-131`). | Session is an HMAC cookie; user/membership is re-read for each guard (`hub/src/admin/auth/session.ts:31-57`, `hub/src/admin/auth/guards.ts:43-63`). | Guard behavior is integration-tested (`hub/test/integration/admin-auth.test.ts:56-131`). | **API-backed, UI missing.** The plugin says the static SPA is “later” (`hub/src/admin/plugin.ts:1-3`); no route/static handler serves `/admin/org/:slug`. OAuth therefore redirects to an unmounted page. |
| Organization OWNER/ADMIN manages members and roles | REST endpoints list/add/change/revoke members (`hub/src/admin/routes/membersRoutes.ts:20-98`). | OWNER/ADMIN route guard plus service rules: only OWNER may affect OWNER and the last OWNER cannot be removed (`hub/src/org/members.ts:1-8`, `hub/src/org/members.ts:97-205`). | `OrganizationMembership` persists org, user, role and revocation (`hub/prisma/schema.prisma:54-76`). | Add/list/last-OWNER protection is covered through HTTP (`hub/test/integration/admin-members-teams.test.ts:49-83`). | **HTTP-backed and persisted; management UI missing.** Addition requires a raw `feishuOpenId` and may create the `User` directly (`hub/src/org/members.ts:46-85`); there is no invite/accept or org-connection directory picker. Role-change/revoke variants have less direct HTTP coverage than add/list. |
| Organization OWNER/ADMIN manages teams and team membership | REST endpoints cover list/create/update/archive and membership add/revoke (`hub/src/admin/routes/teamsRoutes.ts:21-139`). | All routes require Organization OWNER/ADMIN; service verifies the team and added user belong to the same Organization (`hub/src/org/teams.ts:177-215`, `hub/src/org/teams.ts:263-275`). | `Team` and `TeamMembership` are Organization/project-principal persistence (`hub/prisma/schema.prisma:143-177`). | Create/add-member/archive is covered through HTTP (`hub/test/integration/admin-members-teams.test.ts:85-160`). | **HTTP-backed and persisted; management UI missing.** No production route exposes `ExternalDirectoryConnection`/external team binding sync, so the Feishu directory portion remains service-only. |
| Organization OWNER/ADMIN creates and manages Folders/Projects | REST endpoints cover explorer, folder create/update/archive, project create/detail/rename/move/archive (`hub/src/admin/routes/explorerRoutes.ts:33-212`). | All routes require Organization OWNER/ADMIN. Service operations scope folder/project ids to the Organization (`hub/src/org/explorer.ts:301-341`); HTTP cross-org lookup is tested. | Folder and Project carry `organizationId`; Folder is transparent and Project owns workspace/session/run relations (`hub/prisma/schema.prisma:238-285`). Project creation also creates a real workspace and default permission rows (`hub/src/projectOnboarding.ts:275-348`). | Create/list and move/archive are HTTP-tested (`hub/test/integration/admin-explorer.test.ts:66-196`); cross-org id access is tested (`hub/test/integration/admin-explorer.test.ts:198-223`). | **HTTP-backed and substantially tested; management UI missing.** This is the strongest accepted product slice. |
| Organization OWNER/ADMIN manages Feishu chat binding | HTTP can archive a binding (`hub/src/admin/routes/explorerRoutes.ts:214-227`). New binding is exposed through the Feishu onboarding card, not web. | Archive requires OWNER/ADMIN at HTTP and then project manager authorization in the shared service (`hub/src/projectOnboarding.ts:231-272`, `hub/src/org/explorer.ts:301-314`). Bind existing requires project MANAGE (`hub/src/projectOnboarding.ts:179-229`). | `ProjectGroupBinding` and the FEISHU_CHAT EDIT grant are transactionally stored (`hub/src/projectOnboarding.ts:190-215`, `hub/prisma/schema.prisma:287-304`). | Bind/one-active/archive/rebind is service-tested (`hub/test/integration/project-onboarding.test.ts:114-160`); card bind is integration-tested (`hub/test/integration/trigger.test.ts:271-330`). No direct HTTP archive test was found. | **Feishu + partial HTTP-backed; UI missing.** Binding identity is not scoped by Feishu application, so the 1:1 uniqueness and lookup are process-global rather than Organization-connection-scoped. |
| Organization OWNER/ADMIN manages project team access | REST list/grant/revoke endpoints exist (`hub/src/admin/routes/accessRoutes.ts:19-79`). | OWNER/ADMIN route guard; service refuses a team from another Organization (`hub/src/permissions/projectTeamAccess.ts:168-195`). | TEAM -> PROJECT `PermissionGrant` persists the role (`hub/prisma/schema.prisma:428-447`). | Grant/list/archive-induced revoke is HTTP-tested (`hub/test/integration/admin-members-teams.test.ts:123-156`). | **HTTP-backed and tested; UI missing.** Only team grants are surfaced. There is no HTTP management of direct USER/chat grants, `PermissionSettings`, or per-role `RoleTriggerGrant`. |
| Organization OWNER/ADMIN manages general project settings, roles/models and agent policies | Only `membersCanCreateProjects` is reachable through GET/PATCH `/settings` (`hub/src/admin/routes/orgRoutes.ts:46-81`). | OWNER/ADMIN route guard. | `OrganizationProjectSettings` stores only that Boolean (`hub/prisma/schema.prisma:78-87`). `PermissionSettings` exists, but no admin route references it (`hub/prisma/schema.prisma:449-467`). Model/role registry is explicitly an in-memory skeleton awaiting DB/admin wiring (`hub/src/agent/models.ts:65-79`). | The Boolean setting is HTTP-tested (`hub/test/integration/admin-auth.test.ts:133-162`); model env behavior is unit-tested (`hub/test/unit/runtime-settings.test.ts:4-64`). | **Partial.** The creation policy works, but the accepted “projects and configuration” control plane lacks editable permission settings, model/role settings and run policies. |
| Organization OWNER/ADMIN chooses BYOK vs platform-managed Provider mode; BYOK admin writes key/base URL | None. | No Organization provider guard because no route/service exists. | No Prisma provider connection, credential mode, encrypted secret handle or rotation metadata. The only persistent `provider` fields are descriptive strings on sessions/runs (`hub/prisma/schema.prisma:308-330`, `hub/prisma/schema.prisma:352-387`). | No provider-mode test. Existing tests assert the global env resolver (`hub/test/unit/runtime-settings.test.ts:18-55`). | **Missing + contract divergence.** Lean/ADR have the mode but production always calls `settings.provider("openrouter", { projectId })` (`hub/src/feishu/trigger.ts:195-199`), whose implementation discards the scope (`hub/src/settings/runtime.ts:46-67`). |
| Organization OWNER/ADMIN views sessions and runs | HTTP list and detail endpoints exist (`hub/src/admin/routes/sessionsRoutes.ts:14-45`). | OWNER/ADMIN route guard; service scopes project/session through Organization id (`hub/src/org/sessions.ts:6-23`, `hub/src/org/sessions.ts:49-87`). | `AgentSession`/`AgentRun` persist provider/model/status/tokens/cost and relations (`hub/prisma/schema.prisma:308-387`). | No direct test for these HTTP routes or `org/sessions.ts` was found. Trigger tests do prove sessions/runs are written, e.g. separate role sessions (`hub/test/integration/trigger.test.ts:850-876`). | **HTTP-backed and persisted, but untested at the product boundary and UI missing.** |
| Organization OWNER/ADMIN views Organization/Project/Folder usage | HTTP Organization and Project usage endpoints exist, with optional folder/date filtering (`hub/src/admin/routes/sessionsRoutes.ts:47-79`). | OWNER/ADMIN route guard. | Rollup derives Organization through Project and uses persisted token/cost facts (`hub/src/org/usage.ts:35-77`, `hub/src/org/usage.ts:106-151`). Missing cost increments `runsWithoutCost`, not known cost (`hub/src/org/usage.ts:106-118`). | No direct usage route/service test was found. `/cost` tests cover the Feishu session command, not these admin endpoints (`hub/test/integration/trigger.test.ts:423-480`). | **HTTP-backed and persisted, but incomplete relative to ADR-0022.** It does not attribute by Provider Connection because none exists, has no alert configuration/delivery, no UI, and no product-boundary tests. |
| Ordinary MEMBER creates a Project from an unbound Feishu group | Mentioning the bot in an unbound chat sends an onboarding card (`hub/src/feishu/trigger.ts:850-887`); card action calls shared project creation (`hub/src/feishu/trigger.ts:554-614`). | User must already exist and have an active Organization membership; ordinary creation is gated by `membersCanCreateProjects` (`hub/src/projectOnboarding.ts:149-177`, `hub/src/projectOnboarding.ts:380-403`). Creator receives USER MANAGE and the chat receives EDIT (`hub/src/projectOnboarding.ts:302-340`). | Project, Folder, workspace, binding, default settings and grants are persisted (`hub/src/projectOnboarding.ts:275-348`). | Card display/create/bind is integration-tested (`hub/test/integration/trigger.test.ts:211-330`); policy denial and grants are service-tested (`hub/test/integration/project-onboarding.test.ts:70-112`). | **Feishu-backed and well-tested for the single-app case.** For a user with multiple active memberships, onboarding refuses and says the org selector does not exist (`hub/src/feishu/trigger.ts:889-929`). With a per-Org app, the inbound connection should supply tenant context; current global bot cannot. |
| Ordinary MEMBER binds an existing manageable Project | Onboarding card lists unbound projects and card action binds one (`hub/src/feishu/trigger.ts:868-885`, `hub/src/feishu/trigger.ts:932-969`). | Non-admin candidate projects require `collaborator.manage`; bind service rechecks project MANAGE (`hub/src/feishu/trigger.ts:951-960`, `hub/src/projectOnboarding.ts:179-215`). | Binding + FEISHU_CHAT EDIT grant persist transactionally. | Card journey is integration-tested (`hub/test/integration/trigger.test.ts:271-330`). | **Feishu-backed for the single-app case; per-Organization connection routing is absent.** |
| Ordinary authorized member triggers an Agent Run from Feishu | Bound message event resolves Project, evaluates `agent.trigger`, resolves role/model/provider, creates session/run and starts the runner (`hub/src/feishu/trigger.ts:684-848`, especially `hub/src/feishu/trigger.ts:715-755` and `hub/src/feishu/trigger.ts:140-256`). | Missing sender id is fail-closed; project EDIT+ and optional role grant are enforced (`hub/src/feishu/trigger.ts:171-194`, `hub/src/feishu/trigger.ts:726-755`). | Session/run, messages, file changes, event receipt and audit fragments persist in Prisma (`hub/prisma/schema.prisma:306-405`, `hub/prisma/schema.prisma:496-525`). | Lifecycle, permission denial, dedup, audit, messages and interrupt have integration coverage (`hub/test/integration/trigger.test.ts:116-154`, `hub/test/integration/trigger.test.ts:597-607`, `hub/test/integration/trigger.test.ts:878-928`, `hub/test/integration/trigger.test.ts:983-1100`). | **Feishu-backed and tested for one process-global provider/Feishu connection.** It is not an Organization-resolved run: provider scope is ignored and bot/identity connection is absent. |
| Accepted work waits under Project/Organization/platform capacity and survives restart | Only a process-local queue per Project exists (`hub/src/feishu/triggerQueue.ts:18-45`). It is drained after the active Project run (`hub/src/feishu/trigger.ts:430-495`). | No Organization admission identity or capacity policy is evaluated. | Queue is a `Map` in memory (`hub/src/feishu/triggerQueue.ts:21-26`), not a Prisma model; accepted requests vanish on process exit. Expired entries are dropped and only logged (`hub/src/feishu/trigger.ts:475-483`). | FIFO/full/expiry behavior is unit-tested (`hub/test/unit/trigger-queue.test.ts:10-78`); locked-trigger enqueue/drain is integration-tested (`hub/test/integration/trigger.test.ts:609-673`). | **Contract divergence.** ADR-0022 requires durable bounded admission, Organization-fair scheduling, explicit EXPIRED/CANCELED states and notifications (`docs/adr/0022-layered-capacity-admission.md:33-46`); none are present. |
| Platform ceiling + optional lower Organization policy protect request/run/file/storage/entity capacity | None beyond Fastify defaults, a five-item Project queue and global max turns. `Fastify({ logger: true })` has no capacity options/plugins (`hub/src/server.ts:55-55`); dependencies include no rate-limit plugin (`hub/package.json:9-17`). | No admin policy route. | No capacity/limit model. `OrganizationProjectSettings` has only `membersCanCreateProjects` (`hub/prisma/schema.prisma:78-87`). Run policy contains only global `maxTurns` (`hub/src/settings/runtime.ts:24-37`, `hub/src/settings/runtime.ts:74-78`). The systemd unit has no CPU/memory/process controls (`hub/deploy/cph-hub.service:6-26`). | Unit tests cover only global max turns and the local queue (`hub/test/unit/runtime-settings.test.ts:58-64`, `hub/test/unit/trigger-queue.test.ts:5-113`). | **Missing.** None of ADR-0022's mandatory platform/org request rate, body, concurrency, file/archive, storage, entity-count, wall-time/tool/output or process limits are product-reachable or persistently enforced (`spec/Spec/System/Capacity.lean:13-60`). |
## Reachability summary
### Production-wired HTTP surface
The actual production plugin mounts these capability groups:
- global Feishu OAuth/session: `/auth/feishu`, callback, logout, `/api/me`, and
the minimal `/admin/login` HTML (`hub/src/admin/routes/authRoutes.ts:49-207`);
- Organization summary and one onboarding Boolean
(`hub/src/admin/routes/orgRoutes.ts:27-81`);
- Folder/Project explorer and binding archive
(`hub/src/admin/routes/explorerRoutes.ts:33-227`);
- member roles (`hub/src/admin/routes/membersRoutes.ts:20-98`);
- teams/team membership (`hub/src/admin/routes/teamsRoutes.ts:21-139`);
- TEAM -> PROJECT grants (`hub/src/admin/routes/accessRoutes.ts:19-79`);
- sessions and usage (`hub/src/admin/routes/sessionsRoutes.ts:14-79`).
Every `/api/org/:orgSlug/*` route uses the same OWNER/ADMIN guard. This satisfies
the clarified requirement that a management API cannot be used by an anonymous
or ordinary Organization member (`hub/src/admin/auth/guards.ts:43-113`). It does
not provide the accepted management **panel**: only `/admin/login` is rendered,
and the post-login `/admin/org/:slug` destination is not served.
### Production-wired Feishu surface
The trigger handler is a real end-to-end application path for one Feishu app:
event receipt/dedup, chat binding, permission checks, unbound-chat onboarding,
Project creation/binding, sessions/runs, streaming and interrupt. The service
wires the handler to a single SDK client and WebSocket listener
(`hub/src/server.ts:76-90`, `hub/src/feishu/client.ts:854-901`). The code and tests
therefore establish a useful single-app vertical slice, not the required
per-Organization connection architecture.
### Only database/service/test reachable
- Organization creation is direct Prisma fixture work
(`hub/test/integration/helpers.ts:60-77`).
- `PlatformRoleAssignment` is a stored type with no platform auth/HTTP surface
(`hub/prisma/schema.prisma:111-129`).
- external-directory sync has service code and can create membership rows
(`hub/src/permissions/externalSync.ts:40-90`), but no registered production
admin route.
- project `PermissionSettings` and `RoleTriggerGrant` are stored/enforced in
parts of the runtime, but have no Organization management API
(`hub/prisma/schema.prisma:449-494`).
### Entirely absent from runtime persistence/control plane
- Organization-scoped Feishu application connection and encrypted secret
handle;
- Organization-scoped Provider Connection, BYOK/platform-managed mode, encrypted
secret handle, rotation and runtime resolver;
- platform administrator session/guard, platform Organization CRUD, platform
admin invitation, platform-operation audit;
- versioned platform capacity ceiling loader/validation;
- Organization policy-limit rows and Organization admin endpoints;
- durable run request/admission state, fair Organization scheduler, queue cancel
and expiry notification;
- Organization/platform workload brake state and audited actions;
- usage/cost alert configuration and delivery;
- the Organization and platform management UI.
## Critical production blockers
1. **No Organization connection control plane.** The running service has one
Feishu app/bot and one provider token/base URL. This contradicts the defining
SaaS requirement even though Project/Team data is otherwise Organization-
scoped (`hub/src/server.ts:44-52`, `hub/src/settings/runtime.ts:46-67`).
2. **No platform control plane.** A Platform Administrator cannot authenticate,
create or operate an Organization, provision its Feishu app, configure its
platform-managed provider connection, or apply a workload brake. The only
apparent Organization creation path is direct database/test code.
3. **No usable management panel.** The backend Organization APIs are meaningful
and guarded, but OAuth redirects to an unmounted `/admin/org/:slug`; platform
UI does not exist at all (`hub/src/admin/plugin.ts:1-8`,
`hub/src/admin/routes/authRoutes.ts:237-267`).
4. **ADR-0022 is almost wholly unimplemented.** The in-memory Project queue is
specifically the implementation divergence ADR-0022 rejects; there are no
Organization-fair durable admission, mandatory layered limits, hard run/file/
storage/process budgets or emergency brakes
(`docs/adr/0022-layered-capacity-admission.md:79-88`).
5. **Tenant context is inferred too late and from global identifiers.** OAuth
creates a globally keyed `User`, bot events bind globally by `chatId`, and an
unbound-chat user with more than one Organization is rejected rather than
routed by the customer-owned Feishu connection (`hub/prisma/schema.prisma:89-109`,
`hub/src/feishu/trigger.ts:715-723`, `hub/src/feishu/trigger.ts:889-929`). This
blocks a correct multi-app implementation and also leaves Organization status
unenforced on the bot path.
## What is already worth retaining
The audit does not imply rewriting the working core. The following seams are
concrete and have useful test evidence:
- Organization ownership on Project/Team/Folder and same-Organization access
checks (`hub/prisma/schema.prisma:143-177`, `hub/prisma/schema.prisma:238-285`);
- OWNER/ADMIN guards and last-OWNER protection
(`hub/src/admin/auth/guards.ts:66-113`, `hub/src/org/members.ts:97-205`);
- reusable service operations for project creation, chat binding and grants
(`hub/src/projectOnboarding.ts:128-229`, `hub/src/projectOnboarding.ts:275-348`);
- Organization-scoped explorer/member/team/team-access HTTP APIs and their
integration tests;
- Feishu onboarding, project permission checks, session/run persistence and
dedup in the single-app vertical slice.
The shortest path to the accepted destination is therefore to add the missing
platform/connection/capacity control planes and real admin UI around these seams,
then change ingress and runtime resolution to carry an explicit Organization
connection identity end to end. Treating the existing env credentials as an
acceptable pilot fallback would preserve the wrong tenant boundary.
@@ -0,0 +1,259 @@
# Accepted SaaS product-surface completeness audit
Audit date: 2026-07-10
## Verdict
The current Hub is **not a usable or production-complete SaaS product surface**
for the accepted destination. It has a worthwhile Organization-scoped backend
foundation and a well-tested single-Feishu-application Agent Run slice, but the
running product still has one process-global Feishu app/bot and one global model
provider credential. It has no platform-administrator control plane, no
Organization connection/secret plane, and no Organization-scoped Provider
Credential Mode. Its only HTML product page is a login form; successful OAuth
redirects an administrator to an unmounted `/admin/org/:orgSlug` route.
ADR-0022's required production safety surface is also almost wholly absent:
there is no durable Organization-fair admission, layered platform/Organization
limits, hard file/storage/run/process budgets, usage-alert control plane, or
audited workload brake. These are release blockers, not optional polish.
The strongest code should be retained rather than rewritten: all current
`/api/org/:orgSlug/*` routes reauthorize an active OWNER/ADMIN membership;
Folder/Project, member, Team, Team-to-Project access, session, and usage services
are Organization-scoped; and the shared project-onboarding services correctly
create Project/chat/grant relationships for the single-app case. The shortest
route is to add explicit Organization connection identity and the two private
admin control planes around those seams.
The claim-by-claim source inventory, including route, schema and test locations,
is in [accepted product-surface code inventory](product-surface-code-inventory.md).
Official Feishu identifier, OAuth, event and long-connection semantics are in
[Feishu product-surface primary sources](product-surface-feishu-primary-sources.md).
## Accepted boundary
The audit treats these as required because they are already accepted by the
repository's `CONTEXT.md`, ADR-0020, ADR-0021, ADR-0022, and Lean contract:
- a Platform Administrator is separate from Organization membership and uses a
private `/admin/platform` area to create and operate Organizations;
- an Organization OWNER/ADMIN uses a private `/admin/org/:orgSlug` area to
manage members, projects, Teams, connections, settings, sessions and usage;
- every Organization authenticates and exchanges bot/directory traffic through
its own customer-owned Feishu Application;
- every Organization chooses BYOK or an Organization-distinct platform-managed
Provider Connection, with different administrators owning the respective
plaintext inputs;
- ordinary members can create/bind a Project from an unbound Feishu group only
under the Organization policy, while all Project actions retain Project-level
authorization;
- production has mandatory platform ceilings, optional lower Organization
limits, durable fair admission, bounded work and audited emergency brakes.
The audit does not add deferred self-service signup, payment collection,
folder ACL inheritance, commercial billing, or a live editor for platform
capacity ceilings. Guided Feishu app setup and manually initiated Organization
creation by an authenticated Platform Administrator remain valid for the pilot.
## Journey result
| Accepted journey | Current product entry | Result | Owning frontier |
| --- | --- | --- | --- |
| Platform Administrator signs in | No platform login/session/guard; only a stored `PlatformRoleAssignment` type | **Missing** | tickets 42, 45 |
| Platform Administrator creates and operates Organizations | Direct Prisma calls in test fixtures only; no `/api/platform/*` or UI | **Test/database only** | ticket 45 |
| Platform Administrator configures each customer Feishu app | One env `FEISHU_APP_ID/SECRET/BOT_OPEN_ID`; no connection or secret model | **Missing and contract-divergent** | tickets 4345 |
| Platform Administrator supplies a distinct platform-managed provider key/base URL | One process-global `ANTHROPIC_AUTH_TOKEN/BASE_URL`; resolver ignores Project scope | **Missing and contract-divergent** | tickets 43, 45 |
| Platform Administrator applies and reverses `DRAIN`/`STOP_NOW` | No persistence, guard, API or UI | **Missing** | tickets 37, 45 |
| Organization user logs in through that Organization's Feishu app | OAuth exists, but is built once from the global app and has no Organization/application issuer | **Single-app only** | tickets 43, 44 |
| Logged-in Organization OWNER/ADMIN opens the management panel | OAuth redirects to `/admin/org/:slug`; no handler/static application serves it | **Stable 404** | ticket 46 |
| Organization OWNER/ADMIN manages members, Teams, Projects/Folders and Team access | Guarded Organization JSON APIs and useful integration coverage exist | **Backend present; browser product absent** | ticket 46 |
| Organization OWNER/ADMIN manages permission/model/role/run policy | Only `membersCanCreateProjects` is exposed; other stored/runtime settings have no control plane | **Partial** | ticket 46 |
| Organization selects Provider Credential Mode and manages BYOK | No mode/connection model, API or UI | **Missing** | tickets 43, 46 |
| Organization views sessions and usage | Guarded JSON APIs exist, but have no direct product-boundary tests or UI and cannot attribute Provider Connection | **Partial** | tickets 36, 46 |
| Ordinary member creates or binds a Project in Feishu | Card/service path is present and well tested | **Works for the one-global-app case** | ticket 44 for SaaS routing |
| Authorized member runs the agent through its Organization bot | Full single-app trigger path exists; provider/app resolution is global | **Works only as a single-app deployment** | tickets 43, 44 plus lifecycle tickets |
| Accepted work queues fairly and durably across Organizations | Five-item process-memory Project queue | **Contract-divergent** | tickets 23, 3235 |
| Organization sets lower policy limits and receives soft alerts | No policy/alert persistence or product surface | **Missing** | tickets 32, 3436, 46 |
## Executed defect evidence
The probes used the real Fastify plugin and test PostgreSQL database through a
temporary Vitest integration file. The file was removed after capture and was
run twice to separate deterministic behavior from a transient test failure.
### 1. The successful-login destination is not served
The probe seeded an active Organization ADMIN, minted the normal signed session
cookie, and requested the exact destination selected by
`resolvePostLoginRedirect`:
```text
GET /admin/org/test-default
expected_status=200
actual_status=404
```
Both runs returned 404. The root cause is structural: `registerAdminPlugin`
mounts cookie, auth and Organization API routes only; its own comment says the
static SPA is “later”. No other repository HTML/TSX/Vue/Svelte/CSS product
application or deployment-level static handler exists. Existing OAuth tests
assert only the redirect `Location`, so they stop immediately before the broken
journey.
This is not an authorization defect in the JSON APIs. Those APIs correctly
return 401 without a session and 403 to a MEMBER. It is a reachability defect:
there is no management panel for either an authorized or unauthorized request
to reach. Ticket 46 must guard both the page shell and its data, so an anonymous
user may reach only the login entry and never management content.
### 2. Different Organizations start OAuth with the same application
The probe seeded two Organizations and requested the current OAuth entry with
two different Organization return paths:
```text
GET /auth/feishu?returnTo=/admin/org/test-default
client_id=cli_process_global
GET /auth/feishu?returnTo=/admin/org/second
client_id=cli_process_global
```
Both runs produced the same result. The root cause is that
`registerAuthRoutes` constructs one immutable OAuth configuration from plugin
startup arguments. The `returnTo` path is signed only as a destination; it does
not resolve or bind an Organization Feishu Application. `server.ts` obtains the
same app credentials from one environment tuple and also uses them for the only
WebSocket listener.
Changing only the redirect page cannot fix this. The composition root and every
identifier-bearing record/action need an explicit Organization connection
identity, backed by the encrypted resolver in ticket 43.
## Feishu identifier and transport consequence
Feishu's official identifier contract makes the current global identifier
shape unsafe for customer-owned applications:
- `open_id` identifies a user inside one application and differs across apps;
- `user_id` is stable inside one tenant but differs across tenants;
- `union_id` can link apps from the same application service provider, but the
service cannot assume independently customer-owned apps share that provider;
- OAuth authorization identifies the app with `client_id`; the callback brings
`code` and `state`, so state and token exchange must remain bound to the
selected Organization/application;
- v2 HTTP event/card headers expose application and tenant identity, while an
encrypted outer request must be routed to the correct connection before its
verification/decryption key is known; a long connection is itself created by
one app credential pair, so the local client/listener identity is the issuer;
- Feishu rejects cross-tenant chat/operator combinations and does not provide a
contract that permits `chat_id` to be treated as a global SaaS key.
The current `User.feishuOpenId @unique`, session `{userId, feishuOpenId}`,
`ProjectGroupBinding.chatId`, `FeishuEventReceipt.eventId`, principal IDs,
sender caches, and batching keys omit this issuer namespace. An `open_id`
collision across customer apps could merge unrelated external identities;
different `open_id` values for one person across apps cannot be resolved as one
global identity automatically. A globally looked-up `chatId` or event ID has
the analogous ambiguity.
The implementation must therefore persist provider-local external identities
under a Feishu Application Connection, for example conceptually
`(connectionId, externalIdType, externalId)`, and carry that connection through
OAuth state/session, ingress, deduplication, chat binding, authorization,
onboarding/card actions, rate-limit keys, directory sync, outbound delivery and
audit. Whether two external identities represent the same global `User` is a
separate explicit linking decision; it must not be inferred from a provider ID
outside its documented scope.
## Root causes rather than symptoms
### 1. Tenant data was added without tenant-scoping external connections
Organization ownership is correctly present on Projects, Teams, Folders and
directory-connection metadata, but the process composition still comes from
the former single-deployment shape. The app/bot/provider credentials and the
identifiers they issue never crossed an Organization connection boundary.
`RuntimeScope.projectId` exists but is discarded by the env resolver, making
the missing seam especially explicit.
### 2. The control-plane work stopped at guarded Organization APIs
The Organization routes provide a credible backend slice, but there is no web
application, platform route family, platform guard, Organization provisioning
service, connection configuration, or browser journey test. The comment
“until admin SPA lands” and redirect-only OAuth assertion document the precise
unfinished handoff. A small catch-all page would only hide the gap; the fix must
deliver the authenticated, authorized workflows and their safe states.
### 3. Production capacity semantics exist only in the contract
ADR-0022 intentionally declares the in-memory queue and global-only settings to
be implementation divergences. There is no common admission identity that can
join Organization, Feishu connection, Project, user and request, so fair
scheduling, workload brakes, rate limits, alerts and control-plane operations
cannot currently compose. The previously created capacity tickets own this
implementation; the admin tickets must expose their safe operator/customer
surfaces rather than invent parallel controls.
## What should remain
- Keep `requireOrgRole`'s per-request session, active Organization, active
membership and OWNER/ADMIN checks; extend the same principle to page data and
add a separate `requirePlatformAdmin` rather than an Organization override.
- Keep the Organization-scoped explorer/member/Team/access/session/usage APIs
as the initial backend for the org panel, while filling their missing tests,
pagination/limits, audit and typed-error contracts through existing tickets.
- Keep shared Project creation and chat-binding services so web and Feishu
entries do not drift into two rule sets.
- Keep the single-app Feishu handler's authorization, onboarding, run/session
and dedup behavior, but call it from a connection-bound listener context and
move accepted work/delivery into the durable lifecycle tickets.
- Keep missing provider cost unknown rather than fabricating zero; add Provider
Connection attribution before presenting SaaS usage claims.
## Newly clear execution frontier
The product audit creates four implementation/decision slices after the
existing secret and browser-boundary design work:
1. [Decide the platform-administrator identity and audit boundary](../issues/42-decide-platform-admin-identity-audit.md).
2. [Implement the Organization connection and secret plane](../issues/43-implement-org-connection-secret-plane.md).
3. [Route Feishu identity and traffic by Organization connection](../issues/44-route-feishu-by-org-connection.md).
4. [Build the private platform-administrator control plane](../issues/45-build-private-platform-admin-control-plane.md).
5. [Build the private Organization-administrator control plane](../issues/46-build-private-org-admin-control-plane.md).
The Feishu routing slice precedes durable inbound/action/outbound and signed
traffic limiting so those mechanisms use the right issuer namespace from their
first durable schema. The platform panel precedes the emergency-brake product
entry so ticket 37 can reuse one authenticated/audited operator boundary.
Existing tickets 3238 continue to own capacity enforcement, attribution,
alerts, brakes and calibration; the new UI tickets do not duplicate them.
## Required production journey evidence
The final release gate must run against at least two configured customer-owned
Feishu applications and two Organizations, using deliberately colliding fake
provider-local IDs where the test harness permits. It must prove:
1. an unauthenticated request and an authenticated wrong-role user cannot load
either management page data or perform mutations;
2. a Platform Administrator can create an Organization, bootstrap its OWNER,
configure/test its Feishu app and distinct platform-managed Provider
Connection, with redacted responses and complete audit history;
3. an Organization OWNER/ADMIN logs in through that Organization's app, reaches
the real org panel, manages the accepted settings/resources, configures BYOK
when selected, and cannot read another Organization or platform secret;
4. each bot receives, binds, authorizes, queues and replies only inside its
connection's Organization even when `open_id`, `chat_id`, event or callback
values collide; SUSPENDED/ARCHIVED Organizations fail closed;
5. provider resolution for every run returns that Organization's selected
connection and never a process-global fallback;
6. current member onboarding, Project authorization and Agent Run behavior
remain intact after connection namespacing;
7. layered limits, fair admission, usage alerts and `DRAIN`/`STOP_NOW` are
visible only to the corresponding authorized control plane and behave as
proven by their dedicated fault/capacity suites;
8. the complete browser + Feishu journey survives the production-shaped deploy,
restart, backup/restore and rollback evidence owned by ticket 08.
@@ -0,0 +1,333 @@
# Feishu multi-app identity and event-routing primary sources
This note answers one narrow design question for the production SaaS audit:
what identity and routing boundaries apply when **every Hub Organization uses
its own Feishu App and bot**. External claims below use only first-party Feishu
Open Platform documentation. Statements labelled **Derived requirement** are
Hub design implications, not guarantees made by Feishu.
Research date: 2026-07-10.
## Executive answer
1. A customer-owned enterprise custom app is a single-tenant app: Feishu says
it is developed, reviewed, published, and used inside one tenant and cannot
be used by another organization. That fits “one Organization, one Feishu
App,” but it means the Hub is operating many independent app security and
identity domains, not one shared Feishu identity domain.
([custom apps versus store apps](https://open.feishu.cn/document/home/app-types-introduction/self-built-apps-and-store-apps))
2. A bare `open_id` is **not** a SaaS-global user key. It identifies a user
inside one app, and the same user has different Open IDs in different apps.
`user_id` is tenant-scoped; it is stable across apps in one tenant but
differs across tenants. `union_id` crosses apps only for apps from the same
application provider/developer; it differs between providers.
([user resource overview](https://open.feishu.cn/document/server-docs/contact-v3/user/field-overview?lang=zh-CN),
[official ID parameter definitions](https://open.feishu.cn/document/server-docs/contact-v3/department/search?lang=zh-CN),
[Union ID guide](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-union-id))
3. OAuth must be routed to the Organization's app **before** authorization.
The authorization request contains that app's App ID as `client_id`; the
callback contains `code` and the echoed `state`, not an independently
trustworthy Organization choice. Exchanging the code requires the matching
app's `client_id` and `client_secret`, and the same `redirect_uri`.
([obtain authorization code](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code),
[obtain user access token](https://open.feishu.cn/document/authentication-management/access-token/get-user-access-token))
4. A version 2 event or callback carries `header.app_id` and
`header.tenant_key` after parsing/decryption. An encrypted HTTP delivery,
however, exposes only an `encrypt` envelope before decryption, so a shared
endpoint cannot safely choose among many apps' Encrypt Keys from the body.
([callback structure](https://open.feishu.cn/document/event-subscription-guide/callback-subscription/callback-overview),
[encrypted Webhook delivery](https://open.feishu.cn/document/event-subscription-guide/event-subscriptions/event-subscription-configure-/choose-a-subscription-mode/send-notifications-to-developers-server?lang=zh-CN))
5. A WebSocket/long-connection client is authenticated with one app's App ID
and App Secret. Therefore its local client instance is the pre-authenticated
source-app route. It is not a process-global “all customer apps” listener.
([long-connection event subscription](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case?lang=zh-CN))
6. Feishu documents `chat_id` as the conversation identifier, but the cited
documentation does not promise that a raw `chat_id` is globally unique
across every tenant and app. Chat APIs explicitly reject an operator and
chat from different tenants and often require the app bot to be in the
chat. A bot is represented by its application's `app_id`.
([common parameters](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology),
[get chat members](https://open.feishu.cn/document/server-docs/group/chat-member/get?lang=zh-CN),
[message resource](https://open.feishu.cn/document/server-docs/im-v1/message/intro))
## 1. Application and tenant boundary
### Official facts
- `app_id` is the immutable identifier generated by Feishu for an application.
`tenant_key` is the enterprise identifier; one `tenant_key` corresponds to
one enterprise in actual use.
([common parameters](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology))
- An enterprise custom app is restricted to one tenant. Its developers,
administrators, and users belong to that tenant, and another organization
cannot use it. A store app is the contrasting model that can be installed by
multiple tenants.
([custom apps versus store apps](https://open.feishu.cn/document/home/app-types-introduction/self-built-apps-and-store-apps))
- Access tokens tell Feishu which app/user is calling and which tenant's data
is being accessed. A `tenant_access_token` calls as the application and its
data range is determined by that application's permissions.
([access-token overview](https://open.feishu.cn/document/server-docs/api-call-guide/calling-process/get-access-token))
- A custom app obtains its `tenant_access_token` using that app's `app_id` and
`app_secret`.
([custom-app tenant access token](https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal?lang=zh-CN))
### Derived requirements
- Model a durable `FeishuAppConnection` (or equivalent) owned by exactly one
Hub Organization. At minimum it binds the internal connection ID,
`organization_id`, `app_id`, expected `tenant_key`, encrypted `app_secret`,
event/callback credentials, lifecycle state, and credential version.
- Do not select Feishu credentials from a process-global default. Every OAuth
exchange, token cache entry, OpenAPI call, Webhook verification, and
long-connection client must start from the Organization's connection.
- Pin the first verified `tenant_key` to the connection. Later login, event,
and callback data must match both the expected `app_id` and pinned
`tenant_key`; unknown or mismatched pairs must be rejected rather than
auto-attached to another Organization.
## 2. User identifier scopes
### Official scope matrix
| Identifier | Feishu's documented scope | Cross-app/tenant behavior | Availability caveat |
| --- | --- | --- | --- |
| `open_id` | User identity inside one application | The same user has a different `open_id` in different apps | The normal app-relative user identifier |
| `user_id` | User identity inside one tenant | Stable across all apps in the same tenant; different in tenant A and tenant B | Reading it is a sensitive field permission and is limited to custom apps in the cited user API |
| `union_id` | User identity across apps from the same application provider/developer | Same across that provider's apps; different across different providers | It does not make unrelated customer-owned apps one identity domain |
| `tenant_key` | Enterprise identity | One key corresponds to one enterprise | Returned by login and carried in events/callbacks |
Sources:
- Feishu's current user resource overview states that `user_id` is tenant
identity, `open_id` is app identity, and `union_id` is identity across apps
developed by the same application provider. It expressly says the same
user's Open ID differs between apps.
([user resource overview](https://open.feishu.cn/document/server-docs/contact-v3/user/field-overview?lang=zh-CN))
- Feishu's API parameter text is more explicit: the same user's User ID differs
between tenants but remains the same across apps within one tenant; the same
user's Union ID is the same under one developer and different under another.
([official ID parameter definitions](https://open.feishu.cn/document/server-docs/contact-v3/department/search?lang=zh-CN))
- Feishu describes Union ID as the identity linking one user across multiple
applications supplied by the same application service provider.
([Union ID guide](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-union-id))
- Feishu's common-error guidance says not to use an Open ID obtained from app A
in app B, and even a test app and its production app produce different Open
IDs for the same user. It also reports cross-tenant errors for both User ID
and Union ID use.
([common errors](https://open.feishu.cn/document/ukTMukTMukTM/ugjM14COyUjL4ITN?op_tracking=hc))
- The current login user-info API returns `open_id`, `union_id`, optional
sensitive `user_id`, and `tenant_key` from a `user_access_token`.
([get login user information](https://open.feishu.cn/document/server-docs/authentication-management/login-state-management/get?lang=zh-CN))
### Answer: can different customer Apps share one global `open_id` key?
**No.** This directly contradicts Feishu's documented Open ID semantics. Two
customer-owned custom apps are different applications, so the same person has
different Open IDs in them. Because those apps are independently owned custom
apps, the Hub also has no documented basis to assume they are applications of
one application provider and therefore no basis to auto-merge on `union_id`.
`user_id` cannot solve cross-customer identity either because it changes across
tenants.
Derived identity model:
- The safe external login key is `(feishu_app_connection_id, open_id)`; retain
`tenant_key`, `user_id`, and `union_id` as scoped attributes, not as an
unqualified SaaS-global primary key.
- If the product needs one Hub human account to participate in multiple
Organizations, represent each Feishu login as a separate external identity
attached through an explicit, separately verified account-linking flow. Do
not silently merge on Open ID, Union ID, email, phone, or display name.
- Do not make `user_id` mandatory for login unless every customer app is
required to obtain its sensitive field permission. `open_id` and
`tenant_key` are returned by the user-info flow without using `user_id` as
the login key.
- Replacing an Organization's Feishu App changes the application identity
domain and therefore its users' Open IDs. Treat app replacement as an
explicit connection migration/re-link operation, not an in-place secret
edit.
## 3. OAuth login must be app-routed
### Official facts
- The authorization URL requires `client_id`, which is the selected
application's App ID. `redirect_uri` must already appear in that
application's security settings. On success Feishu redirects with `code`
and, when supplied, the original `state`.
([obtain authorization code](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code))
- Feishu says `state` maintains request/callback context and must be compared
to prevent CSRF. Its official sample rejects a callback whose state differs
from the stored state.
([obtain authorization code](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code),
[user-access-token sample](https://open.feishu.cn/document/authentication-management/access-token/get-user-access-token))
- The code lasts five minutes and can be used once. Token exchange requires
`client_id`, `client_secret`, `code`, and, when used during authorization,
the same `redirect_uri`. Feishu defines error 20024 for a code/refresh token
that does not match the supplied client ID and error 20071 for a mismatched
redirect URI.
([obtain user access token](https://open.feishu.cn/document/authentication-management/access-token/get-user-access-token))
- Only redirect URLs configured for the selected application pass Feishu's
security check; an app may configure multiple URLs.
([access-token acquisition flow](https://open.feishu.cn/document/server-docs/api-call-guide/calling-process/get-access-token))
### Derived request flow
1. Begin at an Organization-qualified route or invitation, resolve exactly one
active `FeishuAppConnection`, and create a short-lived, one-time server-side
login transaction bound to that Organization, connection, redirect URI,
desired return path, and PKCE verifier if used.
2. Put only a high-entropy opaque transaction nonce in OAuth `state`; generate
the authorization URL with that connection's `app_id` as `client_id`.
3. On callback, atomically consume and validate `state` before selecting
credentials. The callback's untrusted query parameters must never be
allowed to choose another Organization or App.
4. Exchange `code` with the connection's `app_id`, decrypted `app_secret`, and
the exact stored redirect URI. Then call user-info and require its
`tenant_key` to equal the connection's pinned tenant.
5. Resolve or create the membership through
`(feishu_app_connection_id, open_id)`. Never search all Organizations for a
bare Open ID and accept the first match.
A single physical callback URL may be configured independently in multiple
apps, but a shared URL does not remove the need for state-bound app routing.
## 4. HTTP event and callback routing
### Official facts
- A version 2 callback header contains the application's Verification Token,
`event_type`, `tenant_key` (the callback application's tenant), and `app_id`
(the callback application's App ID).
([callback overview and structure](https://open.feishu.cn/document/event-subscription-guide/callback-subscription/callback-overview))
- Version 2 event examples likewise place `app_id` and `tenant_key` in the
event header.
([user-leaves-chat event](https://open.feishu.cn/document/server-docs/group/chat-member/event/deleted-2))
- Verification Token is an application validation identifier. Feishu says the
server can compare the event's token with that application's token to check
that a request is for the specified app. With an Encrypt Key, the request
signature is calculated from request timestamp, nonce, that app's Encrypt
Key, and the original request body.
([receive and verify events](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/encrypt-key-encryption-configuration-case))
- When an Encrypt Key is configured, the HTTP request body visible before
decryption is an envelope containing only `{"encrypt":"..."}`; `app_id`
and `tenant_key` are inside the ciphertext.
([send events to developer server](https://open.feishu.cn/document/event-subscription-guide/event-subscriptions/event-subscription-configure-/choose-a-subscription-mode/send-notifications-to-developers-server?lang=zh-CN))
### Derived routing and verification requirements
- Give every Feishu connection a stable, unguessable callback route such as a
connection-specific path token. Resolve the connection from that route,
load exactly its Encrypt Key/Verification Token, verify and decrypt, then
require the authenticated payload's `app_id` and `tenant_key` to match the
route's connection.
- For an unencrypted common endpoint, `header.app_id` may identify a candidate
connection, but it is untrusted until that connection's signature/token has
been verified. Never route the side effect solely from an unverified body.
- A single encrypted URL shared by every customer app has no documented
pre-decryption app selector. Trying every tenant's secret is not an identity
protocol. Use connection-specific routing instead.
- Namespace delivery deduplication by the authenticated connection (for
example `(connection_id, event_id)`), and dispatch business work only after
app/tenant verification. Unknown, disabled, or mismatched connections fail
closed and produce an auditable security log.
## 5. Long-connection routing
### Official facts
- Feishu's SDK creates a WebSocket client with one app's required `APP_ID` and
`APP_SECRET`. Events subscribed by that application are then delivered over
that channel. Authentication is performed when the connection is
established.
([long-connection event subscription](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case?lang=zh-CN))
- Long-connection event subscription supports enterprise custom apps, not
store apps. Each app may establish at most 50 connections. If one app has
multiple clients, delivery is cluster-mode rather than broadcast: one random
client receives a message.
([long-connection event subscription](https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case?lang=zh-CN))
- Long-connection callbacks use the same per-app App ID/App Secret client
construction and have the same cluster-mode single-recipient behavior.
([long-connection callback subscription](https://open.feishu.cn/document/event-subscription-guide/callback-subscription/step-1-choose-a-subscription-mode/configure-callback-request-address?lang=zh-CN))
### Derived routing requirements
- Create one logical client binding per enabled `FeishuAppConnection`, even if
an in-process supervisor manages many clients. Attach immutable
`organization_id` and `connection_id` context to each handler; the client
that received the frame is the pre-authenticated application route.
- Never infer the receiving Organization by looking up a bare actor Open ID or
chat ID. First use the authenticated client binding; then cross-check any
delivered `app_id`/`tenant_key` fields available in the event structure.
- Scaling several replicas for the same app does not create duplicate
broadcast processing according to Feishu's contract, but delivery still
requires application-level idempotency and health ownership. Capacity must
count clients per app against the documented 50-connection ceiling.
## 6. Bot, chat, and message identifiers
### Official facts
- In the message resource, a sender can be a user or an application. For an
application sender, `id_type` is `app_id`; the sender also carries a
`tenant_key`. Thus the Feishu bot identity used by messaging is its
application identity, not a Hub-global bot ID.
([message management overview](https://open.feishu.cn/document/server-docs/im-v1/message/intro))
- `chat_id` identifies a conversation, including direct and group chats.
Feishu's group resource also exposes the chat's `tenant_key`.
([common parameters](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology),
[group management overview](https://open.feishu.cn/document/server-docs/group/chat/intro?lang=zh-CN))
- Chat APIs require the operator to be in the same tenant as an internal chat;
common errors include `Operator and chat can NOT be in different tenants`.
Many operations additionally require the calling app's bot to be in the
group and reject an app that is unavailable in that tenant.
([get chat members](https://open.feishu.cn/document/server-docs/group/chat-member/get?lang=zh-CN))
- The first bot direct-chat event calls `chat_id` the conversation ID between
**that robot and user** and includes `app_id`, `tenant_key`, and app-scoped
user Open IDs in the same event.
([user and bot first conversation](https://open.feishu.cn/document/server-docs/group/chat-member/event/bot-events?lang=zh-CN))
### What the official sources do not establish
The cited documentation calls `chat_id` a conversation/group identifier but
does not state that its value is globally unique across every Feishu tenant and
every independently created app. It does establish tenant and bot-membership
authorization boundaries. Under the repository rule that unspecified scope
must not be assumed, raw global uniqueness would be an unsupported inference.
Derived storage/call rules:
- Store chats as `(feishu_app_connection_id, chat_id)` and retain observed
`tenant_key` and chat type. Do the same for other resource identifiers unless
Feishu explicitly contracts a wider scope.
- Always call chat/message APIs with the token obtained from the chat's bound
connection. Never retry an authorization failure with another Organization's
token.
- Treat bot direct chats as connection-specific. A `chat_id` for user X's
conversation with Organization A's bot is not evidence of a conversation
with Organization B's bot.
- For group chats, two customer apps may both be members of a real Feishu
group, but that does not authorize the Hub to move configuration, Projects,
or memberships between Organizations. The authenticated connection remains
the tenant route and Hub Organization remains the authorization root.
## Minimum invariants for implementation review
1. `FeishuIdentity` uniqueness is `(connection_id, open_id)`, never bare
`open_id`.
2. OAuth `state` resolves one server-side transaction bound to one Organization
and connection; code exchange uses only that connection's credentials and
stored redirect URI.
3. Login accepts the returned identity only when `tenant_key` matches the
connection's pinned tenant.
4. HTTP Webhooks choose a connection-specific verification/decryption context
before trusting payload routing fields; authenticated `app_id` and
`tenant_key` must match it.
5. Every long-connection handler carries the local connection/Organization
binding of the App ID/App Secret client that received the frame.
6. Bot, chat, event-deduplication, token-cache, and user identifiers are all
namespaced by the Feishu connection unless Feishu explicitly guarantees a
wider scope.
7. A credential, app, or tenant mismatch is a hard failure with traceable
logging; there is no fallback to a default app or search across tenant
secrets.
@@ -1,7 +1,7 @@
# Audit the accepted product surface for production completeness # Audit the accepted product surface for production completeness
Type: research Type: research
Status: open Status: resolved
## Question ## Question
@@ -11,3 +11,48 @@ policy-limit, emergency workload-control, and Feishu flows are absent,
unreachable, or only test-facing rather than usable in the initial production unreachable, or only test-facing rather than usable in the initial production
service? Treat ADR-0022's capacity control surfaces as part of the accepted service? Treat ADR-0022's capacity control surfaces as part of the accepted
product boundary. product boundary.
## Answer
The current Hub is not a usable or production-complete SaaS product surface.
Its Organization-scoped JSON APIs and single-Feishu-app Agent Run path are
worth retaining, but the running service has one process-global Feishu
app/bot, one global provider key/base URL, no Organization connection/secret
plane, no platform-administrator control plane, and no real Organization or
platform management UI. OAuth redirects a successful login to an unmounted
`/admin/org/:orgSlug` route.
Two real Fastify integration probes, each run twice, proved that an authenticated
Organization ADMIN receives 404 at the post-login destination and two distinct
Organization return paths produce the same OAuth `client_id`. Official Feishu
documentation also proves that `open_id` is application-scoped, `user_id` is
tenant-scoped, and `union_id` only links applications from the same application
provider. The current globally unique `User.feishuOpenId`, raw `chatId`, event
identity, OAuth state and one-listener composition therefore cannot safely
route independently customer-owned applications.
ADR-0022 remains an additional release blocker: beyond a process-memory
Project queue and global max-turn setting, layered limits, durable
Organization-fair admission, hard storage/run/process budgets, alerts and
audited emergency brakes have no complete persistence or product surface.
The full verdict, deterministic probe results, root causes, retained seams and
required end-to-end release evidence are in the
[accepted SaaS product-surface completeness audit](../assets/product-surface-completeness-audit.md).
The detailed route/schema/test matrix is in the
[accepted product-surface code inventory](../assets/product-surface-code-inventory.md),
and the Feishu identifier/OAuth/event/long-connection facts are in the
[Feishu product-surface primary sources](../assets/product-surface-feishu-primary-sources.md).
The newly-clear execution frontier is:
- [Decide the platform-administrator identity and audit boundary](42-decide-platform-admin-identity-audit.md)
- [Implement the Organization connection and secret plane](43-implement-org-connection-secret-plane.md)
- [Route Feishu identity and traffic by Organization connection](44-route-feishu-by-org-connection.md)
- [Build the private platform-administrator control plane](45-build-private-platform-admin-control-plane.md)
- [Build the private Organization-administrator control plane](46-build-private-org-admin-control-plane.md)
Existing tickets 23 and 3238 remain the owners of durable ingress/admission,
capacity enforcement, usage alerts, workload brakes and calibration. Ticket 08
now requires all of these product slices before claiming the production release
gate.
@@ -2,7 +2,7 @@
Type: task Type: task
Status: open Status: open
Blocked by: 01, 02, 03, 04, 05, 06, 07, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41 Blocked by: 01, 02, 03, 04, 05, 06, 07, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46
## Question ## Question
@@ -2,6 +2,7 @@
Type: task Type: task
Status: open Status: open
Blocked by: 44
## Question ## Question
@@ -2,7 +2,7 @@
Type: task Type: task
Status: open Status: open
Blocked by: 18 Blocked by: 18, 44
## Question ## Question
@@ -2,6 +2,7 @@
Type: task Type: task
Status: open Status: open
Blocked by: 44
## Question ## Question
@@ -2,7 +2,7 @@
Type: task Type: task
Status: open Status: open
Blocked by: 20, 22 Blocked by: 20, 22, 44
## Question ## Question
@@ -2,7 +2,7 @@
Type: task Type: task
Status: open Status: open
Blocked by: 19, 24, 29, 32 Blocked by: 19, 24, 29, 32, 43
## Question ## Question
@@ -2,7 +2,7 @@
Type: task Type: task
Status: open Status: open
Blocked by: 22, 23, 24, 27, 30, 32, 33 Blocked by: 22, 23, 24, 27, 30, 32, 33, 45
## Question ## Question
@@ -2,7 +2,7 @@
Type: task Type: task
Status: open Status: open
Blocked by: 10, 12, 14, 19, 23, 24, 25, 26, 29, 39, 41 Blocked by: 10, 12, 14, 19, 23, 24, 25, 26, 29, 39, 41, 43
## Question ## Question
@@ -0,0 +1,13 @@
# Decide the platform-administrator identity and audit boundary
Type: grilling
Status: open
## Question
Which identity issuer, bootstrap and recovery procedure, invite/allowlist model,
session claims, role lifecycle, separation from Organization membership, and
append-only audit contract will authenticate Platform Administrators for the
initial service? The decision must keep `/admin/platform` private, make every
Organization/connection/workload-control mutation attributable, and define a
reviewable break-glass path without treating platform staff as customer members.
@@ -0,0 +1,16 @@
# Implement the Organization connection and secret plane
Type: task
Status: open
Blocked by: 19
## Question
Implement encrypted, versioned, Organization-scoped Feishu Application and
model Provider Connection records plus one resolver boundary. Support one
customer-owned Feishu app per Organization, Organization-admin-managed BYOK,
and one distinct platform-managed key/base URL per Organization; enforce which
administrator may write each mode, readiness and rotation lifecycle, runtime
resolution from Organization/Project, redacted observability, backup/key
recovery hooks, and no process-global credential fallback or plaintext leakage
into business rows, logs, HTTP responses, or agent environments.
@@ -0,0 +1,17 @@
# Route Feishu identity and traffic by Organization connection
Type: task
Status: open
Blocked by: 20, 22, 43
## Question
Make every Feishu OAuth, WebSocket/event, card action, directory sync, chat
binding, API call, and outbound message start from an explicit active
Organization Feishu Application Connection. Model external user, chat, event,
and callback identifiers in that connection's namespace; bind OAuth state to
the intended Organization/application; run and observe one listener/client
lifecycle per configured app; and prove two customer-owned apps with colliding
provider-local identifiers cannot authenticate, bind, authorize, deduplicate,
rate-limit, or deliver across Organizations. Do not infer tenant from a global
`open_id`, `chat_id`, or from the user's membership count.
@@ -0,0 +1,17 @@
# Build the private platform-administrator control plane
Type: task
Status: open
Blocked by: 20, 27, 28, 42, 43, 44
## Question
Implement authenticated `requirePlatformAdmin` APIs and a private
`/admin/platform` web area for Platform Administrators to create/list/update
Organizations, bootstrap their OWNER, view/change lifecycle state, configure
and test each customer-owned Feishu Application, configure the Organization's
distinct platform-managed Provider Connection, and manage platform-admin
allowlisting. Prove anonymous users, ordinary members, and Organization admins
cannot load either page data or mutations; every sensitive action is validated,
redacted, audited, observable, and covered by browser-level critical-journey
tests. Emergency workload controls remain integrated through ticket 37.
@@ -0,0 +1,17 @@
# Build the private Organization-administrator control plane
Type: task
Status: open
Blocked by: 20, 27, 28, 32, 36, 43, 44
## Question
Turn the guarded Organization JSON APIs into a usable private
`/admin/org/:orgSlug` web control plane for OWNER/ADMIN users. Cover members and
roles, teams and Project access, Folders/Projects/chat unbinding, sessions and
usage, model/role/run policy, Organization policy limits and alerts, Feishu
connection readiness, Provider Credential Mode selection, and BYOK key/base URL
management without ever displaying stored plaintext. Preserve server-side
authorization on every request, define safe loading/error/empty states, and
prove login redirect plus browser journeys, cross-Organization denial, MEMBER
denial, secret redaction, and logout/session expiry end to end.
@@ -41,6 +41,7 @@ rollback and recovery, and no known critical security or data-integrity gaps.
- [Define production observability and incident recovery](issues/04-define-observability-recovery.md) — keep the existing Fastify/Prisma/systemd signal fragments, but production requires dependency-aware readiness, typed and correlated errors, durable-work telemetry, an explicit SLO/retention/alerting control plane, and recovery runbooks proven by fault drills. - [Define production observability and incident recovery](issues/04-define-observability-recovery.md) — keep the existing Fastify/Prisma/systemd signal fragments, but production requires dependency-aware readiness, typed and correlated errors, durable-work telemetry, an explicit SLO/retention/alerting control plane, and recovery runbooks proven by fault drills.
- [Define initial abuse and capacity controls](issues/05-define-abuse-capacity-controls.md) — require layered fail-closed limits, durable Organization-fair admission, explicit backpressure, bounded files/storage/runs, soft usage alerts, and audited emergency workload brakes; calibrate numerical ceilings under production-like load. - [Define initial abuse and capacity controls](issues/05-define-abuse-capacity-controls.md) — require layered fail-closed limits, durable Organization-fair admission, explicit backpressure, bounded files/storage/runs, soft usage alerts, and audited emergency workload brakes; calibrate numerical ceilings under production-like load.
- [Audit backup, migration, and disaster recovery safety](issues/06-audit-backup-migration-recovery.md) — separate persistent state from releases, define a database/workspace checkpoint and recovery objectives, make migrations compatibility-gated, and prove complete off-host backup sets through isolated restore and cutover drills. - [Audit backup, migration, and disaster recovery safety](issues/06-audit-backup-migration-recovery.md) — separate persistent state from releases, define a database/workspace checkpoint and recovery objectives, make migrations compatibility-gated, and prove complete off-host backup sets through isolated restore and cutover drills.
- [Audit the accepted product surface for production completeness](issues/07-audit-product-surface-completeness.md) — retain the guarded Organization APIs and single-app Feishu vertical slice, but require explicit Organization-scoped Feishu/provider connections and external-identity namespaces, private platform and Organization admin control planes, and the full ADR-0022 capacity surface before release.
## Fog ## Fog