northcinder.shop
§ · Nested detail page

本地证据只展示,审计与订单图分开落盘 深度解析

这一页拆 NorthCinder 的可追责层:trust engine 控制冷探测预算,本地历史只做 display evidence,audit log append-only,订单图不篡改 checkout log。

§ 00 · IMPORT CHAIN

导入链与契约

trust engine 在 service/src/trust/engine.ts:44 定义默认 TTL,service/src/trust/engine.ts:63 定义 800ms 预算,service/src/trust/engine.ts:92 先看 allow/deny/platform seed,service/src/trust/engine.ts:119 再按预算冷探测。这说明搜索路径优先稳定响应,而不是阻塞等完整风控。

src/service/src/trust/engine.ts · L44-84TypeScript
44}
45
46function domainMatches(domain: string, seedDomain: string): boolean {
47  const d = domain.toLowerCase();
48  const s = seedDomain.toLowerCase();
49  return d === s || d.endsWith(`.${s}`);
50}
51function findEntry(entries: TrustSeedEntry[], domain: string): TrustSeedEntry | undefined {
52  return entries.find((e) => domainMatches(domain, e.domain));
53}
54
55interface SeedEvaluation {
56  inputs: Pick<TrustDerivationInputs, "allowHit" | "denyHit" | "platformHit">;
57  evidence: TrustEvidence[];
58  /** Set when deny/allow is authoritative — the engine can skip probes entirely. */
59  shortCircuit: boolean;
60}
61
62/** Curation layer: instant, live, no I/O. Only the verifiable DOMAIN is matched. */
63function evaluateSeed(seed: TrustSeed, domain: string): SeedEvaluation {
64  const denied = findEntry(seed.deny, domain);
65  if (denied) {
66    return { inputs: { denyHit: true }, evidence: [{ source: "seed-list", detail: `deny-listed: ${denied.detail}` }], shortCircuit: true };
67  }
68  const allowed = findEntry(seed.allow, domain);
69  if (allowed) {
70    return { inputs: { allowHit: true }, evidence: [{ source: "seed-list", detail: `allow-listed: ${allowed.detail}` }], shortCircuit: true };
71  }
72  const platform = findEntry(seed.knownPlatformDomains, domain);
73  if (platform) {
74    return { inputs: { platformHit: true }, evidence: [{ source: "platform-heuristic", detail: platform.detail }], shortCircuit: false };
75  }
76  return { inputs: {}, evidence: [], shortCircuit: false };
77}
78
79function toSignal(merchant: Merchant, inputs: TrustDerivationInputs, evidence: TrustEvidence[]): TrustSignal {
80  const { level } = deriveTrustLevel(inputs);
81  // Evidence is non-empty by construction (schema min(1)); a merchant with no
82  // curation and no measured signals still carries the explicit unknown floor.
83  const lines =
84    evidence.length > 0
§ 01 · CORE IMPL

核心实现,逐段拆

▸audit log:追加失败即 fail closed

audit log 的注释在 client/src/audit-log.ts:1 说明 append-only,client/src/audit-log.ts:40 定义 audit 类型,client/src/audit-log.ts:57 append 时 mkdir 0700,client/src/audit-log.ts:62 追加 JSONL,client/src/audit-log.ts:64 若 chmod/write 失败直接抛错。

src/client/src/audit-log.ts · L1-79TypeScript
1/**
2 * Append-only local audit log (spec §2 "user-auditable", §4.5): every search,
3 * ranking (with its machine-readable reasons), mandate issuance/approval, and
4 * checkout attempt lands here as one JSONL line in the user's config dir.
5 * This file is what makes "neutral" PROVABLE to the user — so writes fail
6 * CLOSED: if the trail cannot be written, the operation that needed auditing
7 * reports an error instead of proceeding un-audited.
8 *
9 * The log is never truncated or rewritten by this module; `append` is the
10 * only operation.
11 */
12import { appendFileSync, chmodSync, existsSync, mkdirSync } from "node:fs";
13import { join } from "node:path";
14import { forEachLineFromEnd } from "./bounded-tail-reader.js";
15import { runFsOp } from "./fs-error-sanitizer.js";
16
17export const AUDIT_LOG_FILENAME = "audit.jsonl";
18
19export interface AuditEvent extends Record<string, unknown> {
20  /** e.g. "search" | "trust" | "authorization_requested" | "authorization_approved"
21   *  | "authorization_denied" | "checkout_attempt" | "checkout_result" */
22  type: string;
23}
24
25export interface AuditLog {
26  readonly path: string;
27  /** Appends one JSONL line ({at: ISO timestamp, ...event}). Throws on I/O failure. */
28  append(event: AuditEvent): void;
29}
30
31/** One page of the audit trail (local UI dashboard browser — read side only). */
32export interface AuditPage {
33  /** Entries NEWEST FIRST. An unparsable line surfaces as { raw } — never hidden. */
34  entries: Array<Record<string, unknown>>;
35  page: number;
36  pageSize: number;
37  totalEntries: number;
38  totalPages: number;
39}
40
41export const AUDIT_PAGE_SIZE_DEFAULT = 50;
42
43/**
44 * Paged, read-only view over the append-only JSONL trail, newest first
45 * (page 1 = the most recent events). Purely a READER: it never writes,
46 * truncates, or rewrites — the log's only mutation stays `append`.
47 *
48 * BOUNDED reader (local UI follow-up fix): this never does
49 * `readFileSync(path).split("\n")` — that loads the ENTIRE log into one
50 * string and one array before paginating, and the log only grows (one line
51 * per search/auth event, forever). Instead it walks the file BACKWARD in
52 * fixed-size chunks via `forEachLineFromEnd`, so memory stays bounded to
53 * ~one chunk regardless of log size — a first pass counts total non-blank
54 * lines (needed for totalPages), a second stops early once it has collected
55 * this page's window.
56 */
57export function readAuditPage(
58  path: string,
59  opts: { page?: number; pageSize?: number; chunkSize?: number } = {},
60): AuditPage {
61  const pageSize = Math.max(1, opts.pageSize ?? AUDIT_PAGE_SIZE_DEFAULT);
62
63  let totalEntries = 0;
64  forEachLineFromEnd(
65    path,
66    (line) => {
67      if (line.trim().length > 0) totalEntries += 1;
68    },
69    opts.chunkSize,
70  );
71
72  const totalPages = Math.max(1, Math.ceil(totalEntries / pageSize));
73  const page = Math.min(Math.max(1, opts.page ?? 1), totalPages);
74  // Newest first: page 1 starts at the END of the file. `offset`/`windowEnd`
75  // are ranks counted from EOF (rank 0 = newest non-blank line).
76  const offset = (page - 1) * pageSize;
77  const windowEnd = page * pageSize;
78
79  const rawLines: string[] = [];

▸订单图:邮件解析与 checkout log 分离

orders store 在 packages/orders/src/store.ts:8 声明与 checkout order-store 相邻而非扩展,packages/orders/src/store.ts:100 读取 graph,packages/orders/src/store.ts:110 以 tmp+rename 持久化,packages/orders/src/store.ts:136 保留 unparsed 邮件,packages/orders/src/store.ts:153 把 checkout record 只读适配为 Order。

src/packages/orders/src/store.ts · L95-164TypeScript
95export function createOrderGraphStore(configDir: string): OrderGraphStore {
96  const path = join(configDir, ORDER_GRAPH_FILENAME);
97  const unparsedPath = join(configDir, UNPARSED_FILENAME);
98  const processedPath = join(configDir, PROCESSED_MESSAGES_FILENAME);
99
100  function loadGraph(): OrderGraphFile {
101    if (!existsSync(path)) return { ...EMPTY_GRAPH, orders: {}, shipments: {}, returnWindows: {} };
102    try {
103      const raw = JSON.parse(readFileSync(path, "utf8")) as OrderGraphFile;
104      return { version: 1, orders: raw.orders ?? {}, shipments: raw.shipments ?? {}, returnWindows: raw.returnWindows ?? {} };
105    } catch {
106      throw new Error(`${BRAND_NAME} orders: ${path} is not valid JSON — refusing to touch it (fix or remove the file)`);
107    }
108  }
109
110  function persistGraph(graph: OrderGraphFile): void {
111    mkdirSync(configDir, { recursive: true, mode: 0o700 });
112    const tmp = `${path}.tmp`;
113    writeFileSync(tmp, `${JSON.stringify(graph, null, 2)}\n`, { mode: 0o600 });
114    chmodSync(tmp, 0o600);
115    renameSync(tmp, path);
116  }
117
118  function loadProcessed(): Set<string> {
119    if (!existsSync(processedPath)) return new Set();
120    try {
121      const raw = JSON.parse(readFileSync(processedPath, "utf8")) as { messageIds: string[] };
122      return new Set(raw.messageIds ?? []);
123    } catch {
124      return new Set();
125    }
126  }
127
128  function persistProcessed(ids: Set<string>): void {
129    mkdirSync(configDir, { recursive: true, mode: 0o700 });
130    const tmp = `${processedPath}.tmp`;
131    writeFileSync(tmp, `${JSON.stringify({ version: 1, messageIds: [...ids] }, null, 2)}\n`, { mode: 0o600 });
132    chmodSync(tmp, 0o600);
133    renameSync(tmp, processedPath);
134  }
135
136  function appendUnparsed(record: UnparsedEmailRecord): void {
137    mkdirSync(configDir, { recursive: true, mode: 0o700 });
138    const existed = existsSync(unparsedPath);
139    appendFileSync(unparsedPath, `${JSON.stringify(record)}\n`, { mode: 0o600 });
140    if (!existed) chmodSync(unparsedPath, 0o600);
141  }
142
143  /** Strips control chars from the free-text fields a hostile mail/caller can influence (leak/injection discipline — see sanitize.ts). */
144function sanitizeOrderText(order: Order): Order {
145  return {
146    ...order,
147    merchantName: sanitizeTextField(order.merchantName),
148    ...(order.orderNumber !== undefined ? { orderNumber: sanitizeTextField(order.orderNumber) } : {}),
149    items: order.items.map((item) => ({ ...item, title: sanitizeTextField(item.title) })),
150  };
151}
152
153function orderRecordToOrder(record: OrderRecord): Order {
154    return {
155      id: record.orderId,
156      merchantName: record.merchantId,
157      merchantDomain: record.merchantId,
158      orderDate: record.createdAt,
159      items: [{ title: record.mandate.intent, quantity: 1 }],
160      total: record.mandate.constraints.maxAmount,
161      status: record.status === "completed" ? "confirmed" : "unknown",
162      source: { kind: "checkout", orderId: record.orderId },
163    };
164  }
§ 02 · KEY FLOW

关键流程

merchant -> trust engine -> service evidence checkout/order emails -> order graph client server -> local history evidence appended after service signal audit log -> append-only JSONL for every search/auth/checkout/order read

server 的 client/src/server.ts:1219 获取 service trust 后,client/src/server.ts:1225 才追加 localEvidence,client/src/server.ts:1228 审计 trust 事件。订单工具测试在 client/test/order-tools.test.ts:125 用真实 MCP client/server 检查 drop-dir 重扫、get_order 和 ICS 刷新。

▸验证联动

这页把实现和测试一起看:源码引用说明控制点,测试引用说明这些控制点已经被行为化约束,避免只把 README 的安全承诺复述成架构结论。

§ 03 · EDGE CASES

边界 / 失败模式

  • service/test/trust-engine.test.ts:73 覆盖新鲜缓存不触发 probe,service/test/trust-engine.test.ts:101 覆盖 timeout 返回 unknown。
  • packages/orders/src/parser.ts:31 插件逐个尝试,packages/orders/src/parser.ts:48 捕获异常并继续,避免坏插件吞掉邮件。
  • client/test/order-tools.test.ts:151 检查响应不泄漏 configDir,client/test/order-tools.test.ts:155 检查 order_read audit。
Sub-page · 2026-08-20 Source · cinderline/northcinder ↑ northcinder 主页面