@codemem/server 0.46.3 → 0.47.0-beta.3

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, FILE_CONTEXT_TRANSPORT_MAX_BYTES, 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, buildFilterBranchesWithContext, 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, dispatchFileContextTransport, 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, parseFileContextTransportRequest, 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.
@@ -1028,38 +1751,6 @@ function diagnosticsRoutes(getStore) {
1028
1751
  return app;
1029
1752
  }
1030
1753
  //#endregion
1031
- //#region src/routes/health.ts
1032
- function databaseReachable(getStore) {
1033
- try {
1034
- getStore().db.pragma("schema_version", { simple: true });
1035
- return true;
1036
- } catch {
1037
- return false;
1038
- }
1039
- }
1040
- function healthRoutes(getStore) {
1041
- const app = new Hono();
1042
- app.get("/api/viewer-status", (c) => {
1043
- const store = getStore();
1044
- store.db.pragma("schema_version", { simple: true });
1045
- c.header("Cache-Control", "no-store");
1046
- return c.json({ identity: { actor_id: store.actorId } });
1047
- });
1048
- app.get("/api/health", (c) => {
1049
- const reachable = databaseReachable(getStore);
1050
- c.header("Cache-Control", "no-store");
1051
- return c.json({
1052
- service: VIEWER_SERVICE_DISCRIMINATOR,
1053
- version: VERSION$1,
1054
- pid: process.pid,
1055
- uptime_ms: Math.max(0, Math.floor(process.uptime() * 1e3)),
1056
- ready: reachable,
1057
- database: { reachable }
1058
- });
1059
- });
1060
- return app;
1061
- }
1062
- //#endregion
1063
1754
  //#region src/helpers.ts
1064
1755
  /**
1065
1756
  * Shared helpers for viewer-server routes.
@@ -1153,6 +1844,181 @@ async function parseJsonObjectBody(c, maxBytes) {
1153
1844
  return parsed;
1154
1845
  }
1155
1846
  //#endregion
1847
+ //#region src/routes/target-validation.ts
1848
+ var IDENTITY_TARGET_KEYS = new Set(VIEWER_IDENTITY_TARGET_KEYS);
1849
+ var BOOLEAN_IDENTITY_TARGET_KEYS = /* @__PURE__ */ new Set([
1850
+ "actor_id_present",
1851
+ "embedding_disabled",
1852
+ "embedding_offline"
1853
+ ]);
1854
+ function invalidRequest$1(message) {
1855
+ return {
1856
+ ok: false,
1857
+ status: 400,
1858
+ body: { error: {
1859
+ code: "invalid_request",
1860
+ message
1861
+ } }
1862
+ };
1863
+ }
1864
+ function conflict(code, message) {
1865
+ return {
1866
+ ok: false,
1867
+ status: 409,
1868
+ body: { error: {
1869
+ code,
1870
+ message
1871
+ } }
1872
+ };
1873
+ }
1874
+ function isRecord$1(value) {
1875
+ return value != null && typeof value === "object" && !Array.isArray(value);
1876
+ }
1877
+ function currentIdentityTarget() {
1878
+ return buildViewerIdentityTarget();
1879
+ }
1880
+ function requestedDbMatches(store, value) {
1881
+ if (value == null) return true;
1882
+ if (typeof value !== "string" || !value.trim()) return null;
1883
+ return resolve(resolveDbPath(value.trim())) === resolve(store.dbPath);
1884
+ }
1885
+ function implicitConfigPath() {
1886
+ const { resolved, fallbackChain } = resolveCodememConfigPath();
1887
+ if (resolved.source !== "env-codemem-config") return resolve(resolved.path);
1888
+ const candidates = fallbackChain.filter((candidate) => candidate.valid);
1889
+ const implicit = candidates.find((candidate) => candidate.exists) ?? candidates[0];
1890
+ return implicit ? resolve(implicit.path) : null;
1891
+ }
1892
+ function identityFieldMatches(key, requested, expected) {
1893
+ if (requested === expected) return true;
1894
+ if (key !== "config_path" || requested !== null && expected !== null) return false;
1895
+ return (requested ?? expected) === implicitConfigPath();
1896
+ }
1897
+ function requestedIdentityMatches(value) {
1898
+ if (value == null) return true;
1899
+ if (!isRecord$1(value)) return null;
1900
+ if (Object.keys(value).length !== IDENTITY_TARGET_KEYS.size || Object.keys(value).some((key) => !IDENTITY_TARGET_KEYS.has(key))) return "unsupported";
1901
+ const expected = currentIdentityTarget();
1902
+ for (const key of VIEWER_IDENTITY_TARGET_KEYS) {
1903
+ const requested = value[key];
1904
+ if (BOOLEAN_IDENTITY_TARGET_KEYS.has(key)) {
1905
+ if (typeof requested !== "boolean") return null;
1906
+ } else if (requested !== null && typeof requested !== "string") return null;
1907
+ if (!identityFieldMatches(key, requested, expected[key])) return false;
1908
+ }
1909
+ return true;
1910
+ }
1911
+ function validateViewerTarget(store, payload, options = {}) {
1912
+ if (options.requirePairedTargets) {
1913
+ const hasDbPath = Object.hasOwn(payload, "db_path");
1914
+ if (hasDbPath !== Object.hasOwn(payload, "identity_target")) return invalidRequest$1("db_path and identity_target must be provided together");
1915
+ if (hasDbPath && (payload.db_path == null || payload.identity_target == null)) return invalidRequest$1("db_path and identity_target must be valid when provided");
1916
+ }
1917
+ const dbMatches = requestedDbMatches(store, payload.db_path);
1918
+ if (dbMatches == null) return invalidRequest$1("db_path must be a non-empty string");
1919
+ if (!dbMatches) return conflict("viewer_db_mismatch", "viewer database does not match request");
1920
+ const identityMatches = requestedIdentityMatches(payload.identity_target);
1921
+ if (identityMatches === "unsupported") return conflict("viewer_contract_unsupported", "viewer request contract is incompatible");
1922
+ if (identityMatches == null) return invalidRequest$1("identity_target is invalid");
1923
+ if (!identityMatches) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
1924
+ if ((options.requireCurrentIdentity || payload.identity_target != null) && !store.hasCurrentIdentity()) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
1925
+ return { ok: true };
1926
+ }
1927
+ /**
1928
+ * Middleware read of db_path / identity_target from the query string.
1929
+ * Native tools (pi extension) send targets as query params so a pre-handler
1930
+ * guard can reject a foreign viewer without consuming POST bodies. Lenient:
1931
+ * clients that omit targets (UI, MCP, curl) are unaffected.
1932
+ */
1933
+ function viewerTargetQueryGuard(getStore) {
1934
+ return async (c, next) => {
1935
+ const dbPath = c.req.query("db_path");
1936
+ const rawIdentity = c.req.query("identity_target");
1937
+ if (dbPath == null && rawIdentity == null) return next();
1938
+ let identityTarget;
1939
+ if (rawIdentity != null) try {
1940
+ identityTarget = JSON.parse(rawIdentity);
1941
+ } catch {
1942
+ return c.json({ error: {
1943
+ code: "invalid_request",
1944
+ message: "identity_target must be JSON"
1945
+ } }, 400);
1946
+ }
1947
+ const target = validateViewerTarget(getStore(), {
1948
+ db_path: dbPath ?? void 0,
1949
+ identity_target: identityTarget
1950
+ });
1951
+ if (!target.ok) return c.json(target.body, target.status);
1952
+ return next();
1953
+ };
1954
+ }
1955
+ //#endregion
1956
+ //#region src/routes/file-context.ts
1957
+ function fileContextRoutes(getStore) {
1958
+ const app = new Hono();
1959
+ app.post("/api/file-context", async (c) => {
1960
+ const body = await parseJsonObjectBody(c, FILE_CONTEXT_TRANSPORT_MAX_BYTES);
1961
+ if (body instanceof Response) return body;
1962
+ const parsed = parseFileContextTransportRequest(body);
1963
+ if (!parsed.success) return c.json({ error: {
1964
+ code: "invalid_request",
1965
+ message: "invalid file-context request"
1966
+ } }, 400);
1967
+ try {
1968
+ const store = getStore();
1969
+ const target = validateViewerTarget(store, body, {
1970
+ requireCurrentIdentity: true,
1971
+ requirePairedTargets: true
1972
+ });
1973
+ if (!target.ok) return c.json(target.body, target.status);
1974
+ const response = dispatchFileContextTransport(store, parsed.data);
1975
+ if (!response.ok) return c.json({ error: {
1976
+ code: "invalid_request",
1977
+ message: "delivery does not match attempt"
1978
+ } }, 400);
1979
+ return c.json(response);
1980
+ } catch {
1981
+ return c.json({ error: {
1982
+ code: "file_context_failed",
1983
+ message: "file context unavailable"
1984
+ } }, 503);
1985
+ }
1986
+ });
1987
+ return app;
1988
+ }
1989
+ //#endregion
1990
+ //#region src/routes/health.ts
1991
+ function databaseReachable(getStore) {
1992
+ try {
1993
+ getStore().db.pragma("schema_version", { simple: true });
1994
+ return true;
1995
+ } catch {
1996
+ return false;
1997
+ }
1998
+ }
1999
+ function healthRoutes(getStore) {
2000
+ const app = new Hono();
2001
+ app.get("/api/viewer-status", (c) => {
2002
+ const store = getStore();
2003
+ store.db.pragma("schema_version", { simple: true });
2004
+ c.header("Cache-Control", "no-store");
2005
+ return c.json({ identity: { actor_id: store.actorId } });
2006
+ });
2007
+ app.get("/api/health", (c) => {
2008
+ const reachable = databaseReachable(getStore);
2009
+ c.header("Cache-Control", "no-store");
2010
+ return c.json({
2011
+ service: VIEWER_SERVICE_DISCRIMINATOR,
2012
+ version: VERSION$1,
2013
+ pid: process.pid,
2014
+ uptime_ms: Math.max(0, Math.floor(process.uptime() * 1e3)),
2015
+ ready: reachable,
2016
+ database: { reachable }
2017
+ });
2018
+ });
2019
+ return app;
2020
+ }
2021
+ //#endregion
1156
2022
  //#region src/routes/memory.ts
1157
2023
  var MAX_FEED_QUERY_CODE_UNITS = 256;
1158
2024
  var searchFunctionsRegisteredStores = /* @__PURE__ */ new WeakSet();
@@ -1359,16 +2225,19 @@ function memorySearchSql(query) {
1359
2225
  params: [query]
1360
2226
  };
1361
2227
  }
1362
- function countVisibleObservationRows(store, filters) {
1363
- const filterResult = buildViewerMemoryFilters(store, filters);
1364
- const clauses = [
1365
- "memory_items.active = 1",
1366
- `NOT ${summaryLikeSqlPredicate()}`,
1367
- ...filterResult.clauses
1368
- ];
1369
- const from = filterResult.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
1370
- const row = store.db.prepare(`SELECT COUNT(*) AS total FROM ${from} WHERE ${clauses.join(" AND ")}`).get(...filterResult.params);
1371
- return Number(row?.total ?? 0);
2228
+ function countVisibleMemoryClassifications(store, filters) {
2229
+ const branches = buildFilterBranchesWithContext(filters, store.ownershipFilterContext());
2230
+ const source = branches.map((branch) => {
2231
+ const from = branch.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
2232
+ return `SELECT ${summaryLikeSqlPredicate()} AS is_summary FROM ${from}
2233
+ WHERE memory_items.active = 1 AND ${branch.clauses.join(" AND ") || "1 = 1"}`;
2234
+ }).join(" UNION ALL ");
2235
+ const row = store.db.prepare(`SELECT COUNT(*) AS memories,
2236
+ COUNT(CASE WHEN NOT is_summary THEN 1 END) AS observations FROM (${source})`).get(...branches.flatMap((branch) => branch.params));
2237
+ return {
2238
+ memories: Number(row?.memories ?? 0),
2239
+ observations: Number(row?.observations ?? 0)
2240
+ };
1372
2241
  }
1373
2242
  function sessionAllowsArtifactAccess(store, sessionId) {
1374
2243
  const visibleCount = countVisibleMemoryRows(store, { session_id: sessionId });
@@ -1544,18 +2413,13 @@ function memoryRoutes(getStore) {
1544
2413
  const project = c.req.query("project") || null;
1545
2414
  let prompts;
1546
2415
  let artifacts;
1547
- let memories;
1548
- let observations;
2416
+ const { memories, observations } = countVisibleMemoryClassifications(store, project ? { project } : void 0);
1549
2417
  if (project) {
1550
2418
  prompts = countVisiblePromptRows(store, project);
1551
2419
  artifacts = countVisibleArtifactRows(store, project);
1552
- memories = countVisibleMemoryRows(store, { project });
1553
- observations = countVisibleObservationRows(store, { project });
1554
2420
  } else {
1555
2421
  prompts = countVisiblePromptRows(store);
1556
2422
  artifacts = countVisibleArtifactRows(store);
1557
- memories = countVisibleMemoryRows(store);
1558
- observations = countVisibleObservationRows(store);
1559
2423
  }
1560
2424
  const total = prompts + artifacts + memories;
1561
2425
  return c.json({
@@ -1647,11 +2511,11 @@ function memoryRoutes(getStore) {
1647
2511
  }
1648
2512
  const memoryId = parseStrictInteger(typeof body.memory_id === "string" ? body.memory_id : String(body.memory_id ?? ""));
1649
2513
  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);
2514
+ if (!store.getForMutation(memoryId)) return c.json({ error: "memory not found" }, 404);
1651
2515
  const visibility = String(body.visibility ?? "").trim();
1652
2516
  if (visibility !== "private" && visibility !== "shared") return c.json({ error: "visibility must be private or shared" }, 400);
1653
2517
  try {
1654
- const item = store.updateMemoryVisibility(memoryId, visibility);
2518
+ const item = store.updateMemoryVisibilityForUser(memoryId, visibility);
1655
2519
  return c.json({ item });
1656
2520
  } catch (err) {
1657
2521
  const msg = err instanceof Error ? err.message : String(err);
@@ -1672,9 +2536,9 @@ function memoryRoutes(getStore) {
1672
2536
  if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
1673
2537
  const project = String(body.project ?? "").trim();
1674
2538
  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);
2539
+ if (!store.getForMutation(memoryId)) return c.json({ error: "memory not found" }, 404);
1676
2540
  try {
1677
- const result = store.moveMemoryProject(memoryId, project);
2541
+ const result = store.moveMemoryProjectForUser(memoryId, project);
1678
2542
  return c.json(result);
1679
2543
  } catch (err) {
1680
2544
  const msg = err instanceof Error ? err.message : String(err);
@@ -1699,11 +2563,11 @@ function memoryRoutes(getStore) {
1699
2563
  if (!store.memoryOwnedBySelf(row)) return c.json({ error: "memory not owned by this device" }, 403);
1700
2564
  if (Number(row.active ?? 1) === 0 || row.deleted_at != null) return c.json({ status: "ok" });
1701
2565
  try {
1702
- store.forget(memoryId);
2566
+ forgetMemoryForViewer(store, memoryId);
1703
2567
  return c.json({ status: "ok" });
1704
2568
  } catch (err) {
1705
2569
  const msg = err instanceof Error ? err.message : String(err);
1706
- if (msg.includes("not found")) return c.json({ error: msg }, 404);
2570
+ if (msg.includes("not found")) return c.json(...forgetNotFound(forgetSafetyFilters(body)));
1707
2571
  if (msg.includes("not owned")) return c.json({ error: msg }, 403);
1708
2572
  if (msg.includes("sync_rebootstrap_in_progress")) return c.json({ error: msg }, 409);
1709
2573
  return c.json({ error: msg }, 400);
@@ -1721,9 +2585,13 @@ function forgetSafetyFilters(body) {
1721
2585
  return filters;
1722
2586
  }
1723
2587
  function forgetTargetExists(store, memoryId, filters) {
2588
+ if (!store.canMutateMemory(memoryId)) return false;
1724
2589
  if (Object.keys(filters).length > 0) return store.timeline(null, memoryId, 0, 0, filters).some((row) => row.id === memoryId);
1725
2590
  return store.get(memoryId) != null;
1726
2591
  }
2592
+ function forgetMemoryForViewer(store, memoryId) {
2593
+ if (!store.forgetForUser(memoryId)) throw new Error("memory not found");
2594
+ }
1727
2595
  //#endregion
1728
2596
  //#region src/routes/memory-tools.ts
1729
2597
  /**
@@ -1867,7 +2735,8 @@ function parseJsonBody(body) {
1867
2735
  };
1868
2736
  }
1869
2737
  function rememberMemory(store, input) {
1870
- return store.db.transaction(() => {
2738
+ const createdMemories = [];
2739
+ const result = store.db.transaction(() => {
1871
2740
  const now = (/* @__PURE__ */ new Date()).toISOString();
1872
2741
  const user = process.env.USER ?? "unknown";
1873
2742
  const cwd = process.cwd();
@@ -1875,10 +2744,10 @@ function rememberMemory(store, input) {
1875
2744
  project: input.project,
1876
2745
  envProject: process.env.CODEMEM_PROJECT
1877
2746
  });
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 }));
2747
+ const sessionInfo = store.db.prepare(`INSERT INTO sessions(started_at, ended_at, cwd, project, user, tool_version, metadata_json, import_key)
2748
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(now, now, cwd, project, user, "viewer-api", toJson({ viewer: true }), randomUUID());
1880
2749
  const sessionId = Number(sessionInfo.lastInsertRowid);
1881
- const memId = store.remember(sessionId, input.kind, input.title, input.body, input.confidence);
2750
+ const memId = store.rememberForUser(sessionId, input.kind, input.title, input.body, input.confidence, void 0, void 0, { createdMemories });
1882
2751
  if (!getMemoryForAccess(store, memId)) throw new Error("unauthorized_scope");
1883
2752
  store.db.prepare("UPDATE sessions SET ended_at = ?, metadata_json = ? WHERE id = ?").run((/* @__PURE__ */ new Date()).toISOString(), toJson({ viewer: true }), sessionId);
1884
2753
  return {
@@ -1887,6 +2756,8 @@ function rememberMemory(store, input) {
1887
2756
  body: input.body
1888
2757
  };
1889
2758
  })();
2759
+ store.enqueueCommittedVectorWrites(createdMemories);
2760
+ return result;
1890
2761
  }
1891
2762
  function readContextFile(path, displayPath, scope) {
1892
2763
  if (!existsSync(path)) return null;
@@ -2052,7 +2923,7 @@ function memoryToolRoutes(getStore) {
2052
2923
  project
2053
2924
  });
2054
2925
  try {
2055
- await storeVectors(store.db, result.memId, result.title, result.body);
2926
+ await store.flushPendingVectorWrites();
2056
2927
  } catch {}
2057
2928
  return c.json({ id: result.memId });
2058
2929
  } catch (err) {
@@ -2268,128 +3139,31 @@ function observerStatusRoutes(deps) {
2268
3139
  auth_backoff_remaining_s: 0
2269
3140
  }
2270
3141
  });
2271
- const queueTotals = store.rawEventBacklogTotals();
2272
- const authBackoff = sweeper?.authBackoffStatus() ?? {
2273
- active: false,
2274
- remainingS: 0
2275
- };
2276
- const latestFailure = store.latestRawEventFlushFailure();
2277
- const active = normalizeActiveObserver(observer?.getStatus() ?? null);
2278
- const availableCredentials = probeAvailableCredentials();
2279
- const failureWithImpact = latestFailure != null && (authBackoff.active || queueTotals.pending > 0) && latestFailure ? {
2280
- ...latestFailure,
2281
- impact: buildFailureImpact(latestFailure, queueTotals, authBackoff)
2282
- } : null;
2283
- return c.json({
2284
- active,
2285
- available_credentials: availableCredentials,
2286
- capture_enabled: readCaptureEnabled(),
2287
- latest_failure: failureWithImpact,
2288
- queue: {
2289
- ...queueTotals,
2290
- auth_backoff_active: authBackoff.active,
2291
- auth_backoff_remaining_s: authBackoff.remainingS
2292
- }
2293
- });
2294
- });
2295
- return app;
2296
- }
2297
- //#endregion
2298
- //#region src/routes/target-validation.ts
2299
- var IDENTITY_TARGET_KEYS = new Set(VIEWER_IDENTITY_TARGET_KEYS);
2300
- var BOOLEAN_IDENTITY_TARGET_KEYS = /* @__PURE__ */ new Set([
2301
- "actor_id_present",
2302
- "embedding_disabled",
2303
- "embedding_offline"
2304
- ]);
2305
- function invalidRequest$1(message) {
2306
- return {
2307
- ok: false,
2308
- status: 400,
2309
- body: { error: {
2310
- code: "invalid_request",
2311
- message
2312
- } }
2313
- };
2314
- }
2315
- function conflict(code, message) {
2316
- return {
2317
- ok: false,
2318
- status: 409,
2319
- body: { error: {
2320
- code,
2321
- message
2322
- } }
2323
- };
2324
- }
2325
- function isRecord$1(value) {
2326
- return value != null && typeof value === "object" && !Array.isArray(value);
2327
- }
2328
- function currentIdentityTarget() {
2329
- return buildViewerIdentityTarget();
2330
- }
2331
- function requestedDbMatches(store, value) {
2332
- if (value == null) return true;
2333
- if (typeof value !== "string" || !value.trim()) return null;
2334
- return resolve(resolveDbPath(value.trim())) === resolve(store.dbPath);
2335
- }
2336
- function requestedIdentityMatches(value) {
2337
- if (value == null) return true;
2338
- if (!isRecord$1(value)) return null;
2339
- if (Object.keys(value).length !== IDENTITY_TARGET_KEYS.size || Object.keys(value).some((key) => !IDENTITY_TARGET_KEYS.has(key))) return "unsupported";
2340
- const expected = currentIdentityTarget();
2341
- for (const key of VIEWER_IDENTITY_TARGET_KEYS) {
2342
- const requested = value[key];
2343
- if (BOOLEAN_IDENTITY_TARGET_KEYS.has(key)) {
2344
- if (typeof requested !== "boolean") return null;
2345
- } else if (requested !== null && typeof requested !== "string") return null;
2346
- if (requested !== expected[key]) return false;
2347
- }
2348
- return true;
2349
- }
2350
- function validateViewerTarget(store, payload, options = {}) {
2351
- if (options.requirePairedTargets) {
2352
- const hasDbPath = Object.hasOwn(payload, "db_path");
2353
- if (hasDbPath !== Object.hasOwn(payload, "identity_target")) return invalidRequest$1("db_path and identity_target must be provided together");
2354
- if (hasDbPath && (payload.db_path == null || payload.identity_target == null)) return invalidRequest$1("db_path and identity_target must be valid when provided");
2355
- }
2356
- const dbMatches = requestedDbMatches(store, payload.db_path);
2357
- if (dbMatches == null) return invalidRequest$1("db_path must be a non-empty string");
2358
- if (!dbMatches) return conflict("viewer_db_mismatch", "viewer database does not match request");
2359
- const identityMatches = requestedIdentityMatches(payload.identity_target);
2360
- if (identityMatches === "unsupported") return conflict("viewer_contract_unsupported", "viewer request contract is incompatible");
2361
- if (identityMatches == null) return invalidRequest$1("identity_target is invalid");
2362
- if (!identityMatches) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
2363
- if ((options.requireCurrentIdentity || payload.identity_target != null) && !store.hasCurrentIdentity()) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
2364
- return { ok: true };
2365
- }
2366
- /**
2367
- * Middleware read of db_path / identity_target from the query string.
2368
- * Native tools (pi extension) send targets as query params so a pre-handler
2369
- * guard can reject a foreign viewer without consuming POST bodies. Lenient:
2370
- * clients that omit targets (UI, MCP, curl) are unaffected.
2371
- */
2372
- function viewerTargetQueryGuard(getStore) {
2373
- return async (c, next) => {
2374
- const dbPath = c.req.query("db_path");
2375
- const rawIdentity = c.req.query("identity_target");
2376
- if (dbPath == null && rawIdentity == null) return next();
2377
- let identityTarget;
2378
- if (rawIdentity != null) try {
2379
- identityTarget = JSON.parse(rawIdentity);
2380
- } catch {
2381
- return c.json({ error: {
2382
- code: "invalid_request",
2383
- message: "identity_target must be JSON"
2384
- } }, 400);
2385
- }
2386
- const target = validateViewerTarget(getStore(), {
2387
- db_path: dbPath ?? void 0,
2388
- identity_target: identityTarget
3142
+ const queueTotals = store.rawEventBacklogTotals();
3143
+ const authBackoff = sweeper?.authBackoffStatus() ?? {
3144
+ active: false,
3145
+ remainingS: 0
3146
+ };
3147
+ const latestFailure = store.latestRawEventFlushFailure();
3148
+ const active = normalizeActiveObserver(observer?.getStatus() ?? null);
3149
+ const availableCredentials = probeAvailableCredentials();
3150
+ const failureWithImpact = latestFailure != null && (authBackoff.active || queueTotals.pending > 0) && latestFailure ? {
3151
+ ...latestFailure,
3152
+ impact: buildFailureImpact(latestFailure, queueTotals, authBackoff)
3153
+ } : null;
3154
+ return c.json({
3155
+ active,
3156
+ available_credentials: availableCredentials,
3157
+ capture_enabled: readCaptureEnabled(),
3158
+ latest_failure: failureWithImpact,
3159
+ queue: {
3160
+ ...queueTotals,
3161
+ auth_backoff_active: authBackoff.active,
3162
+ auth_backoff_remaining_s: authBackoff.remainingS
3163
+ }
2389
3164
  });
2390
- if (!target.ok) return c.json(target.body, target.status);
2391
- return next();
2392
- };
3165
+ });
3166
+ return app;
2393
3167
  }
2394
3168
  //#endregion
2395
3169
  //#region src/routes/pack.ts
@@ -3619,11 +4393,7 @@ function chunkNumbers(values, chunkSize = 500) {
3619
4393
  }
3620
4394
  function visibleMemoryIdSet(store, memoryIds) {
3621
4395
  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
- });
4396
+ const filterResult = buildFilterClausesWithContext(null, store.ownershipFilterContext());
3627
4397
  const visibleIds = /* @__PURE__ */ new Set();
3628
4398
  for (const chunk of chunkNumbers([...memoryIds])) {
3629
4399
  const clauses = [
@@ -3638,11 +4408,7 @@ function visibleMemoryIdSet(store, memoryIds) {
3638
4408
  }
3639
4409
  function visibleSessionIdSet(store, sessionIds) {
3640
4410
  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
- });
4411
+ const filterResult = buildFilterClausesWithContext(null, store.ownershipFilterContext());
3646
4412
  const visibleIds = /* @__PURE__ */ new Set();
3647
4413
  for (const chunk of chunkNumbers([...sessionIds])) {
3648
4414
  const clauses = [
@@ -3680,8 +4446,20 @@ function usageRowVisible(row, visibility) {
3680
4446
  }
3681
4447
  function sanitizePackUsageMetadata(metadata, visibility) {
3682
4448
  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));
4449
+ const sanitized = MEMORY_ID_METADATA_KEYS.some((key) => {
4450
+ if (!Object.hasOwn(metadata, key)) return false;
4451
+ const ids = metadata[key];
4452
+ if (!Array.isArray(ids)) return true;
4453
+ return ids.some((value) => {
4454
+ if (typeof value !== "number" && typeof value !== "string") return true;
4455
+ const id = Number(value);
4456
+ return !Number.isInteger(id) || id <= 0 || !visibility.visibleMemoryIds.has(id);
4457
+ });
4458
+ }) ? {} : { ...metadata };
4459
+ for (const key of MEMORY_ID_METADATA_KEYS) {
4460
+ const ids = metadata[key];
4461
+ if (Array.isArray(ids)) sanitized[key] = metadataMemoryIds(ids.filter((value) => typeof value === "number" || typeof value === "string")).filter((memoryId) => visibility.visibleMemoryIds.has(memoryId));
4462
+ }
3685
4463
  return sanitized;
3686
4464
  }
3687
4465
  /**
@@ -3740,15 +4518,15 @@ function usageAggregatePayload(store, project) {
3740
4518
  };
3741
4519
  }
3742
4520
  /**
3743
- * Short-lived cache for the computed /api/usage payload.
4521
+ * Short-lived cache for the unfiltered /api/usage aggregates only.
3744
4522
  *
3745
4523
  * The health dot polls /api/usage on every 5s refresh regardless of the
3746
4524
  * 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.
4525
+ * recent-pack window must resolve current visibility on every request, even
4526
+ * on cache hits. Only the cumulative dashboard totals tolerate staleness.
4527
+ * Keyed by db path + scope identity + project filter so distinct
4528
+ * stores/projects never share an entry. Never cache recent packs or their
4529
+ * authorization: key, proof, policy, and ownership changes must apply now.
3752
4530
  */
3753
4531
  var usagePayloadCache = /* @__PURE__ */ new Map();
3754
4532
  var USAGE_PAYLOAD_CACHE_MS = 1e4;
@@ -3787,12 +4565,11 @@ function sweepUsagePayloadCache(cache, nowMs) {
3787
4565
  /**
3788
4566
  * Cheap fingerprint of the state that drives scope visibility
3789
4567
  * (`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.
4568
+ * cache key preserves aggregate invalidation on membership/scope changes.
4569
+ * This fingerprint is not authorization: recent packs always use the current
4570
+ * ownership/scope filter independently of the aggregate cache. These tables
4571
+ * are small, so the aggregate is negligible next to the usage_events scan the
4572
+ * cache exists to avoid. Any error forces a unique value to bypass the cache.
3796
4573
  */
3797
4574
  function scopeVisibilityGeneration(store) {
3798
4575
  try {
@@ -3873,8 +4650,16 @@ function statsRoutes(getStore) {
3873
4650
  const cacheKey = usageCacheKey(store, projectFilter);
3874
4651
  const nowMs = Date.now();
3875
4652
  const cached = usagePayloadCache.get(cacheKey);
3876
- if (cached && nowMs < cached.expiresAtMs) return c.json(cached.payload);
3877
- const aggregates = usageAggregatePayload(store, projectFilter);
4653
+ const cacheHit = cached && nowMs < cached.expiresAtMs;
4654
+ const aggregates = cacheHit ? cached.payload : usageAggregatePayload(store, projectFilter);
4655
+ if (!cacheHit) {
4656
+ const setAtMs = Date.now();
4657
+ sweepUsagePayloadCache(usagePayloadCache, setAtMs);
4658
+ usagePayloadCache.set(cacheKey, {
4659
+ payload: aggregates,
4660
+ expiresAtMs: setAtMs + USAGE_PAYLOAD_CACHE_MS
4661
+ });
4662
+ }
3878
4663
  const RECENT_PACK_SCAN_LIMIT = 200;
3879
4664
  const loadRecentPackWindow = (project) => {
3880
4665
  return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
@@ -3907,12 +4692,6 @@ function statsRoutes(getStore) {
3907
4692
  totals_filtered: aggregates.totalsFiltered,
3908
4693
  recent_packs: recentPacks
3909
4694
  };
3910
- const setAtMs = Date.now();
3911
- sweepUsagePayloadCache(usagePayloadCache, setAtMs);
3912
- usagePayloadCache.set(cacheKey, {
3913
- payload,
3914
- expiresAtMs: setAtMs + USAGE_PAYLOAD_CACHE_MS
3915
- });
3916
4695
  return c.json(payload);
3917
4696
  }
3918
4697
  });
@@ -3921,10 +4700,7 @@ function statsRoutes(getStore) {
3921
4700
  function healthStats(store) {
3922
4701
  return {
3923
4702
  ...store.stats(),
3924
- automatic_recall: automaticRecallHealth(store.db, {
3925
- actorId: store.actorId,
3926
- deviceId: store.deviceId
3927
- })
4703
+ automatic_recall: automaticRecallHealth(store.db, store.ownershipFilterContext())
3928
4704
  };
3929
4705
  }
3930
4706
  //#endregion
@@ -4542,7 +5318,7 @@ async function buildSyncStatusResponse(input) {
4542
5318
  return response;
4543
5319
  }
4544
5320
  var PEERS_QUERY$1 = `
4545
- SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
5321
+ SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.public_key, p.addresses_json,
4546
5322
  p.last_seen_at, p.last_sync_at, p.last_error,
4547
5323
  p.runtime_version, p.runtime_version_observed_at,
4548
5324
  p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
@@ -4879,6 +5655,52 @@ var defaultSnapshotLoaderDependencies = {
4879
5655
  function safeCoordinatorError() {
4880
5656
  return /* @__PURE__ */ new Error("team_setup_roster_unavailable");
4881
5657
  }
5658
+ var CoordinatorAdminConfigurationMissingError = class extends Error {
5659
+ constructor() {
5660
+ super("team_setup_roster_unavailable");
5661
+ this.name = "CoordinatorAdminConfigurationMissingError";
5662
+ }
5663
+ };
5664
+ function isPassthroughSnapshotLoadError(error) {
5665
+ return error instanceof CoordinatorAdminConfigurationMissingError || error instanceof Error && error.message === "legacy_team_setup_roster_too_large";
5666
+ }
5667
+ /**
5668
+ * Without coordinator admin access this Viewer cannot discover groups to
5669
+ * migrate, so the summary suppresses discovery. Members still see their Teams
5670
+ * from local records, and Devices reports incomplete coordinator setup. Saved,
5671
+ * unfinished migrations stay visible so they can be resumed once access
5672
+ * returns. Only the summary uses this; Team detail and finish stay fail-closed.
5673
+ */
5674
+ function summaryWithoutAdminAccess(error, getStore, readConfig) {
5675
+ if (!(error instanceof CoordinatorAdminConfigurationMissingError)) throw error;
5676
+ const store = getStore();
5677
+ const config = readConfig();
5678
+ const eligibleCandidateRefs = new Set(legacyTeamCandidateGroupDescriptors(store, config.syncCoordinatorUrl, config.syncCoordinatorGroups).map(({ coordinatorId, groupId }) => legacyTeamCandidateId(coordinatorId, groupId)));
5679
+ if (eligibleCandidateRefs.size === 0) return [];
5680
+ const eligibleRefs = [...eligibleCandidateRefs];
5681
+ const placeholders = eligibleRefs.map(() => "?").join(", ");
5682
+ return store.db.prepare(`SELECT DISTINCT coordinator_id, group_id FROM legacy_team_setup_drafts
5683
+ WHERE candidate_id IN (${placeholders})
5684
+ ORDER BY coordinator_id, group_id`).all(...eligibleRefs).flatMap((row) => {
5685
+ const candidateRef = legacyTeamCandidateId(row.coordinator_id, row.group_id);
5686
+ if (!eligibleCandidateRefs.has(candidateRef)) return [];
5687
+ const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
5688
+ if (!draft || draft.state === "completed" || hasCompletedLegacyTeamSetup(store, candidateRef)) return [];
5689
+ return [{
5690
+ coordinatorId: row.coordinator_id,
5691
+ groupId: row.group_id,
5692
+ displayName: draft.displayName,
5693
+ devices: []
5694
+ }];
5695
+ }).slice(0, MAX_CONFIGURED_GROUPS);
5696
+ }
5697
+ function loadSummarySnapshots(load, getStore, readConfig) {
5698
+ return () => load().catch((error) => summaryWithoutAdminAccess(error, getStore, readConfig));
5699
+ }
5700
+ function createLoadedSummarySnapshotCache(load, options) {
5701
+ const readConfig = options.readCoordinatorConfig ?? options.snapshotLoaderDependencies?.readConfig ?? readCoordinatorSyncConfig;
5702
+ return createCachedSnapshotLoader(loadSummarySnapshots(load, options.getStore, readConfig), SUMMARY_SNAPSHOT_CACHE_TTL_MS);
5703
+ }
4882
5704
  function isCoordinatorRosterTooLargeError(error) {
4883
5705
  return error instanceof Error && error.message === "coordinator_response_too_large";
4884
5706
  }
@@ -5028,7 +5850,8 @@ async function loadConfiguredLegacyTeamGroupSnapshotsWith(dependencies, options
5028
5850
  const hasUrl = Boolean(config.syncCoordinatorUrl);
5029
5851
  const hasSecret = Boolean(config.syncCoordinatorAdminSecret);
5030
5852
  if (!hasUrl && !hasSecret && config.syncCoordinatorGroups.length === 0) return [];
5031
- if (!hasUrl || !hasSecret) throw safeCoordinatorError();
5853
+ if (hasUrl && !hasSecret) throw new CoordinatorAdminConfigurationMissingError();
5854
+ if (!hasUrl) throw safeCoordinatorError();
5032
5855
  try {
5033
5856
  const configuredCoordinatorId = buildBaseUrl(config.syncCoordinatorUrl);
5034
5857
  if (!configuredCoordinatorId) throw safeCoordinatorError();
@@ -5100,7 +5923,7 @@ async function loadConfiguredLegacyTeamGroupSnapshotsWith(dependencies, options
5100
5923
  if (validSnapshots.length === 0 && outcomes.some((outcome) => outcome.kind === "unavailable")) throw safeCoordinatorError();
5101
5924
  return validSnapshots;
5102
5925
  } catch (error) {
5103
- if (error instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
5926
+ if (isPassthroughSnapshotLoadError(error)) throw error;
5104
5927
  throw safeCoordinatorError();
5105
5928
  }
5106
5929
  }
@@ -5762,11 +6585,11 @@ function teamSetupRoutes(options) {
5762
6585
  deadlineMs
5763
6586
  } : void 0);
5764
6587
  } catch (error) {
5765
- if (error instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
6588
+ if (isPassthroughSnapshotLoadError(error)) throw error;
5766
6589
  throw safeCoordinatorError();
5767
6590
  }
5768
6591
  }
5769
- const loadedSummarySnapshots = createCachedSnapshotLoader(() => loadedSnapshots(), SUMMARY_SNAPSHOT_CACHE_TTL_MS);
6592
+ const loadedSummarySnapshots = createLoadedSummarySnapshotCache(loadedSnapshots, options);
5770
6593
  options.registerSummaryInvalidator?.(() => loadedSummarySnapshots.invalidate());
5771
6594
  const completionDependencies = options.completionDependencies === void 0 ? defaultCompletionDependencies : options.completionDependencies;
5772
6595
  /**
@@ -6760,7 +7583,7 @@ async function maybeGrantDefaultSpaceOnJoin(opts) {
6760
7583
  adminSecret: opts.config.syncCoordinatorAdminSecret || null
6761
7584
  });
6762
7585
  }
6763
- function configuredGroups(config) {
7586
+ function configuredGroups$1(config) {
6764
7587
  const rawGroups = config.sync_coordinator_groups;
6765
7588
  if (Array.isArray(rawGroups)) return rawGroups.map((group) => String(group).trim()).filter(Boolean);
6766
7589
  if (typeof rawGroups === "string") return rawGroups.split(",").map((group) => group.trim()).filter(Boolean);
@@ -6772,7 +7595,7 @@ function removeConfiguredCoordinatorGroup(groupId) {
6772
7595
  if (!targetGroup) return [];
6773
7596
  let nextGroups = [];
6774
7597
  mutateCodememConfigFile((config) => {
6775
- const groups = configuredGroups(config);
7598
+ const groups = configuredGroups$1(config);
6776
7599
  nextGroups = groups.filter((group) => group !== targetGroup);
6777
7600
  const configuredGroup = String(config.sync_coordinator_group ?? "").trim();
6778
7601
  const removesPluralGroup = nextGroups.length !== groups.length;
@@ -7295,7 +8118,7 @@ async function reconcileProjectInviteAcceptance(store, operationId) {
7295
8118
  const owner = store.db.prepare("SELECT inviter_actor_id FROM share_operations WHERE operation_id = ?").pluck().get(operationId);
7296
8119
  if (!owner) throw new Error("operation_not_found");
7297
8120
  if (owner !== store.actorId) throw new Error("operation_scope_mismatch");
7298
- ensureLocalActorRecord(store);
8121
+ requireLocalActorRecord(store);
7299
8122
  const { groupId, payload } = await coordinatorProjectInvitePayload(operationId);
7300
8123
  if (payload.state === "waiting_for_acceptance") return {
7301
8124
  accepted: false,
@@ -7329,7 +8152,7 @@ async function reconcileProjectInviteAcceptance(store, operationId) {
7329
8152
  throw error;
7330
8153
  }
7331
8154
  const projects = parseAcceptedProjectIntent(payload.projects);
7332
- reconcileShareOperationAcceptance(store.db, {
8155
+ commitProjectInviteAcceptance(store, {
7333
8156
  operationId,
7334
8157
  localInviterActorId: store.actorId,
7335
8158
  coordinatorGroupId: groupId,
@@ -7350,6 +8173,12 @@ async function reconcileProjectInviteAcceptance(store, operationId) {
7350
8173
  payload
7351
8174
  };
7352
8175
  }
8176
+ function commitProjectInviteAcceptance(store, input) {
8177
+ store.db.transaction(() => {
8178
+ store.assertPendingLocalCaptureAdoptionActors();
8179
+ reconcileShareOperationAcceptance(store.db, input);
8180
+ }).immediate();
8181
+ }
7353
8182
  async function shareOperationReadModels(store, operationId, includeInviteLinks = true) {
7354
8183
  const rows = store.db.prepare(`SELECT o.operation_id, o.state, o.person_id, o.teammate_name,
7355
8184
  o.recipient_actor_id, o.recipient_display_name, o.recipient_device_id,
@@ -7529,6 +8358,10 @@ function errorStatus(error) {
7529
8358
  function projectInviteOwnerErrorResponse(error, fallback) {
7530
8359
  const message = error instanceof Error ? error.message : "";
7531
8360
  const status = errorStatus(error);
8361
+ if (isPendingCaptureActorConflict(error) && (status == null || status === 409)) return {
8362
+ code: message,
8363
+ status: 409
8364
+ };
7532
8365
  if (message === "operation_not_found" && (status == null || status === 404)) return {
7533
8366
  code: message,
7534
8367
  status: 404
@@ -8003,6 +8836,14 @@ function unauthorizedPayload() {
8003
8836
  return { error: "unauthorized" };
8004
8837
  }
8005
8838
  var SYNC_AUTH_STORE_BUSY_REASON = "sync_auth_store_busy";
8839
+ function authenticatedSyncPeer(deviceId, publicKey) {
8840
+ return {
8841
+ ok: true,
8842
+ reason: "ok",
8843
+ deviceId,
8844
+ authenticatedPeerSigningKey: publicKey
8845
+ };
8846
+ }
8006
8847
  var DIRECT_PEER_SIGNATURE_VERSION_NUMBER = 3;
8007
8848
  function logDirectPeerAuthDiagnostic(...reasons) {
8008
8849
  const reason = reasons.find((candidate) => candidate === "recipient_mismatch" || candidate === "signature_downgrade");
@@ -8173,11 +9014,39 @@ function authorizeSyncRequest(store, request, body) {
8173
9014
  const versionResult = recordDirectPeerSignatureVersion(store, deviceId, verification.version);
8174
9015
  if (versionResult) return versionResult;
8175
9016
  }
8176
- return {
8177
- ok: true,
8178
- reason: "ok",
8179
- deviceId
8180
- };
9017
+ return authenticatedSyncPeer(deviceId, publicKey);
9018
+ }
9019
+ function bootstrapPayloadRecord(value) {
9020
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9021
+ }
9022
+ function bootstrapPayloadText(value) {
9023
+ return typeof value === "string" && value.length > 0 && !value.includes("\0");
9024
+ }
9025
+ function bootstrapNullableText(value) {
9026
+ return value === null || typeof value === "string";
9027
+ }
9028
+ function validBootstrapEnrollment(value) {
9029
+ if (!bootstrapPayloadRecord(value)) return false;
9030
+ 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;
9031
+ }
9032
+ function validBootstrapVerification(value, grantId) {
9033
+ if (!bootstrapPayloadRecord(value) || value.authorization_version !== 1) return false;
9034
+ const grant = value.grant;
9035
+ if (!bootstrapPayloadRecord(grant)) return false;
9036
+ 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;
9037
+ const seed = value.seed_enrollment;
9038
+ const worker = value.worker_enrollment;
9039
+ if (!validBootstrapEnrollment(seed) || !validBootstrapEnrollment(worker)) return false;
9040
+ 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;
9041
+ }
9042
+ function bootstrapSeedMatchesLocalIdentity(store, seed) {
9043
+ try {
9044
+ const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
9045
+ const publicKey = loadPublicKey(syncKeysDir());
9046
+ return deviceId === seed.device_id && publicKey === seed.public_key && fingerprint === seed.fingerprint && publicKey !== null && fingerprintPublicKey(publicKey) === seed.fingerprint;
9047
+ } catch {
9048
+ return false;
9049
+ }
8181
9050
  }
8182
9051
  async function authorizeBootstrapGrantRequest(store, request, body) {
8183
9052
  const grantId = (request.header("X-Codemem-Bootstrap-Grant") ?? "").trim();
@@ -8196,9 +9065,10 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
8196
9065
  reason: "bootstrap_grant_coordinator_not_configured",
8197
9066
  deviceId
8198
9067
  };
8199
- const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
9068
+ let localDeviceId;
8200
9069
  let verification;
8201
9070
  try {
9071
+ [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
8202
9072
  let payload = null;
8203
9073
  if (config.syncCoordinatorAdminSecret) {
8204
9074
  const [status, adminPayload] = await requestJson("GET", `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/admin/bootstrap-grants/${encodeURIComponent(grantId)}`, {
@@ -8229,6 +9099,11 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
8229
9099
  reason: "bootstrap_grant_lookup_failed",
8230
9100
  deviceId
8231
9101
  };
9102
+ if (!validBootstrapVerification(payload, grantId)) return {
9103
+ ok: false,
9104
+ reason: "bootstrap_grant_invalid_payload",
9105
+ deviceId
9106
+ };
8232
9107
  verification = payload;
8233
9108
  } catch {
8234
9109
  return {
@@ -8238,44 +9113,15 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
8238
9113
  };
8239
9114
  }
8240
9115
  const grant = verification.grant;
9116
+ const seedEnrollment = verification.seed_enrollment;
8241
9117
  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 {
9118
+ if (config.syncCoordinatorGroups.length > 0 && !config.syncCoordinatorGroups.includes(grant.group_id)) return {
8248
9119
  ok: false,
8249
9120
  reason: "bootstrap_grant_group_mismatch",
8250
9121
  deviceId
8251
9122
  };
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
- };
9123
+ const workerPublicKey = workerEnrollment.public_key;
9124
+ const workerFingerprint = workerEnrollment.fingerprint;
8279
9125
  if (grant.worker_device_id !== deviceId) return {
8280
9126
  ok: false,
8281
9127
  reason: "bootstrap_grant_worker_mismatch",
@@ -8286,11 +9132,16 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
8286
9132
  reason: "bootstrap_grant_seed_mismatch",
8287
9133
  deviceId
8288
9134
  };
8289
- if (new Date(grant.expires_at) <= /* @__PURE__ */ new Date()) return {
9135
+ if (parseBootstrapExpiry(grant.expires_at) <= Date.now()) return {
8290
9136
  ok: false,
8291
9137
  reason: "bootstrap_grant_expired",
8292
9138
  deviceId
8293
9139
  };
9140
+ if (!bootstrapSeedMatchesLocalIdentity(store, seedEnrollment)) return {
9141
+ ok: false,
9142
+ reason: "bootstrap_grant_seed_identity_invalid",
9143
+ deviceId
9144
+ };
8294
9145
  const existingPeer = store.db.prepare(`SELECT highest_observed_direct_signature_version
8295
9146
  FROM sync_peer_signature_state WHERE peer_device_id = ? LIMIT 1`).get(deviceId);
8296
9147
  let signatureVerification;
@@ -8321,7 +9172,7 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
8321
9172
  const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
8322
9173
  if (nonceResult) return nonceResult;
8323
9174
  updatePeerAddresses(store.db, deviceId, [], {
8324
- name: String(workerEnrollment.display_name ?? "").trim() || void 0,
9175
+ name: workerEnrollment.display_name?.trim() || void 0,
8325
9176
  pinnedFingerprint: workerFingerprint,
8326
9177
  publicKey: workerPublicKey,
8327
9178
  replaceTrust: true
@@ -8330,11 +9181,7 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
8330
9181
  const versionResult = recordDirectPeerSignatureVersion(store, deviceId, signatureVerification.version);
8331
9182
  if (versionResult) return versionResult;
8332
9183
  }
8333
- return {
8334
- ok: true,
8335
- reason: "ok",
8336
- deviceId
8337
- };
9184
+ return authenticatedSyncPeer(deviceId, workerPublicKey);
8338
9185
  }
8339
9186
  function parseJsonList(value) {
8340
9187
  if (value == null) return [];
@@ -8409,7 +9256,23 @@ function peerAuthorizedScopes(store, peerDeviceId) {
8409
9256
  updated_at: cleanPeerScopeText(row.updated_at)
8410
9257
  }));
8411
9258
  }
9259
+ function isPendingCaptureActorConflict(error) {
9260
+ return error instanceof Error && error.constructor === Error && error.message === "device_adoption_actor_conflict";
9261
+ }
9262
+ /** False leaves the conflict untouched; callers must check before claiming or merging identity. */
8412
9263
  function ensureLocalActorRecord(store) {
9264
+ return store.db.transaction(() => {
9265
+ try {
9266
+ store.assertPendingLocalCaptureAdoptionActors();
9267
+ } catch (error) {
9268
+ if (isPendingCaptureActorConflict(error)) return false;
9269
+ throw error;
9270
+ }
9271
+ persistLocalActorRecord(store);
9272
+ return true;
9273
+ }).immediate();
9274
+ }
9275
+ function persistLocalActorRecord(store) {
8413
9276
  const d = drizzle(store.db, { schema });
8414
9277
  const now = (/* @__PURE__ */ new Date()).toISOString();
8415
9278
  d.update(schema.actors).set({
@@ -8437,7 +9300,16 @@ function ensureLocalActorRecord(store) {
8437
9300
  updated_at: now
8438
9301
  }).run();
8439
9302
  }
9303
+ function requireLocalActorRecord(store) {
9304
+ if (!ensureLocalActorRecord(store)) throw new Error("device_adoption_actor_conflict");
9305
+ }
8440
9306
  function ensureStableStoreIdentity(store) {
9307
+ const deviceId = ensureStableStoreDeviceIdentity(store);
9308
+ requireLocalActorRecord(store);
9309
+ return deviceId;
9310
+ }
9311
+ /** Preserve normal actor setup for reads, without repairing a pending conflict. */
9312
+ function ensureStableStoreIdentityForRead(store) {
8441
9313
  const deviceId = ensureStableStoreDeviceIdentity(store);
8442
9314
  ensureLocalActorRecord(store);
8443
9315
  return deviceId;
@@ -8749,7 +9621,7 @@ function missingArchivedCoordinatorGroupResponse(groupId, status, groups) {
8749
9621
  * When showDiag is false, sensitive fields (fingerprint, last_error, addresses)
8750
9622
  * are redacted.
8751
9623
  */
8752
- function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer, localDeviceId) {
9624
+ function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer, localDeviceId, loadDiagnosticKey) {
8753
9625
  const peerId = String(row.peer_device_id ?? "");
8754
9626
  const recentOps = recentOpsByPeer?.get(peerId) ?? {
8755
9627
  in: 0,
@@ -8759,7 +9631,8 @@ function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer
8759
9631
  const addresses = safeJsonList(row.addresses_json);
8760
9632
  const perScopeSync = listPerPeerScopeSyncState(store.db, {
8761
9633
  localDeviceId: localDeviceId ?? null,
8762
- peerDeviceId: peerId
9634
+ peerDeviceId: peerId,
9635
+ ...peerScopeDiagnosticKeys(store, row, localDeviceId, loadDiagnosticKey)
8763
9636
  });
8764
9637
  return {
8765
9638
  peer_device_id: row.peer_device_id,
@@ -8821,6 +9694,16 @@ function recentScopeRejectionsByPeer(store) {
8821
9694
  }
8822
9695
  return map;
8823
9696
  }
9697
+ function staleRowsForCleanupDiagnostics(store, localDeviceId) {
9698
+ return diagnoseStalePeerReceivedRows(store.db, {
9699
+ localDeviceId,
9700
+ keysDir: syncKeysDir(),
9701
+ maxRows: CLEANUP_DIAGNOSTICS_MAX_ROWS
9702
+ });
9703
+ }
9704
+ function applyInboundSyncOps(store, ops, localDeviceId) {
9705
+ return applyReplicationOps(store.db, ops, localDeviceId, store.scanner, { keysDir: syncKeysDir() });
9706
+ }
8824
9707
  function cleanupDiagnostics(store, localDeviceId, showDiag) {
8825
9708
  const opRows = store.db.prepare(`SELECT
8826
9709
  SUM(CASE WHEN device_id = ? THEN 1 ELSE 0 END) AS source_authored,
@@ -8828,10 +9711,7 @@ function cleanupDiagnostics(store, localDeviceId, showDiag) {
8828
9711
  MAX(created_at) AS latest_at
8829
9712
  FROM replication_ops
8830
9713
  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
- }) : {
9714
+ const stale = localDeviceId && showDiag ? staleRowsForCleanupDiagnostics(store, localDeviceId) : {
8835
9715
  ambiguous: [],
8836
9716
  checked: 0,
8837
9717
  retained: 0,
@@ -9114,8 +9994,8 @@ function forgetProjectInventoryLocalMemories(store, input) {
9114
9994
  };
9115
9995
  if (input.confirmationToken !== confirmationToken) throw new Error("project memories changed before cleanup; refresh and try again");
9116
9996
  store.db.transaction(() => {
9117
- for (const row of localRows) store.forget(row.id);
9118
- })();
9997
+ for (const row of localRows) if (!store.forgetForUser(row.id)) throw new Error("unauthorized_scope");
9998
+ }).immediate();
9119
9999
  return {
9120
10000
  confirmed: true,
9121
10001
  forgotten_memory_count: localRows.length,
@@ -9132,7 +10012,7 @@ function assertLocalDeviceScopeMembership(store, scopeId) {
9132
10012
  AND sm.status = 'active'
9133
10013
  AND rs.status = 'active'
9134
10014
  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}`);
10015
+ LIMIT 1`).get(scopeId, store.deviceId) || !store.isScopeWritable(scopeId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
9136
10016
  }
9137
10017
  function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
9138
10018
  const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
@@ -9195,18 +10075,19 @@ function legacySharedReviewReassignmentPreview(store, input) {
9195
10075
  };
9196
10076
  }
9197
10077
  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(() => {
10078
+ return store.db.transaction(() => {
10079
+ const preview = legacySharedReviewReassignmentPreview(store, input);
10080
+ if (input.confirmationToken !== preview.confirmation_token) throw new Error("legacy shared review group changed before reassignment; refresh and try again");
10081
+ const rows = reassignableLegacySharedReviewRows(store, legacySharedReviewRowsForWorkspace(store, input.workspaceIdentity));
10082
+ assertLocalDeviceScopeMembership(store, input.scopeId);
9202
10083
  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
- };
10084
+ return {
10085
+ ok: true,
10086
+ ...preview,
10087
+ reassigned_memory_count: rows.length,
10088
+ legacy_shared_review: legacySharedReviewSummary(store)
10089
+ };
10090
+ }).immediate();
9210
10091
  }
9211
10092
  var SYNC_DIRECTION_WINDOW_SECONDS = 86400;
9212
10093
  function recentPeerOps(store) {
@@ -9281,7 +10162,7 @@ function readViewerBinding(dbPath) {
9281
10162
  return null;
9282
10163
  }
9283
10164
  var PEERS_QUERY = `
9284
- SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
10165
+ SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.public_key, p.addresses_json,
9285
10166
  p.last_seen_at, p.last_sync_at, p.last_error,
9286
10167
  p.runtime_version, p.runtime_version_observed_at,
9287
10168
  p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
@@ -9318,15 +10199,116 @@ function negotiatedSyncCapability(c) {
9318
10199
  * network-accessible. All requests are auth-gated via signature
9319
10200
  * verification so unauthenticated callers are rejected.
9320
10201
  */
9321
- function serveAuthorizedSnapshot(c, store, peerDeviceId) {
10202
+ function syncScopeAuthorizationContext(store, auth, localDeviceId) {
10203
+ return {
10204
+ db: store.db,
10205
+ localDeviceId,
10206
+ peerDeviceId: auth.deviceId,
10207
+ localSigningPublicKey: loadPublicKey(syncKeysDir()),
10208
+ authenticatedPeerSigningKey: auth.authenticatedPeerSigningKey
10209
+ };
10210
+ }
10211
+ function peerScopeDiagnosticKeys(store, row, localDeviceId, loadDiagnosticKey) {
10212
+ return {
10213
+ localSigningPublicKey: loadDiagnosticKey?.(store, localDeviceId) ?? null,
10214
+ authenticatedPeerSigningKey: typeof row.public_key === "string" ? row.public_key : null
10215
+ };
10216
+ }
10217
+ function createPeerScopeDiagnosticKeyLoader() {
10218
+ let publicKey;
10219
+ return (store, localDeviceId) => {
10220
+ if (publicKey !== void 0) return publicKey;
10221
+ publicKey = null;
10222
+ if (!localDeviceId) return null;
10223
+ if (!store.db.prepare(`SELECT 1 FROM scope_memberships m
10224
+ JOIN replication_scopes s ON s.scope_id = m.scope_id
10225
+ WHERE m.device_id = ? AND m.status = 'active'
10226
+ AND s.authority_type = 'coordinator' LIMIT 1`).get(localDeviceId)) return null;
10227
+ publicKey = loadRuntimeSigningPublicKey(store.db, {
10228
+ deviceId: localDeviceId,
10229
+ keysDir: syncKeysDir()
10230
+ });
10231
+ return publicKey;
10232
+ };
10233
+ }
10234
+ function readPeerScopeDiagnostics(store, showDiag) {
10235
+ const rows = store.db.prepare(PEERS_QUERY).all();
10236
+ const recentOpsByPeer = recentPeerOps(store);
10237
+ const scopeRejectionsByPeer = recentScopeRejectionsByPeer(store);
10238
+ const localDeviceId = drizzle(store.db, { schema }).select({ device_id: schema.syncDevice.device_id }).from(schema.syncDevice).limit(1).get()?.device_id ?? null;
10239
+ const loadDiagnosticKey = createPeerScopeDiagnosticKeyLoader();
10240
+ return rows.map((row) => mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer, localDeviceId, loadDiagnosticKey));
10241
+ }
10242
+ function validateManagedScopes(scopeIds, context) {
10243
+ for (const scopeId of scopeIds) {
10244
+ if (!scopeId || scopeId === LOCAL_DEFAULT_SCOPE_ID) continue;
10245
+ const localAuthorization = getEffectiveCachedScopeAuthorization(context.db, {
10246
+ deviceId: context.localDeviceId,
10247
+ scopeId,
10248
+ expectedPublicKey: context.localSigningPublicKey?.trim() ?? ""
10249
+ });
10250
+ const authority = localAuthorization.scope?.authority_type;
10251
+ if (authority === "manual" || authority === "invite" || authority === "local") continue;
10252
+ if (authority !== "coordinator") return {
10253
+ scopeId,
10254
+ reason: "missing_scope"
10255
+ };
10256
+ const localReason = scopeAuthorizationFailureReason(localAuthorization);
10257
+ if (localReason) return {
10258
+ scopeId,
10259
+ reason: localReason
10260
+ };
10261
+ const peerAuthorization = getEffectiveCachedScopeAuthorization(context.db, {
10262
+ deviceId: context.peerDeviceId,
10263
+ scopeId,
10264
+ expectedPublicKey: context.authenticatedPeerSigningKey.trim()
10265
+ });
10266
+ const peerReason = scopeAuthorizationFailureReason(peerAuthorization);
10267
+ if (peerReason) return {
10268
+ scopeId,
10269
+ reason: peerReason
10270
+ };
10271
+ }
10272
+ return null;
10273
+ }
10274
+ function addExistingMutationScopes(db, ops, scopeIds) {
10275
+ const existingScopes = db.prepare("SELECT DISTINCT scope_id FROM memory_items WHERE import_key = ?");
10276
+ for (const op of ops) {
10277
+ if (![
10278
+ "upsert",
10279
+ "delete",
10280
+ "reassign_scope"
10281
+ ].includes(op.op_type)) continue;
10282
+ const rows = existingScopes.all(op.entity_id);
10283
+ for (const { scope_id: scope } of rows) if (typeof scope === "string" && scope.trim()) scopeIds.add(scope.trim());
10284
+ }
10285
+ }
10286
+ function rejectUnauthorizedInboundBatch(c, ops, context, options) {
10287
+ const rejected = rejectInboundScopeFailures(context.db, ops, context.localDeviceId, {
10288
+ enabled: options.enabled,
10289
+ peerDeviceId: context.peerDeviceId
10290
+ });
10291
+ if (rejected) return c.json({
10292
+ error: "scope_rejected",
10293
+ reason: rejected.rejections[0]?.reason ?? "scope_mismatch",
10294
+ rejections: rejected.rejections,
10295
+ sync_capability: LOCAL_SYNC_CAPABILITY,
10296
+ scope_id: options.scopeId
10297
+ }, 403);
10298
+ const scopeIds = new Set(options.managedScopeIds);
10299
+ addExistingMutationScopes(context.db, ops, scopeIds);
10300
+ const failure = validateManagedScopes(scopeIds, context);
10301
+ if (!failure) return null;
10302
+ return c.json(syncScopeResetRequiredPayload(getSyncResetState(context.db), failure.reason, LOCAL_SYNC_CAPABILITY, failure.scopeId), 409);
10303
+ }
10304
+ function serveAuthorizedSnapshot(c, store, auth) {
10305
+ const peerDeviceId = auth.deviceId;
9322
10306
  try {
9323
10307
  const rawScopeId = c.req.query(SYNC_SCOPE_QUERY_PARAM);
9324
10308
  const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
9325
10309
  const scopeRequest = parseSyncScopeRequest(rawScopeId, rawScopeId !== void 0, {
9326
- db: store.db,
9327
- localDeviceId,
9328
- negotiatedCapability: negotiatedSyncCapability(c),
9329
- peerDeviceId
10310
+ ...syncScopeAuthorizationContext(store, auth, localDeviceId),
10311
+ negotiatedCapability: negotiatedSyncCapability(c)
9330
10312
  });
9331
10313
  if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
9332
10314
  const pageRequest = snapshotPageRequest((name) => c.req.query(name), localDeviceId);
@@ -9407,10 +10389,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
9407
10389
  const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
9408
10390
  const syncReset = getSyncResetState(store.db);
9409
10391
  const negotiated = negotiatedSyncCapability(c);
9410
- const authorizedScopes = isScopedSyncCapability(negotiated) ? listAuthorizedScopesForPeer(store.db, {
9411
- localDeviceId: deviceId,
9412
- peerDeviceId: auth.deviceId
9413
- }) : null;
10392
+ const authorizedScopes = isScopedSyncCapability(negotiated) ? listAuthorizedScopesForPeer(store.db, { ...syncScopeAuthorizationContext(store, auth, deviceId) }) : null;
9414
10393
  const response = {
9415
10394
  device_id: deviceId,
9416
10395
  protocol_version: SYNC_PROTOCOL_VERSION,
@@ -9443,10 +10422,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
9443
10422
  const negotiated = negotiatedSyncCapability(c);
9444
10423
  const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
9445
10424
  const scopeRequest = parseSyncScopeRequest(rawScopeId, rawScopeId !== void 0, {
9446
- db: store.db,
9447
- localDeviceId,
9448
- negotiatedCapability: negotiated,
9449
- peerDeviceId
10425
+ ...syncScopeAuthorizationContext(store, auth, localDeviceId),
10426
+ negotiatedCapability: negotiated
9450
10427
  });
9451
10428
  if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
9452
10429
  const since = c.req.query("since") ?? null;
@@ -9520,7 +10497,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
9520
10497
  }
9521
10498
  const limited = rateLimitedResponse(c, auth.deviceId, true);
9522
10499
  if (limited) return limited;
9523
- return serveAuthorizedSnapshot(c, store, auth.deviceId);
10500
+ return serveAuthorizedSnapshot(c, store, auth);
9524
10501
  })();
9525
10502
  });
9526
10503
  app.post("/v1/ops", async (c) => {
@@ -9546,21 +10523,25 @@ function syncProtocolRoutes(getStore, opts = {}) {
9546
10523
  }
9547
10524
  const negotiated = negotiateSyncCapability(LOCAL_SYNC_CAPABILITY, normalizeSyncCapability(body.sync_capability));
9548
10525
  const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
10526
+ const scopeContext = syncScopeAuthorizationContext(store, auth, localDeviceId);
9549
10527
  const scopeRequest = parseSyncScopeRequest(body.scope_id, Object.hasOwn(body, "scope_id"), {
9550
- db: store.db,
9551
- localDeviceId,
9552
- negotiatedCapability: negotiated,
9553
- peerDeviceId
10528
+ ...scopeContext,
10529
+ negotiatedCapability: negotiated
9554
10530
  });
9555
10531
  if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
9556
10532
  if (!Array.isArray(body.ops)) return c.json({ error: "invalid_ops" }, 400);
9557
10533
  if (body.ops.length > MAX_SYNC_OPS) return c.json({ error: "too_many_ops" }, 413);
9558
10534
  const normalizedOps = extractReplicationOps(body);
10535
+ const scopeIds = new Set(normalizedOps.map((op) => typeof op.scope_id === "string" ? op.scope_id.trim() : ""));
9559
10536
  const peerFeatures = normalizeSyncFeatures(body.sync_features);
9560
10537
  const reassignOps = normalizedOps.filter((op) => op.op_type === "reassign_scope");
9561
10538
  if (reassignOps.length > 0 && !peerFeatures.includes("reassign_scope")) return c.json({ error: "reassign_capability_required" }, 409);
9562
10539
  try {
9563
- for (const op of reassignOps) parseReassignScopePayload(op);
10540
+ for (const op of reassignOps) {
10541
+ const reassignment = parseReassignScopePayload(op);
10542
+ scopeIds.add(reassignment.old_scope_id);
10543
+ scopeIds.add(reassignment.new_scope_id);
10544
+ }
9564
10545
  } catch {
9565
10546
  return c.json({ error: "reassign_payload_invalid" }, 400);
9566
10547
  }
@@ -9573,18 +10554,13 @@ function syncProtocolRoutes(getStore, opts = {}) {
9573
10554
  applyScopeFilter: false,
9574
10555
  supportsReassignScope: peerFeatures.includes("reassign_scope")
9575
10556
  });
9576
- const rejected = rejectInboundScopeFailures(store.db, normalizedOps, localDeviceId, {
10557
+ const rejected = rejectUnauthorizedInboundBatch(c, normalizedOps, scopeContext, {
9577
10558
  enabled: negotiated !== "unsupported",
9578
- peerDeviceId
10559
+ scopeId: scopeRequest.scope_id,
10560
+ managedScopeIds: scopeIds
9579
10561
  });
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);
10562
+ if (rejected) return rejected;
10563
+ const result = applyInboundSyncOps(store, filtered.allowed, localDeviceId);
9588
10564
  const skipped = result.skipped + filtered.skipped;
9589
10565
  return c.json({
9590
10566
  ...result,
@@ -9785,10 +10761,15 @@ function wakeRecipientPolicyProjects(store, projectIdentities, now) {
9785
10761
  wakeRecipientPoliciesForProjectIdentities(store.db, [...new Set(projectIdentities)], now);
9786
10762
  }
9787
10763
  function saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs) {
10764
+ store.adoptEnsuredDeviceIdentity(deviceId);
9788
10765
  return store.db.transaction(() => {
9789
10766
  const before = policyWakeMappings(store);
9790
10767
  const previousMappings = existingRequestedMappings(before, mappingInputs);
9791
- const mappings = upsertProjectScopeSettingsMappings(store.db, mappingInputs, { deviceId });
10768
+ const writableScopeIds = new Set(resolveVisibleScopeIds(store.db, store.deviceId, store.scopeResolutionDeviceContext()));
10769
+ const mappings = upsertProjectScopeSettingsMappings(store.db, mappingInputs, {
10770
+ deviceId,
10771
+ canWriteScope: (scopeId) => writableScopeIds.has(scopeId ?? "")
10772
+ });
9792
10773
  const after = policyWakeMappings(store);
9793
10774
  const changedScopeIds = /* @__PURE__ */ new Set([...previousMappings.map((mapping) => mapping.scope_id), ...mappings.map((mapping) => mapping.scope_id)]);
9794
10775
  wakeRecipientPolicyProjects(store, [...mappedProjectsInScopes(before, changedScopeIds), ...mappedProjectsInScopes(after, changedScopeIds)], (/* @__PURE__ */ new Date()).toISOString());
@@ -9796,12 +10777,14 @@ function saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs) {
9796
10777
  }).immediate();
9797
10778
  }
9798
10779
  function deleteProjectMappingAndWakePolicies(store, mappingId, deviceId, confirmedGuardrailTokens) {
10780
+ store.adoptEnsuredDeviceIdentity(deviceId);
9799
10781
  return store.db.transaction(() => {
9800
10782
  const before = policyWakeMappings(store);
9801
10783
  const removed = before.find((mapping) => mapping.id === mappingId);
9802
10784
  const deleted = deleteProjectScopeSettingsMapping(store.db, mappingId, {
9803
10785
  deviceId,
9804
- confirmedGuardrailTokens
10786
+ confirmedGuardrailTokens,
10787
+ canWriteScope: (scopeId) => store.isScopeWritable(scopeId)
9805
10788
  });
9806
10789
  if (deleted && removed) {
9807
10790
  const changedScopeIds = /* @__PURE__ */ new Set([removed.scope_id]);
@@ -9817,8 +10800,125 @@ function deleteProjectMappingAndWakePolicies(store, mappingId, deviceId, confirm
9817
10800
  * provide sync status, peer management, and coordinator UI for the
9818
10801
  * local viewer.
9819
10802
  */
10803
+ function reassignProjectInventoryForUser(store, input) {
10804
+ store.adoptEnsuredDeviceIdentity(input.deviceId);
10805
+ return reassignProjectScopeInventoryProject(store.db, {
10806
+ ...input,
10807
+ canWriteScope: (scopeId) => store.isScopeWritable(scopeId)
10808
+ });
10809
+ }
10810
+ function pendingCaptureActorMutationFailure(store) {
10811
+ try {
10812
+ store.assertPendingLocalCaptureAdoptionActors();
10813
+ return null;
10814
+ } catch (error) {
10815
+ if (isPendingCaptureActorConflict(error)) return {
10816
+ error: error.message,
10817
+ status: 409
10818
+ };
10819
+ throw error;
10820
+ }
10821
+ }
10822
+ function runViewerActorMutation(store, mutate) {
10823
+ return store.db.transaction(() => pendingCaptureActorMutationFailure(store) ?? mutate()).immediate();
10824
+ }
10825
+ function actorMergeDigest(primaryActorId, secondaryActorId) {
10826
+ return createHash("sha256").update(JSON.stringify({
10827
+ kind: "identity-merge-v1",
10828
+ primaryActorId,
10829
+ secondaryActorId,
10830
+ nonce: randomUUID()
10831
+ })).digest("hex");
10832
+ }
10833
+ function actorMergePrincipalFailure(store, primary, secondary) {
10834
+ if (primary.status !== "active") return {
10835
+ error: "primary actor not active",
10836
+ status: 409
10837
+ };
10838
+ if (secondary.status !== "active") return {
10839
+ error: "secondary actor not active",
10840
+ status: 409
10841
+ };
10842
+ if (secondary.is_local && secondary.actor_id === store.actorId) return {
10843
+ error: "cannot merge this device's own local actor",
10844
+ status: 409
10845
+ };
10846
+ return null;
10847
+ }
10848
+ function resolvePeerIdentityAssignment(store, opts) {
10849
+ const requestedActorId = opts.body.actor_id == null ? void 0 : String(opts.body.actor_id ?? "").trim() || null;
10850
+ const claimedLocalActor = typeof opts.body.claimed_local_actor === "boolean" ? opts.body.claimed_local_actor : void 0;
10851
+ let nextActorId = opts.actorId;
10852
+ let nextClaimedLocalActor = opts.claimedLocalActor;
10853
+ if (requestedActorId !== void 0) {
10854
+ nextActorId = requestedActorId;
10855
+ if (requestedActorId) {
10856
+ const actor = drizzle(store.db, { schema }).select({
10857
+ actor_id: schema.actors.actor_id,
10858
+ is_local: schema.actors.is_local,
10859
+ status: schema.actors.status
10860
+ }).from(schema.actors).where(eq(schema.actors.actor_id, requestedActorId)).get();
10861
+ if (!actor) return {
10862
+ error: "actor not found",
10863
+ status: 404
10864
+ };
10865
+ if (actor.status !== "active") return {
10866
+ error: "actor not active",
10867
+ status: 409
10868
+ };
10869
+ nextClaimedLocalActor = claimedLocalActor ?? Boolean(actor.is_local);
10870
+ } else nextClaimedLocalActor = claimedLocalActor ?? false;
10871
+ } else if (claimedLocalActor !== void 0) {
10872
+ nextClaimedLocalActor = claimedLocalActor;
10873
+ if (claimedLocalActor) nextActorId = store.actorId;
10874
+ else if (nextActorId === store.actorId) nextActorId = null;
10875
+ }
10876
+ return {
10877
+ actor_id: nextActorId,
10878
+ claimed_local_actor: nextClaimedLocalActor
10879
+ };
10880
+ }
10881
+ function persistPeerIdentityMutation(store, body) {
10882
+ const peerDeviceId = String(body.peer_device_id ?? "").trim();
10883
+ if (!peerDeviceId) return {
10884
+ error: "peer_device_id required",
10885
+ status: 400
10886
+ };
10887
+ const d = drizzle(store.db, { schema });
10888
+ const peer = d.select({
10889
+ peer_device_id: schema.syncPeers.peer_device_id,
10890
+ actor_id: schema.syncPeers.actor_id,
10891
+ claimed_local_actor: schema.syncPeers.claimed_local_actor
10892
+ }).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get();
10893
+ if (!peer) return {
10894
+ error: "peer not found",
10895
+ status: 404
10896
+ };
10897
+ const assignment = resolvePeerIdentityAssignment(store, {
10898
+ body,
10899
+ actorId: peer.actor_id ?? null,
10900
+ claimedLocalActor: Boolean(peer.claimed_local_actor)
10901
+ });
10902
+ if ("error" in assignment) return assignment;
10903
+ d.update(schema.syncPeers).set({
10904
+ actor_id: assignment.actor_id,
10905
+ claimed_local_actor: assignment.claimed_local_actor ? 1 : 0
10906
+ }).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();
10907
+ return assignment;
10908
+ }
10909
+ function createActorMutationPreflightRoutes(getStore) {
10910
+ const app = new Hono();
10911
+ const preflight = async (c, next) => {
10912
+ if (!ensureLocalActorRecord(getStore())) return c.json({ error: "device_adoption_actor_conflict" }, 409);
10913
+ await next();
10914
+ };
10915
+ app.post("/api/sync/peers/identity", preflight);
10916
+ app.post("/api/sync/actors/merge", preflight);
10917
+ return app;
10918
+ }
9820
10919
  function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
9821
- const app = new Hono().post("/api/sync/invites/inspect", inspectPairingInvite);
10920
+ const app = createActorMutationPreflightRoutes(getStore);
10921
+ app.post("/api/sync/invites/inspect", inspectPairingInvite);
9822
10922
  const readCoordinatorConfig = options.readCoordinatorConfig ?? readCoordinatorSyncConfig;
9823
10923
  const renameCoordinatorGroup = options.renameCoordinatorGroup ?? coordinatorRenameGroupAction;
9824
10924
  const loadCoordinatorEvidence = options.loadDeviceIdentityCoordinatorEvidence ?? loadConfiguredDeviceIdentityCoordinatorEvidence;
@@ -9852,7 +10952,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
9852
10952
  };
9853
10953
  app.get("/api/sync/recipient-policy/v1/device-inventory", async (c) => {
9854
10954
  const store = getStore();
9855
- ensureStableStoreIdentity(store);
10955
+ ensureStableStoreIdentityForRead(store);
9856
10956
  return c.json(listDeviceIdentityInventory(store.db, await localInventoryInput(store)));
9857
10957
  });
9858
10958
  app.patch("/api/sync/recipient-policy/v1/teams/:teamId", async (c) => {
@@ -9944,7 +11044,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
9944
11044
  if (!body) return c.json({ error: "binding_request_invalid" }, 400);
9945
11045
  const store = getStore();
9946
11046
  try {
9947
- ensureStableStoreIdentity(store);
11047
+ ensureStableStoreIdentityForRead(store);
9948
11048
  const result = previewDeviceIdentityBindings(store.db, await localInventoryInput(store), body);
9949
11049
  if (result.status === "invalid") return c.json(result, 400);
9950
11050
  if (result.status === "not_found") return c.json(result, 404);
@@ -9985,6 +11085,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
9985
11085
  }
9986
11086
  });
9987
11087
  app.get("/api/sync/status", async (c) => {
11088
+ const loadDiagnosticKey = createPeerScopeDiagnosticKeyLoader();
9988
11089
  const response = await buildSyncStatusResponse({
9989
11090
  store: getStore(),
9990
11091
  showDiagnostics: queryBool(c.req.query("includeDiagnostics")),
@@ -9996,7 +11097,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
9996
11097
  isRecentIso,
9997
11098
  legacySharedReviewSummary,
9998
11099
  listRecipientPolicyReconciliationStatus,
9999
- mapPeerRow,
11100
+ mapPeerRow: (...args) => mapPeerRow(...args, loadDiagnosticKey),
10000
11101
  mapSyncAttemptRow,
10001
11102
  peerStatus,
10002
11103
  readViewerBinding,
@@ -10013,11 +11114,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10013
11114
  const store = getStore();
10014
11115
  {
10015
11116
  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));
11117
+ const peers = readPeerScopeDiagnostics(store, showDiag);
10021
11118
  return c.json({
10022
11119
  items: peers,
10023
11120
  redacted: !showDiag
@@ -10407,7 +11504,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10407
11504
  const body = await parseViewerJsonBody(c);
10408
11505
  const request = parseReviewRequest(body);
10409
11506
  if (!request) return c.json({ error: "request_invalid" }, 400);
10410
- ensureLocalActorRecord(store);
11507
+ requireLocalActorRecord(store);
10411
11508
  const result = resolveRecipientPolicyReview(store.db, {
10412
11509
  localActorId: store.actorId,
10413
11510
  localDeviceId: store.deviceId
@@ -10423,7 +11520,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10423
11520
  if (!body || !Array.isArray(body.requests) || body.requests.length === 0 || body.requests.length > 100) return c.json({ error: "request_invalid" }, 400);
10424
11521
  const requests = body.requests.map(parseReviewRequest);
10425
11522
  if (requests.some((request) => request == null)) return c.json({ error: "request_invalid" }, 400);
10426
- ensureLocalActorRecord(store);
11523
+ requireLocalActorRecord(store);
10427
11524
  const result = resolveRecipientPolicyReviewBulk(store.db, {
10428
11525
  localActorId: store.actorId,
10429
11526
  localDeviceId: store.deviceId
@@ -10453,7 +11550,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10453
11550
  const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity") ?? "";
10454
11551
  const project = optionalViewerStrictString(body, "project") ?? "";
10455
11552
  const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
10456
- const result = reassignProjectScopeInventoryProject(store.db, {
11553
+ const result = reassignProjectInventoryForUser(store, {
10457
11554
  deviceId,
10458
11555
  project,
10459
11556
  workspaceIdentity
@@ -10758,51 +11855,17 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10758
11855
  app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => deleteProjectScopeMappingRoute(c, getStore()));
10759
11856
  app.post("/api/sync/peers/identity", async (c) => {
10760
11857
  const store = getStore();
10761
- ensureLocalActorRecord(store);
10762
11858
  let body;
10763
11859
  try {
10764
11860
  body = await c.req.json();
10765
11861
  } catch {
10766
11862
  return c.json({ error: "invalid json" }, 400);
10767
11863
  }
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();
11864
+ const result = runViewerActorMutation(store, () => persistPeerIdentityMutation(store, body));
11865
+ if ("error" in result) return c.json({ error: result.error }, result.status);
10802
11866
  return c.json({
10803
11867
  ok: true,
10804
- actor_id: nextActorId,
10805
- claimed_local_actor: nextClaimedLocalActor
11868
+ ...result
10806
11869
  });
10807
11870
  });
10808
11871
  app.post("/api/sync/actors", async (c) => {
@@ -10869,7 +11932,6 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10869
11932
  });
10870
11933
  app.post("/api/sync/actors/merge", async (c) => {
10871
11934
  const store = getStore();
10872
- ensureLocalActorRecord(store);
10873
11935
  let body;
10874
11936
  try {
10875
11937
  body = await c.req.json();
@@ -10886,24 +11948,16 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10886
11948
  queueMicrotask(releaseActorMutations);
10887
11949
  const now = (/* @__PURE__ */ new Date()).toISOString();
10888
11950
  const result = store.db.transaction(() => {
11951
+ const conflict = pendingCaptureActorMutationFailure(store);
11952
+ if (conflict) return conflict;
10889
11953
  const primary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, primaryActorId)).get();
10890
11954
  const secondary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, secondaryActorId)).get();
10891
11955
  if (!primary || !secondary) return {
10892
11956
  error: "actor not found",
10893
11957
  status: 404
10894
11958
  };
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
- };
11959
+ const principalFailure = actorMergePrincipalFailure(store, primary, secondary);
11960
+ if (principalFailure) return principalFailure;
10907
11961
  const memberships = d.select().from(schema.policyTeamMemberships).where(eq(schema.policyTeamMemberships.identity_id, secondaryActorId)).all();
10908
11962
  const primaryMemberships = /* @__PURE__ */ new Map();
10909
11963
  for (const membership of memberships) {
@@ -10924,12 +11978,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10924
11978
  };
10925
11979
  primaryRecipients.set(recipient.canonical_project_identity, primaryRecipient);
10926
11980
  }
10927
- const mergeDigest = createHash("sha256").update(JSON.stringify({
10928
- kind: "identity-merge-v1",
10929
- primaryActorId,
10930
- secondaryActorId,
10931
- nonce: randomUUID()
10932
- })).digest("hex");
11981
+ const mergeDigest = actorMergeDigest(primaryActorId, secondaryActorId);
10933
11982
  const bindings = d.select({
10934
11983
  deviceId: schema.identityDevices.device_id,
10935
11984
  assignmentVersion: schema.identityDevices.assignment_version
@@ -11146,7 +12195,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
11146
12195
  if (!payload.operation_id && !recipientInvite) return c.json({ kind: "legacy_team_invite" });
11147
12196
  const [status, inspected] = await requestJson("POST", `${buildBaseUrl(payload.coordinator_url)}/v1/invites/inspect`, {
11148
12197
  body: { token: payload.token },
11149
- timeoutS: 10
12198
+ timeoutS: 10,
12199
+ redirect: "error"
11150
12200
  });
11151
12201
  if (status < 200 || status >= 300) return c.json({ error: recipientInvite ? safeRecipientInviteError(inspected?.error) : String(inspected?.error ?? "invite_invalid") }, status === 410 ? 410 : 400);
11152
12202
  const config = readCodememConfigFile();
@@ -12541,6 +13591,122 @@ function updateStatusRoutes(options = {}) {
12541
13591
  return app;
12542
13592
  }
12543
13593
  //#endregion
13594
+ //#region src/account/context.ts
13595
+ function validId(value) {
13596
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
13597
+ }
13598
+ function label(value) {
13599
+ return validId(value) ? value : void 0;
13600
+ }
13601
+ function clean(value) {
13602
+ return typeof value === "string" ? value.trim() : "";
13603
+ }
13604
+ function configuredGroups(raw) {
13605
+ const value = process.env.CODEMEM_SYNC_COORDINATOR_GROUPS ?? raw.sync_coordinator_groups;
13606
+ if (value === void 0 || value === null || value === "") return [];
13607
+ const values = typeof value === "string" ? value.split(",") : value;
13608
+ if (!Array.isArray(values) || values.some((item) => !validId(clean(item)))) return null;
13609
+ return values.map(clean);
13610
+ }
13611
+ function readyContext(options, groupId, coordinatorUrl, raw, groups) {
13612
+ const profile = options.getProfile();
13613
+ if (!profile) return "device_unavailable";
13614
+ const before = statSync(profile.dbPath, { bigint: true });
13615
+ const device = readAccountLinkDevice(profile.dbPath);
13616
+ const after = statSync(profile.dbPath, { bigint: true });
13617
+ const override = process.env.CODEMEM_DEVICE_ID?.trim() || void 0;
13618
+ if (!before.isFile() || before.dev !== after.dev || before.ino !== after.ino || device.deviceId !== profile.deviceId || override !== void 0 && override !== device.deviceId) return "device_unavailable";
13619
+ let actor = clean(raw.actor_id);
13620
+ if (Object.hasOwn(process.env, "CODEMEM_ACTOR_ID")) actor = clean(process.env.CODEMEM_ACTOR_ID);
13621
+ actor ||= `local:${device.deviceId}`;
13622
+ if (!validId(actor) || actor !== profile.identityId) return "identity_unavailable";
13623
+ const context = {
13624
+ dbPath: profile.dbPath,
13625
+ keysDir: options.keysDir,
13626
+ configPath: options.configPath,
13627
+ coordinatorUrl,
13628
+ groupId,
13629
+ deviceId: device.deviceId,
13630
+ fingerprint: device.fingerprint,
13631
+ identityId: actor,
13632
+ identityLabel: label(profile.identityLabel),
13633
+ deviceLabel: label(profile.deviceLabel),
13634
+ fileIdentity: {
13635
+ dev: after.dev.toString(),
13636
+ ino: after.ino.toString()
13637
+ }
13638
+ };
13639
+ const contextId = createHash("sha256").update(JSON.stringify({
13640
+ context,
13641
+ groups,
13642
+ actorConfig: clean(raw.actor_id),
13643
+ actorEnv: process.env.CODEMEM_ACTOR_ID,
13644
+ deviceEnv: override,
13645
+ keyStore: process.env.CODEMEM_SYNC_KEY_STORE,
13646
+ home: process.env.HOME
13647
+ })).digest("hex").slice(0, 32);
13648
+ return {
13649
+ ...context,
13650
+ contextId
13651
+ };
13652
+ }
13653
+ function readContext(options, groupId) {
13654
+ const raw = options.readConfig();
13655
+ const config = readCoordinatorSyncConfig(raw);
13656
+ if (!configuredGroups(raw)) return { error: "account_context_unavailable" };
13657
+ const groups = [...new Set(config.syncCoordinatorGroups)];
13658
+ if (groups.length > 25 || groups.some((id) => !validId(id))) return { error: "account_context_unavailable" };
13659
+ if (groupId !== void 0 && !groups.includes(groupId)) return { error: "account_group_not_configured" };
13660
+ const selectedGroupId = groupId ?? (groups.length === 1 ? groups[0] : null) ?? null;
13661
+ const origin = normalizeCoordinatorAccountLinkOrigin(config.syncCoordinatorUrl);
13662
+ const base = {
13663
+ groups,
13664
+ selectedGroupId,
13665
+ coordinator: origin ? { origin } : null,
13666
+ context: null
13667
+ };
13668
+ if (!config.syncCoordinatorUrl || groups.length === 0) return {
13669
+ ...base,
13670
+ setup: "coordinator_not_configured"
13671
+ };
13672
+ if (!origin) return {
13673
+ ...base,
13674
+ setup: "coordinator_url_invalid"
13675
+ };
13676
+ if (!selectedGroupId) return {
13677
+ ...base,
13678
+ setup: "group_selection_required"
13679
+ };
13680
+ let context;
13681
+ try {
13682
+ context = readyContext(options, selectedGroupId, origin, raw, groups);
13683
+ } catch {
13684
+ return {
13685
+ ...base,
13686
+ setup: "device_unavailable"
13687
+ };
13688
+ }
13689
+ if (typeof context === "string") return {
13690
+ ...base,
13691
+ setup: context
13692
+ };
13693
+ return {
13694
+ ...base,
13695
+ setup: "ready",
13696
+ context
13697
+ };
13698
+ }
13699
+ /** Trusted callbacks describe the running profile, never a browser-selected path. */
13700
+ function createAccountContextReader(options) {
13701
+ return (groupId) => {
13702
+ try {
13703
+ return readContext(options, groupId);
13704
+ } catch {
13705
+ return { error: "account_context_unavailable" };
13706
+ }
13707
+ };
13708
+ }
13709
+ //#endregion
12544
13710
  //#region src/index.ts
12545
13711
  /**
12546
13712
  * @codemem/server — HTTP server (viewer, sync, API).
@@ -12590,6 +13756,21 @@ function registerConfigRoutes(app, opts, sweeper) {
12590
13756
  getObserverApplyStatus: opts?.getObserverApplyStatus
12591
13757
  }));
12592
13758
  }
13759
+ function registerAccountRoutes(app, options) {
13760
+ if (!options) return;
13761
+ const account = createAccountGuard(options);
13762
+ app.use("*", account.middleware);
13763
+ app.route("/", account.routes);
13764
+ if (options.readContext) app.route("/", accountStatusRoutes({
13765
+ ...options,
13766
+ readContext: options.readContext
13767
+ }));
13768
+ if (options.readContext && options.registry) app.route("/", accountOperationRoutes({
13769
+ ...options,
13770
+ readContext: options.readContext,
13771
+ registry: options.registry
13772
+ }));
13773
+ }
12593
13774
  function createApp(opts) {
12594
13775
  const storeFactory = opts?.storeFactory ?? getStore;
12595
13776
  const sweeper = opts?.sweeper ?? null;
@@ -12597,6 +13778,7 @@ function createApp(opts) {
12597
13778
  const getSyncRuntimeStatus = opts?.getSyncRuntimeStatus ?? (() => null);
12598
13779
  let invalidateTeamSetupSummary = () => {};
12599
13780
  const app = new Hono();
13781
+ registerAccountRoutes(app, opts?.accountLink);
12600
13782
  app.use("*", preflightHandler());
12601
13783
  app.use("*", originGuard({ unsafeGetPathPrefixes: [TEAM_SETUP_ROUTE_PREFIX] }));
12602
13784
  const viewerTargetGuard = viewerTargetQueryGuard(storeFactory);
@@ -12608,6 +13790,7 @@ function createApp(opts) {
12608
13790
  app.route("/", memoryRoutes(storeFactory));
12609
13791
  app.route("/", memoryToolRoutes(storeFactory));
12610
13792
  app.route("/", packTransportRoutes(storeFactory));
13793
+ app.route("/", fileContextRoutes(storeFactory));
12611
13794
  app.route("/", observerStatusRoutes({
12612
13795
  getStore: storeFactory,
12613
13796
  getSweeper: () => sweeper,
@@ -12662,6 +13845,6 @@ function createSyncApp(opts) {
12662
13845
  return app;
12663
13846
  }
12664
13847
  //#endregion
12665
- export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory };
13848
+ export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, createAccountContextReader, createAccountLinkRegistry, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory };
12666
13849
 
12667
13850
  //# sourceMappingURL=index.js.map