完善企微开通与部署端口契约(07-26-internal-ops-recovery-baseline)

这个提交包含在:
2026-07-26 23:44:44 +08:00
父节点 9a624700d0
当前提交 130518f471
共修改 21 个文件,包含 1350 行新增和 55 行删除
@@ -517,6 +517,81 @@ bash scripts/qa/run-local-candidate.sh full
# Only this successful branch emits LOCAL_CANDIDATE_PASS.
```
## Scenario: Compose-Resolved Loopback Host Port
### 1. Scope / Trigger
- Trigger: changing Compose port publication, deployment health checks, Caddy
routing or operator commands for the server container.
- Purpose: support a non-default host port without changing the container's
fixed application port or accidentally checking a stale service.
### 2. Signatures
- Env: `APP_HOST_PORT=<1-65535>`; default `3000`.
- Compose mapping: `127.0.0.1:${APP_HOST_PORT:-3000}:3000`.
- Runtime resolution: `docker compose port server 3000` or the compatible
`docker-compose` command selected by the deployment script.
- Backend container still listens on `PORT=3000`.
### 3. Contracts
- Publication remains loopback-only. `APP_HOST_PORT` changes only the host side;
Caddy points to the same resolved host port.
- Deployment health checks must parse the running Compose mapping after startup;
do not hardcode 3000 and do not assume `.env` variables entered the current
shell merely because Compose read `--env-file`.
- The parsed port must be numeric before it is used in curl. A missing,
multi-line or non-numeric mapping stops deployment before readiness success.
- `server`, `worker` and `init` retain the shared env-file contract; only server
publishes an HTTP host port.
- Changing the real host port or Caddy target is production configuration and
requires separate approval plus rollback evidence.
### 4. Validation & Error Matrix
- Missing `APP_HOST_PORT` -> Compose uses loopback port 3000.
- Valid alternate port -> Compose publishes it and deployment checks that port.
- Non-numeric/unresolved `compose port` output -> deployment exits non-zero.
- Caddy target differs from Compose mapping -> deployment is not accepted even
if one local `/ready` endpoint answers.
- Host port already occupied -> Compose startup fails; do not start a second
deployment on a guessed port.
### 5. Good/Base/Bad Cases
- Good: `.env` selects an alternate loopback port, Compose reports it, deploy
checks the matching build SHA there and Caddy targets the same port.
- Base: default port 3000 remains compatible without extra configuration.
- Bad: Compose publishes 3001 while deploy/runbook curls 3000 and reports an old
container as the new release.
### 6. Tests Required
- `bash -n deploy/trialrun/deploy.sh deploy/trialrun/setup-server.sh`.
- `node deploy/trialrun/validate-env-contract.js` must assert the exact
loopback-only variable mapping and env example key.
- Review runbook/checklist commands for Compose resolution rather than shell
expansion of an unloaded `.env`.
- Public deployment additionally proves Caddy, `/ready` and frontend marker use
the approved build on the same resolved port.
### 7. Wrong vs Correct
#### Wrong
```bash
curl -fsS http://127.0.0.1:3000/ready
```
#### Correct
```bash
SERVER_HOST_PORT="$(docker compose port server 3000)"
SERVER_HOST_PORT="${SERVER_HOST_PORT##*:}"
curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
```
## Scenario: Paired Backup And Shadow-Only Restore
### 1. Scope / Trigger
+90
查看文件
@@ -142,3 +142,93 @@ WeCom leave(category=调休) -> WeCom approval -> OA minimum read-only history
- Full-stack Playwright coverage that writes local data still requires a running
local backend/API environment; a Vite-only run is not a valid failure signal
for those real-chain tests.
## Scenario: One-Time Private-Info OAuth Onboarding
### 1. Scope / Trigger
- Trigger: obtaining phone/email for an app-visible member or allowing an
unbound active member to create the minimum OA account.
- This is a one-time onboarding path, not the ordinary WeCom login path and not
a replacement for administrator-assigned finance, CEO or admin roles.
### 2. Signatures
- Entry: `GET /api/auth/wecom/login?mode=onboard&returnTo=<internal-path>`.
- Ordinary entry: the same route with `mode=login|bind`.
- Provider chain: `snsapi_privateinfo` -> `/cgi-bin/auth/getuserinfo` ->
`/cgi-bin/auth/getuserdetail` with `{ "user_ticket": "<short-lived>" }`.
- Config: `WECOM_OAUTH_ONBOARDING_ENABLED=false` by default.
- Directory chain: `WECOM_CONTACT_SECRET` only for `/user/list_id`;
`WECOM_AGENT_SECRET` for `/user/get`, `/department/list` and
`/hr/get_staff_info`.
### 3. Contracts
- `login|bind` always keeps `snsapi_base`; only explicit `onboard` uses
`snsapi_privateinfo`.
- The member must be visible to the app, active, return the same userid from
both identity calls, and authorize a valid phone plus personal email or
business email. A blank/invalid personal email may fall back to a valid
business email.
- `user_ticket`, OAuth code, access token and raw provider responses exist only
in request memory. Do not persist them or include them in Cookie, audit,
response details or logs; provider/network failures use fixed safe codes.
- Resolve userid, phone and email conflicts before writing. A member already
bound to another employee or an employee bound to another member is rejected.
- New accounts are created transactionally with employee, exactly the
`employee` role, identity binding and dehydrated audit. Existing active
accounts may complete normalized profile data and binding but keep all roles.
- The feature flag controls the entry and callback provisioning path. Enabling
it and changing real app permissions are production configuration operations
requiring separate approval.
### 4. Validation & Error Matrix
- Flag disabled -> entry is `404`; no private scope or account write.
- Missing `user_ticket` -> safe `consent_required` redirect; no write.
- Provider/network failure -> `wecom_api_failed` or safe auth error; no raw
payload, token or ticket in output/logs.
- Userid mismatch -> `identity_mismatch`; inactive member -> `member_inactive`.
- Missing/invalid phone, both emails invalid, or unresolved department ->
`missing_required_field`.
- Multiple employees, unique-field race or cross-binding -> `employee_conflict`.
- Department binding points to missing/archived data -> `department_invalid`;
multiple same-name OA departments -> `department_ambiguous`.
### 5. Good/Base/Bad Cases
- Good: an unbound active member explicitly consents, receives an employee-only
account and authenticated session, then an administrator separately assigns
any business role with existing audit/session-revocation semantics.
- Base: a bound active employee repeats onboarding to complete normalized
profile data; no role is added or removed.
- Bad: change ordinary login to `snsapi_privateinfo`, use batch `/user/get`
absence as proof that email is unavailable, or auto-grant `finance`/`ceo`.
### 6. Tests Required
- Route tests for default-off, scope separation, missing ticket, safe provider
failure, successful session and employee-only provisioning.
- Service tests for active state, userid match, personal-to-business email
fallback, invalid fields, multiple matches, cross-binding and existing-role
preservation.
- Assertions must prove ticket, token, raw payload, phone and email are absent
from audit/error serialization; run full backend tests and Prisma validation.
### 7. Wrong vs Correct
#### Wrong
```text
ordinary login -> snsapi_privateinfo -> batch sync guesses missing fields
-> auto-grant finance/ceo
```
#### Correct
```text
ordinary login -> snsapi_base
one-time onboard -> explicit private consent -> safe unique employee-only write
business role assignment -> separate administrator-audited operation
```