@seasonkoh/webaz 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +5 -1
  2. package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
  3. package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
  4. package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
  5. package/dist/layer0-foundation/L0-1-database/db.js +65 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
  7. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
  8. package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
  11. package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
  12. package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
  13. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
  14. package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
  15. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
  20. package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
  21. package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
  22. package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
  23. package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
  24. package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
  25. package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
  26. package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
  27. package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
  28. package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
  29. package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
  30. package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
  31. package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
  32. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
  33. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
  34. package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  38. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  39. package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
  40. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
  41. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  42. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  43. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  44. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  45. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  46. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  47. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  48. package/dist/pwa/acp-feed.js +13 -1
  49. package/dist/pwa/admin-bearer-auth.js +21 -0
  50. package/dist/pwa/contract-fingerprint.js +2 -0
  51. package/dist/pwa/email-delivery.js +127 -0
  52. package/dist/pwa/endpoint-actions.js +5 -1
  53. package/dist/pwa/goal-index.js +8 -8
  54. package/dist/pwa/human-presence.js +62 -0
  55. package/dist/pwa/public/app.js +1485 -283
  56. package/dist/pwa/public/i18n.js +297 -59
  57. package/dist/pwa/public/index.html +1 -0
  58. package/dist/pwa/public/openapi.json +5 -5
  59. package/dist/pwa/public/whitepaper/en/index.html +153 -0
  60. package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
  61. package/dist/pwa/rate-limit.js +22 -0
  62. package/dist/pwa/routes/account-deletion.js +15 -13
  63. package/dist/pwa/routes/addresses.js +10 -9
  64. package/dist/pwa/routes/admin-admins.js +13 -14
  65. package/dist/pwa/routes/admin-analytics.js +109 -69
  66. package/dist/pwa/routes/admin-atomic.js +10 -4
  67. package/dist/pwa/routes/admin-catalog.js +13 -11
  68. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  69. package/dist/pwa/routes/admin-events.js +5 -3
  70. package/dist/pwa/routes/admin-health.js +2 -1
  71. package/dist/pwa/routes/admin-moderation.js +50 -29
  72. package/dist/pwa/routes/admin-ops.js +35 -23
  73. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  74. package/dist/pwa/routes/admin-reports.js +23 -21
  75. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  76. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  77. package/dist/pwa/routes/admin-users-query.js +65 -53
  78. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  79. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  80. package/dist/pwa/routes/admin-wallet-ops.js +32 -7
  81. package/dist/pwa/routes/agent-buy.js +46 -22
  82. package/dist/pwa/routes/agent-governance.js +52 -56
  83. package/dist/pwa/routes/ai.js +7 -5
  84. package/dist/pwa/routes/analytics.js +43 -41
  85. package/dist/pwa/routes/anchors.js +19 -20
  86. package/dist/pwa/routes/announcements.js +13 -13
  87. package/dist/pwa/routes/arbitrator.js +97 -31
  88. package/dist/pwa/routes/auction.js +157 -116
  89. package/dist/pwa/routes/auth-login.js +6 -4
  90. package/dist/pwa/routes/auth-read.js +21 -10
  91. package/dist/pwa/routes/auth-register.js +111 -26
  92. package/dist/pwa/routes/auth-sessions.js +12 -11
  93. package/dist/pwa/routes/blocklist.js +16 -15
  94. package/dist/pwa/routes/build-feedback.js +10 -9
  95. package/dist/pwa/routes/build-reputation.js +6 -2
  96. package/dist/pwa/routes/build-tasks.js +45 -13
  97. package/dist/pwa/routes/buyer-feeds.js +27 -25
  98. package/dist/pwa/routes/cart.js +16 -15
  99. package/dist/pwa/routes/charity.js +212 -150
  100. package/dist/pwa/routes/chat.js +42 -43
  101. package/dist/pwa/routes/checkin-tasks.js +10 -9
  102. package/dist/pwa/routes/checkout-helpers.js +12 -10
  103. package/dist/pwa/routes/claim-initiators.js +34 -14
  104. package/dist/pwa/routes/claim-verify.js +86 -53
  105. package/dist/pwa/routes/claim-voting.js +43 -18
  106. package/dist/pwa/routes/contribution-identity.js +164 -0
  107. package/dist/pwa/routes/contribution-score.js +19 -0
  108. package/dist/pwa/routes/coupons.js +19 -16
  109. package/dist/pwa/routes/dashboards.js +18 -16
  110. package/dist/pwa/routes/dispute-cases.js +25 -24
  111. package/dist/pwa/routes/disputes-read.js +45 -51
  112. package/dist/pwa/routes/disputes-write.js +124 -61
  113. package/dist/pwa/routes/evidence.js +9 -9
  114. package/dist/pwa/routes/external-anchors.js +13 -12
  115. package/dist/pwa/routes/feedback.js +29 -33
  116. package/dist/pwa/routes/flash-sales.js +18 -16
  117. package/dist/pwa/routes/follows.js +25 -24
  118. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  119. package/dist/pwa/routes/governance-onboarding.js +70 -59
  120. package/dist/pwa/routes/group-buys.js +22 -22
  121. package/dist/pwa/routes/growth.js +34 -31
  122. package/dist/pwa/routes/import-product.js +12 -10
  123. package/dist/pwa/routes/kyc.js +9 -8
  124. package/dist/pwa/routes/leaderboard.js +20 -18
  125. package/dist/pwa/routes/listings.js +23 -22
  126. package/dist/pwa/routes/logistics.js +10 -8
  127. package/dist/pwa/routes/manifests.js +27 -27
  128. package/dist/pwa/routes/me-data.js +23 -21
  129. package/dist/pwa/routes/notifications.js +7 -6
  130. package/dist/pwa/routes/offers.js +30 -12
  131. package/dist/pwa/routes/orders-action.js +51 -29
  132. package/dist/pwa/routes/orders-create.js +75 -20
  133. package/dist/pwa/routes/orders-read.js +21 -20
  134. package/dist/pwa/routes/p2p-products.js +30 -18
  135. package/dist/pwa/routes/payments-governance.js +61 -56
  136. package/dist/pwa/routes/peers.js +9 -8
  137. package/dist/pwa/routes/pin-receipts.js +13 -13
  138. package/dist/pwa/routes/products-aliases.js +12 -10
  139. package/dist/pwa/routes/products-claims.js +36 -17
  140. package/dist/pwa/routes/products-create.js +53 -38
  141. package/dist/pwa/routes/products-crud.js +17 -16
  142. package/dist/pwa/routes/products-links.js +49 -26
  143. package/dist/pwa/routes/products-list.js +6 -4
  144. package/dist/pwa/routes/products-meta.js +40 -39
  145. package/dist/pwa/routes/products-update.js +19 -5
  146. package/dist/pwa/routes/profile-credentials.js +20 -19
  147. package/dist/pwa/routes/profile-identity.js +14 -13
  148. package/dist/pwa/routes/profile-location.js +7 -6
  149. package/dist/pwa/routes/profile-placement.js +20 -19
  150. package/dist/pwa/routes/profile-prefs.js +11 -11
  151. package/dist/pwa/routes/promoter.js +58 -66
  152. package/dist/pwa/routes/public-build-tasks.js +19 -0
  153. package/dist/pwa/routes/public-utils.js +108 -46
  154. package/dist/pwa/routes/push.js +16 -15
  155. package/dist/pwa/routes/ratings.js +92 -32
  156. package/dist/pwa/routes/recover-key.js +66 -26
  157. package/dist/pwa/routes/referral.js +37 -52
  158. package/dist/pwa/routes/reputation.js +3 -2
  159. package/dist/pwa/routes/returns.js +76 -73
  160. package/dist/pwa/routes/reviews.js +41 -18
  161. package/dist/pwa/routes/rewards-apply.js +16 -15
  162. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  163. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  164. package/dist/pwa/routes/rfqs.js +163 -85
  165. package/dist/pwa/routes/search.js +16 -14
  166. package/dist/pwa/routes/secondhand.js +25 -22
  167. package/dist/pwa/routes/seller-quota.js +24 -26
  168. package/dist/pwa/routes/share-redirects.js +60 -55
  169. package/dist/pwa/routes/shareables-interactions.js +34 -35
  170. package/dist/pwa/routes/shareables.js +55 -51
  171. package/dist/pwa/routes/shop-referral.js +58 -0
  172. package/dist/pwa/routes/shops.js +25 -20
  173. package/dist/pwa/routes/signaling.js +10 -9
  174. package/dist/pwa/routes/skill-market.js +16 -16
  175. package/dist/pwa/routes/skills.js +15 -14
  176. package/dist/pwa/routes/snf.js +14 -13
  177. package/dist/pwa/routes/tags.js +10 -9
  178. package/dist/pwa/routes/task-proposals.js +121 -0
  179. package/dist/pwa/routes/trial.js +72 -52
  180. package/dist/pwa/routes/trusted-kpi.js +20 -18
  181. package/dist/pwa/routes/url-claim.js +67 -28
  182. package/dist/pwa/routes/users-public.js +62 -70
  183. package/dist/pwa/routes/variants.js +12 -13
  184. package/dist/pwa/routes/verifier-user.js +61 -21
  185. package/dist/pwa/routes/verify-tasks.js +49 -25
  186. package/dist/pwa/routes/waitlist.js +16 -15
  187. package/dist/pwa/routes/wallet-read.js +75 -37
  188. package/dist/pwa/routes/wallet-write.js +12 -9
  189. package/dist/pwa/routes/webauthn.js +25 -26
  190. package/dist/pwa/routes/webhooks.js +26 -26
  191. package/dist/pwa/routes/welcome.js +45 -50
  192. package/dist/pwa/routes/wishlist-qa.js +29 -32
  193. package/dist/pwa/server.js +304 -90
  194. package/dist/version.js +1 -1
  195. package/package.json +76 -3
@@ -16,6 +16,7 @@
16
16
  * - 其他技能目前免费(增强曝光,间接提升成交)
17
17
  */
18
18
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
19
+ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
19
20
  import { toUnits, toDecimal } from '../../money.js';
20
21
  import { applyWalletDelta } from '../../ledger.js';
21
22
  // ─── Schema ───────────────────────────────────────────────────
@@ -135,7 +136,9 @@ export function publishSkill(db, input) {
135
136
  `).run(id, input.sellerId, input.name, input.description, input.category ?? 'general', input.skillType, config, input.pricePerUse ?? 0);
136
137
  return getSkillById(db, id);
137
138
  }
138
- export function listSkills(db, filter = {}) {
139
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点均确认不在 db.transaction )
140
+ // 注:getSkillById(被同步写 publishSkill 内部调用)+ shouldAutoAccept(orders-create tx 内)留 Phase 3。
141
+ export async function listSkills(_db, filter = {}) {
139
142
  const params = [];
140
143
  let sql = `
141
144
  SELECT s.*, u.name as seller_name,
@@ -161,7 +164,7 @@ export function listSkills(db, filter = {}) {
161
164
  }
162
165
  sql += ` ORDER BY s.total_uses DESC, s.rating DESC LIMIT ?`;
163
166
  params.push(filter.limit ?? 20);
164
- return db.prepare(sql).all(...params);
167
+ return await dbAll(sql, params);
165
168
  }
166
169
  export function getSkillById(db, skillId) {
167
170
  return db.prepare(`
@@ -170,12 +173,12 @@ export function getSkillById(db, skillId) {
170
173
  FROM skills s JOIN users u ON s.seller_id = u.id WHERE s.id = ?
171
174
  `).get(skillId);
172
175
  }
173
- export function getMySkills(db, sellerId) {
174
- return db.prepare(`
176
+ export async function getMySkills(_db, sellerId) {
177
+ return await dbAll(`
175
178
  SELECT s.*,
176
179
  (SELECT COUNT(*) FROM skill_subscriptions ss WHERE ss.skill_id = s.id AND ss.active = 1) as subscriber_count
177
180
  FROM skills s WHERE s.seller_id = ? ORDER BY s.created_at DESC
178
- `).all(sellerId);
181
+ `, [sellerId]);
179
182
  }
180
183
  // ─── 订阅 / 取消订阅 ──────────────────────────────────────────
181
184
  export function subscribeSkill(db, userId, skillId, config = {}) {
@@ -195,8 +198,8 @@ export function subscribeSkill(db, userId, skillId, config = {}) {
195
198
  export function unsubscribeSkill(db, userId, skillId) {
196
199
  db.prepare('UPDATE skill_subscriptions SET active = 0 WHERE skill_id = ? AND user_id = ?').run(skillId, userId);
197
200
  }
198
- export function getMySubscriptions(db, userId) {
199
- return db.prepare(`
201
+ export async function getMySubscriptions(_db, userId) {
202
+ return await dbAll(`
200
203
  SELECT s.*, u.name as seller_name, 1 as subscribed,
201
204
  (SELECT COUNT(*) FROM skill_subscriptions ss WHERE ss.skill_id = s.id AND ss.active = 1) as subscriber_count
202
205
  FROM skill_subscriptions sub
@@ -204,7 +207,7 @@ export function getMySubscriptions(db, userId) {
204
207
  JOIN users u ON s.seller_id = u.id
205
208
  WHERE sub.user_id = ? AND sub.active = 1
206
209
  ORDER BY sub.created_at DESC
207
- `).all(userId);
210
+ `, [userId]);
208
211
  }
209
212
  // ─── 使用记录 + 佣金 ──────────────────────────────────────────
210
213
  /**
@@ -18,6 +18,7 @@
18
18
  * 绝不进入 PV 二元匹配 / 推土机三级佣金 / fund_base —— 保持双引擎解耦、规避 PV 合规问题。
19
19
  */
20
20
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
21
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
21
22
  import { toUnits, toDecimal } from '../../money.js';
22
23
  import { applyWalletDelta } from '../../ledger.js';
23
24
  export const SKILL_KINDS = ['template', 'prompt', 'guide', 'checklist'];
@@ -170,7 +171,9 @@ function getListingRaw(db, id) {
170
171
  return db.prepare('SELECT * FROM skill_listings WHERE id = ?').get(id);
171
172
  }
172
173
  /** 公开列表:仅 approved + active 的技能,不含 content */
173
- export function listMarket(db, filter = {}) {
174
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点 skill-market.ts 均 inTx=false)
175
+ // 注:hasUnlock(被同步写 purchaseListing/readContent 复用)留 Phase 3,故 getMarketDetail 的 owned 检查内联为 dbOne。
176
+ export async function listMarket(_db, filter = {}) {
174
177
  const params = [];
175
178
  let ownedSelect = '';
176
179
  if (filter.viewerId) {
@@ -198,29 +201,32 @@ export function listMarket(db, filter = {}) {
198
201
  }
199
202
  sql += ' ORDER BY l.total_sales DESC, l.rating DESC, l.created_at DESC LIMIT ?';
200
203
  params.push(filter.limit ?? 30);
201
- return db.prepare(sql).all(...params);
204
+ return await dbAll(sql, params);
202
205
  }
203
206
  /** 公开详情(不含 content);附加 owned 标记。未上架的仅作者本人可见,防 metadata 泄露 */
204
- export function getMarketDetail(db, id, viewerId) {
205
- const row = db.prepare(`
207
+ export async function getMarketDetail(_db, id, viewerId) {
208
+ const row = await dbOne(`
206
209
  SELECT ${PUBLIC_COLS}, u.name as author_name
207
210
  FROM skill_listings l JOIN users u ON l.author_id = u.id
208
211
  WHERE l.id = ?
209
- `).get(id);
212
+ `, [id]);
210
213
  if (!row)
211
214
  return null;
212
215
  if (row.status !== 'approved' && row.author_id !== viewerId)
213
216
  return null;
214
- if (viewerId)
215
- row.owned = hasUnlock(db, viewerId, id) ? 1 : 0;
217
+ if (viewerId) {
218
+ // owned 检查内联(hasUnlock 被同步写路径复用,留 Phase 3;此处直接走 seam,与 hasUnlock 同 SQL)
219
+ const unlocked = await dbOne(`SELECT 1 FROM skill_orders o WHERE o.listing_id = ? AND o.buyer_id = ? AND o.billing_mode IN ('free','one_time') LIMIT 1`, [id, viewerId]);
220
+ row.owned = unlocked ? 1 : 0;
221
+ }
216
222
  return row;
217
223
  }
218
224
  /** 作者视角:自己的全部技能(含各状态、含 content 由调用方决定是否回传) */
219
- export function getMyListings(db, authorId) {
220
- return db.prepare(`
225
+ export async function getMyListings(_db, authorId) {
226
+ return await dbAll(`
221
227
  SELECT ${PUBLIC_COLS}, l.audit_note, l.reviewed_at, l.total_revenue
222
228
  FROM skill_listings l WHERE l.author_id = ? ORDER BY l.created_at DESC
223
- `).all(authorId);
229
+ `, [authorId]);
224
230
  }
225
231
  // ─── 访问权 / 解锁 ────────────────────────────────────────────
226
232
  /** one_time/free 是否已永久解锁(per_use 不算永久解锁) */
@@ -303,8 +309,8 @@ export function readContent(db, userId, listingId, feeRate) {
303
309
  return { content: listing.content, billing_mode: listing.billing_mode };
304
310
  }
305
311
  /** 我的技能库:已解锁(free/one_time)的技能 + per_use 使用过的技能 */
306
- export function getMyLibrary(db, userId) {
307
- return db.prepare(`
312
+ export async function getMyLibrary(_db, userId) {
313
+ return await dbAll(`
308
314
  SELECT ${PUBLIC_COLS}, u.name as author_name, 1 as owned,
309
315
  MAX(o.created_at) as last_used
310
316
  FROM skill_orders o
@@ -313,15 +319,15 @@ export function getMyLibrary(db, userId) {
313
319
  WHERE o.buyer_id = ?
314
320
  GROUP BY l.id
315
321
  ORDER BY last_used DESC
316
- `).all(userId);
322
+ `, [userId]);
317
323
  }
318
324
  // ─── 审计(WebAZ content admin)─────────────────────────────────
319
- export function listPendingAudit(db, limit = 100) {
320
- return db.prepare(`
325
+ export async function listPendingAudit(_db, limit = 100) {
326
+ return await dbAll(`
321
327
  SELECT l.*, u.name as author_name
322
328
  FROM skill_listings l JOIN users u ON l.author_id = u.id
323
329
  WHERE l.status = 'submitted' ORDER BY l.created_at ASC LIMIT ?
324
- `).all(limit);
330
+ `, [limit]);
325
331
  }
326
332
  export function auditListing(db, listingId, reviewerId, decision, note) {
327
333
  const cur = getListingRaw(db, listingId);
@@ -87,11 +87,23 @@ export function buildAcpProductFeed(db, opts = {}) {
87
87
  generated_at: new Date().toISOString(),
88
88
  source: 'WebAZ',
89
89
  spec: {
90
- based_on: 'OpenAI Agentic Commerce Protocol — product feed',
90
+ feed_kind: 'acp-inspired-discovery',
91
+ based_on: 'OpenAI Agentic Commerce Protocol — product feed (SHAPE only; this is a discovery projection, not a strict ACP-ingestable merchant feed)',
91
92
  api_version_observed: '2025-09-12',
92
93
  reference: 'https://developers.openai.com/commerce/specs/feed',
93
94
  rfc: `${BASE}/docs/INTEGRATOR.md`,
94
95
  },
96
+ // Explicit non-compliance so an ACP ingester does NOT treat this as a strict feed (Codex #151).
97
+ compatibility: {
98
+ is_strict_acp_ingestable: false,
99
+ summary: 'Discovery projection that borrows the ACP product-feed shape. It is intentionally NOT a strict ACP-ingestable feed.',
100
+ non_compliant_points: [
101
+ 'price.currency is SIMULATED WAZ (pre-launch internal unit), not an ISO 4217 fiat currency.',
102
+ 'is_eligible_checkout is false for every item: ACP checkout (card + PSP) is not wired.',
103
+ 'Merchant-level required fields (target_countries, store_country) are not emitted.',
104
+ ],
105
+ strict_export: 'A strict/export feed (ISO 4217 + merchant required fields, compliant items only) is deferred to a later RFC-015 phase, after fiat pricing + ACP checkout exist — it would be empty today.',
106
+ },
95
107
  _disclosures: {
96
108
  phase: 'pre-launch',
97
109
  currency: "Each item's price.currency is the protocol's SIMULATED pre-launch internal unit (WAZ, or legacy 'DCP'), NOT an ISO 4217 fiat currency. No real money is involved.",
@@ -0,0 +1,21 @@
1
+ export function resolveBearerProtocolAdmin(db, req, isProtocolAdmin) {
2
+ const authz = req?.headers?.authorization;
3
+ if (typeof authz !== 'string' || !authz.startsWith('Bearer '))
4
+ return null; // Bearer only — NOT req.body.api_key
5
+ const key = authz.slice('Bearer '.length).trim();
6
+ if (!key)
7
+ return null;
8
+ const u = db.prepare('SELECT * FROM users WHERE api_key = ?').get(key);
9
+ if (!u)
10
+ return null;
11
+ const mod = db.prepare('SELECT suspended FROM user_moderation WHERE user_id = ?').get(u.id);
12
+ if (mod?.suspended)
13
+ return null; // suspended admin cannot approve
14
+ const session = db.prepare('SELECT revoked_at FROM user_sessions WHERE api_key = ? ORDER BY created_at DESC LIMIT 1')
15
+ .get(key);
16
+ if (session?.revoked_at)
17
+ return null; // remotely logged-out session cannot approve
18
+ if (!isProtocolAdmin(u))
19
+ return null; // admin role + protocol permission (caller-supplied, central logic)
20
+ return u;
21
+ }
@@ -33,6 +33,8 @@ export function contractFingerprints() {
33
33
  export const CONTRACT_CHANGES = [
34
34
  { contract_version: 1, date: '2026-06-06', surface: 'all', kind: 'genesis', summary: 'Contract v1 baseline: capability matrix (§②), entity dictionary + order lifecycle (§①), event cursor stream (§⑥), two version axes (§④).' },
35
35
  { contract_version: 2, date: '2026-06-06', surface: 'entity', kind: 'added', summary: '§① entity dictionary gains product + dispute entities (conservative public fields; dispute = redacted dispute_cases) + goal_index pointer. Additive — existing order entity unchanged; agents may ignore.' },
36
+ { contract_version: 3, date: '2026-06-09', surface: 'entity', kind: 'changed', summary: '§① order lifecycle: corrected declined_nofault state meaning text — it is NOT terminal (transitions declined_nofault→completed on settlement). Dropped the contradictory "(terminal)" label that conflicted with the auto-derived terminal:false. Semantics/state-machine unchanged; text-only clarification for agents reading the entity dictionary.' },
37
+ { contract_version: 4, date: '2026-06-09', surface: 'capability', kind: 'changed', summary: '§② capability matrix: POST /api/reviews/:type/:id/claim now requires the new "review_claim" action scope instead of being SAFE (unscoped). The review-claim path locks a 5 WAZ stake (escrow), so it belongs under default-deny accountability like other value writes. GET reviews endpoints stay open.', migration: 'A declared agent that calls review claim must add the "review_claim" scope to its api_key (or hold a Passkey, or declare "*"). Passkey-bound humans and "*" agents are unaffected; GET reviews unchanged.' },
36
38
  ];
37
39
  export function buildChangeFeed() {
38
40
  return {
@@ -0,0 +1,127 @@
1
+ const DEFAULT_FROM = 'WebAZ <noreply@webaz.xyz>';
2
+ const DEFAULT_BASE_URL = 'https://webaz.xyz';
3
+ export function emailDeliveryNotConfigured() {
4
+ return {
5
+ ok: false,
6
+ status: 503,
7
+ error_code: 'EMAIL_DELIVERY_NOT_CONFIGURED',
8
+ error: '邮箱发送服务未配置,请稍后再试',
9
+ };
10
+ }
11
+ export function emailDeliveryFailed() {
12
+ return {
13
+ ok: false,
14
+ status: 502,
15
+ error_code: 'EMAIL_DELIVERY_FAILED',
16
+ error: '验证码邮件发送失败,请稍后再试',
17
+ };
18
+ }
19
+ function isProtectedEmailEnv(env) {
20
+ return env.NODE_ENV === 'production'
21
+ || !!env.RAILWAY_ENVIRONMENT
22
+ || !!env.RAILWAY_PROJECT_ID
23
+ || !!env.RAILWAY_SERVICE_ID;
24
+ }
25
+ export function isVerificationEmailReady(env = process.env) {
26
+ if (!isProtectedEmailEnv(env))
27
+ return true;
28
+ return !!env.RESEND_API_KEY?.trim();
29
+ }
30
+ function purposeText(purpose) {
31
+ if (purpose === 'register')
32
+ return { zh: '注册账户(验证邮箱)', en: 'register your account (verify email)' };
33
+ if (purpose === 'bind_email')
34
+ return { zh: '绑定邮箱', en: 'bind your email address' };
35
+ if (purpose === 'recover_key')
36
+ return { zh: '找回密钥', en: 'recover your account key' };
37
+ if (purpose.startsWith('withdraw_confirm'))
38
+ return { zh: '确认提现', en: 'confirm a withdrawal' };
39
+ return { zh: '验证身份', en: 'verify your identity' };
40
+ }
41
+ function escapeHtml(s) {
42
+ return s.replace(/[&<>"']/g, c => ({
43
+ '&': '&amp;',
44
+ '<': '&lt;',
45
+ '>': '&gt;',
46
+ '"': '&quot;',
47
+ "'": '&#39;',
48
+ }[c] || c));
49
+ }
50
+ export function buildVerificationEmail(input) {
51
+ const purpose = purposeText(input.purpose);
52
+ const baseUrl = input.baseUrl?.trim() || DEFAULT_BASE_URL;
53
+ const subject = 'WebAZ 验证码 / Verification code';
54
+ const text = [
55
+ `WebAZ 验证码: ${input.code}`,
56
+ '',
57
+ `用途: ${purpose.zh}`,
58
+ `有效期: ${input.ttlMin} 分钟`,
59
+ '',
60
+ `Your WebAZ verification code is ${input.code}.`,
61
+ `Use it to ${purpose.en}. It expires in ${input.ttlMin} minutes.`,
62
+ '',
63
+ 'If you did not request this code, you can ignore this email.',
64
+ baseUrl,
65
+ ].join('\n');
66
+ const html = [
67
+ '<div style="font-family:system-ui,-apple-system,Segoe UI,sans-serif;line-height:1.5;color:#111827">',
68
+ '<h2 style="margin:0 0 12px">WebAZ verification code</h2>',
69
+ `<p style="margin:0 0 8px">用途: ${escapeHtml(purpose.zh)} / ${escapeHtml(purpose.en)}</p>`,
70
+ `<p style="font-size:28px;font-weight:700;letter-spacing:4px;margin:16px 0">${escapeHtml(input.code)}</p>`,
71
+ `<p style="margin:0 0 8px">有效期 ${input.ttlMin} 分钟 / Expires in ${input.ttlMin} minutes.</p>`,
72
+ '<p style="margin:16px 0 0;color:#6b7280">If you did not request this code, you can ignore this email.</p>',
73
+ `<p style="margin:16px 0 0"><a href="${escapeHtml(baseUrl)}">${escapeHtml(baseUrl)}</a></p>`,
74
+ '</div>',
75
+ ].join('');
76
+ return { subject, text, html };
77
+ }
78
+ export async function deliverVerificationCode(input) {
79
+ const env = input.env || process.env;
80
+ const logger = input.logger || console;
81
+ if (!isProtectedEmailEnv(env)) {
82
+ logger.log(`[verify] ${input.purpose} -> ${input.target} code=${input.code} (expires ${input.ttlMin}min)`);
83
+ return { ok: true, provider: 'dev_console' };
84
+ }
85
+ const apiKey = env.RESEND_API_KEY?.trim();
86
+ if (!apiKey)
87
+ return emailDeliveryNotConfigured();
88
+ const fetchImpl = input.fetchImpl || globalThis.fetch;
89
+ if (!fetchImpl)
90
+ return emailDeliveryNotConfigured();
91
+ const baseUrl = env.WEBAZ_PUBLIC_URL?.trim() || env.PUBLIC_BASE_URL?.trim() || DEFAULT_BASE_URL;
92
+ const email = buildVerificationEmail({
93
+ code: input.code,
94
+ purpose: input.purpose,
95
+ ttlMin: input.ttlMin,
96
+ baseUrl,
97
+ });
98
+ const body = {
99
+ from: env.EMAIL_FROM?.trim() || DEFAULT_FROM,
100
+ to: [input.target],
101
+ subject: email.subject,
102
+ text: email.text,
103
+ html: email.html,
104
+ };
105
+ const replyTo = env.EMAIL_REPLY_TO?.trim();
106
+ if (replyTo)
107
+ body.reply_to = replyTo;
108
+ try {
109
+ const response = await fetchImpl('https://api.resend.com/emails', {
110
+ method: 'POST',
111
+ headers: {
112
+ Authorization: `Bearer ${apiKey}`,
113
+ 'Content-Type': 'application/json',
114
+ },
115
+ body: JSON.stringify(body),
116
+ });
117
+ if (!response.ok) {
118
+ logger.warn(`[verify] resend delivery failed: status=${response.status} purpose=${input.purpose}`);
119
+ return emailDeliveryFailed();
120
+ }
121
+ return { ok: true, provider: 'resend' };
122
+ }
123
+ catch {
124
+ logger.warn(`[verify] resend delivery failed: network purpose=${input.purpose}`);
125
+ return emailDeliveryFailed();
126
+ }
127
+ }
@@ -31,6 +31,9 @@ export const WRITE_RULES = [
31
31
  { method: 'POST', re: /^\/api\/skill-market\/[^/]+\/purchase/, action: 'purchase' },
32
32
  { method: 'POST', re: /^\/api\/secondhand\/[^/]+\/order/, action: 'buy_secondhand' },
33
33
  { method: 'POST', re: /^\/api\/group-buys\/[^/]+\/join/, action: 'group_buy_join' },
34
+ // Codex #98 P1:review claim 锁 5 WAZ stake(扣 balance + escrowed)—— 资金写,绝不能落 SAFE,纳入 default-deny 问责门。
35
+ // 只命中 .../:type/:id/claim;其余 reviews 写无规则 → 落通用 'write'(仍 default-deny),GET reviews 由 endpointToAction(GET) 返回 null。
36
+ { method: 'POST', re: /^\/api\/reviews\/[^/]+\/[^/]+\/claim$/, action: 'review_claim' },
34
37
  // #1115 P1:写 PII(收货地址)。原为 (addresses OR profile/default-address);拆两条等价规则,顺序保持。
35
38
  { method: 'WRITE', re: /^\/api\/addresses(\/|$)/, action: 'set_address' },
36
39
  { method: 'WRITE', exact: '/api/profile/default-address', action: 'set_address' },
@@ -55,7 +58,8 @@ export const SAFE_WRITE = [
55
58
  /^\/api\/me\/(delete-cancel|notify-claim-tasks)/,
56
59
  /^\/api\/peers\//, /^\/api\/signaling\//,
57
60
  /^\/api\/product-share\/touch$/, /^\/api\/anchor\/[^/]+\/touch$/,
58
- /^\/api\/reviews\//,
61
+ // Codex #98 P1:不再整段放行 /api/reviews/ —— claim 是 5 WAZ 质押资金写,已上提到 WRITE_RULES(review_claim);
62
+ // GET reviews(recent / :type/:id/claims)无须写 scope(GET 由 endpointToAction 返回 null,不依赖 SAFE)。
59
63
  ];
60
64
  function methodMatches(m, method) {
61
65
  if (m === 'POST')
@@ -12,29 +12,29 @@ import { SOFTWARE_VERSION, CONTRACT_VERSION } from '../version.js';
12
12
  import { capabilityMatrix } from './endpoint-actions.js';
13
13
  const GOALS = [
14
14
  // ── discover / buy ──
15
- { goal: 'Find a specific product by title/SKU/exact desc', when: 'buyer knows what they want (strict match)', action: 'open', endpoint: 'GET /api/search?query=...', mcp_tool: 'webaz_search', pwa: '#buy', see: '② read scope "search"', notes: 'STRICT no fuzzy fallback; 0 hits → guide user to #discover (fuzzy is a human action, not agent-automated).' },
16
- { goal: 'Match a pasted external link (taobao/douyin/xhs/jd/...)', when: 'buyer pastes an off-site product URL', action: 'open', endpoint: 'GET /api/search?external_link=...', mcp_tool: 'webaz_search', pwa: '#buy', notes: 'matches the anchor registry product fingerprint.' },
15
+ { goal: 'Find a specific product by title/SKU/exact desc', when: 'buyer knows what they want (strict match)', action: 'open', endpoint: 'GET /api/products?q=...', mcp_tool: 'webaz_search', pwa: '#buy', see: '② read scope "search"', notes: 'protocol-level STRICT alias match (mode=agent), no fuzzy fallback; 0 hits → guide user to #discover (fuzzy is a human action, not agent-automated).' },
16
+ { goal: 'Match a pasted external link (taobao/douyin/xhs/jd/...)', when: 'buyer pastes an off-site product URL or share text', action: 'open', endpoint: 'POST /api/search-by-link', mcp_tool: 'webaz_search', pwa: '#buy', notes: 'body: { text } (raw paste of share text/URL) OR { external_link: { platform, external_id, external_title, canonical_url } }. Matches the anchor registry product fingerprint.' },
17
17
  { goal: "Browse what's popular near me / same city", when: 'geo discovery, no keyword', action: 'search', endpoint: 'GET /api/nearby', mcp_tool: 'webaz_nearby', pwa: '#nearby', see: '② read scope "search"', notes: 'k-anonymity ≥3.' },
18
18
  { goal: 'Find used / pre-owned / secondhand items', when: 'pre-owned, separate space from new catalog', action: 'open', endpoint: 'GET /api/secondhand', mcp_tool: 'webaz_secondhand', pwa: '#secondhand', notes: 'webaz_search does NOT return secondhand.' },
19
19
  { goal: 'Verify a price before buying', when: 'BEFORE every purchase', action: 'open', endpoint: 'GET /api/products/:id (+ verify)', mcp_tool: 'webaz_verify_price', pwa: '#buy', notes: 'defeats flash-sale/hidden-fee race; protocol only liable for the verified T0 price.' },
20
20
  { goal: 'Place an order (buy a catalog product)', when: 'buyer commits to purchase', action: 'place_order', endpoint: 'POST /api/orders', mcp_tool: 'webaz_place_order', pwa: '#buy', see: '① entity order · ⑧ value flow', notes: 'pass expected_price (T0 guard, 409 on drift).' },
21
21
  { goal: 'Buy a secondhand item', when: 'order a pre-owned listing', action: 'buy_secondhand', endpoint: 'POST /api/secondhand/:id/order', mcp_tool: 'webaz_secondhand', pwa: '#secondhand' },
22
22
  { goal: 'Buy a knowledge skill', when: 'purchase a prompt/template/checklist', action: 'purchase', endpoint: 'POST /api/skill-market/:id/purchase', mcp_tool: 'webaz_skill_market', pwa: '#skill-market', notes: 'content market — distinct from webaz_skill behavior plugins.' },
23
- { goal: 'Bid in an auction', when: 'time-windowed price discovery on listed item', action: 'bid', endpoint: 'POST /api/auctions/:id/bid', mcp_tool: 'webaz_bid', pwa: '#auctions', notes: 'anti-snipe time extension.' },
23
+ { goal: 'Bid in an auction', when: 'time-windowed price discovery on listed item', action: 'bid', endpoint: 'POST /api/auctions/:id/bids', mcp_tool: 'webaz_bid', pwa: '#auctions', notes: 'anti-snipe time extension.' },
24
24
  { goal: 'Post a buy request (RFQ) for sellers to quote', when: 'no good match / bulk / custom / wants competing quotes', action: 'rfq', endpoint: 'POST /api/rfqs', mcp_tool: 'webaz_rfq', pwa: '#rfqs', notes: 'reverse match — buyer posts need + 1% stake.' },
25
25
  // ── sell / fulfill ──
26
- { goal: 'List / update a product', when: 'seller publishes, edits, delists own listing', action: 'list_product', endpoint: 'POST|PUT /api/products', mcp_tool: 'webaz_list_product', pwa: '#me', see: '① entity product', notes: 'system suggests stake ~15% of price (buyer protection).' },
27
- { goal: 'Fulfill an order (accept / ship / deliver / pickup)', when: 'seller or logistics advances fulfilment', action: 'fulfill', endpoint: 'POST /api/orders/:id/{accept|ship|deliver|pickup|transit}', mcp_tool: 'webaz_update_order', pwa: '#me', see: '① order lifecycle · ⑦ liability', notes: 'missing a deadline → auto fault (see order lifecycle).' },
28
- { goal: 'Confirm receipt (buyer closes the order)', when: 'buyer received the goods', action: 'confirm_order', endpoint: 'POST /api/orders/:id/confirm', mcp_tool: 'webaz_update_order', pwa: '#me', notes: 'auto-confirm on confirm_deadline timeout.' },
26
+ { goal: 'List / update a product', when: 'seller publishes, edits, delists own listing', action: 'list_product', endpoint: 'POST /api/products · PUT /api/products/:id', mcp_tool: 'webaz_list_product', pwa: '#me', see: '① entity product', notes: 'POST creates, PUT /:id edits/delists. System suggests stake ~15% of price (buyer protection).' },
27
+ { goal: 'Fulfill an order (accept / ship / deliver / pickup)', when: 'seller or logistics advances fulfilment', action: 'fulfill', endpoint: 'POST /api/orders/:id/action', mcp_tool: 'webaz_update_order', pwa: '#me', see: '① order lifecycle · ⑦ liability', notes: 'single state-machine endpoint; body { action } ∈ {accept|ship|pickup|transit|deliver|confirm|dispute}. Missing a deadline → auto fault.' },
28
+ { goal: 'Confirm receipt (buyer closes the order)', when: 'buyer received the goods', action: 'confirm_order', endpoint: 'POST /api/orders/:id/action', mcp_tool: 'webaz_update_order', pwa: '#me', notes: 'body { action: "confirm" }. Auto-confirm on confirm_deadline timeout.' },
29
29
  // ── dispute / verify ──
30
30
  { goal: 'Respond to a dispute as a party', when: 'a counterparty opened a dispute on your order', action: 'dispute_respond', endpoint: 'POST /api/disputes/:id/respond', mcp_tool: 'webaz_dispute', pwa: '#me', see: '① entity dispute · ⑦ liability' },
31
31
  { goal: 'Look up public dispute precedents', when: 'assess a seller / understand likely ruling', action: 'open', endpoint: 'GET /api/disputes/cases (+ /by-product/:id)', mcp_tool: 'webaz_dispute', pwa: '#disputes', see: '① entity dispute', notes: 'redacted post-ruling cases; amount is bucketed.' },
32
32
  { goal: 'Verify an agent passport / external anchor / AP2 mandate', when: 'check a counterparty/data is genuine', action: 'open', endpoint: 'GET /.well-known/webaz-verifiability.json', mcp_tool: null, pwa: '(n/a)', see: '⑤ verifiability index', notes: 'offline-verifiable where signed; order-chain is integrity-only.' },
33
33
  // ── participate / social ──
34
34
  { goal: 'Become a value participant (earn/pay/stake)', when: 'integrate as seller/logistics/verifier/insurer/etc.', action: 'open', endpoint: 'GET /.well-known/webaz-economic.json', mcp_tool: null, pwa: '(n/a)', see: '⑧ economic participation', notes: 'roles + live rates + collateral + conserved liability.' },
35
- { goal: 'Communicate with a trade counterparty', when: 'ask seller a question / coordinate an order', action: 'chat', endpoint: 'POST /api/conversations', mcp_tool: 'webaz_chat', pwa: '#messages', notes: 'every message attaches to a trade context — not general LLM chat.' },
35
+ { goal: 'Communicate with a trade counterparty', when: 'ask seller a question / coordinate an order', action: 'chat', endpoint: 'POST /api/conversations/start', mcp_tool: 'webaz_chat', pwa: '#messages', notes: 'start a thread; then POST /api/conversations/:id/messages. Every message attaches to a trade context — not general LLM chat.' },
36
36
  { goal: 'Share / refer a product for commission', when: 'promote a listing; attributed sales pay commission', action: 'share', endpoint: 'POST /api/shareables', mcp_tool: 'webaz_shareables', pwa: '#me', see: '⑧ promoter role' },
37
- { goal: 'Publish or fund a charity wish / community fund', when: 'community mutual-aid', action: 'charity', endpoint: 'POST /api/wishes · POST /api/charity', mcp_tool: 'webaz_charity', pwa: '#charity', notes: 'distinct from place_order donation_pct.' },
37
+ { goal: 'Publish or fund a charity wish / community fund', when: 'community mutual-aid', action: 'charity', endpoint: 'POST /api/wishes', mcp_tool: 'webaz_charity', pwa: '#charity', notes: 'publish a wish; fund the shared pool via POST /api/charity/fund/donate. Distinct from place_order donation_pct.' },
38
38
  { goal: 'Donate to the community fund', when: 'contribute to the shared fund', action: 'donate', endpoint: 'POST /api/charity/fund/donate', mcp_tool: 'webaz_charity', pwa: '#charity' },
39
39
  // ── self state ──
40
40
  { goal: 'Set a shipping address (PII write)', when: 'before a shipped order', action: 'set_address', endpoint: 'POST /api/addresses', mcp_tool: 'webaz_default_address', pwa: '#me', see: '② write_action set_address (元规则#3 PII gate)' },
@@ -0,0 +1,62 @@
1
+ export function createHumanPresence(db, getProtocolParam) {
2
+ // 验证 gate token:被业务端点(如 /api/wallet/withdraw)消费。
3
+ // M-1: 改 CAS — 先抢占性 UPDATE,只有 changes=1 才认为本次成功消费;然后再读 row 校验
4
+ // user/purpose/业务字段。多副本部署也安全。
5
+ function consumeGateToken(userId, token, purpose, validate) {
6
+ if (!token)
7
+ return { ok: false, reason: '缺少 X-WebAuthn-Token' };
8
+ // 先抢占:未消费 + 未过期 才能 mark consumed
9
+ const claim = db.prepare(`UPDATE webauthn_gate_tokens
10
+ SET consumed_at = datetime('now')
11
+ WHERE id = ? AND consumed_at IS NULL AND expires_at > datetime('now')`).run(token);
12
+ if (claim.changes !== 1) {
13
+ // 抢占失败的两种原因区分(仅用于 reason 文案)
14
+ const exist = db.prepare('SELECT consumed_at FROM webauthn_gate_tokens WHERE id = ?').get(token);
15
+ if (!exist)
16
+ return { ok: false, reason: 'token 不存在' };
17
+ if (exist.consumed_at)
18
+ return { ok: false, reason: 'token 已使用' };
19
+ return { ok: false, reason: 'token 已过期' };
20
+ }
21
+ // 已抢占 → 读 row 校验 user/purpose/业务字段;若校验失败 token 仍然作废(防止枚举攻击下的重试)
22
+ const row = db.prepare(`SELECT user_id, purpose, purpose_data FROM webauthn_gate_tokens WHERE id = ?`)
23
+ .get(token);
24
+ if (row.user_id !== userId)
25
+ return { ok: false, reason: 'token 用户不匹配' };
26
+ if (row.purpose !== purpose)
27
+ return { ok: false, reason: 'token 用途不匹配' };
28
+ let data = null;
29
+ try {
30
+ data = row.purpose_data ? JSON.parse(row.purpose_data) : null;
31
+ }
32
+ catch { }
33
+ if (!validate(data))
34
+ return { ok: false, reason: 'token 业务参数不匹配' };
35
+ return { ok: true };
36
+ }
37
+ // ─── Agent 治理铁律:人工铁律节点 ───
38
+ // 关键节点(verifier 投票 / arbitrator 仲裁 / agent_revoke / delete_passkey / identity_claim)必须真实
39
+ // 人工参与,agent 代操作被拦截。实现:要求 webauthn_gate_token(一次性 · 60s)+ 协议参数开关。
40
+ // is_system fixture 旁路只对 vote/arbitrate 生效(有白名单表);其余无豁免。
41
+ function requireHumanPresence(userId, purpose, token, paramKey, validate = () => true) {
42
+ const enabled = Number(getProtocolParam(paramKey, 1)) === 1;
43
+ if (!enabled)
44
+ return { ok: true }; // 协议参数关闭 → 不强制
45
+ if (purpose === 'vote') {
46
+ const wl = db.prepare('SELECT is_system FROM verifier_whitelist WHERE user_id = ?').get(userId);
47
+ if (wl?.is_system === 1)
48
+ return { ok: true };
49
+ }
50
+ else if (purpose === 'arbitrate') {
51
+ const wl = db.prepare('SELECT is_system FROM arbitrator_whitelist WHERE user_id = ?').get(userId);
52
+ if (wl?.is_system === 1)
53
+ return { ok: true };
54
+ }
55
+ const result = consumeGateToken(userId, token, purpose, validate);
56
+ if (!result.ok) {
57
+ return { ok: false, error_code: 'HUMAN_PRESENCE_REQUIRED', reason: result.reason || '此操作需真实人工 WebAuthn 验证', required_when_enabled: true };
58
+ }
59
+ return { ok: true };
60
+ }
61
+ return { consumeGateToken, requireHumanPresence };
62
+ }