@seasonkoh/webaz 0.1.26 → 0.1.27
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.
- package/LICENSE +2 -2
- package/NOTICE +24 -3
- package/README.md +74 -330
- package/README.zh-CN.md +419 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +36 -28
- package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
- package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
- package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
- package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
- package/dist/ledger.js +1 -1
- package/dist/pwa/admin-audit.js +38 -0
- package/dist/pwa/anti-abuse-thresholds.js +135 -0
- package/dist/pwa/cf-origin-guard.js +33 -0
- package/dist/pwa/contract-fingerprint.js +1 -0
- package/dist/pwa/data/onboarding-cases.js +2 -2
- package/dist/pwa/data/onboarding-quiz.js +1 -1
- package/dist/pwa/economic-participation.js +2 -2
- package/dist/pwa/integration-contract.js +46 -4
- package/dist/pwa/internal/pv-settlement.js +12 -0
- package/dist/pwa/internal/wallet-signer.js +26 -0
- package/dist/pwa/public/app.js +679 -679
- package/dist/pwa/public/i18n.js +15 -28
- package/dist/pwa/public/index.html +1 -1
- package/dist/pwa/public/openapi.json +4760 -2769
- package/dist/pwa/pv-kill-switch.js +31 -0
- package/dist/pwa/routes/admin-admins.js +48 -1
- package/dist/pwa/routes/admin-analytics.js +1 -10
- package/dist/pwa/routes/admin-atomic.js +4 -17
- package/dist/pwa/routes/admin-operator-claims.js +280 -0
- package/dist/pwa/routes/admin-reports.js +4 -26
- package/dist/pwa/routes/admin-tokenomics.js +2 -76
- package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
- package/dist/pwa/routes/admin-users-query.js +23 -1
- package/dist/pwa/routes/admin-wallet-ops.js +1 -1
- package/dist/pwa/routes/auth-read.js +1 -5
- package/dist/pwa/routes/auth-register.js +3 -13
- package/dist/pwa/routes/build-task-quota.js +113 -0
- package/dist/pwa/routes/claim-verify.js +15 -11
- package/dist/pwa/routes/contribution-facts.js +18 -0
- package/dist/pwa/routes/dispute-cases.js +5 -4
- package/dist/pwa/routes/growth.js +3 -3
- package/dist/pwa/routes/orders-action.js +27 -10
- package/dist/pwa/routes/orders-create.js +1 -1
- package/dist/pwa/routes/products-meta.js +19 -6
- package/dist/pwa/routes/profile-placement.js +1 -1
- package/dist/pwa/routes/promoter.js +10 -29
- package/dist/pwa/routes/public-build-tasks.js +5 -1
- package/dist/pwa/routes/public-utils.js +9 -12
- package/dist/pwa/routes/referral.js +5 -26
- package/dist/pwa/routes/rewards-apply.js +3 -2
- package/dist/pwa/routes/share-redirects.js +1 -1
- package/dist/pwa/routes/shareables-interactions.js +2 -1
- package/dist/pwa/routes/task-proposals.js +85 -9
- package/dist/pwa/routes/users-public.js +1 -4
- package/dist/pwa/routes/wallet-read.js +2 -14
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +156 -469
- package/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +33 -7
- package/dist/index.js +0 -182
- package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
- package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
- package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
- package/dist/test-dispute.js +0 -153
- package/dist/test-manifest.js +0 -61
- package/dist/test-mcp-tools.js +0 -135
- package/dist/test-reputation.js +0 -116
- package/dist/test-skill-market.js +0 -101
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
|
|
2
|
+
const err = (code, message) => ({ ok: false, code, message });
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
+
const isAdminUser = (db, id) => {
|
|
5
|
+
const u = db.prepare('SELECT role, roles FROM users WHERE id = ?').get(id);
|
|
6
|
+
if (!u)
|
|
7
|
+
return false;
|
|
8
|
+
if (u.role === 'admin')
|
|
9
|
+
return true;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(u.roles || '[]').includes('admin');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const userExists = (db, id) => !!db.prepare('SELECT 1 FROM users WHERE id = ?').get(id);
|
|
18
|
+
const isRoot = (db, id) => {
|
|
19
|
+
const u = db.prepare('SELECT role, admin_type FROM users WHERE id = ?').get(id);
|
|
20
|
+
return !!u && u.role === 'admin' && u.admin_type === 'root';
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Latest VALID contributor confirmation for a claim (or null). Defense-in-depth read-side validation:
|
|
24
|
+
* a confirmation only counts if its admin/contributor match the claimed event AND decided_by is the
|
|
25
|
+
* contributor — so a mismatched/forged row (however it got into the table) is IGNORED, never read as
|
|
26
|
+
* 'confirmed'. (The DB also CHECK/UNIQUE/trigger-enforces this on write.)
|
|
27
|
+
*/
|
|
28
|
+
function latestConfirmation(db, claimed) {
|
|
29
|
+
return db.prepare(`SELECT decision, decided_by, created_at FROM admin_operator_claim_confirmations
|
|
30
|
+
WHERE claimed_event_id = ? AND admin_account_id = ? AND contributor_account_id = ? AND decided_by = contributor_account_id
|
|
31
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`).get(claimed.event_id, claimed.admin_account_id, claimed.contributor_account_id);
|
|
32
|
+
}
|
|
33
|
+
/** Any event that supersedes the given event id (approved/revoked/superseded chained onto it). */
|
|
34
|
+
function supersederOf(db, eventId) {
|
|
35
|
+
return db.prepare("SELECT event_id, event_type, created_at FROM admin_operator_claim_events WHERE supersedes_event_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1").get(eventId);
|
|
36
|
+
}
|
|
37
|
+
/** Derive the workflow status of a claim from its append-only events + confirmations. */
|
|
38
|
+
export function deriveClaimState(db, claimedEventId) {
|
|
39
|
+
const claimed = db.prepare("SELECT * FROM admin_operator_claim_events WHERE event_id = ? AND event_type = 'claimed'").get(claimedEventId);
|
|
40
|
+
if (!claimed)
|
|
41
|
+
return null;
|
|
42
|
+
const confirmation = latestConfirmation(db, claimed);
|
|
43
|
+
const onClaim = supersederOf(db, claimedEventId); // approved | revoked directly on the proposal
|
|
44
|
+
if (onClaim && onClaim.event_type === 'revoked')
|
|
45
|
+
return { status: 'rejected_by_root', claimed, confirmation, approved: null, terminal: onClaim };
|
|
46
|
+
if (onClaim && onClaim.event_type === 'approved') {
|
|
47
|
+
const onApproved = supersederOf(db, onClaim.event_id); // revoked | superseded on the approval
|
|
48
|
+
if (onApproved && onApproved.event_type === 'revoked')
|
|
49
|
+
return { status: 'revoked', claimed, confirmation, approved: onClaim, terminal: onApproved };
|
|
50
|
+
if (onApproved && onApproved.event_type === 'superseded')
|
|
51
|
+
return { status: 'superseded', claimed, confirmation, approved: onClaim, terminal: onApproved };
|
|
52
|
+
return { status: 'approved', claimed, confirmation, approved: onClaim, terminal: null };
|
|
53
|
+
}
|
|
54
|
+
if (confirmation?.decision === 'rejected')
|
|
55
|
+
return { status: 'rejected_by_contributor', claimed, confirmation, approved: null, terminal: null };
|
|
56
|
+
if (confirmation?.decision === 'accepted')
|
|
57
|
+
return { status: 'confirmed', claimed, confirmation, approved: null, terminal: null };
|
|
58
|
+
return { status: 'proposed', claimed, confirmation: null, approved: null, terminal: null };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Step 1 — an admin proposes linking THEIR OWN seat to a contributor account (append-only 'claimed').
|
|
62
|
+
* The seat is ALWAYS the actor (admin_account_id = actorAdminId) — a caller can NOT propose for someone
|
|
63
|
+
* else's seat. (Root-delegated proposals are out of scope for Phase 2; they would need an explicit
|
|
64
|
+
* targetAdminAccountId + its own approval/audit path.)
|
|
65
|
+
*/
|
|
66
|
+
export function proposeClaim(db, input) {
|
|
67
|
+
const { actorAdminId, contributorAccountId, rationale } = input;
|
|
68
|
+
if (!actorAdminId || !contributorAccountId)
|
|
69
|
+
return err('invalid_input', 'actorAdminId + contributorAccountId required');
|
|
70
|
+
if (!isAdminUser(db, actorAdminId))
|
|
71
|
+
return err('not_admin', 'only an admin may propose a claim for their own seat');
|
|
72
|
+
if (!userExists(db, contributorAccountId))
|
|
73
|
+
return err('contributor_not_found', 'contributor account does not exist');
|
|
74
|
+
const eventId = generateId('aoce');
|
|
75
|
+
db.prepare(`INSERT INTO admin_operator_claim_events (event_id, event_type, admin_account_id, contributor_account_id, conflict_disclosure, rationale, immutable)
|
|
76
|
+
VALUES (?, 'claimed', ?, ?, ?, ?, 1)`).run(eventId, actorAdminId, contributorAccountId, actorAdminId === contributorAccountId ? 'self_or_related' : 'unknown', rationale ?? null);
|
|
77
|
+
return { ok: true, claimedEventId: eventId };
|
|
78
|
+
}
|
|
79
|
+
/** Step 2 — the contributor (and only the contributor) accepts/rejects a claim pointing at them. */
|
|
80
|
+
export function confirmClaim(db, input) {
|
|
81
|
+
const { claimedEventId, deciderId, decision, rationale } = input;
|
|
82
|
+
if (decision !== 'accepted' && decision !== 'rejected')
|
|
83
|
+
return err('invalid_input', "decision must be 'accepted' or 'rejected'");
|
|
84
|
+
const view = deriveClaimState(db, claimedEventId);
|
|
85
|
+
if (!view)
|
|
86
|
+
return err('claim_not_found', 'no such proposed claim');
|
|
87
|
+
if (view.claimed.contributor_account_id !== deciderId)
|
|
88
|
+
return err('not_contributor', 'only the named contributor may confirm this claim');
|
|
89
|
+
if (view.status !== 'proposed')
|
|
90
|
+
return err('bad_state', `claim is '${view.status}', not awaiting contributor confirmation`);
|
|
91
|
+
const confirmationId = generateId('aocc');
|
|
92
|
+
db.prepare(`INSERT INTO admin_operator_claim_confirmations (confirmation_id, claimed_event_id, admin_account_id, contributor_account_id, decision, decided_by, rationale, immutable)
|
|
93
|
+
VALUES (?,?,?,?,?,?,?,1)`).run(confirmationId, claimedEventId, view.claimed.admin_account_id, view.claimed.contributor_account_id, decision, deciderId, rationale ?? null);
|
|
94
|
+
return { ok: true, confirmationId };
|
|
95
|
+
}
|
|
96
|
+
/** Step 3 — root approves a claim (append-only 'approved' superseding the proposal). Auto-supersedes any
|
|
97
|
+
* currently-approved claim on the same seat so a seat has at most one active contributor. */
|
|
98
|
+
export function approveClaim(db, input) {
|
|
99
|
+
const { claimedEventId, approverId, approvalKind, conflictDisclosure, rationale } = input;
|
|
100
|
+
if (!isRoot(db, approverId))
|
|
101
|
+
return err('not_root', 'only a root admin may approve operator claims');
|
|
102
|
+
const view = deriveClaimState(db, claimedEventId);
|
|
103
|
+
if (!view)
|
|
104
|
+
return err('claim_not_found', 'no such proposed claim');
|
|
105
|
+
if (view.status !== 'proposed' && view.status !== 'confirmed')
|
|
106
|
+
return err('bad_state', `claim is '${view.status}', not approvable`);
|
|
107
|
+
if (view.confirmation?.decision === 'rejected')
|
|
108
|
+
return err('contributor_rejected', 'contributor rejected this claim');
|
|
109
|
+
const selfLink = view.claimed.admin_account_id === view.claimed.contributor_account_id;
|
|
110
|
+
if (selfLink) {
|
|
111
|
+
if (approvalKind !== 'founder_bootstrap_override' && approvalKind !== 'root_approval')
|
|
112
|
+
return err('self_link_requires_marking', 'a self-link must be founder_bootstrap_override or root_approval');
|
|
113
|
+
if (conflictDisclosure !== 'self_or_related')
|
|
114
|
+
return err('self_link_requires_disclosure', 'a self-link must disclose conflict_disclosure=self_or_related');
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// cross-account claim: contributor MUST have accepted before root can approve
|
|
118
|
+
if (view.status !== 'confirmed')
|
|
119
|
+
return err('not_confirmed', 'contributor has not accepted this claim yet');
|
|
120
|
+
}
|
|
121
|
+
if (approvalKind === 'independent_governance' && conflictDisclosure === 'self_or_related') {
|
|
122
|
+
return err('dishonest_marking', 'self_or_related conflict cannot be labelled independent_governance');
|
|
123
|
+
}
|
|
124
|
+
const tx = db.transaction(() => {
|
|
125
|
+
// supersede any active approved claim on this seat (append-only 'superseded' marker)
|
|
126
|
+
const activeApproved = db.prepare(`SELECT e.event_id, e.contributor_account_id FROM admin_operator_claim_events e
|
|
127
|
+
WHERE e.admin_account_id = ? AND e.event_type = 'approved'
|
|
128
|
+
AND NOT EXISTS (SELECT 1 FROM admin_operator_claim_events s WHERE s.supersedes_event_id = e.event_id)`).all(view.claimed.admin_account_id);
|
|
129
|
+
for (const a of activeApproved) {
|
|
130
|
+
// the 'superseded' marker records the OLD approval's contributor (history must not be rewritten)
|
|
131
|
+
db.prepare(`INSERT INTO admin_operator_claim_events (event_id, event_type, admin_account_id, contributor_account_id, conflict_disclosure, effective_from, supersedes_event_id, rationale, immutable)
|
|
132
|
+
VALUES (?, 'superseded', ?, ?, 'unknown', datetime('now'), ?, 'superseded by a newer approved claim', 1)`).run(generateId('aoce'), view.claimed.admin_account_id, a.contributor_account_id, a.event_id);
|
|
133
|
+
}
|
|
134
|
+
const approvedEventId = generateId('aoce');
|
|
135
|
+
db.prepare(`INSERT INTO admin_operator_claim_events (event_id, event_type, admin_account_id, contributor_account_id, approval_kind, approved_by, conflict_disclosure, effective_from, supersedes_event_id, rationale, immutable)
|
|
136
|
+
VALUES (?, 'approved', ?, ?, ?, ?, ?, datetime('now'), ?, ?, 1)`).run(approvedEventId, view.claimed.admin_account_id, view.claimed.contributor_account_id, approvalKind, approverId, conflictDisclosure, claimedEventId, rationale ?? null);
|
|
137
|
+
return approvedEventId;
|
|
138
|
+
});
|
|
139
|
+
return { ok: true, approvedEventId: tx() };
|
|
140
|
+
}
|
|
141
|
+
/** Root rejects a still-proposed/confirmed claim (terminal 'revoked' on the proposal). */
|
|
142
|
+
export function rejectClaim(db, input) {
|
|
143
|
+
const { claimedEventId, approverId, rationale } = input;
|
|
144
|
+
if (!isRoot(db, approverId))
|
|
145
|
+
return err('not_root', 'only a root admin may reject operator claims');
|
|
146
|
+
const view = deriveClaimState(db, claimedEventId);
|
|
147
|
+
if (!view)
|
|
148
|
+
return err('claim_not_found', 'no such proposed claim');
|
|
149
|
+
if (view.status !== 'proposed' && view.status !== 'confirmed' && view.status !== 'rejected_by_contributor')
|
|
150
|
+
return err('bad_state', `claim is '${view.status}', not rejectable`);
|
|
151
|
+
const revokedEventId = generateId('aoce');
|
|
152
|
+
db.prepare(`INSERT INTO admin_operator_claim_events (event_id, event_type, admin_account_id, contributor_account_id, approved_by, conflict_disclosure, effective_from, supersedes_event_id, rationale, immutable)
|
|
153
|
+
VALUES (?, 'revoked', ?, ?, ?, 'unknown', datetime('now'), ?, ?, 1)`).run(revokedEventId, view.claimed.admin_account_id, view.claimed.contributor_account_id, approverId, claimedEventId, rationale ?? 'rejected by root');
|
|
154
|
+
return { ok: true, revokedEventId };
|
|
155
|
+
}
|
|
156
|
+
/** Root revokes an APPROVED (active) claim (append-only 'revoked' on the approved event). */
|
|
157
|
+
export function revokeApprovedClaim(db, input) {
|
|
158
|
+
const { approvedEventId, revokerId, rationale } = input;
|
|
159
|
+
if (!isRoot(db, revokerId))
|
|
160
|
+
return err('not_root', 'only a root admin may revoke operator claims');
|
|
161
|
+
const approved = db.prepare("SELECT * FROM admin_operator_claim_events WHERE event_id = ? AND event_type = 'approved'").get(approvedEventId);
|
|
162
|
+
if (!approved)
|
|
163
|
+
return err('approved_not_found', 'no such approved claim');
|
|
164
|
+
if (supersederOf(db, approvedEventId))
|
|
165
|
+
return err('bad_state', 'this approved claim is already revoked/superseded');
|
|
166
|
+
const revokedEventId = generateId('aoce');
|
|
167
|
+
db.prepare(`INSERT INTO admin_operator_claim_events (event_id, event_type, admin_account_id, contributor_account_id, approved_by, conflict_disclosure, effective_from, supersedes_event_id, rationale, immutable)
|
|
168
|
+
VALUES (?, 'revoked', ?, ?, ?, 'unknown', datetime('now'), ?, ?, 1)`).run(revokedEventId, approved.admin_account_id, approved.contributor_account_id, revokerId, approvedEventId, rationale ?? 'revoked by root');
|
|
169
|
+
return { ok: true, revokedEventId };
|
|
170
|
+
}
|
|
171
|
+
/** An approved claim is active iff nothing supersedes it (no revoke/superseded chained on it). */
|
|
172
|
+
function approvedIsActive(db, approvedEventId) {
|
|
173
|
+
const a = db.prepare("SELECT 1 FROM admin_operator_claim_events WHERE event_id = ? AND event_type = 'approved'").get(approvedEventId);
|
|
174
|
+
return !!a && !supersederOf(db, approvedEventId);
|
|
175
|
+
}
|
|
176
|
+
/** The still-pending unlink 'requested' event for an approved claim (no decision yet), or null. */
|
|
177
|
+
export function pendingUnlinkForApproved(db, approvedEventId) {
|
|
178
|
+
const reqs = db.prepare("SELECT * FROM admin_operator_unlink_requests WHERE approved_event_id = ? AND event_type = 'requested' ORDER BY created_at DESC, rowid DESC").all(approvedEventId);
|
|
179
|
+
for (const r of reqs) {
|
|
180
|
+
const decided = db.prepare('SELECT 1 FROM admin_operator_unlink_requests WHERE supersedes_request_id = ?').get(r.request_event_id);
|
|
181
|
+
if (!decided)
|
|
182
|
+
return r;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
/** Either party (admin-seat owner OR contributor) requests unlink of an ACTIVE approved claim. The
|
|
187
|
+
* caller MUST have already passed the passkey gate; humanAuthRef records which token was consumed. */
|
|
188
|
+
export function requestUnlink(db, input) {
|
|
189
|
+
const { approvedEventId, requesterId, reason, humanAuthRef } = input;
|
|
190
|
+
const ap = db.prepare("SELECT event_id, admin_account_id, contributor_account_id, supersedes_event_id FROM admin_operator_claim_events WHERE event_id = ? AND event_type = 'approved'").get(approvedEventId);
|
|
191
|
+
if (!ap)
|
|
192
|
+
return err('approved_not_found', 'no such approved claim');
|
|
193
|
+
if (!approvedIsActive(db, approvedEventId))
|
|
194
|
+
return err('bad_state', 'claim is not active (already revoked/superseded)');
|
|
195
|
+
let role;
|
|
196
|
+
if (requesterId === ap.admin_account_id)
|
|
197
|
+
role = 'admin_seat';
|
|
198
|
+
else if (requesterId === ap.contributor_account_id)
|
|
199
|
+
role = 'contributor';
|
|
200
|
+
else
|
|
201
|
+
return err('not_party', 'only the admin-seat owner or the contributor may request unlink');
|
|
202
|
+
if (pendingUnlinkForApproved(db, approvedEventId))
|
|
203
|
+
return err('already_pending', 'an unlink request is already pending for this claim');
|
|
204
|
+
const id = generateId('aour');
|
|
205
|
+
db.prepare(`INSERT INTO admin_operator_unlink_requests (request_event_id, event_type, approved_event_id, claimed_event_id, admin_account_id, contributor_account_id, requested_by, requester_role, reason, human_auth_ref, immutable)
|
|
206
|
+
VALUES (?, 'requested', ?, ?, ?, ?, ?, ?, ?, ?, 1)`).run(id, approvedEventId, ap.supersedes_event_id, ap.admin_account_id, ap.contributor_account_id, requesterId, role, reason ?? null, humanAuthRef ?? null);
|
|
207
|
+
return { ok: true, requestEventId: id, requesterRole: role, adminAccountId: ap.admin_account_id, contributorAccountId: ap.contributor_account_id };
|
|
208
|
+
}
|
|
209
|
+
/** A root deciding an unlink is "self-or-related" when they are themselves the admin-seat owner, the
|
|
210
|
+
* contributor, or the party who filed the request. The same posture as approveClaim's self-link: a
|
|
211
|
+
* root MAY decide it, but MUST mark the conflict honestly (never label a related decision as
|
|
212
|
+
* independent_governance). Returns the marking to persist, or a WorkflowError on dishonest/missing marking. */
|
|
213
|
+
function resolveUnlinkMarking(req, approverId, approvalKind, conflictDisclosure) {
|
|
214
|
+
const selfOrRelated = approverId === req.admin_account_id || approverId === req.contributor_account_id || approverId === req.requested_by;
|
|
215
|
+
let kind = approvalKind;
|
|
216
|
+
let disc = conflictDisclosure;
|
|
217
|
+
if (selfOrRelated) {
|
|
218
|
+
if (kind !== 'founder_bootstrap_override' && kind !== 'root_approval') {
|
|
219
|
+
return err('self_related_requires_marking', 'a self-or-related unlink decision must be marked founder_bootstrap_override or root_approval');
|
|
220
|
+
}
|
|
221
|
+
if (disc !== 'self_or_related') {
|
|
222
|
+
return err('self_related_requires_disclosure', 'a self-or-related unlink decision must disclose conflict_disclosure=self_or_related');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// independent decision: default to the honest baseline when caller omits the marking
|
|
227
|
+
kind = kind ?? 'root_approval';
|
|
228
|
+
disc = disc ?? 'none';
|
|
229
|
+
}
|
|
230
|
+
if (kind === 'independent_governance' && disc === 'self_or_related') {
|
|
231
|
+
return err('dishonest_marking', 'self_or_related conflict cannot be labelled independent_governance');
|
|
232
|
+
}
|
|
233
|
+
return { approvalKind: kind, conflictDisclosure: disc };
|
|
234
|
+
}
|
|
235
|
+
/** ROOT approves an unlink request → atomically records the decision AND revokes the claim.
|
|
236
|
+
* When root is self-or-related to the relationship/request, approvalKind + conflictDisclosure are
|
|
237
|
+
* required and recorded on the decision event (governance honesty, mirrors approveClaim). */
|
|
238
|
+
export function approveUnlink(db, input) {
|
|
239
|
+
const { requestEventId, approverId } = input;
|
|
240
|
+
if (!isRoot(db, approverId))
|
|
241
|
+
return err('not_root', 'only a root admin may approve an unlink request');
|
|
242
|
+
const req = db.prepare("SELECT * FROM admin_operator_unlink_requests WHERE request_event_id = ? AND event_type = 'requested'").get(requestEventId);
|
|
243
|
+
if (!req)
|
|
244
|
+
return err('request_not_found', 'no such unlink request');
|
|
245
|
+
if (db.prepare('SELECT 1 FROM admin_operator_unlink_requests WHERE supersedes_request_id = ?').get(requestEventId))
|
|
246
|
+
return err('bad_state', 'this unlink request is already decided');
|
|
247
|
+
if (!approvedIsActive(db, req.approved_event_id))
|
|
248
|
+
return err('bad_state', 'the claim is no longer active');
|
|
249
|
+
const marking = resolveUnlinkMarking(req, approverId, input.approvalKind, input.conflictDisclosure);
|
|
250
|
+
if (!marking.approvalKind)
|
|
251
|
+
return marking;
|
|
252
|
+
const { approvalKind, conflictDisclosure } = marking;
|
|
253
|
+
const run = db.transaction(() => {
|
|
254
|
+
const decId = generateId('aour');
|
|
255
|
+
db.prepare(`INSERT INTO admin_operator_unlink_requests (request_event_id, event_type, approved_event_id, claimed_event_id, admin_account_id, contributor_account_id, decided_by, approval_kind, conflict_disclosure, supersedes_request_id, immutable)
|
|
256
|
+
VALUES (?, 'approved', ?, ?, ?, ?, ?, ?, ?, ?, 1)`).run(decId, req.approved_event_id, req.claimed_event_id, req.admin_account_id, req.contributor_account_id, approverId, approvalKind, conflictDisclosure, requestEventId);
|
|
257
|
+
const rev = revokeApprovedClaim(db, { approvedEventId: req.approved_event_id, revokerId: approverId, rationale: 'unlink request approved by root' });
|
|
258
|
+
if (!rev.ok)
|
|
259
|
+
throw new Error('revoke failed: ' + rev.code);
|
|
260
|
+
return decId;
|
|
261
|
+
});
|
|
262
|
+
return { ok: true, decisionEventId: run(), approvedEventId: req.approved_event_id, claimedEventId: req.claimed_event_id, adminAccountId: req.admin_account_id, contributorAccountId: req.contributor_account_id, approvalKind, conflictDisclosure };
|
|
263
|
+
}
|
|
264
|
+
/** ROOT rejects an unlink request → records the decision; the claim stays active. Same self-or-related
|
|
265
|
+
* marking discipline as approveUnlink. */
|
|
266
|
+
export function rejectUnlink(db, input) {
|
|
267
|
+
const { requestEventId, approverId } = input;
|
|
268
|
+
if (!isRoot(db, approverId))
|
|
269
|
+
return err('not_root', 'only a root admin may reject an unlink request');
|
|
270
|
+
const req = db.prepare("SELECT * FROM admin_operator_unlink_requests WHERE request_event_id = ? AND event_type = 'requested'").get(requestEventId);
|
|
271
|
+
if (!req)
|
|
272
|
+
return err('request_not_found', 'no such unlink request');
|
|
273
|
+
if (db.prepare('SELECT 1 FROM admin_operator_unlink_requests WHERE supersedes_request_id = ?').get(requestEventId))
|
|
274
|
+
return err('bad_state', 'this unlink request is already decided');
|
|
275
|
+
const marking = resolveUnlinkMarking(req, approverId, input.approvalKind, input.conflictDisclosure);
|
|
276
|
+
if (!marking.approvalKind)
|
|
277
|
+
return marking;
|
|
278
|
+
const { approvalKind, conflictDisclosure } = marking;
|
|
279
|
+
const decId = generateId('aour');
|
|
280
|
+
db.prepare(`INSERT INTO admin_operator_unlink_requests (request_event_id, event_type, approved_event_id, claimed_event_id, admin_account_id, contributor_account_id, decided_by, approval_kind, conflict_disclosure, supersedes_request_id, immutable)
|
|
281
|
+
VALUES (?, 'rejected', ?, ?, ?, ?, ?, ?, ?, ?, 1)`).run(decId, req.approved_event_id, req.claimed_event_id, req.admin_account_id, req.contributor_account_id, approverId, approvalKind, conflictDisclosure, requestEventId);
|
|
282
|
+
return { ok: true, decisionEventId: decId, claimedEventId: req.claimed_event_id, adminAccountId: req.admin_account_id, contributorAccountId: req.contributor_account_id, approvalKind, conflictDisclosure };
|
|
283
|
+
}
|
|
284
|
+
// ── GOVERNANCE-MARKING CORRECTION (append-only): fix a mis-marked self/related approval's disclosure
|
|
285
|
+
// WITHOUT touching the original approved event (no UPDATE, no backdate, no change to effective interval).
|
|
286
|
+
// A root appends a correction referencing the approved event; the resolver overlays it at read time. ──
|
|
287
|
+
export function correctClaimMarking(db, input) {
|
|
288
|
+
const { approvedEventId, correctorId, approvalKind, conflictDisclosure, correctionReason } = input;
|
|
289
|
+
if (!isRoot(db, correctorId))
|
|
290
|
+
return err('not_root', 'only a root admin may correct a claim marking');
|
|
291
|
+
const ap = db.prepare("SELECT event_id, admin_account_id, contributor_account_id, approved_by, approval_kind, conflict_disclosure FROM admin_operator_claim_events WHERE event_id = ? AND event_type = 'approved'").get(approvedEventId);
|
|
292
|
+
if (!ap)
|
|
293
|
+
return err('approved_not_found', 'no such approved claim event');
|
|
294
|
+
// ONLY a self/related approval (approver was itself a party) may be corrected — a genuinely
|
|
295
|
+
// independent claim has no self_or_related disclosure to make, and appending one would falsify its
|
|
296
|
+
// provenance (and is append-only/irreversible). Guards a typo'd approvedEventId.
|
|
297
|
+
const selfRelated = !!ap.approved_by && (ap.approved_by === ap.admin_account_id || ap.approved_by === ap.contributor_account_id);
|
|
298
|
+
if (!selfRelated)
|
|
299
|
+
return err('not_self_related', 'only self/related approvals may receive a marking correction');
|
|
300
|
+
// honest marking only — a correction can NEVER (re)assert independent_governance or drop disclosure.
|
|
301
|
+
if (approvalKind !== 'root_approval' && approvalKind !== 'founder_bootstrap_override') {
|
|
302
|
+
return err('dishonest_marking', 'correction approval_kind must be root_approval or founder_bootstrap_override');
|
|
303
|
+
}
|
|
304
|
+
if (conflictDisclosure !== 'self_or_related') {
|
|
305
|
+
return err('dishonest_marking', 'correction conflict_disclosure must be self_or_related');
|
|
306
|
+
}
|
|
307
|
+
if (!correctionReason || !correctionReason.trim())
|
|
308
|
+
return err('reason_required', 'correction_reason is required');
|
|
309
|
+
const id = generateId('aocmc');
|
|
310
|
+
db.prepare(`INSERT INTO admin_operator_claim_marking_corrections (correction_event_id, approved_event_id, approval_kind, conflict_disclosure, correction_reason, corrected_by_root_admin_id, immutable)
|
|
311
|
+
VALUES (?, ?, ?, ?, ?, ?, 1)`).run(id, approvedEventId, approvalKind, conflictDisclosure, correctionReason.trim(), correctorId);
|
|
312
|
+
return { ok: true, correctionEventId: id, approvedEventId };
|
|
313
|
+
}
|
|
314
|
+
/** Latest append-only marking correction for an approved claim event (or null). */
|
|
315
|
+
export function latestMarkingCorrection(db, approvedEventId) {
|
|
316
|
+
return db.prepare("SELECT * FROM admin_operator_claim_marking_corrections WHERE approved_event_id = ? ORDER BY corrected_at DESC, rowid DESC LIMIT 1").get(approvedEventId) ?? null;
|
|
317
|
+
}
|
|
318
|
+
/** All claims whose CONTRIBUTOR is this user (any status) — the contributor self-view. */
|
|
319
|
+
export function listContributorRelationships(db, contributorId) {
|
|
320
|
+
const ids = db.prepare("SELECT event_id FROM admin_operator_claim_events WHERE contributor_account_id = ? AND event_type = 'claimed' ORDER BY created_at DESC").all(contributorId);
|
|
321
|
+
return ids.map(r => deriveClaimState(db, r.event_id)).filter(Boolean);
|
|
322
|
+
}
|
|
323
|
+
/** Pending unlink requests across all claims — the ROOT review queue. */
|
|
324
|
+
export function listPendingUnlinkRequests(db) {
|
|
325
|
+
const reqs = db.prepare("SELECT * FROM admin_operator_unlink_requests WHERE event_type = 'requested' ORDER BY created_at DESC").all();
|
|
326
|
+
return reqs.filter(r => !db.prepare('SELECT 1 FROM admin_operator_unlink_requests WHERE supersedes_request_id = ?').get(r.request_event_id));
|
|
327
|
+
}
|
|
328
|
+
/** Pure: who to notify + what, for a claim transition. Self-link (admin==contributor) → deduped. */
|
|
329
|
+
export function claimNotificationSpecs(kind, claim, rootIds) {
|
|
330
|
+
const admin = claim.admin_account_id, contrib = claim.contributor_account_id;
|
|
331
|
+
const ME = '#me/operator-claims', ADMIN = '#admin/operator-claims';
|
|
332
|
+
const uniq = (xs) => { const seen = new Set(); return xs.filter(s => s.userId && !seen.has(s.userId) && seen.add(s.userId)); };
|
|
333
|
+
switch (kind) {
|
|
334
|
+
case 'proposed':
|
|
335
|
+
return [{ userId: contrib, title: '🔗 待确认的贡献归属关联', body: `管理席位 ${admin} 请求把协调贡献归属到你的账号,请确认或拒绝。`, href: ME, label: '去确认' }];
|
|
336
|
+
case 'accepted':
|
|
337
|
+
return uniq(rootIds.map(r => ({ userId: r, title: '🪪 操作席位关联待审批', body: `贡献人已确认来自管理席位 ${admin} 的关联,待你审批。`, href: ADMIN, label: '去审批' })));
|
|
338
|
+
case 'rejected_by_contributor':
|
|
339
|
+
return [{ userId: admin, title: '🔗 关联被贡献人拒绝', body: '贡献人拒绝了你发起的归属关联。', href: ME, label: '查看' }];
|
|
340
|
+
case 'approved':
|
|
341
|
+
return uniq([contrib, admin].map(u => ({ userId: u, title: '✅ 贡献归属关联已生效', body: `管理席位 ${admin} 的协调贡献现归属到该贡献人账号。`, href: ME, label: '查看' })));
|
|
342
|
+
case 'rejected_by_root':
|
|
343
|
+
return [{ userId: admin, title: '🔗 关联未通过审批', body: 'root 未通过你发起的归属关联。', href: ME, label: '查看' }];
|
|
344
|
+
case 'revoked':
|
|
345
|
+
return uniq([contrib, admin].map(u => ({ userId: u, title: '🪪 贡献归属关联已撤销', body: '此前的归属关联已被撤销。', href: ME, label: '查看' })));
|
|
346
|
+
case 'unlink_requested':
|
|
347
|
+
return uniq(rootIds.map(r => ({ userId: r, title: '🔓 贡献归属解除申请待审批', body: `有人申请解除管理席位 ${admin} 与贡献人的关联,待你审批。`, href: ADMIN, label: '去审批' })));
|
|
348
|
+
case 'unlink_approved':
|
|
349
|
+
return uniq([contrib, admin].map(u => ({ userId: u, title: '🔓 解除申请已通过,关联已撤销', body: `管理席位 ${admin} 与该贡献人账号的归属关联已解除。`, href: ME, label: '查看' })));
|
|
350
|
+
case 'unlink_rejected':
|
|
351
|
+
return uniq([contrib, admin].map(u => ({ userId: u, title: '🔒 解除申请被驳回,关联仍有效', body: `root 未通过解除申请,管理席位 ${admin} 的归属关联仍然有效。`, href: ME, label: '查看' })));
|
|
352
|
+
default: return [];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Insert the transition's notifications (best-effort; caller should try/catch so a notify failure never
|
|
357
|
+
* rolls back the claim). Returns the specs emitted. Uses the existing notifications table + its `actions`
|
|
358
|
+
* deep-link column so the notification is clickable straight to the right page.
|
|
359
|
+
*/
|
|
360
|
+
export function emitClaimNotifications(db, kind, claimedEventId) {
|
|
361
|
+
const claim = db.prepare("SELECT admin_account_id, contributor_account_id FROM admin_operator_claim_events WHERE event_id = ? AND event_type = 'claimed'").get(claimedEventId);
|
|
362
|
+
if (!claim)
|
|
363
|
+
return [];
|
|
364
|
+
const rootIds = db.prepare("SELECT id FROM users WHERE role = 'admin' AND admin_type = 'root'").all().map(r => r.id);
|
|
365
|
+
const specs = claimNotificationSpecs(kind, claim, rootIds);
|
|
366
|
+
const ins = db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, actions, created_at) VALUES (?,?,?,?,?,?,datetime('now'))`);
|
|
367
|
+
for (const s of specs) {
|
|
368
|
+
ins.run(generateId('ntf'), s.userId, 'operator_claim', s.title, s.body, JSON.stringify([{ kind: 'navigate', href: s.href, label: s.label, style: 'primary' }]));
|
|
369
|
+
}
|
|
370
|
+
return specs;
|
|
371
|
+
}
|
|
372
|
+
/** Resolve the claim (claimed) event id behind an approved event (for revoke notifications). */
|
|
373
|
+
export function claimedEventIdOfApproved(db, approvedEventId) {
|
|
374
|
+
const r = db.prepare("SELECT supersedes_event_id FROM admin_operator_claim_events WHERE event_id = ? AND event_type = 'approved'").get(approvedEventId);
|
|
375
|
+
return r?.supersedes_event_id ?? null;
|
|
376
|
+
}
|
|
377
|
+
// ── read helpers (route surfaces) ──
|
|
378
|
+
export function listClaimsForSeat(db, adminAccountId) {
|
|
379
|
+
const ids = db.prepare("SELECT event_id FROM admin_operator_claim_events WHERE admin_account_id = ? AND event_type = 'claimed' ORDER BY created_at DESC").all(adminAccountId);
|
|
380
|
+
return ids.map(r => deriveClaimState(db, r.event_id)).filter(Boolean);
|
|
381
|
+
}
|
|
382
|
+
export function listPendingConfirmationsForContributor(db, contributorId) {
|
|
383
|
+
const ids = db.prepare("SELECT event_id FROM admin_operator_claim_events WHERE contributor_account_id = ? AND event_type = 'claimed' ORDER BY created_at DESC").all(contributorId);
|
|
384
|
+
return ids.map(r => deriveClaimState(db, r.event_id)).filter(v => v && v.status === 'proposed');
|
|
385
|
+
}
|
|
386
|
+
export function listAllClaims(db, statusFilter) {
|
|
387
|
+
const ids = db.prepare("SELECT event_id FROM admin_operator_claim_events WHERE event_type = 'claimed' ORDER BY created_at DESC").all();
|
|
388
|
+
const all = ids.map(r => deriveClaimState(db, r.event_id)).filter(Boolean);
|
|
389
|
+
return statusFilter ? all.filter(v => v.status === statusFilter) : all;
|
|
390
|
+
}
|
|
@@ -180,3 +180,27 @@ export function setBuildTaskAudience(db, taskId, audience) {
|
|
|
180
180
|
const r = db.prepare(`UPDATE build_task_agent_metadata SET audience = ? WHERE task_id = ?`).run(a, taskId);
|
|
181
181
|
return r.changes;
|
|
182
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Set a task's real effort estimate (publish gate, #34/#5). Validates a non-zero duration (max >= min,
|
|
185
|
+
* max >= 1 — i.e. NOT the 0–0 placeholder) and optional budget / context-size enums. Used by the
|
|
186
|
+
* proposal→draft publish path so a placeholder draft can be given a real estimate before going public.
|
|
187
|
+
*/
|
|
188
|
+
export function setBuildTaskEstimate(db, taskId, e) {
|
|
189
|
+
if (!Number.isInteger(e.minMinutes) || !Number.isInteger(e.maxMinutes) || e.minMinutes < 0 || e.maxMinutes < e.minMinutes || e.maxMinutes < 1) {
|
|
190
|
+
throw new Error('estimated_duration must be integer minutes with max >= min and max >= 1 (a real, non-zero estimate)');
|
|
191
|
+
}
|
|
192
|
+
const budget = e.budget !== undefined ? assertEnum('estimated_agent_budget', e.budget, AGENT_BUDGETS) : null;
|
|
193
|
+
const context = e.contextSize !== undefined ? assertEnum('estimated_context_size', e.contextSize, CONTEXT_SIZES) : null;
|
|
194
|
+
const sets = ['estimated_duration_min_minutes = ?', 'estimated_duration_max_minutes = ?'];
|
|
195
|
+
const params = [e.minMinutes, e.maxMinutes];
|
|
196
|
+
if (budget !== null) {
|
|
197
|
+
sets.push('estimated_agent_budget = ?');
|
|
198
|
+
params.push(budget);
|
|
199
|
+
}
|
|
200
|
+
if (context !== null) {
|
|
201
|
+
sets.push('estimated_context_size = ?');
|
|
202
|
+
params.push(context);
|
|
203
|
+
}
|
|
204
|
+
params.push(taskId);
|
|
205
|
+
return db.prepare(`UPDATE build_task_agent_metadata SET ${sets.join(', ')} WHERE task_id = ?`).run(...params).changes;
|
|
206
|
+
}
|
|
@@ -5,9 +5,13 @@ export function guardParticipation(db, id, action) {
|
|
|
5
5
|
const task = getBuildTaskWithAgentMetadata(db, id, 'member'); // member scope hides restricted/internal; releaseExpiredClaims runs inside
|
|
6
6
|
if (!task)
|
|
7
7
|
return { ok: false, status: 404, code: 'NOT_FOUND', message: '任务不存在' }; // also covers restricted/internal → no existence leak
|
|
8
|
+
// Enforce on the DERIVED claimability (shapeMetadata, #5), NOT the raw auto_claimable — so a 0–0/null
|
|
9
|
+
// placeholder estimate (→ claimability 'manual_review') is refused server-side even when its raw
|
|
10
|
+
// auto_claimable flag is true, matching the list/detail/MCP/filter behavior. A metadata-bearing task always
|
|
11
|
+
// carries claimability; a no-metadata legacy task (meta null) keeps the legacy RFC-006 flow (not gated here).
|
|
8
12
|
const meta = task.agent_metadata;
|
|
9
|
-
if (action === 'claim' && meta && meta.
|
|
10
|
-
return { ok: false, status: 409, code: 'NOT_AUTO_CLAIMABLE', message: '
|
|
13
|
+
if (action === 'claim' && meta && meta.claimability !== 'auto_claimable') {
|
|
14
|
+
return { ok: false, status: 409, code: 'NOT_AUTO_CLAIMABLE', message: '该任务不可自助认领(claimability=manual_review):需真人在环(human_in_the_loop / human_only),或其估算为占位(0–0/未知)、须人工复核后再认领。 / Not auto-claimable (claimability=manual_review): needs a human in the loop, or its estimate is a placeholder (0–0/unknown) and must be reviewed before claiming.' };
|
|
11
15
|
}
|
|
12
16
|
return { ok: true, task };
|
|
13
17
|
}
|