@codemem/server 0.30.0 → 0.31.1
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.d.ts.map +1 -1
- package/dist/index.js +367 -27
- package/dist/index.js.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +1329 -56
- package/static/index.html +295 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAiB,OAAO,EAAE,MAAM,eAAe,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,yBAAyB,CAAC;AAQjC,OAAO,EAAE,OAAO,EAAE,CAAC;AAKnB,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,EAAE,MAAM,WAAW,CAAC;IACjC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE;QACtB,OAAO,CAAC,EAAE,0BAA0B,CAAC;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAC;KACtC,CAAC;IACF,oBAAoB,CAAC,EAAE,MAAM;QAC5B,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,IAAI,CAAC;CACT;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAiB,OAAO,EAAE,MAAM,eAAe,CAAC;AAE1F,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,yBAAyB,CAAC;AAQjC,OAAO,EAAE,OAAO,EAAE,CAAC;AAKnB,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,WAAW,CAKtC;AAED,mDAAmD;AACnD,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B,YAAY,CAAC,EAAE,MAAM,WAAW,CAAC;IACjC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE;QACtB,OAAO,CAAC,EAAE,0BAA0B,CAAC;QACrC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,wBAAwB,CAAC,EAAE,MAAM,CAAC;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAC;KACtC,CAAC;IACF,oBAAoB,CAAC,EAAE,MAAM;QAC5B,KAAK,EACF,UAAU,GACV,SAAS,GACT,UAAU,GACV,OAAO,GACP,UAAU,GACV,iBAAiB,GACjB,iBAAiB,GACjB,IAAI,CAAC;QACR,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvB,GAAG,IAAI,CAAC;CACT;AAED,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,UAAU,8EA6D1C;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,CAAC,EAAE,UAAU,8EA8B9C"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
+
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
4
4
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { createMiddleware } from "hono/factory";
|
|
@@ -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,18 +2468,228 @@ function recentScopeRejectionsByPeer(store) {
|
|
|
2454
2468
|
return map;
|
|
2455
2469
|
}
|
|
2456
2470
|
function legacySharedReviewSummary(store) {
|
|
2457
|
-
const
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
const
|
|
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();
|
|
2475
|
+
const candidatesByIdentity = new Map(listProjectScopeCandidates(store.db, { limit: null }).map((candidate) => [candidate.workspace_identity, candidate]));
|
|
2476
|
+
const groupsByIdentity = /* @__PURE__ */ new Map();
|
|
2477
|
+
for (const row of rows) {
|
|
2478
|
+
const identity = canonicalWorkspaceIdentity({
|
|
2479
|
+
cwd: row.cwd,
|
|
2480
|
+
gitBranch: row.git_branch,
|
|
2481
|
+
gitRemote: row.git_remote,
|
|
2482
|
+
project: row.project,
|
|
2483
|
+
workspaceId: row.workspace_id
|
|
2484
|
+
});
|
|
2485
|
+
const candidate = candidatesByIdentity.get(identity.value);
|
|
2486
|
+
const resolvedScope = candidate?.resolved_scope_id ?? null;
|
|
2487
|
+
const suggestedScope = resolvedScope && resolvedScope !== LOCAL_DEFAULT_SCOPE_ID && resolvedScope !== LEGACY_SHARED_REVIEW_SCOPE_ID ? resolvedScope : candidate?.suggested_scope_id;
|
|
2488
|
+
const existing = groupsByIdentity.get(identity.value);
|
|
2489
|
+
const isOwnedBySelf = ownedBySelf(row);
|
|
2490
|
+
const rowUpdatedAt = row.updated_at ?? null;
|
|
2491
|
+
if (existing) {
|
|
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;
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
groupsByIdentity.set(identity.value, {
|
|
2499
|
+
workspace_identity: identity.value,
|
|
2500
|
+
identity_source: identity.source,
|
|
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,
|
|
2506
|
+
suggested_scope_id: suggestedScope ?? null,
|
|
2507
|
+
suggestion_reason: suggestedScope ? candidate?.suggestion_reason ?? "Existing project mapping can be reviewed as a destination, but legacy data is not promoted automatically." : null
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
const totalGroupCount = groupsByIdentity.size;
|
|
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));
|
|
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) => ({
|
|
2513
|
+
authority_type: scope.authority_type,
|
|
2514
|
+
label: scope.label || scope.scope_id,
|
|
2515
|
+
scope_id: scope.scope_id
|
|
2516
|
+
}));
|
|
2464
2517
|
return {
|
|
2465
2518
|
scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
|
|
2466
2519
|
memory_count: memoryCount,
|
|
2467
2520
|
has_data: memoryCount > 0,
|
|
2468
|
-
last_updated_at:
|
|
2521
|
+
last_updated_at: lastUpdatedAt,
|
|
2522
|
+
groups,
|
|
2523
|
+
total_group_count: totalGroupCount,
|
|
2524
|
+
target_scopes: targetScopes
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
function legacySharedReviewRows(store) {
|
|
2528
|
+
return store.db.prepare(`SELECT m.id,
|
|
2529
|
+
m.rev,
|
|
2530
|
+
m.active,
|
|
2531
|
+
m.deleted_at,
|
|
2532
|
+
m.updated_at,
|
|
2533
|
+
m.scope_id,
|
|
2534
|
+
s.project,
|
|
2535
|
+
s.cwd,
|
|
2536
|
+
s.git_remote,
|
|
2537
|
+
s.git_branch,
|
|
2538
|
+
m.workspace_id,
|
|
2539
|
+
m.actor_id,
|
|
2540
|
+
m.origin_device_id,
|
|
2541
|
+
m.metadata_json
|
|
2542
|
+
FROM memory_items m
|
|
2543
|
+
LEFT JOIN sessions s ON s.id = m.session_id
|
|
2544
|
+
WHERE m.scope_id = ?
|
|
2545
|
+
AND m.active = 1
|
|
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) => {
|
|
2629
|
+
return canonicalWorkspaceIdentity({
|
|
2630
|
+
cwd: row.cwd,
|
|
2631
|
+
gitBranch: row.git_branch,
|
|
2632
|
+
gitRemote: row.git_remote,
|
|
2633
|
+
project: row.project,
|
|
2634
|
+
workspaceId: row.workspace_id
|
|
2635
|
+
}).value === workspaceIdentity;
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
function reassignableLegacySharedReviewRows(store, rows) {
|
|
2639
|
+
const reassignableRows = rows.filter((row) => store.memoryOwnedBySelf(row));
|
|
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.");
|
|
2641
|
+
const currentRows = store.db.prepare(`SELECT id, scope_id, active, deleted_at
|
|
2642
|
+
FROM memory_items
|
|
2643
|
+
WHERE id IN (${reassignableRows.map(() => "?").join(",")})`).all(...reassignableRows.map((row) => row.id));
|
|
2644
|
+
const currentById = new Map(currentRows.map((row) => [Number(row.id), row]));
|
|
2645
|
+
for (const row of reassignableRows) {
|
|
2646
|
+
const current = currentById.get(row.id);
|
|
2647
|
+
if (current?.scope_id !== LEGACY_SHARED_REVIEW_SCOPE_ID || current.active !== 1 || current.deleted_at) throw new Error("legacy shared review group changed before reassignment; refresh and try again");
|
|
2648
|
+
}
|
|
2649
|
+
return reassignableRows;
|
|
2650
|
+
}
|
|
2651
|
+
function legacySharedReviewReassignmentPreview(store, input) {
|
|
2652
|
+
if (!input.workspaceIdentity.trim()) throw new Error("workspace_identity must be a non-empty string");
|
|
2653
|
+
if (!input.scopeId.trim()) throw new Error("scope_id must be a non-empty string");
|
|
2654
|
+
if (input.scopeId === LOCAL_DEFAULT_SCOPE_ID) throw new Error("local-default is not a valid target for legacy shared review reassignment");
|
|
2655
|
+
if (input.scopeId === LEGACY_SHARED_REVIEW_SCOPE_ID) throw new Error("legacy-shared-review is a review bucket, not an assignable Sharing domain");
|
|
2656
|
+
const targetScope = listSharingDomainSettingsScopes(store.db).find((scope) => scope.scope_id === input.scopeId);
|
|
2657
|
+
if (!targetScope) throw new Error(`scope_id ${input.scopeId} is not an active Sharing domain`);
|
|
2658
|
+
assertLocalDeviceScopeMembership(store, input.scopeId);
|
|
2659
|
+
const rows = legacySharedReviewRowsForWorkspace(store, input.workspaceIdentity);
|
|
2660
|
+
if (rows.length === 0) throw new Error("legacy shared review group not found");
|
|
2661
|
+
const reassignableRows = reassignableLegacySharedReviewRows(store, rows);
|
|
2662
|
+
const peerDeviceIds = [...new Set(rows.map((row) => String(row.origin_device_id ?? "").trim()).filter((deviceId) => deviceId && deviceId !== store.deviceId))].sort();
|
|
2663
|
+
return {
|
|
2664
|
+
affected_peer_device_count: peerDeviceIds.length,
|
|
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
|
+
}),
|
|
2672
|
+
memory_count: rows.length,
|
|
2673
|
+
reassignable_memory_count: reassignableRows.length,
|
|
2674
|
+
scope_id: input.scopeId,
|
|
2675
|
+
skipped_memory_count: rows.length - reassignableRows.length,
|
|
2676
|
+
target_scope_label: targetScope.label || targetScope.scope_id,
|
|
2677
|
+
warning: "This changes future sync authorization for reassignable local memories. It does not erase data already copied to peers under legacy-shared-review.",
|
|
2678
|
+
workspace_identity: input.workspaceIdentity
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
function reassignLegacySharedReviewGroup(store, input) {
|
|
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");
|
|
2684
|
+
const rows = reassignableLegacySharedReviewRows(store, legacySharedReviewRowsForWorkspace(store, input.workspaceIdentity));
|
|
2685
|
+
store.db.transaction(() => {
|
|
2686
|
+
for (const row of rows) store.reassignMemoryScope(row.id, input.scopeId);
|
|
2687
|
+
})();
|
|
2688
|
+
return {
|
|
2689
|
+
ok: true,
|
|
2690
|
+
...preview,
|
|
2691
|
+
reassigned_memory_count: rows.length,
|
|
2692
|
+
legacy_shared_review: legacySharedReviewSummary(store)
|
|
2469
2693
|
};
|
|
2470
2694
|
}
|
|
2471
2695
|
var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
|
|
@@ -3162,32 +3386,104 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3162
3386
|
local_default_scope_id: LOCAL_DEFAULT_SCOPE_ID
|
|
3163
3387
|
});
|
|
3164
3388
|
});
|
|
3165
|
-
app.
|
|
3389
|
+
app.get("/api/sync/projects", (c) => {
|
|
3390
|
+
const store = getStore();
|
|
3391
|
+
const limit = Math.max(1, queryInt(c.req.query("limit"), 50));
|
|
3392
|
+
const offset = Math.max(0, queryInt(c.req.query("offset"), 0));
|
|
3393
|
+
return c.json(listProjectScopeInventory(store.db, {
|
|
3394
|
+
identitySource: c.req.query("identity_source"),
|
|
3395
|
+
limit,
|
|
3396
|
+
offset,
|
|
3397
|
+
query: c.req.query("q"),
|
|
3398
|
+
scopeId: c.req.query("scope_id"),
|
|
3399
|
+
status: c.req.query("status")
|
|
3400
|
+
}));
|
|
3401
|
+
});
|
|
3402
|
+
app.post("/api/sync/projects/reassign-project", async (c) => {
|
|
3403
|
+
const store = getStore();
|
|
3404
|
+
const body = await parseViewerJsonBody(c);
|
|
3405
|
+
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
3406
|
+
try {
|
|
3407
|
+
const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
|
|
3408
|
+
const project = optionalViewerStrictString(body, "project") ?? "";
|
|
3409
|
+
const result = reassignProjectScopeInventoryProject(store.db, {
|
|
3410
|
+
project,
|
|
3411
|
+
workspaceIdentity
|
|
3412
|
+
});
|
|
3413
|
+
return c.json({
|
|
3414
|
+
ok: true,
|
|
3415
|
+
...result
|
|
3416
|
+
});
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3419
|
+
return c.json({ error: message }, message.includes("not found") ? 404 : 400);
|
|
3420
|
+
}
|
|
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
|
+
});
|
|
3448
|
+
app.post("/api/sync/legacy-shared-review/reassign", async (c) => {
|
|
3166
3449
|
const store = getStore();
|
|
3167
3450
|
const body = await parseViewerJsonBody(c);
|
|
3168
3451
|
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
3169
3452
|
try {
|
|
3170
|
-
const
|
|
3171
|
-
const priority = optionalViewerInteger(body, "priority");
|
|
3172
|
-
if (Number.isNaN(id)) return c.json({ error: "id must be an integer" }, 400);
|
|
3173
|
-
if (Number.isNaN(priority)) return c.json({ error: "priority must be an integer" }, 400);
|
|
3174
|
-
const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity");
|
|
3175
|
-
const projectPattern = optionalViewerStrictString(body, "project_pattern");
|
|
3453
|
+
const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
|
|
3176
3454
|
const scopeId = optionalViewerStrictString(body, "scope_id") ?? "";
|
|
3177
|
-
const
|
|
3455
|
+
const confirmationToken = optionalViewerStrictString(body, "confirmation_token");
|
|
3456
|
+
const confirmedOldCopies = body.confirmed_old_copies === true;
|
|
3457
|
+
const preview = legacySharedReviewReassignmentPreview(store, {
|
|
3458
|
+
scopeId,
|
|
3459
|
+
workspaceIdentity
|
|
3460
|
+
});
|
|
3461
|
+
if (!confirmedOldCopies) return c.json({
|
|
3462
|
+
error: "legacy_review_confirmation_required",
|
|
3463
|
+
message: preview.warning,
|
|
3464
|
+
preview
|
|
3465
|
+
}, 409);
|
|
3466
|
+
return c.json(reassignLegacySharedReviewGroup(store, {
|
|
3467
|
+
confirmationToken,
|
|
3468
|
+
scopeId,
|
|
3469
|
+
workspaceIdentity
|
|
3470
|
+
}));
|
|
3471
|
+
} catch (error) {
|
|
3472
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3473
|
+
return c.json({ error: message }, message.includes("not found") ? 404 : 400);
|
|
3474
|
+
}
|
|
3475
|
+
});
|
|
3476
|
+
app.put("/api/sync/sharing-domains/project-mappings", async (c) => {
|
|
3477
|
+
const store = getStore();
|
|
3478
|
+
const body = await parseViewerJsonBody(c);
|
|
3479
|
+
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
3480
|
+
try {
|
|
3178
3481
|
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
3179
|
-
const mappingInput =
|
|
3180
|
-
id,
|
|
3181
|
-
workspace_identity: workspaceIdentity,
|
|
3182
|
-
project_pattern: projectPattern,
|
|
3183
|
-
scope_id: scopeId,
|
|
3184
|
-
priority,
|
|
3185
|
-
source
|
|
3186
|
-
};
|
|
3482
|
+
const mappingInput = parseViewerProjectMappingInput(body);
|
|
3187
3483
|
const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput);
|
|
3188
|
-
if (analysis.requested_workspace_identity?.startsWith("unmapped:")
|
|
3484
|
+
if (analysis.requested_workspace_identity?.startsWith("unmapped:")) return c.json({
|
|
3189
3485
|
error: "unmapped_project_local_only",
|
|
3190
|
-
message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a
|
|
3486
|
+
message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a Sharing domain.",
|
|
3191
3487
|
guardrail_warnings: analysis.warnings
|
|
3192
3488
|
}, 400);
|
|
3193
3489
|
const missingConfirmations = missingGuardrailConfirmations(analysis.warnings, confirmedGuardrailTokens);
|
|
@@ -3211,6 +3507,45 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3211
3507
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3212
3508
|
}
|
|
3213
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
|
+
});
|
|
3214
3549
|
app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => {
|
|
3215
3550
|
const store = getStore();
|
|
3216
3551
|
const id = Number(c.req.param("id"));
|
|
@@ -4706,6 +5041,10 @@ function createApp(opts) {
|
|
|
4706
5041
|
app.route("/", rawEventsRoutes(storeFactory, sweeper));
|
|
4707
5042
|
app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus));
|
|
4708
5043
|
const staticRoot = process.env.CODEMEM_VIEWER_STATIC_DIR ?? join(import.meta.dirname ?? ".", "../static");
|
|
5044
|
+
app.use("/assets/*", async (c, next) => {
|
|
5045
|
+
c.header("Cache-Control", "no-cache");
|
|
5046
|
+
await next();
|
|
5047
|
+
});
|
|
4709
5048
|
app.use("/assets/*", serveStatic({
|
|
4710
5049
|
root: staticRoot,
|
|
4711
5050
|
rewriteRequestPath: (path) => path.replace(/^\/assets/, "")
|
|
@@ -4715,6 +5054,7 @@ function createApp(opts) {
|
|
|
4715
5054
|
const indexHtml = readFileSync(indexPath, "utf-8");
|
|
4716
5055
|
app.get("*", (c) => {
|
|
4717
5056
|
if (c.req.path.startsWith("/api/")) return c.json({ error: "not found" }, 404);
|
|
5057
|
+
c.header("Cache-Control", "no-store");
|
|
4718
5058
|
return c.html(indexHtml);
|
|
4719
5059
|
});
|
|
4720
5060
|
return app;
|