@codemem/server 0.46.2 → 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/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, storeVectors, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMappings, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, validateRawEvents, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, wakeRecipientPoliciesForIdentities, wakeRecipientPoliciesForProjectIdentities } from "@codemem/core";
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.get(memoryId)) return c.json({ error: "memory not found" }, 404);
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.updateMemoryVisibility(memoryId, visibility);
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.get(memoryId)) return c.json({ error: "memory not found" }, 404);
2398
+ if (!store.getForMutation(memoryId)) return c.json({ error: "memory not found" }, 404);
1676
2399
  try {
1677
- const result = store.moveMemoryProject(memoryId, project);
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.forget(memoryId);
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({ error: msg }, 404);
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
- return store.db.transaction(() => {
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.remember(sessionId, input.kind, input.title, input.body, input.confidence);
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 storeVectors(store.db, result.memId, result.title, result.body);
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 !== expected[key]) return false;
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 = { ...metadata };
3684
- for (const key of MEMORY_ID_METADATA_KEYS) if (Array.isArray(sanitized[key])) sanitized[key] = metadataMemoryIds(sanitized[key]).filter((memoryId) => visibility.visibleMemoryIds.has(memoryId));
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 computed /api/usage payload.
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 runs scope-visibility resolution over its bounded set, so
3748
- * each miss is cheap — but the totals are cumulative dashboard figures where a
3749
- * few seconds of staleness is acceptable, so caching still collapses the
3750
- * repeated recompute on the polling loop. Keyed by db path + scope identity +
3751
- * project filter so distinct stores/projects never share an entry.
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 ensures a membership/scope status change — e.g. a revocation —
3791
- * invalidates any cached payload immediately instead of letting a device keep
3792
- * reading a scope it can no longer see for up to the TTL. These tables are
3793
- * small, so the aggregate is negligible next to the usage_events scan the
3794
- * cache exists to avoid. Fails safe: any error forces a unique value so the
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
- if (cached && nowMs < cached.expiresAtMs) return c.json(cached.payload);
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 (!hasUrl || !hasSecret) throw safeCoordinatorError();
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 instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw 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 instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
6556
+ if (isPassthroughSnapshotLoadError(error)) throw error;
5766
6557
  throw safeCoordinatorError();
5767
6558
  }
5768
6559
  }
5769
- const loadedSummarySnapshots = createCachedSnapshotLoader(() => loadedSnapshots(), SUMMARY_SNAPSHOT_CACHE_TTL_MS);
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
- ensureLocalActorRecord(store);
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
- reconcileShareOperationAcceptance(store.db, {
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
- ok: true,
8178
- reason: "ok",
8179
- deviceId
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
- const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
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 (!grant || !workerEnrollment) return {
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
- if (grant.revoked_at) return {
8253
- ok: false,
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 (new Date(grant.expires_at) <= /* @__PURE__ */ new Date()) return {
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: String(workerEnrollment.display_name ?? "").trim() || void 0,
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 ? diagnoseStalePeerReceivedRows(store.db, {
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.forget(row.id);
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
- const preview = legacySharedReviewReassignmentPreview(store, input);
9199
- if (input.confirmationToken !== preview.confirmation_token) throw new Error("legacy shared review group changed before reassignment; refresh and try again");
9200
- const rows = reassignableLegacySharedReviewRows(store, legacySharedReviewRowsForWorkspace(store, input.workspaceIdentity));
9201
- store.db.transaction(() => {
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
- return {
9205
- ok: true,
9206
- ...preview,
9207
- reassigned_memory_count: rows.length,
9208
- legacy_shared_review: legacySharedReviewSummary(store)
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 serveAuthorizedSnapshot(c, store, peerDeviceId) {
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
- db: store.db,
9327
- localDeviceId,
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
- db: store.db,
9447
- localDeviceId,
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.deviceId);
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
- db: store.db,
9551
- localDeviceId,
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) parseReassignScopePayload(op);
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 = rejectInboundScopeFailures(store.db, normalizedOps, localDeviceId, {
10525
+ const rejected = rejectUnauthorizedInboundBatch(c, normalizedOps, scopeContext, {
9577
10526
  enabled: negotiated !== "unsupported",
9578
- peerDeviceId
10527
+ scopeId: scopeRequest.scope_id,
10528
+ managedScopeIds: scopeIds
9579
10529
  });
9580
- if (rejected) return c.json({
9581
- error: "scope_rejected",
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 mappings = upsertProjectScopeSettingsMappings(store.db, mappingInputs, { deviceId });
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 = new Hono().post("/api/sync/invites/inspect", inspectPairingInvite);
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
- ensureStableStoreIdentity(store);
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
- ensureStableStoreIdentity(store);
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 rows = store.db.prepare(PEERS_QUERY).all();
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
- ensureLocalActorRecord(store);
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
- ensureLocalActorRecord(store);
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 = reassignProjectScopeInventoryProject(store.db, {
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 peerDeviceId = String(body.peer_device_id ?? "").trim();
10769
- if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
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
- actor_id: nextActorId,
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
- if (primary.status !== "active") return {
10896
- error: "primary actor not active",
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 = createHash("sha256").update(JSON.stringify({
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