@seasonkoh/webaz 0.1.26 → 0.1.28

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 (99) hide show
  1. package/LICENSE +2 -2
  2. package/NOTICE +24 -3
  3. package/README.md +74 -330
  4. package/README.zh-CN.md +419 -0
  5. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
  6. package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
  7. package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +270 -82
  10. package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
  11. package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
  12. package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
  13. package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
  14. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
  15. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
  16. package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
  20. package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
  21. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
  22. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
  23. package/dist/ledger.js +1 -1
  24. package/dist/pwa/admin-audit.js +38 -0
  25. package/dist/pwa/anti-abuse-thresholds.js +135 -0
  26. package/dist/pwa/cf-origin-guard.js +33 -0
  27. package/dist/pwa/contract-fingerprint.js +1 -0
  28. package/dist/pwa/data/onboarding-cases.js +2 -2
  29. package/dist/pwa/data/onboarding-quiz.js +1 -1
  30. package/dist/pwa/economic-participation.js +2 -2
  31. package/dist/pwa/integration-contract.js +46 -4
  32. package/dist/pwa/internal/pv-settlement.js +12 -0
  33. package/dist/pwa/internal/wallet-signer.js +26 -0
  34. package/dist/pwa/public/app-account.js +977 -0
  35. package/dist/pwa/public/app-admin.js +608 -0
  36. package/dist/pwa/public/app-agents.js +63 -0
  37. package/dist/pwa/public/app-ai.js +2162 -0
  38. package/dist/pwa/public/app-contribution.js +836 -0
  39. package/dist/pwa/public/app-discover.js +1296 -0
  40. package/dist/pwa/public/app-listings.js +226 -0
  41. package/dist/pwa/public/app-profile.js +1692 -0
  42. package/dist/pwa/public/app-seller.js +199 -0
  43. package/dist/pwa/public/app-shop.js +1145 -0
  44. package/dist/pwa/public/app.js +15075 -23960
  45. package/dist/pwa/public/i18n.js +31 -28
  46. package/dist/pwa/public/index.html +11 -1
  47. package/dist/pwa/public/openapi.json +4851 -2776
  48. package/dist/pwa/pv-kill-switch.js +31 -0
  49. package/dist/pwa/routes/admin-admins.js +48 -1
  50. package/dist/pwa/routes/admin-analytics.js +1 -10
  51. package/dist/pwa/routes/admin-atomic.js +4 -17
  52. package/dist/pwa/routes/admin-operator-claims.js +280 -0
  53. package/dist/pwa/routes/admin-reports.js +4 -26
  54. package/dist/pwa/routes/admin-tokenomics.js +2 -76
  55. package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
  56. package/dist/pwa/routes/admin-users-query.js +23 -1
  57. package/dist/pwa/routes/admin-wallet-ops.js +1 -1
  58. package/dist/pwa/routes/agent-grants.js +255 -0
  59. package/dist/pwa/routes/auth-read.js +1 -5
  60. package/dist/pwa/routes/auth-register.js +3 -13
  61. package/dist/pwa/routes/build-task-quota.js +113 -0
  62. package/dist/pwa/routes/claim-verify.js +15 -11
  63. package/dist/pwa/routes/contribution-facts.js +18 -0
  64. package/dist/pwa/routes/dispute-cases.js +5 -4
  65. package/dist/pwa/routes/growth.js +3 -3
  66. package/dist/pwa/routes/orders-action.js +27 -10
  67. package/dist/pwa/routes/orders-create.js +1 -1
  68. package/dist/pwa/routes/products-meta.js +19 -6
  69. package/dist/pwa/routes/profile-placement.js +1 -1
  70. package/dist/pwa/routes/promoter.js +10 -29
  71. package/dist/pwa/routes/public-build-tasks.js +5 -1
  72. package/dist/pwa/routes/public-utils.js +9 -12
  73. package/dist/pwa/routes/referral.js +5 -26
  74. package/dist/pwa/routes/rewards-apply.js +3 -2
  75. package/dist/pwa/routes/share-redirects.js +1 -1
  76. package/dist/pwa/routes/shareables-interactions.js +2 -1
  77. package/dist/pwa/routes/task-proposals.js +85 -9
  78. package/dist/pwa/routes/users-public.js +1 -4
  79. package/dist/pwa/routes/wallet-read.js +2 -14
  80. package/dist/pwa/routes/webauthn.js +7 -2
  81. package/dist/pwa/server-schema.js +9 -0
  82. package/dist/pwa/server.js +319 -2034
  83. package/dist/runtime/agent-grant-scopes.js +128 -0
  84. package/dist/runtime/agent-grant-verifier.js +67 -0
  85. package/dist/runtime/agent-pairing.js +60 -0
  86. package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
  87. package/dist/runtime/webaz-schema-helpers.js +1848 -0
  88. package/dist/settlement-math.js +3 -3
  89. package/dist/version.js +6 -4
  90. package/package.json +43 -8
  91. package/dist/index.js +0 -182
  92. package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
  93. package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
  94. package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
  95. package/dist/test-dispute.js +0 -153
  96. package/dist/test-manifest.js +0 -61
  97. package/dist/test-mcp-tools.js +0 -135
  98. package/dist/test-reputation.js +0 -116
  99. package/dist/test-skill-market.js +0 -101
@@ -0,0 +1,31 @@
1
+ /** Participation recording switch (default ON — only an explicit '0' disables). */
2
+ export const PARTICIPATION_RECORDING_KEY = 'participation_recording_active';
3
+ /** Matching-rewards operational on-switch + legal/governance clearance marker (both required, default OFF). */
4
+ export const MATCHING_REWARDS_ACTIVE_KEY = 'matching_rewards_active';
5
+ export const MATCHING_REWARDS_CLEARED_KEY = 'matching_rewards_activation_cleared';
6
+ const readParam = (db, k) => db.prepare('SELECT value FROM protocol_params WHERE key = ?').get(k)?.value;
7
+ /**
8
+ * DEFAULT ON. Neutral participation/contribution recording (PV ledger + PV aggregation) is allowed unless
9
+ * explicitly turned off. Absent param / query error → ON (recording is safe + neutral; not a payout).
10
+ */
11
+ export function participationRecordingActive(db) {
12
+ try {
13
+ return readParam(db, PARTICIPATION_RECORDING_KEY) !== '0'; // default ON; only an explicit '0' disables
14
+ }
15
+ catch {
16
+ return true; // recording is neutral/no-payout — failing to ON preserves the visible-participation principle
17
+ }
18
+ }
19
+ /**
20
+ * DEFAULT OFF. Matching-rewards settlement + payout + any reward distribution. True ONLY when the
21
+ * operational on-switch AND the legal/governance-clearance marker are BOTH '1'. FAIL-CLOSED on any
22
+ * missing param / query error → false (never pay on uncertainty; never break the order-settlement hot path).
23
+ */
24
+ export function matchingRewardsActive(db) {
25
+ try {
26
+ return readParam(db, MATCHING_REWARDS_ACTIVE_KEY) === '1' && readParam(db, MATCHING_REWARDS_CLEARED_KEY) === '1';
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
@@ -140,11 +140,58 @@ export function registerAdminAdminsRoutes(app, deps) {
140
140
  catch { }
141
141
  rolesArr = rolesArr.filter(r => r !== 'admin');
142
142
  const newRole = rolesArr[0] || 'buyer';
143
- db.prepare(`UPDATE users SET role = ?, roles = ?, admin_type = NULL, admin_scope = NULL, updated_at = datetime('now') WHERE id = ?`)
143
+ db.prepare(`UPDATE users SET role = ?, roles = ?, admin_type = NULL, admin_scope = NULL, admin_permissions = NULL, updated_at = datetime('now') WHERE id = ?`)
144
144
  .run(newRole, JSON.stringify(rolesArr), targetId);
145
145
  db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
146
146
  .run(generateId('audit'), root.id, 'admin_revoke', 'user', targetId, JSON.stringify({ revoked_type: target.admin_type, new_role: newRole }));
147
147
  })();
148
148
  res.json({ ok: true });
149
149
  });
150
+ // ── ROOT-only emergency freeze of an admin: suspend + strip ALL admin capability + revoke sessions ──
151
+ // For incident response (e.g. a compromised / rogue admin). Atomic. Cannot freeze self or the last root.
152
+ app.post('/api/admin/admins/:id/emergency-freeze', async (req, res) => {
153
+ const root = requireRootAdmin(req, res);
154
+ if (!root)
155
+ return;
156
+ const targetId = req.params.id;
157
+ if (targetId === root.id)
158
+ return void res.json({ error: '不能冻结自己' });
159
+ const reason = (req.body?.reason ? String(req.body.reason).slice(0, 200) : '') || 'emergency admin freeze';
160
+ const target = await dbOne(`SELECT id, role, roles, admin_type FROM users WHERE id = ?`, [targetId]);
161
+ if (!target)
162
+ return void res.json({ error: '用户不存在' });
163
+ if (!target.admin_type)
164
+ return void res.json({ error: '目标必须是 admin 账号' });
165
+ if (target.admin_type === 'root') {
166
+ const rootCount = (await dbOne(`SELECT COUNT(1) as n FROM users WHERE admin_type = 'root'`, [])).n;
167
+ if (rootCount <= 1)
168
+ return void res.json({ error: '不能冻结最后一个 root admin' });
169
+ }
170
+ let sessionsRevoked = 0;
171
+ db.transaction(() => {
172
+ let rolesArr = [];
173
+ try {
174
+ rolesArr = JSON.parse(target.roles || '[]');
175
+ }
176
+ catch { }
177
+ const oldRoles = [...rolesArr];
178
+ rolesArr = rolesArr.filter(r => r !== 'admin');
179
+ const newRole = rolesArr[0] || 'buyer';
180
+ // 1) suspend
181
+ db.prepare(`INSERT INTO user_moderation (user_id, suspended, reason, suspended_by, suspended_at)
182
+ VALUES (?, 1, ?, ?, datetime('now'))
183
+ ON CONFLICT(user_id) DO UPDATE SET suspended = 1, reason = excluded.reason, suspended_by = excluded.suspended_by, suspended_at = excluded.suspended_at`)
184
+ .run(targetId, reason, root.id);
185
+ // 2) strip ALL admin capability
186
+ db.prepare(`UPDATE users SET role = ?, roles = ?, admin_type = NULL, admin_scope = NULL, admin_permissions = NULL, updated_at = datetime('now') WHERE id = ?`)
187
+ .run(newRole, JSON.stringify(rolesArr), targetId);
188
+ // 3) revoke all active sessions
189
+ const r = db.prepare(`UPDATE user_sessions SET revoked_at = datetime('now') WHERE user_id = ? AND revoked_at IS NULL`).run(targetId);
190
+ sessionsRevoked = r.changes;
191
+ // 4) audit
192
+ db.prepare(`INSERT INTO admin_audit_log (id, admin_id, action, target_type, target_id, detail) VALUES (?,?,?,?,?,?)`)
193
+ .run(generateId('audit'), root.id, 'admin_emergency_freeze', 'user', targetId, JSON.stringify({ old_admin_type: target.admin_type, old_roles: oldRoles, sessions_revoked: sessionsRevoked, reason }));
194
+ })();
195
+ res.json({ ok: true, frozen: targetId, sessions_revoked: sessionsRevoked });
196
+ });
150
197
  }
@@ -215,24 +215,15 @@ export function registerAdminAnalyticsRoutes(app, deps) {
215
215
  AND (vs.suspended_until IS NULL OR vs.suspended_until < datetime('now'))
216
216
  `));
217
217
  const tokenomics = await (async () => {
218
- const gf = await dbOne("SELECT pool_balance, total_scores_pending, current_n, last_settled_at FROM global_fund WHERE id=1");
219
- const mb = await dbOne("SELECT balance FROM management_bonus_pool WHERE id=1");
218
+ // matching-rewards admin metrics (pool / scores / mgmt-bonus / binary payout) removed — engine excised (#401).
220
219
  const pendingLedger = (await dbOne("SELECT COUNT(*) as n FROM pv_ledger WHERE processed = 0")).n;
221
220
  const commCount = (await dbOne("SELECT COUNT(*) as n, COALESCE(SUM(amount),0) as t FROM commission_records"));
222
221
  const dirtyUsers = (await dbOne("SELECT COUNT(*) as n FROM users WHERE pv_dirty_at IS NOT NULL")).n;
223
- const matchedTotal = (await dbOne("SELECT COUNT(*) as n, COALESCE(SUM(waz_amount),0) as w FROM binary_score_records WHERE settled_at IS NOT NULL"));
224
222
  return {
225
- pool_balance: Number(gf?.pool_balance ?? 0),
226
- scores_pending: Number(gf?.total_scores_pending ?? 0),
227
- current_n: Number(gf?.current_n ?? 0),
228
- last_settled_at: gf?.last_settled_at,
229
- management_bonus: Number(mb?.balance ?? 0),
230
223
  ledger_pending: pendingLedger,
231
224
  dirty_users: dirtyUsers,
232
225
  commission_records: commCount.n,
233
226
  commission_total: commCount.t,
234
- binary_settled: matchedTotal.n,
235
- binary_waz_total: matchedTotal.w,
236
227
  };
237
228
  })();
238
229
  res.json({
@@ -1,5 +1,8 @@
1
1
  export function registerAdminAtomicRoutes(app, deps) {
2
- const { requireProtocolAdmin, processPvLedger, runBinarySettlement, executeSafeSettlementCron, logAdminAction } = deps;
2
+ // matching settlement / payout endpoints removed engine excised (#401). Only neutral
3
+ // participation-recording (process-ledger) remains. (runBinarySettlement / executeSafeSettlementCron
4
+ // are still injected by the server but no longer wired to any endpoint here.)
5
+ const { requireProtocolAdmin, processPvLedger, logAdminAction } = deps;
3
6
  app.post('/api/admin/atomic/process-ledger', (req, res) => {
4
7
  const admin = requireProtocolAdmin(req, res);
5
8
  if (!admin)
@@ -8,20 +11,4 @@ export function registerAdminAtomicRoutes(app, deps) {
8
11
  logAdminAction(admin.id, 'atomic_process_ledger', 'protocol', null, { processed });
9
12
  res.json({ processed });
10
13
  });
11
- app.post('/api/admin/atomic/run-settlement', (req, res) => {
12
- const admin = requireProtocolAdmin(req, res);
13
- if (!admin)
14
- return;
15
- const settled = runBinarySettlement();
16
- logAdminAction(admin.id, 'atomic_run_settlement', 'protocol', null, { settled });
17
- res.json({ settled });
18
- });
19
- app.post('/api/admin/atomic/distribute', (req, res) => {
20
- const admin = requireProtocolAdmin(req, res);
21
- if (!admin)
22
- return;
23
- const result = executeSafeSettlementCron();
24
- logAdminAction(admin.id, 'atomic_distribute', 'protocol', null, { result });
25
- res.json(result);
26
- });
27
14
  }
@@ -0,0 +1,280 @@
1
+ import { logAdminAction } from '../admin-audit.js';
2
+ import { proposeClaim, confirmClaim, approveClaim, rejectClaim, revokeApprovedClaim, deriveClaimState, listClaimsForSeat, listPendingConfirmationsForContributor, listAllClaims, emitClaimNotifications, claimedEventIdOfApproved, requestUnlink, approveUnlink, rejectUnlink, pendingUnlinkForApproved, listContributorRelationships, listPendingUnlinkRequests, } from '../../layer2-business/L2-9-contribution/admin-operator-claim-workflow.js';
3
+ function httpFor(code) {
4
+ switch (code) {
5
+ case 'claim_not_found':
6
+ case 'approved_not_found':
7
+ case 'request_not_found': return 404;
8
+ case 'not_admin':
9
+ case 'not_root':
10
+ case 'not_contributor':
11
+ case 'not_party':
12
+ case 'gate_failed': return 403;
13
+ case 'bad_state':
14
+ case 'not_confirmed':
15
+ case 'contributor_rejected':
16
+ case 'already_pending': return 409;
17
+ default: return 400; // invalid_input / contributor_not_found / self_link_* / self_related_* / dishonest_marking
18
+ }
19
+ }
20
+ /* eslint-disable @typescript-eslint/no-explicit-any */
21
+ function shape(v) {
22
+ if (!v)
23
+ return null;
24
+ return {
25
+ claimed_event_id: v.claimed.event_id,
26
+ admin_account_id: v.claimed.admin_account_id,
27
+ contributor_account_id: v.claimed.contributor_account_id,
28
+ status: v.status,
29
+ proposed_at: v.claimed.created_at,
30
+ confirmation: v.confirmation ? { decision: v.confirmation.decision, decided_by: v.confirmation.decided_by, at: v.confirmation.created_at } : null,
31
+ approved: v.approved ? { event_id: v.approved.event_id, at: v.approved.created_at } : null,
32
+ };
33
+ }
34
+ export function registerAdminOperatorClaimRoutes(app, deps) {
35
+ const { db, errorRes, auth, requireAdmin, requireRootAdmin, consumeGateToken } = deps;
36
+ // Best-effort: a notify failure must never roll back / fail the claim mutation.
37
+ const notify = (kind, claimedEventId) => { try {
38
+ emitClaimNotifications(db, kind, claimedEventId);
39
+ }
40
+ catch { /* notifications are degradable */ } };
41
+ // shape + surface whether an ACTIVE approved claim has a pending unlink request (drives the UI).
42
+ const shapeClaim = (v) => {
43
+ const base = shape(v);
44
+ if (base && v?.status === 'approved' && v.approved) {
45
+ const pu = pendingUnlinkForApproved(db, v.approved.event_id);
46
+ base.unlink_pending = pu ? { request_event_id: pu.request_event_id, requested_by: pu.requested_by, requester_role: pu.requester_role, at: pu.created_at } : null;
47
+ }
48
+ return base;
49
+ };
50
+ // ── admin proposes linking THEIR OWN seat to a contributor account ──
51
+ app.post('/api/admin/operator-claims', (req, res) => {
52
+ const admin = requireAdmin(req, res);
53
+ if (!admin)
54
+ return;
55
+ const contributorAccountId = String((req.body?.contributor_account_id ?? '')).trim();
56
+ const rationale = req.body?.rationale ? String(req.body.rationale) : undefined;
57
+ if (!contributorAccountId)
58
+ return errorRes(res, 400, 'invalid_input', 'contributor_account_id required');
59
+ try {
60
+ const out = db.transaction(() => {
61
+ const r = proposeClaim(db, { actorAdminId: admin.id, contributorAccountId, rationale });
62
+ if (!r.ok)
63
+ return r;
64
+ logAdminAction(db, { adminId: admin.id, action: 'operator_claim.propose', targetType: 'user', targetId: contributorAccountId, detail: { claimed_event_id: r.claimedEventId }, context: { actorType: 'admin_account', agentMode: 'human_direct' } });
65
+ return r;
66
+ })();
67
+ if (!out.ok)
68
+ return errorRes(res, httpFor(out.code), out.code, out.message);
69
+ notify('proposed', out.claimedEventId); // → tell the contributor to confirm
70
+ res.json({ ok: true, claim: shape(deriveClaimState(db, out.claimedEventId)) });
71
+ }
72
+ catch (e) {
73
+ errorRes(res, 500, 'internal', e.message);
74
+ }
75
+ });
76
+ // ── the calling admin's own seat: its claims + states (shapeClaim → carries unlink_pending so the
77
+ // admin-seat owner can request/track unlink on their own active claims) ──
78
+ app.get('/api/admin/operator-claims/me', (req, res) => {
79
+ const admin = requireAdmin(req, res);
80
+ if (!admin)
81
+ return;
82
+ res.json({ seat: admin.id, claims: listClaimsForSeat(db, admin.id).map(shapeClaim) });
83
+ });
84
+ // ── contributor: claims pointing at ME awaiting my confirmation ──
85
+ app.get('/api/me/operator-claim-confirmations', (req, res) => {
86
+ const user = auth(req, res);
87
+ if (!user)
88
+ return;
89
+ res.json({ pending: listPendingConfirmationsForContributor(db, user.id).map(shape) });
90
+ });
91
+ // ── contributor accepts/rejects a claim pointing at them ──
92
+ app.post('/api/me/operator-claim-confirmations/:claimedEventId', (req, res) => {
93
+ const user = auth(req, res);
94
+ if (!user)
95
+ return;
96
+ const decision = String(req.body?.decision ?? '');
97
+ const rationale = req.body?.rationale ? String(req.body.rationale) : undefined;
98
+ try {
99
+ const out = db.transaction(() => {
100
+ const r = confirmClaim(db, { claimedEventId: String(req.params.claimedEventId), deciderId: user.id, decision: decision, rationale });
101
+ if (!r.ok)
102
+ return r;
103
+ logAdminAction(db, { adminId: user.id, action: 'operator_claim.confirm', targetType: 'operator_claim', targetId: String(req.params.claimedEventId), detail: { decision }, context: { actorType: 'human', agentMode: 'human_direct' } });
104
+ return r;
105
+ })();
106
+ if (!out.ok)
107
+ return errorRes(res, httpFor(out.code), out.code, out.message);
108
+ notify(decision === 'accepted' ? 'accepted' : 'rejected_by_contributor', String(req.params.claimedEventId)); // → root to approve, or admin that it was declined
109
+ res.json({ ok: true, claim: shape(deriveClaimState(db, String(req.params.claimedEventId))) });
110
+ }
111
+ catch (e) {
112
+ errorRes(res, 500, 'internal', e.message);
113
+ }
114
+ });
115
+ // ── ROOT: review queue (all claims, optional ?status=) ──
116
+ app.get('/api/admin/operator-claims', (req, res) => {
117
+ const root = requireRootAdmin(req, res);
118
+ if (!root)
119
+ return;
120
+ const status = req.query.status ? String(req.query.status) : undefined;
121
+ res.json({ claims: listAllClaims(db, status).map(shape) });
122
+ });
123
+ // ── ROOT: claim detail ──
124
+ app.get('/api/admin/operator-claims/:claimedEventId', (req, res) => {
125
+ const root = requireRootAdmin(req, res);
126
+ if (!root)
127
+ return;
128
+ const v = deriveClaimState(db, String(req.params.claimedEventId));
129
+ if (!v)
130
+ return errorRes(res, 404, 'claim_not_found', 'no such claim');
131
+ res.json({ claim: shape(v) });
132
+ });
133
+ // Shared tx + audit + error boilerplate. Each ROOT route below inlines requireRootAdmin(req, res)
134
+ // FIRST (so the api-docs/OpenAPI generator detects the auth gate) then delegates here.
135
+ const runRootMutation = (res, root, req, action, targetId, fn, notifyKind, notifyClaimedEventId) => {
136
+ try {
137
+ const out = db.transaction(() => {
138
+ const r = fn();
139
+ if (!r.ok)
140
+ return r;
141
+ logAdminAction(db, { adminId: root.id, action, targetType: 'operator_claim', targetId, detail: { result: r }, context: { actorType: 'admin_account', agentMode: 'human_direct', approvalKind: req.body?.approval_kind, conflictDisclosure: req.body?.conflict_disclosure } });
142
+ return r;
143
+ })();
144
+ if (!out.ok)
145
+ return errorRes(res, httpFor(out.code), out.code, out.message);
146
+ if (notifyKind && notifyClaimedEventId)
147
+ notify(notifyKind, notifyClaimedEventId); // → tell contributor + proposing admin
148
+ res.json({ ok: true, result: out });
149
+ }
150
+ catch (e) {
151
+ errorRes(res, 500, 'internal', e.message);
152
+ }
153
+ };
154
+ // ── ROOT: approve a proposed-or-confirmed claim ──
155
+ app.post('/api/admin/operator-claims/:claimedEventId/approve', (req, res) => {
156
+ const root = requireRootAdmin(req, res);
157
+ if (!root)
158
+ return;
159
+ const id = String(req.params.claimedEventId);
160
+ runRootMutation(res, root, req, 'operator_claim.approve', id, () => approveClaim(db, { claimedEventId: id, approverId: root.id, approvalKind: String(req.body?.approval_kind ?? ''), conflictDisclosure: String(req.body?.conflict_disclosure ?? ''), rationale: req.body?.rationale ? String(req.body.rationale) : undefined }), 'approved', id);
161
+ });
162
+ // ── ROOT: reject a still-proposed/confirmed claim ──
163
+ app.post('/api/admin/operator-claims/:claimedEventId/reject', (req, res) => {
164
+ const root = requireRootAdmin(req, res);
165
+ if (!root)
166
+ return;
167
+ const id = String(req.params.claimedEventId);
168
+ runRootMutation(res, root, req, 'operator_claim.reject', id, () => rejectClaim(db, { claimedEventId: id, approverId: root.id, rationale: req.body?.rationale ? String(req.body.rationale) : undefined }), 'rejected_by_root', id);
169
+ });
170
+ // ── ROOT: revoke an APPROVED (active) claim ──
171
+ app.post('/api/admin/operator-claims/:approvedEventId/revoke', (req, res) => {
172
+ const root = requireRootAdmin(req, res);
173
+ if (!root)
174
+ return;
175
+ const id = String(req.params.approvedEventId);
176
+ const claimedId = claimedEventIdOfApproved(db, id) ?? undefined; // resolve the claim behind the approval for notifications
177
+ runRootMutation(res, root, req, 'operator_claim.revoke', id, () => revokeApprovedClaim(db, { approvedEventId: id, revokerId: root.id, rationale: req.body?.rationale ? String(req.body.rationale) : undefined }), 'revoked', claimedId);
178
+ });
179
+ // ── contributor self-view: ALL relationships pointing at me (pending/confirmed/approved/history) ──
180
+ app.get('/api/me/operator-claims', (req, res) => {
181
+ const user = auth(req, res);
182
+ if (!user)
183
+ return;
184
+ res.json({ relationships: listContributorRelationships(db, user.id).map(shapeClaim) });
185
+ });
186
+ // ── EITHER PARTY requests UNLINK of an active approved claim — passkey-gated (not just a UI confirm) ──
187
+ app.post('/api/me/operator-claims/:approvedEventId/request-unlink', (req, res) => {
188
+ const user = auth(req, res);
189
+ if (!user)
190
+ return;
191
+ const approvedEventId = String(req.params.approvedEventId);
192
+ const webauthnToken = req.body?.webauthn_token ? String(req.body.webauthn_token) : undefined;
193
+ const reason = req.body?.reason ? String(req.body.reason) : undefined;
194
+ // fresh passkey gate bound to THIS approved claim (purpose 'operator_claim_unlink'). TODO: could be
195
+ // promoted to the typed requireHumanPresence iron-rule purpose; consumeGateToken is the same token store.
196
+ const gate = consumeGateToken(user.id, webauthnToken, 'operator_claim_unlink', (data) => !!data && typeof data === 'object' && data.approved_event_id === approvedEventId);
197
+ if (!gate.ok)
198
+ return errorRes(res, 403, 'gate_failed', gate.reason || '需要 Passkey 验证(重新发起解除申请)');
199
+ try {
200
+ const out = db.transaction(() => {
201
+ const r = requestUnlink(db, { approvedEventId, requesterId: user.id, reason, humanAuthRef: webauthnToken });
202
+ if (!r.ok)
203
+ return r;
204
+ logAdminAction(db, { adminId: user.id, action: 'operator_claim.unlink_request', targetType: 'operator_claim', targetId: approvedEventId, detail: { request_event_id: r.requestEventId, requester_role: r.requesterRole, reason: reason ?? null, human_auth_ref: webauthnToken ?? null }, context: { actorType: 'human', agentMode: 'human_direct', humanAuthorizationId: webauthnToken } });
205
+ return r;
206
+ })();
207
+ if (!out.ok)
208
+ return errorRes(res, httpFor(out.code), out.code, out.message);
209
+ notify('unlink_requested', claimedEventIdOfApproved(db, approvedEventId) ?? ''); // → root review queue
210
+ res.json({ ok: true, request_event_id: out.requestEventId, requester_role: out.requesterRole });
211
+ }
212
+ catch (e) {
213
+ errorRes(res, 500, 'internal', e.message);
214
+ }
215
+ });
216
+ // ── ROOT: pending unlink requests (review queue). Path under /unlink/ to avoid the /:claimedEventId match.
217
+ // self_or_related flags each request the viewing root is a party to → the UI then REQUIRES honest marking. ──
218
+ app.get('/api/admin/operator-claims/unlink/requests', (req, res) => {
219
+ const root = requireRootAdmin(req, res);
220
+ if (!root)
221
+ return;
222
+ const rid = root.id;
223
+ const requests = listPendingUnlinkRequests(db).map((r) => ({
224
+ ...r,
225
+ self_or_related: rid === r.admin_account_id || rid === r.contributor_account_id || rid === r.requested_by,
226
+ }));
227
+ res.json({ requests });
228
+ });
229
+ // ── ROOT: approve an unlink request → revokes the claim. When root is self-or-related to the
230
+ // relationship/request, approval_kind + conflict_disclosure are required (governance honesty). ──
231
+ app.post('/api/admin/operator-claims/unlink/:requestEventId/approve', (req, res) => {
232
+ const root = requireRootAdmin(req, res);
233
+ if (!root)
234
+ return;
235
+ const id = String(req.params.requestEventId);
236
+ const approvalKind = req.body?.approval_kind ? String(req.body.approval_kind) : undefined;
237
+ const conflictDisclosure = req.body?.conflict_disclosure ? String(req.body.conflict_disclosure) : undefined;
238
+ try {
239
+ const out = db.transaction(() => {
240
+ const r = approveUnlink(db, { requestEventId: id, approverId: root.id, approvalKind, conflictDisclosure });
241
+ if (!r.ok)
242
+ return r;
243
+ logAdminAction(db, { adminId: root.id, action: 'operator_claim.unlink_approve', targetType: 'operator_claim', targetId: id, detail: { result: r }, context: { actorType: 'admin_account', agentMode: 'human_direct', approvalKind: r.approvalKind, conflictDisclosure: r.conflictDisclosure } });
244
+ return r;
245
+ })();
246
+ if (!out.ok)
247
+ return errorRes(res, httpFor(out.code), out.code, out.message);
248
+ notify('unlink_approved', out.claimedEventId);
249
+ res.json({ ok: true, result: out });
250
+ }
251
+ catch (e) {
252
+ errorRes(res, 500, 'internal', e.message);
253
+ }
254
+ });
255
+ // ── ROOT: reject an unlink request → claim stays active. Same self-or-related marking discipline. ──
256
+ app.post('/api/admin/operator-claims/unlink/:requestEventId/reject', (req, res) => {
257
+ const root = requireRootAdmin(req, res);
258
+ if (!root)
259
+ return;
260
+ const id = String(req.params.requestEventId);
261
+ const approvalKind = req.body?.approval_kind ? String(req.body.approval_kind) : undefined;
262
+ const conflictDisclosure = req.body?.conflict_disclosure ? String(req.body.conflict_disclosure) : undefined;
263
+ try {
264
+ const out = db.transaction(() => {
265
+ const r = rejectUnlink(db, { requestEventId: id, approverId: root.id, approvalKind, conflictDisclosure });
266
+ if (!r.ok)
267
+ return r;
268
+ logAdminAction(db, { adminId: root.id, action: 'operator_claim.unlink_reject', targetType: 'operator_claim', targetId: id, detail: { result: r }, context: { actorType: 'admin_account', agentMode: 'human_direct', approvalKind: r.approvalKind, conflictDisclosure: r.conflictDisclosure } });
269
+ return r;
270
+ })();
271
+ if (!out.ok)
272
+ return errorRes(res, httpFor(out.code), out.code, out.message);
273
+ notify('unlink_rejected', out.claimedEventId);
274
+ res.json({ ok: true, result: out });
275
+ }
276
+ catch (e) {
277
+ errorRes(res, 500, 'internal', e.message);
278
+ }
279
+ });
280
+ }
@@ -61,9 +61,8 @@ export function registerAdminReportsRoutes(app, deps) {
61
61
  sql += ` ORDER BY vt.created_at DESC LIMIT 100`;
62
62
  res.json({ tasks: await dbAll(sql, params) });
63
63
  });
64
- // 双引擎收入治理视图三级奖励(推土机) vs PV 对碰(原子能) 协议级聚合
65
- // 治理依据:根据两引擎实际拨付量决定费率/阈值/拨出率等调参方案
66
- // 隐私第一:运营财务,仅 protocol admin 可见(详见 docs/REWARD-ENGINES-DECOUPLING.md)
64
+ // 收入治理视图联盟佣金(按真实成交)协议级聚合。匹配奖励引擎已切除(#401),不再聚合其指标。
65
+ // 隐私第一:运营财务,仅 protocol admin 可见。
67
66
  app.get('/api/admin/economic-summary', async (req, res) => {
68
67
  const admin = requireProtocolAdmin(req, res);
69
68
  if (!admin)
@@ -87,12 +86,7 @@ export function registerAdminReportsRoutes(app, deps) {
87
86
  // charity_fund 自此纯净(仅捐款/还愿/拨款),不再承接佣金兜底。
88
87
  const charity = await dbOne(`SELECT balance, total_donated, total_disbursed, total_redirected FROM charity_fund WHERE id='main'`);
89
88
  const cpool = await dbOne(`SELECT balance, total_chain_gap, total_orphan_sponsor, total_region_cap FROM commission_reserve WHERE id='main'`);
90
- // 引擎 B:PV 对碰(binary_score_records 现金拨付 + 待拨 Score
91
- const binarySettled = (await dbOne(`SELECT COUNT(*) AS cnt, COALESCE(SUM(waz_amount),0) AS waz FROM binary_score_records WHERE settled_at IS NOT NULL`));
92
- const binaryPending = (await dbOne(`SELECT COUNT(*) AS cnt, COALESCE(SUM(score),0) AS score FROM binary_score_records WHERE settled_at IS NULL`));
93
- const periods = (await dbOne(`SELECT COUNT(*) AS cnt, COALESCE(SUM(cash_distributed),0) AS dist, COALESCE(SUM(cash_retained),0) AS retained FROM settlement_periods WHERE status='completed'`));
94
- const gfund = await dbOne(`SELECT pool_balance, total_scores_pending, current_n, pv_escrow_reserve FROM global_fund WHERE id=1`);
95
- // 资金管道:global_fund 蓄水来源(fund_base 1% + commission region_cap redirect)
89
+ // 资金管道:fund_base 1% 累计 + commission region_cap redirect(历史,新订单恒 0
96
90
  const pipe = (await dbOne(`SELECT COALESCE(SUM(amount_base),0) AS base, COALESCE(SUM(amount_l3),0) AS redirect FROM fund_deposits`));
97
91
  res.json({
98
92
  engine_a_commission: {
@@ -105,22 +99,6 @@ export function registerAdminReportsRoutes(app, deps) {
105
99
  legacy_global_fund_redirect: r2(pipe.redirect), // 历史:解耦前曾入 global_fund(fund_deposits.amount_l3),新订单恒 0
106
100
  note: '即时分账;兜底全部入 commission_reserve(独立科目,只进不出,治理决定用途),不再污染 charity / global_fund。',
107
101
  },
108
- engine_b_pv_matching: {
109
- cash_distributed_total: r2(binarySettled.waz),
110
- settled_match_count: binarySettled.cnt,
111
- pending_score: r2(binaryPending.score),
112
- pending_match_count: binaryPending.cnt,
113
- settlement_periods_completed: periods.cnt,
114
- periods_cash_distributed: r2(periods.dist),
115
- periods_cash_retained: r2(periods.retained),
116
- note: 'Score → 现金经安全阀拨付,源自 global_fund,永不透支。',
117
- },
118
- global_fund: {
119
- pool_balance: r2(Number(gfund?.pool_balance || 0)),
120
- pv_escrow_reserve: r2(Number(gfund?.pv_escrow_reserve || 0)), // #1106 已承诺未兑付的 PV 负债(隔离于可分配池)
121
- total_scores_pending: r2(Number(gfund?.total_scores_pending || 0)),
122
- current_n: r2(Number(gfund?.current_n || 0)),
123
- },
124
102
  funding_pipe: {
125
103
  fund_base_1pct_accumulated: r2(pipe.base),
126
104
  commission_redirect_accumulated: r2(pipe.redirect),
@@ -131,7 +109,7 @@ export function registerAdminReportsRoutes(app, deps) {
131
109
  total_donated: r2(Number(charity?.total_donated || 0)),
132
110
  total_disbursed: r2(Number(charity?.total_disbursed || 0)),
133
111
  },
134
- governance_hint: '引擎A=消费即时奖励(分账→钱包,兜底→commission_reserve);引擎B=团队对碰(global_fund 安全阀)。两者解耦,调参互不影响。详见 docs/REWARD-ENGINES-DECOUPLING.md',
112
+ governance_hint: '联盟佣金=消费即时分账→钱包,兜底→commission_reserve(独立科目,只进不出)。匹配奖励引擎已切除(#401)',
135
113
  generated_at: new Date().toISOString(),
136
114
  });
137
115
  });
@@ -7,23 +7,14 @@ export function registerAdminTokenomicsRoutes(app, deps) {
7
7
  const admin = requireProtocolAdmin(req, res);
8
8
  if (!admin)
9
9
  return;
10
- const tiers = await dbAll("SELECT * FROM binary_tier_config ORDER BY tier ASC");
10
+ // matching-rewards admin views (tier config / binary leaderboard / mgmt-bonus / pool injection) removed —
11
+ // engine excised (#401). Only neutral participation-record + affiliate-commission stats remain.
11
12
  const topComm = await dbAll(`
12
13
  SELECT cr.beneficiary_id, u.name, COUNT(*) as records, COALESCE(SUM(cr.amount),0) as earned
13
14
  FROM commission_records cr LEFT JOIN users u ON u.id = cr.beneficiary_id
14
15
  WHERE cr.beneficiary_id != 'sys_protocol'
15
16
  GROUP BY cr.beneficiary_id ORDER BY earned DESC LIMIT 10
16
17
  `);
17
- const topBinary = await dbAll(`
18
- SELECT bsr.user_id, u.name,
19
- COUNT(*) as hits,
20
- COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as waz_total,
21
- COALESCE(SUM(score),0) as score_total
22
- FROM binary_score_records bsr LEFT JOIN users u ON u.id = bsr.user_id
23
- GROUP BY bsr.user_id ORDER BY waz_total DESC LIMIT 10
24
- `);
25
- const gf = await dbOne("SELECT * FROM global_fund WHERE id=1");
26
- const mb = await dbOne("SELECT balance FROM management_bonus_pool WHERE id=1");
27
18
  const pvLedger = await dbOne(`
28
19
  SELECT COUNT(*) as total,
29
20
  SUM(CASE WHEN processed=0 THEN 1 ELSE 0 END) as pending,
@@ -31,43 +22,10 @@ export function registerAdminTokenomicsRoutes(app, deps) {
31
22
  FROM pv_ledger
32
23
  `);
33
24
  res.json({
34
- global_fund: gf,
35
- management_bonus_pool: mb,
36
- tier_config: tiers,
37
25
  pv_ledger: pvLedger,
38
26
  top_commission: topComm,
39
- top_binary: topBinary,
40
27
  });
41
28
  });
42
- // 调整 Tier 配置
43
- app.post('/api/admin/tokenomics/tier', async (req, res) => {
44
- const admin = requireProtocolAdmin(req, res);
45
- if (!admin)
46
- return;
47
- const { tier, pv_threshold, score_per_hit, active } = req.body;
48
- if (!Number.isInteger(tier) || tier < 1 || tier > 10)
49
- return void res.json({ error: 'tier 无效' });
50
- if (Number(pv_threshold) <= 0 || Number(score_per_hit) <= 0)
51
- return void res.json({ error: '阈值/分数必须 > 0' });
52
- await dbRun(`INSERT OR REPLACE INTO binary_tier_config (tier, pv_threshold, score_per_hit, active) VALUES (?,?,?,?)`, [tier, Number(pv_threshold), Number(score_per_hit), active ? 1 : 0]);
53
- logAdminAction(admin.id, 'tokenomics_tier_update', 'tier', String(tier), { pv_threshold, score_per_hit, active });
54
- res.json({ success: true });
55
- });
56
- // 管理津贴资格列表 + 开关
57
- app.get('/api/admin/tokenomics/mgmt-bonus', async (req, res) => {
58
- const admin = requireProtocolAdmin(req, res);
59
- if (!admin)
60
- return;
61
- const enabled = (await dbOne("SELECT value FROM system_state WHERE key='mgmt_bonus_enabled'"))?.value === '1';
62
- const eligible = await dbAll(`
63
- SELECT u.id, u.name, u.created_at,
64
- (SELECT COUNT(*) FROM users WHERE sponsor_id = u.id) as l1_count,
65
- COALESCE((SELECT SUM(amount) FROM commission_records WHERE beneficiary_id = u.id),0) as total_commission
66
- FROM users u WHERE u.mgmt_bonus_eligible = 1
67
- ORDER BY u.created_at DESC LIMIT 100
68
- `);
69
- res.json({ enabled, eligible_users: eligible, eligible_count: eligible.length });
70
- });
71
29
  // 注册必须 ref 开关
72
30
  app.post('/api/admin/tokenomics/require-ref/toggle', async (req, res) => {
73
31
  const admin = requireProtocolAdmin(req, res);
@@ -79,36 +37,4 @@ export function registerAdminTokenomicsRoutes(app, deps) {
79
37
  logAdminAction(admin.id, 'require_ref_toggle', 'system', 'require_ref_to_register', { value: v });
80
38
  res.json({ success: true, enabled: !!enabled });
81
39
  });
82
- // 管理津贴池开关
83
- app.post('/api/admin/tokenomics/mgmt-bonus/toggle', async (req, res) => {
84
- const admin = requireProtocolAdmin(req, res);
85
- if (!admin)
86
- return;
87
- const { enabled } = req.body;
88
- const v = enabled ? '1' : '0';
89
- await dbRun("INSERT OR REPLACE INTO system_state (key, value) VALUES ('mgmt_bonus_enabled', ?)", [v]);
90
- logAdminAction(admin.id, 'mgmt_bonus_toggle', 'system', 'mgmt_bonus_enabled', { value: v });
91
- res.json({ success: true, enabled: !!enabled });
92
- });
93
- // 池注资
94
- app.post('/api/admin/tokenomics/inject', async (req, res) => {
95
- const admin = requireProtocolAdmin(req, res);
96
- if (!admin)
97
- return;
98
- const { pool, amount } = req.body;
99
- const n = Number(amount);
100
- if (!(n > 0))
101
- return void res.json({ error: '金额必须 > 0' });
102
- if (pool === 'global_fund') {
103
- await dbRun("UPDATE global_fund SET pool_balance = pool_balance + ? WHERE id=1", [n]);
104
- }
105
- else if (pool === 'management_bonus') {
106
- await dbRun("UPDATE management_bonus_pool SET balance = balance + ? WHERE id=1", [n]);
107
- }
108
- else {
109
- return void res.json({ error: 'pool 名称无效(global_fund / management_bonus)' });
110
- }
111
- logAdminAction(admin.id, 'tokenomics_pool_inject', 'pool', pool, { amount: n });
112
- res.json({ success: true });
113
- });
114
40
  }
@@ -20,20 +20,7 @@ export function registerAdminUsersLifecycleRoutes(app, deps) {
20
20
  logAdminAction(admin.id, 'l1_share_override', 'user', req.params.id, { value: v, note: note || null });
21
21
  res.json({ success: true, value: v });
22
22
  });
23
- app.post('/api/admin/users/:id/mgmt-bonus-eligible', async (req, res) => {
24
- const admin = requireProtocolAdmin(req, res);
25
- if (!admin)
26
- return;
27
- if (!adminCanOperateOn(admin, req.params.id, res))
28
- return;
29
- const { eligible, note } = req.body;
30
- const target = await dbOne("SELECT id, name FROM users WHERE id = ?", [req.params.id]);
31
- if (!target)
32
- return void res.json({ error: '用户不存在' });
33
- await dbRun("UPDATE users SET mgmt_bonus_eligible = ?, updated_at = datetime('now') WHERE id = ?", [eligible ? 1 : 0, req.params.id]);
34
- logAdminAction(admin.id, eligible ? 'mgmt_bonus_grant' : 'mgmt_bonus_revoke', 'user', req.params.id, { note: note || null });
35
- res.json({ success: true });
36
- });
23
+ // 解除账号登录锁定:清零失败次数 + 解锁
37
24
  app.post('/api/admin/users/:id/reset-failed-attempts', async (req, res) => {
38
25
  const admin = requireUsersAdmin(req, res);
39
26
  if (!admin)