@codemem/server 0.31.0 → 0.31.2
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/dist/index.js +202 -68
- package/dist/index.js.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +3 -3
- package/static/app.js +298 -39
package/dist/index.js
CHANGED
|
@@ -1771,6 +1771,20 @@ function missingGuardrailConfirmations(warnings, confirmedTokens) {
|
|
|
1771
1771
|
const confirmed = new Set(confirmedTokens);
|
|
1772
1772
|
return warnings.filter((warning) => warning.requires_confirmation && (!warning.confirmation_token || !confirmed.has(warning.confirmation_token)));
|
|
1773
1773
|
}
|
|
1774
|
+
function parseViewerProjectMappingInput(body) {
|
|
1775
|
+
const id = optionalViewerInteger(body, "id");
|
|
1776
|
+
const priority = optionalViewerInteger(body, "priority");
|
|
1777
|
+
if (Number.isNaN(id)) throw new Error("id must be an integer");
|
|
1778
|
+
if (Number.isNaN(priority)) throw new Error("priority must be an integer");
|
|
1779
|
+
return {
|
|
1780
|
+
id,
|
|
1781
|
+
workspace_identity: optionalViewerStrictString(body, "workspace_identity"),
|
|
1782
|
+
project_pattern: optionalViewerStrictString(body, "project_pattern"),
|
|
1783
|
+
scope_id: optionalViewerStrictString(body, "scope_id") ?? "",
|
|
1784
|
+
priority,
|
|
1785
|
+
source: optionalViewerStrictString(body, "source") ?? "user"
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1774
1788
|
function coordinatorAdminMutationStatus(message) {
|
|
1775
1789
|
if (message.includes("scope_not_found") || message.includes("membership_not_found") || message.includes("group_not_found") || message.includes("Group not found") || message.includes("Scope not found") || message.includes("device_not_enrolled_for_scope_group") || message.includes("device must be enrolled")) return 404;
|
|
1776
1790
|
if (message.includes("not active") || message.includes("scope_not_active") || message.includes("group_archived") || message.includes("Group is archived")) return 409;
|
|
@@ -2454,60 +2468,47 @@ function recentScopeRejectionsByPeer(store) {
|
|
|
2454
2468
|
return map;
|
|
2455
2469
|
}
|
|
2456
2470
|
function legacySharedReviewSummary(store) {
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
AND active = 1
|
|
2462
|
-
AND deleted_at IS NULL`).get(LEGACY_SHARED_REVIEW_SCOPE_ID);
|
|
2463
|
-
const memoryCount = Number(row?.memory_count ?? 0);
|
|
2471
|
+
const rows = legacySharedReviewRows(store);
|
|
2472
|
+
const memoryCount = rows.length;
|
|
2473
|
+
const lastUpdatedAt = rows.reduce((current, row) => row.updated_at && (!current || row.updated_at > current) ? row.updated_at : current, null);
|
|
2474
|
+
const ownedBySelf = store.buildOwnershipPredicate();
|
|
2464
2475
|
const candidatesByIdentity = new Map(listProjectScopeCandidates(store.db, { limit: null }).map((candidate) => [candidate.workspace_identity, candidate]));
|
|
2465
|
-
const groupRows = store.db.prepare(`SELECT s.project,
|
|
2466
|
-
s.cwd,
|
|
2467
|
-
s.git_remote,
|
|
2468
|
-
s.git_branch,
|
|
2469
|
-
m.workspace_id,
|
|
2470
|
-
COUNT(*) AS memory_count,
|
|
2471
|
-
MAX(m.updated_at) AS last_updated_at
|
|
2472
|
-
FROM memory_items m
|
|
2473
|
-
LEFT JOIN sessions s ON s.id = m.session_id
|
|
2474
|
-
WHERE m.scope_id = ?
|
|
2475
|
-
AND m.active = 1
|
|
2476
|
-
AND m.deleted_at IS NULL
|
|
2477
|
-
GROUP BY s.project, s.cwd, s.git_remote, s.git_branch, m.workspace_id
|
|
2478
|
-
ORDER BY memory_count DESC, last_updated_at DESC`).all(LEGACY_SHARED_REVIEW_SCOPE_ID);
|
|
2479
2476
|
const groupsByIdentity = /* @__PURE__ */ new Map();
|
|
2480
|
-
for (const
|
|
2477
|
+
for (const row of rows) {
|
|
2481
2478
|
const identity = canonicalWorkspaceIdentity({
|
|
2482
|
-
cwd:
|
|
2483
|
-
gitBranch:
|
|
2484
|
-
gitRemote:
|
|
2485
|
-
project:
|
|
2486
|
-
workspaceId:
|
|
2479
|
+
cwd: row.cwd,
|
|
2480
|
+
gitBranch: row.git_branch,
|
|
2481
|
+
gitRemote: row.git_remote,
|
|
2482
|
+
project: row.project,
|
|
2483
|
+
workspaceId: row.workspace_id
|
|
2487
2484
|
});
|
|
2488
2485
|
const candidate = candidatesByIdentity.get(identity.value);
|
|
2489
2486
|
const resolvedScope = candidate?.resolved_scope_id ?? null;
|
|
2490
2487
|
const suggestedScope = resolvedScope && resolvedScope !== LOCAL_DEFAULT_SCOPE_ID && resolvedScope !== LEGACY_SHARED_REVIEW_SCOPE_ID ? resolvedScope : candidate?.suggested_scope_id;
|
|
2491
2488
|
const existing = groupsByIdentity.get(identity.value);
|
|
2492
|
-
const
|
|
2493
|
-
const
|
|
2489
|
+
const isOwnedBySelf = ownedBySelf(row);
|
|
2490
|
+
const rowUpdatedAt = row.updated_at ?? null;
|
|
2494
2491
|
if (existing) {
|
|
2495
|
-
existing.memory_count +=
|
|
2496
|
-
if (
|
|
2492
|
+
existing.memory_count += 1;
|
|
2493
|
+
if (isOwnedBySelf) existing.reassignable_memory_count += 1;
|
|
2494
|
+
else existing.peer_owned_memory_count += 1;
|
|
2495
|
+
if (rowUpdatedAt && (!existing.last_updated_at || rowUpdatedAt > existing.last_updated_at)) existing.last_updated_at = rowUpdatedAt;
|
|
2497
2496
|
continue;
|
|
2498
2497
|
}
|
|
2499
2498
|
groupsByIdentity.set(identity.value, {
|
|
2500
2499
|
workspace_identity: identity.value,
|
|
2501
2500
|
identity_source: identity.source,
|
|
2502
|
-
display_project: identity.displayProject ??
|
|
2503
|
-
memory_count:
|
|
2504
|
-
|
|
2501
|
+
display_project: identity.displayProject ?? row.project ?? row.cwd ?? identity.value,
|
|
2502
|
+
memory_count: 1,
|
|
2503
|
+
reassignable_memory_count: isOwnedBySelf ? 1 : 0,
|
|
2504
|
+
peer_owned_memory_count: isOwnedBySelf ? 0 : 1,
|
|
2505
|
+
last_updated_at: rowUpdatedAt,
|
|
2505
2506
|
suggested_scope_id: suggestedScope ?? null,
|
|
2506
2507
|
suggestion_reason: suggestedScope ? candidate?.suggestion_reason ?? "Existing project mapping can be reviewed as a destination, but legacy data is not promoted automatically." : null
|
|
2507
2508
|
});
|
|
2508
2509
|
}
|
|
2509
2510
|
const totalGroupCount = groupsByIdentity.size;
|
|
2510
|
-
const groups = [...groupsByIdentity.values()].sort((left, right) => right.memory_count - left.memory_count || (right.last_updated_at ?? "").localeCompare(left.last_updated_at ?? "") || left.workspace_identity.localeCompare(right.workspace_identity))
|
|
2511
|
+
const groups = [...groupsByIdentity.values()].sort((left, right) => right.memory_count - left.memory_count || (right.last_updated_at ?? "").localeCompare(left.last_updated_at ?? "") || left.workspace_identity.localeCompare(right.workspace_identity));
|
|
2511
2512
|
const targetScopes = listSharingDomainSettingsScopes(store.db).filter((scope) => scope.scope_id !== LOCAL_DEFAULT_SCOPE_ID && scope.scope_id !== LEGACY_SHARED_REVIEW_SCOPE_ID).map((scope) => ({
|
|
2512
2513
|
authority_type: scope.authority_type,
|
|
2513
2514
|
label: scope.label || scope.scope_id,
|
|
@@ -2517,28 +2518,18 @@ function legacySharedReviewSummary(store) {
|
|
|
2517
2518
|
scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
|
|
2518
2519
|
memory_count: memoryCount,
|
|
2519
2520
|
has_data: memoryCount > 0,
|
|
2520
|
-
last_updated_at:
|
|
2521
|
+
last_updated_at: lastUpdatedAt,
|
|
2521
2522
|
groups,
|
|
2522
2523
|
total_group_count: totalGroupCount,
|
|
2523
2524
|
target_scopes: targetScopes
|
|
2524
2525
|
};
|
|
2525
2526
|
}
|
|
2526
|
-
function
|
|
2527
|
-
if (scopeId === LOCAL_DEFAULT_SCOPE_ID) return;
|
|
2528
|
-
if (!store.db.prepare(`SELECT 1
|
|
2529
|
-
FROM scope_memberships sm
|
|
2530
|
-
JOIN replication_scopes rs ON rs.scope_id = sm.scope_id
|
|
2531
|
-
WHERE sm.scope_id = ?
|
|
2532
|
-
AND sm.device_id = ?
|
|
2533
|
-
AND sm.status = 'active'
|
|
2534
|
-
AND rs.status = 'active'
|
|
2535
|
-
AND sm.membership_epoch >= rs.membership_epoch
|
|
2536
|
-
LIMIT 1`).get(scopeId, store.deviceId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
|
|
2537
|
-
}
|
|
2538
|
-
function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
|
|
2527
|
+
function legacySharedReviewRows(store) {
|
|
2539
2528
|
return store.db.prepare(`SELECT m.id,
|
|
2529
|
+
m.rev,
|
|
2540
2530
|
m.active,
|
|
2541
2531
|
m.deleted_at,
|
|
2532
|
+
m.updated_at,
|
|
2542
2533
|
m.scope_id,
|
|
2543
2534
|
s.project,
|
|
2544
2535
|
s.cwd,
|
|
@@ -2552,7 +2543,89 @@ function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
|
|
|
2552
2543
|
LEFT JOIN sessions s ON s.id = m.session_id
|
|
2553
2544
|
WHERE m.scope_id = ?
|
|
2554
2545
|
AND m.active = 1
|
|
2555
|
-
AND m.deleted_at IS NULL`).all(LEGACY_SHARED_REVIEW_SCOPE_ID)
|
|
2546
|
+
AND m.deleted_at IS NULL`).all(LEGACY_SHARED_REVIEW_SCOPE_ID);
|
|
2547
|
+
}
|
|
2548
|
+
function legacySharedReviewConfirmationToken(input) {
|
|
2549
|
+
const ownedBySelf = input.store.buildOwnershipPredicate();
|
|
2550
|
+
const payload = input.rows.map((row) => {
|
|
2551
|
+
const ownership = ownedBySelf(row) ? "local" : "peer";
|
|
2552
|
+
return `${row.id}:${row.rev ?? 0}:${row.scope_id ?? ""}:${ownership}`;
|
|
2553
|
+
}).sort().join(",");
|
|
2554
|
+
return Buffer.from(`${input.workspaceIdentity}|${input.scopeId}|${payload}`, "utf8").toString("base64url");
|
|
2555
|
+
}
|
|
2556
|
+
function projectInventoryRowsForWorkspace(store, workspaceIdentity) {
|
|
2557
|
+
return store.db.prepare(`SELECT m.id,
|
|
2558
|
+
m.rev,
|
|
2559
|
+
s.project,
|
|
2560
|
+
s.cwd,
|
|
2561
|
+
s.git_remote,
|
|
2562
|
+
s.git_branch,
|
|
2563
|
+
m.workspace_id,
|
|
2564
|
+
m.actor_id,
|
|
2565
|
+
m.origin_device_id,
|
|
2566
|
+
m.metadata_json
|
|
2567
|
+
FROM memory_items m
|
|
2568
|
+
LEFT JOIN sessions s ON s.id = m.session_id
|
|
2569
|
+
WHERE m.active = 1
|
|
2570
|
+
AND m.deleted_at IS NULL`).all().filter((row) => {
|
|
2571
|
+
return canonicalWorkspaceIdentity({
|
|
2572
|
+
cwd: row.cwd,
|
|
2573
|
+
gitBranch: row.git_branch,
|
|
2574
|
+
gitRemote: row.git_remote,
|
|
2575
|
+
project: row.project,
|
|
2576
|
+
workspaceId: row.workspace_id
|
|
2577
|
+
}).value === workspaceIdentity;
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
function projectForgetConfirmationToken(store, rows) {
|
|
2581
|
+
const ownedBySelf = store.buildOwnershipPredicate();
|
|
2582
|
+
const payload = rows.map((row) => {
|
|
2583
|
+
const ownership = ownedBySelf(row) ? "local" : "peer";
|
|
2584
|
+
return `${row.id}:${row.rev ?? 0}:${ownership}`;
|
|
2585
|
+
}).sort().join(",");
|
|
2586
|
+
return Buffer.from(payload, "utf8").toString("base64url");
|
|
2587
|
+
}
|
|
2588
|
+
function forgetProjectInventoryLocalMemories(store, input) {
|
|
2589
|
+
if (!input.workspaceIdentity.trim()) throw new Error("workspace_identity must be a non-empty string");
|
|
2590
|
+
const rows = projectInventoryRowsForWorkspace(store, input.workspaceIdentity);
|
|
2591
|
+
if (rows.length === 0) throw new Error("project identity not found");
|
|
2592
|
+
const ownedBySelf = store.buildOwnershipPredicate();
|
|
2593
|
+
const localRows = rows.filter((row) => ownedBySelf(row));
|
|
2594
|
+
const confirmationToken = projectForgetConfirmationToken(store, rows);
|
|
2595
|
+
const preview = {
|
|
2596
|
+
confirmation_token: confirmationToken,
|
|
2597
|
+
local_owned_memory_count: localRows.length,
|
|
2598
|
+
peer_owned_memory_count: rows.length - localRows.length,
|
|
2599
|
+
workspace_identity: input.workspaceIdentity
|
|
2600
|
+
};
|
|
2601
|
+
if (!input.confirmed) return {
|
|
2602
|
+
confirmed: false,
|
|
2603
|
+
...preview
|
|
2604
|
+
};
|
|
2605
|
+
if (input.confirmationToken !== confirmationToken) throw new Error("project memories changed before cleanup; refresh and try again");
|
|
2606
|
+
store.db.transaction(() => {
|
|
2607
|
+
for (const row of localRows) store.forget(row.id);
|
|
2608
|
+
})();
|
|
2609
|
+
return {
|
|
2610
|
+
confirmed: true,
|
|
2611
|
+
forgotten_memory_count: localRows.length,
|
|
2612
|
+
...preview
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
function assertLocalDeviceScopeMembership(store, scopeId) {
|
|
2616
|
+
if (scopeId === LOCAL_DEFAULT_SCOPE_ID) return;
|
|
2617
|
+
if (!store.db.prepare(`SELECT 1
|
|
2618
|
+
FROM scope_memberships sm
|
|
2619
|
+
JOIN replication_scopes rs ON rs.scope_id = sm.scope_id
|
|
2620
|
+
WHERE sm.scope_id = ?
|
|
2621
|
+
AND sm.device_id = ?
|
|
2622
|
+
AND sm.status = 'active'
|
|
2623
|
+
AND rs.status = 'active'
|
|
2624
|
+
AND sm.membership_epoch >= rs.membership_epoch
|
|
2625
|
+
LIMIT 1`).get(scopeId, store.deviceId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
|
|
2626
|
+
}
|
|
2627
|
+
function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
|
|
2628
|
+
return legacySharedReviewRows(store).filter((row) => {
|
|
2556
2629
|
return canonicalWorkspaceIdentity({
|
|
2557
2630
|
cwd: row.cwd,
|
|
2558
2631
|
gitBranch: row.git_branch,
|
|
@@ -2564,7 +2637,7 @@ function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
|
|
|
2564
2637
|
}
|
|
2565
2638
|
function reassignableLegacySharedReviewRows(store, rows) {
|
|
2566
2639
|
const reassignableRows = rows.filter((row) => store.memoryOwnedBySelf(row));
|
|
2567
|
-
if (reassignableRows.length === 0) throw new Error("legacy
|
|
2640
|
+
if (reassignableRows.length === 0) throw new Error("This legacy review group contains only peer-owned memories; this device cannot reassign them to a Sharing domain.");
|
|
2568
2641
|
const currentRows = store.db.prepare(`SELECT id, scope_id, active, deleted_at
|
|
2569
2642
|
FROM memory_items
|
|
2570
2643
|
WHERE id IN (${reassignableRows.map(() => "?").join(",")})`).all(...reassignableRows.map((row) => row.id));
|
|
@@ -2590,6 +2663,12 @@ function legacySharedReviewReassignmentPreview(store, input) {
|
|
|
2590
2663
|
return {
|
|
2591
2664
|
affected_peer_device_count: peerDeviceIds.length,
|
|
2592
2665
|
affected_peer_device_ids: peerDeviceIds.slice(0, 5),
|
|
2666
|
+
confirmation_token: legacySharedReviewConfirmationToken({
|
|
2667
|
+
rows,
|
|
2668
|
+
scopeId: input.scopeId,
|
|
2669
|
+
store,
|
|
2670
|
+
workspaceIdentity: input.workspaceIdentity
|
|
2671
|
+
}),
|
|
2593
2672
|
memory_count: rows.length,
|
|
2594
2673
|
reassignable_memory_count: reassignableRows.length,
|
|
2595
2674
|
scope_id: input.scopeId,
|
|
@@ -2601,8 +2680,11 @@ function legacySharedReviewReassignmentPreview(store, input) {
|
|
|
2601
2680
|
}
|
|
2602
2681
|
function reassignLegacySharedReviewGroup(store, input) {
|
|
2603
2682
|
const preview = legacySharedReviewReassignmentPreview(store, input);
|
|
2683
|
+
if (input.confirmationToken !== preview.confirmation_token) throw new Error("legacy shared review group changed before reassignment; refresh and try again");
|
|
2604
2684
|
const rows = reassignableLegacySharedReviewRows(store, legacySharedReviewRowsForWorkspace(store, input.workspaceIdentity));
|
|
2605
|
-
|
|
2685
|
+
store.db.transaction(() => {
|
|
2686
|
+
for (const row of rows) store.reassignMemoryScope(row.id, input.scopeId);
|
|
2687
|
+
})();
|
|
2606
2688
|
return {
|
|
2607
2689
|
ok: true,
|
|
2608
2690
|
...preview,
|
|
@@ -3337,6 +3419,32 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3337
3419
|
return c.json({ error: message }, message.includes("not found") ? 404 : 400);
|
|
3338
3420
|
}
|
|
3339
3421
|
});
|
|
3422
|
+
app.post("/api/sync/projects/forget", async (c) => {
|
|
3423
|
+
const store = getStore();
|
|
3424
|
+
const body = await parseViewerJsonBody(c);
|
|
3425
|
+
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
3426
|
+
try {
|
|
3427
|
+
const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
|
|
3428
|
+
const confirmationToken = optionalViewerStrictString(body, "confirmation_token");
|
|
3429
|
+
const confirmed = body.confirmed === true;
|
|
3430
|
+
const result = forgetProjectInventoryLocalMemories(store, {
|
|
3431
|
+
confirmationToken,
|
|
3432
|
+
confirmed,
|
|
3433
|
+
workspaceIdentity
|
|
3434
|
+
});
|
|
3435
|
+
if (!confirmed) return c.json({
|
|
3436
|
+
error: "project_forget_confirmation_required",
|
|
3437
|
+
preview: result
|
|
3438
|
+
}, 409);
|
|
3439
|
+
return c.json({
|
|
3440
|
+
ok: true,
|
|
3441
|
+
...result
|
|
3442
|
+
});
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3445
|
+
return c.json({ error: message }, message.includes("not found") ? 404 : 400);
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3340
3448
|
app.post("/api/sync/legacy-shared-review/reassign", async (c) => {
|
|
3341
3449
|
const store = getStore();
|
|
3342
3450
|
const body = await parseViewerJsonBody(c);
|
|
@@ -3344,6 +3452,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3344
3452
|
try {
|
|
3345
3453
|
const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
|
|
3346
3454
|
const scopeId = optionalViewerStrictString(body, "scope_id") ?? "";
|
|
3455
|
+
const confirmationToken = optionalViewerStrictString(body, "confirmation_token");
|
|
3347
3456
|
const confirmedOldCopies = body.confirmed_old_copies === true;
|
|
3348
3457
|
const preview = legacySharedReviewReassignmentPreview(store, {
|
|
3349
3458
|
scopeId,
|
|
@@ -3355,6 +3464,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3355
3464
|
preview
|
|
3356
3465
|
}, 409);
|
|
3357
3466
|
return c.json(reassignLegacySharedReviewGroup(store, {
|
|
3467
|
+
confirmationToken,
|
|
3358
3468
|
scopeId,
|
|
3359
3469
|
workspaceIdentity
|
|
3360
3470
|
}));
|
|
@@ -3368,23 +3478,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3368
3478
|
const body = await parseViewerJsonBody(c);
|
|
3369
3479
|
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
3370
3480
|
try {
|
|
3371
|
-
const id = optionalViewerInteger(body, "id");
|
|
3372
|
-
const priority = optionalViewerInteger(body, "priority");
|
|
3373
|
-
if (Number.isNaN(id)) return c.json({ error: "id must be an integer" }, 400);
|
|
3374
|
-
if (Number.isNaN(priority)) return c.json({ error: "priority must be an integer" }, 400);
|
|
3375
|
-
const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity");
|
|
3376
|
-
const projectPattern = optionalViewerStrictString(body, "project_pattern");
|
|
3377
|
-
const scopeId = optionalViewerStrictString(body, "scope_id") ?? "";
|
|
3378
|
-
const source = optionalViewerStrictString(body, "source") ?? "user";
|
|
3379
3481
|
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
3380
|
-
const mappingInput =
|
|
3381
|
-
id,
|
|
3382
|
-
workspace_identity: workspaceIdentity,
|
|
3383
|
-
project_pattern: projectPattern,
|
|
3384
|
-
scope_id: scopeId,
|
|
3385
|
-
priority,
|
|
3386
|
-
source
|
|
3387
|
-
};
|
|
3482
|
+
const mappingInput = parseViewerProjectMappingInput(body);
|
|
3388
3483
|
const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput);
|
|
3389
3484
|
if (analysis.requested_workspace_identity?.startsWith("unmapped:")) return c.json({
|
|
3390
3485
|
error: "unmapped_project_local_only",
|
|
@@ -3412,6 +3507,45 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3412
3507
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3413
3508
|
}
|
|
3414
3509
|
});
|
|
3510
|
+
app.put("/api/sync/sharing-domains/project-mappings/bulk", async (c) => {
|
|
3511
|
+
const store = getStore();
|
|
3512
|
+
const body = await parseViewerJsonBody(c);
|
|
3513
|
+
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
3514
|
+
try {
|
|
3515
|
+
const rawMappings = body.mappings;
|
|
3516
|
+
if (!Array.isArray(rawMappings) || rawMappings.length === 0) return c.json({ error: "mappings must be a non-empty array" }, 400);
|
|
3517
|
+
const mappingInputs = rawMappings.map((raw) => {
|
|
3518
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
|
|
3519
|
+
return parseViewerProjectMappingInput(raw);
|
|
3520
|
+
});
|
|
3521
|
+
const analyses = mappingInputs.map((mappingInput) => analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput));
|
|
3522
|
+
const unmapped = analyses.find((analysis) => analysis.requested_workspace_identity?.startsWith("unmapped:"));
|
|
3523
|
+
if (unmapped) return c.json({
|
|
3524
|
+
error: "unmapped_project_local_only",
|
|
3525
|
+
message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a Sharing domain.",
|
|
3526
|
+
guardrail_warnings: unmapped.warnings
|
|
3527
|
+
}, 400);
|
|
3528
|
+
const missingConfirmations = analyses.flatMap((analysis) => missingGuardrailConfirmations(analysis.warnings, []));
|
|
3529
|
+
if (missingConfirmations.length > 0) {
|
|
3530
|
+
const missingCodes = [...new Set(missingConfirmations.map((warning) => warning.code))];
|
|
3531
|
+
const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
|
|
3532
|
+
return c.json({
|
|
3533
|
+
error: "guardrail_confirmation_required",
|
|
3534
|
+
required_guardrails: missingCodes,
|
|
3535
|
+
required_guardrail_tokens: missingTokens,
|
|
3536
|
+
guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
|
|
3537
|
+
}, 409);
|
|
3538
|
+
}
|
|
3539
|
+
const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, mappingInput)));
|
|
3540
|
+
return c.json({
|
|
3541
|
+
ok: true,
|
|
3542
|
+
mappings: saveMappings(),
|
|
3543
|
+
guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
|
|
3544
|
+
});
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
3415
3549
|
app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => {
|
|
3416
3550
|
const store = getStore();
|
|
3417
3551
|
const id = Number(c.req.param("id"));
|