实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 18:57:34 +08:00
父节点 e123d50f86
当前提交 410d8cd71b
共修改 23 个文件,包含 1352 行新增和 69 行删除
@@ -510,6 +510,11 @@ const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = { bus: "travel_bus" };
- Reimbursement attachments and invoice PDFs retain `inline`; purchase,
employee and payment receipt downloads keep their existing default
`attachment` behavior through the same shared builder.
- Inside the WeCom self-built app, reimbursement invoice and attachment viewing
uses the official `previewFile` JSAPI with accurate persisted file name/size.
Because the native layer does not reliably carry OA authentication headers,
only the narrow grant defined in `wecom-boundary.md` may use a query token;
expiry is at most 120 seconds and ordinary PC downloads remain unchanged.
### 4. Validation & Error Matrix
@@ -519,6 +524,8 @@ const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = { bus: "travel_bus" };
- Unsupported MIME, empty file or oversized file -> existing stable file
validation error; no partial persistence.
- Chinese name -> `inline; filename="attachment.<ext>"; filename*=UTF-8''...`.
- WeCom preview grant is expired, tampered or bound to another reimbursement or
attachment -> `403` with no file bytes.
- Invalid disposition value -> `TypeError`; callers may use only `inline` or
`attachment`.
@@ -539,6 +546,10 @@ const TRAVEL_TRANSPORT_EXPENSE_TYPE_CODES = { bus: "travel_bus" };
- Download an existing Chinese-name reimbursement attachment and invoice PDF;
assert `inline`, ASCII `filename`, percent-encoded `filename*`, and no raw
Chinese in the header.
- Assert the WeCom grant carries the exact original name and file size, expires
within 120 seconds, records issue/access audits and rejects tamper, expiry and
object mismatch; Playwright verifies PC browser behavior and WeCom
`previewFile` behavior with synthetic files.
- Cover CR/LF, quote and path cleanup in the shared Content-Disposition helper,
while retaining existing purchase `attachment` regression tests.
- Run backend full tests and frontend typecheck/build because the limit and
+15 -8
查看文件
@@ -134,7 +134,10 @@
`buffer`, `extension`, `mimeType`, `fileSize` and `fileSha256`.
- Browser downloads of OA-stored attachments must use a currently
authenticated request, preferably an API-local `POST` with optional JSON
`{ "reason": "..." }`, rather than a query bearer URL.
`{ "reason": "..." }`, rather than a query bearer URL. The only approved
exception is the at-most-120-second, purpose/object/actor-bound grant used by
the official WeCom `previewFile` native layer; follow `wecom-boundary.md` and
do not reuse that grant for ordinary browser downloads or other integrations.
### 3. Contracts
@@ -163,9 +166,12 @@
- Successful download audit is part of authorization: write it before setting
attachment headers or sending bytes. Audit failure must return a non-success
response with no file body.
- Successful responses use `application/octet-stream`, `attachment`,
`nosniff`, private no-store caching, no-referrer and a route-level sandbox
CSP. Do not restore same-origin inline preview or query-token download routes.
- Successful ordinary browser downloads use `application/octet-stream`,
`attachment`, `nosniff`, private no-store caching, no-referrer and a
route-level sandbox CSP. Do not restore general same-origin inline preview or
query-token download routes. The WeCom-native reimbursement invoice/attachment
exception retains the domain-specific `inline` response and must still audit
before bytes, use no-store/no-referrer and reject expired or mismatched grants.
### 4. Validation & Error Matrix
@@ -220,10 +226,11 @@
- Frontend typecheck/build after API type changes.
- Focused UI verification for pages that add attachment governance labels or
download-reason interactions.
- Frontend download tests must assert authenticated JSON request + Blob browser
download and absence of popup/inline preview. Use only synthetic in-memory
fixtures; production attachments and historical bulk scanning require a
separate approval.
- Frontend ordinary-download tests must assert authenticated JSON request + Blob
browser download. WeCom-native preview tests instead assert the official
bridge receives the exact short URL/name/size and that `window.open` is not
used. Use only synthetic in-memory fixtures; production attachments and
historical bulk scanning require a separate approval.
### 7. Wrong vs Correct
+92
查看文件
@@ -41,6 +41,98 @@ or administration workflow has already migrated.
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
`previewFile` JSAPI.
- 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.
- Native call: `previewFile({ url, name, size })`; `name` includes an extension
and `size` is 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-store` plus `no-referrer`.
- PC keeps its browser/authenticated download behavior. Only a detected WeCom
WebView dynamically loads `@wecom/jssdk` and calls `previewFile`; do not send
the actual file through a WeCom message or native approval payload.
### 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 `503` detail without provider values.
- Missing file metadata, extension or positive integer size -> frontend stops
before `previewFile` and shows a stable error.
- Missing required download reason -> `400 { reason: "required" }`.
- Expired, tampered, wrong-purpose or object-mismatched file grant -> `403`.
- Access audit failure -> `5xx` with no file body.
### 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.
- Bad: expose a long-lived generic `?token=`, trust a caller-supplied size/name,
put a provider ticket in the response, or use async `window.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`, asserts `previewFile` receives
the exact URL/name/size for invoice and attachment, and asserts no
`window.open` call in WeCom. PC assertions keep browser URL and Blob behavior.
- Production deployment and real-device preview remain separate approvals.
### 7. Wrong vs Correct
#### Wrong
```text
await downloadWithBearer();
window.open(URL.createObjectURL(blob)); // WeCom may block after await
```
#### Correct
```text
authenticated grant issue -> <=120s object-bound URL
-> ww.previewFile({ url, nameWithExtension, exactSize })
-> audit before file bytes
```
## Scenario: WeCom Member Data Is A Read-Only Source For OA
### 1. Scope / Trigger