feat(addin): implement e-Court ticket auth (ticket_data + HmacSha256)
Replace the token/owner pass-through with the ticket flow from
docs/requirement/addin-workflow.md (full plan: docs/PLAN-ADDIN-AUTH.md):
- utils/docver-ticket.ts: shared pure ticket parse + hex digest helpers,
client/server inject their own crypto (Web Crypto / node:crypto)
- lib/addin-ticket.ts + POST /api/documents/token second entry: body
{ticket_data, HmacSha256} verified server-side (HMAC timing-safe + exp,
secret ADDIN_TICKET_SECRET -> SECRET -> "super_secret"); x-api-key path
unchanged
- wordaddin: ticketService (Web Crypto HMAC, localStorage session
wordaddin-auth keyed by fid, same-origin token request), authService
rewrite (7-step flow, silent renew), apiClient 401 renew-retry once,
metadataService reads ticket_data/HmacSha256, no-access messages per
cause
- scripts/make-ticket-docx.mjs: dev tool to stamp tickets onto any .docx
for testing in real Word
- app/wordaddin/error.tsx: error boundary showing the real error + stack
in the pane (webviews have no devtools)
- tests: docver-ticket + addin-ticket unit suites (21 cases) + temporary
crash repro suite for the ongoing client-crash debug session
- docs sync: PLAN-DOCVER, WORDADDIN-DEV, addin-ui-flow, openapi (two
request bodies for /token), CLAUDE.md, .env.example
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
8ebe5f8644
commit
cffc571da5
21 changed files with 1725 additions and 124 deletions
|
|
@ -23,9 +23,10 @@ export const LAST_VERSION_TIMESTAMP_PROPERTY = "LastVersionTimestamp";
|
|||
/** FR-16 — stamp เฉพาะไฟล์ร่าง (draft) ที่เว็บสร้าง: template ต้นทาง (ค่าเดียวกับ AddInMetadata.TemplateIdPropertyName ฝั่ง backend) */
|
||||
export const TEMPLATE_ID_PROPERTY = "TemplateId";
|
||||
|
||||
/** e-Court pass-through — ไฟล์ที่ e-Court stamp มาใน custom properties เข้าใช้ได้เลย (auth ทางเข้าเดียว) */
|
||||
export const ECOURT_TOKEN_PROPERTY = "token";
|
||||
export const ECOURT_OWNER_PROPERTY = "owner";
|
||||
/** e-Court ticket — ไฟล์ที่ e-Court stamp คู่นี้มาใน custom properties คือ auth ทางเข้าเดียวของ add-in
|
||||
* (PLAN-ADDIN-AUTH.md §2: HMAC-SHA256(secret, ticket_data ดิบ) เป็น hex lowercase 64 ตัว) */
|
||||
export const TICKET_DATA_PROPERTY = "ticket_data";
|
||||
export const TICKET_HMAC_PROPERTY = "HmacSha256";
|
||||
|
||||
/** ค่า property ที่หาไม่เจอเป็น `undefined` (ไม่ใช่ throw) */
|
||||
export interface TrackedDocumentProperties {
|
||||
|
|
@ -35,9 +36,9 @@ export interface TrackedDocumentProperties {
|
|||
lastVersionTimestamp?: string;
|
||||
/** มีเฉพาะไฟล์ร่าง (FR-16) — ส่งต่อให้ backend ตอน Start Versioning เพื่อผูก Document กับ template */
|
||||
templateId?: string;
|
||||
/** e-Court pass-through — มีอย่างใดอย่างหนึ่ง = ใช้งานได้ทันที (username จาก owner หรือ uid ใน token) */
|
||||
ecourtToken?: string;
|
||||
ecourtOwner?: string;
|
||||
/** e-Court ticket — มีครบทั้งคู่ = ตรวจ HMAC แล้วเข้าใช้งานได้ (auth ทางเข้าเดียว) */
|
||||
ticketData?: string;
|
||||
ticketHmac?: string;
|
||||
}
|
||||
|
||||
/** item ใน Custom XML Part `<VersionHistory>` */
|
||||
|
|
@ -49,7 +50,7 @@ export interface XmlVersionEntry {
|
|||
}
|
||||
|
||||
/** อ่าน custom properties ทั้ง 7 ตัวที่ระบบสนใจ (4 ตัวของ tracked + TemplateId ของไฟล์ร่าง FR-16
|
||||
* + token/owner ที่ e-Court stamp มากับไฟล์) */
|
||||
* + ticket_data/HmacSha256 ที่ e-Court stamp มากับไฟล์) */
|
||||
export async function readTrackedProperties(): Promise<TrackedDocumentProperties> {
|
||||
return Word.run(async (context) => {
|
||||
const properties = context.document.properties.customProperties;
|
||||
|
|
@ -58,16 +59,16 @@ export async function readTrackedProperties(): Promise<TrackedDocumentProperties
|
|||
const lastVersionId = properties.getItemOrNullObject(LAST_VERSION_ID_PROPERTY);
|
||||
const lastVersionTimestamp = properties.getItemOrNullObject(LAST_VERSION_TIMESTAMP_PROPERTY);
|
||||
const templateId = properties.getItemOrNullObject(TEMPLATE_ID_PROPERTY);
|
||||
const ecourtToken = properties.getItemOrNullObject(ECOURT_TOKEN_PROPERTY);
|
||||
const ecourtOwner = properties.getItemOrNullObject(ECOURT_OWNER_PROPERTY);
|
||||
const ticketData = properties.getItemOrNullObject(TICKET_DATA_PROPERTY);
|
||||
const ticketHmac = properties.getItemOrNullObject(TICKET_HMAC_PROPERTY);
|
||||
// load "value" ของ null-object proxy ก็ทำให้ isNullObject แม่นหลัง sync
|
||||
savedByAddIn.load("value");
|
||||
documentTrackingId.load("value");
|
||||
lastVersionId.load("value");
|
||||
lastVersionTimestamp.load("value");
|
||||
templateId.load("value");
|
||||
ecourtToken.load("value");
|
||||
ecourtOwner.load("value");
|
||||
ticketData.load("value");
|
||||
ticketHmac.load("value");
|
||||
await context.sync();
|
||||
|
||||
const read = (property: Word.CustomProperty): string | undefined =>
|
||||
|
|
@ -81,8 +82,8 @@ export async function readTrackedProperties(): Promise<TrackedDocumentProperties
|
|||
lastVersionId: read(lastVersionId),
|
||||
lastVersionTimestamp: read(lastVersionTimestamp),
|
||||
templateId: read(templateId),
|
||||
ecourtToken: read(ecourtToken),
|
||||
ecourtOwner: read(ecourtOwner),
|
||||
ticketData: read(ticketData),
|
||||
ticketHmac: read(ticketHmac),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue