@codemem/server 0.46.0-beta.1 → 0.46.0-rc.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/application/coordinator-maintenance.d.ts.map +1 -1
- package/dist/application/sync-status.d.ts +7 -0
- package/dist/application/sync-status.d.ts.map +1 -1
- package/dist/index.js +323 -134
- package/dist/index.js.map +1 -1
- package/dist/routes/sync.d.ts +14 -0
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +44 -55
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/index.html +152 -63
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
3
|
-
import { ACCESS_CLEANUP_OP_TYPE, CodememConfigMutationError, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, LegacyTeamSetupAdditiveConvergenceError, LegacyTeamSetupRosterCapacityError, MEMORY_FILTER_FIELD_TYPES, MEMORY_FILTER_NAMES, MEMORY_KIND_DESCRIPTIONS, MemoryStore, ObserverClient, PROMPT_TRANSPORT_PROTOCOL_RANGE, REMEMBER_MEMORY_KINDS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterClausesWithContext, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalWorkspaceIdentity, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, dedupeOrderedIds, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, deriveLegacyTeamSetupCompletionManifest, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrideValues, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isAutomaticRecallMeasurement, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, judgeDistillReport, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, memoryFilterValueMatchesType, mergeAddresses, migrateRecipientPolicyIntent, mutateCodememConfigFile, negotiateSyncCapability, normalizeAddress, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectMatchesFilter, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordAutomaticRecall, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveDbPath, resolveObserverRuntime, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, storeVectors, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference,
|
|
3
|
+
import { ACCESS_CLEANUP_OP_TYPE, CodememConfigMutationError, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, LegacyTeamSetupAdditiveConvergenceError, LegacyTeamSetupRosterCapacityError, MEMORY_FILTER_FIELD_TYPES, MEMORY_FILTER_NAMES, MEMORY_KIND_DESCRIPTIONS, MemoryStore, ObserverClient, PROMPT_TRANSPORT_PROTOCOL_RANGE, REMEMBER_MEMORY_KINDS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, analyzeProjectScopeMappingChangesGuardrails, analyzeProjectScopeMappingDeletionGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterClausesWithContext, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalRepositoryProjectIdentity, canonicalWorkspaceIdentity, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, dedupeOrderedIds, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, deriveLegacyTeamSetupCompletionManifest, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrideValues, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isAutomaticRecallMeasurement, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, judgeDistillReport, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, memoryFilterValueMatchesType, mergeAddresses, migrateRecipientPolicyIntent, mutateCodememConfigFile, negotiateSyncCapability, normalizeAddress, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectMatchesFilter, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordAutomaticRecall, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, repositoryIdentitiesByWorkspace, repositoryIdentityForWorkspace, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveDbPath, resolveObserverRuntime, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, storeVectors, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMappings, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, validateRawEvents, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, wakeRecipientPoliciesForIdentities, wakeRecipientPoliciesForProjectIdentities } 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";
|
|
@@ -3847,6 +3847,8 @@ var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 3e4;
|
|
|
3847
3847
|
var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 3e5;
|
|
3848
3848
|
var TERMINAL_SHARE_MAINTENANCE_ERRORS = /* @__PURE__ */ new Set([
|
|
3849
3849
|
"coordinator_not_configured",
|
|
3850
|
+
"conflicting_repository_mappings",
|
|
3851
|
+
"project_mapping_conflict",
|
|
3850
3852
|
"team_sharing_not_configured",
|
|
3851
3853
|
"team_selection_ambiguous",
|
|
3852
3854
|
"initiating_device_not_reviewed",
|
|
@@ -4048,25 +4050,69 @@ async function advancePendingProjectSharesOperation(store, options) {
|
|
|
4048
4050
|
var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
|
|
4049
4051
|
var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
|
|
4050
4052
|
var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
|
|
4053
|
+
function recipientPolicyMaintenanceCandidates(store, retryBefore, limit) {
|
|
4054
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
4055
|
+
const canonicalize = (projectId) => canonicalRepositoryProjectIdentity(repositoryIdentities, projectId);
|
|
4056
|
+
const projectIds = store.db.prepare(`SELECT canonical_project_identity FROM project_recipients
|
|
4057
|
+
UNION SELECT canonical_project_identity FROM recipient_policy_authority_states`).all();
|
|
4058
|
+
const authorityRows = store.db.prepare(`SELECT authority_state, canonical_project_identity, last_attempt_at, safe_error_code
|
|
4059
|
+
FROM recipient_policy_authority_states`).all();
|
|
4060
|
+
const authorityByProject = /* @__PURE__ */ new Map();
|
|
4061
|
+
for (const row of authorityRows) {
|
|
4062
|
+
const projectId = canonicalize(row.canonical_project_identity);
|
|
4063
|
+
const current = authorityByProject.get(projectId);
|
|
4064
|
+
if (!current) {
|
|
4065
|
+
authorityByProject.set(projectId, row);
|
|
4066
|
+
continue;
|
|
4067
|
+
}
|
|
4068
|
+
if (current.canonical_project_identity === projectId) continue;
|
|
4069
|
+
if (row.canonical_project_identity === projectId) {
|
|
4070
|
+
authorityByProject.set(projectId, row);
|
|
4071
|
+
continue;
|
|
4072
|
+
}
|
|
4073
|
+
if (String(row.last_attempt_at ?? "") > String(current.last_attempt_at ?? "")) authorityByProject.set(projectId, row);
|
|
4074
|
+
}
|
|
4075
|
+
return [...new Set(projectIds.map((row) => canonicalize(row.canonical_project_identity)))].filter((projectId) => {
|
|
4076
|
+
const authority = authorityByProject.get(projectId);
|
|
4077
|
+
return authority == null || authority.last_attempt_at == null || authority.safe_error_code == null && authority.authority_state !== "active" || authority.last_attempt_at <= retryBefore;
|
|
4078
|
+
}).toSorted((left, right) => {
|
|
4079
|
+
const leftAttempt = authorityByProject.get(left)?.last_attempt_at;
|
|
4080
|
+
const rightAttempt = authorityByProject.get(right)?.last_attempt_at;
|
|
4081
|
+
if (leftAttempt == null && rightAttempt != null) return -1;
|
|
4082
|
+
if (leftAttempt != null && rightAttempt == null) return 1;
|
|
4083
|
+
return String(leftAttempt ?? "").localeCompare(String(rightAttempt ?? "")) || left.localeCompare(right);
|
|
4084
|
+
}).slice(0, limit);
|
|
4085
|
+
}
|
|
4086
|
+
function recordRecipientPolicyMaintenanceFailure(store, projectId, attemptedAt) {
|
|
4087
|
+
store.db.prepare(`INSERT INTO recipient_policy_authority_states(
|
|
4088
|
+
canonical_project_identity, state_changed_at, created_at, updated_at,
|
|
4089
|
+
attempt_count, last_attempt_at, safe_error_code, last_error_at
|
|
4090
|
+
) VALUES (?, ?, ?, ?, 1, ?, 'recipient_policy_reconciliation_failed', ?)
|
|
4091
|
+
ON CONFLICT(canonical_project_identity) DO UPDATE SET
|
|
4092
|
+
attempt_count = attempt_count + 1, last_attempt_at = excluded.last_attempt_at,
|
|
4093
|
+
safe_error_code = excluded.safe_error_code, last_error_at = excluded.last_error_at,
|
|
4094
|
+
updated_at = excluded.updated_at`).run(projectId, attemptedAt, attemptedAt, attemptedAt, attemptedAt, attemptedAt);
|
|
4095
|
+
}
|
|
4096
|
+
function isSqliteBusy$1(error) {
|
|
4097
|
+
if (!error || typeof error !== "object" || !("code" in error)) return false;
|
|
4098
|
+
const code = error.code;
|
|
4099
|
+
return typeof code === "string" && (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_LOCKED"));
|
|
4100
|
+
}
|
|
4101
|
+
function recordMaintenanceFailureIfAvailable(store, projectId, attemptedAt, error) {
|
|
4102
|
+
if (isSqliteBusy$1(error)) return;
|
|
4103
|
+
try {
|
|
4104
|
+
recordRecipientPolicyMaintenanceFailure(store, projectId, attemptedAt);
|
|
4105
|
+
} catch (bookkeepingError) {
|
|
4106
|
+
if (!isSqliteBusy$1(bookkeepingError)) throw bookkeepingError;
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4051
4109
|
async function reconcileRecipientPolicyProjectsOperation(store, options) {
|
|
4052
4110
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT), RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT));
|
|
4053
4111
|
const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
|
|
4054
4112
|
const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS));
|
|
4055
4113
|
const retryBefore = new Date(maintenanceNow.getTime() - backoffMs).toISOString();
|
|
4056
|
-
const
|
|
4057
|
-
|
|
4058
|
-
UNION
|
|
4059
|
-
SELECT canonical_project_identity FROM recipient_policy_authority_states
|
|
4060
|
-
)
|
|
4061
|
-
SELECT projects.canonical_project_identity
|
|
4062
|
-
FROM projects
|
|
4063
|
-
LEFT JOIN recipient_policy_authority_states authority
|
|
4064
|
-
ON authority.canonical_project_identity = projects.canonical_project_identity
|
|
4065
|
-
WHERE authority.safe_error_code IS NULL OR authority.last_attempt_at IS NULL
|
|
4066
|
-
OR authority.last_attempt_at <= ?
|
|
4067
|
-
ORDER BY CASE WHEN authority.last_attempt_at IS NULL THEN 0 ELSE 1 END,
|
|
4068
|
-
authority.last_attempt_at, projects.canonical_project_identity
|
|
4069
|
-
LIMIT ?`).all(retryBefore, limit);
|
|
4114
|
+
const attemptedAt = maintenanceNow.toISOString();
|
|
4115
|
+
const projectIds = recipientPolicyMaintenanceCandidates(store, retryBefore, limit);
|
|
4070
4116
|
const reconcileProject = options.reconcileProject ?? reconcileRecipientPolicyProject;
|
|
4071
4117
|
const result = {
|
|
4072
4118
|
processed: 0,
|
|
@@ -4076,25 +4122,26 @@ async function reconcileRecipientPolicyProjectsOperation(store, options) {
|
|
|
4076
4122
|
failed: 0,
|
|
4077
4123
|
items: []
|
|
4078
4124
|
};
|
|
4079
|
-
for (const
|
|
4125
|
+
for (const projectId of projectIds) {
|
|
4080
4126
|
result.processed += 1;
|
|
4081
4127
|
try {
|
|
4082
4128
|
const outcome = await reconcileProject(store.db, {
|
|
4083
|
-
canonicalProjectIdentity:
|
|
4129
|
+
canonicalProjectIdentity: projectId,
|
|
4084
4130
|
leaseOwner: options.leaseOwner ?? `recipient-policy-maintenance:${store.deviceId || process.pid}`
|
|
4085
4131
|
}, options.effects);
|
|
4086
4132
|
if (outcome.status === "active") result.active += 1;
|
|
4087
4133
|
else if (outcome.status === "needs_attention") result.attention += 1;
|
|
4088
4134
|
else result.waiting += 1;
|
|
4089
4135
|
result.items.push({
|
|
4090
|
-
canonicalProjectIdentity:
|
|
4136
|
+
canonicalProjectIdentity: projectId,
|
|
4091
4137
|
status: outcome.status,
|
|
4092
4138
|
safeErrorCode: outcome.safeErrorCode
|
|
4093
4139
|
});
|
|
4094
|
-
} catch {
|
|
4140
|
+
} catch (error) {
|
|
4141
|
+
recordMaintenanceFailureIfAvailable(store, projectId, attemptedAt, error);
|
|
4095
4142
|
result.failed += 1;
|
|
4096
4143
|
result.items.push({
|
|
4097
|
-
canonicalProjectIdentity:
|
|
4144
|
+
canonicalProjectIdentity: projectId,
|
|
4098
4145
|
status: "failed",
|
|
4099
4146
|
safeErrorCode: "recipient_policy_reconciliation_failed"
|
|
4100
4147
|
});
|
|
@@ -4104,6 +4151,14 @@ async function reconcileRecipientPolicyProjectsOperation(store, options) {
|
|
|
4104
4151
|
}
|
|
4105
4152
|
//#endregion
|
|
4106
4153
|
//#region src/application/sync-status.ts
|
|
4154
|
+
var SAFE_RUNTIME_DETAILS = {
|
|
4155
|
+
starting: "Running initial sync in background",
|
|
4156
|
+
stopping: "Stopping sync daemon",
|
|
4157
|
+
error: "Background sync failed. Open diagnostics for details.",
|
|
4158
|
+
rebootstrapping: "Restoring sync baseline",
|
|
4159
|
+
needs_attention: "Sync needs attention",
|
|
4160
|
+
disabled: "Sync is off"
|
|
4161
|
+
};
|
|
4107
4162
|
function traceSync(label, operation) {
|
|
4108
4163
|
if (process.env.CODEMEM_TRACE_SYNC_STATUS !== "1") return operation();
|
|
4109
4164
|
const startedAt = Date.now();
|
|
@@ -4168,6 +4223,10 @@ function initialDaemonState(config, rows, daemonRunning) {
|
|
|
4168
4223
|
if (!daemonRunning) return "stopped";
|
|
4169
4224
|
return "ok";
|
|
4170
4225
|
}
|
|
4226
|
+
function safeDaemonIssueCode(state, lastError) {
|
|
4227
|
+
if (state !== "error" || !lastError?.includes("coordinator enrollment maintenance failed")) return null;
|
|
4228
|
+
return lastError.includes(":request_timeout") || lastError.endsWith(" (coordinator_timeout)") ? "coordinator_timeout" : "coordinator_error";
|
|
4229
|
+
}
|
|
4171
4230
|
function retentionPayload(config, rows, retainedFloor) {
|
|
4172
4231
|
const state = rows.retention;
|
|
4173
4232
|
return {
|
|
@@ -4235,8 +4294,9 @@ function readBaseStatus(input) {
|
|
|
4235
4294
|
state = runtime.phase;
|
|
4236
4295
|
statusPayload.daemon_state = state;
|
|
4237
4296
|
statusPayload.daemon_running = runtime.phase === "starting" || running;
|
|
4238
|
-
statusPayload.daemon_detail = runtime.detail ?? detail;
|
|
4297
|
+
statusPayload.daemon_detail = input.showDiagnostics ? runtime.detail ?? detail : SAFE_RUNTIME_DETAILS[runtime.phase] ?? null;
|
|
4239
4298
|
}
|
|
4299
|
+
statusPayload.daemon_issue_code = safeDaemonIssueCode(state, runtime?.phase === "error" ? null : rows.daemon?.last_error);
|
|
4240
4300
|
return {
|
|
4241
4301
|
config,
|
|
4242
4302
|
localDeviceId: rows.device?.device_id ?? null,
|
|
@@ -4293,16 +4353,35 @@ function readAttemptStatus(input) {
|
|
|
4293
4353
|
latestError: String(rows[0]?.error || "").trim()
|
|
4294
4354
|
};
|
|
4295
4355
|
}
|
|
4356
|
+
function latestFailedPeerIds(store, activePeerIds, isRecentIso) {
|
|
4357
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - 6e5)).toISOString();
|
|
4358
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4359
|
+
return traceSync("recentPeerFailures", () => {
|
|
4360
|
+
const rows = store.db.prepare(`SELECT peer_device_id, ok, error, finished_at
|
|
4361
|
+
FROM sync_attempts
|
|
4362
|
+
WHERE (CASE WHEN finished_at IS NULL THEN started_at ELSE finished_at END) BETWEEN ? AND ?
|
|
4363
|
+
ORDER BY (CASE WHEN finished_at IS NULL THEN started_at ELSE finished_at END) DESC, id DESC`).iterate(cutoff, now);
|
|
4364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4365
|
+
const failed = /* @__PURE__ */ new Set();
|
|
4366
|
+
for (const row of rows) {
|
|
4367
|
+
if (!activePeerIds.has(row.peer_device_id) || seen.has(row.peer_device_id)) continue;
|
|
4368
|
+
if (!isRecentIso(row.finished_at)) continue;
|
|
4369
|
+
seen.add(row.peer_device_id);
|
|
4370
|
+
if (!row.ok && row.error) failed.add(row.peer_device_id);
|
|
4371
|
+
}
|
|
4372
|
+
return failed;
|
|
4373
|
+
});
|
|
4374
|
+
}
|
|
4296
4375
|
function peerStates(peers) {
|
|
4297
4376
|
return new Set(peers.items.map((peer) => String(peer.status?.peer_state ?? "")));
|
|
4298
4377
|
}
|
|
4299
4378
|
function allPeersOffline(peers) {
|
|
4300
4379
|
return peers.items.length > 0 && peers.items.every((peer) => String(peer.status?.peer_state ?? "") === "offline");
|
|
4301
4380
|
}
|
|
4302
|
-
function degradedDaemonState(
|
|
4381
|
+
function degradedDaemonState(peers, failedPeerIds) {
|
|
4303
4382
|
const states = peerStates(peers);
|
|
4304
4383
|
const allOffline = allPeersOffline(peers);
|
|
4305
|
-
if (
|
|
4384
|
+
if (failedPeerIds.size > 0) {
|
|
4306
4385
|
if (states.has("online") || states.has("degraded")) return "degraded";
|
|
4307
4386
|
if (allOffline) return "offline-peers";
|
|
4308
4387
|
if (peers.items.length > 0) return "stale";
|
|
@@ -4312,14 +4391,14 @@ function degradedDaemonState(input, peers, attempts) {
|
|
|
4312
4391
|
if (peers.items.length > 0 && !states.has("online")) return "stale";
|
|
4313
4392
|
return "ok";
|
|
4314
4393
|
}
|
|
4315
|
-
function applyDerivedDaemonState(
|
|
4394
|
+
function applyDerivedDaemonState(base, peers, attempts, failedPeerIds, statusBlock) {
|
|
4316
4395
|
let state = base.daemonState;
|
|
4317
4396
|
if (state === "ok" && attempts.latestError.startsWith("needs_attention:")) {
|
|
4318
4397
|
state = "needs_attention";
|
|
4319
4398
|
const detail = attempts.latestError.replace(/^needs_attention:/, "");
|
|
4320
4399
|
base.statusPayload.daemon_detail = detail;
|
|
4321
4400
|
statusBlock.daemon_detail = detail;
|
|
4322
|
-
} else if (state === "ok") state = degradedDaemonState(
|
|
4401
|
+
} else if (state === "ok") state = degradedDaemonState(peers, failedPeerIds);
|
|
4323
4402
|
base.statusPayload.daemon_state = state;
|
|
4324
4403
|
statusBlock.daemon_state = state;
|
|
4325
4404
|
}
|
|
@@ -4337,6 +4416,12 @@ async function buildSyncStatusResponse(input) {
|
|
|
4337
4416
|
const coordinator = traceSync("coordinator", () => input.operations.redactCoordinatorStatus(coordinatorSnapshot, input.showDiagnostics));
|
|
4338
4417
|
const peers = readPeerStatus(input, base.localDeviceId);
|
|
4339
4418
|
const attempts = readAttemptStatus(input);
|
|
4419
|
+
const activePeerIds = new Set(peers.items.map((peer) => String(peer.peer_device_id ?? "")));
|
|
4420
|
+
const failedPeerIds = latestFailedPeerIds(input.store, activePeerIds, input.operations.isRecentIso);
|
|
4421
|
+
for (const peer of peers.items) {
|
|
4422
|
+
const status = peer.status;
|
|
4423
|
+
status.recent_failed_attempt = failedPeerIds.has(String(peer.peer_device_id ?? ""));
|
|
4424
|
+
}
|
|
4340
4425
|
const statusBlock = {
|
|
4341
4426
|
...base.statusPayload,
|
|
4342
4427
|
background_maintenance: input.operations.summarizeMaintenanceJobs(listMaintenanceJobs(input.store.db), input.showDiagnostics),
|
|
@@ -4350,7 +4435,7 @@ async function buildSyncStatusResponse(input) {
|
|
|
4350
4435
|
const sharingReview = traceSync("sharingReview", () => input.store.sharingReviewSummary(input.project));
|
|
4351
4436
|
const recipientPolicyReconciliation = traceSync("recipientPolicyReconciliation", () => input.operations.listRecipientPolicyReconciliationStatus(input.store));
|
|
4352
4437
|
const joinRequests = await readJoinRequests(input, base.config);
|
|
4353
|
-
applyDerivedDaemonState(
|
|
4438
|
+
applyDerivedDaemonState(base, peers, attempts, failedPeerIds, statusBlock);
|
|
4354
4439
|
const response = {
|
|
4355
4440
|
...base.statusPayload,
|
|
4356
4441
|
status: statusBlock,
|
|
@@ -6594,6 +6679,14 @@ function missingGuardrailConfirmations(warnings, confirmedTokens) {
|
|
|
6594
6679
|
const confirmed = new Set(confirmedTokens);
|
|
6595
6680
|
return warnings.filter((warning) => warning.requires_confirmation && (!warning.confirmation_token || !confirmed.has(warning.confirmation_token)));
|
|
6596
6681
|
}
|
|
6682
|
+
function mappingGuardrailChallenge(warnings, missing) {
|
|
6683
|
+
return {
|
|
6684
|
+
error: "guardrail_confirmation_required",
|
|
6685
|
+
required_guardrails: [...new Set(missing.map((warning) => warning.code))],
|
|
6686
|
+
required_guardrail_tokens: [...new Set(missing.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0),
|
|
6687
|
+
guardrail_warnings: warnings
|
|
6688
|
+
};
|
|
6689
|
+
}
|
|
6597
6690
|
function parseViewerProjectMappingInput(body) {
|
|
6598
6691
|
const id = optionalViewerInteger(body, "id");
|
|
6599
6692
|
const priority = optionalViewerInteger(body, "priority");
|
|
@@ -6608,6 +6701,14 @@ function parseViewerProjectMappingInput(body) {
|
|
|
6608
6701
|
source: optionalViewerStrictString(body, "source") ?? "user"
|
|
6609
6702
|
};
|
|
6610
6703
|
}
|
|
6704
|
+
function parseViewerProjectMappingInputs(body) {
|
|
6705
|
+
const rawMappings = body.mappings;
|
|
6706
|
+
if (!Array.isArray(rawMappings) || rawMappings.length === 0) throw new Error("mappings must be a non-empty array");
|
|
6707
|
+
return rawMappings.map((raw) => {
|
|
6708
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
|
|
6709
|
+
return parseViewerProjectMappingInput(raw);
|
|
6710
|
+
});
|
|
6711
|
+
}
|
|
6611
6712
|
function coordinatorAdminMutationStatus(message) {
|
|
6612
6713
|
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;
|
|
6613
6714
|
if (message.includes("not active") || message.includes("scope_not_active") || message.includes("scope_membership_effect_conflict") || message.includes("group_archived") || message.includes("Group is archived")) return 409;
|
|
@@ -7366,6 +7467,21 @@ function recipientPolicyPresenceCapabilityExpiresAt(enrollment, observedAt) {
|
|
|
7366
7467
|
function recipientPolicyEnrollmentValueIsValid(value) {
|
|
7367
7468
|
return typeof value === "string" && value.length > 0 && value === value.trim();
|
|
7368
7469
|
}
|
|
7470
|
+
function recipientPolicyPresenceCapabilityExpiries(enrollments, observedAt) {
|
|
7471
|
+
return new Map(enrollments.map((enrollment) => [enrollment.device_id, recipientPolicyPresenceCapabilityExpiresAt(enrollment, observedAt)]));
|
|
7472
|
+
}
|
|
7473
|
+
function recipientPolicySnapshotUnavailable(stage, error) {
|
|
7474
|
+
console.warn(`[sync] recipient policy coordinator snapshot failed: stage=${stage} code=${coordinatorEnrollmentFailureCode(error)}`);
|
|
7475
|
+
throw new Error("recipient_policy_snapshot_not_fresh");
|
|
7476
|
+
}
|
|
7477
|
+
async function readRecipientPolicyCoordinatorSnapshot(read) {
|
|
7478
|
+
for (let attempt = 0;; attempt++) try {
|
|
7479
|
+
return await read();
|
|
7480
|
+
} catch (error) {
|
|
7481
|
+
if (!(error instanceof RemoteCoordinatorRequestError) || error.status !== 429 || attempt >= 2) throw error;
|
|
7482
|
+
await new Promise((resolve) => setTimeout(resolve, (attempt + 1) * 1e3));
|
|
7483
|
+
}
|
|
7484
|
+
}
|
|
7369
7485
|
function createRecipientPolicyReconcilerEffects(store, options = {}) {
|
|
7370
7486
|
const config = options.config ?? readCoordinatorSyncConfig();
|
|
7371
7487
|
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -7407,15 +7523,13 @@ function createRecipientPolicyReconcilerEffects(store, options = {}) {
|
|
|
7407
7523
|
now,
|
|
7408
7524
|
snapshot: async ({ scopeId }) => {
|
|
7409
7525
|
const targetOptions = coordinatorOptions(scopeId);
|
|
7410
|
-
const snapshotMemberships = (await coordinatorListScopeMembershipsAction({
|
|
7526
|
+
const snapshotMemberships = (await readRecipientPolicyCoordinatorSnapshot(() => coordinatorListScopeMembershipsAction({
|
|
7411
7527
|
groupId: targetOptions.groupId,
|
|
7412
7528
|
scopeId,
|
|
7413
7529
|
includeRevoked: true,
|
|
7414
7530
|
remoteUrl: targetOptions.remoteUrl,
|
|
7415
7531
|
adminSecret: targetOptions.adminSecret
|
|
7416
|
-
}).catch(() => {
|
|
7417
|
-
throw new Error("recipient_policy_snapshot_not_fresh");
|
|
7418
|
-
})).map((membership) => {
|
|
7532
|
+
})).catch((error) => recipientPolicySnapshotUnavailable("scope_memberships", error))).map((membership) => {
|
|
7419
7533
|
if (typeof membership.device_id !== "string" || membership.status !== "active" && membership.status !== "revoked") throw new Error("recipient_policy_snapshot_invalid");
|
|
7420
7534
|
const status = membership.status;
|
|
7421
7535
|
return {
|
|
@@ -7436,15 +7550,13 @@ function createRecipientPolicyReconcilerEffects(store, options = {}) {
|
|
|
7436
7550
|
listBoundaryEnrollments: async ({ scopeId }) => {
|
|
7437
7551
|
const targetOptions = coordinatorOptions(scopeId);
|
|
7438
7552
|
const observedAt = now();
|
|
7439
|
-
const enrollments = await listDevices({
|
|
7553
|
+
const enrollments = await readRecipientPolicyCoordinatorSnapshot(() => listDevices({
|
|
7440
7554
|
groupId: targetOptions.groupId,
|
|
7441
7555
|
includeDisabled: true,
|
|
7442
7556
|
remoteUrl: targetOptions.remoteUrl,
|
|
7443
7557
|
adminSecret: targetOptions.adminSecret
|
|
7444
|
-
}).catch(() =>
|
|
7445
|
-
|
|
7446
|
-
});
|
|
7447
|
-
const presenceCapabilityExpiries = new Map(enrollments.map((enrollment) => [enrollment.device_id, recipientPolicyPresenceCapabilityExpiresAt(enrollment, observedAt)]));
|
|
7558
|
+
})).catch((error) => recipientPolicySnapshotUnavailable("device_enrollments", error));
|
|
7559
|
+
const presenceCapabilityExpiries = recipientPolicyPresenceCapabilityExpiries(enrollments, observedAt);
|
|
7448
7560
|
const mapped = enrollments.map((enrollment) => {
|
|
7449
7561
|
if (enrollment.group_id !== targetOptions.groupId || !recipientPolicyEnrollmentValueIsValid(enrollment.device_id) || !recipientPolicyEnrollmentValueIsValid(enrollment.public_key) || !recipientPolicyEnrollmentValueIsValid(enrollment.fingerprint) || fingerprintPublicKey(enrollment.public_key) !== enrollment.fingerprint || enrollment.enabled !== 0 && enrollment.enabled !== 1 || enrollment.identity_id !== null && !recipientPolicyEnrollmentValueIsValid(enrollment.identity_id)) throw new Error("recipient_policy_snapshot_invalid");
|
|
7450
7562
|
return {
|
|
@@ -7565,13 +7677,25 @@ function recipientPolicyReadCopy(state) {
|
|
|
7565
7677
|
explanation: "Legacy scope enforcement remains in control while reconciliation is pending."
|
|
7566
7678
|
};
|
|
7567
7679
|
}
|
|
7680
|
+
function recipientPolicyAuthorityForAliases(store, canonicalProjectIdentity, aliases) {
|
|
7681
|
+
const canonical = getRecipientPolicyAuthorityState(store.db, canonicalProjectIdentity);
|
|
7682
|
+
if (canonical) return canonical;
|
|
7683
|
+
return aliases.filter((identity) => identity !== canonicalProjectIdentity).map((identity) => getRecipientPolicyAuthorityState(store.db, identity)).filter((state) => state != null).toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? null;
|
|
7684
|
+
}
|
|
7568
7685
|
function listRecipientPolicyReconciliationStatus(store) {
|
|
7686
|
+
const projectIds = store.db.prepare(`SELECT DISTINCT canonical_project_identity FROM project_recipients WHERE status = 'active'
|
|
7687
|
+
UNION SELECT canonical_project_identity FROM recipient_policy_authority_states
|
|
7688
|
+
ORDER BY canonical_project_identity`).all().map((row) => row.canonical_project_identity);
|
|
7689
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
7690
|
+
const aliasesByProject = /* @__PURE__ */ new Map();
|
|
7691
|
+
for (const projectId of projectIds) {
|
|
7692
|
+
const canonical = canonicalRepositoryProjectIdentity(repositoryIdentities, projectId);
|
|
7693
|
+
aliasesByProject.set(canonical, [...aliasesByProject.get(canonical) ?? [], projectId]);
|
|
7694
|
+
}
|
|
7569
7695
|
return {
|
|
7570
7696
|
version: 1,
|
|
7571
|
-
items:
|
|
7572
|
-
|
|
7573
|
-
ORDER BY canonical_project_identity`).all().map((row) => row.canonical_project_identity).map((canonicalProjectIdentity) => {
|
|
7574
|
-
const state = recipientPolicyReadState(getRecipientPolicyAuthorityState(store.db, canonicalProjectIdentity));
|
|
7697
|
+
items: [...aliasesByProject.keys()].toSorted().map((canonicalProjectIdentity) => {
|
|
7698
|
+
const state = recipientPolicyReadState(recipientPolicyAuthorityForAliases(store, canonicalProjectIdentity, aliasesByProject.get(canonicalProjectIdentity) ?? []));
|
|
7575
7699
|
return {
|
|
7576
7700
|
canonicalProjectIdentity,
|
|
7577
7701
|
state,
|
|
@@ -8565,6 +8689,7 @@ function addLegacySharedReviewRow(groups, row, identity, candidate, isOwnedBySel
|
|
|
8565
8689
|
}
|
|
8566
8690
|
function collectLegacySharedReviewGroups(store) {
|
|
8567
8691
|
const ownedBySelf = store.buildOwnershipPredicate();
|
|
8692
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
8568
8693
|
const candidatesByIdentity = new Map(listProjectScopeCandidates(store.db, { limit: null }).map((candidate) => [candidate.workspace_identity, candidate]));
|
|
8569
8694
|
const groups = /* @__PURE__ */ new Map();
|
|
8570
8695
|
let memoryCount = 0;
|
|
@@ -8576,6 +8701,11 @@ function collectLegacySharedReviewGroups(store) {
|
|
|
8576
8701
|
cwd: row.cwd,
|
|
8577
8702
|
gitBranch: row.git_branch,
|
|
8578
8703
|
gitRemote: row.git_remote,
|
|
8704
|
+
repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
|
|
8705
|
+
cwd: row.cwd,
|
|
8706
|
+
gitRemote: row.git_remote,
|
|
8707
|
+
metadataJson: row.session_metadata_json
|
|
8708
|
+
}),
|
|
8579
8709
|
project: row.project,
|
|
8580
8710
|
workspaceId: row.workspace_id
|
|
8581
8711
|
});
|
|
@@ -8650,7 +8780,8 @@ function legacySharedReviewSummaryRows(store) {
|
|
|
8650
8780
|
m.workspace_id,
|
|
8651
8781
|
m.actor_id,
|
|
8652
8782
|
m.origin_device_id,
|
|
8653
|
-
m.metadata_json
|
|
8783
|
+
m.metadata_json,
|
|
8784
|
+
s.metadata_json AS session_metadata_json
|
|
8654
8785
|
FROM memory_items m
|
|
8655
8786
|
LEFT JOIN sessions s ON s.id = m.session_id
|
|
8656
8787
|
WHERE m.scope_id = ?
|
|
@@ -8690,7 +8821,8 @@ function legacySharedReviewRows(store) {
|
|
|
8690
8821
|
m.workspace_id,
|
|
8691
8822
|
m.actor_id,
|
|
8692
8823
|
m.origin_device_id,
|
|
8693
|
-
m.metadata_json
|
|
8824
|
+
m.metadata_json,
|
|
8825
|
+
s.metadata_json AS session_metadata_json
|
|
8694
8826
|
FROM memory_items m
|
|
8695
8827
|
LEFT JOIN sessions s ON s.id = m.session_id
|
|
8696
8828
|
WHERE m.scope_id = ?
|
|
@@ -8707,7 +8839,7 @@ function legacySharedReviewConfirmationToken(input) {
|
|
|
8707
8839
|
return Buffer.from(`${input.workspaceIdentity}|${input.scopeId}|${payload}`, "utf8").toString("base64url");
|
|
8708
8840
|
}
|
|
8709
8841
|
function projectInventoryRowsForWorkspace(store, workspaceIdentity) {
|
|
8710
|
-
|
|
8842
|
+
const rows = store.db.prepare(`SELECT m.id,
|
|
8711
8843
|
m.rev,
|
|
8712
8844
|
s.project,
|
|
8713
8845
|
s.cwd,
|
|
@@ -8716,15 +8848,23 @@ function projectInventoryRowsForWorkspace(store, workspaceIdentity) {
|
|
|
8716
8848
|
m.workspace_id,
|
|
8717
8849
|
m.actor_id,
|
|
8718
8850
|
m.origin_device_id,
|
|
8719
|
-
m.metadata_json
|
|
8851
|
+
m.metadata_json,
|
|
8852
|
+
s.metadata_json AS session_metadata_json
|
|
8720
8853
|
FROM memory_items m
|
|
8721
8854
|
LEFT JOIN sessions s ON s.id = m.session_id
|
|
8722
8855
|
WHERE m.active = 1
|
|
8723
|
-
AND m.deleted_at IS NULL`).all()
|
|
8856
|
+
AND m.deleted_at IS NULL`).all();
|
|
8857
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
8858
|
+
return rows.filter((row) => {
|
|
8724
8859
|
return canonicalWorkspaceIdentity({
|
|
8725
8860
|
cwd: row.cwd,
|
|
8726
8861
|
gitBranch: row.git_branch,
|
|
8727
8862
|
gitRemote: row.git_remote,
|
|
8863
|
+
repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
|
|
8864
|
+
cwd: row.cwd,
|
|
8865
|
+
gitRemote: row.git_remote,
|
|
8866
|
+
metadataJson: row.session_metadata_json
|
|
8867
|
+
}),
|
|
8728
8868
|
project: row.project,
|
|
8729
8869
|
workspaceId: row.workspace_id
|
|
8730
8870
|
}).value === workspaceIdentity;
|
|
@@ -8778,11 +8918,17 @@ function assertLocalDeviceScopeMembership(store, scopeId) {
|
|
|
8778
8918
|
LIMIT 1`).get(scopeId, store.deviceId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
|
|
8779
8919
|
}
|
|
8780
8920
|
function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
|
|
8921
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
8781
8922
|
return legacySharedReviewRows(store).filter((row) => {
|
|
8782
8923
|
return canonicalWorkspaceIdentity({
|
|
8783
8924
|
cwd: row.cwd,
|
|
8784
8925
|
gitBranch: row.git_branch,
|
|
8785
8926
|
gitRemote: row.git_remote,
|
|
8927
|
+
repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
|
|
8928
|
+
cwd: row.cwd,
|
|
8929
|
+
gitRemote: row.git_remote,
|
|
8930
|
+
metadataJson: row.session_metadata_json
|
|
8931
|
+
}),
|
|
8786
8932
|
project: row.project,
|
|
8787
8933
|
workspaceId: row.workspace_id
|
|
8788
8934
|
}).value === workspaceIdentity;
|
|
@@ -9329,6 +9475,119 @@ function serializeProjectScopeInventory(store, inventory, sharingByProject) {
|
|
|
9329
9475
|
}))
|
|
9330
9476
|
};
|
|
9331
9477
|
}
|
|
9478
|
+
async function deleteProjectScopeMappingRoute(c, store) {
|
|
9479
|
+
const id = Number(c.req.param("id"));
|
|
9480
|
+
let releasePublicationMutation;
|
|
9481
|
+
try {
|
|
9482
|
+
const body = c.req.header("content-type")?.includes("application/json") ? await parseViewerJsonBody(c) : {};
|
|
9483
|
+
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
9484
|
+
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
9485
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
9486
|
+
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
9487
|
+
const warnings = analyzeProjectScopeMappingDeletionGuardrails(store.db, id, deviceId);
|
|
9488
|
+
const missing = missingGuardrailConfirmations(warnings, confirmedGuardrailTokens);
|
|
9489
|
+
if (missing.length > 0) return c.json({
|
|
9490
|
+
error: "guardrail_confirmation_required",
|
|
9491
|
+
required_guardrails: [...new Set(missing.map((warning) => warning.code))],
|
|
9492
|
+
required_guardrail_tokens: missing.map((warning) => warning.confirmation_token),
|
|
9493
|
+
guardrail_warnings: warnings
|
|
9494
|
+
}, 409);
|
|
9495
|
+
const deleted = deleteProjectMappingAndWakePolicies(store, id, deviceId, confirmedGuardrailTokens);
|
|
9496
|
+
return c.json({
|
|
9497
|
+
ok: true,
|
|
9498
|
+
deleted
|
|
9499
|
+
});
|
|
9500
|
+
} catch (error) {
|
|
9501
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
9502
|
+
} finally {
|
|
9503
|
+
releasePublicationMutation?.();
|
|
9504
|
+
}
|
|
9505
|
+
}
|
|
9506
|
+
function projectSharingByRepository(store, operations) {
|
|
9507
|
+
const operationById = new Map(operations.map((operation) => [operation.operation_id, operation]));
|
|
9508
|
+
const sharingByProject = /* @__PURE__ */ new Map();
|
|
9509
|
+
const mappings = store.db.prepare("SELECT workspace_identity, project_pattern FROM project_scope_mappings").all();
|
|
9510
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db, { knownRepositoryIdentities: mappings.flatMap((mapping) => [mapping.workspace_identity, mapping.project_pattern]) });
|
|
9511
|
+
const seenOperationProjects = /* @__PURE__ */ new Set();
|
|
9512
|
+
const reviewed = store.db.prepare(`SELECT p.operation_id, p.canonical_project_identity
|
|
9513
|
+
FROM share_operation_projects p
|
|
9514
|
+
JOIN share_operations o ON o.operation_id = p.operation_id
|
|
9515
|
+
WHERE o.inviter_actor_id = ? ORDER BY o.created_at, p.ordinal`).all(store.actorId);
|
|
9516
|
+
for (const item of reviewed) {
|
|
9517
|
+
const operation = operationById.get(item.operation_id);
|
|
9518
|
+
if (!operation) continue;
|
|
9519
|
+
const projectId = canonicalRepositoryProjectIdentity(repositoryIdentities, item.canonical_project_identity);
|
|
9520
|
+
const operationProjectKey = `${item.operation_id}\u0000${projectId}`;
|
|
9521
|
+
if (seenOperationProjects.has(operationProjectKey)) continue;
|
|
9522
|
+
seenOperationProjects.add(operationProjectKey);
|
|
9523
|
+
const current = sharingByProject.get(projectId) ?? [];
|
|
9524
|
+
current.push({
|
|
9525
|
+
person: operation.person,
|
|
9526
|
+
lifecycle: {
|
|
9527
|
+
state: operation.lifecycle.state,
|
|
9528
|
+
label: operation.lifecycle.label,
|
|
9529
|
+
explanation: operation.lifecycle.explanation
|
|
9530
|
+
}
|
|
9531
|
+
});
|
|
9532
|
+
sharingByProject.set(projectId, current);
|
|
9533
|
+
}
|
|
9534
|
+
return sharingByProject;
|
|
9535
|
+
}
|
|
9536
|
+
function deactivateActorAndWakePolicies(store, actorId, now) {
|
|
9537
|
+
store.db.transaction(() => {
|
|
9538
|
+
store.db.prepare("UPDATE actors SET status = 'deactivated', updated_at = ? WHERE actor_id = ?").run(now, actorId);
|
|
9539
|
+
store.db.prepare("UPDATE sync_peers SET actor_id = NULL WHERE actor_id = ?").run(actorId);
|
|
9540
|
+
wakeRecipientPoliciesForIdentities(store.db, [actorId], now);
|
|
9541
|
+
}).immediate();
|
|
9542
|
+
}
|
|
9543
|
+
function finishActorMerge(store, primaryActorId, secondaryActorId, now) {
|
|
9544
|
+
store.db.prepare("UPDATE actors SET status = 'merged', merged_into_actor_id = ?, updated_at = ? WHERE actor_id = ?").run(primaryActorId, now, secondaryActorId);
|
|
9545
|
+
wakeRecipientPoliciesForIdentities(store.db, [primaryActorId, secondaryActorId], now);
|
|
9546
|
+
}
|
|
9547
|
+
function policyWakeMappings(store) {
|
|
9548
|
+
return store.db.prepare("SELECT id, scope_id, workspace_identity FROM project_scope_mappings ORDER BY id").all();
|
|
9549
|
+
}
|
|
9550
|
+
function existingRequestedMappings(mappings, inputs) {
|
|
9551
|
+
const requestedIds = new Set(inputs.map((input) => input.id).filter((id) => id != null));
|
|
9552
|
+
const requestedIdentities = new Set(inputs.map((input) => input.workspace_identity).filter((identity) => identity != null).map((identity) => canonicalWorkspaceIdentity({ cwd: identity }).value));
|
|
9553
|
+
return mappings.filter((mapping) => {
|
|
9554
|
+
if (requestedIds.has(mapping.id)) return true;
|
|
9555
|
+
if (!mapping.workspace_identity) return false;
|
|
9556
|
+
return requestedIdentities.has(canonicalWorkspaceIdentity({ cwd: mapping.workspace_identity }).value);
|
|
9557
|
+
});
|
|
9558
|
+
}
|
|
9559
|
+
function mappedProjectsInScopes(mappings, scopeIds) {
|
|
9560
|
+
return mappings.filter((mapping) => scopeIds.has(mapping.scope_id)).map((mapping) => mapping.workspace_identity).filter((identity) => identity != null);
|
|
9561
|
+
}
|
|
9562
|
+
function wakeRecipientPolicyProjects(store, projectIdentities, now) {
|
|
9563
|
+
wakeRecipientPoliciesForProjectIdentities(store.db, [...new Set(projectIdentities)], now);
|
|
9564
|
+
}
|
|
9565
|
+
function saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs) {
|
|
9566
|
+
return store.db.transaction(() => {
|
|
9567
|
+
const before = policyWakeMappings(store);
|
|
9568
|
+
const previousMappings = existingRequestedMappings(before, mappingInputs);
|
|
9569
|
+
const mappings = upsertProjectScopeSettingsMappings(store.db, mappingInputs, { deviceId });
|
|
9570
|
+
const after = policyWakeMappings(store);
|
|
9571
|
+
const changedScopeIds = /* @__PURE__ */ new Set([...previousMappings.map((mapping) => mapping.scope_id), ...mappings.map((mapping) => mapping.scope_id)]);
|
|
9572
|
+
wakeRecipientPolicyProjects(store, [...mappedProjectsInScopes(before, changedScopeIds), ...mappedProjectsInScopes(after, changedScopeIds)], (/* @__PURE__ */ new Date()).toISOString());
|
|
9573
|
+
return mappings;
|
|
9574
|
+
}).immediate();
|
|
9575
|
+
}
|
|
9576
|
+
function deleteProjectMappingAndWakePolicies(store, mappingId, deviceId, confirmedGuardrailTokens) {
|
|
9577
|
+
return store.db.transaction(() => {
|
|
9578
|
+
const before = policyWakeMappings(store);
|
|
9579
|
+
const removed = before.find((mapping) => mapping.id === mappingId);
|
|
9580
|
+
const deleted = deleteProjectScopeSettingsMapping(store.db, mappingId, {
|
|
9581
|
+
deviceId,
|
|
9582
|
+
confirmedGuardrailTokens
|
|
9583
|
+
});
|
|
9584
|
+
if (deleted && removed) {
|
|
9585
|
+
const changedScopeIds = /* @__PURE__ */ new Set([removed.scope_id]);
|
|
9586
|
+
wakeRecipientPolicyProjects(store, [...mappedProjectsInScopes(before, changedScopeIds), ...mappedProjectsInScopes(policyWakeMappings(store), changedScopeIds)], (/* @__PURE__ */ new Date()).toISOString());
|
|
9587
|
+
}
|
|
9588
|
+
return deleted;
|
|
9589
|
+
}).immediate();
|
|
9590
|
+
}
|
|
9332
9591
|
/**
|
|
9333
9592
|
* Viewer-facing sync management routes (/api/sync/*).
|
|
9334
9593
|
*
|
|
@@ -9961,27 +10220,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9961
10220
|
scopeId: c.req.query("scope_id"),
|
|
9962
10221
|
status: c.req.query("status")
|
|
9963
10222
|
});
|
|
9964
|
-
const
|
|
9965
|
-
const operationById = new Map(operations.map((operation) => [operation.operation_id, operation]));
|
|
9966
|
-
const sharingByProject = /* @__PURE__ */ new Map();
|
|
9967
|
-
const reviewed = store.db.prepare(`SELECT p.operation_id, p.canonical_project_identity
|
|
9968
|
-
FROM share_operation_projects p
|
|
9969
|
-
JOIN share_operations o ON o.operation_id = p.operation_id
|
|
9970
|
-
WHERE o.inviter_actor_id = ? ORDER BY o.created_at, p.ordinal`).all(store.actorId);
|
|
9971
|
-
for (const item of reviewed) {
|
|
9972
|
-
const operation = operationById.get(item.operation_id);
|
|
9973
|
-
if (!operation) continue;
|
|
9974
|
-
const current = sharingByProject.get(item.canonical_project_identity) ?? [];
|
|
9975
|
-
current.push({
|
|
9976
|
-
person: operation.person,
|
|
9977
|
-
lifecycle: {
|
|
9978
|
-
state: operation.lifecycle.state,
|
|
9979
|
-
label: operation.lifecycle.label,
|
|
9980
|
-
explanation: operation.lifecycle.explanation
|
|
9981
|
-
}
|
|
9982
|
-
});
|
|
9983
|
-
sharingByProject.set(item.canonical_project_identity, current);
|
|
9984
|
-
}
|
|
10223
|
+
const sharingByProject = projectSharingByRepository(store, await shareOperationReadModels(store, void 0, false));
|
|
9985
10224
|
return c.json(serializeProjectScopeInventory(store, inventory, sharingByProject));
|
|
9986
10225
|
});
|
|
9987
10226
|
app.post("/api/sync/projects/reassign-project", async (c) => {
|
|
@@ -10238,28 +10477,19 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10238
10477
|
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
10239
10478
|
const mappingInput = parseViewerProjectMappingInput(body);
|
|
10240
10479
|
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
10241
|
-
const
|
|
10480
|
+
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10481
|
+
const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, {
|
|
10482
|
+
...mappingInput,
|
|
10483
|
+
deviceId
|
|
10484
|
+
});
|
|
10242
10485
|
if (analysis.requested_workspace_identity?.startsWith("unmapped:")) return c.json({
|
|
10243
10486
|
error: "unmapped_project_local_only",
|
|
10244
10487
|
message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a Sharing domain.",
|
|
10245
10488
|
guardrail_warnings: analysis.warnings
|
|
10246
10489
|
}, 400);
|
|
10247
10490
|
const missingConfirmations = missingGuardrailConfirmations(analysis.warnings, confirmedGuardrailTokens);
|
|
10248
|
-
if (missingConfirmations.length > 0)
|
|
10249
|
-
|
|
10250
|
-
const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
|
|
10251
|
-
return c.json({
|
|
10252
|
-
error: "guardrail_confirmation_required",
|
|
10253
|
-
required_guardrails: missingCodes,
|
|
10254
|
-
required_guardrail_tokens: missingTokens,
|
|
10255
|
-
guardrail_warnings: analysis.warnings
|
|
10256
|
-
}, 409);
|
|
10257
|
-
}
|
|
10258
|
-
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10259
|
-
const mapping = upsertProjectScopeSettingsMapping(store.db, {
|
|
10260
|
-
deviceId,
|
|
10261
|
-
...mappingInput
|
|
10262
|
-
});
|
|
10491
|
+
if (missingConfirmations.length > 0) return c.json(mappingGuardrailChallenge(analysis.warnings, missingConfirmations), 409);
|
|
10492
|
+
const [mapping] = saveProjectMappingsAndWakePolicies(store, deviceId, [mappingInput]);
|
|
10263
10493
|
return c.json({
|
|
10264
10494
|
ok: true,
|
|
10265
10495
|
mapping,
|
|
@@ -10277,14 +10507,13 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10277
10507
|
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
10278
10508
|
let releasePublicationMutation;
|
|
10279
10509
|
try {
|
|
10280
|
-
const
|
|
10281
|
-
if (!Array.isArray(rawMappings) || rawMappings.length === 0) return c.json({ error: "mappings must be a non-empty array" }, 400);
|
|
10282
|
-
const mappingInputs = rawMappings.map((raw) => {
|
|
10283
|
-
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
|
|
10284
|
-
return parseViewerProjectMappingInput(raw);
|
|
10285
|
-
});
|
|
10510
|
+
const mappingInputs = parseViewerProjectMappingInputs(body);
|
|
10286
10511
|
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
10287
|
-
const
|
|
10512
|
+
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10513
|
+
const analyses = analyzeProjectScopeMappingChangesGuardrails(store.db, mappingInputs.map((input) => ({
|
|
10514
|
+
...input,
|
|
10515
|
+
deviceId
|
|
10516
|
+
})));
|
|
10288
10517
|
const unmapped = analyses.find((analysis) => analysis.requested_workspace_identity?.startsWith("unmapped:"));
|
|
10289
10518
|
if (unmapped) return c.json({
|
|
10290
10519
|
error: "unmapped_project_local_only",
|
|
@@ -10292,24 +10521,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10292
10521
|
guardrail_warnings: unmapped.warnings
|
|
10293
10522
|
}, 400);
|
|
10294
10523
|
const missingConfirmations = analyses.flatMap((analysis) => missingGuardrailConfirmations(analysis.warnings, []));
|
|
10295
|
-
if (missingConfirmations.length > 0)
|
|
10296
|
-
const missingCodes = [...new Set(missingConfirmations.map((warning) => warning.code))];
|
|
10297
|
-
const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
|
|
10298
|
-
return c.json({
|
|
10299
|
-
error: "guardrail_confirmation_required",
|
|
10300
|
-
required_guardrails: missingCodes,
|
|
10301
|
-
required_guardrail_tokens: missingTokens,
|
|
10302
|
-
guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
|
|
10303
|
-
}, 409);
|
|
10304
|
-
}
|
|
10305
|
-
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10306
|
-
const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, {
|
|
10307
|
-
deviceId,
|
|
10308
|
-
...mappingInput
|
|
10309
|
-
})));
|
|
10524
|
+
if (missingConfirmations.length > 0) return c.json(mappingGuardrailChallenge(analyses.flatMap((analysis) => analysis.warnings), missingConfirmations), 409);
|
|
10310
10525
|
return c.json({
|
|
10311
10526
|
ok: true,
|
|
10312
|
-
mappings:
|
|
10527
|
+
mappings: saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs),
|
|
10313
10528
|
guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
|
|
10314
10529
|
});
|
|
10315
10530
|
} catch (error) {
|
|
@@ -10318,23 +10533,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10318
10533
|
releasePublicationMutation?.();
|
|
10319
10534
|
}
|
|
10320
10535
|
});
|
|
10321
|
-
app.delete("/api/sync/sharing-domains/project-mappings/:id",
|
|
10322
|
-
const store = getStore();
|
|
10323
|
-
const id = Number(c.req.param("id"));
|
|
10324
|
-
let releasePublicationMutation;
|
|
10325
|
-
try {
|
|
10326
|
-
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
10327
|
-
const deleted = deleteProjectScopeSettingsMapping(store.db, id);
|
|
10328
|
-
return c.json({
|
|
10329
|
-
ok: true,
|
|
10330
|
-
deleted
|
|
10331
|
-
});
|
|
10332
|
-
} catch (error) {
|
|
10333
|
-
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
10334
|
-
} finally {
|
|
10335
|
-
releasePublicationMutation?.();
|
|
10336
|
-
}
|
|
10337
|
-
});
|
|
10536
|
+
app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => deleteProjectScopeMappingRoute(c, getStore()));
|
|
10338
10537
|
app.post("/api/sync/peers/identity", async (c) => {
|
|
10339
10538
|
const store = getStore();
|
|
10340
10539
|
ensureLocalActorRecord(store);
|
|
@@ -10592,11 +10791,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10592
10791
|
WHERE inviter_actor_id = ? OR recipient_actor_id = ? OR person_id = ?`).run(secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, now, secondaryActorId, secondaryActorId, secondaryActorId);
|
|
10593
10792
|
const peerUpdate = d.update(schema.syncPeers).set({ actor_id: primaryActorId }).where(eq(schema.syncPeers.actor_id, secondaryActorId)).run();
|
|
10594
10793
|
if (primary.is_local) d.update(schema.syncPeers).set({ claimed_local_actor: 1 }).where(eq(schema.syncPeers.actor_id, primaryActorId)).run();
|
|
10595
|
-
|
|
10596
|
-
status: "merged",
|
|
10597
|
-
merged_into_actor_id: primaryActorId,
|
|
10598
|
-
updated_at: now
|
|
10599
|
-
}).where(eq(schema.actors.actor_id, secondaryActorId)).run();
|
|
10794
|
+
finishActorMerge(store, primaryActorId, secondaryActorId, now);
|
|
10600
10795
|
return { mergedCount: Number(peerUpdate.changes ?? 0) };
|
|
10601
10796
|
}).immediate();
|
|
10602
10797
|
if ("error" in result) return c.json({ error: result.error }, result.status);
|
|
@@ -10615,16 +10810,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10615
10810
|
if (actorId === store.actorId) return c.json({ error: "cannot deactivate this device's own local actor" }, 409);
|
|
10616
10811
|
const releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, [actorId]);
|
|
10617
10812
|
queueMicrotask(releaseActorMutations);
|
|
10618
|
-
const
|
|
10619
|
-
const actor = d.select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
|
|
10813
|
+
const actor = drizzle(store.db, { schema }).select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
|
|
10620
10814
|
if (!actor) return c.json({ error: "actor not found" }, 404);
|
|
10621
10815
|
if (actor.status !== "active") return c.json({ error: "actor not active" }, 409);
|
|
10622
|
-
|
|
10623
|
-
d.update(schema.actors).set({
|
|
10624
|
-
status: "deactivated",
|
|
10625
|
-
updated_at: now
|
|
10626
|
-
}).where(eq(schema.actors.actor_id, actorId)).run();
|
|
10627
|
-
d.update(schema.syncPeers).set({ actor_id: null }).where(eq(schema.syncPeers.actor_id, actorId)).run();
|
|
10816
|
+
deactivateActorAndWakePolicies(store, actorId, (/* @__PURE__ */ new Date()).toISOString());
|
|
10628
10817
|
return c.json({ ok: true });
|
|
10629
10818
|
});
|
|
10630
10819
|
app.post("/api/sync/legacy-devices/claim", async (c) => {
|