@codemem/server 0.39.1 → 0.40.0

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,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { ACCESS_CLEANUP_OP_TYPE, CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_CAPABILITY_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, diagnoseStalePeerReceivedRows, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isScopedSyncCapability, isSummaryLikeMemory, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeAddress, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripPrivateObj, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifySignature, writeCodememConfigFile } from "@codemem/core";
3
+ import { ACCESS_CLEANUP_OP_TYPE, CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, MemoryStore, RecipientPolicyEdgeRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildAuthHeaders, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, coerceObserverCommand, commitRecipientPolicyEdges, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, diagnoseStalePeerReceivedRows, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, inviteTokenDigest, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, isSummaryLikeMemory, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, migrateRecipientPolicyIntent, negotiateSyncCapability, normalizeAddress, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parseReassignScopePayload, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectShareLifecycle, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, recordNonce, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, rejectInboundScopeFailures, requestJson, resolveDbPath, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, stripPrivateObj, summarizeInboundScopeRejections, supportsSyncFeature, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifyRecipientReviewedIntent, verifySignature, writeCodememConfigFile } from "@codemem/core";
4
4
  import { serveStatic } from "@hono/node-server/serve-static";
5
5
  import { Hono } from "hono";
6
6
  import { createMiddleware } from "hono/factory";
@@ -8,7 +8,7 @@ import { RateLimiter } from "limiter";
8
8
  import { and, count, desc, eq, inArray, max, ne } from "drizzle-orm";
9
9
  import { drizzle } from "drizzle-orm/better-sqlite3";
10
10
  import { createHash, randomUUID } from "node:crypto";
11
- import { networkInterfaces } from "node:os";
11
+ import { hostname, networkInterfaces } from "node:os";
12
12
  //#region src/middleware.ts
13
13
  var LOOPBACK_HOSTS = new Set([
14
14
  "127.0.0.1",
@@ -164,7 +164,11 @@ function createInMemoryRequestRateLimiter(windowMs = 6e4) {
164
164
  * codemem/viewer_routes/config.py, scoped to the TS runtime's current needs.
165
165
  */
166
166
  var REDACTED_VALUE = "[redacted]";
167
- var RUNTIMES = new Set(["api_http", "claude_sidecar"]);
167
+ var RUNTIMES = new Set([
168
+ "api_http",
169
+ "claude_sidecar",
170
+ "codex_sidecar"
171
+ ]);
168
172
  var AUTH_SOURCES = new Set([
169
173
  "auto",
170
174
  "env",
@@ -175,6 +179,7 @@ var AUTH_SOURCES = new Set([
175
179
  var HOT_RELOAD_KEYS = new Set(["raw_events_sweeper_interval_s"]);
176
180
  var PROTECTED_WRITE_KEYS = new Set([
177
181
  "claude_command",
182
+ "codex_command",
178
183
  "observer_base_url",
179
184
  "observer_auth_file",
180
185
  "observer_auth_command",
@@ -190,12 +195,15 @@ var SECRET_CONFIG_KEYS = new Set([
190
195
  var REMOVED_CONFIG_KEYS = new Set(["observer_rich_openai_use_responses"]);
191
196
  var ALLOWED_KEYS = [
192
197
  "claude_command",
198
+ "codex_command",
193
199
  "observer_base_url",
194
200
  "observer_provider",
195
201
  "observer_model",
196
202
  "observer_tier_routing_enabled",
197
203
  "observer_simple_model",
198
204
  "observer_simple_temperature",
205
+ "observer_reasoning_effort",
206
+ "observer_reasoning_summary",
199
207
  "observer_rich_model",
200
208
  "observer_rich_temperature",
201
209
  "observer_rich_reasoning_effort",
@@ -227,6 +235,7 @@ var ALLOWED_KEYS = [
227
235
  ];
228
236
  var DEFAULTS = {
229
237
  claude_command: ["claude"],
238
+ codex_command: ["codex"],
230
239
  observer_runtime: "api_http",
231
240
  observer_auth_source: "auto",
232
241
  observer_tier_routing_enabled: false,
@@ -262,9 +271,12 @@ function getEffectiveConfig(configData) {
262
271
  ...DEFAULTS,
263
272
  ...configData
264
273
  };
274
+ for (const key of ["claude_command", "codex_command"]) effective[key] = coerceObserverCommand(effective[key]) ?? DEFAULTS[key];
265
275
  for (const [key, envVar] of Object.entries(CODEMEM_CONFIG_ENV_OVERRIDES)) {
266
276
  const val = process.env[envVar];
267
- if (val != null && val !== "") effective[key] = val;
277
+ if (val == null || val === "") continue;
278
+ if (key === "claude_command" || key === "codex_command") effective[key] = coerceObserverCommand(val) ?? effective[key];
279
+ else effective[key] = val;
268
280
  }
269
281
  return effective;
270
282
  }
@@ -358,7 +370,7 @@ function validateAndApplyUpdate(configData, key, value, providers) {
358
370
  if (key === "observer_runtime") {
359
371
  if (typeof value !== "string") return "observer_runtime must be string";
360
372
  const runtime = value.trim().toLowerCase();
361
- if (!RUNTIMES.has(runtime)) return "observer_runtime must be one of: api_http, claude_sidecar";
373
+ if (!RUNTIMES.has(runtime)) return "observer_runtime must be one of: api_http, claude_sidecar, codex_sidecar";
362
374
  configData[key] = runtime;
363
375
  return null;
364
376
  }
@@ -369,7 +381,7 @@ function validateAndApplyUpdate(configData, key, value, providers) {
369
381
  configData[key] = source;
370
382
  return null;
371
383
  }
372
- if (key === "claude_command" || key === "observer_auth_command") {
384
+ if (key === "claude_command" || key === "codex_command" || key === "observer_auth_command") {
373
385
  const argv = asExecutableArgv(value);
374
386
  if (argv == null) return `${key} must be string array`;
375
387
  if (argv.length > 0) configData[key] = argv;
@@ -393,7 +405,7 @@ function validateAndApplyUpdate(configData, key, value, providers) {
393
405
  configData[key] = value;
394
406
  return null;
395
407
  }
396
- if (key === "observer_base_url" || key === "observer_model" || key === "observer_simple_model" || key === "observer_rich_model" || key === "observer_rich_reasoning_effort" || key === "observer_rich_reasoning_summary" || key === "observer_auth_file" || key === "sync_host" || key === "sync_coordinator_url" || key === "sync_coordinator_group") {
408
+ if (key === "observer_base_url" || key === "observer_model" || key === "observer_simple_model" || key === "observer_reasoning_effort" || key === "observer_reasoning_summary" || key === "observer_rich_model" || key === "observer_rich_reasoning_effort" || key === "observer_rich_reasoning_summary" || key === "observer_auth_file" || key === "sync_host" || key === "sync_coordinator_url" || key === "sync_coordinator_group") {
397
409
  if (typeof value !== "string") return `${key} must be string`;
398
410
  const trimmed = value.trim();
399
411
  if (!trimmed) delete configData[key];
@@ -471,7 +483,7 @@ function configRoutes(opts = {}) {
471
483
  }
472
484
  const afterEffective = getEffectiveConfig(nextConfig);
473
485
  const savedChangedKeys = ALLOWED_KEYS.filter((key) => beforeConfig[key] !== nextConfig[key]);
474
- const effectiveChangedKeys = ALLOWED_KEYS.filter((key) => beforeEffective[key] !== afterEffective[key]);
486
+ const effectiveChangedKeys = ALLOWED_KEYS.filter((key) => !configValuesEqual(beforeEffective[key], afterEffective[key]));
475
487
  const envOverrides = getCodememEnvOverrides();
476
488
  const ignoredByEnvKeys = savedChangedKeys.filter((key) => !effectiveChangedKeys.includes(key) && key in envOverrides);
477
489
  const hotReloadedKeys = applyRuntimeEffects([...new Set([
@@ -1819,6 +1831,7 @@ async function ensureDefaultSpaceForTeam(opts) {
1819
1831
  return {
1820
1832
  scope,
1821
1833
  membership: await coordinatorGrantScopeMembershipAction({
1834
+ effectId: `team-default-grant:${opts.groupId}:${scopeId}:${deviceId}:1`,
1822
1835
  groupId: opts.groupId,
1823
1836
  scopeId,
1824
1837
  deviceId,
@@ -1851,6 +1864,7 @@ async function maybeGrantDefaultSpaceOnJoin(opts) {
1851
1864
  adminSecret: opts.config.syncCoordinatorAdminSecret || null
1852
1865
  })).find((scope) => scope.scope_id === scopeId) ?? null)?.kind !== "team_default") return null;
1853
1866
  return await coordinatorGrantScopeMembershipAction({
1867
+ effectId: `team-default-join:${opts.requestId}:${scopeId}:${opts.deviceId}:1`,
1854
1868
  groupId: opts.groupId,
1855
1869
  scopeId,
1856
1870
  deviceId: opts.deviceId,
@@ -1892,6 +1906,30 @@ function coordinatorAdminUnavailable(status) {
1892
1906
  };
1893
1907
  return null;
1894
1908
  }
1909
+ function recipientInviteCreationUnavailable(status, kind) {
1910
+ if (status.readiness === "not_configured") return {
1911
+ body: {
1912
+ error: "coordinator_not_configured",
1913
+ status
1914
+ },
1915
+ httpStatus: 400
1916
+ };
1917
+ if (!status.has_groups) return {
1918
+ body: {
1919
+ error: "coordinator_group_missing",
1920
+ status
1921
+ },
1922
+ httpStatus: 400
1923
+ };
1924
+ if (kind === "team_member" && !status.has_admin_secret) return {
1925
+ body: {
1926
+ error: "coordinator_admin_secret_missing",
1927
+ status
1928
+ },
1929
+ httpStatus: 400
1930
+ };
1931
+ return null;
1932
+ }
1895
1933
  async function parseViewerJsonBody(c, options = {}) {
1896
1934
  const raw = await c.req.text();
1897
1935
  if (!raw.trim()) return options.allowEmpty ? {} : null;
@@ -1929,6 +1967,21 @@ function optionalViewerNumber(body, key) {
1929
1967
  const number = typeof value === "number" ? value : Number(value);
1930
1968
  return Number.isFinite(number) ? Math.trunc(number) : NaN;
1931
1969
  }
1970
+ function coordinatorAdminMembershipEffectId(action, attemptId, input) {
1971
+ return `viewer-admin-membership:${action}:${createHash("sha256").update(JSON.stringify({
1972
+ action,
1973
+ attemptId,
1974
+ groupId: input.groupId,
1975
+ scopeId: input.scopeId,
1976
+ deviceId: input.deviceId,
1977
+ role: input.role ?? null,
1978
+ membershipEpoch: input.membershipEpoch,
1979
+ coordinatorId: input.coordinatorId ?? null,
1980
+ manifestIssuerDeviceId: input.manifestIssuerDeviceId ?? null,
1981
+ manifestHash: input.manifestHash,
1982
+ signedManifestJson: input.signedManifestJson
1983
+ })).digest("hex")}`;
1984
+ }
1932
1985
  function optionalViewerInteger(body, key) {
1933
1986
  const value = body[key];
1934
1987
  if (value == null || value === "") return null;
@@ -1955,7 +2008,7 @@ function parseViewerProjectMappingInput(body) {
1955
2008
  }
1956
2009
  function coordinatorAdminMutationStatus(message) {
1957
2010
  if (message.includes("scope_not_found") || message.includes("membership_not_found") || message.includes("group_not_found") || message.includes("Group not found") || message.includes("Scope not found") || message.includes("device_not_enrolled_for_scope_group") || message.includes("device must be enrolled")) return 404;
1958
- if (message.includes("not active") || message.includes("scope_not_active") || message.includes("group_archived") || message.includes("Group is archived")) return 409;
2011
+ if (message.includes("not active") || message.includes("scope_not_active") || message.includes("scope_membership_effect_conflict") || message.includes("group_archived") || message.includes("Group is archived")) return 409;
1959
2012
  if (message.includes("Remote coordinator request failed (5") || message.includes("fetch failed") || message.includes("network") || message.includes("timed out")) return 502;
1960
2013
  return 400;
1961
2014
  }
@@ -1976,6 +2029,1141 @@ function parseInviteTtlHours(value) {
1976
2029
  if (!Number.isInteger(ttlHours) || ttlHours < 1) return null;
1977
2030
  return ttlHours;
1978
2031
  }
2032
+ function projectInviteAcceptanceFailure(error) {
2033
+ if (isProjectSyncEnablementError(error)) return {
2034
+ error: error.code,
2035
+ detail: error.detail
2036
+ };
2037
+ const code = error instanceof Error ? error.message : "project_invite_acceptance_failed";
2038
+ const detailByCode = {
2039
+ invite_already_bound: "This invitation was accepted by another device. Ask the owner to create a new invitation.",
2040
+ invite_expired: "This invitation expired. Ask the owner to create a new invitation.",
2041
+ invite_identity_conflict: "This invitation does not match this Identity. Ask the owner to create a new invitation for this recipient.",
2042
+ invite_invalid: "This invitation is no longer valid. Ask the owner to create a new invitation.",
2043
+ inviter_identity_invalid: "The owner's device identity could not be verified. Ask the owner to review the share and create a new invitation.",
2044
+ project_invite_self_acceptance_forbidden: "The owner cannot accept this recipient invitation on the owner's device.",
2045
+ project_invite_bootstrap_incomplete: "The owner's device is not ready to establish trust yet. Retry once, then ask the owner to review the share.",
2046
+ project_invite_trust_state_invalid: "The owner's trust setup could not be verified. Ask the owner to review the share."
2047
+ };
2048
+ return {
2049
+ error: Object.hasOwn(detailByCode, code) ? code : "project_invite_acceptance_failed",
2050
+ detail: detailByCode[code] ?? "The invitation could not be accepted safely. Retry once, then ask the owner to review the share."
2051
+ };
2052
+ }
2053
+ var PROJECT_INVITE_TTL_HOURS = 168;
2054
+ var PROJECT_INVITE_BODY_KEYS = new Set([
2055
+ "teammate_name",
2056
+ "project_ids",
2057
+ "reviewed_project_set_digest"
2058
+ ]);
2059
+ function projectInviteStringList(body, key) {
2060
+ const value = body[key];
2061
+ if (!Array.isArray(value)) throw new Error(`${key}_must_be_string_array`);
2062
+ if (value.length > 100) throw new Error(`${key}_too_large`);
2063
+ const items = value.map((item) => {
2064
+ if (typeof item !== "string") throw new Error(`${key}_must_be_string_array`);
2065
+ return item.trim();
2066
+ });
2067
+ if (items.some((item) => !item)) throw new Error(`${key}_contains_empty_value`);
2068
+ if (new Set(items).size !== items.length) throw new Error(`${key}_contains_duplicates`);
2069
+ return items;
2070
+ }
2071
+ function assertProjectInviteBody(body, creating) {
2072
+ const allowed = creating ? PROJECT_INVITE_BODY_KEYS : new Set(["teammate_name", "project_ids"]);
2073
+ if (Object.keys(body).some((key) => !allowed.has(key))) throw new Error("unexpected_project_invite_fields");
2074
+ if (typeof body.teammate_name !== "string") throw new Error("teammate_name_must_be_string");
2075
+ if (creating && typeof body.reviewed_project_set_digest !== "string") throw new Error("reviewed_project_set_digest_required");
2076
+ if (creating && !/^[a-f0-9]{64}$/u.test(String(body.reviewed_project_set_digest).trim())) throw new Error("reviewed_project_set_digest_invalid");
2077
+ }
2078
+ function projectInviteStatus(config = readCoordinatorSyncConfig()) {
2079
+ const status = coordinatorAdminStatusPayload(config);
2080
+ if (status.readiness !== "ready") throw new Error("team_sharing_not_configured");
2081
+ if (status.groups.length !== 1) throw new Error("team_selection_ambiguous");
2082
+ if (!status.active_group) throw new Error("team_sharing_not_configured");
2083
+ return {
2084
+ groupId: status.active_group,
2085
+ config
2086
+ };
2087
+ }
2088
+ function recipientInviteKind(value) {
2089
+ if (value === "team_member" || value === "add_device") return value;
2090
+ throw new Error("recipient_invite_kind_invalid");
2091
+ }
2092
+ function localOnboardingBinding(store, deviceName, options = {}) {
2093
+ const deviceId = options.materializeActor === false ? ensureStableStoreDeviceIdentity(store) : ensureStableStoreIdentity(store);
2094
+ const devicePublicKey = String(store.db.prepare("SELECT public_key FROM sync_device WHERE device_id = ?").pluck().get(deviceId) ?? "").trim();
2095
+ if (!devicePublicKey) throw new Error("device_public_key_missing");
2096
+ const config = readCodememConfigFile();
2097
+ return {
2098
+ deviceId,
2099
+ devicePublicKey,
2100
+ deviceDisplayName: friendlyDeviceName({
2101
+ explicitName: deviceName ?? String(config.sync_device_name ?? ""),
2102
+ osName: hostname(),
2103
+ fallbackSeed: deviceId
2104
+ })
2105
+ };
2106
+ }
2107
+ function recipientInviteOnboardingPreview(store, body, invitationId) {
2108
+ const kind = recipientInviteKind(body.kind ?? body.invite_kind);
2109
+ const binding = localOnboardingBinding(store, optionalViewerString(body, "device_name"));
2110
+ const teamId = optionalViewerStrictString(body, "policy_team_id");
2111
+ const targetIdentityId = optionalViewerStrictString(body, "target_identity_id");
2112
+ if (kind === "team_member") {
2113
+ if (!teamId || targetIdentityId) throw new Error("recipient_invite_metadata_invalid");
2114
+ return previewRecipientPolicyOnboarding(store.db, {
2115
+ version: 1,
2116
+ journey: "team",
2117
+ invitationId,
2118
+ identityId: store.actorId,
2119
+ ...binding,
2120
+ teamId
2121
+ });
2122
+ }
2123
+ if (!targetIdentityId || teamId) throw new Error("recipient_invite_metadata_invalid");
2124
+ return previewRecipientPolicyOnboarding(store.db, {
2125
+ version: 1,
2126
+ journey: "add_device",
2127
+ invitationId,
2128
+ identityId: targetIdentityId,
2129
+ ...binding
2130
+ });
2131
+ }
2132
+ function recipientInviteReviewedIntent(store, kind, targetId, preview) {
2133
+ if (kind === "team_member") {
2134
+ if (preview.journey !== "team" || !preview.team || preview.team.teamId !== targetId) throw new Error("recipient_invite_intent_mismatch");
2135
+ return {
2136
+ version: 1,
2137
+ journey: "team",
2138
+ team: preview.team,
2139
+ projects: preview.projects,
2140
+ excludedProjects: []
2141
+ };
2142
+ }
2143
+ if (preview.journey !== "add_device" || preview.binding.identityId !== targetId) throw new Error("recipient_invite_intent_mismatch");
2144
+ const targetIdentity = store.db.prepare(`SELECT display_name FROM actors
2145
+ WHERE actor_id = ? AND status = 'active' AND merged_into_actor_id IS NULL`).get(targetId);
2146
+ if (!targetIdentity?.display_name) throw new Error("identity_not_found");
2147
+ return {
2148
+ version: 1,
2149
+ journey: "add_device",
2150
+ targetIdentity: {
2151
+ identityId: targetId,
2152
+ displayName: targetIdentity.display_name
2153
+ },
2154
+ projects: preview.projects,
2155
+ excludedProjects: preview.excludedProjects
2156
+ };
2157
+ }
2158
+ function recipientInviteOnboardingPreviewFromReviewedIntent(store, body, invitationId, reviewedIntent) {
2159
+ const kind = recipientInviteKind(body.kind ?? body.invite_kind);
2160
+ const binding = localOnboardingBinding(store, optionalViewerString(body, "device_name"), { materializeActor: false });
2161
+ const teamId = optionalViewerStrictString(body, "policy_team_id");
2162
+ const targetIdentityId = optionalViewerStrictString(body, "target_identity_id");
2163
+ const assignedIdentityId = optionalViewerStrictString(body, "assigned_identity_id");
2164
+ if (kind === "team_member") {
2165
+ if (!teamId || !assignedIdentityId || targetIdentityId) throw new Error("recipient_invite_metadata_invalid");
2166
+ return previewRecipientPolicyOnboardingFromReviewedIntent(reviewedIntent, {
2167
+ version: 1,
2168
+ journey: "team",
2169
+ invitationId,
2170
+ identityId: assignedIdentityId,
2171
+ ...binding,
2172
+ teamId
2173
+ });
2174
+ }
2175
+ if (!targetIdentityId || teamId || assignedIdentityId) throw new Error("recipient_invite_metadata_invalid");
2176
+ return previewRecipientPolicyOnboardingFromReviewedIntent(reviewedIntent, {
2177
+ version: 1,
2178
+ journey: "add_device",
2179
+ invitationId,
2180
+ identityId: targetIdentityId,
2181
+ ...binding
2182
+ });
2183
+ }
2184
+ var SAFE_RECIPIENT_INVITE_ERRORS = new Set([
2185
+ "invite_already_bound",
2186
+ "invite_expired",
2187
+ "invite_identity_conflict",
2188
+ "invite_invalid",
2189
+ "recipient_invite_intent_mismatch",
2190
+ "recipient_invite_review_unavailable",
2191
+ "reviewed_onboarding_stale"
2192
+ ]);
2193
+ function safeRecipientInviteError(value, fallback = "invite_invalid") {
2194
+ const code = value instanceof Error ? value.message : String(value ?? "").trim();
2195
+ return SAFE_RECIPIENT_INVITE_ERRORS.has(code) ? code : fallback;
2196
+ }
2197
+ function recipientInvitePreviewId(kind, targetId) {
2198
+ return `recipient-invite-preview:${kind}:${targetId}`;
2199
+ }
2200
+ function recipientPolicyCapabilityFromStatus(payload) {
2201
+ if (!isScopedSyncCapability(normalizeSyncCapability(payload.sync_capability))) return "unsupported";
2202
+ return supportsSyncFeature(payload.sync_features, "reassign_scope") ? "supported" : "unsupported";
2203
+ }
2204
+ async function peerSupportsSyncRequirements(store, deviceId, requirements) {
2205
+ const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
2206
+ if (deviceId === localDeviceId) return "supported";
2207
+ const row = store.db.prepare("SELECT addresses_json, pinned_fingerprint, public_key FROM sync_peers WHERE peer_device_id = ?").get(deviceId);
2208
+ const expectedFingerprint = String(row?.pinned_fingerprint ?? "").trim() || (row?.public_key ? fingerprintPublicKey(row.public_key) : "");
2209
+ const addresses = safeJsonList(row?.addresses_json ?? null);
2210
+ for (const address of addresses) try {
2211
+ const url = `${buildBaseUrl(address)}/v1/status`;
2212
+ const [status, payload] = await requestJson("GET", url, {
2213
+ headers: {
2214
+ ...buildAuthHeaders({
2215
+ deviceId: localDeviceId,
2216
+ method: "GET",
2217
+ url,
2218
+ bodyBytes: Buffer.alloc(0),
2219
+ keysDir: syncKeysDir()
2220
+ }),
2221
+ [SYNC_CAPABILITY_HEADER]: LOCAL_SYNC_CAPABILITY,
2222
+ [SYNC_FEATURES_HEADER]: LOCAL_SYNC_FEATURES.join(",")
2223
+ },
2224
+ timeoutS: 5
2225
+ });
2226
+ if (status < 200 || status >= 300) continue;
2227
+ if (String(payload?.device_id ?? "").trim() !== deviceId) continue;
2228
+ if (expectedFingerprint && String(payload?.fingerprint ?? "").trim() !== expectedFingerprint) continue;
2229
+ if (requirements.scoped && requirements.reassignScope && recipientPolicyCapabilityFromStatus(payload ?? {}) !== "supported") return "unsupported";
2230
+ if (requirements.scoped && !isScopedSyncCapability(normalizeSyncCapability(payload?.sync_capability))) return "unsupported";
2231
+ if (requirements.reassignScope && !supportsSyncFeature(payload?.sync_features, "reassign_scope")) return "unsupported";
2232
+ return "supported";
2233
+ } catch {}
2234
+ return "undetermined";
2235
+ }
2236
+ async function peerSupportsReassignScope(store, deviceId) {
2237
+ return peerSupportsSyncRequirements(store, deviceId, {
2238
+ scoped: false,
2239
+ reassignScope: true
2240
+ });
2241
+ }
2242
+ function resolveProjectInviteSelection(store, requestedIds, initiatingDeviceId) {
2243
+ if (requestedIds.length === 0) throw new Error("project_selection_empty");
2244
+ const requested = new Set(requestedIds);
2245
+ const byIdentity = /* @__PURE__ */ new Map();
2246
+ let offset = 0;
2247
+ while (byIdentity.size < requested.size) {
2248
+ const page = listProjectScopeInventory(store.db, {
2249
+ limit: 250,
2250
+ offset
2251
+ });
2252
+ for (const project of page.projects) if (requested.has(project.workspace_identity)) byIdentity.set(project.workspace_identity, project);
2253
+ if (!page.has_more) break;
2254
+ offset += page.limit;
2255
+ }
2256
+ return requestedIds.map((projectId) => {
2257
+ const project = byIdentity.get(projectId);
2258
+ if (!project) throw new Error("project_selection_unknown");
2259
+ if (project.guardrail_warnings.some((warning) => warning.code === "basename_collision_review" && warning.requires_confirmation)) throw new Error("project_selection_ambiguous");
2260
+ if (project.read_only || project.identity_source === "unmapped" || project.memory_count == null || project.guardrail_warnings.some((warning) => warning.requires_confirmation)) throw new Error("project_selection_unsupported");
2261
+ return {
2262
+ canonicalIdentity: project.workspace_identity,
2263
+ displayName: project.display_project,
2264
+ identitySource: project.identity_source,
2265
+ existingMemoryCount: countShareableProjectMemories(store.db, {
2266
+ canonicalIdentity: project.workspace_identity,
2267
+ initiatingDeviceId
2268
+ })
2269
+ };
2270
+ });
2271
+ }
2272
+ function resolveSharePerson(store, teammateName) {
2273
+ const matches = store.db.prepare(`SELECT actor_id, display_name, status
2274
+ FROM actors
2275
+ WHERE status IN ('active', 'pending') AND lower(trim(display_name)) = lower(?)
2276
+ ORDER BY actor_id`).all(teammateName);
2277
+ if (matches.length > 1) throw new Error("teammate_match_ambiguous");
2278
+ const match = matches[0];
2279
+ if (match) return match.status === "pending" ? {
2280
+ kind: "pending",
2281
+ personId: match.actor_id,
2282
+ displayName: match.display_name
2283
+ } : {
2284
+ kind: "existing",
2285
+ personId: match.actor_id,
2286
+ displayName: match.display_name
2287
+ };
2288
+ return {
2289
+ kind: "pending",
2290
+ displayName: teammateName
2291
+ };
2292
+ }
2293
+ function projectInvitePlan(store, body, createdAt, inviteExpiresAt) {
2294
+ const teammateName = normalizeTeammateName(String(body.teammate_name));
2295
+ const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
2296
+ store.adoptEnsuredDeviceIdentity(localDeviceId);
2297
+ const projects = resolveProjectInviteSelection(store, projectInviteStringList(body, "project_ids"), localDeviceId);
2298
+ const { groupId, config } = projectInviteStatus();
2299
+ return {
2300
+ plan: planShareOperation({
2301
+ inviterActorId: store.actorId,
2302
+ inviterDeviceIds: [localDeviceId],
2303
+ person: resolveSharePerson(store, teammateName),
2304
+ projects,
2305
+ coordinatorGroupId: groupId,
2306
+ createdAt,
2307
+ inviteExpiresAt
2308
+ }),
2309
+ config
2310
+ };
2311
+ }
2312
+ function projectInvitePreview(plan) {
2313
+ return {
2314
+ operation_id: plan.operationId,
2315
+ teammate: {
2316
+ display_name: plan.teammateName,
2317
+ match: plan.personKind,
2318
+ ...plan.personKind === "existing" ? { person_id: plan.personId } : {}
2319
+ },
2320
+ projects: plan.projects.map((project) => ({
2321
+ project_id: project.canonicalIdentity,
2322
+ display_name: project.displayName,
2323
+ existing_memory_count: project.existingMemoryCount
2324
+ })),
2325
+ existing_memory_count: plan.projects.reduce((total, project) => total + project.existingMemoryCount, 0),
2326
+ future_memories_shared: true,
2327
+ history_policy: plan.historyPolicy,
2328
+ reviewed_project_set_digest: plan.reviewedProjectSetDigest
2329
+ };
2330
+ }
2331
+ async function coordinatorProjectInvitePayload(operationId) {
2332
+ const { groupId, config } = projectInviteStatus();
2333
+ const [status, payload] = await requestJson("GET", `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/admin/project-invites/${encodeURIComponent(operationId)}?group_id=${encodeURIComponent(groupId)}`, {
2334
+ headers: { "X-Codemem-Coordinator-Admin": config.syncCoordinatorAdminSecret },
2335
+ timeoutS: 10
2336
+ }).catch((cause) => {
2337
+ const error = new Error("coordinator_unavailable", { cause });
2338
+ Object.assign(error, { status: 503 });
2339
+ throw error;
2340
+ });
2341
+ if (status < 200 || status >= 300) {
2342
+ const error = new Error(String(payload?.error ?? "operation_read_failed"));
2343
+ Object.assign(error, { status });
2344
+ throw error;
2345
+ }
2346
+ if (!payload || payload.operation_id !== operationId || payload.group_id !== groupId) {
2347
+ const error = /* @__PURE__ */ new Error("operation_scope_mismatch");
2348
+ Object.assign(error, { status: 409 });
2349
+ throw error;
2350
+ }
2351
+ return {
2352
+ groupId,
2353
+ config,
2354
+ payload
2355
+ };
2356
+ }
2357
+ async function reconcileProjectInviteAcceptance(store, operationId) {
2358
+ const owner = store.db.prepare("SELECT inviter_actor_id FROM share_operations WHERE operation_id = ?").pluck().get(operationId);
2359
+ if (!owner) throw new Error("operation_not_found");
2360
+ if (owner !== store.actorId) throw new Error("operation_scope_mismatch");
2361
+ ensureLocalActorRecord(store);
2362
+ const { groupId, payload } = await coordinatorProjectInvitePayload(operationId);
2363
+ if (payload.state === "waiting_for_acceptance") return {
2364
+ accepted: false,
2365
+ payload
2366
+ };
2367
+ if (payload.state !== "accepted") {
2368
+ const error = /* @__PURE__ */ new Error("operation_state_invalid");
2369
+ Object.assign(error, { status: 409 });
2370
+ throw error;
2371
+ }
2372
+ if ([
2373
+ "reviewed_project_set_digest",
2374
+ "recipient_actor_id",
2375
+ "recipient_display_name",
2376
+ "recipient_device_id",
2377
+ "recipient_device_display_name",
2378
+ "recipient_public_key",
2379
+ "recipient_fingerprint",
2380
+ "consumed_at",
2381
+ "trust_state"
2382
+ ].some((key) => typeof payload[key] !== "string" || !String(payload[key]).trim())) {
2383
+ const error = /* @__PURE__ */ new Error("operation_acceptance_invalid");
2384
+ Object.assign(error, { status: 409 });
2385
+ throw error;
2386
+ }
2387
+ const trustState = String(payload.trust_state);
2388
+ const bootstrapGrantId = typeof payload.bootstrap_grant_id === "string" && payload.bootstrap_grant_id.trim() ? payload.bootstrap_grant_id : null;
2389
+ if (trustState !== "pending_inviter_device" && trustState !== "bootstrap_grant_created" || trustState === "bootstrap_grant_created" !== Boolean(bootstrapGrantId)) {
2390
+ const error = /* @__PURE__ */ new Error("operation_trust_state_invalid");
2391
+ Object.assign(error, { status: 409 });
2392
+ throw error;
2393
+ }
2394
+ const projects = parseAcceptedProjectIntent(payload.projects);
2395
+ reconcileShareOperationAcceptance(store.db, {
2396
+ operationId,
2397
+ localInviterActorId: store.actorId,
2398
+ coordinatorGroupId: groupId,
2399
+ reviewedProjectSetDigest: String(payload.reviewed_project_set_digest),
2400
+ recipientActorId: String(payload.recipient_actor_id),
2401
+ recipientDisplayName: String(payload.recipient_display_name),
2402
+ recipientDeviceId: String(payload.recipient_device_id),
2403
+ recipientDeviceDisplayName: String(payload.recipient_device_display_name),
2404
+ recipientPublicKey: String(payload.recipient_public_key),
2405
+ recipientFingerprint: String(payload.recipient_fingerprint),
2406
+ consumedAt: String(payload.consumed_at),
2407
+ trustState,
2408
+ bootstrapGrantId,
2409
+ projects
2410
+ });
2411
+ return {
2412
+ accepted: true,
2413
+ payload
2414
+ };
2415
+ }
2416
+ async function shareOperationReadModels(store, operationId, includeInviteLinks = true) {
2417
+ const rows = store.db.prepare(`SELECT o.operation_id, o.state, o.person_id, o.teammate_name,
2418
+ o.recipient_actor_id, o.recipient_display_name, o.recipient_device_id,
2419
+ o.recipient_device_display_name, o.invite_expires_at, o.acceptance_consumed_at,
2420
+ o.created_at, o.updated_at, a.display_name AS actor_display_name,
2421
+ p.name AS peer_display_name, p.last_seen_at AS device_last_seen_at,
2422
+ p.last_sync_at AS device_last_reached_at
2423
+ FROM share_operations o
2424
+ LEFT JOIN actors a ON a.actor_id = o.person_id
2425
+ LEFT JOIN sync_peers p ON p.peer_device_id = o.recipient_device_id
2426
+ WHERE o.inviter_actor_id = ? AND (? IS NULL OR o.operation_id = ?)
2427
+ ORDER BY o.created_at DESC, o.operation_id`).all(store.actorId, operationId ?? null, operationId ?? null);
2428
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2429
+ return Promise.all(rows.map(async (row) => {
2430
+ const projects = store.db.prepare(`SELECT canonical_project_identity AS project_id, display_name, existing_memory_count
2431
+ FROM share_operation_projects WHERE operation_id = ? ORDER BY ordinal`).all(row.operation_id);
2432
+ const steps = store.db.prepare(`SELECT step_key, status, attempt_count, started_at, last_attempt_at,
2433
+ safe_error_code, updated_at FROM share_operation_steps WHERE operation_id = ?`).all(row.operation_id).map((step) => ({
2434
+ attemptCount: step.attempt_count,
2435
+ lastAttemptAt: step.last_attempt_at,
2436
+ safeErrorCode: step.safe_error_code,
2437
+ startedAt: step.started_at,
2438
+ status: step.status,
2439
+ stepKey: step.step_key,
2440
+ updatedAt: step.updated_at
2441
+ }));
2442
+ let inviteLink = null;
2443
+ let projectedState = row.state;
2444
+ if (includeInviteLinks && row.state === "waiting_for_acceptance") try {
2445
+ const remote = await coordinatorProjectInvitePayload(row.operation_id);
2446
+ if (remote.payload.state === "accepted") projectedState = "accepted";
2447
+ inviteLink = typeof remote.payload.invite_link === "string" && remote.payload.invite_link.trim() ? remote.payload.invite_link : null;
2448
+ } catch {}
2449
+ const personId = row.recipient_actor_id || row.person_id;
2450
+ const personName = row.recipient_display_name || row.actor_display_name || row.teammate_name;
2451
+ const deviceName = row.recipient_device_display_name || row.peer_display_name;
2452
+ const lifecycle = projectShareLifecycle({
2453
+ deviceLastSeenAt: row.device_last_seen_at,
2454
+ deviceLastReachedAt: row.device_last_reached_at,
2455
+ deviceName,
2456
+ inviteLink,
2457
+ now,
2458
+ personName,
2459
+ state: projectedState,
2460
+ steps
2461
+ });
2462
+ return {
2463
+ operation_id: row.operation_id,
2464
+ person: {
2465
+ actor_id: personId,
2466
+ display_name: personName
2467
+ },
2468
+ devices: row.recipient_device_id && deviceName ? [{
2469
+ device_id: row.recipient_device_id,
2470
+ display_name: deviceName,
2471
+ last_seen_at: row.device_last_seen_at
2472
+ }] : [],
2473
+ projects,
2474
+ project_count: projects.length,
2475
+ lifecycle: {
2476
+ state: lifecycle.lifecycle,
2477
+ label: lifecycle.label,
2478
+ explanation: lifecycle.explanation,
2479
+ primary_action: lifecycle.primaryAction?.kind === "copy_invite" ? {
2480
+ kind: "copy_invite",
2481
+ label: lifecycle.primaryAction.label,
2482
+ invite_link: lifecycle.primaryAction.inviteLink
2483
+ } : !includeInviteLinks && lifecycle.lifecycle === "waiting_for_acceptance" ? {
2484
+ kind: "copy_invite",
2485
+ label: "Copy invite"
2486
+ } : lifecycle.primaryAction
2487
+ },
2488
+ timestamps: {
2489
+ created_at: row.created_at,
2490
+ updated_at: row.updated_at,
2491
+ accepted_at: row.acceptance_consumed_at,
2492
+ invite_expires_at: row.invite_expires_at
2493
+ }
2494
+ };
2495
+ }));
2496
+ }
2497
+ async function executeProjectShareProvisioning(store, operationId) {
2498
+ const { groupId, config } = projectInviteStatus();
2499
+ const coordinatorId = config.syncCoordinatorUrl || null;
2500
+ if (!coordinatorId) throw new Error("coordinator_not_configured");
2501
+ const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
2502
+ return executeShareProvisioning(store.db, {
2503
+ operationId,
2504
+ initiatingDeviceId: localDeviceId
2505
+ }, {
2506
+ createOrGetBoundary: async (project, expectedGroupId) => {
2507
+ if (expectedGroupId !== groupId) throw new Error("operation_scope_mismatch");
2508
+ const readExisting = async () => (await coordinatorListScopesAction({
2509
+ groupId,
2510
+ includeInactive: true,
2511
+ remoteUrl: config.syncCoordinatorUrl || null,
2512
+ adminSecret: config.syncCoordinatorAdminSecret || null
2513
+ })).find((scope) => scope.scope_id === project.boundaryId) ?? null;
2514
+ const existing = await readExisting();
2515
+ if (existing) return existing;
2516
+ try {
2517
+ return await coordinatorCreateScopeAction({
2518
+ groupId,
2519
+ scopeId: project.boundaryId,
2520
+ label: project.displayName,
2521
+ kind: "managed_project",
2522
+ authorityType: "coordinator",
2523
+ coordinatorId,
2524
+ membershipEpoch: 1,
2525
+ status: "active",
2526
+ remoteUrl: config.syncCoordinatorUrl || null,
2527
+ adminSecret: config.syncCoordinatorAdminSecret || null
2528
+ });
2529
+ } catch (error) {
2530
+ const reread = await readExisting();
2531
+ if (reread) return reread;
2532
+ throw error;
2533
+ }
2534
+ },
2535
+ grantMembership: ({ effectId, groupId: expectedGroupId, scopeId, deviceId, role }) => {
2536
+ if (expectedGroupId !== groupId) throw new Error("operation_scope_mismatch");
2537
+ return coordinatorGrantScopeMembershipAction({
2538
+ effectId,
2539
+ groupId,
2540
+ scopeId,
2541
+ deviceId,
2542
+ role,
2543
+ membershipEpoch: 1,
2544
+ coordinatorId,
2545
+ remoteUrl: config.syncCoordinatorUrl || null,
2546
+ adminSecret: config.syncCoordinatorAdminSecret || null
2547
+ });
2548
+ },
2549
+ supportsReassignScope: (deviceId) => peerSupportsReassignScope(store, deviceId),
2550
+ refreshAuthorization: async (expectedGroupId) => {
2551
+ if (expectedGroupId !== groupId) throw new Error("operation_scope_mismatch");
2552
+ if ((await refreshConfiguredScopeMembershipCache(store.db, config, { keysDir: syncKeysDir() })).groups.find((item) => item.groupId === groupId)?.status !== "refreshed") throw new Error("authorization_refresh_failed");
2553
+ },
2554
+ runInitialSync: (recipientDeviceId) => runSyncPass(store.db, recipientDeviceId, {
2555
+ keysDir: syncKeysDir(),
2556
+ scanner: store.scanner,
2557
+ refreshAuthorization: true
2558
+ })
2559
+ });
2560
+ }
2561
+ async function advanceProjectShareOperation(store, operationId) {
2562
+ const operation = store.db.prepare("SELECT state, inviter_actor_id, recipient_device_id FROM share_operations WHERE operation_id = ?").get(operationId);
2563
+ if (!operation) throw new Error("operation_not_found");
2564
+ if (operation.inviter_actor_id !== store.actorId) throw new Error("operation_scope_mismatch");
2565
+ if (operation.state === "cancelled") return {
2566
+ advanced: false,
2567
+ state: "cancelled"
2568
+ };
2569
+ if (!operation.recipient_device_id) {
2570
+ if (!(await reconcileProjectInviteAcceptance(store, operationId)).accepted) return {
2571
+ advanced: false,
2572
+ state: "waiting_for_acceptance"
2573
+ };
2574
+ }
2575
+ if ((await executeProjectShareProvisioning(store, operationId)).superseded) return {
2576
+ advanced: false,
2577
+ state: "cancelled"
2578
+ };
2579
+ return {
2580
+ advanced: true,
2581
+ state: "active"
2582
+ };
2583
+ }
2584
+ var AUTOMATIC_SHARE_OPERATION_STATES = [
2585
+ "waiting_for_acceptance",
2586
+ "accepted",
2587
+ "provisioning",
2588
+ "initial_sync",
2589
+ "waiting_for_device"
2590
+ ];
2591
+ var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 30 * 1e3;
2592
+ var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 300 * 1e3;
2593
+ var TERMINAL_SHARE_MAINTENANCE_ERRORS = new Set([
2594
+ "coordinator_not_configured",
2595
+ "team_sharing_not_configured",
2596
+ "team_selection_ambiguous",
2597
+ "initiating_device_not_reviewed",
2598
+ "inviter_project_access_ambiguous",
2599
+ "managed_boundary_plan_missing",
2600
+ "operation_device_binding_missing",
2601
+ "operation_intent_invalid",
2602
+ "provisioning_membership_plan_invalid",
2603
+ "device_binding_conflict",
2604
+ "intent_conflict",
2605
+ "inviter_identity_conflict"
2606
+ ]);
2607
+ var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = new Set([
2608
+ "operation_scope_mismatch",
2609
+ "operation_state_invalid",
2610
+ "operation_acceptance_invalid",
2611
+ "operation_trust_state_invalid",
2612
+ "operation_intent_invalid",
2613
+ "operation_intent_mismatch",
2614
+ "recipient_fingerprint_mismatch",
2615
+ "recipient_device_identity_conflict",
2616
+ "recipient_actor_conflict",
2617
+ "pending_person_identity_conflict",
2618
+ "device_binding_conflict",
2619
+ "intent_conflict",
2620
+ "inviter_identity_conflict"
2621
+ ]);
2622
+ var TERMINAL_RECONCILIATION_ERRORS = new Set([
2623
+ "coordinator_not_configured",
2624
+ "team_sharing_not_configured",
2625
+ "team_selection_ambiguous",
2626
+ "operation_not_found",
2627
+ ...PROJECT_INVITE_OWNER_CONFLICT_ERRORS
2628
+ ]);
2629
+ function errorStatus(error) {
2630
+ const value = error && typeof error === "object" ? error.status : null;
2631
+ return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
2632
+ }
2633
+ function projectInviteOwnerErrorResponse(error, fallback) {
2634
+ const message = error instanceof Error ? error.message : "";
2635
+ const status = errorStatus(error);
2636
+ if (message === "operation_not_found" && (status == null || status === 404)) return {
2637
+ code: message,
2638
+ status: 404
2639
+ };
2640
+ if (PROJECT_INVITE_OWNER_CONFLICT_ERRORS.has(message) && (status == null || status === 409)) return {
2641
+ code: message,
2642
+ status: 409
2643
+ };
2644
+ if (status != null && status >= 500 && message !== "coordinator_unavailable") return {
2645
+ code: "coordinator_upstream_failed",
2646
+ status: 502
2647
+ };
2648
+ if (message === "coordinator_unavailable" || status === 408 || status === 429 || status === 503) return {
2649
+ code: "coordinator_unavailable",
2650
+ status: 503
2651
+ };
2652
+ return fallback;
2653
+ }
2654
+ function isRetryableReconciliationError(error) {
2655
+ const message = error instanceof Error ? error.message : String(error);
2656
+ if (TERMINAL_RECONCILIATION_ERRORS.has(message)) return false;
2657
+ const status = errorStatus(error);
2658
+ return status == null || status === 408 || status === 429 || status >= 500;
2659
+ }
2660
+ function safeReconciliationErrorCode(error) {
2661
+ const message = error instanceof Error ? error.message : String(error);
2662
+ return TERMINAL_RECONCILIATION_ERRORS.has(message) ? message : "operation_read_failed";
2663
+ }
2664
+ function recordInviteReconciliationFailure(store, operationId, error, now) {
2665
+ const safeErrorCode = safeReconciliationErrorCode(error);
2666
+ const retryable = isRetryableReconciliationError(error);
2667
+ store.db.prepare(`UPDATE share_operation_steps SET
2668
+ status = CASE WHEN ? = 1 THEN 'pending' ELSE 'failed' END,
2669
+ attempt_count = attempt_count + 1, last_attempt_at = ?, safe_error_code = ?, updated_at = ?
2670
+ WHERE operation_id = ? AND step_key = 'invite_consumption'`).run(retryable ? 1 : 0, now, safeErrorCode, now, operationId);
2671
+ const outcome = retryable ? "retry_scheduled" : "needs_attention";
2672
+ store.db.prepare("UPDATE share_operations SET state = ?, updated_at = ? WHERE operation_id = ?").run(outcome === "needs_attention" ? "needs_attention" : "waiting_for_acceptance", now, operationId);
2673
+ return outcome;
2674
+ }
2675
+ async function advancePendingProjectShares(store, options = {}) {
2676
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 3), 10));
2677
+ const placeholders = AUTOMATIC_SHARE_OPERATION_STATES.map(() => "?").join(", ");
2678
+ const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
2679
+ const waitingAcceptanceRetryBefore = (/* @__PURE__ */ new Date(maintenanceNow.getTime() - AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS)).toISOString();
2680
+ const waitingRetryBefore = (/* @__PURE__ */ new Date(maintenanceNow.getTime() - AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS)).toISOString();
2681
+ const rows = store.db.prepare(`SELECT operation.operation_id FROM share_operations AS operation
2682
+ WHERE operation.inviter_actor_id = ?
2683
+ AND operation.state IN (${placeholders})
2684
+ AND (operation.state <> 'waiting_for_acceptance' OR operation.updated_at <= ?)
2685
+ AND (
2686
+ operation.state <> 'waiting_for_device'
2687
+ OR operation.updated_at <= ?
2688
+ OR (
2689
+ NOT EXISTS (
2690
+ SELECT 1 FROM share_operation_steps AS step
2691
+ WHERE step.operation_id = operation.operation_id
2692
+ AND step.step_key = 'capability_preflight'
2693
+ AND step.status <> 'completed'
2694
+ AND (
2695
+ step.status = 'running'
2696
+ OR step.safe_error_code IN (
2697
+ 'waiting_for_device', 'device_offline', 'recipient_offline'
2698
+ )
2699
+ )
2700
+ )
2701
+ AND EXISTS (
2702
+ SELECT 1 FROM sync_attempts AS attempt
2703
+ WHERE attempt.id = (
2704
+ SELECT latest.id FROM sync_attempts AS latest
2705
+ WHERE latest.peer_device_id = operation.recipient_device_id
2706
+ ORDER BY latest.started_at DESC, latest.id DESC
2707
+ LIMIT 1
2708
+ )
2709
+ AND attempt.ok = 1
2710
+ AND julianday(COALESCE(attempt.finished_at, attempt.started_at)) >
2711
+ julianday(operation.updated_at)
2712
+ )
2713
+ )
2714
+ )
2715
+ ORDER BY CASE WHEN operation.state IN ('accepted', 'provisioning', 'initial_sync') THEN 0 ELSE 1 END,
2716
+ CASE WHEN operation.state IN ('waiting_for_acceptance', 'waiting_for_device')
2717
+ THEN operation.updated_at ELSE operation.created_at END ASC,
2718
+ operation.created_at ASC, operation.operation_id ASC
2719
+ LIMIT ?`).all(store.actorId, ...AUTOMATIC_SHARE_OPERATION_STATES, waitingAcceptanceRetryBefore, waitingRetryBefore, limit);
2720
+ const advanceOperation = options.advanceOperation ?? advanceProjectShareOperation;
2721
+ const result = {
2722
+ processed: 0,
2723
+ advanced: 0,
2724
+ waiting: 0,
2725
+ attention: 0,
2726
+ failed: 0,
2727
+ items: []
2728
+ };
2729
+ for (const row of rows) {
2730
+ result.processed += 1;
2731
+ try {
2732
+ const advanced = await advanceOperation(store, row.operation_id);
2733
+ if (!advanced.advanced) {
2734
+ if (advanced.state === "cancelled") {
2735
+ result.items.push({
2736
+ operationId: row.operation_id,
2737
+ outcome: "superseded"
2738
+ });
2739
+ continue;
2740
+ }
2741
+ store.db.prepare("UPDATE share_operations SET updated_at = ? WHERE operation_id = ?").run(maintenanceNow.toISOString(), row.operation_id);
2742
+ result.waiting += 1;
2743
+ result.items.push({
2744
+ operationId: row.operation_id,
2745
+ outcome: "waiting_for_acceptance"
2746
+ });
2747
+ continue;
2748
+ }
2749
+ result.advanced += 1;
2750
+ result.items.push({
2751
+ operationId: row.operation_id,
2752
+ outcome: "advanced"
2753
+ });
2754
+ } catch (error) {
2755
+ const message = error instanceof Error ? error.message : String(error);
2756
+ const state = store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(row.operation_id);
2757
+ if (state === "cancelled") {
2758
+ result.items.push({
2759
+ operationId: row.operation_id,
2760
+ outcome: "superseded"
2761
+ });
2762
+ continue;
2763
+ }
2764
+ if (message === "waiting_for_device" || state === "waiting_for_device") {
2765
+ result.waiting += 1;
2766
+ result.items.push({
2767
+ operationId: row.operation_id,
2768
+ outcome: "waiting_for_device"
2769
+ });
2770
+ continue;
2771
+ }
2772
+ if (state === "needs_attention") {
2773
+ result.attention += 1;
2774
+ result.items.push({
2775
+ operationId: row.operation_id,
2776
+ outcome: "needs_attention",
2777
+ error: message
2778
+ });
2779
+ continue;
2780
+ }
2781
+ if (state === "waiting_for_acceptance") {
2782
+ const outcome = recordInviteReconciliationFailure(store, row.operation_id, error, maintenanceNow.toISOString());
2783
+ if (outcome === "needs_attention") result.attention += 1;
2784
+ else result.waiting += 1;
2785
+ result.items.push({
2786
+ operationId: row.operation_id,
2787
+ outcome,
2788
+ error: message
2789
+ });
2790
+ continue;
2791
+ }
2792
+ if (TERMINAL_SHARE_MAINTENANCE_ERRORS.has(message)) {
2793
+ store.db.prepare("UPDATE share_operations SET state = 'needs_attention', updated_at = ? WHERE operation_id = ?").run(maintenanceNow.toISOString(), row.operation_id);
2794
+ result.attention += 1;
2795
+ result.items.push({
2796
+ operationId: row.operation_id,
2797
+ outcome: "needs_attention",
2798
+ error: message
2799
+ });
2800
+ continue;
2801
+ }
2802
+ result.failed += 1;
2803
+ result.items.push({
2804
+ operationId: row.operation_id,
2805
+ outcome: "failed",
2806
+ error: message
2807
+ });
2808
+ }
2809
+ }
2810
+ return result;
2811
+ }
2812
+ async function reconcileConfiguredCoordinatorEnrollment(store, options = {}) {
2813
+ const config = options.config ?? readCoordinatorSyncConfig();
2814
+ const remoteUrl = config.syncCoordinatorUrl.trim();
2815
+ const adminSecret = config.syncCoordinatorAdminSecret.trim();
2816
+ if (!remoteUrl || !adminSecret || config.syncCoordinatorGroups.length === 0) return {
2817
+ skipped: true,
2818
+ groupsProcessed: 0,
2819
+ failedGroups: 0,
2820
+ devicesAdded: 0,
2821
+ membershipsAdded: 0,
2822
+ identitiesAdded: 0,
2823
+ unchanged: 0,
2824
+ issues: 0
2825
+ };
2826
+ const listDevices = options.listDevices ?? coordinatorListDevicesAction;
2827
+ const listConsumedTeamInvites = options.listConsumedTeamInvites ?? coordinatorListConsumedTeamInvitesAction;
2828
+ const reconcileSnapshot = options.reconcileSnapshot ?? ((input) => reconcileCoordinatorEnrollmentSnapshot(store.db, input));
2829
+ const total = {
2830
+ skipped: false,
2831
+ groupsProcessed: 0,
2832
+ failedGroups: 0,
2833
+ devicesAdded: 0,
2834
+ membershipsAdded: 0,
2835
+ identitiesAdded: 0,
2836
+ unchanged: 0,
2837
+ issues: 0
2838
+ };
2839
+ for (const groupId of config.syncCoordinatorGroups) try {
2840
+ const [enrollments, consumedTeamInvites] = await Promise.all([listDevices({
2841
+ groupId,
2842
+ remoteUrl,
2843
+ adminSecret
2844
+ }), listConsumedTeamInvites({
2845
+ groupId,
2846
+ remoteUrl,
2847
+ adminSecret
2848
+ })]);
2849
+ const result = reconcileSnapshot({
2850
+ coordinatorId: buildBaseUrl(remoteUrl),
2851
+ groupId,
2852
+ enrollments,
2853
+ consumedTeamInvites,
2854
+ localDeviceId: store.deviceId
2855
+ });
2856
+ total.groupsProcessed += 1;
2857
+ total.devicesAdded += result.devicesAdded;
2858
+ total.membershipsAdded += result.membershipsAdded;
2859
+ total.identitiesAdded += result.identitiesAdded;
2860
+ total.unchanged += result.unchanged;
2861
+ total.issues += result.issues.length;
2862
+ } catch {
2863
+ total.failedGroups += 1;
2864
+ }
2865
+ return total;
2866
+ }
2867
+ var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
2868
+ var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
2869
+ var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
2870
+ var RECIPIENT_POLICY_WAITING_ERRORS = new Set([
2871
+ "recipient_policy_capability_undetermined",
2872
+ "recipient_policy_parity_incomplete",
2873
+ "recipient_policy_snapshot_not_fresh"
2874
+ ]);
2875
+ function recipientPolicyCoordinatorBoundary(store, scopeId) {
2876
+ const row = store.db.prepare(`SELECT coordinator_id, group_id, membership_epoch FROM replication_scopes
2877
+ WHERE scope_id = ? AND kind = 'managed_project' AND authority_type = 'coordinator'
2878
+ AND status = 'active'`).get(scopeId);
2879
+ const coordinatorId = String(row?.coordinator_id ?? "").trim();
2880
+ const groupId = String(row?.group_id ?? "").trim();
2881
+ const membershipEpoch = Number(row?.membership_epoch);
2882
+ if (!coordinatorId || !groupId || !Number.isSafeInteger(membershipEpoch) || membershipEpoch < 0) throw new Error("recipient_policy_active_managed_scope_required");
2883
+ return {
2884
+ coordinatorId,
2885
+ groupId,
2886
+ membershipEpoch
2887
+ };
2888
+ }
2889
+ function recipientPolicySnapshotFingerprint(scopeId, scopeMembershipEpoch, memberships) {
2890
+ const canonical = memberships.toSorted((left, right) => left.deviceId.localeCompare(right.deviceId) || left.status.localeCompare(right.status) || left.membershipEpoch - right.membershipEpoch).map((membership) => `${membership.deviceId}\u0000${membership.status}\u0000${membership.membershipEpoch}`).join("");
2891
+ return `recipient-policy-coordinator-snapshot-v2:${createHash("sha256").update(`${scopeId}\u0000${scopeMembershipEpoch}\u0000${canonical}`).digest("hex")}`;
2892
+ }
2893
+ function recipientPolicyPresenceCapabilityExpiresAt(enrollment, observedAt) {
2894
+ const expiresAt = enrollment.presence_expires_at;
2895
+ const capabilities = enrollment.presence_capabilities;
2896
+ if (typeof expiresAt !== "string" || !capabilities) return null;
2897
+ const expiresAtMs = Date.parse(expiresAt);
2898
+ const observedAtMs = Date.parse(observedAt);
2899
+ if (!Number.isFinite(expiresAtMs) || !Number.isFinite(observedAtMs) || new Date(expiresAtMs).toISOString() !== expiresAt || expiresAtMs <= observedAtMs || capabilities.sync_capability !== "scoped" || !Array.isArray(capabilities.sync_features) || !capabilities.sync_features.every((feature) => typeof feature === "string") || !capabilities.sync_features.includes("reassign_scope")) return null;
2900
+ return expiresAtMs;
2901
+ }
2902
+ function recipientPolicyEnrollmentValueIsValid(value) {
2903
+ return typeof value === "string" && value.length > 0 && value === value.trim();
2904
+ }
2905
+ function createRecipientPolicyReconcilerEffects(store, options = {}) {
2906
+ const config = options.config ?? readCoordinatorSyncConfig();
2907
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
2908
+ const listDevices = options.listDevices ?? coordinatorListDevicesAction;
2909
+ const listReviewedRecipientInviteEvidence = options.listReviewedRecipientInviteEvidence ?? coordinatorListReviewedRecipientInviteEvidenceAction;
2910
+ const reviewedRecipientInviteEvidence = /* @__PURE__ */ new Map();
2911
+ const loadReviewedRecipientInviteEvidence = (groupId) => {
2912
+ let evidence = reviewedRecipientInviteEvidence.get(groupId);
2913
+ if (!evidence) {
2914
+ evidence = listReviewedRecipientInviteEvidence({
2915
+ groupId,
2916
+ remoteUrl: config.syncCoordinatorUrl || null,
2917
+ adminSecret: config.syncCoordinatorAdminSecret || null
2918
+ });
2919
+ reviewedRecipientInviteEvidence.set(groupId, evidence);
2920
+ }
2921
+ return evidence;
2922
+ };
2923
+ const boundaryEnrollments = /* @__PURE__ */ new Map();
2924
+ const target = (scopeId) => recipientPolicyCoordinatorBoundary(store, scopeId);
2925
+ const coordinatorOptions = (scopeId) => {
2926
+ const boundary = target(scopeId);
2927
+ const remoteUrl = config.syncCoordinatorUrl?.trim();
2928
+ const adminSecret = config.syncCoordinatorAdminSecret?.trim();
2929
+ if (!remoteUrl || !adminSecret) throw new Error("recipient_policy_effect_failed");
2930
+ if (!config.syncCoordinatorGroups.includes(boundary.groupId)) throw new Error("recipient_policy_effect_failed");
2931
+ try {
2932
+ if (buildBaseUrl(boundary.coordinatorId) !== buildBaseUrl(remoteUrl)) throw new Error("recipient_policy_effect_failed");
2933
+ } catch {
2934
+ throw new Error("recipient_policy_effect_failed");
2935
+ }
2936
+ return {
2937
+ ...boundary,
2938
+ remoteUrl,
2939
+ adminSecret
2940
+ };
2941
+ };
2942
+ return {
2943
+ now,
2944
+ snapshot: async ({ scopeId }) => {
2945
+ const targetOptions = coordinatorOptions(scopeId);
2946
+ const snapshotMemberships = (await coordinatorListScopeMembershipsAction({
2947
+ groupId: targetOptions.groupId,
2948
+ scopeId,
2949
+ includeRevoked: true,
2950
+ remoteUrl: targetOptions.remoteUrl,
2951
+ adminSecret: targetOptions.adminSecret
2952
+ }).catch(() => {
2953
+ throw new Error("recipient_policy_snapshot_not_fresh");
2954
+ })).map((membership) => {
2955
+ if (typeof membership.device_id !== "string" || membership.status !== "active" && membership.status !== "revoked") throw new Error("recipient_policy_snapshot_invalid");
2956
+ const status = membership.status;
2957
+ return {
2958
+ deviceId: membership.device_id,
2959
+ status,
2960
+ membershipEpoch: membership.membership_epoch
2961
+ };
2962
+ });
2963
+ return {
2964
+ authoritative: true,
2965
+ scopeId,
2966
+ scopeMembershipEpoch: targetOptions.membershipEpoch,
2967
+ fingerprint: recipientPolicySnapshotFingerprint(scopeId, targetOptions.membershipEpoch, snapshotMemberships),
2968
+ observedAt: now(),
2969
+ memberships: snapshotMemberships
2970
+ };
2971
+ },
2972
+ listBoundaryEnrollments: async ({ scopeId }) => {
2973
+ const targetOptions = coordinatorOptions(scopeId);
2974
+ const observedAt = now();
2975
+ const enrollments = await listDevices({
2976
+ groupId: targetOptions.groupId,
2977
+ includeDisabled: true,
2978
+ remoteUrl: targetOptions.remoteUrl,
2979
+ adminSecret: targetOptions.adminSecret
2980
+ }).catch(() => {
2981
+ throw new Error("recipient_policy_snapshot_not_fresh");
2982
+ });
2983
+ const presenceCapabilityExpiries = new Map(enrollments.map((enrollment) => [enrollment.device_id, recipientPolicyPresenceCapabilityExpiresAt(enrollment, observedAt)]));
2984
+ const mapped = enrollments.map((enrollment) => {
2985
+ if (enrollment.group_id !== targetOptions.groupId || !recipientPolicyEnrollmentValueIsValid(enrollment.device_id) || !recipientPolicyEnrollmentValueIsValid(enrollment.public_key) || !recipientPolicyEnrollmentValueIsValid(enrollment.fingerprint) || fingerprintPublicKey(enrollment.public_key) !== enrollment.fingerprint || enrollment.enabled !== 0 && enrollment.enabled !== 1 || enrollment.identity_id !== null && !recipientPolicyEnrollmentValueIsValid(enrollment.identity_id)) throw new Error("recipient_policy_snapshot_invalid");
2986
+ return {
2987
+ deviceId: enrollment.device_id,
2988
+ identityId: enrollment.identity_id,
2989
+ publicKey: enrollment.public_key,
2990
+ fingerprint: enrollment.fingerprint,
2991
+ enabled: enrollment.enabled === 1
2992
+ };
2993
+ });
2994
+ boundaryEnrollments.set(scopeId, new Map(mapped.flatMap((enrollment) => enrollment.enabled && enrollment.identityId !== null ? [[enrollment.deviceId, {
2995
+ identityId: enrollment.identityId,
2996
+ publicKey: enrollment.publicKey,
2997
+ fingerprint: enrollment.fingerprint,
2998
+ presenceCapabilityExpiresAtMs: presenceCapabilityExpiries.get(enrollment.deviceId) ?? null
2999
+ }]] : [])));
3000
+ return mapped;
3001
+ },
3002
+ probeCapability: async ({ deviceId, scopeId }) => {
3003
+ if (deviceId === store.deviceId) return "supported";
3004
+ const targetOptions = coordinatorOptions(scopeId);
3005
+ const enrollment = boundaryEnrollments.get(scopeId)?.get(deviceId);
3006
+ if (!enrollment) return "undetermined";
3007
+ const peer = store.db.prepare("SELECT pinned_fingerprint, public_key FROM sync_peers WHERE peer_device_id = ?").get(deviceId);
3008
+ if (peer) {
3009
+ const peerPublicKey = String(peer.public_key ?? "").trim();
3010
+ const peerFingerprint = String(peer.pinned_fingerprint ?? "").trim() || (peerPublicKey ? fingerprintPublicKey(peerPublicKey) : "");
3011
+ if ((peerFingerprint || peerPublicKey) && (peerFingerprint !== enrollment.fingerprint || peerPublicKey && peerPublicKey !== enrollment.publicKey)) return "undetermined";
3012
+ }
3013
+ const observed = await peerSupportsSyncRequirements(store, deviceId, {
3014
+ scoped: true,
3015
+ reassignScope: true
3016
+ });
3017
+ if (observed !== "undetermined") return observed;
3018
+ const observedAtMs = Date.parse(now());
3019
+ if (enrollment.presenceCapabilityExpiresAtMs == null || !Number.isFinite(observedAtMs) || enrollment.presenceCapabilityExpiresAtMs <= observedAtMs) return "undetermined";
3020
+ return (await loadReviewedRecipientInviteEvidence(targetOptions.groupId).catch(() => [])).some((invite) => invite.group_id === targetOptions.groupId && invite.bound_device_id === deviceId && invite.recipient_actor_id === enrollment.identityId && invite.bound_public_key === enrollment.publicKey && invite.bound_fingerprint === enrollment.fingerprint) ? "supported" : "undetermined";
3021
+ },
3022
+ revoke: async (input) => {
3023
+ const targetOptions = coordinatorOptions(input.scopeId);
3024
+ await coordinatorRevokeScopeMembershipAction({
3025
+ effectId: input.effectId,
3026
+ groupId: targetOptions.groupId,
3027
+ scopeId: input.scopeId,
3028
+ deviceId: input.deviceId,
3029
+ remoteUrl: targetOptions.remoteUrl,
3030
+ adminSecret: targetOptions.adminSecret
3031
+ });
3032
+ return {
3033
+ effectId: input.effectId,
3034
+ scopeId: input.scopeId,
3035
+ deviceId: input.deviceId,
3036
+ status: "revoked"
3037
+ };
3038
+ },
3039
+ grant: async (input) => {
3040
+ const targetOptions = coordinatorOptions(input.scopeId);
3041
+ const membership = await coordinatorGrantScopeMembershipAction({
3042
+ effectId: input.effectId,
3043
+ groupId: targetOptions.groupId,
3044
+ scopeId: input.scopeId,
3045
+ deviceId: input.deviceId,
3046
+ role: input.role,
3047
+ coordinatorId: targetOptions.coordinatorId,
3048
+ remoteUrl: targetOptions.remoteUrl,
3049
+ adminSecret: targetOptions.adminSecret
3050
+ });
3051
+ return {
3052
+ effectId: input.effectId,
3053
+ scopeId: membership.scope_id,
3054
+ deviceId: membership.device_id,
3055
+ status: membership.status === "active" ? "active" : "revoked"
3056
+ };
3057
+ },
3058
+ refresh: async ({ scopeId }) => {
3059
+ const boundary = target(scopeId);
3060
+ if ((await refreshConfiguredScopeMembershipCache(store.db, config, { keysDir: syncKeysDir() })).groups.find((item) => item.groupId === boundary.groupId)?.status !== "refreshed") throw new Error("recipient_policy_effect_failed");
3061
+ await refreshAuthorizedCoordinatorPeerTrust(store, config);
3062
+ }
3063
+ };
3064
+ }
3065
+ async function reconcileRecipientPolicyProjects(store, options = {}) {
3066
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT), RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT));
3067
+ const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
3068
+ const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS));
3069
+ const retryBefore = new Date(maintenanceNow.getTime() - backoffMs).toISOString();
3070
+ const rows = store.db.prepare(`WITH projects AS (
3071
+ SELECT DISTINCT canonical_project_identity FROM project_recipients
3072
+ UNION
3073
+ SELECT canonical_project_identity FROM recipient_policy_authority_states
3074
+ )
3075
+ SELECT projects.canonical_project_identity
3076
+ FROM projects
3077
+ LEFT JOIN recipient_policy_authority_states authority
3078
+ ON authority.canonical_project_identity = projects.canonical_project_identity
3079
+ WHERE authority.safe_error_code IS NULL OR authority.last_attempt_at IS NULL
3080
+ OR authority.last_attempt_at <= ?
3081
+ ORDER BY CASE WHEN authority.last_attempt_at IS NULL THEN 0 ELSE 1 END,
3082
+ authority.last_attempt_at, projects.canonical_project_identity
3083
+ LIMIT ?`).all(retryBefore, limit);
3084
+ const effects = options.effects ?? createRecipientPolicyReconcilerEffects(store);
3085
+ const reconcileProject = options.reconcileProject ?? reconcileRecipientPolicyProject;
3086
+ const result = {
3087
+ processed: 0,
3088
+ active: 0,
3089
+ waiting: 0,
3090
+ attention: 0,
3091
+ failed: 0,
3092
+ items: []
3093
+ };
3094
+ for (const row of rows) {
3095
+ result.processed += 1;
3096
+ try {
3097
+ const outcome = await reconcileProject(store.db, {
3098
+ canonicalProjectIdentity: row.canonical_project_identity,
3099
+ leaseOwner: options.leaseOwner ?? `recipient-policy-maintenance:${store.deviceId || process.pid}`
3100
+ }, effects);
3101
+ if (outcome.status === "active") result.active += 1;
3102
+ else if (outcome.status === "needs_attention") result.attention += 1;
3103
+ else result.waiting += 1;
3104
+ result.items.push({
3105
+ canonicalProjectIdentity: row.canonical_project_identity,
3106
+ status: outcome.status,
3107
+ safeErrorCode: outcome.safeErrorCode
3108
+ });
3109
+ } catch {
3110
+ result.failed += 1;
3111
+ result.items.push({
3112
+ canonicalProjectIdentity: row.canonical_project_identity,
3113
+ status: "failed",
3114
+ safeErrorCode: "recipient_policy_reconciliation_failed"
3115
+ });
3116
+ }
3117
+ }
3118
+ return result;
3119
+ }
3120
+ function recipientPolicyReadState(authority) {
3121
+ if (!authority) return "pending";
3122
+ if (authority.safeErrorCode && RECIPIENT_POLICY_WAITING_ERRORS.has(authority.safeErrorCode)) return "waiting";
3123
+ if (authority.safeErrorCode || authority.authorityState === "rolled_back") return "needs_attention";
3124
+ if (authority.authorityState === "active") return "active";
3125
+ if (authority.authorityState === "eligible") return "verifying";
3126
+ return "pending";
3127
+ }
3128
+ function recipientPolicyReadCopy(state) {
3129
+ if (state === "active") return {
3130
+ label: "Recipient policy active",
3131
+ explanation: "Recipient policy now controls future access for this Project."
3132
+ };
3133
+ if (state === "verifying") return {
3134
+ label: "Verifying recipient policy",
3135
+ explanation: "Current access matches recipient policy and needs one more stable check."
3136
+ };
3137
+ if (state === "waiting") return {
3138
+ label: "Waiting to reconcile",
3139
+ explanation: "Waiting for devices or a fresh coordinator snapshot. No partial grant is applied."
3140
+ };
3141
+ if (state === "needs_attention") return {
3142
+ label: "Reconciliation needs attention",
3143
+ explanation: "Legacy scope enforcement remains in control until this Project is safe to retry."
3144
+ };
3145
+ return {
3146
+ label: "Recipient policy pending",
3147
+ explanation: "Legacy scope enforcement remains in control while reconciliation is pending."
3148
+ };
3149
+ }
3150
+ function listRecipientPolicyReconciliationStatus(store) {
3151
+ return {
3152
+ version: 1,
3153
+ items: store.db.prepare(`SELECT DISTINCT canonical_project_identity FROM project_recipients WHERE status = 'active'
3154
+ UNION SELECT canonical_project_identity FROM recipient_policy_authority_states
3155
+ ORDER BY canonical_project_identity`).all().map((row) => row.canonical_project_identity).map((canonicalProjectIdentity) => {
3156
+ const state = recipientPolicyReadState(getRecipientPolicyAuthorityState(store.db, canonicalProjectIdentity));
3157
+ return {
3158
+ canonicalProjectIdentity,
3159
+ state,
3160
+ ...recipientPolicyReadCopy(state),
3161
+ deliveredCopiesMayRemain: true,
3162
+ revocationWarning: SCOPE_MEMBERSHIP_REVOCATION_LIMITATION
3163
+ };
3164
+ })
3165
+ };
3166
+ }
1979
3167
  function sortActiveMaintenanceJobs(jobs) {
1980
3168
  return [...jobs].sort((a, b) => {
1981
3169
  const aTime = a.started_at ?? a.updated_at;
@@ -2017,6 +3205,11 @@ function redactSemanticIndexDiagnostics(diagnostics, showDiagnostics) {
2017
3205
  function syncKeysDir() {
2018
3206
  return process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
2019
3207
  }
3208
+ /** Strip control/format characters and cap length so attacker-controlled
3209
+ * headers cannot forge log lines when included in diagnostics. */
3210
+ function loggableHeaderValue(value) {
3211
+ return (value ?? "").replace(/[\p{Cc}\p{Cf}]/gu, "").slice(0, 64).trim() || "unknown";
3212
+ }
2020
3213
  function intEnvOr(name, fallback) {
2021
3214
  const value = Number.parseInt(process.env[name] ?? "", 10);
2022
3215
  return Number.isFinite(value) ? value : fallback;
@@ -2169,7 +3362,7 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2169
3362
  deviceId
2170
3363
  };
2171
3364
  const config = readCoordinatorSyncConfig();
2172
- if (!config.syncCoordinatorUrl || !config.syncCoordinatorAdminSecret) return {
3365
+ if (!config.syncCoordinatorUrl) return {
2173
3366
  ok: false,
2174
3367
  reason: "bootstrap_grant_coordinator_not_configured",
2175
3368
  deviceId
@@ -2177,11 +3370,31 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2177
3370
  const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
2178
3371
  let verification;
2179
3372
  try {
2180
- const [status, payload] = await requestJson("GET", `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/admin/bootstrap-grants/${encodeURIComponent(grantId)}`, {
2181
- headers: { "X-Codemem-Coordinator-Admin": config.syncCoordinatorAdminSecret },
2182
- timeoutS: Math.max(1, config.syncCoordinatorTimeoutS)
2183
- });
2184
- if (status !== 200 || !payload) return {
3373
+ let payload = null;
3374
+ if (config.syncCoordinatorAdminSecret) {
3375
+ const [status, adminPayload] = await requestJson("GET", `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/admin/bootstrap-grants/${encodeURIComponent(grantId)}`, {
3376
+ headers: { "X-Codemem-Coordinator-Admin": config.syncCoordinatorAdminSecret },
3377
+ timeoutS: Math.max(1, config.syncCoordinatorTimeoutS)
3378
+ });
3379
+ if (status === 200) payload = adminPayload;
3380
+ } else for (const groupId of config.syncCoordinatorGroups) {
3381
+ const lookupUrl = `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/bootstrap-grants/${encodeURIComponent(grantId)}?group_id=${encodeURIComponent(groupId)}`;
3382
+ const [status, signedPayload] = await requestJson("GET", lookupUrl, {
3383
+ headers: buildAuthHeaders({
3384
+ deviceId: localDeviceId,
3385
+ method: "GET",
3386
+ url: lookupUrl,
3387
+ bodyBytes: Buffer.alloc(0),
3388
+ keysDir: syncKeysDir()
3389
+ }),
3390
+ timeoutS: Math.max(1, config.syncCoordinatorTimeoutS)
3391
+ });
3392
+ if (status === 200) {
3393
+ payload = signedPayload;
3394
+ break;
3395
+ }
3396
+ }
3397
+ if (!payload) return {
2185
3398
  ok: false,
2186
3399
  reason: "bootstrap_grant_lookup_failed",
2187
3400
  deviceId
@@ -2201,6 +3414,11 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2201
3414
  reason: "bootstrap_grant_invalid_payload",
2202
3415
  deviceId
2203
3416
  };
3417
+ if (config.syncCoordinatorGroups.length > 0 && !config.syncCoordinatorGroups.includes(String(grant.group_id))) return {
3418
+ ok: false,
3419
+ reason: "bootstrap_grant_group_mismatch",
3420
+ deviceId
3421
+ };
2204
3422
  if (grant.revoked_at) return {
2205
3423
  ok: false,
2206
3424
  reason: "bootstrap_grant_revoked",
@@ -2221,6 +3439,13 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2221
3439
  reason: "bootstrap_grant_worker_disabled",
2222
3440
  deviceId
2223
3441
  };
3442
+ const workerPublicKey = String(workerEnrollment.public_key ?? "").trim();
3443
+ const workerFingerprint = String(workerEnrollment.fingerprint ?? "").trim();
3444
+ if (!workerPublicKey || fingerprintPublicKey(workerPublicKey) !== workerFingerprint) return {
3445
+ ok: false,
3446
+ reason: "bootstrap_grant_worker_identity_invalid",
3447
+ deviceId
3448
+ };
2224
3449
  if (grant.worker_device_id !== deviceId) return {
2225
3450
  ok: false,
2226
3451
  reason: "bootstrap_grant_worker_mismatch",
@@ -2245,7 +3470,7 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2245
3470
  timestamp,
2246
3471
  nonce,
2247
3472
  signature,
2248
- publicKey: String(workerEnrollment.public_key),
3473
+ publicKey: workerPublicKey,
2249
3474
  deviceId
2250
3475
  });
2251
3476
  } catch {
@@ -2260,8 +3485,19 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2260
3485
  reason: "bootstrap_grant_invalid_signature",
2261
3486
  deviceId
2262
3487
  };
3488
+ if (!isPeerTrustBindingCompatible(store.db, deviceId, workerPublicKey, workerFingerprint)) return {
3489
+ ok: false,
3490
+ reason: "bootstrap_grant_peer_trust_conflict",
3491
+ deviceId
3492
+ };
2263
3493
  const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
2264
3494
  if (nonceResult) return nonceResult;
3495
+ updatePeerAddresses(store.db, deviceId, [], {
3496
+ name: String(workerEnrollment.display_name ?? "").trim() || void 0,
3497
+ pinnedFingerprint: workerFingerprint,
3498
+ publicKey: workerPublicKey,
3499
+ replaceTrust: true
3500
+ });
2265
3501
  return {
2266
3502
  ok: true,
2267
3503
  reason: "ok",
@@ -2348,8 +3584,12 @@ function ensureLocalActorRecord(store) {
2348
3584
  is_local: 0,
2349
3585
  updated_at: now
2350
3586
  }).where(and(eq(schema.actors.is_local, 1), ne(schema.actors.actor_id, store.actorId))).run();
2351
- if (d.select({ actor_id: schema.actors.actor_id }).from(schema.actors).where(eq(schema.actors.actor_id, store.actorId)).get()) {
2352
- d.update(schema.actors).set({
3587
+ const existing = d.select({
3588
+ actor_id: schema.actors.actor_id,
3589
+ is_local: schema.actors.is_local
3590
+ }).from(schema.actors).where(eq(schema.actors.actor_id, store.actorId)).get();
3591
+ if (existing) {
3592
+ if (existing.is_local !== 1) d.update(schema.actors).set({
2353
3593
  is_local: 1,
2354
3594
  updated_at: now
2355
3595
  }).where(eq(schema.actors.actor_id, store.actorId)).run();
@@ -2365,6 +3605,16 @@ function ensureLocalActorRecord(store) {
2365
3605
  updated_at: now
2366
3606
  }).run();
2367
3607
  }
3608
+ function ensureStableStoreIdentity(store) {
3609
+ const deviceId = ensureStableStoreDeviceIdentity(store);
3610
+ ensureLocalActorRecord(store);
3611
+ return deviceId;
3612
+ }
3613
+ function ensureStableStoreDeviceIdentity(store) {
3614
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3615
+ store.adoptEnsuredDeviceIdentity(deviceId);
3616
+ return deviceId;
3617
+ }
2368
3618
  function findPeerDeviceIdForAddress(store, address) {
2369
3619
  const rows = store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all();
2370
3620
  for (const row of rows) if (safeJsonList(row.addresses_json).includes(address)) return String(row.peer_device_id ?? "").trim() || null;
@@ -2615,7 +3865,8 @@ async function acceptDiscoveredPeer(store, input) {
2615
3865
  function filterOpsForPeer(store, peerDeviceId, localDeviceId, ops, options = {}) {
2616
3866
  const [allowed, , skipped] = filterReplicationOpsForSyncWithStatus(store.db, ops, peerDeviceId, {
2617
3867
  localDeviceId,
2618
- applyScopeFilter: options.applyScopeFilter
3868
+ applyScopeFilter: options.applyScopeFilter,
3869
+ supportsReassignScope: options.supportsReassignScope
2619
3870
  });
2620
3871
  return {
2621
3872
  allowed,
@@ -3132,6 +4383,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
3132
4383
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
3133
4384
  let preauthChecked = false;
3134
4385
  let bootstrapAttempted = false;
4386
+ let deviceAuthReason = null;
3135
4387
  if (!auth.ok) {
3136
4388
  if ((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()) {
3137
4389
  preauthChecked = true;
@@ -3142,19 +4394,21 @@ function syncProtocolRoutes(getStore, opts = {}) {
3142
4394
  const unauthLimited = rateLimitedResponse(c, c.req.path, false);
3143
4395
  if (unauthLimited) return unauthLimited;
3144
4396
  }
4397
+ deviceAuthReason = auth.reason;
3145
4398
  auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
3146
4399
  bootstrapAttempted = true;
3147
4400
  }
3148
4401
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
3149
4402
  if (!auth.ok) {
3150
4403
  const grantPresent = Boolean((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim());
3151
- if (bootstrapAttempted && grantPresent) console.warn(`[sync] bootstrap grant auth failed: reason=${auth.reason} grant_present=${grantPresent} path=${c.req.path}`);
3152
- const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
4404
+ if (bootstrapAttempted && (grantPresent || deviceAuthReason != null && deviceAuthReason !== "missing_headers")) console.warn(`[sync] peer auth failed: reason=${auth.reason} device_auth_reason=${deviceAuthReason ?? "none"} grant_present=${grantPresent} path=${c.req.path} device=${loggableHeaderValue(c.req.header("X-Opencode-Device"))}`);
4405
+ const wireReason = bootstrapAttempted && grantPresent ? "bootstrap_grant_invalid" : deviceAuthReason ?? auth.reason;
3153
4406
  return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason, true), 401);
3154
4407
  }
3155
4408
  const limited = rateLimitedResponse(c, auth.deviceId, true);
3156
4409
  if (limited) return limited;
3157
4410
  try {
4411
+ if (c.req.header(SYNC_AUTHORIZATION_REFRESH_HEADER) === "1" && supportsSyncFeature(c.req.header(SYNC_FEATURES_HEADER), "reassign_scope")) await refreshConfiguredScopeMembershipCache(store.db, void 0, { keysDir: syncKeysDir() });
3158
4412
  const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3159
4413
  const syncReset = getSyncResetState(store.db);
3160
4414
  const authorizedScopes = isScopedSyncCapability(negotiatedSyncCapability(c)) ? listAuthorizedScopesForPeer(store.db, {
@@ -3166,7 +4420,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
3166
4420
  protocol_version: SYNC_PROTOCOL_VERSION,
3167
4421
  fingerprint,
3168
4422
  sync_reset: addSyncScopeToBoundary(syncReset, null),
3169
- sync_capability: LOCAL_SYNC_CAPABILITY
4423
+ sync_capability: LOCAL_SYNC_CAPABILITY,
4424
+ sync_features: LOCAL_SYNC_FEATURES
3170
4425
  };
3171
4426
  if (authorizedScopes !== null) response.authorized_scopes = authorizedScopes;
3172
4427
  return c.json(response);
@@ -3216,7 +4471,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
3216
4471
  ...addSyncScopeToBoundary(result.reset, scopeRequest.scope_id)
3217
4472
  }, 409);
3218
4473
  const { ops, nextCursor, boundary } = result;
3219
- const filtered = filterOpsForPeer(store, peerDeviceId, localDeviceId, ops);
4474
+ const filtered = filterOpsForPeer(store, peerDeviceId, localDeviceId, ops, { supportsReassignScope: supportsSyncFeature(c.req.header(SYNC_FEATURES_HEADER), "reassign_scope") });
3220
4475
  return c.json({
3221
4476
  reset_required: false,
3222
4477
  sync_capability: LOCAL_SYNC_CAPABILITY,
@@ -3241,6 +4496,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
3241
4496
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
3242
4497
  let preauthChecked = false;
3243
4498
  let bootstrapAttempted = false;
4499
+ let deviceAuthReason = null;
3244
4500
  if (!auth.ok) {
3245
4501
  if ((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()) {
3246
4502
  preauthChecked = true;
@@ -3251,14 +4507,15 @@ function syncProtocolRoutes(getStore, opts = {}) {
3251
4507
  const unauthLimited = rateLimitedResponse(c, c.req.path, false);
3252
4508
  if (unauthLimited) return unauthLimited;
3253
4509
  }
4510
+ deviceAuthReason = auth.reason;
3254
4511
  auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
3255
4512
  bootstrapAttempted = true;
3256
4513
  }
3257
4514
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
3258
4515
  if (!auth.ok) {
3259
4516
  const grantPresent = Boolean((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim());
3260
- if (bootstrapAttempted && grantPresent) console.warn(`[sync] bootstrap grant auth failed: reason=${auth.reason} grant_present=${grantPresent} path=${c.req.path}`);
3261
- const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
4517
+ if (bootstrapAttempted && (grantPresent || deviceAuthReason != null && deviceAuthReason !== "missing_headers")) console.warn(`[sync] peer auth failed: reason=${auth.reason} device_auth_reason=${deviceAuthReason ?? "none"} grant_present=${grantPresent} path=${c.req.path} device=${loggableHeaderValue(c.req.header("X-Opencode-Device"))}`);
4518
+ const wireReason = bootstrapAttempted && grantPresent ? "bootstrap_grant_invalid" : deviceAuthReason ?? auth.reason;
3262
4519
  return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason, true), 401);
3263
4520
  }
3264
4521
  const limited = rateLimitedResponse(c, auth.deviceId, true);
@@ -3340,12 +4597,23 @@ function syncProtocolRoutes(getStore, opts = {}) {
3340
4597
  if (!Array.isArray(body.ops)) return c.json({ error: "invalid_ops" }, 400);
3341
4598
  if (body.ops.length > MAX_SYNC_OPS) return c.json({ error: "too_many_ops" }, 413);
3342
4599
  const normalizedOps = extractReplicationOps(body);
4600
+ const peerFeatures = normalizeSyncFeatures(body.sync_features);
4601
+ const reassignOps = normalizedOps.filter((op) => op.op_type === "reassign_scope");
4602
+ if (reassignOps.length > 0 && !peerFeatures.includes("reassign_scope")) return c.json({ error: "reassign_capability_required" }, 409);
4603
+ try {
4604
+ for (const op of reassignOps) parseReassignScopePayload(op);
4605
+ } catch {
4606
+ return c.json({ error: "reassign_payload_invalid" }, 400);
4607
+ }
3343
4608
  for (const op of normalizedOps) if (op.device_id !== peerDeviceId || op.clock_device_id !== peerDeviceId) return c.json({
3344
4609
  error: "invalid_op_device",
3345
4610
  reason: "device_id_mismatch",
3346
4611
  op_id: op.op_id
3347
4612
  }, 400);
3348
- const filtered = filterOpsForPeer(store, peerDeviceId, localDeviceId, normalizedOps, { applyScopeFilter: false });
4613
+ const filtered = filterOpsForPeer(store, peerDeviceId, localDeviceId, normalizedOps, {
4614
+ applyScopeFilter: false,
4615
+ supportsReassignScope: peerFeatures.includes("reassign_scope")
4616
+ });
3349
4617
  const rejected = rejectInboundScopeFailures(store.db, normalizedOps, localDeviceId, {
3350
4618
  enabled: negotiated !== "unsupported",
3351
4619
  peerDeviceId
@@ -3411,6 +4679,23 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3411
4679
  }
3412
4680
  const lastSyncRow = traceSync("lastSyncRow", () => d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get());
3413
4681
  const semanticIndex = traceSync("semanticIndex", () => redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db), showDiag));
4682
+ const enrollmentIssueSummary = traceSync("coordinatorEnrollmentIssues", () => getCoordinatorEnrollmentReconciliationIssueSummary(store.db));
4683
+ const enrollmentIssueStatus = {
4684
+ counts: enrollmentIssueSummary.counts,
4685
+ ...showDiag ? { issues: enrollmentIssueSummary.issues.map((issue) => ({
4686
+ coordinator_id: issue.coordinatorId,
4687
+ group_id: issue.groupId,
4688
+ kind: issue.kind,
4689
+ reference_id: issue.referenceId,
4690
+ code: issue.code,
4691
+ status: issue.status,
4692
+ first_seen_at: issue.firstSeenAt,
4693
+ last_seen_at: issue.lastSeenAt,
4694
+ resolved_at: issue.resolvedAt,
4695
+ occurrence_count: issue.occurrenceCount,
4696
+ updated_at: issue.updatedAt
4697
+ })) } : {}
4698
+ };
3414
4699
  const lastError = daemonState?.last_error;
3415
4700
  const lastErrorAt = daemonState?.last_error_at;
3416
4701
  const lastOkAt = daemonState?.last_ok_at;
@@ -3438,6 +4723,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3438
4723
  last_error_at: retentionState?.last_error_at ?? null
3439
4724
  },
3440
4725
  semantic_index: semanticIndex,
4726
+ coordinator_enrollment_reconciliation_issues: enrollmentIssueStatus,
3441
4727
  peer_count: Number(peerCountRow?.total ?? 0),
3442
4728
  last_sync_at: lastSyncRow?.last_sync_at ?? null,
3443
4729
  daemon_state: daemonStateValue,
@@ -3510,6 +4796,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3510
4796
  const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
3511
4797
  const legacyReview = traceSync("legacySharedReview", () => legacySharedReviewSummary(store));
3512
4798
  const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
4799
+ const recipientPolicyReconciliation = traceSync("recipientPolicyReconciliation", () => listRecipientPolicyReconciliationStatus(store));
3513
4800
  let joinRequests = [];
3514
4801
  if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
3515
4802
  joinRequests = await listCoordinatorJoinRequests(config);
@@ -3545,6 +4832,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3545
4832
  legacy_devices: legacyDevices,
3546
4833
  legacy_shared_review: legacyReview,
3547
4834
  sharing_review: sharingReview,
4835
+ recipient_policy_reconciliation: recipientPolicyReconciliation,
3548
4836
  coordinator
3549
4837
  };
3550
4838
  if (includeJoinRequests && showDiag) responsePayload.join_requests = joinRequests;
@@ -3698,7 +4986,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3698
4986
  else peerIds = store.db.prepare("SELECT peer_device_id FROM sync_peers").all().map((row) => String(row.peer_device_id ?? "").trim()).filter(Boolean);
3699
4987
  const items = [];
3700
4988
  for (const peerId of peerIds) {
3701
- const result = await runSyncPass(store.db, peerId, { scanner: store.scanner });
4989
+ const result = await runSyncPass(store.db, peerId, {
4990
+ keysDir: syncKeysDir(),
4991
+ scanner: store.scanner
4992
+ });
3702
4993
  items.push({
3703
4994
  peer_device_id: peerId,
3704
4995
  ...result,
@@ -3747,18 +5038,245 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3747
5038
  local_default_scope_id: LOCAL_DEFAULT_SCOPE_ID
3748
5039
  });
3749
5040
  });
3750
- app.get("/api/sync/projects", (c) => {
5041
+ app.get("/api/sync/recipient-policy/v1/projection", (c) => {
5042
+ const store = getStore();
5043
+ return c.json(listLegacyRecipientPolicyProjections(store.db, {
5044
+ localActorId: store.actorId,
5045
+ localDeviceId: store.deviceId
5046
+ }));
5047
+ });
5048
+ app.get("/api/sync/recipient-policy/v1/review", (c) => {
5049
+ const store = getStore();
5050
+ ensureLocalActorRecord(store);
5051
+ return c.json(listRecipientPolicyReview(store.db, {
5052
+ localActorId: store.actorId,
5053
+ localDeviceId: store.deviceId
5054
+ }));
5055
+ });
5056
+ app.get("/api/sync/recipient-policy/v1/intent", (c) => {
5057
+ const store = getStore();
5058
+ return c.json(listRecipientPolicyIntent(store.db));
5059
+ });
5060
+ app.get("/api/sync/recipient-policy/v1/reconciliation-status", (c) => {
5061
+ return c.json(listRecipientPolicyReconciliationStatus(getStore()));
5062
+ });
5063
+ app.post("/api/sync/recipient-policy/v1/edges/preview", async (c) => {
5064
+ const store = getStore();
5065
+ const body = await parseViewerJsonBody(c);
5066
+ try {
5067
+ return c.json(previewRecipientPolicyEdges(store.db, body));
5068
+ } catch (error) {
5069
+ if (error instanceof RecipientPolicyEdgeRequestError) {
5070
+ if (error.status === "not_found") return c.json({ error: error.errorCode }, 404);
5071
+ return c.json({ error: error.errorCode }, 400);
5072
+ }
5073
+ if (isSqliteBusy(error)) {
5074
+ c.header("Retry-After", "1");
5075
+ return c.json({ error: "edge_preview_busy" }, 503);
5076
+ }
5077
+ return c.json({ error: "edge_preview_failed" }, 500);
5078
+ }
5079
+ });
5080
+ app.post("/api/sync/recipient-policy/v1/edges/commit", async (c) => {
5081
+ const store = getStore();
5082
+ const body = await parseViewerJsonBody(c);
5083
+ try {
5084
+ const result = commitRecipientPolicyEdges(store.db, body);
5085
+ if (result.status === "invalid") return c.json(result, 400);
5086
+ if (result.status === "not_found") return c.json(result, 404);
5087
+ if (result.status === "stale" || result.status === "conflict") return c.json(result, 409);
5088
+ return c.json(result);
5089
+ } catch (error) {
5090
+ if (isSqliteBusy(error)) {
5091
+ c.header("Retry-After", "1");
5092
+ return c.json({ error: "edge_commit_busy" }, 503);
5093
+ }
5094
+ return c.json({
5095
+ version: 1,
5096
+ status: "conflict",
5097
+ reviewedPolicyDigest: "",
5098
+ errorCode: "edge_commit_conflict",
5099
+ outcomes: [],
5100
+ writeCount: 0,
5101
+ idempotent: false
5102
+ }, 409);
5103
+ }
5104
+ });
5105
+ app.post("/api/sync/recipient-policy/v1/invites/preview", async (c) => {
5106
+ const store = getStore();
5107
+ const body = await parseViewerJsonBody(c);
5108
+ if (!body) return c.json({ error: "request_invalid" }, 400);
5109
+ try {
5110
+ const kind = recipientInviteKind(body.kind ?? body.invite_kind);
5111
+ const unavailable = recipientInviteCreationUnavailable(coordinatorAdminStatusPayload(readCoordinatorSyncConfig()), kind);
5112
+ if (unavailable) return c.json(unavailable.body, unavailable.httpStatus);
5113
+ const targetId = kind === "team_member" ? optionalViewerStrictString(body, "policy_team_id") : optionalViewerStrictString(body, "target_identity_id");
5114
+ if (!targetId) throw new Error("recipient_invite_metadata_invalid");
5115
+ if (kind === "add_device" && targetId !== store.actorId) throw new Error("invite_identity_conflict");
5116
+ const preview = recipientInviteOnboardingPreview(store, body, recipientInvitePreviewId(kind, targetId));
5117
+ return c.json({
5118
+ kind,
5119
+ preview
5120
+ });
5121
+ } catch (error) {
5122
+ return c.json({ error: error instanceof Error ? error.message : "recipient_invite_invalid" }, 400);
5123
+ }
5124
+ });
5125
+ app.post("/api/sync/recipient-policy/v1/invites", async (c) => {
5126
+ const store = getStore();
5127
+ const body = await parseViewerJsonBody(c);
5128
+ if (!body) return c.json({ error: "request_invalid" }, 400);
5129
+ try {
5130
+ const kind = recipientInviteKind(body.kind ?? body.invite_kind);
5131
+ const config = readCoordinatorSyncConfig();
5132
+ const status = coordinatorAdminStatusPayload(config);
5133
+ const unavailable = recipientInviteCreationUnavailable(status, kind);
5134
+ if (unavailable) return c.json(unavailable.body, unavailable.httpStatus);
5135
+ const targetId = kind === "team_member" ? optionalViewerStrictString(body, "policy_team_id") : optionalViewerStrictString(body, "target_identity_id");
5136
+ if (!targetId) throw new Error("recipient_invite_metadata_invalid");
5137
+ if (kind === "add_device" && targetId !== store.actorId) throw new Error("invite_identity_conflict");
5138
+ const preview = recipientInviteOnboardingPreview(store, body, recipientInvitePreviewId(kind, targetId));
5139
+ const reviewedDigest = optionalViewerStrictString(body, "reviewed_onboarding_digest");
5140
+ if (!reviewedDigest) throw new Error("reviewed_onboarding_digest_required");
5141
+ if (reviewedDigest !== preview.reviewedOnboardingDigest) return c.json({
5142
+ error: "reviewed_onboarding_stale",
5143
+ preview
5144
+ }, 409);
5145
+ const reviewedIntent = recipientInviteReviewedIntent(store, kind, targetId, preview);
5146
+ const reviewedPreviewDigest = await recipientReviewedIntentDigest(reviewedIntent);
5147
+ const groupId = resolveCoordinatorAdminGroup(optionalViewerString(body, "group_id"), status);
5148
+ const ttlHours = parseInviteTtlHours(body.ttl_hours);
5149
+ if (!groupId) throw new Error("group_id_required");
5150
+ if (ttlHours == null) throw new Error("ttl_hours_invalid");
5151
+ const result = kind === "team_member" || status.has_admin_secret ? await coordinatorCreateInviteAction({
5152
+ groupId,
5153
+ coordinatorUrl: config.syncCoordinatorUrl || null,
5154
+ policy: "auto_admit",
5155
+ ttlHours,
5156
+ createdBy: store.actorId,
5157
+ remoteUrl: config.syncCoordinatorUrl || null,
5158
+ adminSecret: config.syncCoordinatorAdminSecret || null,
5159
+ inviteKind: kind,
5160
+ policyTeamId: kind === "team_member" ? targetId : null,
5161
+ targetIdentityId: kind === "add_device" ? targetId : null,
5162
+ inviterDeviceId: kind === "add_device" ? preview.binding.deviceId : null,
5163
+ reviewedPreviewDigest,
5164
+ reviewedIntent
5165
+ }) : await coordinatorCreateAddDeviceInviteAction({
5166
+ groupId,
5167
+ coordinatorUrl: config.syncCoordinatorUrl || null,
5168
+ ttlHours,
5169
+ deviceId: preview.binding.deviceId,
5170
+ keysDir: syncKeysDir(),
5171
+ remoteUrl: config.syncCoordinatorUrl || null,
5172
+ reviewedPreviewDigest,
5173
+ reviewedIntent
5174
+ });
5175
+ return c.json({
5176
+ ok: true,
5177
+ kind,
5178
+ preview,
5179
+ invite: result
5180
+ });
5181
+ } catch (error) {
5182
+ return c.json({ error: error instanceof Error ? error.message : "recipient_invite_failed" }, 400);
5183
+ }
5184
+ });
5185
+ app.post("/api/sync/recipient-policy/v1/migrate", async (c) => {
5186
+ const store = getStore();
5187
+ const value = await parseViewerJsonBody(c);
5188
+ if (!value || typeof value !== "object" || Array.isArray(value)) return c.json({ error: "request_invalid" }, 400);
5189
+ const body = value;
5190
+ if (Object.keys(body).some((key) => key !== "dryRun") || Object.hasOwn(body, "dryRun") && typeof body.dryRun !== "boolean") return c.json({ error: "request_invalid" }, 400);
5191
+ return c.json(migrateRecipientPolicyIntent(store.db, {
5192
+ localActorId: store.actorId,
5193
+ localDeviceId: store.deviceId
5194
+ }, { dryRun: body.dryRun === true }));
5195
+ });
5196
+ const parseReviewRequest = (value) => {
5197
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5198
+ const body = value;
5199
+ const allowedKeys = new Set([
5200
+ "reviewItemId",
5201
+ "sourceFingerprint",
5202
+ "decision",
5203
+ "decisionInput"
5204
+ ]);
5205
+ if (Object.keys(body).some((key) => !allowedKeys.has(key)) || typeof body.reviewItemId !== "string" || typeof body.sourceFingerprint !== "string" || typeof body.decision !== "string") return null;
5206
+ return {
5207
+ reviewItemId: body.reviewItemId,
5208
+ sourceFingerprint: body.sourceFingerprint,
5209
+ decision: body.decision,
5210
+ ...Object.hasOwn(body, "decisionInput") ? { decisionInput: body.decisionInput } : {}
5211
+ };
5212
+ };
5213
+ app.post("/api/sync/recipient-policy/v1/review/resolve", async (c) => {
5214
+ const store = getStore();
5215
+ const request = parseReviewRequest(await parseViewerJsonBody(c));
5216
+ if (!request) return c.json({ error: "request_invalid" }, 400);
5217
+ ensureLocalActorRecord(store);
5218
+ const result = resolveRecipientPolicyReview(store.db, {
5219
+ localActorId: store.actorId,
5220
+ localDeviceId: store.deviceId
5221
+ }, request);
5222
+ if (result.status === "stale" || result.status === "conflict") return c.json(result, 409);
5223
+ if (result.status === "not_found") return c.json(result, 404);
5224
+ if (result.status === "invalid") return c.json(result, 400);
5225
+ return c.json(result);
5226
+ });
5227
+ app.post("/api/sync/recipient-policy/v1/review/resolve-bulk", async (c) => {
5228
+ const store = getStore();
5229
+ const body = await parseViewerJsonBody(c);
5230
+ if (!body || !Array.isArray(body.requests) || body.requests.length === 0 || body.requests.length > 100) return c.json({ error: "request_invalid" }, 400);
5231
+ const requests = body.requests.map(parseReviewRequest);
5232
+ if (requests.some((request) => request == null)) return c.json({ error: "request_invalid" }, 400);
5233
+ ensureLocalActorRecord(store);
5234
+ const result = resolveRecipientPolicyReviewBulk(store.db, {
5235
+ localActorId: store.actorId,
5236
+ localDeviceId: store.deviceId
5237
+ }, requests);
5238
+ return c.json(result, result.results.some((item) => item.status !== "applied") ? 207 : 200);
5239
+ });
5240
+ app.get("/api/sync/projects", async (c) => {
3751
5241
  const store = getStore();
3752
5242
  const limit = Math.max(1, queryInt(c.req.query("limit"), 50));
3753
5243
  const offset = Math.max(0, queryInt(c.req.query("offset"), 0));
3754
- return c.json(listProjectScopeInventory(store.db, {
5244
+ const inventory = listProjectScopeInventory(store.db, {
3755
5245
  identitySource: c.req.query("identity_source"),
3756
5246
  limit,
3757
5247
  offset,
3758
5248
  query: c.req.query("q"),
3759
5249
  scopeId: c.req.query("scope_id"),
3760
5250
  status: c.req.query("status")
3761
- }));
5251
+ });
5252
+ const operations = await shareOperationReadModels(store, void 0, false);
5253
+ const operationById = new Map(operations.map((operation) => [operation.operation_id, operation]));
5254
+ const sharingByProject = /* @__PURE__ */ new Map();
5255
+ const reviewed = store.db.prepare(`SELECT p.operation_id, p.canonical_project_identity
5256
+ FROM share_operation_projects p
5257
+ JOIN share_operations o ON o.operation_id = p.operation_id
5258
+ WHERE o.inviter_actor_id = ? ORDER BY o.created_at, p.ordinal`).all(store.actorId);
5259
+ for (const item of reviewed) {
5260
+ const operation = operationById.get(item.operation_id);
5261
+ if (!operation) continue;
5262
+ const current = sharingByProject.get(item.canonical_project_identity) ?? [];
5263
+ current.push({
5264
+ person: operation.person,
5265
+ lifecycle: {
5266
+ state: operation.lifecycle.state,
5267
+ label: operation.lifecycle.label,
5268
+ explanation: operation.lifecycle.explanation
5269
+ }
5270
+ });
5271
+ sharingByProject.set(item.canonical_project_identity, current);
5272
+ }
5273
+ return c.json({
5274
+ ...inventory,
5275
+ projects: inventory.projects.map((project) => ({
5276
+ ...project,
5277
+ sharing: sharingByProject.get(project.workspace_identity) ?? []
5278
+ }))
5279
+ });
3762
5280
  });
3763
5281
  app.post("/api/sync/projects/reassign-project", async (c) => {
3764
5282
  const store = getStore();
@@ -3808,6 +5326,175 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3808
5326
  return c.json({ error: message }, message.includes("not found") ? 404 : 400);
3809
5327
  }
3810
5328
  });
5329
+ app.post("/api/sync/project-invites/preview", async (c) => {
5330
+ const store = getStore();
5331
+ const body = await parseViewerJsonBody(c);
5332
+ if (!body) return c.json({ error: "invalid_json" }, 400);
5333
+ try {
5334
+ assertProjectInviteBody(body, false);
5335
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
5336
+ const { plan } = projectInvitePlan(store, body, createdAt, new Date(new Date(createdAt).getTime() + PROJECT_INVITE_TTL_HOURS * 3600 * 1e3).toISOString());
5337
+ return c.json(projectInvitePreview(plan));
5338
+ } catch (error) {
5339
+ return c.json({ error: error instanceof Error ? error.message : "project_invite_invalid" }, 400);
5340
+ }
5341
+ });
5342
+ app.post("/api/sync/project-invites", async (c) => {
5343
+ const store = getStore();
5344
+ const body = await parseViewerJsonBody(c);
5345
+ if (!body) return c.json({ error: "invalid_json" }, 400);
5346
+ try {
5347
+ assertProjectInviteBody(body, true);
5348
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
5349
+ const { plan, config } = projectInvitePlan(store, body, createdAt, new Date(new Date(createdAt).getTime() + PROJECT_INVITE_TTL_HOURS * 3600 * 1e3).toISOString());
5350
+ if (String(body.reviewed_project_set_digest).trim() !== plan.reviewedProjectSetDigest) return c.json({ error: "reviewed_project_set_changed" }, 409);
5351
+ const existingState = (() => {
5352
+ try {
5353
+ return store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(plan.operationId);
5354
+ } catch {
5355
+ return;
5356
+ }
5357
+ })();
5358
+ if (existingState && existingState !== "waiting_for_acceptance") return c.json({ error: "operation_state_invalid" }, 409);
5359
+ const result = await coordinatorCreateInviteAction({
5360
+ groupId: plan.coordinatorGroupId,
5361
+ coordinatorUrl: config.syncCoordinatorUrl || null,
5362
+ policy: "auto_admit",
5363
+ ttlHours: PROJECT_INVITE_TTL_HOURS,
5364
+ createdBy: store.actorId,
5365
+ remoteUrl: config.syncCoordinatorUrl || null,
5366
+ adminSecret: config.syncCoordinatorAdminSecret || null,
5367
+ operationId: plan.operationId,
5368
+ reviewedProjectSetDigest: plan.reviewedProjectSetDigest,
5369
+ inviterActorId: plan.inviterActorId,
5370
+ inviterDisplayName: store.actorDisplayName,
5371
+ inviterDeviceId: plan.inviterDeviceIds[0],
5372
+ pendingPersonId: plan.personId,
5373
+ projectSummaries: plan.projects.map((project) => ({
5374
+ display_name: project.displayName,
5375
+ existing_memory_count: project.existingMemoryCount
5376
+ })),
5377
+ projectIntent: plan.projects.map((project) => ({
5378
+ canonical_identity: project.canonicalIdentity,
5379
+ display_name: project.displayName,
5380
+ existing_memory_count: project.existingMemoryCount
5381
+ }))
5382
+ });
5383
+ if (String(result.operation_id ?? "").trim() !== plan.operationId || String(result.reviewed_project_set_digest ?? "").trim() !== plan.reviewedProjectSetDigest) throw new Error("coordinator_invite_intent_mismatch");
5384
+ const payload = result.payload;
5385
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("coordinator_invite_payload_missing");
5386
+ const token = String(payload.token ?? "").trim();
5387
+ const expiresAt = String(payload.expires_at ?? "").trim();
5388
+ const link = String(result.link ?? "").trim();
5389
+ const encoded = String(result.encoded ?? "").trim();
5390
+ if (!token || !expiresAt || !link || !encoded) throw new Error("coordinator_invite_payload_invalid");
5391
+ const persistedPlan = {
5392
+ ...plan,
5393
+ inviteExpiresAt: new Date(expiresAt).toISOString()
5394
+ };
5395
+ persistShareOperation(store.db, persistedPlan, {
5396
+ inviteId: String(result.invite_id ?? "").trim() || null,
5397
+ tokenDigest: inviteTokenDigest(token)
5398
+ });
5399
+ return c.json({
5400
+ ok: true,
5401
+ ...projectInvitePreview(persistedPlan),
5402
+ invite: {
5403
+ link,
5404
+ encoded,
5405
+ expires_at: persistedPlan.inviteExpiresAt
5406
+ }
5407
+ });
5408
+ } catch (error) {
5409
+ return c.json({ error: error instanceof Error ? error.message : "project_invite_failed" }, 400);
5410
+ }
5411
+ });
5412
+ app.get("/api/sync/project-invites/:operationId", async (c) => {
5413
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5414
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5415
+ try {
5416
+ const { payload } = await coordinatorProjectInvitePayload(operationId);
5417
+ return c.json(payload.state === "accepted" ? {
5418
+ ...payload,
5419
+ state: "pending_setup"
5420
+ } : payload);
5421
+ } catch (error) {
5422
+ const mapped = projectInviteOwnerErrorResponse(error, {
5423
+ code: "operation_read_failed",
5424
+ status: 400
5425
+ });
5426
+ return c.json({ error: mapped.code }, mapped.status);
5427
+ }
5428
+ });
5429
+ app.post("/api/sync/project-invites/:operationId/reconcile", async (c) => {
5430
+ const store = getStore();
5431
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5432
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5433
+ try {
5434
+ const result = await reconcileProjectInviteAcceptance(store, operationId);
5435
+ return c.json({
5436
+ ok: true,
5437
+ operation_id: operationId,
5438
+ reconciled: result.accepted,
5439
+ state: result.accepted ? "pending_setup" : "waiting_for_acceptance"
5440
+ });
5441
+ } catch (error) {
5442
+ const mapped = projectInviteOwnerErrorResponse(error, {
5443
+ code: "operation_reconcile_failed",
5444
+ status: 409
5445
+ });
5446
+ return c.json({ error: mapped.code }, mapped.status);
5447
+ }
5448
+ });
5449
+ app.get("/api/sync/share-operations", async (c) => {
5450
+ const items = await shareOperationReadModels(getStore(), void 0, false);
5451
+ return c.json({ items });
5452
+ });
5453
+ app.get("/api/sync/share-operations/:operationId", async (c) => {
5454
+ const store = getStore();
5455
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5456
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5457
+ const [item] = await shareOperationReadModels(store, operationId);
5458
+ return item ? c.json(item) : c.json({ error: "operation_not_found" }, 404);
5459
+ });
5460
+ const advanceProjectShare = async (c) => {
5461
+ const store = getStore();
5462
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5463
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5464
+ try {
5465
+ const advanced = await advanceProjectShareOperation(store, operationId);
5466
+ if (!advanced.advanced) return c.json({ error: advanced.state === "cancelled" ? "operation_superseded" : "invitation_not_accepted" }, 409);
5467
+ const [operation] = await shareOperationReadModels(store, operationId);
5468
+ return c.json({
5469
+ ok: true,
5470
+ operation
5471
+ });
5472
+ } catch (error) {
5473
+ const code = error instanceof Error ? error.message : "provisioning_failed";
5474
+ if (store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(operationId) === "cancelled") return c.json({ error: "operation_superseded" }, 409);
5475
+ return c.json({ error: code }, code === "operation_not_found" ? 404 : 409);
5476
+ }
5477
+ };
5478
+ app.post("/api/sync/share-operations/:operationId/advance", advanceProjectShare);
5479
+ app.post("/api/sync/project-invites/:operationId/provision", async (c) => {
5480
+ const store = getStore();
5481
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5482
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5483
+ try {
5484
+ const plan = await executeProjectShareProvisioning(store, operationId);
5485
+ if (plan.superseded) return c.json({ error: "operation_superseded" }, 409);
5486
+ return c.json({
5487
+ ok: true,
5488
+ operation_id: operationId,
5489
+ state: "active",
5490
+ plan
5491
+ });
5492
+ } catch (error) {
5493
+ const code = error instanceof Error ? error.message : "provisioning_failed";
5494
+ if (store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(operationId) === "cancelled") return c.json({ error: "operation_superseded" }, 409);
5495
+ return c.json({ error: code }, code === "operation_not_found" ? 404 : 409);
5496
+ }
5497
+ });
3811
5498
  app.post("/api/sync/legacy-shared-review/reassign", async (c) => {
3812
5499
  const store = getStore();
3813
5500
  const body = await parseViewerJsonBody(c);
@@ -4173,6 +5860,73 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4173
5860
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
4174
5861
  }
4175
5862
  });
5863
+ app.post("/api/sync/invites/inspect", async (c) => {
5864
+ const store = getStore();
5865
+ const body = await parseViewerJsonBody(c);
5866
+ if (!body || typeof body.invite !== "string") return c.json({ error: "invite_invalid" }, 400);
5867
+ try {
5868
+ const payload = decodeInvitePayload(extractInvitePayload(body.invite));
5869
+ const recipientInvite = payload.kind === "team_member" || payload.kind === "add_device";
5870
+ if (recipientInvite) ensureStableStoreDeviceIdentity(store);
5871
+ else ensureStableStoreIdentity(store);
5872
+ if (!payload.operation_id && !recipientInvite) return c.json({ kind: "legacy_team_invite" });
5873
+ const [status, inspected] = await requestJson("POST", `${buildBaseUrl(payload.coordinator_url)}/v1/invites/inspect`, {
5874
+ body: { token: payload.token },
5875
+ timeoutS: 10
5876
+ });
5877
+ if (status < 200 || status >= 300) return c.json({ error: recipientInvite ? safeRecipientInviteError(inspected?.error) : String(inspected?.error ?? "invite_invalid") }, status === 410 ? 410 : 400);
5878
+ const config = readCodememConfigFile();
5879
+ const identityId = recipientInvite ? recipientInviteAuthoritativeIdentityId({
5880
+ kind: payload.kind,
5881
+ assigned_identity_id: inspected?.assigned_identity_id,
5882
+ target_identity_id: inspected?.target_identity_id
5883
+ }) : store.actorId;
5884
+ const expectedTarget = payload.kind === "team_member" ? String(inspected?.policy_team_id ?? "").trim() : String(inspected?.target_identity_id ?? "").trim();
5885
+ if (recipientInvite) {
5886
+ if (String(inspected?.kind ?? "").trim() !== payload.kind || !expectedTarget || String(inspected?.reviewed_preview_digest ?? "").trim() !== String(payload.reviewed_preview_digest ?? "").trim() || payload.kind === "team_member" && expectedTarget !== payload.policy_team_id || payload.kind === "team_member" && (identityId !== String(payload.assigned_identity_id ?? "").trim() || !identityId) || payload.kind === "add_device" && expectedTarget !== payload.target_identity_id) return c.json({ error: "recipient_invite_intent_mismatch" }, 409);
5887
+ if (inspected?.reviewed_intent == null) return c.json({ error: "recipient_invite_review_unavailable" }, 409);
5888
+ let reviewedIntent;
5889
+ try {
5890
+ reviewedIntent = await verifyRecipientReviewedIntent(inspected.reviewed_intent, {
5891
+ target: payload.kind === "team_member" ? {
5892
+ kind: "team_member",
5893
+ policyTeamId: expectedTarget
5894
+ } : {
5895
+ kind: "add_device",
5896
+ targetIdentityId: expectedTarget
5897
+ },
5898
+ digest: String(payload.reviewed_preview_digest ?? "").trim()
5899
+ });
5900
+ } catch {
5901
+ return c.json({ error: "recipient_invite_intent_mismatch" }, 409);
5902
+ }
5903
+ const preview = recipientInviteOnboardingPreviewFromReviewedIntent(store, {
5904
+ kind: payload.kind,
5905
+ policy_team_id: payload.kind === "team_member" ? expectedTarget : null,
5906
+ assigned_identity_id: payload.kind === "team_member" ? identityId : null,
5907
+ target_identity_id: payload.kind === "add_device" ? identityId : null,
5908
+ device_name: optionalViewerString(body, "device_name")
5909
+ }, String(payload.token), reviewedIntent);
5910
+ return c.json({
5911
+ ...inspected,
5912
+ recipient_name: reviewedIntent.journey === "add_device" ? reviewedIntent.targetIdentity.displayName : store.actorDisplayName,
5913
+ device_name: preview.binding.deviceDisplayName,
5914
+ onboarding: preview
5915
+ });
5916
+ }
5917
+ return c.json({
5918
+ ...inspected,
5919
+ recipient_name: store.actorDisplayName,
5920
+ device_name: friendlyDeviceName({
5921
+ explicitName: String(config.sync_device_name ?? ""),
5922
+ osName: hostname(),
5923
+ fallbackSeed: store.deviceId
5924
+ })
5925
+ });
5926
+ } catch (error) {
5927
+ return c.json({ error: safeRecipientInviteError(error) }, 400);
5928
+ }
5929
+ });
4176
5930
  app.post("/api/sync/invites/import", async (c) => {
4177
5931
  const store = getStore();
4178
5932
  let body;
@@ -4205,16 +5959,49 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4205
5959
  } catch (error) {
4206
5960
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
4207
5961
  }
5962
+ let projectInvite = false;
5963
+ let recipientInvite = false;
4208
5964
  try {
5965
+ const decoded = decodeInvitePayload(extractInvitePayload(rawValue));
5966
+ projectInvite = Boolean(decoded.operation_id);
5967
+ recipientInvite = decoded.kind === "team_member" || decoded.kind === "add_device";
5968
+ if (recipientInvite) ensureStableStoreDeviceIdentity(store);
5969
+ else ensureStableStoreIdentity(store);
5970
+ const reviewedOnboardingDigest = typeof body.reviewed_onboarding_digest === "string" ? body.reviewed_onboarding_digest.trim() : "";
5971
+ if (recipientInvite && !reviewedOnboardingDigest) return c.json({ error: "reviewed_onboarding_digest_required" }, 400);
4209
5972
  const result = await coordinatorImportInviteAction({
4210
5973
  inviteValue: rawValue,
4211
- dbPath: store.dbPath
5974
+ dbPath: store.dbPath,
5975
+ recipientActorId: recipientInvite ? null : store.actorId,
5976
+ recipientDisplayName: typeof body.recipient_name === "string" ? body.recipient_name : store.actorDisplayName,
5977
+ deviceDisplayName: typeof body.device_name === "string" ? body.device_name : null,
5978
+ reviewedOnboardingDigest: reviewedOnboardingDigest || null
4212
5979
  });
5980
+ const type = recipientInvite ? "recipient_onboarding" : projectInvite ? "project_share" : "team_join";
5981
+ if (recipientInvite) {
5982
+ const persistedIdentityId = String(result.identity_id ?? "").trim();
5983
+ if (!store.refreshPersistedLocalIdentity(persistedIdentityId)) return c.json({
5984
+ ...result,
5985
+ type,
5986
+ setup_state: "restart_required",
5987
+ restart_required: true,
5988
+ detail: "The invitation was accepted and the reviewed Identity was persisted, but this process could not safely adopt it. Restart codemem before continuing."
5989
+ }, 200);
5990
+ }
5991
+ if ((projectInvite || recipientInvite) && getSyncRuntimeStatus?.()?.phase === "disabled") return c.json({
5992
+ ...result,
5993
+ type,
5994
+ setup_state: "restart_required",
5995
+ restart_required: true,
5996
+ detail: projectInvite ? "The invitation was accepted and sync was enabled. Restart codemem to start the sync service; Project setup remains pending until the first sync finishes." : "The invitation was accepted and sync was enabled. Restart codemem to start the sync service and receive the reviewed Projects."
5997
+ }, 200);
4213
5998
  return c.json({
4214
5999
  ...result,
4215
- type: "team_join"
6000
+ type
4216
6001
  });
4217
6002
  } catch (error) {
6003
+ if (projectInvite) return c.json(projectInviteAcceptanceFailure(error), 400);
6004
+ if (recipientInvite) return c.json({ error: safeRecipientInviteError(error) }, 400);
4218
6005
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
4219
6006
  }
4220
6007
  });
@@ -4659,17 +6446,34 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4659
6446
  error: "membership_epoch must be number",
4660
6447
  status
4661
6448
  }, 400);
6449
+ const role = optionalViewerString(body, "role");
6450
+ const coordinatorId = optionalViewerString(body, "coordinator_id");
6451
+ const manifestIssuerDeviceId = optionalViewerString(body, "manifest_issuer_device_id");
6452
+ const manifestHash = optionalViewerString(body, "manifest_hash");
6453
+ const signedManifestJson = optionalViewerString(body, "signed_manifest_json");
6454
+ const effectId = optionalViewerString(body, "effect_id") ?? coordinatorAdminMembershipEffectId("grant", randomUUID(), {
6455
+ groupId,
6456
+ scopeId,
6457
+ deviceId,
6458
+ role,
6459
+ membershipEpoch,
6460
+ coordinatorId,
6461
+ manifestIssuerDeviceId,
6462
+ manifestHash,
6463
+ signedManifestJson
6464
+ });
4662
6465
  try {
4663
6466
  const membership = await coordinatorGrantScopeMembershipAction({
6467
+ effectId,
4664
6468
  groupId,
4665
6469
  scopeId,
4666
6470
  deviceId,
4667
- role: optionalViewerString(body, "role"),
6471
+ role,
4668
6472
  membershipEpoch,
4669
- coordinatorId: optionalViewerString(body, "coordinator_id"),
4670
- manifestIssuerDeviceId: optionalViewerString(body, "manifest_issuer_device_id"),
4671
- manifestHash: optionalViewerString(body, "manifest_hash"),
4672
- signedManifestJson: optionalViewerString(body, "signed_manifest_json"),
6473
+ coordinatorId,
6474
+ manifestIssuerDeviceId,
6475
+ manifestHash,
6476
+ signedManifestJson,
4673
6477
  remoteUrl: config.syncCoordinatorUrl || null,
4674
6478
  adminSecret: config.syncCoordinatorAdminSecret || null
4675
6479
  });
@@ -4718,14 +6522,25 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4718
6522
  error: "membership_epoch must be number",
4719
6523
  status
4720
6524
  }, 400);
6525
+ const manifestHash = optionalViewerString(body, "manifest_hash");
6526
+ const signedManifestJson = optionalViewerString(body, "signed_manifest_json");
6527
+ const effectId = optionalViewerString(body, "effect_id") ?? coordinatorAdminMembershipEffectId("revoke", randomUUID(), {
6528
+ groupId,
6529
+ scopeId,
6530
+ deviceId,
6531
+ membershipEpoch,
6532
+ manifestHash,
6533
+ signedManifestJson
6534
+ });
4721
6535
  try {
4722
6536
  if (!await coordinatorRevokeScopeMembershipAction({
6537
+ effectId,
4723
6538
  groupId,
4724
6539
  scopeId,
4725
6540
  deviceId,
4726
6541
  membershipEpoch,
4727
- manifestHash: optionalViewerString(body, "manifest_hash"),
4728
- signedManifestJson: optionalViewerString(body, "signed_manifest_json"),
6542
+ manifestHash,
6543
+ signedManifestJson,
4729
6544
  remoteUrl: config.syncCoordinatorUrl || null,
4730
6545
  adminSecret: config.syncCoordinatorAdminSecret || null
4731
6546
  })) return c.json({
@@ -4878,6 +6693,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4878
6693
  store: getStore(),
4879
6694
  config,
4880
6695
  groupId: result.group_id,
6696
+ requestId,
4881
6697
  deviceId: result.device_id
4882
6698
  });
4883
6699
  } catch (grantError) {
@@ -5519,6 +7335,6 @@ function createSyncApp(opts) {
5519
7335
  return app;
5520
7336
  }
5521
7337
  //#endregion
5522
- export { VERSION, closeStore, createApp, createSyncApp, getStore };
7338
+ export { VERSION, advancePendingProjectShares, advanceProjectShareOperation, closeStore, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects };
5523
7339
 
5524
7340
  //# sourceMappingURL=index.js.map