35 KiB
35 KiB
Enterprise WeChat Boundary
Ownership
Use an Enterprise-WeChat-first, OA-complements model for this single-company, 5–10-person internal system:
- Enterprise WeChat owns mature commodity capabilities: workbench entry, OAuth/SSO, directory and basic organization, application messages, HR/attendance actions and low-risk, simple-field, mobile-first approvals.
- General administration and simple procurement with no downstream financial handling, sensitive attachment or cross-business ledger may use Enterprise WeChat for submission and approval after a dedicated task verifies the real template and approval chain. OA retains only a minimum reference when needed.
- OA owns the complete reimbursement journey: request, invoice upload/PDF/OCR and duplicate detection, approval, finance review, payment status and receipt. Enterprise WeChat provides SSO/workbench entry, dehydrated notifications and exact deeplinks into the relevant OA page; users must not re-enter or chase the same reimbursement across both systems.
- OA also owns financial ledgers, payment/AP and maker-checker, contract bodies, sensitive attachments, cross-business relationships and long-term audit.
This boundary is a selection rule, not evidence that any existing procurement or administration workflow has already migrated.
Rules
- Enterprise WeChat API checks that need trusted IP must run from the deployment server, not local.
- Do not write Enterprise WeChat, create templates, create approvals, expand real users, or enable live trials without explicit approval and a Trellis task.
- Approval-rule configuration cannot be fully read through official APIs; keep manual backend/real-device gates where specified.
- Messages to Enterprise WeChat must be dehydrated and link back to OA; do not send sensitive attachments or business documents into native WeCom approvals.
- Prefer a WeCom-native mobile form over duplicating it in OA when the process is simple and low-risk. Do not build full payload mirrors, bidirectional writeback, duplicate native approval UIs or generic approval-rule operations unless a later task proves a concrete business and audit need.
- For every WeCom approval promoted to formal use, manually verify the backend rule, applicant-visible mobile submission page and actual post-submission approval chain; API template metadata alone is insufficient.
Scenario: Native File Preview With Narrow Short-Lived Grants
1. Scope / Trigger
- Trigger: an authenticated OA page inside the Enterprise WeChat self-built app
must preview a reimbursement invoice or supporting file with the official
previewFileJSAPI. - Purpose: WeCom's native downloader does not reliably carry the WebView cookie or OA bearer header, while ordinary PDF links may download instead of preview. This is a narrow native-preview bridge, not a general query-token download design.
2. Signatures
- JS-SDK signature:
POST /api/auth/wecom/js-sdk-signature, authenticated body{ url: string }; response{ corpId, timestamp, nonceStr, signature }. - Invoice grant:
POST /api/invoice-pool/:id/signed-url; response includes{ signedUrl, expiresAt, originalName, mimeType, fileSize }. - Reimbursement attachment grant:
POST /api/reimbursements/:id/attachments/:attachmentId/preview-url, optional body{ reason }; response uses the same file metadata shape. - Reimbursement material bundle:
POST /api/reimbursements/:id/materials/preview-bundle, optional body{ reason }; response contains one short-lived merged PDF URL plus exactname,sizeandpageCount. - Native call:
previewFile({ url, name, size });nameincludes an extension andsizeis the persisted positive byte count.
3. Contracts
- Fetch and cache the WeCom JSAPI ticket only on the server. Never return or log the ticket, app access token, Agent Secret or raw provider response.
- Sign only the first configured OA
APP_ORIGIN; reject cross-origin URLs, URL credentials and fragments. The signed page URL excludes the hash. - A native file grant expires in at most 120 seconds and binds purpose, exact business object, exact file object and actor. It is signed only after current authentication, business-read permission and download-reason checks pass.
- Invoice access may reuse the existing current-pending-task exception. Do not grant the approver invoice-management, edit, unbind or delete permission.
- The native GET writes a successful access audit before sending headers or
bytes and returns
private, no-storeplusno-referrer. - PC keeps its browser/authenticated download behavior. Only a detected WeCom
WebView dynamically loads
@wecom/jssdkand callspreviewFile; do not send the actual file through a WeCom message or native approval payload. - Whole-reimbursement continuous preview derives one deterministic PDF: PDF
pages are copied and images are embedded as pages, then WeCom receives one
previewFilecall. The derivative is explicitly not an original or digital- signature fact source; individual originals remain separately authorized. - JS-SDK is allowed only on authenticated business pages that need a native
capability and is dynamically imported behind WeCom detection. The hosted PC
login/password/email-code page remains script-free as required by the
Hosted PC WeCom Web Loginscenario below.
4. Validation & Error Matrix
- Missing authentication on signature/grant issue ->
401. - Cross-origin page URL ->
403; malformed, credential-bearing or fragment URL ->400, before any provider call. - Ticket/token provider failure -> stable
503detail without provider values. - Missing file metadata, extension or positive integer size -> frontend stops
before
previewFileand shows a stable error. - Missing required download reason ->
400 { reason: "required" }. - Expired, tampered, wrong-purpose or object-mismatched file grant ->
403. - Access audit failure ->
5xxwith no file body. - Missing/invalid merged PDF URL, name or size -> frontend stops before the bridge call. Bundle expiry/cache loss -> safe fallback to individually authorized originals; do not broaden token scope or extend it silently.
5. Good/Base/Bad Cases
- Good: a pending reimbursement approver opens a 120-second invoice URL through
previewFile, sees the original in WeCom and receives no edit capability. - Base: a PC user opens the same invoice through the browser and downloads an attachment through the existing authenticated Blob path.
- Base: a WeCom approver requests one authorized reimbursement PDF bundle,
then
previewFileopens it for continuous native scrolling. - Bad: expose a long-lived generic
?token=, trust a caller-supplied size/name, put a provider ticket in the response, or use asyncwindow.open(blob:)in WeCom and assume desktop popup behavior applies.
6. Tests Required
- Route tests cover same-origin signing, provider failure/redaction and rejection of cross-origin, credential-bearing and fragment URLs before provider access.
- Grant tests assert exact object/actor binding, at-most-120-second expiry, metadata, reason gate, issue audit, access audit, tamper/expiry/mismatch denial and audit-failure fail-closed behavior using synthetic files only.
- Playwright injects a synthetic
WeixinJSBridge, assertspreviewFilereceives the exact URL/name/size for invoice and attachment, and asserts nowindow.opencall in WeCom. PC assertions keep browser URL and Blob behavior. - Continuous-preview Playwright asserts one
previewFilecall with exact URL, filename and byte size. Backend tests separately prove deterministic output, exact response size and object/actor/bundle binding. - Production deployment and real-device preview remain separate approvals.
7. Wrong vs Correct
Wrong
await downloadWithBearer();
window.open(URL.createObjectURL(blob)); // WeCom may block after await
Correct
authenticated grant issue -> <=120s object-bound URL
-> ww.previewFile({ url, nameWithExtension, exactSize })
-> audit before file bytes
authenticated material-bundle issue -> one <=120s deterministic PDF URL
-> ww.previewFile({ url, nameWithExtension, exactSize })
-> audit before exact cached PDF bytes
Scenario: WeCom Member Data Is A Read-Only Source For OA
1. Scope / Trigger
- Trigger: importing directory members, changing OA employee/profile or manager routes, enabling directory sync, or adding any WeCom member create/update/disable call.
- Purpose: keep WeCom as the member-account fact source and prevent an ordinary OA account-management action from unexpectedly changing the real WeCom directory.
2. Signatures
- Approved inbound CLI:
npm run import:wecom-directory-export -- --input=- --dry-run|--apply; apply retains its existing dual gate and writes OA only. - OA workflow manager field:
Employee.direct_manager_id; it is not the WeCom directory supervisor field. - Outbound safety gates:
WECOM_CONTACT_SYNC_ENABLED=falseand effective feature flaghr_wecom_directory_sync_v1=force_offfor formal internal operation. - Existing outbound API family
/cgi-bin/user/create|updateand disable payloads remain unavailable to ordinary OA user/manager maintenance.
3. Contracts
- A directory export or approved read API may create/update the minimum OA
employee mirror and
WecomIdentityBinding; it must not create, update or disable the WeCom member. - WeCom name, department, supervisor, phone, email and active state are edited by the member in an allowed WeCom surface or by a WeCom administrator. OA learns later changes through another approved inbound read/import.
- Setting
direct_manager_idchanges only OA workflow routing. It does not imply or trigger a WeCom supervisor update. - An OA account route may record a skipped directory-sync run while the outbound gate is disabled, but it must stop before token acquisition or an external member-write request. The skipped state is evidence of zero external writes.
- Any future reverse member sync requires a separate Trellis task, per-field side-effect design, dry-run, explicit production approval and rollback. It may not be introduced as an implicit side effect of an existing OA route.
4. Validation & Error Matrix
- Import input conflicts or is ambiguous -> block the OA write; never merge or repair the WeCom member automatically.
- OA manager/profile action with outbound gate disabled -> OA transaction may
succeed, outbound sync is
skipped, zero WeCom access-token/member-write calls. - Effective outbound flag unexpectedly enabled during ordinary internal operation -> deployment/operation finding; do not exercise the write path to diagnose it.
- A requested WeCom profile change -> direct the member or WeCom administrator to WeCom; do not patch the OA mirror and push it outward.
5. Good/Base/Bad Cases
- Good: administrator exports one existing member, OA imports one
employeeaccount and identity binding, then sets an OA-only manager while outbound sync remains skipped. - Base: a member later changes phone/email in WeCom; OA keeps the previous mirror until the next approved inbound refresh.
- Bad:
POST /api/users/batch/set-managersilently calls/cgi-bin/user/update, or OA profile editing becomes a back door for changing the WeCom directory.
6. Tests Required
- Directory-export tests prove stdin-only parsing, dual apply gate, employee-only role and no outbound client invocation.
- User manager/profile route tests assert an effective disabled outbound gate
produces
skippedbefore token acquisition/fetch while preserving OA audit. - Env/feature-contract checks keep the formal default off and reject a production claim that relies on an unapproved reverse-sync flag.
- A separately proposed reverse-sync task must add mocked API payload assertions, negative permission/conflict cases and explicit live approval evidence.
7. Wrong vs Correct
Wrong
OA set-manager/profile update -> WeCom /user/update -> real member changed
Correct
WeCom member/admin edits source data -> approved inbound read/import -> OA mirror
OA direct_manager_id update -> OA workflow only; outbound member sync skipped
Scenario: Compensatory Leave Is A WeCom Leave Category
1. Scope / Trigger
- Trigger: adding, displaying, validating or operating compensatory leave
(
调休) in the WeCom-owned HR/attendance flow. - Purpose: support the required leave category without creating a fifth approval integration, a parallel OA submission journey or an unneeded balance engine.
2. Signatures
- WeCom approval target remains
leave; the existing target set remainsleave,missed_punch,outing,overtime. - WeCom template/form category label:
调休under the leave template. - OA read-only archive fields: normalized leave category, interval, status and minimum approval summary. No new ordinary OA submit/approve API is implied.
3. Contracts
- WeCom owns employee submission, approval and normal mobile history. OA must
distinguish
调休from other leave categories in read-only history but must not expose a parallel ordinary submission or approval flow. - The initial internal-team scope does not build an OA compensatory-leave balance, automatic overtime-to-leave conversion, carry-over engine or payroll calculation. If WeCom native balance management is enabled, it is the source of truth; otherwise HR uses a controlled manual rule until a later task proves automation is worth maintaining.
- Template metadata is insufficient acceptance evidence. Formal use requires a backend check, applicant-visible real-device form and actual submitted approval chain with no self-approval.
4. Validation & Error Matrix
- WeCom backend lacks
调休-> remain not ready; do not emulate it with an OA ordinary form. - Real-device form does not show
调休->PAUSED_MANUAL_CONFIRMATION. - Applicant can select/change the approver or becomes the approver -> blocker; do not enable real-team use.
- OA archive cannot distinguish the leave category -> keep the data read-only but do not claim compensatory-leave acceptance.
- WeCom native balance is absent -> use the approved manual HR rule; do not silently create an OA balance ledger.
5. Good/Base/Bad Cases
- Good: a test user selects
调休in WeCom, a different real approver handles it, and OA read-only history displays调休, interval and final status. - Base: WeCom manages the request while HR maintains the small-team balance by a controlled manual rule; OA stores no balance.
- Bad: introduce a
compensatory_leaveapproval target and duplicate submit, balance and approval state across OA and WeCom.
6. Tests Required
- Unit/fixture coverage proves a WeCom
leavedetail containing调休remains targetleaveand preserves the category in the minimum archive/display path. - Mocked frontend coverage proves ordinary employees see read-only history and no OA ordinary compensatory-leave submit action.
- Formal-use evidence must include the backend option, real-device form and actual no-self-approval chain; live writes require a separately approved task.
7. Wrong vs Correct
Wrong
调休 -> new compensatory_leave target -> OA balance -> OA approval -> WeCom sync
Correct
WeCom leave(category=调休) -> WeCom approval -> OA minimum read-only history
HR / Attendance UI Contracts
请假记录is the PC leave entry label and page title. Ordinary employees must see records, balances and a WeCom-owned explanation, not an OA leave creation form.- OA leave creation, edit, submit and withdraw controls are emergency-only UI.
They require
leave.manageand must be labeled withOA 应急/应急草稿/提交应急审批; do not expose them based only onleave.create. - Mobile leave defaults to records for ordinary employees. Hide the form tab and
row actions unless the user has
leave.manage; keep the first card explicit:企业微信主办and只读. - Attendance correction forms are emergency-only UI. They require
attendance.manageand useOA 应急修正/提交应急修正. Ordinary employees see a WeCom handling explanation for missed punch, outing and overtime. - WeCom approval archive pages are read-only history and reconciliation. They may show status/action history labels from WeCom data, but must not offer OA approve/reject/return/transfer actions.
Tests Required
- Mocked frontend Playwright coverage should assert that ordinary employees do not see OA leave/correction create actions, while admin/HR roles retain explicitly labeled emergency entry points.
- 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/getuserdetailwith{ "user_ticket": "<short-lived>" }. - Config:
WECOM_OAUTH_ONBOARDING_ENABLED=falseby default. - Directory chain:
WECOM_CONTACT_SECRETonly for/user/list_id;WECOM_AGENT_SECRETfor/user/get,/department/listand/hr/get_staff_info.
3. Contracts
login|bindalways keepssnsapi_base; only explicitonboardusessnsapi_privateinfo.- The member must be visible to the app, active, return the same userid from both identity calls, and authorize a valid personal email or business email. Phone may be absent; when returned it must be normalized and checked for uniqueness. 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
employeerole, 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-> safeconsent_requiredredirect; no write. - Provider/network failure ->
wecom_api_failedor safe auth error; no raw payload, token or ticket in output/logs. - Userid mismatch ->
identity_mismatch; inactive member ->member_inactive. - Both emails invalid or unresolved department ->
missing_required_field. - Phone absent -> continue with
NULL; phone present but conflicting ->employee_conflict. - 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/getabsence as proof that email is unavailable, or auto-grantfinance/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
ordinary login -> snsapi_privateinfo -> batch sync guesses missing fields
-> auto-grant finance/ceo
Correct
ordinary login -> snsapi_base
one-time onboard -> explicit private consent -> safe unique employee-only write
business role assignment -> separate administrator-audited operation
Scenario: Passwordless First WeCom Login For Existing OA Accounts
1. Scope / Trigger
- Trigger: changing ordinary
mode=login, identity binding, directory-email matching, the password login fallback or account-activation readiness. - Purpose: every existing active OA employee must be able to enter from the WeCom workbench on the first attempt without entering an OA password, while ambiguous identities remain fail-closed.
2. Signatures
- Entry:
GET /api/auth/wecom/login?returnTo=<internal-path>withsnsapi_base. - OAuth callback identity: internal
useridfrom/cgi-bin/auth/getuserinfo. - Unbound member profile:
GET /cgi-bin/user/get?userid=<oauth-userid>using the in-memory app access token. - Binding DB: unique
WecomIdentityBinding.employee_idandWecomIdentityBinding.external_user_id. - Audit actions:
AUTH_WECOM_AUTO_BOUNDfollowed byAUTH_WECOM_LOGIN_SUCCESS; the latter records onlybindingSourceand the fixed allowlistedentrySource.
3. Contracts
- Existing active binding is the fast path and does not call
/user/get. - Without a binding, exact OA
employee_no == useridis a valid stable match. A normalized WeCombiz_mail/emailmay also match one existing active OAEmployee.emailusing case-insensitive exact equality. If userid and email point to different employees, stop. - Transactionally recheck both unique binding directions before create. A concurrent unique-constraint race is a conflict, not permission to overwrite.
- Ordinary login never creates an employee, updates roles, writes the WeCom directory, matches by name/phone, or consumes a pending cookie during OA password login.
- Access tokens, full userid, enterprise email and raw provider payload stay in
request memory. Auto-bind audit contains only
matchByandemailMatched; errors and redirects use stable categories. - Missing enterprise email does not block an exact userid/employee-number match. Without either stable match, no session or binding is created.
4. Validation & Error Matrix
- Existing binding + active employee -> direct session.
- Existing binding + inactive employee ->
account_mapping_inactive. /user/getnetwork/API failure or no OA match ->account_mapping_required; no write.- WeCom member inactive or matched OA employee inactive ->
account_mapping_inactive; no write. - Userid/email point to different employees, either side already has a
different binding, identity mismatch or binding race ->
account_mapping_conflict; no overwrite. - Password login with a legacy
wecom_login_bindcookie -> normal password login only, clear the cookie, returnwecomBindingStatus=null.
5. Good/Base/Bad Cases
- Good: an unbound active member's enterprise email matches exactly one active OA employee; callback creates one binding and audit, then redirects into the requested OA page without a password.
- Base: a previously bound member uses the existing fast path and creates only the normal WeCom login session/audit.
- Bad: bind by display name, overwrite another userid, create a new employee, or ask the member to enter an OA password to complete first binding.
6. Tests Required
- Service tests: profile userid equality, member state, business-email normalization, userid-only match, email-only match, split-key conflict, inactive employee, cross-binding and redacted audit.
- Route tests: existing binding fast path, passwordless auto-bind + session, provider-unavailable fail-closed redirect and no pending binding cookie.
- Password-route regression: a legacy pending cookie cannot create/update a binding and is cleared.
- Frontend typecheck/build and focused WeCom notice tests; full backend suite, Prisma validation, sensitive-content and Trellis governance gates.
7. Wrong vs Correct
Wrong
unbound OAuth userid -> password login -> consume pending cookie -> bind
Correct
OAuth userid -> existing binding OR exact userid/unique enterprise email match
-> transactional binding -> redacted audit -> direct WeCom session
Scenario: Hosted PC WeCom Web Login
1. Scope / Trigger
- Trigger: adding or changing ordinary-browser WeCom login, the public WeCom config response, OAuth state/callback handling or login-entry audit.
- Purpose: let an existing OA employee use the WeCom-hosted Web login page from a normal PC browser without loading third-party scripts beside OA password or email-code inputs.
2. Signatures
- Public config:
GET /api/auth/wecom/configreturnspcWebLoginEnabled: booleanandpcLoginUrl: string | nullseparately from the workbenchloginUrl. - OA entry:
GET /api/auth/wecom/login?returnTo=<internal-path>&entrySource=pc_qr. - Provider entry:
https://login.work.weixin.qq.com/wwlogin/sso/loginwithlogin_type=CorpApp, CorpID, AgentID, callback and signed state. - Shared callback:
GET /api/auth/wecom/callback?code=<one-time-code>&state=<signed-state>. - Env gate:
WECOM_PC_WEB_LOGIN_ENABLED=falseby default. - Audit source:
entrySource=pc_qr | wecom_workbench;moderemainslogin | bind | onboard.
3. Contracts
- The OA login page uses a normal redirect to the WeCom-hosted page. Do not add
@wecom/jssdk, a WeCom CDN script or a QR iframe to the credential page. entrySourceis independent ofmode, is validated against a fixed server allowlist and never acceptspc_qrfor bind/onboard. Missing source in an in-flight legacy state meanswecom_workbench; an unknown signed value fails.- PC state remains a short-lived HMAC-signed payload and is correlated to the initiating browser with a short-lived HttpOnly, Secure-in-production, SameSite=Lax cookie. Validate and clear it before code exchange.
- The cookie is a CSRF/browser-correlation control, not server-side nonce storage. Normal-browser replay fails after cookie clearing; deliberate replay of both URL and old cookie is finally stopped by WeCom's one-time, five-minute code contract and must not create a second OA session.
- Reuse existing userid mapping, fail-closed auto-binding, session limit, device fingerprint, returnTo allowlist and structured mapping notices.
- Successful login audit contains only
bindingSourceandentrySource. Code, state, nonce, cookies, full userid, Secret and raw provider payload stay out of logs, audit and browser error details.
4. Validation & Error Matrix
- PC env gate off or base entry config incomplete ->
pcLoginUrl=null; direct PC entry is unavailable while workbench/password/email-code behavior remains. pc_qr + bind/onboardor unknown query source -> validation failure before redirecting to WeCom.- Tampered/expired state, unknown callback source, missing/mismatched/expired correlation cookie or missing code -> fail before provider code exchange.
- Signed PC state after the PC gate is turned off -> clear the cookie and fail.
- Existing binding/unique auto-match -> shared session flow; mapping required, conflict or inactive -> existing stable notice and no session.
- Replayed URL plus old cookie -> provider rejects the already-consumed code; OA session and success audit counts remain unchanged.
- External/absolute/protocol-relative returnTo -> safe internal default.
5. Good/Base/Bad Cases
- Good: normal PC browser redirects to the hosted page, scans, returns with a correlated callback and reaches the requested internal OA page.
- Base: WeCom workbench continues through
snsapi_basewithentrySource=wecom_workbench; missing source remains compatible during a rolling deployment. - Bad: add
mode=qr, load an SDK next to password fields, expose Secret in the frontend, accept callback without browser correlation, or claim a signed cookie is a server-consumed nonce.
6. Tests Required
- Route tests assert config gate, exact hosted host/path and parameters, signed source/state, secure cookie attributes and PC bind/onboard rejection.
- Callback tests assert missing/mismatched/expired cookie, expired/tampered state, missing code, unknown source and gate-off failures occur before fetch.
- Success/replay tests assert shared mapping and session behavior, sanitized
entrySource, cookie clearing and exactly one created session. - Frontend E2E asserts PC-only button, safe returnTo/device fingerprint and no duplicate button in a WeCom user agent; typecheck and production build pass.
- Run full backend tests, Prisma validation, external/readiness contracts,
sensitive-content scan, process/Trellis gates and
git diff --check.
7. Wrong vs Correct
Wrong
normal browser -> mode=qr -> third-party SDK on OA credential page
-> callback trusts signed state alone -> audit raw userid
Correct
normal browser -> entrySource=pc_qr -> OA sets correlation cookie
-> WeCom-hosted login -> shared callback validates state + cookie
-> one-time code -> existing userid mapping -> sanitized session audit
Scenario: WeCom Directory Export With Optional Phone
1. Scope / Trigger
- Trigger: importing an administrator-exported WeCom directory XLSX, or
changing
Employee.phoneacross database, API, HR sync, OAuth and UI layers. - Purpose: allow a real member to remain temporarily without a phone while preserving stable identity, uniqueness and privacy controls.
2. Signatures
- DB:
Employee.phone String? @unique; migration drops only theNOT NULLconstraint and keeps the unique index. - CLI:
npm run import:wecom-directory-export -- --input=- --dry-run. - Apply:
WECOM_DIRECTORY_EXPORT_APPLY=1 ... --input=- --apply --actor-employee-no=<employee-no>. - When apply runs through Compose v1, pass the confirmation variable into the
container explicitly:
docker-compose exec -T -e WECOM_DIRECTORY_EXPORT_APPLY server ...; exporting it only in the host shell does not satisfy the in-container gate. - Binary stdin reader:
fs.readFileSync(0); do not assumefs.promises.readFile(0)accepts a numeric file descriptor on the production Node runtime. - User API and frontend employee/user types expose
phone: string | null; create/update accepts an omitted or explicitnullphone.
3. Contracts
- Missing phone is stored as SQL
NULL, never"", a repeated placeholder or a synthesized fake number. MultipleNULLrows are valid; non-null phones remain unique. - WeCom account and valid enterprise email remain required for directory-export onboarding. Account, non-null phone and enterprise email duplicates block the input.
- An empty source phone never clears an existing OA phone. Only an explicitly supplied valid phone participates in update and unique-conflict checks.
- The XLSX is accepted only from stdin and defaults to dry-run. Apply requires
both
--applyandWECOM_DIRECTORY_EXPORT_APPLY=1, plus an active actor. - Read the XLSX directly from fd 0 without a temporary file. A runtime that rejects the stdin read must stop before Prisma sync; do not fall back to persisting the workbook on the host or in the container.
- CLI output contains counts and categories only. The workbook, full local path, names, accounts, phones, emails, departments and raw rows must not enter Git, Trellis artifacts or reports.
- New accounts receive only
employee; finance, CEO and admin roles remain a separate administrator-audited operation.
4. Validation & Error Matrix
- Missing phone cell -> valid profile with
phone=null. - Supplied malformed phone -> incomplete/blocked; do not downgrade it to an absent phone and do not write it.
- Missing/invalid enterprise email, account, name or department -> skipped or
blocked as
missing_required_field. - Duplicate account/non-null phone/enterprise email in the workbook ->
duplicate_values; error output contains counts only. - Existing OA unique-field or binding conflict -> conflict; no automatic merge.
--applywithout confirmation env or active actor -> refuse before sync.- Confirmation env exported only on the Compose host ->
apply_confirmation_required; no sync/write. Add the name-only-einherit flag and rerun the same approved input. - Promise-based stdin fd read returns
ERR_INVALID_ARG_TYPEon the deployed Node runtime -> no sync/write; use the fd-compatible reader and rerun dry-run before requesting apply approval. - Migration rollback with any
phone IS NULLrow -> stop; do not fill fake values merely to restoreNOT NULL.
5. Good/Base/Bad Cases
- Good: five members have unique phones, one member has no phone, all six have unique accounts and enterprise emails; dry-run plans six records and reports only aggregate counts.
- Base: a later export still omits a member phone; the member's existing OA phone is preserved.
- Base: the local and production Node versions differ; the same binary stdin
workbook parses through
fs.readFileSync(0)without a temporary file. - Base: Compose v1 inherits the already-set non-secret apply confirmation only
when
exec -e WECOM_DIRECTORY_EXPORT_APPLYis present. - Bad: write an empty string or shared placeholder, use personal email in place of the required enterprise email for bulk identity, or auto-grant a business role during import.
6. Tests Required
- Prisma validation plus a migration review proving only
DROP NOT NULL. - Unit tests for instruction rows/Chinese headers, five-present/one-missing, malformed phone, missing enterprise email and duplicate account/phone/email.
- Unit regression asserts the CLI stdin helper calls
fs.readFileSync(0); production dry-run must prove aggregate parsing/sync counts before apply. - Apply acceptance must include a negative preflight without the container env
gate and a successful approved run with the name-only Compose
-eflag; both outputs remain count/category only. - Route/service tests for create/update/import
null, non-empty unique checks, OAuth missing phone and empty-source preservation of an existing phone. - Frontend typecheck/build and displays that render a clear empty-phone label.
- Full backend regression, Trellis governance, sensitive-content scan and
git diff --check; a real workbook may be parsed only into aggregate counts and remains Git-ignored.
7. Wrong vs Correct
Wrong
phone: source.phone || "00000000000"
Correct
phone: source.phone || null
// Existing employee updates omit `phone` entirely when the source is empty.
Wrong
const workbook = await fs.promises.readFile(0);
Correct
const workbook = fs.readFileSync(0);