@nelsonlongxiang/dsh-open-a2a-net 0.6.42 → 0.7.0
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/README.md +1 -0
- package/README_zh.md +1 -0
- package/assets/nexusDist/assets/index-CA_YZTWG.js +9 -0
- package/assets/nexusDist/index.html +1 -1
- package/cli/a2a.mjs +43 -7
- package/lib/a2a-client.js +10 -2
- package/lib/admitted-bookkeeping.js +7 -0
- package/lib/canvas-discovery.js +11 -1
- package/lib/canvas-member.js +43 -0
- package/lib/canvas-store.js +315 -17
- package/lib/client.js +62 -72
- package/lib/client.js.map +1 -1
- package/lib/config.js +1 -0
- package/lib/direct-forward.js +9 -7
- package/lib/faces/admin.js +150 -67
- package/lib/faces/files.js +2 -1
- package/lib/faces/wire.js +569 -469
- package/lib/followup-delivery.js +89 -0
- package/lib/idempotency-store.js +8 -0
- package/lib/inbound-verification.js +23 -0
- package/lib/index.js +534 -166
- package/lib/nudge-observability.js +34 -0
- package/lib/origin-claim.js +36 -9
- package/lib/outbound-scope.js +4 -2
- package/lib/peer-directory.js +15 -5
- package/lib/peer-store.js +31 -17
- package/lib/queue-guard.js +4 -3
- package/lib/receipt-ladder.js +1 -1
- package/lib/receipt-retry.js +58 -0
- package/lib/reconciliation-reminders.js +148 -0
- package/lib/reconciliation-state.js +27 -0
- package/lib/session-transcript.js +46 -18
- package/lib/skill/SKILL.md +5 -0
- package/lib/skill/references/cli.md +4 -1
- package/lib/skill/references/routing.md +31 -1
- package/lib/task-auth.js +3 -3
- package/lib/task-delivery.js +23 -0
- package/lib/task-ledger-persistence.js +51 -0
- package/lib/task-ledger-rows.js +5 -0
- package/lib/task-ledger.js +219 -20
- package/lib/tools/info.js +37 -7
- package/lib/types/a2a-client.d.ts +1 -1
- package/lib/types/admitted-bookkeeping.d.ts +5 -0
- package/lib/types/canvas-member.d.ts +10 -0
- package/lib/types/canvas-store.d.ts +56 -1
- package/lib/types/config.d.ts +2 -0
- package/lib/types/faces/admin.d.ts +21 -0
- package/lib/types/faces/files.d.ts +1 -1
- package/lib/types/faces/wire.d.ts +21 -14
- package/lib/types/followup-delivery.d.ts +25 -0
- package/lib/types/idempotency-store.d.ts +2 -0
- package/lib/types/inbound-verification.d.ts +14 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/nudge-observability.d.ts +17 -0
- package/lib/types/origin-claim.d.ts +7 -1
- package/lib/types/outbound-scope.d.ts +1 -0
- package/lib/types/peer-store.d.ts +4 -0
- package/lib/types/queue-guard.d.ts +9 -2
- package/lib/types/receipt-ladder.d.ts +2 -0
- package/lib/types/receipt-retry.d.ts +22 -0
- package/lib/types/reconciliation-reminders.d.ts +41 -0
- package/lib/types/reconciliation-state.d.ts +32 -0
- package/lib/types/session-transcript.d.ts +6 -1
- package/lib/types/task-auth.d.ts +7 -0
- package/lib/types/task-delivery.d.ts +17 -0
- package/lib/types/task-ledger-persistence.d.ts +3 -0
- package/lib/types/task-ledger-rows.d.ts +19 -2
- package/lib/types/task-ledger.d.ts +48 -1
- package/lib/types/tools/info.d.ts +3 -0
- package/lib/types/types.d.ts +3 -0
- package/nexus-stage/src/federation.ts +1 -0
- package/nexus-stage/src/main.ts +17 -2
- package/nexus-stage/src/member-provenance-picker.ts +13 -0
- package/nexus-stage/src/planning-member-handles.ts +4 -1
- package/nexus-stage/src/planning-view.ts +3 -3
- package/nexus-stage/src/workbench.ts +51 -7
- package/nexus-stage/src/world.ts +1 -0
- package/package.json +3 -2
- package/assets/nexusDist/assets/index-NkNSeIx6.js +0 -9
package/lib/faces/admin.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { join } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import {
|
|
21
|
+
import { reminderView } from "../reconciliation-state.js";
|
|
22
|
+
import { canvasNameReserved as reservedCanvasName, isRemoteMemberRef, validRemoteMemberRef, legacyBareMember } from "../canvas-store.js";
|
|
22
23
|
import { qualifiedCanvasId } from "../canvas-id.js";
|
|
23
24
|
import { refreshPeerCard, registerCanvasNotify } from "./canvas-notify.js";
|
|
24
25
|
import { CLI_HANDLE_RE } from "../cli-deposit.js";
|
|
@@ -76,6 +77,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
76
77
|
id: String(agent.id),
|
|
77
78
|
label: sessionLabelOf(agent),
|
|
78
79
|
team: sessionTeamOf(agent),
|
|
80
|
+
memberRef: `${sessionTeamOf(agent)}@${d.ownCanvasFingerprint}`,
|
|
79
81
|
...nodeMetadataOf(agent),
|
|
80
82
|
joined: sessionNodes.has(String(agent.id)),
|
|
81
83
|
live: true,
|
|
@@ -115,6 +117,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
115
117
|
label: `${session}-${id8(id)}`,
|
|
116
118
|
...(d.ownerSeatNames[id] !== undefined ? { name: d.ownerSeatNames[id], seatName: d.ownerSeatNames[id] } : {}),
|
|
117
119
|
team: `${config.team}/${id8(id)}`,
|
|
120
|
+
memberRef: `${config.team}/${id8(id)}@${d.ownCanvasFingerprint}`,
|
|
118
121
|
joined: true,
|
|
119
122
|
live: false,
|
|
120
123
|
teams: [...teamMemberships.teamsOf(id)],
|
|
@@ -133,6 +136,9 @@ export function registerAdminFaces(webServer, d) {
|
|
|
133
136
|
sessions,
|
|
134
137
|
groups: groupStore.list(),
|
|
135
138
|
canvas: {
|
|
139
|
+
...d.canvasMigrationState(),
|
|
140
|
+
// Raw retained slots are an inspection view, never the effective team member list.
|
|
141
|
+
retainedMembers: canvasStore.list().flatMap(name => canvasStore.rawMembersOf(name).filter(id => !canvasStore.membersOf(name).includes(id)).map(id => ({ team: name, member: id, suspended: canvasStore.isSuspended(name, id), records: canvasStore.suspensionOf(name, id) }))),
|
|
136
142
|
teams: canvasStore.list().map(name => ({
|
|
137
143
|
name,
|
|
138
144
|
team: canvasTeamOf(name),
|
|
@@ -148,16 +154,17 @@ export function registerAdminFaces(webServer, d) {
|
|
|
148
154
|
const rows = [];
|
|
149
155
|
const seenLocal = new Set();
|
|
150
156
|
for (const id of canvasStore.membersOf(name)) {
|
|
157
|
+
const legacy = legacyBareMember(id);
|
|
151
158
|
if (isRemoteMemberRef(id)) {
|
|
152
159
|
const localId = localSessionOfHandle(id);
|
|
153
160
|
if (localId !== undefined) {
|
|
154
161
|
if (seenLocal.has(localId))
|
|
155
162
|
continue;
|
|
156
163
|
seenLocal.add(localId);
|
|
157
|
-
rows.push({ id: localId, team: `${config.team}/${id8(localId)}`, joined: sessionNodes.has(localId) || joinedSessions.has(localId), live: sessionNodes.has(localId) });
|
|
164
|
+
rows.push({ ...(legacy ? { provenance: 'legacy-inferred' } : {}), id: localId, team: `${config.team}/${id8(localId)}`, joined: sessionNodes.has(localId) || joinedSessions.has(localId), live: sessionNodes.has(localId) });
|
|
158
165
|
continue;
|
|
159
166
|
}
|
|
160
|
-
rows.push({ id, team: id, remote: true, ...(CLI_HANDLE_RE.test(id) ? { cliHome: canvasStore.cliHomeOf(id) ?? null, cliHeld: cliDeposits.directoryRows().some(row => row.team === id), ...(canvasStore.wasCliHomeCleared(id) ? { cliHomeCleared: true } : {}) } : {}) });
|
|
167
|
+
rows.push({ ...(legacy ? { provenance: 'unknown' } : {}), id, team: id, remote: true, ...(CLI_HANDLE_RE.test(id) ? { cliHome: canvasStore.cliHomeOf(id) ?? null, cliHeld: cliDeposits.directoryRows().some(row => row.team === id), ...(canvasStore.wasCliHomeCleared(id) ? { cliHomeCleared: true } : {}) } : {}) });
|
|
161
168
|
continue;
|
|
162
169
|
}
|
|
163
170
|
if (seenLocal.has(id))
|
|
@@ -169,7 +176,8 @@ export function registerAdminFaces(webServer, d) {
|
|
|
169
176
|
})(),
|
|
170
177
|
})),
|
|
171
178
|
},
|
|
172
|
-
|
|
179
|
+
// #332: home-controlled aliases, never inferred from peer cards or a file reference.
|
|
180
|
+
host: { ...(lanIp !== '' ? { lanIp } : {}), originClaimAudiences: [...config.originClaimAudiences ?? []] },
|
|
173
181
|
// gh#20 W-C (host rail): each peer row also carries its display
|
|
174
182
|
// name and version from the warm card (absent until a fetch),
|
|
175
183
|
// and the last-contact timestamp for the health lamp. W-A's
|
|
@@ -189,7 +197,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
189
197
|
};
|
|
190
198
|
}),
|
|
191
199
|
remote: (refreshRemoteRows(), (d.remoteRowsCache?.rows ?? []).map(row => {
|
|
192
|
-
const canvasTeams = canvasStore.teamsOf(row.team);
|
|
200
|
+
const canvasTeams = [...new Set([...canvasStore.teamsOf(row.team), ...(row.memberRef ? canvasStore.teamsOf(row.memberRef) : [])])];
|
|
193
201
|
return canvasTeams.length > 0 ? { ...row, canvasTeams: [...canvasTeams] } : row;
|
|
194
202
|
})),
|
|
195
203
|
activity: recentActivity.slice(),
|
|
@@ -202,12 +210,19 @@ export function registerAdminFaces(webServer, d) {
|
|
|
202
210
|
// in-flight ring cannot show. Dead-lettered rows ride their own
|
|
203
211
|
// list and settled ones just a count, all additive so the panel
|
|
204
212
|
// keeps rendering plain owed rows until it learns the tiers.
|
|
213
|
+
ledgerPersistence: taskLedger.persistenceState(),
|
|
214
|
+
deliveryBookkeeping: taskLedger.deliveryBookkeepingState(),
|
|
215
|
+
nudgeEscalations: d.nudgeEscalations?.() ?? { queued: 0, unavailable: 0, failed: 0 },
|
|
216
|
+
reconciliationReminders: d.reminderDiagnostics?.(),
|
|
205
217
|
tasks: taskLedger.list()
|
|
206
218
|
.filter(task => task.status === 'pending')
|
|
207
219
|
.map(task => ({
|
|
220
|
+
...reminderView(taskLedger.reminderState(task.taskId)),
|
|
208
221
|
taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, status: task.status,
|
|
209
222
|
direction: task.direction ?? 'outbound', receiptExpected: task.receiptExpected !== false,
|
|
210
223
|
...(task.answeredAt !== undefined ? { answeredAt: task.answeredAt } : {}),
|
|
224
|
+
...(task.receiptDelivery ? { receiptDelivery: task.receiptDelivery } : {}),
|
|
225
|
+
...(task.consumed !== undefined ? { consumed: task.consumed, deliveryObservation: task.consumed ? 'consumed' : 'delivered-unconsumed' } : {}),
|
|
211
226
|
...(task.forwarded === true ? { forwarded: true } : {}),
|
|
212
227
|
...(task.deliveredTo !== undefined ? { deliveredTo: task.deliveredTo } : {}),
|
|
213
228
|
...(task.receiverMembers !== undefined ? { receiverMembers: task.receiverMembers } : {}),
|
|
@@ -229,7 +244,8 @@ export function registerAdminFaces(webServer, d) {
|
|
|
229
244
|
tasksStaleCount: taskLedger.list().filter(task => task.status === 'pending' && (task.receiptExpected === false || Date.now() - task.startedAt >= 60 * 60_000)).length,
|
|
230
245
|
tasksDead: taskLedger.list()
|
|
231
246
|
.filter(task => task.status === 'dead')
|
|
232
|
-
.map(task => ({ taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, deadAt: task.deadAt ?? task.startedAt, reason: task.reason ?? 'unspecified', ...(task.answeredAt !== undefined ? { answeredAt: task.answeredAt } : {}),
|
|
247
|
+
.map(task => ({ taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, deadAt: task.deadAt ?? task.startedAt, reason: task.reason ?? 'unspecified', ...(task.answeredAt !== undefined ? { answeredAt: task.answeredAt } : {}),
|
|
248
|
+
...(task.receiptDelivery ? { receiptDelivery: task.receiptDelivery } : {}), ...(task.brief !== undefined ? { brief: task.brief } : {}), ...(task.direction !== undefined ? { direction: task.direction } : {}), ...(task.sender !== undefined ? { sender: task.sender } : {}), ...(task.auth !== undefined ? { auth: task.auth } : task.direction === 'inbound' ? { authLegacy: true } : {}) })),
|
|
233
249
|
// gh#31: per-task outcomes for rows that LEFT the ledger — the
|
|
234
250
|
// chat drawer used to guess (settled, abandoned or cancelled).
|
|
235
251
|
// Bounded to the archive (≤64), newest first, with the
|
|
@@ -248,6 +264,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
248
264
|
// its direction when known.
|
|
249
265
|
archived: taskLedger.archive().slice(0, ARCHIVE_CAP).map(row => ({
|
|
250
266
|
taskId: row.taskId,
|
|
267
|
+
...(row.withdrawalResult ? { withdrawalResult: row.withdrawalResult } : {}),
|
|
251
268
|
team: row.team,
|
|
252
269
|
peer: row.peer,
|
|
253
270
|
startedAt: row.startedAt,
|
|
@@ -340,6 +357,9 @@ export function registerAdminFaces(webServer, d) {
|
|
|
340
357
|
boundMissInbound: d.boundMissInboundCount,
|
|
341
358
|
// Who missed, by verified caller and the origin it dialled (newest first, ≤16).
|
|
342
359
|
boundMissRecent: d.boundMissRecent,
|
|
360
|
+
// Present-but-unverifiable envelopes by claimed caller and reason (bad-ts = sender clock skew > ±5 min).
|
|
361
|
+
claimRejectsInbound: d.claimRejectsInboundCount,
|
|
362
|
+
claimRejectsRecent: d.claimRejectsRecent,
|
|
343
363
|
// gh#113: CLI mailbox loss, all handles — mail the 64-row cap
|
|
344
364
|
// pushed out, and mail cut at 4000 chars. The team verifier
|
|
345
365
|
// holds `dropped` to zero.
|
|
@@ -445,7 +465,16 @@ export function registerAdminFaces(webServer, d) {
|
|
|
445
465
|
readJsonBody(req, res, (body) => {
|
|
446
466
|
const id = typeof body.id === 'string' ? body.id : '';
|
|
447
467
|
// gh#131: read while the session is still joined — its own handle reads as it only then.
|
|
448
|
-
|
|
468
|
+
try {
|
|
469
|
+
if (id !== '')
|
|
470
|
+
d.revokeCanvasMember(id, 'leave');
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
const payload = JSON.stringify({ ok: false, error: String(error) });
|
|
474
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
475
|
+
res.end(payload);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
449
478
|
if (id !== '')
|
|
450
479
|
sessionNodes.delete(id);
|
|
451
480
|
joinedSessions.remove(id);
|
|
@@ -453,9 +482,6 @@ export function registerAdminFaces(webServer, d) {
|
|
|
453
482
|
// without join consent would be a routing backdoor. Team
|
|
454
483
|
// roster declarations die the same way (S2 consistency) — and so
|
|
455
484
|
// does a membership a fleet edit stored by the session's handle.
|
|
456
|
-
canvasStore.dropMember(id);
|
|
457
|
-
if (ownHandle !== undefined)
|
|
458
|
-
canvasStore.dropMember(ownHandle);
|
|
459
485
|
teamMemberships.dropSession(id);
|
|
460
486
|
const payload = JSON.stringify({ id });
|
|
461
487
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
@@ -517,7 +543,7 @@ export function registerAdminFaces(webServer, d) {
|
|
|
517
543
|
path: '/__dsh_a2a/fleet-canvas-op',
|
|
518
544
|
handler: controlRoute((req, res) => {
|
|
519
545
|
readJsonBody(req, res, async (body) => {
|
|
520
|
-
const { peer, op, name, id } = body;
|
|
546
|
+
const { peer, op, name, id, memberPeer } = body;
|
|
521
547
|
const peerUrl = typeof peer === 'string' ? peer : '';
|
|
522
548
|
const opV = typeof op === 'string' ? op : '';
|
|
523
549
|
const nameV = typeof name === 'string' ? name : '';
|
|
@@ -536,7 +562,13 @@ export function registerAdminFaces(webServer, d) {
|
|
|
536
562
|
}
|
|
537
563
|
try {
|
|
538
564
|
const target = peerUrl.replace(/\/$/, '') + '/a2a/canvas-op';
|
|
539
|
-
const
|
|
565
|
+
const proof = opV === 'add-member' ? await d.canvasMemberForWrite(idV, memberPeer) : undefined;
|
|
566
|
+
if (proof && !proof.ok) {
|
|
567
|
+
send(JSON.stringify(proof));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const selectedId = proof?.ok ? (proof.member.includes('/') ? proof.member : `${config.team}/${id8(proof.member)}@${d.ownCanvasFingerprint}`) : idV !== '' && !isRemoteMemberRef(idV) && (sessionNodes.has(idV) || joinedSessions.has(idV)) ? `${config.team}/${id8(idV)}@${d.ownCanvasFingerprint}` : idV;
|
|
571
|
+
const body = JSON.stringify({ op: opV, name: nameV, ...(opV === 'add-member' || opV === 'remove-member' ? { id: selectedId } : {}) });
|
|
540
572
|
const reply = await d.peerFetch(target, {
|
|
541
573
|
method: 'POST',
|
|
542
574
|
headers: { 'Content-Type': 'application/json', ...originClaimHeaders({ url: target, method: 'POST', body }) },
|
|
@@ -545,6 +577,12 @@ export function registerAdminFaces(webServer, d) {
|
|
|
545
577
|
body,
|
|
546
578
|
signal: AbortSignal.timeout(15_000),
|
|
547
579
|
});
|
|
580
|
+
// #320: a JSON body cannot turn a failed/redirected HTTP operation into success.
|
|
581
|
+
if (!reply.ok || reply.status < 200 || reply.status >= 300) {
|
|
582
|
+
void reply.body?.cancel().catch(() => { });
|
|
583
|
+
send(JSON.stringify({ ok: false, status: reply.status, error: `peer ${peerUrl} answered HTTP ${String(reply.status)} — fleet canvas operation failed` }));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
548
586
|
const raw = await reply.text();
|
|
549
587
|
try {
|
|
550
588
|
const out = JSON.parse(raw);
|
|
@@ -738,72 +776,117 @@ export function registerAdminFaces(webServer, d) {
|
|
|
738
776
|
readJsonBody(req, res, async (body) => {
|
|
739
777
|
const action = body.action;
|
|
740
778
|
let payload;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
779
|
+
try {
|
|
780
|
+
if (action === 'confirm-member') {
|
|
781
|
+
const name = typeof body.name === 'string' ? body.name : '', id = typeof body.id === 'string' ? body.id : '';
|
|
782
|
+
const request = body;
|
|
783
|
+
const replacement = typeof request.replacement === 'string' ? request.replacement : '';
|
|
784
|
+
const originalHandle = id.replace(/@[0-9a-f]{8}$/, '');
|
|
785
|
+
const localChoice = replacement !== '' && !replacement.includes('/');
|
|
786
|
+
const proof = localChoice && originalHandle !== `${config.team}/${id8(replacement)}` ? { ok: false, error: 'local confirmation must match this host zone and the original handle' } : await d.canvasMemberForWrite(replacement, request.peer);
|
|
787
|
+
const out = proof.ok ? canvasStore.confirmMember(name, id, proof.member, proof.ownerKey, request.revision, request.expectedOwner) : proof;
|
|
788
|
+
logger.info('canvas provenance owner confirmation actor=owner-control at=%s team=%s old=%s new=%s ownerKey=%s ok=%s', new Date().toISOString(), name, id, proof.ok ? proof.member : replacement, proof.ok ? proof.ownerKey ?? 'local-id' : 'unconfirmed', out.ok);
|
|
789
|
+
payload = JSON.stringify({ ...out, ...canvasStore.state() });
|
|
790
|
+
}
|
|
791
|
+
else if (action === 'restore-suspended' || action === 'delete-suspended') {
|
|
792
|
+
const request = body;
|
|
793
|
+
const name = typeof request.name === 'string' ? request.name : '', id = typeof request.id === 'string' ? request.id : '';
|
|
794
|
+
const out = canvasStore.regulateSuspended(action === 'restore-suspended' ? 'restore' : 'delete', name, id, request.revision, request.expected);
|
|
795
|
+
logger.info('canvas suspended owner operation %s %s %s: %s', action, name, id, out.ok);
|
|
796
|
+
payload = JSON.stringify({ ...out, ...canvasStore.state() });
|
|
797
|
+
}
|
|
798
|
+
else if (action === 'cli-home-options') {
|
|
799
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
800
|
+
const id = typeof body.id === 'string' ? body.id : '';
|
|
801
|
+
if (!CLI_HANDLE_RE.test(id) || !canvasStore.membersOf(name).includes(id))
|
|
802
|
+
payload = JSON.stringify({ ok: false, error: 'an existing canvas CLI member is required' });
|
|
803
|
+
else {
|
|
804
|
+
try {
|
|
805
|
+
payload = JSON.stringify({ ok: true, ...await d.cliHomeOptions(id) });
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
payload = JSON.stringify({ ok: false, error: 'fresh home card lookup failed' });
|
|
809
|
+
}
|
|
752
810
|
}
|
|
753
811
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
payload = JSON.stringify(stored === undefined ? { ok: false, error: 'invalid, roster-reserved name or team cap reached' } : { ok: true, name: stored, teams: canvasStore.list() });
|
|
759
|
-
}
|
|
760
|
-
else if (action === 'remove') {
|
|
761
|
-
const name = typeof body.name === 'string' ? body.name : '';
|
|
762
|
-
payload = JSON.stringify({ ok: canvasStore.remove(name), teams: canvasStore.list() });
|
|
763
|
-
}
|
|
764
|
-
else if (action === 'add-member') {
|
|
765
|
-
const name = typeof body.name === 'string' ? body.name : '';
|
|
766
|
-
const id = typeof body.id === 'string' ? body.id : '';
|
|
767
|
-
// Dual member shape: a local session id must be a joined node;
|
|
768
|
-
// a remote ref is a zone-qualified team handle whose shape this
|
|
769
|
-
// gate checks and whose existence the UI picker (directory
|
|
770
|
-
// rows) vouches for. Humans compose both via the workbench.
|
|
771
|
-
const memberValid = isRemoteMemberRef(id)
|
|
772
|
-
? validRemoteMemberRef(id)
|
|
773
|
-
: id !== '' && (sessionNodes.has(id) || joinedSessions.has(id));
|
|
774
|
-
if (!canvasStore.hasTeam(name) && canvasNameReserved(name)) {
|
|
775
|
-
payload = JSON.stringify({ ok: false, error: 'roster reserves this canvas name' });
|
|
812
|
+
else if (action === 'create') {
|
|
813
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
814
|
+
const stored = canvasNameReserved(name) ? undefined : canvasStore.create(name);
|
|
815
|
+
payload = JSON.stringify(stored === undefined ? { ok: false, error: 'invalid, roster-reserved name or team cap reached' } : { ok: true, name: stored, teams: canvasStore.list() });
|
|
776
816
|
}
|
|
777
|
-
else if (
|
|
778
|
-
|
|
817
|
+
else if (action === 'remove') {
|
|
818
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
819
|
+
payload = JSON.stringify({ ok: canvasStore.remove(name), teams: canvasStore.list() });
|
|
779
820
|
}
|
|
780
|
-
else {
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
821
|
+
else if (action === 'add-member') {
|
|
822
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
823
|
+
const id = typeof body.id === 'string' ? body.id : '';
|
|
824
|
+
// Dual member shape: a local session id must be a joined node;
|
|
825
|
+
// a remote ref is a zone-qualified team handle whose shape this
|
|
826
|
+
// gate checks and whose existence the UI picker (directory
|
|
827
|
+
// rows) vouches for. Humans compose both via the workbench.
|
|
828
|
+
const memberValid = isRemoteMemberRef(id)
|
|
829
|
+
? validRemoteMemberRef(id)
|
|
830
|
+
: id !== '' && (sessionNodes.has(id) || joinedSessions.has(id));
|
|
831
|
+
if (!canvasStore.hasTeam(name) && canvasNameReserved(name)) {
|
|
832
|
+
payload = JSON.stringify({ ok: false, error: 'roster reserves this canvas name' });
|
|
833
|
+
}
|
|
834
|
+
else if (name === '' || !memberValid) {
|
|
835
|
+
payload = JSON.stringify({ ok: false, error: 'name and a member (joined session id, or a zone-qualified remote team handle like zone/a1b2c3d4) are required' });
|
|
836
|
+
}
|
|
837
|
+
else if (legacyBareMember(id) && canvasStore.rawMembersOf(name).includes(id)) {
|
|
838
|
+
// Re-adding a retained legacy slot is a no-op, never provenance confirmation or restore.
|
|
839
|
+
payload = JSON.stringify({ ok: true, members: canvasStore.membersOf(name), provenance: 'unknown' });
|
|
840
|
+
}
|
|
784
841
|
else {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
842
|
+
const proof = await d.canvasMemberForWrite(id, body.peer);
|
|
843
|
+
if (!proof.ok) {
|
|
844
|
+
payload = JSON.stringify(proof);
|
|
845
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
846
|
+
res.end(payload);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const pin = await d.cliHomeForAdd(proof.member, body.home).catch(() => ({ ok: false, error: 'linked home card lookup failed' }));
|
|
850
|
+
if (!pin.ok)
|
|
851
|
+
payload = JSON.stringify({ ok: false, error: pin.error });
|
|
852
|
+
else {
|
|
853
|
+
canvasStore.create(name);
|
|
854
|
+
const explicitHome = body.home !== undefined;
|
|
855
|
+
const home = explicitHome ? pin.home : canvasStore.cliHomeOf(id) ?? pin.home;
|
|
856
|
+
payload = JSON.stringify({ ok: canvasStore.addMember(name, proof.member, home, { replaceHome: explicitHome, ownerKey: proof.ownerKey }), teams: canvasStore.list(), members: canvasStore.membersOf(name) });
|
|
857
|
+
}
|
|
789
858
|
}
|
|
790
859
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
860
|
+
else if (action === 'remove-member') {
|
|
861
|
+
const name = typeof body.name === 'string' ? body.name : '';
|
|
862
|
+
const id = typeof body.id === 'string' ? body.id : '';
|
|
863
|
+
if (name === '' || id === '') {
|
|
864
|
+
payload = JSON.stringify({ ok: false, error: 'name and id are required' });
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
// gh#131: removing a local session removes its handle-stored slot too.
|
|
868
|
+
let removed = false;
|
|
869
|
+
try {
|
|
870
|
+
removed = isRemoteMemberRef(id) ? (!canvasStore.isSuspended(name, id) && canvasStore.removeMember(name, id)) : d.revokeCanvasMember(id, 'remove', name);
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
payload = JSON.stringify({ ok: false, error: String(error) });
|
|
874
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
875
|
+
res.end(payload);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
payload = JSON.stringify({ ok: removed, teams: canvasStore.list(), members: canvasStore.membersOf(name) });
|
|
879
|
+
}
|
|
797
880
|
}
|
|
798
881
|
else {
|
|
799
|
-
|
|
800
|
-
const ownHandle = isRemoteMemberRef(id) ? undefined : ownCanvasHandleOf(id);
|
|
801
|
-
const removed = [id, ...(ownHandle !== undefined ? [ownHandle] : [])].map(each => canvasStore.removeMember(name, each)).some(Boolean);
|
|
802
|
-
payload = JSON.stringify({ ok: removed, teams: canvasStore.list(), members: canvasStore.membersOf(name) });
|
|
882
|
+
payload = JSON.stringify({ ok: false, error: 'unknown action' });
|
|
803
883
|
}
|
|
804
884
|
}
|
|
805
|
-
|
|
806
|
-
|
|
885
|
+
catch (error) {
|
|
886
|
+
const failure = JSON.stringify({ ok: false, error: String(error) });
|
|
887
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
888
|
+
res.end(failure);
|
|
889
|
+
return;
|
|
807
890
|
}
|
|
808
891
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
|
|
809
892
|
res.end(payload);
|
package/lib/faces/files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BLOB_FILE_MAX } from "../blob-store.js";
|
|
2
2
|
import { isRootOriginUrl } from "../origin-claim.js";
|
|
3
3
|
import { claimCovers } from "../task-auth.js";
|
|
4
|
+
import { splitQualifiedMember } from "../canvas-member.js";
|
|
4
5
|
/** Pulls per proven signer (host zone or CLI handle) in a sliding minute (design §8; the FloodGuard figure). */
|
|
5
6
|
export const BLOB_PULLS_PER_MINUTE = 16;
|
|
6
7
|
/** Distinct signers the pull limiter remembers; a full table refuses new signers rather than forgetting live ones. */
|
|
@@ -91,7 +92,7 @@ export function registerFileFaces(webServer, d) {
|
|
|
91
92
|
refuse(403, 'untrusted-origin', 'the envelope does not prove this caller (it must be the audience-bound form, signed from a root card URL whose zone covers caller_session)');
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
|
-
const grant = blobStore.list().find(g => g.sha256 === sha256 && grantAdmits(g.to, caller, claim.caller, config.team, name => canvasStore.membersOf(name), claim.cli === true));
|
|
95
|
+
const grant = blobStore.list().find(g => g.sha256 === sha256 && grantAdmits(g.to, caller, claim.caller, config.team, name => canvasStore.membersOf(name).flatMap(ref => { const member = splitQualifiedMember(ref); return member ? (canvasStore.ownerKeyOf?.(ref) === claim.card.publicKey ? [member.handle] : []) : [ref]; }), claim.cli === true));
|
|
95
96
|
// One answer for "no such blob", "expired" and "not yours": a refusal is no existence oracle.
|
|
96
97
|
if (grant === undefined) {
|
|
97
98
|
refuse(403, 'not-granted', 'no live grant lets this caller pull that file');
|