@codemem/server 0.46.3 → 0.47.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account/context.d.ts +41 -0
- package/dist/account/context.d.ts.map +1 -0
- package/dist/account/guard.d.ts +13 -0
- package/dist/account/guard.d.ts.map +1 -0
- package/dist/account/link-registry.d.ts +51 -0
- package/dist/account/link-registry.d.ts.map +1 -0
- package/dist/account/status.d.ts +65 -0
- package/dist/account/status.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1378 -228
- package/dist/index.js.map +1 -1
- package/dist/routes/account-operations.d.ts +11 -0
- package/dist/routes/account-operations.d.ts.map +1 -0
- package/dist/routes/account-status.d.ts +7 -0
- package/dist/routes/account-status.d.ts.map +1 -0
- package/dist/routes/account.d.ts +9 -0
- package/dist/routes/account.d.ts.map +1 -0
- package/dist/routes/device-rename.d.ts +7 -7
- package/dist/routes/device-rename.d.ts.map +1 -1
- package/dist/routes/memory-tools.d.ts.map +1 -1
- package/dist/routes/stats.d.ts.map +1 -1
- package/dist/routes/sync.d.ts +1 -29
- package/dist/routes/sync.d.ts.map +1 -1
- package/dist/routes/target-validation.d.ts.map +1 -1
- package/dist/routes/team-setup.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +52 -49
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/controls.css +175 -3
- package/static/controls.css.br +0 -0
- package/static/controls.css.gz +0 -0
- package/static/index.html +109 -305
package/dist/index.js
CHANGED
|
@@ -1,16 +1,120 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } 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, RICH_TIER_ANTHROPIC_DEFAULTS, RICH_TIER_DEFAULTS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SIMPLE_TIER_ANTHROPIC_DEFAULTS, SIMPLE_TIER_DEFAULTS, 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, classifyRecordedSyncFailure, 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, generateWithOpenCodeV2, 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, listOpenCodeV2Models, 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, resolveObserverDefaultModel, resolveObserverRuntime, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping,
|
|
3
|
+
import { ACCESS_CLEANUP_OP_TYPE, CodememConfigMutationError, CoordinatorAccountLinkError, 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, RICH_TIER_ANTHROPIC_DEFAULTS, RICH_TIER_DEFAULTS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SIMPLE_TIER_ANTHROPIC_DEFAULTS, SIMPLE_TIER_DEFAULTS, 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, changeCoordinatorAccount, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, classifyRecordedSyncFailure, 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, generateWithOpenCodeV2, getCodememConfigPath, getCodememEnvOverrideValues, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getEffectiveCachedScopeAuthorization, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isAutomaticRecallMeasurement, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, judgeDistillReport, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, linkCoordinatorAccount, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listOpenCodeV2Models, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadPublicKey, loadReplicationOpsForPeer, loadRuntimeSigningPublicKey, lookupCoordinatorPeers, memoryFilterValueMatchesType, mergeAddresses, migrateRecipientPolicyIntent, mutateCodememConfigFile, negotiateSyncCapability, normalizeAddress, normalizeCoordinatorAccountLinkOrigin, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parseBootstrapExpiry, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectMatchesFilter, projectShareLifecycle, promptPackArtifactFingerprint, readAccountLinkDevice, readCodememConfigFile, readCoordinatorAccountChangeCapabilities, readCoordinatorAccountLinkStatus, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordAutomaticRecall, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, repositoryIdentitiesByWorkspace, repositoryIdentityForWorkspace, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveCodememConfigPath, resolveDbPath, resolveObserverDefaultModel, resolveObserverRuntime, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, resolveVisibleScopeIds, runSyncPass, schema, scopeAuthorizationFailureReason, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, 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
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
7
|
import { createMiddleware } from "hono/factory";
|
|
7
8
|
import { RateLimiter } from "limiter";
|
|
8
|
-
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
9
|
import { and, count, desc, eq, inArray, max, ne } from "drizzle-orm";
|
|
10
10
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
11
11
|
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
12
12
|
import * as z from "zod";
|
|
13
13
|
import { chmod, link, mkdir, open, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
14
|
+
//#region src/routes/account.ts
|
|
15
|
+
/** Mounted only alongside the account request middleware. */
|
|
16
|
+
function accountRoutes(getSession) {
|
|
17
|
+
const app = new Hono();
|
|
18
|
+
app.get("/api/account/session", (c) => c.json(getSession()));
|
|
19
|
+
return app;
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/account/guard.ts
|
|
23
|
+
var ACCOUNT_PREFIX = "/api/account";
|
|
24
|
+
var SESSION_PATH = `${ACCOUNT_PREFIX}/session`;
|
|
25
|
+
var LISTENER_HOSTS = {
|
|
26
|
+
"127.0.0.1": ["127.0.0.1", "localhost"],
|
|
27
|
+
"::1": ["[::1]", "localhost"],
|
|
28
|
+
localhost: [
|
|
29
|
+
"127.0.0.1",
|
|
30
|
+
"[::1]",
|
|
31
|
+
"localhost"
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
function allowedAuthorities(options) {
|
|
35
|
+
try {
|
|
36
|
+
const address = options.getListenerAddress();
|
|
37
|
+
if (!address || !Number.isInteger(address.port) || address.port < 1 || address.port > 65535) return null;
|
|
38
|
+
const { host, port } = address;
|
|
39
|
+
if (!Object.hasOwn(LISTENER_HOSTS, host)) return null;
|
|
40
|
+
const aliases = LISTENER_HOSTS[host];
|
|
41
|
+
if (!aliases) return null;
|
|
42
|
+
return aliases.flatMap((alias) => {
|
|
43
|
+
const explicit = `${alias}:${port}`;
|
|
44
|
+
return [.../* @__PURE__ */ new Set([explicit, new URL(`http://${explicit}`).host])];
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function validRequestContext(c, authorities) {
|
|
51
|
+
const host = c.req.header("Host");
|
|
52
|
+
if (!host || !authorities.includes(host)) return false;
|
|
53
|
+
const expectedOrigin = new URL(`http://${host}`).origin;
|
|
54
|
+
try {
|
|
55
|
+
if (new URL(c.req.url).origin !== expectedOrigin) return false;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const origin = c.req.header("Origin");
|
|
60
|
+
if (origin !== void 0 && origin !== expectedOrigin) return false;
|
|
61
|
+
if (c.req.method === "POST" && origin === void 0) return false;
|
|
62
|
+
const fetchSite = c.req.header("Sec-Fetch-Site");
|
|
63
|
+
return fetchSite === void 0 || fetchSite === "same-origin";
|
|
64
|
+
}
|
|
65
|
+
function validSessionRequest(c) {
|
|
66
|
+
const contentLength = c.req.header("Content-Length");
|
|
67
|
+
return c.req.method === "GET" && !c.req.url.includes("?") && c.req.raw.body === null && (contentLength === void 0 || contentLength === "0") && c.req.header("Transfer-Encoding") === void 0;
|
|
68
|
+
}
|
|
69
|
+
function validCsrfToken(c, token) {
|
|
70
|
+
const supplied = c.req.header("X-Codemem-Account-Csrf");
|
|
71
|
+
if (!supplied || supplied.length !== token.length) return false;
|
|
72
|
+
const candidate = Buffer.from(supplied, "utf8");
|
|
73
|
+
return candidate.length === token.length && timingSafeEqual(candidate, token);
|
|
74
|
+
}
|
|
75
|
+
function requestError(c, options, token) {
|
|
76
|
+
const authorities = allowedAuthorities(options);
|
|
77
|
+
if (!authorities) return "account_local_viewer_required";
|
|
78
|
+
if (c.req.method === "OPTIONS" || !validRequestContext(c, authorities)) return "account_request_rejected";
|
|
79
|
+
if (c.req.path === SESSION_PATH) return validSessionRequest(c) ? null : "account_request_rejected";
|
|
80
|
+
return validCsrfToken(c, token) ? null : "account_csrf_invalid";
|
|
81
|
+
}
|
|
82
|
+
function secureAccountResponse(c) {
|
|
83
|
+
for (const name of [...c.res.headers.keys()]) if (name.toLowerCase().startsWith("access-control-")) c.header(name, void 0);
|
|
84
|
+
c.header("Cache-Control", "no-store");
|
|
85
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
86
|
+
c.header("Referrer-Policy", "no-referrer");
|
|
87
|
+
}
|
|
88
|
+
/** One token per viewer app; only the exact GET session route can return it. */
|
|
89
|
+
function createAccountGuard(options) {
|
|
90
|
+
const csrfToken = randomBytes(32).toString("base64url");
|
|
91
|
+
const token = Buffer.from(csrfToken, "utf8");
|
|
92
|
+
return {
|
|
93
|
+
middleware: createMiddleware(async (c, next) => {
|
|
94
|
+
if (c.req.path !== ACCOUNT_PREFIX && !c.req.path.startsWith(`${ACCOUNT_PREFIX}/`)) {
|
|
95
|
+
await next();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const error = requestError(c, options, token);
|
|
100
|
+
if (error) {
|
|
101
|
+
c.res = c.json({ error }, 403);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await next();
|
|
105
|
+
} catch {
|
|
106
|
+
c.res = c.json({ error: "account_request_rejected" }, 500);
|
|
107
|
+
} finally {
|
|
108
|
+
secureAccountResponse(c);
|
|
109
|
+
}
|
|
110
|
+
}),
|
|
111
|
+
routes: accountRoutes(() => ({
|
|
112
|
+
version: 1,
|
|
113
|
+
csrfToken
|
|
114
|
+
}))
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
14
118
|
//#region src/middleware.ts
|
|
15
119
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
16
120
|
"127.0.0.1",
|
|
@@ -161,6 +265,625 @@ function createInMemoryRequestRateLimiter(windowMs = 6e4) {
|
|
|
161
265
|
} };
|
|
162
266
|
}
|
|
163
267
|
//#endregion
|
|
268
|
+
//#region src/account/link-registry.ts
|
|
269
|
+
var DEADLINE_MS = 6e5;
|
|
270
|
+
var RETENTION_MS = 9e5;
|
|
271
|
+
var BUSY_SLOT = Symbol.for("codemem.account-link.busy-owner");
|
|
272
|
+
var runtime = globalThis;
|
|
273
|
+
runtime[BUSY_SLOT] ??= { owner: null };
|
|
274
|
+
var busySlot = runtime[BUSY_SLOT];
|
|
275
|
+
busySlot.lifecycle ??= Symbol();
|
|
276
|
+
function releaseBusyOwner(owner) {
|
|
277
|
+
if (busySlot.owner !== owner) return;
|
|
278
|
+
busySlot.owner = null;
|
|
279
|
+
busySlot.lifecycle = Symbol();
|
|
280
|
+
}
|
|
281
|
+
var AUTHORITY_FIELDS = [
|
|
282
|
+
"contextId",
|
|
283
|
+
"dbPath",
|
|
284
|
+
"keysDir",
|
|
285
|
+
"configPath",
|
|
286
|
+
"coordinatorUrl",
|
|
287
|
+
"groupId",
|
|
288
|
+
"deviceId",
|
|
289
|
+
"fingerprint",
|
|
290
|
+
"identityId"
|
|
291
|
+
];
|
|
292
|
+
function sameAuthority(captured, fresh) {
|
|
293
|
+
return AUTHORITY_FIELDS.every((key) => captured[key] === fresh[key]) && captured.fileIdentity.dev === fresh.fileIdentity.dev && captured.fileIdentity.ino === fresh.fileIdentity.ino;
|
|
294
|
+
}
|
|
295
|
+
function terminal(job) {
|
|
296
|
+
return job.operation.finishedAt !== null;
|
|
297
|
+
}
|
|
298
|
+
function safeFailure(error) {
|
|
299
|
+
if (!(error instanceof CoordinatorAccountLinkError)) return "request_failed";
|
|
300
|
+
if (error.code === "invalid_options") return "configuration_invalid";
|
|
301
|
+
switch (error.code) {
|
|
302
|
+
case "device_unavailable":
|
|
303
|
+
case "request_failed":
|
|
304
|
+
case "schema_upgrade_required":
|
|
305
|
+
case "review_required":
|
|
306
|
+
case "link_conflict":
|
|
307
|
+
case "invalid_response":
|
|
308
|
+
case "context_changed":
|
|
309
|
+
case "identity_mismatch":
|
|
310
|
+
case "link_stopped":
|
|
311
|
+
case "cancellation_unconfirmed":
|
|
312
|
+
case "receiver_close_failed": return error.code;
|
|
313
|
+
default: return "request_failed";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function validBrowserUrl(value, origin, intent) {
|
|
317
|
+
try {
|
|
318
|
+
const url = new URL(value);
|
|
319
|
+
return url.origin === origin && url.pathname === (intent === "refresh" || intent === "disconnect" ? "/auth/account/change/start" : "/auth/link/start") && !url.username && !url.password && !url.hash;
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Private URLs and captured profiles stay inside this process, never in public snapshots. */
|
|
325
|
+
var InProcessAccountLinkRegistry = class {
|
|
326
|
+
#options;
|
|
327
|
+
#now;
|
|
328
|
+
#jobs = /* @__PURE__ */ new Map();
|
|
329
|
+
#shuttingDown = false;
|
|
330
|
+
#closing = null;
|
|
331
|
+
#uncertainOwner = null;
|
|
332
|
+
constructor(options) {
|
|
333
|
+
this.#options = options;
|
|
334
|
+
this.#now = options.now ?? Date.now;
|
|
335
|
+
}
|
|
336
|
+
#current(context) {
|
|
337
|
+
try {
|
|
338
|
+
const fresh = this.#options.readContext(context.groupId);
|
|
339
|
+
return !("error" in fresh) && fresh.setup === "ready" && fresh.context !== null && sameAuthority(context, fresh.context);
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
#cleanup() {
|
|
345
|
+
const finished = [...this.#jobs.values()].filter(terminal);
|
|
346
|
+
for (const [index, job] of finished.entries()) {
|
|
347
|
+
if (job.operation.state === "outcome_unknown") continue;
|
|
348
|
+
if (this.#now() - Date.parse(job.operation.finishedAt ?? "") >= RETENTION_MS || index < finished.length - 8) this.#jobs.delete(job.operation.operationId);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
#active() {
|
|
352
|
+
this.#cleanup();
|
|
353
|
+
return [...this.#jobs.values()].find((job) => !terminal(job) || !this.#shuttingDown && job.operation.state === "outcome_unknown");
|
|
354
|
+
}
|
|
355
|
+
#inProgress() {
|
|
356
|
+
const pending = this.#active();
|
|
357
|
+
if (pending) return {
|
|
358
|
+
error: "account_link_in_progress",
|
|
359
|
+
operationId: pending.operation.operationId
|
|
360
|
+
};
|
|
361
|
+
return busySlot.owner ? { error: "account_link_in_progress" } : null;
|
|
362
|
+
}
|
|
363
|
+
#snapshot(job) {
|
|
364
|
+
return { ...job.operation };
|
|
365
|
+
}
|
|
366
|
+
#clearUrl(job) {
|
|
367
|
+
job.url = null;
|
|
368
|
+
job.operation.browserUrlAvailable = false;
|
|
369
|
+
}
|
|
370
|
+
#finish(job, state, failure) {
|
|
371
|
+
if (terminal(job)) return;
|
|
372
|
+
this.#clearUrl(job);
|
|
373
|
+
Object.assign(job.operation, {
|
|
374
|
+
state,
|
|
375
|
+
failure,
|
|
376
|
+
finishedAt: new Date(this.#now()).toISOString()
|
|
377
|
+
});
|
|
378
|
+
this.#cleanup();
|
|
379
|
+
}
|
|
380
|
+
#browserStart(job, url) {
|
|
381
|
+
if (this.#shuttingDown || terminal(job) || job.controller.signal.aborted) return;
|
|
382
|
+
if (!this.#current(job.context)) job.abortFailure = "context_changed";
|
|
383
|
+
else if (!validBrowserUrl(url, job.context.coordinatorUrl, job.operation.intent)) job.abortFailure = "invalid_response";
|
|
384
|
+
if (job.abortFailure) {
|
|
385
|
+
this.#clearUrl(job);
|
|
386
|
+
job.controller.abort();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
job.url = url;
|
|
390
|
+
job.operation.state = "awaiting_browser";
|
|
391
|
+
job.operation.browserUrlAvailable = job.deliveries < 5;
|
|
392
|
+
}
|
|
393
|
+
async #run(job, expectedCoordinatorId, expectedAuthority) {
|
|
394
|
+
try {
|
|
395
|
+
if (terminal(job) || job.controller.signal.aborted) {
|
|
396
|
+
this.#finish(job, "cancelled", "link_stopped");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const { dbPath, keysDir, groupId, coordinatorUrl, identityId } = job.context;
|
|
400
|
+
const options = {
|
|
401
|
+
dbPath,
|
|
402
|
+
keysDir,
|
|
403
|
+
groupId,
|
|
404
|
+
coordinatorUrl,
|
|
405
|
+
expectedIdentityId: identityId,
|
|
406
|
+
expectedCoordinatorId,
|
|
407
|
+
assertCurrentContext: () => this.#current(job.context),
|
|
408
|
+
signal: job.controller.signal,
|
|
409
|
+
onBrowserStart: (url) => this.#browserStart(job, url)
|
|
410
|
+
};
|
|
411
|
+
const intent = job.operation.intent ?? "connect";
|
|
412
|
+
if (intent === "connect") await (this.#options.link ?? linkCoordinatorAccount)({
|
|
413
|
+
...options,
|
|
414
|
+
expectedAuthority
|
|
415
|
+
});
|
|
416
|
+
else {
|
|
417
|
+
const result = await (this.#options.change ?? changeCoordinatorAccount)({
|
|
418
|
+
...options,
|
|
419
|
+
intent,
|
|
420
|
+
expectedLinkId: job.expectedLinkId
|
|
421
|
+
});
|
|
422
|
+
if (result.state !== "completed" || result.intent !== intent || result.coordinatorId !== expectedCoordinatorId || result.identityId !== identityId) throw new CoordinatorAccountLinkError("invalid_response");
|
|
423
|
+
}
|
|
424
|
+
this.#finish(job, "succeeded", null);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
this.#fail(job, error);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
#fail(job, error) {
|
|
430
|
+
let failure = safeFailure(error);
|
|
431
|
+
if (job.abortFailure && (failure === "link_stopped" || failure === "request_failed")) failure = job.abortFailure;
|
|
432
|
+
let state = "failed";
|
|
433
|
+
if (failure === "cancellation_unconfirmed" || failure === "receiver_close_failed") state = "outcome_unknown";
|
|
434
|
+
else if (job.cancelled && (failure === "link_stopped" || failure === "request_failed")) state = "cancelled";
|
|
435
|
+
this.#finish(job, state, failure);
|
|
436
|
+
}
|
|
437
|
+
start({ intent = "connect", context, expectedCoordinatorId, expectedAuthority, expectedLifecycleToken, expectedLinkId }) {
|
|
438
|
+
if (this.#shuttingDown) return { error: "account_shutting_down" };
|
|
439
|
+
const pending = this.#inProgress();
|
|
440
|
+
if (pending) return pending;
|
|
441
|
+
if (expectedLifecycleToken && expectedLifecycleToken !== this.lifecycleToken()) return { error: "account_context_changed" };
|
|
442
|
+
if (!this.#current(context)) return { error: "account_context_changed" };
|
|
443
|
+
const started = this.#now();
|
|
444
|
+
const job = {
|
|
445
|
+
context: {
|
|
446
|
+
...context,
|
|
447
|
+
fileIdentity: { ...context.fileIdentity }
|
|
448
|
+
},
|
|
449
|
+
expectedLinkId,
|
|
450
|
+
controller: new AbortController(),
|
|
451
|
+
url: null,
|
|
452
|
+
deliveries: 0,
|
|
453
|
+
cancelled: false,
|
|
454
|
+
abortFailure: null,
|
|
455
|
+
done: Promise.resolve(),
|
|
456
|
+
operation: {
|
|
457
|
+
intent,
|
|
458
|
+
operationId: randomUUID(),
|
|
459
|
+
groupId: context.groupId,
|
|
460
|
+
contextId: context.contextId,
|
|
461
|
+
state: "starting",
|
|
462
|
+
failure: null,
|
|
463
|
+
browserUrlAvailable: false,
|
|
464
|
+
startedAt: new Date(started).toISOString(),
|
|
465
|
+
deadlineAt: new Date(started + DEADLINE_MS).toISOString(),
|
|
466
|
+
finishedAt: null
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
if (this.#shuttingDown) return { error: "account_shutting_down" };
|
|
470
|
+
const reentrantPending = this.#inProgress();
|
|
471
|
+
if (reentrantPending) return reentrantPending;
|
|
472
|
+
if (expectedLifecycleToken && expectedLifecycleToken !== this.lifecycleToken()) return { error: "account_context_changed" };
|
|
473
|
+
const owner = Symbol();
|
|
474
|
+
busySlot.owner = owner;
|
|
475
|
+
busySlot.lifecycle = Symbol();
|
|
476
|
+
this.#jobs.set(job.operation.operationId, job);
|
|
477
|
+
job.done = Promise.resolve().then(() => this.#run(job, expectedCoordinatorId, expectedAuthority)).finally(() => {
|
|
478
|
+
if (job.operation.state === "outcome_unknown" && !this.#shuttingDown) this.#uncertainOwner = owner;
|
|
479
|
+
else releaseBusyOwner(owner);
|
|
480
|
+
});
|
|
481
|
+
return { operation: this.#snapshot(job) };
|
|
482
|
+
}
|
|
483
|
+
lifecycleToken() {
|
|
484
|
+
busySlot.lifecycle ??= Symbol();
|
|
485
|
+
return busySlot.lifecycle;
|
|
486
|
+
}
|
|
487
|
+
get(operationId) {
|
|
488
|
+
this.#cleanup();
|
|
489
|
+
const job = this.#jobs.get(operationId);
|
|
490
|
+
return job ? this.#snapshot(job) : null;
|
|
491
|
+
}
|
|
492
|
+
browserUrl(operationId) {
|
|
493
|
+
this.#cleanup();
|
|
494
|
+
const job = this.#jobs.get(operationId);
|
|
495
|
+
if (!job || this.#shuttingDown || terminal(job) || job.controller.signal.aborted || job.operation.state !== "awaiting_browser" || job.deliveries >= 5) return null;
|
|
496
|
+
if (!this.#current(job.context)) {
|
|
497
|
+
job.abortFailure = "context_changed";
|
|
498
|
+
this.#clearUrl(job);
|
|
499
|
+
job.controller.abort();
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
const url = job.url;
|
|
503
|
+
if (url) job.deliveries += 1;
|
|
504
|
+
if (job.deliveries >= 5) this.#clearUrl(job);
|
|
505
|
+
return url;
|
|
506
|
+
}
|
|
507
|
+
cancel(operationId) {
|
|
508
|
+
this.#cleanup();
|
|
509
|
+
const job = this.#jobs.get(operationId);
|
|
510
|
+
if (!job) return null;
|
|
511
|
+
if (!terminal(job)) {
|
|
512
|
+
job.cancelled = true;
|
|
513
|
+
this.#clearUrl(job);
|
|
514
|
+
job.controller.abort();
|
|
515
|
+
}
|
|
516
|
+
return this.#snapshot(job);
|
|
517
|
+
}
|
|
518
|
+
async #drain() {
|
|
519
|
+
let timer;
|
|
520
|
+
try {
|
|
521
|
+
await Promise.race([Promise.all([...this.#jobs.values()].map((job) => job.done)), new Promise((resolve) => {
|
|
522
|
+
timer = setTimeout(resolve, 5e3);
|
|
523
|
+
})]);
|
|
524
|
+
} finally {
|
|
525
|
+
clearTimeout(timer);
|
|
526
|
+
for (const job of this.#jobs.values()) this.#finish(job, "outcome_unknown", "cancellation_unconfirmed");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
close() {
|
|
530
|
+
if (this.#closing) return this.#closing;
|
|
531
|
+
this.#shuttingDown = true;
|
|
532
|
+
for (const job of this.#jobs.values()) {
|
|
533
|
+
this.#clearUrl(job);
|
|
534
|
+
if (!terminal(job)) {
|
|
535
|
+
job.cancelled = true;
|
|
536
|
+
job.controller.abort();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
this.#closing = this.#drain().finally(() => {
|
|
540
|
+
if (this.#uncertainOwner) releaseBusyOwner(this.#uncertainOwner);
|
|
541
|
+
});
|
|
542
|
+
return this.#closing;
|
|
543
|
+
}
|
|
544
|
+
hasActive() {
|
|
545
|
+
return Boolean(this.#active() || busySlot.owner);
|
|
546
|
+
}
|
|
547
|
+
forContext(contextId) {
|
|
548
|
+
this.#cleanup();
|
|
549
|
+
const job = [...this.#jobs.values()].reverse().find((item) => item.operation.contextId === contextId);
|
|
550
|
+
return job ? this.#snapshot(job) : null;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
function createAccountLinkRegistry(options) {
|
|
554
|
+
return new InProcessAccountLinkRegistry(options);
|
|
555
|
+
}
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region src/account/status.ts
|
|
558
|
+
function attachOperation(view, registry) {
|
|
559
|
+
const operation = view.contextId ? registry?.forContext(view.contextId) ?? null : null;
|
|
560
|
+
if (registry?.hasActive()) return {
|
|
561
|
+
...view,
|
|
562
|
+
operation,
|
|
563
|
+
canLink: false,
|
|
564
|
+
blockedReason: "operation_active"
|
|
565
|
+
};
|
|
566
|
+
return {
|
|
567
|
+
...view,
|
|
568
|
+
operation
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function accountLocalView(result) {
|
|
572
|
+
const context = result.context;
|
|
573
|
+
return {
|
|
574
|
+
version: 1,
|
|
575
|
+
contextId: context?.contextId ?? null,
|
|
576
|
+
setup: result.setup,
|
|
577
|
+
groups: result.groups,
|
|
578
|
+
selectedGroupId: result.selectedGroupId,
|
|
579
|
+
coordinator: result.coordinator,
|
|
580
|
+
local: context ? {
|
|
581
|
+
deviceId: context.deviceId,
|
|
582
|
+
fingerprint: context.fingerprint,
|
|
583
|
+
identityId: context.identityId,
|
|
584
|
+
identityLabel: context.identityLabel,
|
|
585
|
+
deviceLabel: context.deviceLabel
|
|
586
|
+
} : null,
|
|
587
|
+
remote: { kind: "not_checked" },
|
|
588
|
+
canLink: false,
|
|
589
|
+
blockedReason: "setup_incomplete",
|
|
590
|
+
operation: null,
|
|
591
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function applyRemote(view, remote) {
|
|
595
|
+
if (remote.kind !== "status") return {
|
|
596
|
+
...view,
|
|
597
|
+
remote: remote.kind === "unsupported" ? { kind: "unsupported" } : {
|
|
598
|
+
kind: "unavailable",
|
|
599
|
+
code: remote.code
|
|
600
|
+
},
|
|
601
|
+
blockedReason: remote.kind === "unsupported" ? "unsupported" : "status_unavailable"
|
|
602
|
+
};
|
|
603
|
+
const status = remote.status;
|
|
604
|
+
const identityMatches = status.identityId !== null && status.identityId === view.local?.identityId;
|
|
605
|
+
let blockedReason = null;
|
|
606
|
+
if (status.state === "review_required" || status.state === "conflict") blockedReason = status.state;
|
|
607
|
+
else if (!identityMatches) blockedReason = "identity_mismatch";
|
|
608
|
+
else if (status.state === "linked") blockedReason = "already_linked";
|
|
609
|
+
else if (status.state === "revoked") blockedReason = "revoked";
|
|
610
|
+
return {
|
|
611
|
+
...view,
|
|
612
|
+
remote: {
|
|
613
|
+
kind: "status",
|
|
614
|
+
coordinatorId: status.coordinatorId,
|
|
615
|
+
authority: status.authority,
|
|
616
|
+
identityId: status.identityId,
|
|
617
|
+
state: status.state,
|
|
618
|
+
identityMatches,
|
|
619
|
+
account: identityMatches && status.state === "linked" ? status.account : null
|
|
620
|
+
},
|
|
621
|
+
canLink: status.state === "unlinked" && identityMatches,
|
|
622
|
+
blockedReason
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
async function readAccountStatus(options, groupId, signal, readOptions = {}) {
|
|
626
|
+
const initial = options.readContext(groupId);
|
|
627
|
+
if ("error" in initial) return initial;
|
|
628
|
+
const view = accountLocalView(initial);
|
|
629
|
+
if (initial.setup !== "ready" || !initial.context) return attachOperation(view, options.registry);
|
|
630
|
+
const snapshot = JSON.stringify(initial);
|
|
631
|
+
let remote;
|
|
632
|
+
try {
|
|
633
|
+
const { dbPath, keysDir, groupId, coordinatorUrl, identityId } = initial.context;
|
|
634
|
+
remote = await (options.readRemoteStatus ?? readCoordinatorAccountLinkStatus)({
|
|
635
|
+
dbPath,
|
|
636
|
+
keysDir,
|
|
637
|
+
groupId,
|
|
638
|
+
coordinatorUrl,
|
|
639
|
+
identityId,
|
|
640
|
+
signal
|
|
641
|
+
});
|
|
642
|
+
} catch {
|
|
643
|
+
remote = {
|
|
644
|
+
kind: "unavailable",
|
|
645
|
+
code: "coordinator_unreachable"
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
let fresh;
|
|
649
|
+
try {
|
|
650
|
+
fresh = options.readContext(groupId);
|
|
651
|
+
} catch {
|
|
652
|
+
fresh = { error: "account_context_unavailable" };
|
|
653
|
+
}
|
|
654
|
+
const subject = "error" in fresh ? null : fresh.context;
|
|
655
|
+
const tupleChanged = remote.kind === "status" && (remote.status.groupId !== subject?.groupId || remote.status.deviceId !== subject?.deviceId || remote.status.fingerprint !== subject?.fingerprint);
|
|
656
|
+
const expectedChanged = readOptions.expectedContext !== void 0 && (!initial.context || !sameAuthority(readOptions.expectedContext, initial.context));
|
|
657
|
+
if ("error" in fresh || snapshot !== JSON.stringify(fresh) || tupleChanged || expectedChanged) return attachOperation({
|
|
658
|
+
..."error" in fresh ? accountLocalView({
|
|
659
|
+
setup: "device_unavailable",
|
|
660
|
+
groups: [],
|
|
661
|
+
selectedGroupId: null,
|
|
662
|
+
coordinator: null,
|
|
663
|
+
context: null
|
|
664
|
+
}) : accountLocalView(fresh),
|
|
665
|
+
remote: {
|
|
666
|
+
kind: "unavailable",
|
|
667
|
+
code: "context_changed"
|
|
668
|
+
},
|
|
669
|
+
blockedReason: "status_unavailable"
|
|
670
|
+
}, options.registry);
|
|
671
|
+
const currentView = applyRemote(accountLocalView(fresh), remote);
|
|
672
|
+
await attachManagementActions(options, initial.context, currentView, signal);
|
|
673
|
+
return attachOperation(currentView, options.registry);
|
|
674
|
+
}
|
|
675
|
+
async function attachManagementActions(options, context, view, signal) {
|
|
676
|
+
if (view.remote.kind !== "status" || view.remote.state !== "linked" || !view.remote.identityMatches) return;
|
|
677
|
+
const read = options.readRemoteCapabilities ?? (options.readRemoteStatus ? null : readCoordinatorAccountChangeCapabilities);
|
|
678
|
+
if (!read) return;
|
|
679
|
+
let capabilities = null;
|
|
680
|
+
try {
|
|
681
|
+
const { dbPath, keysDir, groupId, coordinatorUrl, identityId } = context;
|
|
682
|
+
capabilities = await read({
|
|
683
|
+
dbPath,
|
|
684
|
+
keysDir,
|
|
685
|
+
groupId,
|
|
686
|
+
coordinatorUrl,
|
|
687
|
+
identityId,
|
|
688
|
+
signal
|
|
689
|
+
});
|
|
690
|
+
} catch {}
|
|
691
|
+
let fresh;
|
|
692
|
+
try {
|
|
693
|
+
fresh = options.readContext(context.groupId);
|
|
694
|
+
} catch {
|
|
695
|
+
fresh = { error: "account_context_unavailable" };
|
|
696
|
+
}
|
|
697
|
+
if (signal.aborted || "error" in fresh || fresh.setup !== "ready" || !fresh.context || !sameAuthority(context, fresh.context)) {
|
|
698
|
+
view.remote = {
|
|
699
|
+
kind: "unavailable",
|
|
700
|
+
code: "context_changed"
|
|
701
|
+
};
|
|
702
|
+
view.blockedReason = "status_unavailable";
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (capabilities?.kind !== "capabilities" || capabilities.coordinatorId !== view.remote.coordinatorId) return;
|
|
706
|
+
view.managementActions = {
|
|
707
|
+
...capabilities.actions,
|
|
708
|
+
disconnect: capabilities.actions.disconnect && !!capabilities.unlinkTarget
|
|
709
|
+
};
|
|
710
|
+
if (view.managementActions.disconnect) view.unlinkTarget = capabilities.unlinkTarget;
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/routes/account-operations.ts
|
|
714
|
+
var ROOT = "/api/account/link-operations";
|
|
715
|
+
function validString(value) {
|
|
716
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
|
|
717
|
+
}
|
|
718
|
+
function noBody(request) {
|
|
719
|
+
const length = request.headers.get("Content-Length");
|
|
720
|
+
return request.body === null && (length === null || length === "0") && !request.headers.has("Transfer-Encoding");
|
|
721
|
+
}
|
|
722
|
+
/** Cap both bytes and elapsed time; cancellation must not wait on an uncooperative stream. */
|
|
723
|
+
async function readBody(request, keys, optional = []) {
|
|
724
|
+
if (!/^application\/json(?:\s*;\s*charset\s*=\s*(?:utf-8|"utf-8"))?\s*$/i.test(request.headers.get("Content-Type") ?? "")) return null;
|
|
725
|
+
const reader = request.body?.getReader();
|
|
726
|
+
if (!reader) return null;
|
|
727
|
+
let timer;
|
|
728
|
+
let stop = () => {};
|
|
729
|
+
try {
|
|
730
|
+
const expired = new Promise((_, reject) => {
|
|
731
|
+
stop = () => reject(/* @__PURE__ */ new Error("invalid body"));
|
|
732
|
+
timer = setTimeout(stop, 1e4);
|
|
733
|
+
request.signal.addEventListener("abort", stop, { once: true });
|
|
734
|
+
if (request.signal.aborted) stop();
|
|
735
|
+
});
|
|
736
|
+
const bytes = /* @__PURE__ */ new Uint8Array(1024);
|
|
737
|
+
let length = 0;
|
|
738
|
+
while (true) {
|
|
739
|
+
const chunk = await Promise.race([reader.read(), expired]);
|
|
740
|
+
if (chunk.done) break;
|
|
741
|
+
if (length + chunk.value.byteLength > bytes.length) return null;
|
|
742
|
+
bytes.set(chunk.value, length);
|
|
743
|
+
length += chunk.value.byteLength;
|
|
744
|
+
}
|
|
745
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length)));
|
|
746
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
747
|
+
const actual = Object.keys(value);
|
|
748
|
+
if (keys.some((key) => !actual.includes(key)) || actual.some((key) => !keys.includes(key) && !optional.includes(key))) return null;
|
|
749
|
+
return value;
|
|
750
|
+
} catch {
|
|
751
|
+
return null;
|
|
752
|
+
} finally {
|
|
753
|
+
clearTimeout(timer);
|
|
754
|
+
request.signal.removeEventListener("abort", stop);
|
|
755
|
+
reader.cancel().catch(() => {});
|
|
756
|
+
reader.releaseLock();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function isCurrent(options, operation) {
|
|
760
|
+
const fresh = options.readContext(operation.groupId);
|
|
761
|
+
return !("error" in fresh) && fresh.setup === "ready" && fresh.context?.groupId === operation.groupId && fresh.context.contextId === operation.contextId;
|
|
762
|
+
}
|
|
763
|
+
function validStartBody(body) {
|
|
764
|
+
return body !== null && validString(body.groupId) && validString(body.contextId) && validString(body.coordinatorId) && (body.intent !== "disconnect" || validString(body.expectedLinkId)) && (body.intent === void 0 || body.intent === "connect" || body.intent === "refresh" || body.intent === "disconnect") && (body.authority === "controller_attestation" || body.authority === "device_claim");
|
|
765
|
+
}
|
|
766
|
+
function unlinkTargetChanged(body, view) {
|
|
767
|
+
return body.intent === "disconnect" && body.expectedLinkId !== view.unlinkTarget;
|
|
768
|
+
}
|
|
769
|
+
async function startOperation(c, options) {
|
|
770
|
+
const body = await readBody(c.req.raw, [
|
|
771
|
+
"groupId",
|
|
772
|
+
"contextId",
|
|
773
|
+
"authority",
|
|
774
|
+
"coordinatorId"
|
|
775
|
+
], ["intent", "expectedLinkId"]);
|
|
776
|
+
if (!validStartBody(body)) return c.json({ error: "account_invalid_request" }, 400);
|
|
777
|
+
const initial = options.readContext(body.groupId);
|
|
778
|
+
if ("error" in initial && initial.error === "account_group_not_configured") return c.json({ error: initial.error }, 400);
|
|
779
|
+
if ("error" in initial || initial.setup !== "ready" || !initial.context || initial.context?.contextId !== body.contextId) return c.json({ error: "account_context_changed" }, 409);
|
|
780
|
+
const checkedContext = structuredClone(initial.context);
|
|
781
|
+
const expectedLifecycleToken = options.registry.lifecycleToken();
|
|
782
|
+
const view = await readAccountStatus(options, body.groupId, c.req.raw.signal, { expectedContext: checkedContext });
|
|
783
|
+
if ("error" in view || view.setup !== "ready" || view.contextId !== body.contextId || view.remote.kind === "unavailable" && view.remote.code === "context_changed") return c.json({ error: "account_context_changed" }, 409);
|
|
784
|
+
if (view.operation && view.operation.finishedAt === null) return c.json({
|
|
785
|
+
error: "account_link_in_progress",
|
|
786
|
+
operationId: view.operation.operationId
|
|
787
|
+
}, 409);
|
|
788
|
+
if (view.blockedReason === "operation_active") return c.json({
|
|
789
|
+
error: "account_link_not_allowed",
|
|
790
|
+
blockedReason: "operation_active"
|
|
791
|
+
}, 409);
|
|
792
|
+
if (view.remote.kind === "unavailable") return c.json({ error: "account_status_unavailable" }, 503);
|
|
793
|
+
if (!actionAllowed(view, body.intent) || view.remote.kind !== "status") return c.json({
|
|
794
|
+
error: "account_link_not_allowed",
|
|
795
|
+
blockedReason: view.blockedReason
|
|
796
|
+
}, 409);
|
|
797
|
+
if (unlinkTargetChanged(body, view)) return c.json({ error: "account_context_changed" }, 409);
|
|
798
|
+
return beginCurrentOperation(c, options, body, view, view.remote.coordinatorId, checkedContext, expectedLifecycleToken);
|
|
799
|
+
}
|
|
800
|
+
function actionAllowed(view, intent = "connect") {
|
|
801
|
+
if (intent === "connect") return view.canLink;
|
|
802
|
+
if (intent === "refresh" || intent === "disconnect") return view.managementActions?.[intent] === true;
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
function beginCurrentOperation(c, options, body, view, expectedCoordinatorId, checkedContext, expectedLifecycleToken) {
|
|
806
|
+
if (c.req.raw.signal.aborted) return c.json({ error: "account_invalid_request" }, 400);
|
|
807
|
+
if (view.remote.kind !== "status" || (body.intent ?? "connect") === "connect" && view.remote.authority !== body.authority || view.remote.coordinatorId !== body.coordinatorId) return c.json({ error: "account_context_changed" }, 409);
|
|
808
|
+
const fresh = options.readContext(body.groupId);
|
|
809
|
+
if ("error" in fresh || fresh.setup !== "ready" || !fresh.context || fresh.context.contextId !== body.contextId || fresh.context.contextId !== view.contextId || fresh.context.groupId !== body.groupId || !sameAuthority(checkedContext, fresh.context)) return c.json({ error: "account_context_changed" }, 409);
|
|
810
|
+
const result = options.registry.start({
|
|
811
|
+
intent: body.intent ?? "connect",
|
|
812
|
+
context: checkedContext,
|
|
813
|
+
expectedCoordinatorId,
|
|
814
|
+
expectedAuthority: body.authority,
|
|
815
|
+
expectedLifecycleToken,
|
|
816
|
+
expectedLinkId: body.expectedLinkId
|
|
817
|
+
});
|
|
818
|
+
if ("error" in result) return c.json(result, result.error === "account_shutting_down" ? 503 : 409);
|
|
819
|
+
return c.json({
|
|
820
|
+
version: 1,
|
|
821
|
+
operation: result.operation
|
|
822
|
+
}, 202);
|
|
823
|
+
}
|
|
824
|
+
async function performOperation(c, options) {
|
|
825
|
+
const { id, action } = c.req.param();
|
|
826
|
+
if (!validString(id) || action !== "browser-url" && action !== "cancel" || !await readBody(c.req.raw, [])) return c.json({ error: "account_invalid_request" }, 400);
|
|
827
|
+
const operation = options.registry.get(id);
|
|
828
|
+
if (!operation) return c.json({ error: "account_operation_not_found" }, 404);
|
|
829
|
+
if (action === "cancel") {
|
|
830
|
+
const cancelled = options.registry.cancel(id);
|
|
831
|
+
if (!cancelled) return c.json({ error: "account_operation_not_found" }, 404);
|
|
832
|
+
return c.json({
|
|
833
|
+
version: 1,
|
|
834
|
+
operation: cancelled
|
|
835
|
+
}, cancelled.finishedAt === null ? 202 : 200);
|
|
836
|
+
}
|
|
837
|
+
if (operation.state !== "awaiting_browser") return c.json({ error: "account_browser_url_unavailable" }, 409);
|
|
838
|
+
const url = options.registry.browserUrl(id);
|
|
839
|
+
if (!url) return c.json({ error: "account_browser_url_unavailable" }, 409);
|
|
840
|
+
return c.json({
|
|
841
|
+
version: 1,
|
|
842
|
+
url
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function accountOperationRoutes(options) {
|
|
846
|
+
const app = new Hono();
|
|
847
|
+
app.onError((_error, c) => c.json({ error: "account_internal_error" }, 500));
|
|
848
|
+
const rejectQuery = async (c, next) => {
|
|
849
|
+
if ([...new URL(c.req.url).searchParams].length) return c.json({ error: "account_invalid_request" }, 400);
|
|
850
|
+
await next();
|
|
851
|
+
};
|
|
852
|
+
app.use(ROOT, rejectQuery);
|
|
853
|
+
app.use(`${ROOT}/*`, rejectQuery);
|
|
854
|
+
app.post(ROOT, (c) => startOperation(c, options));
|
|
855
|
+
app.get(`${ROOT}/:id`, (c) => {
|
|
856
|
+
if (!noBody(c.req.raw) || !validString(c.req.param("id"))) return c.json({ error: "account_invalid_request" }, 400);
|
|
857
|
+
const operation = options.registry.get(c.req.param("id"));
|
|
858
|
+
if (!operation) return c.json({ error: "account_operation_not_found" }, 404);
|
|
859
|
+
return c.json({
|
|
860
|
+
version: 1,
|
|
861
|
+
operation,
|
|
862
|
+
current: isCurrent(options, operation)
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
app.post(`${ROOT}/:id/:action`, (c) => performOperation(c, options));
|
|
866
|
+
return app;
|
|
867
|
+
}
|
|
868
|
+
//#endregion
|
|
869
|
+
//#region src/routes/account-status.ts
|
|
870
|
+
function accountStatusRoutes(options) {
|
|
871
|
+
const app = new Hono();
|
|
872
|
+
app.get("/api/account/link-status", async (c) => {
|
|
873
|
+
try {
|
|
874
|
+
const query = new URL(c.req.url).searchParams;
|
|
875
|
+
const length = c.req.header("Content-Length");
|
|
876
|
+
if ([...query.keys()].some((key) => key !== "group_id") || query.getAll("group_id").length > 1 || c.req.raw.body !== null || length !== void 0 && length !== "0" || c.req.header("Transfer-Encoding") !== void 0) return c.json({ error: "account_invalid_request" }, 400);
|
|
877
|
+
const result = await readAccountStatus(options, query.get("group_id") ?? void 0, c.req.raw.signal);
|
|
878
|
+
if ("error" in result) return c.json(result, result.error === "account_group_not_configured" ? 400 : 503);
|
|
879
|
+
return c.json(result);
|
|
880
|
+
} catch {
|
|
881
|
+
return c.json({ error: "account_internal_error" }, 500);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
return app;
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
164
887
|
//#region src/routes/config.ts
|
|
165
888
|
/**
|
|
166
889
|
* Config routes — GET /api/config, POST /api/config.
|
|
@@ -1647,11 +2370,11 @@ function memoryRoutes(getStore) {
|
|
|
1647
2370
|
}
|
|
1648
2371
|
const memoryId = parseStrictInteger(typeof body.memory_id === "string" ? body.memory_id : String(body.memory_id ?? ""));
|
|
1649
2372
|
if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
|
|
1650
|
-
if (!store.
|
|
2373
|
+
if (!store.getForMutation(memoryId)) return c.json({ error: "memory not found" }, 404);
|
|
1651
2374
|
const visibility = String(body.visibility ?? "").trim();
|
|
1652
2375
|
if (visibility !== "private" && visibility !== "shared") return c.json({ error: "visibility must be private or shared" }, 400);
|
|
1653
2376
|
try {
|
|
1654
|
-
const item = store.
|
|
2377
|
+
const item = store.updateMemoryVisibilityForUser(memoryId, visibility);
|
|
1655
2378
|
return c.json({ item });
|
|
1656
2379
|
} catch (err) {
|
|
1657
2380
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1672,9 +2395,9 @@ function memoryRoutes(getStore) {
|
|
|
1672
2395
|
if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
|
|
1673
2396
|
const project = String(body.project ?? "").trim();
|
|
1674
2397
|
if (!project) return c.json({ error: "project must be a non-empty string" }, 400);
|
|
1675
|
-
if (!store.
|
|
2398
|
+
if (!store.getForMutation(memoryId)) return c.json({ error: "memory not found" }, 404);
|
|
1676
2399
|
try {
|
|
1677
|
-
const result = store.
|
|
2400
|
+
const result = store.moveMemoryProjectForUser(memoryId, project);
|
|
1678
2401
|
return c.json(result);
|
|
1679
2402
|
} catch (err) {
|
|
1680
2403
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -1699,11 +2422,11 @@ function memoryRoutes(getStore) {
|
|
|
1699
2422
|
if (!store.memoryOwnedBySelf(row)) return c.json({ error: "memory not owned by this device" }, 403);
|
|
1700
2423
|
if (Number(row.active ?? 1) === 0 || row.deleted_at != null) return c.json({ status: "ok" });
|
|
1701
2424
|
try {
|
|
1702
|
-
store
|
|
2425
|
+
forgetMemoryForViewer(store, memoryId);
|
|
1703
2426
|
return c.json({ status: "ok" });
|
|
1704
2427
|
} catch (err) {
|
|
1705
2428
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1706
|
-
if (msg.includes("not found")) return c.json(
|
|
2429
|
+
if (msg.includes("not found")) return c.json(...forgetNotFound(forgetSafetyFilters(body)));
|
|
1707
2430
|
if (msg.includes("not owned")) return c.json({ error: msg }, 403);
|
|
1708
2431
|
if (msg.includes("sync_rebootstrap_in_progress")) return c.json({ error: msg }, 409);
|
|
1709
2432
|
return c.json({ error: msg }, 400);
|
|
@@ -1721,9 +2444,13 @@ function forgetSafetyFilters(body) {
|
|
|
1721
2444
|
return filters;
|
|
1722
2445
|
}
|
|
1723
2446
|
function forgetTargetExists(store, memoryId, filters) {
|
|
2447
|
+
if (!store.canMutateMemory(memoryId)) return false;
|
|
1724
2448
|
if (Object.keys(filters).length > 0) return store.timeline(null, memoryId, 0, 0, filters).some((row) => row.id === memoryId);
|
|
1725
2449
|
return store.get(memoryId) != null;
|
|
1726
2450
|
}
|
|
2451
|
+
function forgetMemoryForViewer(store, memoryId) {
|
|
2452
|
+
if (!store.forgetForUser(memoryId)) throw new Error("memory not found");
|
|
2453
|
+
}
|
|
1727
2454
|
//#endregion
|
|
1728
2455
|
//#region src/routes/memory-tools.ts
|
|
1729
2456
|
/**
|
|
@@ -1867,7 +2594,8 @@ function parseJsonBody(body) {
|
|
|
1867
2594
|
};
|
|
1868
2595
|
}
|
|
1869
2596
|
function rememberMemory(store, input) {
|
|
1870
|
-
|
|
2597
|
+
const createdMemories = [];
|
|
2598
|
+
const result = store.db.transaction(() => {
|
|
1871
2599
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1872
2600
|
const user = process.env.USER ?? "unknown";
|
|
1873
2601
|
const cwd = process.cwd();
|
|
@@ -1875,10 +2603,10 @@ function rememberMemory(store, input) {
|
|
|
1875
2603
|
project: input.project,
|
|
1876
2604
|
envProject: process.env.CODEMEM_PROJECT
|
|
1877
2605
|
});
|
|
1878
|
-
const sessionInfo = store.db.prepare(`INSERT INTO sessions(started_at, ended_at, cwd, project, user, tool_version, metadata_json)
|
|
1879
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(now, now, cwd, project, user, "viewer-api", toJson({ viewer: true }));
|
|
2606
|
+
const sessionInfo = store.db.prepare(`INSERT INTO sessions(started_at, ended_at, cwd, project, user, tool_version, metadata_json, import_key)
|
|
2607
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(now, now, cwd, project, user, "viewer-api", toJson({ viewer: true }), randomUUID());
|
|
1880
2608
|
const sessionId = Number(sessionInfo.lastInsertRowid);
|
|
1881
|
-
const memId = store.
|
|
2609
|
+
const memId = store.rememberForUser(sessionId, input.kind, input.title, input.body, input.confidence, void 0, void 0, { createdMemories });
|
|
1882
2610
|
if (!getMemoryForAccess(store, memId)) throw new Error("unauthorized_scope");
|
|
1883
2611
|
store.db.prepare("UPDATE sessions SET ended_at = ?, metadata_json = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), toJson({ viewer: true }), sessionId);
|
|
1884
2612
|
return {
|
|
@@ -1887,6 +2615,8 @@ function rememberMemory(store, input) {
|
|
|
1887
2615
|
body: input.body
|
|
1888
2616
|
};
|
|
1889
2617
|
})();
|
|
2618
|
+
store.enqueueCommittedVectorWrites(createdMemories);
|
|
2619
|
+
return result;
|
|
1890
2620
|
}
|
|
1891
2621
|
function readContextFile(path, displayPath, scope) {
|
|
1892
2622
|
if (!existsSync(path)) return null;
|
|
@@ -2052,7 +2782,7 @@ function memoryToolRoutes(getStore) {
|
|
|
2052
2782
|
project
|
|
2053
2783
|
});
|
|
2054
2784
|
try {
|
|
2055
|
-
await
|
|
2785
|
+
await store.flushPendingVectorWrites();
|
|
2056
2786
|
} catch {}
|
|
2057
2787
|
return c.json({ id: result.memId });
|
|
2058
2788
|
} catch (err) {
|
|
@@ -2333,6 +3063,18 @@ function requestedDbMatches(store, value) {
|
|
|
2333
3063
|
if (typeof value !== "string" || !value.trim()) return null;
|
|
2334
3064
|
return resolve(resolveDbPath(value.trim())) === resolve(store.dbPath);
|
|
2335
3065
|
}
|
|
3066
|
+
function implicitConfigPath() {
|
|
3067
|
+
const { resolved, fallbackChain } = resolveCodememConfigPath();
|
|
3068
|
+
if (resolved.source !== "env-codemem-config") return resolve(resolved.path);
|
|
3069
|
+
const candidates = fallbackChain.filter((candidate) => candidate.valid);
|
|
3070
|
+
const implicit = candidates.find((candidate) => candidate.exists) ?? candidates[0];
|
|
3071
|
+
return implicit ? resolve(implicit.path) : null;
|
|
3072
|
+
}
|
|
3073
|
+
function identityFieldMatches(key, requested, expected) {
|
|
3074
|
+
if (requested === expected) return true;
|
|
3075
|
+
if (key !== "config_path" || requested !== null && expected !== null) return false;
|
|
3076
|
+
return (requested ?? expected) === implicitConfigPath();
|
|
3077
|
+
}
|
|
2336
3078
|
function requestedIdentityMatches(value) {
|
|
2337
3079
|
if (value == null) return true;
|
|
2338
3080
|
if (!isRecord$1(value)) return null;
|
|
@@ -2343,7 +3085,7 @@ function requestedIdentityMatches(value) {
|
|
|
2343
3085
|
if (BOOLEAN_IDENTITY_TARGET_KEYS.has(key)) {
|
|
2344
3086
|
if (typeof requested !== "boolean") return null;
|
|
2345
3087
|
} else if (requested !== null && typeof requested !== "string") return null;
|
|
2346
|
-
if (requested
|
|
3088
|
+
if (!identityFieldMatches(key, requested, expected[key])) return false;
|
|
2347
3089
|
}
|
|
2348
3090
|
return true;
|
|
2349
3091
|
}
|
|
@@ -3619,11 +4361,7 @@ function chunkNumbers(values, chunkSize = 500) {
|
|
|
3619
4361
|
}
|
|
3620
4362
|
function visibleMemoryIdSet(store, memoryIds) {
|
|
3621
4363
|
if (memoryIds.size === 0) return /* @__PURE__ */ new Set();
|
|
3622
|
-
const filterResult = buildFilterClausesWithContext(null,
|
|
3623
|
-
actorId: store.actorId,
|
|
3624
|
-
deviceId: store.deviceId,
|
|
3625
|
-
enforceScopeVisibility: true
|
|
3626
|
-
});
|
|
4364
|
+
const filterResult = buildFilterClausesWithContext(null, store.ownershipFilterContext());
|
|
3627
4365
|
const visibleIds = /* @__PURE__ */ new Set();
|
|
3628
4366
|
for (const chunk of chunkNumbers([...memoryIds])) {
|
|
3629
4367
|
const clauses = [
|
|
@@ -3638,11 +4376,7 @@ function visibleMemoryIdSet(store, memoryIds) {
|
|
|
3638
4376
|
}
|
|
3639
4377
|
function visibleSessionIdSet(store, sessionIds) {
|
|
3640
4378
|
if (sessionIds.size === 0) return /* @__PURE__ */ new Set();
|
|
3641
|
-
const filterResult = buildFilterClausesWithContext(null,
|
|
3642
|
-
actorId: store.actorId,
|
|
3643
|
-
deviceId: store.deviceId,
|
|
3644
|
-
enforceScopeVisibility: true
|
|
3645
|
-
});
|
|
4379
|
+
const filterResult = buildFilterClausesWithContext(null, store.ownershipFilterContext());
|
|
3646
4380
|
const visibleIds = /* @__PURE__ */ new Set();
|
|
3647
4381
|
for (const chunk of chunkNumbers([...sessionIds])) {
|
|
3648
4382
|
const clauses = [
|
|
@@ -3680,8 +4414,20 @@ function usageRowVisible(row, visibility) {
|
|
|
3680
4414
|
}
|
|
3681
4415
|
function sanitizePackUsageMetadata(metadata, visibility) {
|
|
3682
4416
|
if (!metadata) return null;
|
|
3683
|
-
const sanitized =
|
|
3684
|
-
|
|
4417
|
+
const sanitized = MEMORY_ID_METADATA_KEYS.some((key) => {
|
|
4418
|
+
if (!Object.hasOwn(metadata, key)) return false;
|
|
4419
|
+
const ids = metadata[key];
|
|
4420
|
+
if (!Array.isArray(ids)) return true;
|
|
4421
|
+
return ids.some((value) => {
|
|
4422
|
+
if (typeof value !== "number" && typeof value !== "string") return true;
|
|
4423
|
+
const id = Number(value);
|
|
4424
|
+
return !Number.isInteger(id) || id <= 0 || !visibility.visibleMemoryIds.has(id);
|
|
4425
|
+
});
|
|
4426
|
+
}) ? {} : { ...metadata };
|
|
4427
|
+
for (const key of MEMORY_ID_METADATA_KEYS) {
|
|
4428
|
+
const ids = metadata[key];
|
|
4429
|
+
if (Array.isArray(ids)) sanitized[key] = metadataMemoryIds(ids.filter((value) => typeof value === "number" || typeof value === "string")).filter((memoryId) => visibility.visibleMemoryIds.has(memoryId));
|
|
4430
|
+
}
|
|
3685
4431
|
return sanitized;
|
|
3686
4432
|
}
|
|
3687
4433
|
/**
|
|
@@ -3740,15 +4486,15 @@ function usageAggregatePayload(store, project) {
|
|
|
3740
4486
|
};
|
|
3741
4487
|
}
|
|
3742
4488
|
/**
|
|
3743
|
-
* Short-lived cache for the
|
|
4489
|
+
* Short-lived cache for the unfiltered /api/usage aggregates only.
|
|
3744
4490
|
*
|
|
3745
4491
|
* The health dot polls /api/usage on every 5s refresh regardless of the
|
|
3746
4492
|
* active tab. The token/event totals come from indexed SQL aggregates and the
|
|
3747
|
-
* recent-pack window
|
|
3748
|
-
*
|
|
3749
|
-
*
|
|
3750
|
-
*
|
|
3751
|
-
*
|
|
4493
|
+
* recent-pack window must resolve current visibility on every request, even
|
|
4494
|
+
* on cache hits. Only the cumulative dashboard totals tolerate staleness.
|
|
4495
|
+
* Keyed by db path + scope identity + project filter so distinct
|
|
4496
|
+
* stores/projects never share an entry. Never cache recent packs or their
|
|
4497
|
+
* authorization: key, proof, policy, and ownership changes must apply now.
|
|
3752
4498
|
*/
|
|
3753
4499
|
var usagePayloadCache = /* @__PURE__ */ new Map();
|
|
3754
4500
|
var USAGE_PAYLOAD_CACHE_MS = 1e4;
|
|
@@ -3787,12 +4533,11 @@ function sweepUsagePayloadCache(cache, nowMs) {
|
|
|
3787
4533
|
/**
|
|
3788
4534
|
* Cheap fingerprint of the state that drives scope visibility
|
|
3789
4535
|
* (`scope_memberships` / `replication_scopes`). Folding this into the usage
|
|
3790
|
-
* cache key
|
|
3791
|
-
*
|
|
3792
|
-
*
|
|
3793
|
-
* small, so the aggregate is negligible next to the usage_events scan the
|
|
3794
|
-
* cache exists to avoid.
|
|
3795
|
-
* request bypasses the cache and recomputes visibility from scratch.
|
|
4536
|
+
* cache key preserves aggregate invalidation on membership/scope changes.
|
|
4537
|
+
* This fingerprint is not authorization: recent packs always use the current
|
|
4538
|
+
* ownership/scope filter independently of the aggregate cache. These tables
|
|
4539
|
+
* are small, so the aggregate is negligible next to the usage_events scan the
|
|
4540
|
+
* cache exists to avoid. Any error forces a unique value to bypass the cache.
|
|
3796
4541
|
*/
|
|
3797
4542
|
function scopeVisibilityGeneration(store) {
|
|
3798
4543
|
try {
|
|
@@ -3873,8 +4618,16 @@ function statsRoutes(getStore) {
|
|
|
3873
4618
|
const cacheKey = usageCacheKey(store, projectFilter);
|
|
3874
4619
|
const nowMs = Date.now();
|
|
3875
4620
|
const cached = usagePayloadCache.get(cacheKey);
|
|
3876
|
-
|
|
3877
|
-
const aggregates = usageAggregatePayload(store, projectFilter);
|
|
4621
|
+
const cacheHit = cached && nowMs < cached.expiresAtMs;
|
|
4622
|
+
const aggregates = cacheHit ? cached.payload : usageAggregatePayload(store, projectFilter);
|
|
4623
|
+
if (!cacheHit) {
|
|
4624
|
+
const setAtMs = Date.now();
|
|
4625
|
+
sweepUsagePayloadCache(usagePayloadCache, setAtMs);
|
|
4626
|
+
usagePayloadCache.set(cacheKey, {
|
|
4627
|
+
payload: aggregates,
|
|
4628
|
+
expiresAtMs: setAtMs + USAGE_PAYLOAD_CACHE_MS
|
|
4629
|
+
});
|
|
4630
|
+
}
|
|
3878
4631
|
const RECENT_PACK_SCAN_LIMIT = 200;
|
|
3879
4632
|
const loadRecentPackWindow = (project) => {
|
|
3880
4633
|
return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
|
|
@@ -3907,12 +4660,6 @@ function statsRoutes(getStore) {
|
|
|
3907
4660
|
totals_filtered: aggregates.totalsFiltered,
|
|
3908
4661
|
recent_packs: recentPacks
|
|
3909
4662
|
};
|
|
3910
|
-
const setAtMs = Date.now();
|
|
3911
|
-
sweepUsagePayloadCache(usagePayloadCache, setAtMs);
|
|
3912
|
-
usagePayloadCache.set(cacheKey, {
|
|
3913
|
-
payload,
|
|
3914
|
-
expiresAtMs: setAtMs + USAGE_PAYLOAD_CACHE_MS
|
|
3915
|
-
});
|
|
3916
4663
|
return c.json(payload);
|
|
3917
4664
|
}
|
|
3918
4665
|
});
|
|
@@ -3921,10 +4668,7 @@ function statsRoutes(getStore) {
|
|
|
3921
4668
|
function healthStats(store) {
|
|
3922
4669
|
return {
|
|
3923
4670
|
...store.stats(),
|
|
3924
|
-
automatic_recall: automaticRecallHealth(store.db,
|
|
3925
|
-
actorId: store.actorId,
|
|
3926
|
-
deviceId: store.deviceId
|
|
3927
|
-
})
|
|
4671
|
+
automatic_recall: automaticRecallHealth(store.db, store.ownershipFilterContext())
|
|
3928
4672
|
};
|
|
3929
4673
|
}
|
|
3930
4674
|
//#endregion
|
|
@@ -4542,7 +5286,7 @@ async function buildSyncStatusResponse(input) {
|
|
|
4542
5286
|
return response;
|
|
4543
5287
|
}
|
|
4544
5288
|
var PEERS_QUERY$1 = `
|
|
4545
|
-
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
|
|
5289
|
+
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.public_key, p.addresses_json,
|
|
4546
5290
|
p.last_seen_at, p.last_sync_at, p.last_error,
|
|
4547
5291
|
p.runtime_version, p.runtime_version_observed_at,
|
|
4548
5292
|
p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
|
|
@@ -4879,6 +5623,52 @@ var defaultSnapshotLoaderDependencies = {
|
|
|
4879
5623
|
function safeCoordinatorError() {
|
|
4880
5624
|
return /* @__PURE__ */ new Error("team_setup_roster_unavailable");
|
|
4881
5625
|
}
|
|
5626
|
+
var CoordinatorAdminConfigurationMissingError = class extends Error {
|
|
5627
|
+
constructor() {
|
|
5628
|
+
super("team_setup_roster_unavailable");
|
|
5629
|
+
this.name = "CoordinatorAdminConfigurationMissingError";
|
|
5630
|
+
}
|
|
5631
|
+
};
|
|
5632
|
+
function isPassthroughSnapshotLoadError(error) {
|
|
5633
|
+
return error instanceof CoordinatorAdminConfigurationMissingError || error instanceof Error && error.message === "legacy_team_setup_roster_too_large";
|
|
5634
|
+
}
|
|
5635
|
+
/**
|
|
5636
|
+
* Without coordinator admin access this Viewer cannot discover groups to
|
|
5637
|
+
* migrate, so the summary suppresses discovery. Members still see their Teams
|
|
5638
|
+
* from local records, and Devices reports incomplete coordinator setup. Saved,
|
|
5639
|
+
* unfinished migrations stay visible so they can be resumed once access
|
|
5640
|
+
* returns. Only the summary uses this; Team detail and finish stay fail-closed.
|
|
5641
|
+
*/
|
|
5642
|
+
function summaryWithoutAdminAccess(error, getStore, readConfig) {
|
|
5643
|
+
if (!(error instanceof CoordinatorAdminConfigurationMissingError)) throw error;
|
|
5644
|
+
const store = getStore();
|
|
5645
|
+
const config = readConfig();
|
|
5646
|
+
const eligibleCandidateRefs = new Set(legacyTeamCandidateGroupDescriptors(store, config.syncCoordinatorUrl, config.syncCoordinatorGroups).map(({ coordinatorId, groupId }) => legacyTeamCandidateId(coordinatorId, groupId)));
|
|
5647
|
+
if (eligibleCandidateRefs.size === 0) return [];
|
|
5648
|
+
const eligibleRefs = [...eligibleCandidateRefs];
|
|
5649
|
+
const placeholders = eligibleRefs.map(() => "?").join(", ");
|
|
5650
|
+
return store.db.prepare(`SELECT DISTINCT coordinator_id, group_id FROM legacy_team_setup_drafts
|
|
5651
|
+
WHERE candidate_id IN (${placeholders})
|
|
5652
|
+
ORDER BY coordinator_id, group_id`).all(...eligibleRefs).flatMap((row) => {
|
|
5653
|
+
const candidateRef = legacyTeamCandidateId(row.coordinator_id, row.group_id);
|
|
5654
|
+
if (!eligibleCandidateRefs.has(candidateRef)) return [];
|
|
5655
|
+
const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
5656
|
+
if (!draft || draft.state === "completed" || hasCompletedLegacyTeamSetup(store, candidateRef)) return [];
|
|
5657
|
+
return [{
|
|
5658
|
+
coordinatorId: row.coordinator_id,
|
|
5659
|
+
groupId: row.group_id,
|
|
5660
|
+
displayName: draft.displayName,
|
|
5661
|
+
devices: []
|
|
5662
|
+
}];
|
|
5663
|
+
}).slice(0, MAX_CONFIGURED_GROUPS);
|
|
5664
|
+
}
|
|
5665
|
+
function loadSummarySnapshots(load, getStore, readConfig) {
|
|
5666
|
+
return () => load().catch((error) => summaryWithoutAdminAccess(error, getStore, readConfig));
|
|
5667
|
+
}
|
|
5668
|
+
function createLoadedSummarySnapshotCache(load, options) {
|
|
5669
|
+
const readConfig = options.readCoordinatorConfig ?? options.snapshotLoaderDependencies?.readConfig ?? readCoordinatorSyncConfig;
|
|
5670
|
+
return createCachedSnapshotLoader(loadSummarySnapshots(load, options.getStore, readConfig), SUMMARY_SNAPSHOT_CACHE_TTL_MS);
|
|
5671
|
+
}
|
|
4882
5672
|
function isCoordinatorRosterTooLargeError(error) {
|
|
4883
5673
|
return error instanceof Error && error.message === "coordinator_response_too_large";
|
|
4884
5674
|
}
|
|
@@ -5028,7 +5818,8 @@ async function loadConfiguredLegacyTeamGroupSnapshotsWith(dependencies, options
|
|
|
5028
5818
|
const hasUrl = Boolean(config.syncCoordinatorUrl);
|
|
5029
5819
|
const hasSecret = Boolean(config.syncCoordinatorAdminSecret);
|
|
5030
5820
|
if (!hasUrl && !hasSecret && config.syncCoordinatorGroups.length === 0) return [];
|
|
5031
|
-
if (
|
|
5821
|
+
if (hasUrl && !hasSecret) throw new CoordinatorAdminConfigurationMissingError();
|
|
5822
|
+
if (!hasUrl) throw safeCoordinatorError();
|
|
5032
5823
|
try {
|
|
5033
5824
|
const configuredCoordinatorId = buildBaseUrl(config.syncCoordinatorUrl);
|
|
5034
5825
|
if (!configuredCoordinatorId) throw safeCoordinatorError();
|
|
@@ -5100,7 +5891,7 @@ async function loadConfiguredLegacyTeamGroupSnapshotsWith(dependencies, options
|
|
|
5100
5891
|
if (validSnapshots.length === 0 && outcomes.some((outcome) => outcome.kind === "unavailable")) throw safeCoordinatorError();
|
|
5101
5892
|
return validSnapshots;
|
|
5102
5893
|
} catch (error) {
|
|
5103
|
-
if (error
|
|
5894
|
+
if (isPassthroughSnapshotLoadError(error)) throw error;
|
|
5104
5895
|
throw safeCoordinatorError();
|
|
5105
5896
|
}
|
|
5106
5897
|
}
|
|
@@ -5762,11 +6553,11 @@ function teamSetupRoutes(options) {
|
|
|
5762
6553
|
deadlineMs
|
|
5763
6554
|
} : void 0);
|
|
5764
6555
|
} catch (error) {
|
|
5765
|
-
if (error
|
|
6556
|
+
if (isPassthroughSnapshotLoadError(error)) throw error;
|
|
5766
6557
|
throw safeCoordinatorError();
|
|
5767
6558
|
}
|
|
5768
6559
|
}
|
|
5769
|
-
const loadedSummarySnapshots =
|
|
6560
|
+
const loadedSummarySnapshots = createLoadedSummarySnapshotCache(loadedSnapshots, options);
|
|
5770
6561
|
options.registerSummaryInvalidator?.(() => loadedSummarySnapshots.invalidate());
|
|
5771
6562
|
const completionDependencies = options.completionDependencies === void 0 ? defaultCompletionDependencies : options.completionDependencies;
|
|
5772
6563
|
/**
|
|
@@ -6760,7 +7551,7 @@ async function maybeGrantDefaultSpaceOnJoin(opts) {
|
|
|
6760
7551
|
adminSecret: opts.config.syncCoordinatorAdminSecret || null
|
|
6761
7552
|
});
|
|
6762
7553
|
}
|
|
6763
|
-
function configuredGroups(config) {
|
|
7554
|
+
function configuredGroups$1(config) {
|
|
6764
7555
|
const rawGroups = config.sync_coordinator_groups;
|
|
6765
7556
|
if (Array.isArray(rawGroups)) return rawGroups.map((group) => String(group).trim()).filter(Boolean);
|
|
6766
7557
|
if (typeof rawGroups === "string") return rawGroups.split(",").map((group) => group.trim()).filter(Boolean);
|
|
@@ -6772,7 +7563,7 @@ function removeConfiguredCoordinatorGroup(groupId) {
|
|
|
6772
7563
|
if (!targetGroup) return [];
|
|
6773
7564
|
let nextGroups = [];
|
|
6774
7565
|
mutateCodememConfigFile((config) => {
|
|
6775
|
-
const groups = configuredGroups(config);
|
|
7566
|
+
const groups = configuredGroups$1(config);
|
|
6776
7567
|
nextGroups = groups.filter((group) => group !== targetGroup);
|
|
6777
7568
|
const configuredGroup = String(config.sync_coordinator_group ?? "").trim();
|
|
6778
7569
|
const removesPluralGroup = nextGroups.length !== groups.length;
|
|
@@ -7295,7 +8086,7 @@ async function reconcileProjectInviteAcceptance(store, operationId) {
|
|
|
7295
8086
|
const owner = store.db.prepare("SELECT inviter_actor_id FROM share_operations WHERE operation_id = ?").pluck().get(operationId);
|
|
7296
8087
|
if (!owner) throw new Error("operation_not_found");
|
|
7297
8088
|
if (owner !== store.actorId) throw new Error("operation_scope_mismatch");
|
|
7298
|
-
|
|
8089
|
+
requireLocalActorRecord(store);
|
|
7299
8090
|
const { groupId, payload } = await coordinatorProjectInvitePayload(operationId);
|
|
7300
8091
|
if (payload.state === "waiting_for_acceptance") return {
|
|
7301
8092
|
accepted: false,
|
|
@@ -7329,7 +8120,7 @@ async function reconcileProjectInviteAcceptance(store, operationId) {
|
|
|
7329
8120
|
throw error;
|
|
7330
8121
|
}
|
|
7331
8122
|
const projects = parseAcceptedProjectIntent(payload.projects);
|
|
7332
|
-
|
|
8123
|
+
commitProjectInviteAcceptance(store, {
|
|
7333
8124
|
operationId,
|
|
7334
8125
|
localInviterActorId: store.actorId,
|
|
7335
8126
|
coordinatorGroupId: groupId,
|
|
@@ -7350,6 +8141,12 @@ async function reconcileProjectInviteAcceptance(store, operationId) {
|
|
|
7350
8141
|
payload
|
|
7351
8142
|
};
|
|
7352
8143
|
}
|
|
8144
|
+
function commitProjectInviteAcceptance(store, input) {
|
|
8145
|
+
store.db.transaction(() => {
|
|
8146
|
+
store.assertPendingLocalCaptureAdoptionActors();
|
|
8147
|
+
reconcileShareOperationAcceptance(store.db, input);
|
|
8148
|
+
}).immediate();
|
|
8149
|
+
}
|
|
7353
8150
|
async function shareOperationReadModels(store, operationId, includeInviteLinks = true) {
|
|
7354
8151
|
const rows = store.db.prepare(`SELECT o.operation_id, o.state, o.person_id, o.teammate_name,
|
|
7355
8152
|
o.recipient_actor_id, o.recipient_display_name, o.recipient_device_id,
|
|
@@ -7529,6 +8326,10 @@ function errorStatus(error) {
|
|
|
7529
8326
|
function projectInviteOwnerErrorResponse(error, fallback) {
|
|
7530
8327
|
const message = error instanceof Error ? error.message : "";
|
|
7531
8328
|
const status = errorStatus(error);
|
|
8329
|
+
if (isPendingCaptureActorConflict(error) && (status == null || status === 409)) return {
|
|
8330
|
+
code: message,
|
|
8331
|
+
status: 409
|
|
8332
|
+
};
|
|
7532
8333
|
if (message === "operation_not_found" && (status == null || status === 404)) return {
|
|
7533
8334
|
code: message,
|
|
7534
8335
|
status: 404
|
|
@@ -8003,6 +8804,14 @@ function unauthorizedPayload() {
|
|
|
8003
8804
|
return { error: "unauthorized" };
|
|
8004
8805
|
}
|
|
8005
8806
|
var SYNC_AUTH_STORE_BUSY_REASON = "sync_auth_store_busy";
|
|
8807
|
+
function authenticatedSyncPeer(deviceId, publicKey) {
|
|
8808
|
+
return {
|
|
8809
|
+
ok: true,
|
|
8810
|
+
reason: "ok",
|
|
8811
|
+
deviceId,
|
|
8812
|
+
authenticatedPeerSigningKey: publicKey
|
|
8813
|
+
};
|
|
8814
|
+
}
|
|
8006
8815
|
var DIRECT_PEER_SIGNATURE_VERSION_NUMBER = 3;
|
|
8007
8816
|
function logDirectPeerAuthDiagnostic(...reasons) {
|
|
8008
8817
|
const reason = reasons.find((candidate) => candidate === "recipient_mismatch" || candidate === "signature_downgrade");
|
|
@@ -8173,11 +8982,39 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
8173
8982
|
const versionResult = recordDirectPeerSignatureVersion(store, deviceId, verification.version);
|
|
8174
8983
|
if (versionResult) return versionResult;
|
|
8175
8984
|
}
|
|
8176
|
-
return
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8985
|
+
return authenticatedSyncPeer(deviceId, publicKey);
|
|
8986
|
+
}
|
|
8987
|
+
function bootstrapPayloadRecord(value) {
|
|
8988
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8989
|
+
}
|
|
8990
|
+
function bootstrapPayloadText(value) {
|
|
8991
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\0");
|
|
8992
|
+
}
|
|
8993
|
+
function bootstrapNullableText(value) {
|
|
8994
|
+
return value === null || typeof value === "string";
|
|
8995
|
+
}
|
|
8996
|
+
function validBootstrapEnrollment(value) {
|
|
8997
|
+
if (!bootstrapPayloadRecord(value)) return false;
|
|
8998
|
+
return bootstrapPayloadText(value.group_id) && bootstrapPayloadText(value.device_id) && bootstrapPayloadText(value.public_key) && bootstrapPayloadText(value.fingerprint) && bootstrapNullableText(value.identity_id) && bootstrapNullableText(value.display_name) && value.enabled === 1 && bootstrapPayloadText(value.created_at) && fingerprintPublicKey(value.public_key) === value.fingerprint;
|
|
8999
|
+
}
|
|
9000
|
+
function validBootstrapVerification(value, grantId) {
|
|
9001
|
+
if (!bootstrapPayloadRecord(value) || value.authorization_version !== 1) return false;
|
|
9002
|
+
const grant = value.grant;
|
|
9003
|
+
if (!bootstrapPayloadRecord(grant)) return false;
|
|
9004
|
+
if (grant.grant_id !== grantId || !bootstrapPayloadText(grant.group_id) || !bootstrapPayloadText(grant.seed_device_id) || !bootstrapPayloadText(grant.worker_device_id) || !bootstrapPayloadText(grant.created_at) || !bootstrapNullableText(grant.created_by) || grant.revoked_at !== null || !Number.isFinite(parseBootstrapExpiry(grant.expires_at))) return false;
|
|
9005
|
+
const seed = value.seed_enrollment;
|
|
9006
|
+
const worker = value.worker_enrollment;
|
|
9007
|
+
if (!validBootstrapEnrollment(seed) || !validBootstrapEnrollment(worker)) return false;
|
|
9008
|
+
return seed.device_id === grant.seed_device_id && worker.device_id === grant.worker_device_id && seed.group_id === grant.group_id && worker.group_id === grant.group_id;
|
|
9009
|
+
}
|
|
9010
|
+
function bootstrapSeedMatchesLocalIdentity(store, seed) {
|
|
9011
|
+
try {
|
|
9012
|
+
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
9013
|
+
const publicKey = loadPublicKey(syncKeysDir());
|
|
9014
|
+
return deviceId === seed.device_id && publicKey === seed.public_key && fingerprint === seed.fingerprint && publicKey !== null && fingerprintPublicKey(publicKey) === seed.fingerprint;
|
|
9015
|
+
} catch {
|
|
9016
|
+
return false;
|
|
9017
|
+
}
|
|
8181
9018
|
}
|
|
8182
9019
|
async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
8183
9020
|
const grantId = (request.header("X-Codemem-Bootstrap-Grant") ?? "").trim();
|
|
@@ -8196,9 +9033,10 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
8196
9033
|
reason: "bootstrap_grant_coordinator_not_configured",
|
|
8197
9034
|
deviceId
|
|
8198
9035
|
};
|
|
8199
|
-
|
|
9036
|
+
let localDeviceId;
|
|
8200
9037
|
let verification;
|
|
8201
9038
|
try {
|
|
9039
|
+
[localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
8202
9040
|
let payload = null;
|
|
8203
9041
|
if (config.syncCoordinatorAdminSecret) {
|
|
8204
9042
|
const [status, adminPayload] = await requestJson("GET", `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/admin/bootstrap-grants/${encodeURIComponent(grantId)}`, {
|
|
@@ -8229,6 +9067,11 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
8229
9067
|
reason: "bootstrap_grant_lookup_failed",
|
|
8230
9068
|
deviceId
|
|
8231
9069
|
};
|
|
9070
|
+
if (!validBootstrapVerification(payload, grantId)) return {
|
|
9071
|
+
ok: false,
|
|
9072
|
+
reason: "bootstrap_grant_invalid_payload",
|
|
9073
|
+
deviceId
|
|
9074
|
+
};
|
|
8232
9075
|
verification = payload;
|
|
8233
9076
|
} catch {
|
|
8234
9077
|
return {
|
|
@@ -8238,44 +9081,15 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
8238
9081
|
};
|
|
8239
9082
|
}
|
|
8240
9083
|
const grant = verification.grant;
|
|
9084
|
+
const seedEnrollment = verification.seed_enrollment;
|
|
8241
9085
|
const workerEnrollment = verification.worker_enrollment;
|
|
8242
|
-
if (
|
|
8243
|
-
ok: false,
|
|
8244
|
-
reason: "bootstrap_grant_invalid_payload",
|
|
8245
|
-
deviceId
|
|
8246
|
-
};
|
|
8247
|
-
if (config.syncCoordinatorGroups.length > 0 && !config.syncCoordinatorGroups.includes(String(grant.group_id))) return {
|
|
9086
|
+
if (config.syncCoordinatorGroups.length > 0 && !config.syncCoordinatorGroups.includes(grant.group_id)) return {
|
|
8248
9087
|
ok: false,
|
|
8249
9088
|
reason: "bootstrap_grant_group_mismatch",
|
|
8250
9089
|
deviceId
|
|
8251
9090
|
};
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
reason: "bootstrap_grant_revoked",
|
|
8255
|
-
deviceId
|
|
8256
|
-
};
|
|
8257
|
-
if (String(workerEnrollment.device_id) !== String(grant.worker_device_id)) return {
|
|
8258
|
-
ok: false,
|
|
8259
|
-
reason: "bootstrap_grant_worker_enrollment_mismatch",
|
|
8260
|
-
deviceId
|
|
8261
|
-
};
|
|
8262
|
-
if (String(workerEnrollment.group_id) !== String(grant.group_id)) return {
|
|
8263
|
-
ok: false,
|
|
8264
|
-
reason: "bootstrap_grant_group_mismatch",
|
|
8265
|
-
deviceId
|
|
8266
|
-
};
|
|
8267
|
-
if (Number(workerEnrollment.enabled) !== 1) return {
|
|
8268
|
-
ok: false,
|
|
8269
|
-
reason: "bootstrap_grant_worker_disabled",
|
|
8270
|
-
deviceId
|
|
8271
|
-
};
|
|
8272
|
-
const workerPublicKey = String(workerEnrollment.public_key ?? "").trim();
|
|
8273
|
-
const workerFingerprint = String(workerEnrollment.fingerprint ?? "").trim();
|
|
8274
|
-
if (!workerPublicKey || fingerprintPublicKey(workerPublicKey) !== workerFingerprint) return {
|
|
8275
|
-
ok: false,
|
|
8276
|
-
reason: "bootstrap_grant_worker_identity_invalid",
|
|
8277
|
-
deviceId
|
|
8278
|
-
};
|
|
9091
|
+
const workerPublicKey = workerEnrollment.public_key;
|
|
9092
|
+
const workerFingerprint = workerEnrollment.fingerprint;
|
|
8279
9093
|
if (grant.worker_device_id !== deviceId) return {
|
|
8280
9094
|
ok: false,
|
|
8281
9095
|
reason: "bootstrap_grant_worker_mismatch",
|
|
@@ -8286,11 +9100,16 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
8286
9100
|
reason: "bootstrap_grant_seed_mismatch",
|
|
8287
9101
|
deviceId
|
|
8288
9102
|
};
|
|
8289
|
-
if (
|
|
9103
|
+
if (parseBootstrapExpiry(grant.expires_at) <= Date.now()) return {
|
|
8290
9104
|
ok: false,
|
|
8291
9105
|
reason: "bootstrap_grant_expired",
|
|
8292
9106
|
deviceId
|
|
8293
9107
|
};
|
|
9108
|
+
if (!bootstrapSeedMatchesLocalIdentity(store, seedEnrollment)) return {
|
|
9109
|
+
ok: false,
|
|
9110
|
+
reason: "bootstrap_grant_seed_identity_invalid",
|
|
9111
|
+
deviceId
|
|
9112
|
+
};
|
|
8294
9113
|
const existingPeer = store.db.prepare(`SELECT highest_observed_direct_signature_version
|
|
8295
9114
|
FROM sync_peer_signature_state WHERE peer_device_id = ? LIMIT 1`).get(deviceId);
|
|
8296
9115
|
let signatureVerification;
|
|
@@ -8321,7 +9140,7 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
8321
9140
|
const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
|
|
8322
9141
|
if (nonceResult) return nonceResult;
|
|
8323
9142
|
updatePeerAddresses(store.db, deviceId, [], {
|
|
8324
|
-
name:
|
|
9143
|
+
name: workerEnrollment.display_name?.trim() || void 0,
|
|
8325
9144
|
pinnedFingerprint: workerFingerprint,
|
|
8326
9145
|
publicKey: workerPublicKey,
|
|
8327
9146
|
replaceTrust: true
|
|
@@ -8330,11 +9149,7 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
8330
9149
|
const versionResult = recordDirectPeerSignatureVersion(store, deviceId, signatureVerification.version);
|
|
8331
9150
|
if (versionResult) return versionResult;
|
|
8332
9151
|
}
|
|
8333
|
-
return
|
|
8334
|
-
ok: true,
|
|
8335
|
-
reason: "ok",
|
|
8336
|
-
deviceId
|
|
8337
|
-
};
|
|
9152
|
+
return authenticatedSyncPeer(deviceId, workerPublicKey);
|
|
8338
9153
|
}
|
|
8339
9154
|
function parseJsonList(value) {
|
|
8340
9155
|
if (value == null) return [];
|
|
@@ -8409,7 +9224,23 @@ function peerAuthorizedScopes(store, peerDeviceId) {
|
|
|
8409
9224
|
updated_at: cleanPeerScopeText(row.updated_at)
|
|
8410
9225
|
}));
|
|
8411
9226
|
}
|
|
9227
|
+
function isPendingCaptureActorConflict(error) {
|
|
9228
|
+
return error instanceof Error && error.constructor === Error && error.message === "device_adoption_actor_conflict";
|
|
9229
|
+
}
|
|
9230
|
+
/** False leaves the conflict untouched; callers must check before claiming or merging identity. */
|
|
8412
9231
|
function ensureLocalActorRecord(store) {
|
|
9232
|
+
return store.db.transaction(() => {
|
|
9233
|
+
try {
|
|
9234
|
+
store.assertPendingLocalCaptureAdoptionActors();
|
|
9235
|
+
} catch (error) {
|
|
9236
|
+
if (isPendingCaptureActorConflict(error)) return false;
|
|
9237
|
+
throw error;
|
|
9238
|
+
}
|
|
9239
|
+
persistLocalActorRecord(store);
|
|
9240
|
+
return true;
|
|
9241
|
+
}).immediate();
|
|
9242
|
+
}
|
|
9243
|
+
function persistLocalActorRecord(store) {
|
|
8413
9244
|
const d = drizzle(store.db, { schema });
|
|
8414
9245
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8415
9246
|
d.update(schema.actors).set({
|
|
@@ -8437,7 +9268,16 @@ function ensureLocalActorRecord(store) {
|
|
|
8437
9268
|
updated_at: now
|
|
8438
9269
|
}).run();
|
|
8439
9270
|
}
|
|
9271
|
+
function requireLocalActorRecord(store) {
|
|
9272
|
+
if (!ensureLocalActorRecord(store)) throw new Error("device_adoption_actor_conflict");
|
|
9273
|
+
}
|
|
8440
9274
|
function ensureStableStoreIdentity(store) {
|
|
9275
|
+
const deviceId = ensureStableStoreDeviceIdentity(store);
|
|
9276
|
+
requireLocalActorRecord(store);
|
|
9277
|
+
return deviceId;
|
|
9278
|
+
}
|
|
9279
|
+
/** Preserve normal actor setup for reads, without repairing a pending conflict. */
|
|
9280
|
+
function ensureStableStoreIdentityForRead(store) {
|
|
8441
9281
|
const deviceId = ensureStableStoreDeviceIdentity(store);
|
|
8442
9282
|
ensureLocalActorRecord(store);
|
|
8443
9283
|
return deviceId;
|
|
@@ -8749,7 +9589,7 @@ function missingArchivedCoordinatorGroupResponse(groupId, status, groups) {
|
|
|
8749
9589
|
* When showDiag is false, sensitive fields (fingerprint, last_error, addresses)
|
|
8750
9590
|
* are redacted.
|
|
8751
9591
|
*/
|
|
8752
|
-
function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer, localDeviceId) {
|
|
9592
|
+
function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer, localDeviceId, loadDiagnosticKey) {
|
|
8753
9593
|
const peerId = String(row.peer_device_id ?? "");
|
|
8754
9594
|
const recentOps = recentOpsByPeer?.get(peerId) ?? {
|
|
8755
9595
|
in: 0,
|
|
@@ -8759,7 +9599,8 @@ function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer
|
|
|
8759
9599
|
const addresses = safeJsonList(row.addresses_json);
|
|
8760
9600
|
const perScopeSync = listPerPeerScopeSyncState(store.db, {
|
|
8761
9601
|
localDeviceId: localDeviceId ?? null,
|
|
8762
|
-
peerDeviceId: peerId
|
|
9602
|
+
peerDeviceId: peerId,
|
|
9603
|
+
...peerScopeDiagnosticKeys(store, row, localDeviceId, loadDiagnosticKey)
|
|
8763
9604
|
});
|
|
8764
9605
|
return {
|
|
8765
9606
|
peer_device_id: row.peer_device_id,
|
|
@@ -8821,6 +9662,16 @@ function recentScopeRejectionsByPeer(store) {
|
|
|
8821
9662
|
}
|
|
8822
9663
|
return map;
|
|
8823
9664
|
}
|
|
9665
|
+
function staleRowsForCleanupDiagnostics(store, localDeviceId) {
|
|
9666
|
+
return diagnoseStalePeerReceivedRows(store.db, {
|
|
9667
|
+
localDeviceId,
|
|
9668
|
+
keysDir: syncKeysDir(),
|
|
9669
|
+
maxRows: CLEANUP_DIAGNOSTICS_MAX_ROWS
|
|
9670
|
+
});
|
|
9671
|
+
}
|
|
9672
|
+
function applyInboundSyncOps(store, ops, localDeviceId) {
|
|
9673
|
+
return applyReplicationOps(store.db, ops, localDeviceId, store.scanner, { keysDir: syncKeysDir() });
|
|
9674
|
+
}
|
|
8824
9675
|
function cleanupDiagnostics(store, localDeviceId, showDiag) {
|
|
8825
9676
|
const opRows = store.db.prepare(`SELECT
|
|
8826
9677
|
SUM(CASE WHEN device_id = ? THEN 1 ELSE 0 END) AS source_authored,
|
|
@@ -8828,10 +9679,7 @@ function cleanupDiagnostics(store, localDeviceId, showDiag) {
|
|
|
8828
9679
|
MAX(created_at) AS latest_at
|
|
8829
9680
|
FROM replication_ops
|
|
8830
9681
|
WHERE op_type = ?`).get(localDeviceId ?? "", localDeviceId ?? "", ACCESS_CLEANUP_OP_TYPE);
|
|
8831
|
-
const stale = localDeviceId && showDiag ?
|
|
8832
|
-
localDeviceId,
|
|
8833
|
-
maxRows: CLEANUP_DIAGNOSTICS_MAX_ROWS
|
|
8834
|
-
}) : {
|
|
9682
|
+
const stale = localDeviceId && showDiag ? staleRowsForCleanupDiagnostics(store, localDeviceId) : {
|
|
8835
9683
|
ambiguous: [],
|
|
8836
9684
|
checked: 0,
|
|
8837
9685
|
retained: 0,
|
|
@@ -9114,8 +9962,8 @@ function forgetProjectInventoryLocalMemories(store, input) {
|
|
|
9114
9962
|
};
|
|
9115
9963
|
if (input.confirmationToken !== confirmationToken) throw new Error("project memories changed before cleanup; refresh and try again");
|
|
9116
9964
|
store.db.transaction(() => {
|
|
9117
|
-
for (const row of localRows) store.
|
|
9118
|
-
})();
|
|
9965
|
+
for (const row of localRows) if (!store.forgetForUser(row.id)) throw new Error("unauthorized_scope");
|
|
9966
|
+
}).immediate();
|
|
9119
9967
|
return {
|
|
9120
9968
|
confirmed: true,
|
|
9121
9969
|
forgotten_memory_count: localRows.length,
|
|
@@ -9132,7 +9980,7 @@ function assertLocalDeviceScopeMembership(store, scopeId) {
|
|
|
9132
9980
|
AND sm.status = 'active'
|
|
9133
9981
|
AND rs.status = 'active'
|
|
9134
9982
|
AND sm.membership_epoch >= rs.membership_epoch
|
|
9135
|
-
LIMIT 1`).get(scopeId, store.deviceId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
|
|
9983
|
+
LIMIT 1`).get(scopeId, store.deviceId) || !store.isScopeWritable(scopeId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
|
|
9136
9984
|
}
|
|
9137
9985
|
function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
|
|
9138
9986
|
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
@@ -9195,18 +10043,19 @@ function legacySharedReviewReassignmentPreview(store, input) {
|
|
|
9195
10043
|
};
|
|
9196
10044
|
}
|
|
9197
10045
|
function reassignLegacySharedReviewGroup(store, input) {
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
10046
|
+
return store.db.transaction(() => {
|
|
10047
|
+
const preview = legacySharedReviewReassignmentPreview(store, input);
|
|
10048
|
+
if (input.confirmationToken !== preview.confirmation_token) throw new Error("legacy shared review group changed before reassignment; refresh and try again");
|
|
10049
|
+
const rows = reassignableLegacySharedReviewRows(store, legacySharedReviewRowsForWorkspace(store, input.workspaceIdentity));
|
|
10050
|
+
assertLocalDeviceScopeMembership(store, input.scopeId);
|
|
9202
10051
|
for (const row of rows) store.reassignMemoryScope(row.id, input.scopeId);
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
};
|
|
10052
|
+
return {
|
|
10053
|
+
ok: true,
|
|
10054
|
+
...preview,
|
|
10055
|
+
reassigned_memory_count: rows.length,
|
|
10056
|
+
legacy_shared_review: legacySharedReviewSummary(store)
|
|
10057
|
+
};
|
|
10058
|
+
}).immediate();
|
|
9210
10059
|
}
|
|
9211
10060
|
var SYNC_DIRECTION_WINDOW_SECONDS = 86400;
|
|
9212
10061
|
function recentPeerOps(store) {
|
|
@@ -9281,7 +10130,7 @@ function readViewerBinding(dbPath) {
|
|
|
9281
10130
|
return null;
|
|
9282
10131
|
}
|
|
9283
10132
|
var PEERS_QUERY = `
|
|
9284
|
-
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
|
|
10133
|
+
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.public_key, p.addresses_json,
|
|
9285
10134
|
p.last_seen_at, p.last_sync_at, p.last_error,
|
|
9286
10135
|
p.runtime_version, p.runtime_version_observed_at,
|
|
9287
10136
|
p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
|
|
@@ -9318,15 +10167,116 @@ function negotiatedSyncCapability(c) {
|
|
|
9318
10167
|
* network-accessible. All requests are auth-gated via signature
|
|
9319
10168
|
* verification so unauthenticated callers are rejected.
|
|
9320
10169
|
*/
|
|
9321
|
-
function
|
|
10170
|
+
function syncScopeAuthorizationContext(store, auth, localDeviceId) {
|
|
10171
|
+
return {
|
|
10172
|
+
db: store.db,
|
|
10173
|
+
localDeviceId,
|
|
10174
|
+
peerDeviceId: auth.deviceId,
|
|
10175
|
+
localSigningPublicKey: loadPublicKey(syncKeysDir()),
|
|
10176
|
+
authenticatedPeerSigningKey: auth.authenticatedPeerSigningKey
|
|
10177
|
+
};
|
|
10178
|
+
}
|
|
10179
|
+
function peerScopeDiagnosticKeys(store, row, localDeviceId, loadDiagnosticKey) {
|
|
10180
|
+
return {
|
|
10181
|
+
localSigningPublicKey: loadDiagnosticKey?.(store, localDeviceId) ?? null,
|
|
10182
|
+
authenticatedPeerSigningKey: typeof row.public_key === "string" ? row.public_key : null
|
|
10183
|
+
};
|
|
10184
|
+
}
|
|
10185
|
+
function createPeerScopeDiagnosticKeyLoader() {
|
|
10186
|
+
let publicKey;
|
|
10187
|
+
return (store, localDeviceId) => {
|
|
10188
|
+
if (publicKey !== void 0) return publicKey;
|
|
10189
|
+
publicKey = null;
|
|
10190
|
+
if (!localDeviceId) return null;
|
|
10191
|
+
if (!store.db.prepare(`SELECT 1 FROM scope_memberships m
|
|
10192
|
+
JOIN replication_scopes s ON s.scope_id = m.scope_id
|
|
10193
|
+
WHERE m.device_id = ? AND m.status = 'active'
|
|
10194
|
+
AND s.authority_type = 'coordinator' LIMIT 1`).get(localDeviceId)) return null;
|
|
10195
|
+
publicKey = loadRuntimeSigningPublicKey(store.db, {
|
|
10196
|
+
deviceId: localDeviceId,
|
|
10197
|
+
keysDir: syncKeysDir()
|
|
10198
|
+
});
|
|
10199
|
+
return publicKey;
|
|
10200
|
+
};
|
|
10201
|
+
}
|
|
10202
|
+
function readPeerScopeDiagnostics(store, showDiag) {
|
|
10203
|
+
const rows = store.db.prepare(PEERS_QUERY).all();
|
|
10204
|
+
const recentOpsByPeer = recentPeerOps(store);
|
|
10205
|
+
const scopeRejectionsByPeer = recentScopeRejectionsByPeer(store);
|
|
10206
|
+
const localDeviceId = drizzle(store.db, { schema }).select({ device_id: schema.syncDevice.device_id }).from(schema.syncDevice).limit(1).get()?.device_id ?? null;
|
|
10207
|
+
const loadDiagnosticKey = createPeerScopeDiagnosticKeyLoader();
|
|
10208
|
+
return rows.map((row) => mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer, localDeviceId, loadDiagnosticKey));
|
|
10209
|
+
}
|
|
10210
|
+
function validateManagedScopes(scopeIds, context) {
|
|
10211
|
+
for (const scopeId of scopeIds) {
|
|
10212
|
+
if (!scopeId || scopeId === LOCAL_DEFAULT_SCOPE_ID) continue;
|
|
10213
|
+
const localAuthorization = getEffectiveCachedScopeAuthorization(context.db, {
|
|
10214
|
+
deviceId: context.localDeviceId,
|
|
10215
|
+
scopeId,
|
|
10216
|
+
expectedPublicKey: context.localSigningPublicKey?.trim() ?? ""
|
|
10217
|
+
});
|
|
10218
|
+
const authority = localAuthorization.scope?.authority_type;
|
|
10219
|
+
if (authority === "manual" || authority === "invite" || authority === "local") continue;
|
|
10220
|
+
if (authority !== "coordinator") return {
|
|
10221
|
+
scopeId,
|
|
10222
|
+
reason: "missing_scope"
|
|
10223
|
+
};
|
|
10224
|
+
const localReason = scopeAuthorizationFailureReason(localAuthorization);
|
|
10225
|
+
if (localReason) return {
|
|
10226
|
+
scopeId,
|
|
10227
|
+
reason: localReason
|
|
10228
|
+
};
|
|
10229
|
+
const peerAuthorization = getEffectiveCachedScopeAuthorization(context.db, {
|
|
10230
|
+
deviceId: context.peerDeviceId,
|
|
10231
|
+
scopeId,
|
|
10232
|
+
expectedPublicKey: context.authenticatedPeerSigningKey.trim()
|
|
10233
|
+
});
|
|
10234
|
+
const peerReason = scopeAuthorizationFailureReason(peerAuthorization);
|
|
10235
|
+
if (peerReason) return {
|
|
10236
|
+
scopeId,
|
|
10237
|
+
reason: peerReason
|
|
10238
|
+
};
|
|
10239
|
+
}
|
|
10240
|
+
return null;
|
|
10241
|
+
}
|
|
10242
|
+
function addExistingMutationScopes(db, ops, scopeIds) {
|
|
10243
|
+
const existingScopes = db.prepare("SELECT DISTINCT scope_id FROM memory_items WHERE import_key = ?");
|
|
10244
|
+
for (const op of ops) {
|
|
10245
|
+
if (![
|
|
10246
|
+
"upsert",
|
|
10247
|
+
"delete",
|
|
10248
|
+
"reassign_scope"
|
|
10249
|
+
].includes(op.op_type)) continue;
|
|
10250
|
+
const rows = existingScopes.all(op.entity_id);
|
|
10251
|
+
for (const { scope_id: scope } of rows) if (typeof scope === "string" && scope.trim()) scopeIds.add(scope.trim());
|
|
10252
|
+
}
|
|
10253
|
+
}
|
|
10254
|
+
function rejectUnauthorizedInboundBatch(c, ops, context, options) {
|
|
10255
|
+
const rejected = rejectInboundScopeFailures(context.db, ops, context.localDeviceId, {
|
|
10256
|
+
enabled: options.enabled,
|
|
10257
|
+
peerDeviceId: context.peerDeviceId
|
|
10258
|
+
});
|
|
10259
|
+
if (rejected) return c.json({
|
|
10260
|
+
error: "scope_rejected",
|
|
10261
|
+
reason: rejected.rejections[0]?.reason ?? "scope_mismatch",
|
|
10262
|
+
rejections: rejected.rejections,
|
|
10263
|
+
sync_capability: LOCAL_SYNC_CAPABILITY,
|
|
10264
|
+
scope_id: options.scopeId
|
|
10265
|
+
}, 403);
|
|
10266
|
+
const scopeIds = new Set(options.managedScopeIds);
|
|
10267
|
+
addExistingMutationScopes(context.db, ops, scopeIds);
|
|
10268
|
+
const failure = validateManagedScopes(scopeIds, context);
|
|
10269
|
+
if (!failure) return null;
|
|
10270
|
+
return c.json(syncScopeResetRequiredPayload(getSyncResetState(context.db), failure.reason, LOCAL_SYNC_CAPABILITY, failure.scopeId), 409);
|
|
10271
|
+
}
|
|
10272
|
+
function serveAuthorizedSnapshot(c, store, auth) {
|
|
10273
|
+
const peerDeviceId = auth.deviceId;
|
|
9322
10274
|
try {
|
|
9323
10275
|
const rawScopeId = c.req.query(SYNC_SCOPE_QUERY_PARAM);
|
|
9324
10276
|
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
9325
10277
|
const scopeRequest = parseSyncScopeRequest(rawScopeId, rawScopeId !== void 0, {
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
negotiatedCapability: negotiatedSyncCapability(c),
|
|
9329
|
-
peerDeviceId
|
|
10278
|
+
...syncScopeAuthorizationContext(store, auth, localDeviceId),
|
|
10279
|
+
negotiatedCapability: negotiatedSyncCapability(c)
|
|
9330
10280
|
});
|
|
9331
10281
|
if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
|
|
9332
10282
|
const pageRequest = snapshotPageRequest((name) => c.req.query(name), localDeviceId);
|
|
@@ -9407,10 +10357,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
9407
10357
|
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
9408
10358
|
const syncReset = getSyncResetState(store.db);
|
|
9409
10359
|
const negotiated = negotiatedSyncCapability(c);
|
|
9410
|
-
const authorizedScopes = isScopedSyncCapability(negotiated) ? listAuthorizedScopesForPeer(store.db, {
|
|
9411
|
-
localDeviceId: deviceId,
|
|
9412
|
-
peerDeviceId: auth.deviceId
|
|
9413
|
-
}) : null;
|
|
10360
|
+
const authorizedScopes = isScopedSyncCapability(negotiated) ? listAuthorizedScopesForPeer(store.db, { ...syncScopeAuthorizationContext(store, auth, deviceId) }) : null;
|
|
9414
10361
|
const response = {
|
|
9415
10362
|
device_id: deviceId,
|
|
9416
10363
|
protocol_version: SYNC_PROTOCOL_VERSION,
|
|
@@ -9443,10 +10390,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
9443
10390
|
const negotiated = negotiatedSyncCapability(c);
|
|
9444
10391
|
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
9445
10392
|
const scopeRequest = parseSyncScopeRequest(rawScopeId, rawScopeId !== void 0, {
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
negotiatedCapability: negotiated,
|
|
9449
|
-
peerDeviceId
|
|
10393
|
+
...syncScopeAuthorizationContext(store, auth, localDeviceId),
|
|
10394
|
+
negotiatedCapability: negotiated
|
|
9450
10395
|
});
|
|
9451
10396
|
if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
|
|
9452
10397
|
const since = c.req.query("since") ?? null;
|
|
@@ -9520,7 +10465,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
9520
10465
|
}
|
|
9521
10466
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
9522
10467
|
if (limited) return limited;
|
|
9523
|
-
return serveAuthorizedSnapshot(c, store, auth
|
|
10468
|
+
return serveAuthorizedSnapshot(c, store, auth);
|
|
9524
10469
|
})();
|
|
9525
10470
|
});
|
|
9526
10471
|
app.post("/v1/ops", async (c) => {
|
|
@@ -9546,21 +10491,25 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
9546
10491
|
}
|
|
9547
10492
|
const negotiated = negotiateSyncCapability(LOCAL_SYNC_CAPABILITY, normalizeSyncCapability(body.sync_capability));
|
|
9548
10493
|
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10494
|
+
const scopeContext = syncScopeAuthorizationContext(store, auth, localDeviceId);
|
|
9549
10495
|
const scopeRequest = parseSyncScopeRequest(body.scope_id, Object.hasOwn(body, "scope_id"), {
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
negotiatedCapability: negotiated,
|
|
9553
|
-
peerDeviceId
|
|
10496
|
+
...scopeContext,
|
|
10497
|
+
negotiatedCapability: negotiated
|
|
9554
10498
|
});
|
|
9555
10499
|
if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
|
|
9556
10500
|
if (!Array.isArray(body.ops)) return c.json({ error: "invalid_ops" }, 400);
|
|
9557
10501
|
if (body.ops.length > MAX_SYNC_OPS) return c.json({ error: "too_many_ops" }, 413);
|
|
9558
10502
|
const normalizedOps = extractReplicationOps(body);
|
|
10503
|
+
const scopeIds = new Set(normalizedOps.map((op) => typeof op.scope_id === "string" ? op.scope_id.trim() : ""));
|
|
9559
10504
|
const peerFeatures = normalizeSyncFeatures(body.sync_features);
|
|
9560
10505
|
const reassignOps = normalizedOps.filter((op) => op.op_type === "reassign_scope");
|
|
9561
10506
|
if (reassignOps.length > 0 && !peerFeatures.includes("reassign_scope")) return c.json({ error: "reassign_capability_required" }, 409);
|
|
9562
10507
|
try {
|
|
9563
|
-
for (const op of reassignOps)
|
|
10508
|
+
for (const op of reassignOps) {
|
|
10509
|
+
const reassignment = parseReassignScopePayload(op);
|
|
10510
|
+
scopeIds.add(reassignment.old_scope_id);
|
|
10511
|
+
scopeIds.add(reassignment.new_scope_id);
|
|
10512
|
+
}
|
|
9564
10513
|
} catch {
|
|
9565
10514
|
return c.json({ error: "reassign_payload_invalid" }, 400);
|
|
9566
10515
|
}
|
|
@@ -9573,18 +10522,13 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
9573
10522
|
applyScopeFilter: false,
|
|
9574
10523
|
supportsReassignScope: peerFeatures.includes("reassign_scope")
|
|
9575
10524
|
});
|
|
9576
|
-
const rejected =
|
|
10525
|
+
const rejected = rejectUnauthorizedInboundBatch(c, normalizedOps, scopeContext, {
|
|
9577
10526
|
enabled: negotiated !== "unsupported",
|
|
9578
|
-
|
|
10527
|
+
scopeId: scopeRequest.scope_id,
|
|
10528
|
+
managedScopeIds: scopeIds
|
|
9579
10529
|
});
|
|
9580
|
-
if (rejected) return
|
|
9581
|
-
|
|
9582
|
-
reason: rejected.rejections[0]?.reason ?? "scope_mismatch",
|
|
9583
|
-
rejections: rejected.rejections,
|
|
9584
|
-
sync_capability: LOCAL_SYNC_CAPABILITY,
|
|
9585
|
-
scope_id: scopeRequest.scope_id
|
|
9586
|
-
}, 403);
|
|
9587
|
-
const result = applyReplicationOps(store.db, filtered.allowed, localDeviceId, store.scanner);
|
|
10530
|
+
if (rejected) return rejected;
|
|
10531
|
+
const result = applyInboundSyncOps(store, filtered.allowed, localDeviceId);
|
|
9588
10532
|
const skipped = result.skipped + filtered.skipped;
|
|
9589
10533
|
return c.json({
|
|
9590
10534
|
...result,
|
|
@@ -9785,10 +10729,15 @@ function wakeRecipientPolicyProjects(store, projectIdentities, now) {
|
|
|
9785
10729
|
wakeRecipientPoliciesForProjectIdentities(store.db, [...new Set(projectIdentities)], now);
|
|
9786
10730
|
}
|
|
9787
10731
|
function saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs) {
|
|
10732
|
+
store.adoptEnsuredDeviceIdentity(deviceId);
|
|
9788
10733
|
return store.db.transaction(() => {
|
|
9789
10734
|
const before = policyWakeMappings(store);
|
|
9790
10735
|
const previousMappings = existingRequestedMappings(before, mappingInputs);
|
|
9791
|
-
const
|
|
10736
|
+
const writableScopeIds = new Set(resolveVisibleScopeIds(store.db, store.deviceId, store.scopeResolutionDeviceContext()));
|
|
10737
|
+
const mappings = upsertProjectScopeSettingsMappings(store.db, mappingInputs, {
|
|
10738
|
+
deviceId,
|
|
10739
|
+
canWriteScope: (scopeId) => writableScopeIds.has(scopeId ?? "")
|
|
10740
|
+
});
|
|
9792
10741
|
const after = policyWakeMappings(store);
|
|
9793
10742
|
const changedScopeIds = /* @__PURE__ */ new Set([...previousMappings.map((mapping) => mapping.scope_id), ...mappings.map((mapping) => mapping.scope_id)]);
|
|
9794
10743
|
wakeRecipientPolicyProjects(store, [...mappedProjectsInScopes(before, changedScopeIds), ...mappedProjectsInScopes(after, changedScopeIds)], (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -9796,12 +10745,14 @@ function saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs) {
|
|
|
9796
10745
|
}).immediate();
|
|
9797
10746
|
}
|
|
9798
10747
|
function deleteProjectMappingAndWakePolicies(store, mappingId, deviceId, confirmedGuardrailTokens) {
|
|
10748
|
+
store.adoptEnsuredDeviceIdentity(deviceId);
|
|
9799
10749
|
return store.db.transaction(() => {
|
|
9800
10750
|
const before = policyWakeMappings(store);
|
|
9801
10751
|
const removed = before.find((mapping) => mapping.id === mappingId);
|
|
9802
10752
|
const deleted = deleteProjectScopeSettingsMapping(store.db, mappingId, {
|
|
9803
10753
|
deviceId,
|
|
9804
|
-
confirmedGuardrailTokens
|
|
10754
|
+
confirmedGuardrailTokens,
|
|
10755
|
+
canWriteScope: (scopeId) => store.isScopeWritable(scopeId)
|
|
9805
10756
|
});
|
|
9806
10757
|
if (deleted && removed) {
|
|
9807
10758
|
const changedScopeIds = /* @__PURE__ */ new Set([removed.scope_id]);
|
|
@@ -9817,8 +10768,125 @@ function deleteProjectMappingAndWakePolicies(store, mappingId, deviceId, confirm
|
|
|
9817
10768
|
* provide sync status, peer management, and coordinator UI for the
|
|
9818
10769
|
* local viewer.
|
|
9819
10770
|
*/
|
|
10771
|
+
function reassignProjectInventoryForUser(store, input) {
|
|
10772
|
+
store.adoptEnsuredDeviceIdentity(input.deviceId);
|
|
10773
|
+
return reassignProjectScopeInventoryProject(store.db, {
|
|
10774
|
+
...input,
|
|
10775
|
+
canWriteScope: (scopeId) => store.isScopeWritable(scopeId)
|
|
10776
|
+
});
|
|
10777
|
+
}
|
|
10778
|
+
function pendingCaptureActorMutationFailure(store) {
|
|
10779
|
+
try {
|
|
10780
|
+
store.assertPendingLocalCaptureAdoptionActors();
|
|
10781
|
+
return null;
|
|
10782
|
+
} catch (error) {
|
|
10783
|
+
if (isPendingCaptureActorConflict(error)) return {
|
|
10784
|
+
error: error.message,
|
|
10785
|
+
status: 409
|
|
10786
|
+
};
|
|
10787
|
+
throw error;
|
|
10788
|
+
}
|
|
10789
|
+
}
|
|
10790
|
+
function runViewerActorMutation(store, mutate) {
|
|
10791
|
+
return store.db.transaction(() => pendingCaptureActorMutationFailure(store) ?? mutate()).immediate();
|
|
10792
|
+
}
|
|
10793
|
+
function actorMergeDigest(primaryActorId, secondaryActorId) {
|
|
10794
|
+
return createHash("sha256").update(JSON.stringify({
|
|
10795
|
+
kind: "identity-merge-v1",
|
|
10796
|
+
primaryActorId,
|
|
10797
|
+
secondaryActorId,
|
|
10798
|
+
nonce: randomUUID()
|
|
10799
|
+
})).digest("hex");
|
|
10800
|
+
}
|
|
10801
|
+
function actorMergePrincipalFailure(store, primary, secondary) {
|
|
10802
|
+
if (primary.status !== "active") return {
|
|
10803
|
+
error: "primary actor not active",
|
|
10804
|
+
status: 409
|
|
10805
|
+
};
|
|
10806
|
+
if (secondary.status !== "active") return {
|
|
10807
|
+
error: "secondary actor not active",
|
|
10808
|
+
status: 409
|
|
10809
|
+
};
|
|
10810
|
+
if (secondary.is_local && secondary.actor_id === store.actorId) return {
|
|
10811
|
+
error: "cannot merge this device's own local actor",
|
|
10812
|
+
status: 409
|
|
10813
|
+
};
|
|
10814
|
+
return null;
|
|
10815
|
+
}
|
|
10816
|
+
function resolvePeerIdentityAssignment(store, opts) {
|
|
10817
|
+
const requestedActorId = opts.body.actor_id == null ? void 0 : String(opts.body.actor_id ?? "").trim() || null;
|
|
10818
|
+
const claimedLocalActor = typeof opts.body.claimed_local_actor === "boolean" ? opts.body.claimed_local_actor : void 0;
|
|
10819
|
+
let nextActorId = opts.actorId;
|
|
10820
|
+
let nextClaimedLocalActor = opts.claimedLocalActor;
|
|
10821
|
+
if (requestedActorId !== void 0) {
|
|
10822
|
+
nextActorId = requestedActorId;
|
|
10823
|
+
if (requestedActorId) {
|
|
10824
|
+
const actor = drizzle(store.db, { schema }).select({
|
|
10825
|
+
actor_id: schema.actors.actor_id,
|
|
10826
|
+
is_local: schema.actors.is_local,
|
|
10827
|
+
status: schema.actors.status
|
|
10828
|
+
}).from(schema.actors).where(eq(schema.actors.actor_id, requestedActorId)).get();
|
|
10829
|
+
if (!actor) return {
|
|
10830
|
+
error: "actor not found",
|
|
10831
|
+
status: 404
|
|
10832
|
+
};
|
|
10833
|
+
if (actor.status !== "active") return {
|
|
10834
|
+
error: "actor not active",
|
|
10835
|
+
status: 409
|
|
10836
|
+
};
|
|
10837
|
+
nextClaimedLocalActor = claimedLocalActor ?? Boolean(actor.is_local);
|
|
10838
|
+
} else nextClaimedLocalActor = claimedLocalActor ?? false;
|
|
10839
|
+
} else if (claimedLocalActor !== void 0) {
|
|
10840
|
+
nextClaimedLocalActor = claimedLocalActor;
|
|
10841
|
+
if (claimedLocalActor) nextActorId = store.actorId;
|
|
10842
|
+
else if (nextActorId === store.actorId) nextActorId = null;
|
|
10843
|
+
}
|
|
10844
|
+
return {
|
|
10845
|
+
actor_id: nextActorId,
|
|
10846
|
+
claimed_local_actor: nextClaimedLocalActor
|
|
10847
|
+
};
|
|
10848
|
+
}
|
|
10849
|
+
function persistPeerIdentityMutation(store, body) {
|
|
10850
|
+
const peerDeviceId = String(body.peer_device_id ?? "").trim();
|
|
10851
|
+
if (!peerDeviceId) return {
|
|
10852
|
+
error: "peer_device_id required",
|
|
10853
|
+
status: 400
|
|
10854
|
+
};
|
|
10855
|
+
const d = drizzle(store.db, { schema });
|
|
10856
|
+
const peer = d.select({
|
|
10857
|
+
peer_device_id: schema.syncPeers.peer_device_id,
|
|
10858
|
+
actor_id: schema.syncPeers.actor_id,
|
|
10859
|
+
claimed_local_actor: schema.syncPeers.claimed_local_actor
|
|
10860
|
+
}).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get();
|
|
10861
|
+
if (!peer) return {
|
|
10862
|
+
error: "peer not found",
|
|
10863
|
+
status: 404
|
|
10864
|
+
};
|
|
10865
|
+
const assignment = resolvePeerIdentityAssignment(store, {
|
|
10866
|
+
body,
|
|
10867
|
+
actorId: peer.actor_id ?? null,
|
|
10868
|
+
claimedLocalActor: Boolean(peer.claimed_local_actor)
|
|
10869
|
+
});
|
|
10870
|
+
if ("error" in assignment) return assignment;
|
|
10871
|
+
d.update(schema.syncPeers).set({
|
|
10872
|
+
actor_id: assignment.actor_id,
|
|
10873
|
+
claimed_local_actor: assignment.claimed_local_actor ? 1 : 0
|
|
10874
|
+
}).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();
|
|
10875
|
+
return assignment;
|
|
10876
|
+
}
|
|
10877
|
+
function createActorMutationPreflightRoutes(getStore) {
|
|
10878
|
+
const app = new Hono();
|
|
10879
|
+
const preflight = async (c, next) => {
|
|
10880
|
+
if (!ensureLocalActorRecord(getStore())) return c.json({ error: "device_adoption_actor_conflict" }, 409);
|
|
10881
|
+
await next();
|
|
10882
|
+
};
|
|
10883
|
+
app.post("/api/sync/peers/identity", preflight);
|
|
10884
|
+
app.post("/api/sync/actors/merge", preflight);
|
|
10885
|
+
return app;
|
|
10886
|
+
}
|
|
9820
10887
|
function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
9821
|
-
const app =
|
|
10888
|
+
const app = createActorMutationPreflightRoutes(getStore);
|
|
10889
|
+
app.post("/api/sync/invites/inspect", inspectPairingInvite);
|
|
9822
10890
|
const readCoordinatorConfig = options.readCoordinatorConfig ?? readCoordinatorSyncConfig;
|
|
9823
10891
|
const renameCoordinatorGroup = options.renameCoordinatorGroup ?? coordinatorRenameGroupAction;
|
|
9824
10892
|
const loadCoordinatorEvidence = options.loadDeviceIdentityCoordinatorEvidence ?? loadConfiguredDeviceIdentityCoordinatorEvidence;
|
|
@@ -9852,7 +10920,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9852
10920
|
};
|
|
9853
10921
|
app.get("/api/sync/recipient-policy/v1/device-inventory", async (c) => {
|
|
9854
10922
|
const store = getStore();
|
|
9855
|
-
|
|
10923
|
+
ensureStableStoreIdentityForRead(store);
|
|
9856
10924
|
return c.json(listDeviceIdentityInventory(store.db, await localInventoryInput(store)));
|
|
9857
10925
|
});
|
|
9858
10926
|
app.patch("/api/sync/recipient-policy/v1/teams/:teamId", async (c) => {
|
|
@@ -9944,7 +11012,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9944
11012
|
if (!body) return c.json({ error: "binding_request_invalid" }, 400);
|
|
9945
11013
|
const store = getStore();
|
|
9946
11014
|
try {
|
|
9947
|
-
|
|
11015
|
+
ensureStableStoreIdentityForRead(store);
|
|
9948
11016
|
const result = previewDeviceIdentityBindings(store.db, await localInventoryInput(store), body);
|
|
9949
11017
|
if (result.status === "invalid") return c.json(result, 400);
|
|
9950
11018
|
if (result.status === "not_found") return c.json(result, 404);
|
|
@@ -9985,6 +11053,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9985
11053
|
}
|
|
9986
11054
|
});
|
|
9987
11055
|
app.get("/api/sync/status", async (c) => {
|
|
11056
|
+
const loadDiagnosticKey = createPeerScopeDiagnosticKeyLoader();
|
|
9988
11057
|
const response = await buildSyncStatusResponse({
|
|
9989
11058
|
store: getStore(),
|
|
9990
11059
|
showDiagnostics: queryBool(c.req.query("includeDiagnostics")),
|
|
@@ -9996,7 +11065,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9996
11065
|
isRecentIso,
|
|
9997
11066
|
legacySharedReviewSummary,
|
|
9998
11067
|
listRecipientPolicyReconciliationStatus,
|
|
9999
|
-
mapPeerRow,
|
|
11068
|
+
mapPeerRow: (...args) => mapPeerRow(...args, loadDiagnosticKey),
|
|
10000
11069
|
mapSyncAttemptRow,
|
|
10001
11070
|
peerStatus,
|
|
10002
11071
|
readViewerBinding,
|
|
@@ -10013,11 +11082,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10013
11082
|
const store = getStore();
|
|
10014
11083
|
{
|
|
10015
11084
|
const showDiag = queryBool(c.req.query("includeDiagnostics"));
|
|
10016
|
-
const
|
|
10017
|
-
const recentOpsByPeer = recentPeerOps(store);
|
|
10018
|
-
const scopeRejectionsByPeer = recentScopeRejectionsByPeer(store);
|
|
10019
|
-
const localDeviceId = drizzle(store.db, { schema }).select({ device_id: schema.syncDevice.device_id }).from(schema.syncDevice).limit(1).get()?.device_id ?? null;
|
|
10020
|
-
const peers = rows.map((row) => mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer, localDeviceId));
|
|
11085
|
+
const peers = readPeerScopeDiagnostics(store, showDiag);
|
|
10021
11086
|
return c.json({
|
|
10022
11087
|
items: peers,
|
|
10023
11088
|
redacted: !showDiag
|
|
@@ -10407,7 +11472,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10407
11472
|
const body = await parseViewerJsonBody(c);
|
|
10408
11473
|
const request = parseReviewRequest(body);
|
|
10409
11474
|
if (!request) return c.json({ error: "request_invalid" }, 400);
|
|
10410
|
-
|
|
11475
|
+
requireLocalActorRecord(store);
|
|
10411
11476
|
const result = resolveRecipientPolicyReview(store.db, {
|
|
10412
11477
|
localActorId: store.actorId,
|
|
10413
11478
|
localDeviceId: store.deviceId
|
|
@@ -10423,7 +11488,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10423
11488
|
if (!body || !Array.isArray(body.requests) || body.requests.length === 0 || body.requests.length > 100) return c.json({ error: "request_invalid" }, 400);
|
|
10424
11489
|
const requests = body.requests.map(parseReviewRequest);
|
|
10425
11490
|
if (requests.some((request) => request == null)) return c.json({ error: "request_invalid" }, 400);
|
|
10426
|
-
|
|
11491
|
+
requireLocalActorRecord(store);
|
|
10427
11492
|
const result = resolveRecipientPolicyReviewBulk(store.db, {
|
|
10428
11493
|
localActorId: store.actorId,
|
|
10429
11494
|
localDeviceId: store.deviceId
|
|
@@ -10453,7 +11518,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10453
11518
|
const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
|
|
10454
11519
|
const project = optionalViewerStrictString(body, "project") ?? "";
|
|
10455
11520
|
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10456
|
-
const result =
|
|
11521
|
+
const result = reassignProjectInventoryForUser(store, {
|
|
10457
11522
|
deviceId,
|
|
10458
11523
|
project,
|
|
10459
11524
|
workspaceIdentity
|
|
@@ -10758,51 +11823,17 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10758
11823
|
app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => deleteProjectScopeMappingRoute(c, getStore()));
|
|
10759
11824
|
app.post("/api/sync/peers/identity", async (c) => {
|
|
10760
11825
|
const store = getStore();
|
|
10761
|
-
ensureLocalActorRecord(store);
|
|
10762
11826
|
let body;
|
|
10763
11827
|
try {
|
|
10764
11828
|
body = await c.req.json();
|
|
10765
11829
|
} catch {
|
|
10766
11830
|
return c.json({ error: "invalid json" }, 400);
|
|
10767
11831
|
}
|
|
10768
|
-
const
|
|
10769
|
-
if (
|
|
10770
|
-
const requestedActorId = body.actor_id == null ? void 0 : String(body.actor_id ?? "").trim() || null;
|
|
10771
|
-
const claimedLocalActor = typeof body.claimed_local_actor === "boolean" ? body.claimed_local_actor : void 0;
|
|
10772
|
-
const d = drizzle(store.db, { schema });
|
|
10773
|
-
const peer = d.select({
|
|
10774
|
-
peer_device_id: schema.syncPeers.peer_device_id,
|
|
10775
|
-
actor_id: schema.syncPeers.actor_id,
|
|
10776
|
-
claimed_local_actor: schema.syncPeers.claimed_local_actor
|
|
10777
|
-
}).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get();
|
|
10778
|
-
if (!peer) return c.json({ error: "peer not found" }, 404);
|
|
10779
|
-
let nextActorId = peer.actor_id ?? null;
|
|
10780
|
-
let nextClaimedLocalActor = Boolean(peer.claimed_local_actor);
|
|
10781
|
-
if (requestedActorId !== void 0) {
|
|
10782
|
-
nextActorId = requestedActorId;
|
|
10783
|
-
if (requestedActorId) {
|
|
10784
|
-
const actor = d.select({
|
|
10785
|
-
actor_id: schema.actors.actor_id,
|
|
10786
|
-
is_local: schema.actors.is_local,
|
|
10787
|
-
status: schema.actors.status
|
|
10788
|
-
}).from(schema.actors).where(eq(schema.actors.actor_id, requestedActorId)).get();
|
|
10789
|
-
if (!actor) return c.json({ error: "actor not found" }, 404);
|
|
10790
|
-
if (actor.status !== "active") return c.json({ error: "actor not active" }, 409);
|
|
10791
|
-
nextClaimedLocalActor = claimedLocalActor ?? Boolean(actor.is_local);
|
|
10792
|
-
} else nextClaimedLocalActor = claimedLocalActor ?? false;
|
|
10793
|
-
} else if (claimedLocalActor !== void 0) {
|
|
10794
|
-
nextClaimedLocalActor = claimedLocalActor;
|
|
10795
|
-
if (claimedLocalActor) nextActorId = store.actorId;
|
|
10796
|
-
else if (nextActorId === store.actorId) nextActorId = null;
|
|
10797
|
-
}
|
|
10798
|
-
d.update(schema.syncPeers).set({
|
|
10799
|
-
actor_id: nextActorId,
|
|
10800
|
-
claimed_local_actor: nextClaimedLocalActor ? 1 : 0
|
|
10801
|
-
}).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();
|
|
11832
|
+
const result = runViewerActorMutation(store, () => persistPeerIdentityMutation(store, body));
|
|
11833
|
+
if ("error" in result) return c.json({ error: result.error }, result.status);
|
|
10802
11834
|
return c.json({
|
|
10803
11835
|
ok: true,
|
|
10804
|
-
|
|
10805
|
-
claimed_local_actor: nextClaimedLocalActor
|
|
11836
|
+
...result
|
|
10806
11837
|
});
|
|
10807
11838
|
});
|
|
10808
11839
|
app.post("/api/sync/actors", async (c) => {
|
|
@@ -10869,7 +11900,6 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10869
11900
|
});
|
|
10870
11901
|
app.post("/api/sync/actors/merge", async (c) => {
|
|
10871
11902
|
const store = getStore();
|
|
10872
|
-
ensureLocalActorRecord(store);
|
|
10873
11903
|
let body;
|
|
10874
11904
|
try {
|
|
10875
11905
|
body = await c.req.json();
|
|
@@ -10886,24 +11916,16 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10886
11916
|
queueMicrotask(releaseActorMutations);
|
|
10887
11917
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10888
11918
|
const result = store.db.transaction(() => {
|
|
11919
|
+
const conflict = pendingCaptureActorMutationFailure(store);
|
|
11920
|
+
if (conflict) return conflict;
|
|
10889
11921
|
const primary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, primaryActorId)).get();
|
|
10890
11922
|
const secondary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, secondaryActorId)).get();
|
|
10891
11923
|
if (!primary || !secondary) return {
|
|
10892
11924
|
error: "actor not found",
|
|
10893
11925
|
status: 404
|
|
10894
11926
|
};
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
status: 409
|
|
10898
|
-
};
|
|
10899
|
-
if (secondary.status !== "active") return {
|
|
10900
|
-
error: "secondary actor not active",
|
|
10901
|
-
status: 409
|
|
10902
|
-
};
|
|
10903
|
-
if (secondary.is_local && secondary.actor_id === store.actorId) return {
|
|
10904
|
-
error: "cannot merge this device's own local actor",
|
|
10905
|
-
status: 409
|
|
10906
|
-
};
|
|
11927
|
+
const principalFailure = actorMergePrincipalFailure(store, primary, secondary);
|
|
11928
|
+
if (principalFailure) return principalFailure;
|
|
10907
11929
|
const memberships = d.select().from(schema.policyTeamMemberships).where(eq(schema.policyTeamMemberships.identity_id, secondaryActorId)).all();
|
|
10908
11930
|
const primaryMemberships = /* @__PURE__ */ new Map();
|
|
10909
11931
|
for (const membership of memberships) {
|
|
@@ -10924,12 +11946,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10924
11946
|
};
|
|
10925
11947
|
primaryRecipients.set(recipient.canonical_project_identity, primaryRecipient);
|
|
10926
11948
|
}
|
|
10927
|
-
const mergeDigest =
|
|
10928
|
-
kind: "identity-merge-v1",
|
|
10929
|
-
primaryActorId,
|
|
10930
|
-
secondaryActorId,
|
|
10931
|
-
nonce: randomUUID()
|
|
10932
|
-
})).digest("hex");
|
|
11949
|
+
const mergeDigest = actorMergeDigest(primaryActorId, secondaryActorId);
|
|
10933
11950
|
const bindings = d.select({
|
|
10934
11951
|
deviceId: schema.identityDevices.device_id,
|
|
10935
11952
|
assignmentVersion: schema.identityDevices.assignment_version
|
|
@@ -11146,7 +12163,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
11146
12163
|
if (!payload.operation_id && !recipientInvite) return c.json({ kind: "legacy_team_invite" });
|
|
11147
12164
|
const [status, inspected] = await requestJson("POST", `${buildBaseUrl(payload.coordinator_url)}/v1/invites/inspect`, {
|
|
11148
12165
|
body: { token: payload.token },
|
|
11149
|
-
timeoutS: 10
|
|
12166
|
+
timeoutS: 10,
|
|
12167
|
+
redirect: "error"
|
|
11150
12168
|
});
|
|
11151
12169
|
if (status < 200 || status >= 300) return c.json({ error: recipientInvite ? safeRecipientInviteError(inspected?.error) : String(inspected?.error ?? "invite_invalid") }, status === 410 ? 410 : 400);
|
|
11152
12170
|
const config = readCodememConfigFile();
|
|
@@ -12541,6 +13559,122 @@ function updateStatusRoutes(options = {}) {
|
|
|
12541
13559
|
return app;
|
|
12542
13560
|
}
|
|
12543
13561
|
//#endregion
|
|
13562
|
+
//#region src/account/context.ts
|
|
13563
|
+
function validId(value) {
|
|
13564
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
|
|
13565
|
+
}
|
|
13566
|
+
function label(value) {
|
|
13567
|
+
return validId(value) ? value : void 0;
|
|
13568
|
+
}
|
|
13569
|
+
function clean(value) {
|
|
13570
|
+
return typeof value === "string" ? value.trim() : "";
|
|
13571
|
+
}
|
|
13572
|
+
function configuredGroups(raw) {
|
|
13573
|
+
const value = process.env.CODEMEM_SYNC_COORDINATOR_GROUPS ?? raw.sync_coordinator_groups;
|
|
13574
|
+
if (value === void 0 || value === null || value === "") return [];
|
|
13575
|
+
const values = typeof value === "string" ? value.split(",") : value;
|
|
13576
|
+
if (!Array.isArray(values) || values.some((item) => !validId(clean(item)))) return null;
|
|
13577
|
+
return values.map(clean);
|
|
13578
|
+
}
|
|
13579
|
+
function readyContext(options, groupId, coordinatorUrl, raw, groups) {
|
|
13580
|
+
const profile = options.getProfile();
|
|
13581
|
+
if (!profile) return "device_unavailable";
|
|
13582
|
+
const before = statSync(profile.dbPath, { bigint: true });
|
|
13583
|
+
const device = readAccountLinkDevice(profile.dbPath);
|
|
13584
|
+
const after = statSync(profile.dbPath, { bigint: true });
|
|
13585
|
+
const override = process.env.CODEMEM_DEVICE_ID?.trim() || void 0;
|
|
13586
|
+
if (!before.isFile() || before.dev !== after.dev || before.ino !== after.ino || device.deviceId !== profile.deviceId || override !== void 0 && override !== device.deviceId) return "device_unavailable";
|
|
13587
|
+
let actor = clean(raw.actor_id);
|
|
13588
|
+
if (Object.hasOwn(process.env, "CODEMEM_ACTOR_ID")) actor = clean(process.env.CODEMEM_ACTOR_ID);
|
|
13589
|
+
actor ||= `local:${device.deviceId}`;
|
|
13590
|
+
if (!validId(actor) || actor !== profile.identityId) return "identity_unavailable";
|
|
13591
|
+
const context = {
|
|
13592
|
+
dbPath: profile.dbPath,
|
|
13593
|
+
keysDir: options.keysDir,
|
|
13594
|
+
configPath: options.configPath,
|
|
13595
|
+
coordinatorUrl,
|
|
13596
|
+
groupId,
|
|
13597
|
+
deviceId: device.deviceId,
|
|
13598
|
+
fingerprint: device.fingerprint,
|
|
13599
|
+
identityId: actor,
|
|
13600
|
+
identityLabel: label(profile.identityLabel),
|
|
13601
|
+
deviceLabel: label(profile.deviceLabel),
|
|
13602
|
+
fileIdentity: {
|
|
13603
|
+
dev: after.dev.toString(),
|
|
13604
|
+
ino: after.ino.toString()
|
|
13605
|
+
}
|
|
13606
|
+
};
|
|
13607
|
+
const contextId = createHash("sha256").update(JSON.stringify({
|
|
13608
|
+
context,
|
|
13609
|
+
groups,
|
|
13610
|
+
actorConfig: clean(raw.actor_id),
|
|
13611
|
+
actorEnv: process.env.CODEMEM_ACTOR_ID,
|
|
13612
|
+
deviceEnv: override,
|
|
13613
|
+
keyStore: process.env.CODEMEM_SYNC_KEY_STORE,
|
|
13614
|
+
home: process.env.HOME
|
|
13615
|
+
})).digest("hex").slice(0, 32);
|
|
13616
|
+
return {
|
|
13617
|
+
...context,
|
|
13618
|
+
contextId
|
|
13619
|
+
};
|
|
13620
|
+
}
|
|
13621
|
+
function readContext(options, groupId) {
|
|
13622
|
+
const raw = options.readConfig();
|
|
13623
|
+
const config = readCoordinatorSyncConfig(raw);
|
|
13624
|
+
if (!configuredGroups(raw)) return { error: "account_context_unavailable" };
|
|
13625
|
+
const groups = [...new Set(config.syncCoordinatorGroups)];
|
|
13626
|
+
if (groups.length > 25 || groups.some((id) => !validId(id))) return { error: "account_context_unavailable" };
|
|
13627
|
+
if (groupId !== void 0 && !groups.includes(groupId)) return { error: "account_group_not_configured" };
|
|
13628
|
+
const selectedGroupId = groupId ?? (groups.length === 1 ? groups[0] : null) ?? null;
|
|
13629
|
+
const origin = normalizeCoordinatorAccountLinkOrigin(config.syncCoordinatorUrl);
|
|
13630
|
+
const base = {
|
|
13631
|
+
groups,
|
|
13632
|
+
selectedGroupId,
|
|
13633
|
+
coordinator: origin ? { origin } : null,
|
|
13634
|
+
context: null
|
|
13635
|
+
};
|
|
13636
|
+
if (!config.syncCoordinatorUrl || groups.length === 0) return {
|
|
13637
|
+
...base,
|
|
13638
|
+
setup: "coordinator_not_configured"
|
|
13639
|
+
};
|
|
13640
|
+
if (!origin) return {
|
|
13641
|
+
...base,
|
|
13642
|
+
setup: "coordinator_url_invalid"
|
|
13643
|
+
};
|
|
13644
|
+
if (!selectedGroupId) return {
|
|
13645
|
+
...base,
|
|
13646
|
+
setup: "group_selection_required"
|
|
13647
|
+
};
|
|
13648
|
+
let context;
|
|
13649
|
+
try {
|
|
13650
|
+
context = readyContext(options, selectedGroupId, origin, raw, groups);
|
|
13651
|
+
} catch {
|
|
13652
|
+
return {
|
|
13653
|
+
...base,
|
|
13654
|
+
setup: "device_unavailable"
|
|
13655
|
+
};
|
|
13656
|
+
}
|
|
13657
|
+
if (typeof context === "string") return {
|
|
13658
|
+
...base,
|
|
13659
|
+
setup: context
|
|
13660
|
+
};
|
|
13661
|
+
return {
|
|
13662
|
+
...base,
|
|
13663
|
+
setup: "ready",
|
|
13664
|
+
context
|
|
13665
|
+
};
|
|
13666
|
+
}
|
|
13667
|
+
/** Trusted callbacks describe the running profile, never a browser-selected path. */
|
|
13668
|
+
function createAccountContextReader(options) {
|
|
13669
|
+
return (groupId) => {
|
|
13670
|
+
try {
|
|
13671
|
+
return readContext(options, groupId);
|
|
13672
|
+
} catch {
|
|
13673
|
+
return { error: "account_context_unavailable" };
|
|
13674
|
+
}
|
|
13675
|
+
};
|
|
13676
|
+
}
|
|
13677
|
+
//#endregion
|
|
12544
13678
|
//#region src/index.ts
|
|
12545
13679
|
/**
|
|
12546
13680
|
* @codemem/server — HTTP server (viewer, sync, API).
|
|
@@ -12590,6 +13724,21 @@ function registerConfigRoutes(app, opts, sweeper) {
|
|
|
12590
13724
|
getObserverApplyStatus: opts?.getObserverApplyStatus
|
|
12591
13725
|
}));
|
|
12592
13726
|
}
|
|
13727
|
+
function registerAccountRoutes(app, options) {
|
|
13728
|
+
if (!options) return;
|
|
13729
|
+
const account = createAccountGuard(options);
|
|
13730
|
+
app.use("*", account.middleware);
|
|
13731
|
+
app.route("/", account.routes);
|
|
13732
|
+
if (options.readContext) app.route("/", accountStatusRoutes({
|
|
13733
|
+
...options,
|
|
13734
|
+
readContext: options.readContext
|
|
13735
|
+
}));
|
|
13736
|
+
if (options.readContext && options.registry) app.route("/", accountOperationRoutes({
|
|
13737
|
+
...options,
|
|
13738
|
+
readContext: options.readContext,
|
|
13739
|
+
registry: options.registry
|
|
13740
|
+
}));
|
|
13741
|
+
}
|
|
12593
13742
|
function createApp(opts) {
|
|
12594
13743
|
const storeFactory = opts?.storeFactory ?? getStore;
|
|
12595
13744
|
const sweeper = opts?.sweeper ?? null;
|
|
@@ -12597,6 +13746,7 @@ function createApp(opts) {
|
|
|
12597
13746
|
const getSyncRuntimeStatus = opts?.getSyncRuntimeStatus ?? (() => null);
|
|
12598
13747
|
let invalidateTeamSetupSummary = () => {};
|
|
12599
13748
|
const app = new Hono();
|
|
13749
|
+
registerAccountRoutes(app, opts?.accountLink);
|
|
12600
13750
|
app.use("*", preflightHandler());
|
|
12601
13751
|
app.use("*", originGuard({ unsafeGetPathPrefixes: [TEAM_SETUP_ROUTE_PREFIX] }));
|
|
12602
13752
|
const viewerTargetGuard = viewerTargetQueryGuard(storeFactory);
|
|
@@ -12662,6 +13812,6 @@ function createSyncApp(opts) {
|
|
|
12662
13812
|
return app;
|
|
12663
13813
|
}
|
|
12664
13814
|
//#endregion
|
|
12665
|
-
export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory };
|
|
13815
|
+
export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, createAccountContextReader, createAccountLinkRegistry, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory };
|
|
12666
13816
|
|
|
12667
13817
|
//# sourceMappingURL=index.js.map
|