@codemem/server 0.39.0 → 0.40.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, 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, getCoordinatorGroupPreference, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, inviteTokenDigest, 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, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, recordNonce, 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,6 +195,7 @@ 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",
@@ -227,6 +233,7 @@ var ALLOWED_KEYS = [
227
233
  ];
228
234
  var DEFAULTS = {
229
235
  claude_command: ["claude"],
236
+ codex_command: ["codex"],
230
237
  observer_runtime: "api_http",
231
238
  observer_auth_source: "auto",
232
239
  observer_tier_routing_enabled: false,
@@ -262,9 +269,12 @@ function getEffectiveConfig(configData) {
262
269
  ...DEFAULTS,
263
270
  ...configData
264
271
  };
272
+ for (const key of ["claude_command", "codex_command"]) effective[key] = coerceObserverCommand(effective[key]) ?? DEFAULTS[key];
265
273
  for (const [key, envVar] of Object.entries(CODEMEM_CONFIG_ENV_OVERRIDES)) {
266
274
  const val = process.env[envVar];
267
- if (val != null && val !== "") effective[key] = val;
275
+ if (val == null || val === "") continue;
276
+ if (key === "claude_command" || key === "codex_command") effective[key] = coerceObserverCommand(val) ?? effective[key];
277
+ else effective[key] = val;
268
278
  }
269
279
  return effective;
270
280
  }
@@ -358,7 +368,7 @@ function validateAndApplyUpdate(configData, key, value, providers) {
358
368
  if (key === "observer_runtime") {
359
369
  if (typeof value !== "string") return "observer_runtime must be string";
360
370
  const runtime = value.trim().toLowerCase();
361
- if (!RUNTIMES.has(runtime)) return "observer_runtime must be one of: api_http, claude_sidecar";
371
+ if (!RUNTIMES.has(runtime)) return "observer_runtime must be one of: api_http, claude_sidecar, codex_sidecar";
362
372
  configData[key] = runtime;
363
373
  return null;
364
374
  }
@@ -369,7 +379,7 @@ function validateAndApplyUpdate(configData, key, value, providers) {
369
379
  configData[key] = source;
370
380
  return null;
371
381
  }
372
- if (key === "claude_command" || key === "observer_auth_command") {
382
+ if (key === "claude_command" || key === "codex_command" || key === "observer_auth_command") {
373
383
  const argv = asExecutableArgv(value);
374
384
  if (argv == null) return `${key} must be string array`;
375
385
  if (argv.length > 0) configData[key] = argv;
@@ -471,7 +481,7 @@ function configRoutes(opts = {}) {
471
481
  }
472
482
  const afterEffective = getEffectiveConfig(nextConfig);
473
483
  const savedChangedKeys = ALLOWED_KEYS.filter((key) => beforeConfig[key] !== nextConfig[key]);
474
- const effectiveChangedKeys = ALLOWED_KEYS.filter((key) => beforeEffective[key] !== afterEffective[key]);
484
+ const effectiveChangedKeys = ALLOWED_KEYS.filter((key) => !configValuesEqual(beforeEffective[key], afterEffective[key]));
475
485
  const envOverrides = getCodememEnvOverrides();
476
486
  const ignoredByEnvKeys = savedChangedKeys.filter((key) => !effectiveChangedKeys.includes(key) && key in envOverrides);
477
487
  const hotReloadedKeys = applyRuntimeEffects([...new Set([
@@ -1819,6 +1829,7 @@ async function ensureDefaultSpaceForTeam(opts) {
1819
1829
  return {
1820
1830
  scope,
1821
1831
  membership: await coordinatorGrantScopeMembershipAction({
1832
+ effectId: `team-default-grant:${opts.groupId}:${scopeId}:${deviceId}:1`,
1822
1833
  groupId: opts.groupId,
1823
1834
  scopeId,
1824
1835
  deviceId,
@@ -1851,6 +1862,7 @@ async function maybeGrantDefaultSpaceOnJoin(opts) {
1851
1862
  adminSecret: opts.config.syncCoordinatorAdminSecret || null
1852
1863
  })).find((scope) => scope.scope_id === scopeId) ?? null)?.kind !== "team_default") return null;
1853
1864
  return await coordinatorGrantScopeMembershipAction({
1865
+ effectId: `team-default-join:${opts.requestId}:${scopeId}:${opts.deviceId}:1`,
1854
1866
  groupId: opts.groupId,
1855
1867
  scopeId,
1856
1868
  deviceId: opts.deviceId,
@@ -1892,6 +1904,30 @@ function coordinatorAdminUnavailable(status) {
1892
1904
  };
1893
1905
  return null;
1894
1906
  }
1907
+ function recipientInviteCreationUnavailable(status, kind) {
1908
+ if (status.readiness === "not_configured") return {
1909
+ body: {
1910
+ error: "coordinator_not_configured",
1911
+ status
1912
+ },
1913
+ httpStatus: 400
1914
+ };
1915
+ if (!status.has_groups) return {
1916
+ body: {
1917
+ error: "coordinator_group_missing",
1918
+ status
1919
+ },
1920
+ httpStatus: 400
1921
+ };
1922
+ if (kind === "team_member" && !status.has_admin_secret) return {
1923
+ body: {
1924
+ error: "coordinator_admin_secret_missing",
1925
+ status
1926
+ },
1927
+ httpStatus: 400
1928
+ };
1929
+ return null;
1930
+ }
1895
1931
  async function parseViewerJsonBody(c, options = {}) {
1896
1932
  const raw = await c.req.text();
1897
1933
  if (!raw.trim()) return options.allowEmpty ? {} : null;
@@ -1929,6 +1965,21 @@ function optionalViewerNumber(body, key) {
1929
1965
  const number = typeof value === "number" ? value : Number(value);
1930
1966
  return Number.isFinite(number) ? Math.trunc(number) : NaN;
1931
1967
  }
1968
+ function coordinatorAdminMembershipEffectId(action, attemptId, input) {
1969
+ return `viewer-admin-membership:${action}:${createHash("sha256").update(JSON.stringify({
1970
+ action,
1971
+ attemptId,
1972
+ groupId: input.groupId,
1973
+ scopeId: input.scopeId,
1974
+ deviceId: input.deviceId,
1975
+ role: input.role ?? null,
1976
+ membershipEpoch: input.membershipEpoch,
1977
+ coordinatorId: input.coordinatorId ?? null,
1978
+ manifestIssuerDeviceId: input.manifestIssuerDeviceId ?? null,
1979
+ manifestHash: input.manifestHash,
1980
+ signedManifestJson: input.signedManifestJson
1981
+ })).digest("hex")}`;
1982
+ }
1932
1983
  function optionalViewerInteger(body, key) {
1933
1984
  const value = body[key];
1934
1985
  if (value == null || value === "") return null;
@@ -1955,7 +2006,7 @@ function parseViewerProjectMappingInput(body) {
1955
2006
  }
1956
2007
  function coordinatorAdminMutationStatus(message) {
1957
2008
  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;
2009
+ 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
2010
  if (message.includes("Remote coordinator request failed (5") || message.includes("fetch failed") || message.includes("network") || message.includes("timed out")) return 502;
1960
2011
  return 400;
1961
2012
  }
@@ -1976,6 +2027,980 @@ function parseInviteTtlHours(value) {
1976
2027
  if (!Number.isInteger(ttlHours) || ttlHours < 1) return null;
1977
2028
  return ttlHours;
1978
2029
  }
2030
+ function projectInviteAcceptanceFailure(error) {
2031
+ if (isProjectSyncEnablementError(error)) return {
2032
+ error: error.code,
2033
+ detail: error.detail
2034
+ };
2035
+ const code = error instanceof Error ? error.message : "project_invite_acceptance_failed";
2036
+ const detailByCode = {
2037
+ invite_already_bound: "This invitation was accepted by another device. Ask the owner to create a new invitation.",
2038
+ invite_expired: "This invitation expired. Ask the owner to create a new invitation.",
2039
+ invite_identity_conflict: "This invitation does not match this Identity. Ask the owner to create a new invitation for this recipient.",
2040
+ invite_invalid: "This invitation is no longer valid. Ask the owner to create a new invitation.",
2041
+ inviter_identity_invalid: "The owner's device identity could not be verified. Ask the owner to review the share and create a new invitation.",
2042
+ project_invite_self_acceptance_forbidden: "The owner cannot accept this recipient invitation on the owner's device.",
2043
+ 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.",
2044
+ project_invite_trust_state_invalid: "The owner's trust setup could not be verified. Ask the owner to review the share."
2045
+ };
2046
+ return {
2047
+ error: Object.hasOwn(detailByCode, code) ? code : "project_invite_acceptance_failed",
2048
+ detail: detailByCode[code] ?? "The invitation could not be accepted safely. Retry once, then ask the owner to review the share."
2049
+ };
2050
+ }
2051
+ var PROJECT_INVITE_TTL_HOURS = 168;
2052
+ var PROJECT_INVITE_BODY_KEYS = new Set([
2053
+ "teammate_name",
2054
+ "project_ids",
2055
+ "reviewed_project_set_digest"
2056
+ ]);
2057
+ function projectInviteStringList(body, key) {
2058
+ const value = body[key];
2059
+ if (!Array.isArray(value)) throw new Error(`${key}_must_be_string_array`);
2060
+ if (value.length > 100) throw new Error(`${key}_too_large`);
2061
+ const items = value.map((item) => {
2062
+ if (typeof item !== "string") throw new Error(`${key}_must_be_string_array`);
2063
+ return item.trim();
2064
+ });
2065
+ if (items.some((item) => !item)) throw new Error(`${key}_contains_empty_value`);
2066
+ if (new Set(items).size !== items.length) throw new Error(`${key}_contains_duplicates`);
2067
+ return items;
2068
+ }
2069
+ function assertProjectInviteBody(body, creating) {
2070
+ const allowed = creating ? PROJECT_INVITE_BODY_KEYS : new Set(["teammate_name", "project_ids"]);
2071
+ if (Object.keys(body).some((key) => !allowed.has(key))) throw new Error("unexpected_project_invite_fields");
2072
+ if (typeof body.teammate_name !== "string") throw new Error("teammate_name_must_be_string");
2073
+ if (creating && typeof body.reviewed_project_set_digest !== "string") throw new Error("reviewed_project_set_digest_required");
2074
+ if (creating && !/^[a-f0-9]{64}$/u.test(String(body.reviewed_project_set_digest).trim())) throw new Error("reviewed_project_set_digest_invalid");
2075
+ }
2076
+ function projectInviteStatus(config = readCoordinatorSyncConfig()) {
2077
+ const status = coordinatorAdminStatusPayload(config);
2078
+ if (status.readiness !== "ready") throw new Error("team_sharing_not_configured");
2079
+ if (status.groups.length !== 1) throw new Error("team_selection_ambiguous");
2080
+ if (!status.active_group) throw new Error("team_sharing_not_configured");
2081
+ return {
2082
+ groupId: status.active_group,
2083
+ config
2084
+ };
2085
+ }
2086
+ function recipientInviteKind(value) {
2087
+ if (value === "team_member" || value === "add_device") return value;
2088
+ throw new Error("recipient_invite_kind_invalid");
2089
+ }
2090
+ function localOnboardingBinding(store, deviceName, options = {}) {
2091
+ const deviceId = options.materializeActor === false ? ensureStableStoreDeviceIdentity(store) : ensureStableStoreIdentity(store);
2092
+ const devicePublicKey = String(store.db.prepare("SELECT public_key FROM sync_device WHERE device_id = ?").pluck().get(deviceId) ?? "").trim();
2093
+ if (!devicePublicKey) throw new Error("device_public_key_missing");
2094
+ const config = readCodememConfigFile();
2095
+ return {
2096
+ deviceId,
2097
+ devicePublicKey,
2098
+ deviceDisplayName: friendlyDeviceName({
2099
+ explicitName: deviceName ?? String(config.sync_device_name ?? ""),
2100
+ osName: hostname(),
2101
+ fallbackSeed: deviceId
2102
+ })
2103
+ };
2104
+ }
2105
+ function recipientInviteOnboardingPreview(store, body, invitationId) {
2106
+ const kind = recipientInviteKind(body.kind ?? body.invite_kind);
2107
+ const binding = localOnboardingBinding(store, optionalViewerString(body, "device_name"));
2108
+ const teamId = optionalViewerStrictString(body, "policy_team_id");
2109
+ const targetIdentityId = optionalViewerStrictString(body, "target_identity_id");
2110
+ if (kind === "team_member") {
2111
+ if (!teamId || targetIdentityId) throw new Error("recipient_invite_metadata_invalid");
2112
+ return previewRecipientPolicyOnboarding(store.db, {
2113
+ version: 1,
2114
+ journey: "team",
2115
+ invitationId,
2116
+ identityId: store.actorId,
2117
+ ...binding,
2118
+ teamId
2119
+ });
2120
+ }
2121
+ if (!targetIdentityId || teamId) throw new Error("recipient_invite_metadata_invalid");
2122
+ return previewRecipientPolicyOnboarding(store.db, {
2123
+ version: 1,
2124
+ journey: "add_device",
2125
+ invitationId,
2126
+ identityId: targetIdentityId,
2127
+ ...binding
2128
+ });
2129
+ }
2130
+ function recipientInviteReviewedIntent(store, kind, targetId, preview) {
2131
+ if (kind === "team_member") {
2132
+ if (preview.journey !== "team" || !preview.team || preview.team.teamId !== targetId) throw new Error("recipient_invite_intent_mismatch");
2133
+ return {
2134
+ version: 1,
2135
+ journey: "team",
2136
+ team: preview.team,
2137
+ projects: preview.projects,
2138
+ excludedProjects: []
2139
+ };
2140
+ }
2141
+ if (preview.journey !== "add_device" || preview.binding.identityId !== targetId) throw new Error("recipient_invite_intent_mismatch");
2142
+ const targetIdentity = store.db.prepare(`SELECT display_name FROM actors
2143
+ WHERE actor_id = ? AND status = 'active' AND merged_into_actor_id IS NULL`).get(targetId);
2144
+ if (!targetIdentity?.display_name) throw new Error("identity_not_found");
2145
+ return {
2146
+ version: 1,
2147
+ journey: "add_device",
2148
+ targetIdentity: {
2149
+ identityId: targetId,
2150
+ displayName: targetIdentity.display_name
2151
+ },
2152
+ projects: preview.projects,
2153
+ excludedProjects: preview.excludedProjects
2154
+ };
2155
+ }
2156
+ function recipientInviteOnboardingPreviewFromReviewedIntent(store, body, invitationId, reviewedIntent) {
2157
+ const kind = recipientInviteKind(body.kind ?? body.invite_kind);
2158
+ const binding = localOnboardingBinding(store, optionalViewerString(body, "device_name"), { materializeActor: false });
2159
+ const teamId = optionalViewerStrictString(body, "policy_team_id");
2160
+ const targetIdentityId = optionalViewerStrictString(body, "target_identity_id");
2161
+ const assignedIdentityId = optionalViewerStrictString(body, "assigned_identity_id");
2162
+ if (kind === "team_member") {
2163
+ if (!teamId || !assignedIdentityId || targetIdentityId) throw new Error("recipient_invite_metadata_invalid");
2164
+ return previewRecipientPolicyOnboardingFromReviewedIntent(reviewedIntent, {
2165
+ version: 1,
2166
+ journey: "team",
2167
+ invitationId,
2168
+ identityId: assignedIdentityId,
2169
+ ...binding,
2170
+ teamId
2171
+ });
2172
+ }
2173
+ if (!targetIdentityId || teamId || assignedIdentityId) throw new Error("recipient_invite_metadata_invalid");
2174
+ return previewRecipientPolicyOnboardingFromReviewedIntent(reviewedIntent, {
2175
+ version: 1,
2176
+ journey: "add_device",
2177
+ invitationId,
2178
+ identityId: targetIdentityId,
2179
+ ...binding
2180
+ });
2181
+ }
2182
+ var SAFE_RECIPIENT_INVITE_ERRORS = new Set([
2183
+ "invite_already_bound",
2184
+ "invite_expired",
2185
+ "invite_identity_conflict",
2186
+ "invite_invalid",
2187
+ "recipient_invite_intent_mismatch",
2188
+ "recipient_invite_review_unavailable",
2189
+ "reviewed_onboarding_stale"
2190
+ ]);
2191
+ function safeRecipientInviteError(value, fallback = "invite_invalid") {
2192
+ const code = value instanceof Error ? value.message : String(value ?? "").trim();
2193
+ return SAFE_RECIPIENT_INVITE_ERRORS.has(code) ? code : fallback;
2194
+ }
2195
+ function recipientInvitePreviewId(kind, targetId) {
2196
+ return `recipient-invite-preview:${kind}:${targetId}`;
2197
+ }
2198
+ function recipientPolicyCapabilityFromStatus(payload) {
2199
+ if (!isScopedSyncCapability(normalizeSyncCapability(payload.sync_capability))) return "unsupported";
2200
+ return supportsSyncFeature(payload.sync_features, "reassign_scope") ? "supported" : "unsupported";
2201
+ }
2202
+ async function peerSupportsSyncRequirements(store, deviceId, requirements) {
2203
+ const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
2204
+ if (deviceId === localDeviceId) return "supported";
2205
+ const row = store.db.prepare("SELECT addresses_json, pinned_fingerprint, public_key FROM sync_peers WHERE peer_device_id = ?").get(deviceId);
2206
+ const expectedFingerprint = String(row?.pinned_fingerprint ?? "").trim() || (row?.public_key ? fingerprintPublicKey(row.public_key) : "");
2207
+ const addresses = safeJsonList(row?.addresses_json ?? null);
2208
+ for (const address of addresses) try {
2209
+ const url = `${buildBaseUrl(address)}/v1/status`;
2210
+ const [status, payload] = await requestJson("GET", url, {
2211
+ headers: {
2212
+ ...buildAuthHeaders({
2213
+ deviceId: localDeviceId,
2214
+ method: "GET",
2215
+ url,
2216
+ bodyBytes: Buffer.alloc(0),
2217
+ keysDir: syncKeysDir()
2218
+ }),
2219
+ [SYNC_CAPABILITY_HEADER]: LOCAL_SYNC_CAPABILITY,
2220
+ [SYNC_FEATURES_HEADER]: LOCAL_SYNC_FEATURES.join(",")
2221
+ },
2222
+ timeoutS: 5
2223
+ });
2224
+ if (status < 200 || status >= 300) continue;
2225
+ if (String(payload?.device_id ?? "").trim() !== deviceId) continue;
2226
+ if (expectedFingerprint && String(payload?.fingerprint ?? "").trim() !== expectedFingerprint) continue;
2227
+ if (requirements.scoped && requirements.reassignScope && recipientPolicyCapabilityFromStatus(payload ?? {}) !== "supported") return "unsupported";
2228
+ if (requirements.scoped && !isScopedSyncCapability(normalizeSyncCapability(payload?.sync_capability))) return "unsupported";
2229
+ if (requirements.reassignScope && !supportsSyncFeature(payload?.sync_features, "reassign_scope")) return "unsupported";
2230
+ return "supported";
2231
+ } catch {}
2232
+ return "undetermined";
2233
+ }
2234
+ async function peerSupportsReassignScope(store, deviceId) {
2235
+ return peerSupportsSyncRequirements(store, deviceId, {
2236
+ scoped: false,
2237
+ reassignScope: true
2238
+ });
2239
+ }
2240
+ function resolveProjectInviteSelection(store, requestedIds, initiatingDeviceId) {
2241
+ if (requestedIds.length === 0) throw new Error("project_selection_empty");
2242
+ const requested = new Set(requestedIds);
2243
+ const byIdentity = /* @__PURE__ */ new Map();
2244
+ let offset = 0;
2245
+ while (byIdentity.size < requested.size) {
2246
+ const page = listProjectScopeInventory(store.db, {
2247
+ limit: 250,
2248
+ offset
2249
+ });
2250
+ for (const project of page.projects) if (requested.has(project.workspace_identity)) byIdentity.set(project.workspace_identity, project);
2251
+ if (!page.has_more) break;
2252
+ offset += page.limit;
2253
+ }
2254
+ return requestedIds.map((projectId) => {
2255
+ const project = byIdentity.get(projectId);
2256
+ if (!project) throw new Error("project_selection_unknown");
2257
+ if (project.guardrail_warnings.some((warning) => warning.code === "basename_collision_review" && warning.requires_confirmation)) throw new Error("project_selection_ambiguous");
2258
+ 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");
2259
+ return {
2260
+ canonicalIdentity: project.workspace_identity,
2261
+ displayName: project.display_project,
2262
+ identitySource: project.identity_source,
2263
+ existingMemoryCount: countShareableProjectMemories(store.db, {
2264
+ canonicalIdentity: project.workspace_identity,
2265
+ initiatingDeviceId
2266
+ })
2267
+ };
2268
+ });
2269
+ }
2270
+ function resolveSharePerson(store, teammateName) {
2271
+ const matches = store.db.prepare(`SELECT actor_id, display_name, status
2272
+ FROM actors
2273
+ WHERE status IN ('active', 'pending') AND lower(trim(display_name)) = lower(?)
2274
+ ORDER BY actor_id`).all(teammateName);
2275
+ if (matches.length > 1) throw new Error("teammate_match_ambiguous");
2276
+ const match = matches[0];
2277
+ if (match) return match.status === "pending" ? {
2278
+ kind: "pending",
2279
+ personId: match.actor_id,
2280
+ displayName: match.display_name
2281
+ } : {
2282
+ kind: "existing",
2283
+ personId: match.actor_id,
2284
+ displayName: match.display_name
2285
+ };
2286
+ return {
2287
+ kind: "pending",
2288
+ displayName: teammateName
2289
+ };
2290
+ }
2291
+ function projectInvitePlan(store, body, createdAt, inviteExpiresAt) {
2292
+ const teammateName = normalizeTeammateName(String(body.teammate_name));
2293
+ const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
2294
+ store.adoptEnsuredDeviceIdentity(localDeviceId);
2295
+ const projects = resolveProjectInviteSelection(store, projectInviteStringList(body, "project_ids"), localDeviceId);
2296
+ const { groupId, config } = projectInviteStatus();
2297
+ return {
2298
+ plan: planShareOperation({
2299
+ inviterActorId: store.actorId,
2300
+ inviterDeviceIds: [localDeviceId],
2301
+ person: resolveSharePerson(store, teammateName),
2302
+ projects,
2303
+ coordinatorGroupId: groupId,
2304
+ createdAt,
2305
+ inviteExpiresAt
2306
+ }),
2307
+ config
2308
+ };
2309
+ }
2310
+ function projectInvitePreview(plan) {
2311
+ return {
2312
+ operation_id: plan.operationId,
2313
+ teammate: {
2314
+ display_name: plan.teammateName,
2315
+ match: plan.personKind,
2316
+ ...plan.personKind === "existing" ? { person_id: plan.personId } : {}
2317
+ },
2318
+ projects: plan.projects.map((project) => ({
2319
+ project_id: project.canonicalIdentity,
2320
+ display_name: project.displayName,
2321
+ existing_memory_count: project.existingMemoryCount
2322
+ })),
2323
+ existing_memory_count: plan.projects.reduce((total, project) => total + project.existingMemoryCount, 0),
2324
+ future_memories_shared: true,
2325
+ history_policy: plan.historyPolicy,
2326
+ reviewed_project_set_digest: plan.reviewedProjectSetDigest
2327
+ };
2328
+ }
2329
+ async function coordinatorProjectInvitePayload(operationId) {
2330
+ const { groupId, config } = projectInviteStatus();
2331
+ const [status, payload] = await requestJson("GET", `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/admin/project-invites/${encodeURIComponent(operationId)}?group_id=${encodeURIComponent(groupId)}`, {
2332
+ headers: { "X-Codemem-Coordinator-Admin": config.syncCoordinatorAdminSecret },
2333
+ timeoutS: 10
2334
+ }).catch((cause) => {
2335
+ const error = new Error("coordinator_unavailable", { cause });
2336
+ Object.assign(error, { status: 503 });
2337
+ throw error;
2338
+ });
2339
+ if (status < 200 || status >= 300) {
2340
+ const error = new Error(String(payload?.error ?? "operation_read_failed"));
2341
+ Object.assign(error, { status });
2342
+ throw error;
2343
+ }
2344
+ if (!payload || payload.operation_id !== operationId || payload.group_id !== groupId) {
2345
+ const error = /* @__PURE__ */ new Error("operation_scope_mismatch");
2346
+ Object.assign(error, { status: 409 });
2347
+ throw error;
2348
+ }
2349
+ return {
2350
+ groupId,
2351
+ config,
2352
+ payload
2353
+ };
2354
+ }
2355
+ async function reconcileProjectInviteAcceptance(store, operationId) {
2356
+ const owner = store.db.prepare("SELECT inviter_actor_id FROM share_operations WHERE operation_id = ?").pluck().get(operationId);
2357
+ if (!owner) throw new Error("operation_not_found");
2358
+ if (owner !== store.actorId) throw new Error("operation_scope_mismatch");
2359
+ ensureLocalActorRecord(store);
2360
+ const { groupId, payload } = await coordinatorProjectInvitePayload(operationId);
2361
+ if (payload.state === "waiting_for_acceptance") return {
2362
+ accepted: false,
2363
+ payload
2364
+ };
2365
+ if (payload.state !== "accepted") {
2366
+ const error = /* @__PURE__ */ new Error("operation_state_invalid");
2367
+ Object.assign(error, { status: 409 });
2368
+ throw error;
2369
+ }
2370
+ if ([
2371
+ "reviewed_project_set_digest",
2372
+ "recipient_actor_id",
2373
+ "recipient_display_name",
2374
+ "recipient_device_id",
2375
+ "recipient_device_display_name",
2376
+ "recipient_public_key",
2377
+ "recipient_fingerprint",
2378
+ "consumed_at",
2379
+ "trust_state"
2380
+ ].some((key) => typeof payload[key] !== "string" || !String(payload[key]).trim())) {
2381
+ const error = /* @__PURE__ */ new Error("operation_acceptance_invalid");
2382
+ Object.assign(error, { status: 409 });
2383
+ throw error;
2384
+ }
2385
+ const trustState = String(payload.trust_state);
2386
+ const bootstrapGrantId = typeof payload.bootstrap_grant_id === "string" && payload.bootstrap_grant_id.trim() ? payload.bootstrap_grant_id : null;
2387
+ if (trustState !== "pending_inviter_device" && trustState !== "bootstrap_grant_created" || trustState === "bootstrap_grant_created" !== Boolean(bootstrapGrantId)) {
2388
+ const error = /* @__PURE__ */ new Error("operation_trust_state_invalid");
2389
+ Object.assign(error, { status: 409 });
2390
+ throw error;
2391
+ }
2392
+ const projects = parseAcceptedProjectIntent(payload.projects);
2393
+ reconcileShareOperationAcceptance(store.db, {
2394
+ operationId,
2395
+ localInviterActorId: store.actorId,
2396
+ coordinatorGroupId: groupId,
2397
+ reviewedProjectSetDigest: String(payload.reviewed_project_set_digest),
2398
+ recipientActorId: String(payload.recipient_actor_id),
2399
+ recipientDisplayName: String(payload.recipient_display_name),
2400
+ recipientDeviceId: String(payload.recipient_device_id),
2401
+ recipientDeviceDisplayName: String(payload.recipient_device_display_name),
2402
+ recipientPublicKey: String(payload.recipient_public_key),
2403
+ recipientFingerprint: String(payload.recipient_fingerprint),
2404
+ consumedAt: String(payload.consumed_at),
2405
+ trustState,
2406
+ bootstrapGrantId,
2407
+ projects
2408
+ });
2409
+ return {
2410
+ accepted: true,
2411
+ payload
2412
+ };
2413
+ }
2414
+ async function shareOperationReadModels(store, operationId, includeInviteLinks = true) {
2415
+ const rows = store.db.prepare(`SELECT o.operation_id, o.state, o.person_id, o.teammate_name,
2416
+ o.recipient_actor_id, o.recipient_display_name, o.recipient_device_id,
2417
+ o.recipient_device_display_name, o.invite_expires_at, o.acceptance_consumed_at,
2418
+ o.created_at, o.updated_at, a.display_name AS actor_display_name,
2419
+ p.name AS peer_display_name, p.last_sync_at AS device_last_seen_at
2420
+ FROM share_operations o
2421
+ LEFT JOIN actors a ON a.actor_id = o.person_id
2422
+ LEFT JOIN sync_peers p ON p.peer_device_id = o.recipient_device_id
2423
+ WHERE o.inviter_actor_id = ? AND (? IS NULL OR o.operation_id = ?)
2424
+ ORDER BY o.created_at DESC, o.operation_id`).all(store.actorId, operationId ?? null, operationId ?? null);
2425
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2426
+ return Promise.all(rows.map(async (row) => {
2427
+ const projects = store.db.prepare(`SELECT canonical_project_identity AS project_id, display_name, existing_memory_count
2428
+ FROM share_operation_projects WHERE operation_id = ? ORDER BY ordinal`).all(row.operation_id);
2429
+ const steps = store.db.prepare(`SELECT step_key, status, attempt_count, started_at, last_attempt_at,
2430
+ safe_error_code, updated_at FROM share_operation_steps WHERE operation_id = ?`).all(row.operation_id).map((step) => ({
2431
+ attemptCount: step.attempt_count,
2432
+ lastAttemptAt: step.last_attempt_at,
2433
+ safeErrorCode: step.safe_error_code,
2434
+ startedAt: step.started_at,
2435
+ status: step.status,
2436
+ stepKey: step.step_key,
2437
+ updatedAt: step.updated_at
2438
+ }));
2439
+ let inviteLink = null;
2440
+ let projectedState = row.state;
2441
+ if (includeInviteLinks && row.state === "waiting_for_acceptance") try {
2442
+ const remote = await coordinatorProjectInvitePayload(row.operation_id);
2443
+ if (remote.payload.state === "accepted") projectedState = "accepted";
2444
+ inviteLink = typeof remote.payload.invite_link === "string" && remote.payload.invite_link.trim() ? remote.payload.invite_link : null;
2445
+ } catch {}
2446
+ const personId = row.recipient_actor_id || row.person_id;
2447
+ const personName = row.recipient_display_name || row.actor_display_name || row.teammate_name;
2448
+ const deviceName = row.recipient_device_display_name || row.peer_display_name;
2449
+ const lifecycle = projectShareLifecycle({
2450
+ deviceLastSeenAt: row.device_last_seen_at,
2451
+ deviceName,
2452
+ inviteLink,
2453
+ now,
2454
+ personName,
2455
+ state: projectedState,
2456
+ steps
2457
+ });
2458
+ return {
2459
+ operation_id: row.operation_id,
2460
+ person: {
2461
+ actor_id: personId,
2462
+ display_name: personName
2463
+ },
2464
+ devices: row.recipient_device_id && deviceName ? [{
2465
+ device_id: row.recipient_device_id,
2466
+ display_name: deviceName,
2467
+ last_seen_at: row.device_last_seen_at
2468
+ }] : [],
2469
+ projects,
2470
+ project_count: projects.length,
2471
+ lifecycle: {
2472
+ state: lifecycle.lifecycle,
2473
+ label: lifecycle.label,
2474
+ explanation: lifecycle.explanation,
2475
+ primary_action: lifecycle.primaryAction?.kind === "copy_invite" ? {
2476
+ kind: "copy_invite",
2477
+ label: lifecycle.primaryAction.label,
2478
+ invite_link: lifecycle.primaryAction.inviteLink
2479
+ } : !includeInviteLinks && lifecycle.lifecycle === "waiting_for_acceptance" ? {
2480
+ kind: "copy_invite",
2481
+ label: "Copy invite"
2482
+ } : lifecycle.primaryAction
2483
+ },
2484
+ timestamps: {
2485
+ created_at: row.created_at,
2486
+ updated_at: row.updated_at,
2487
+ accepted_at: row.acceptance_consumed_at,
2488
+ invite_expires_at: row.invite_expires_at
2489
+ }
2490
+ };
2491
+ }));
2492
+ }
2493
+ async function executeProjectShareProvisioning(store, operationId) {
2494
+ const { groupId, config } = projectInviteStatus();
2495
+ const coordinatorId = config.syncCoordinatorUrl || null;
2496
+ if (!coordinatorId) throw new Error("coordinator_not_configured");
2497
+ const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
2498
+ return executeShareProvisioning(store.db, {
2499
+ operationId,
2500
+ initiatingDeviceId: localDeviceId
2501
+ }, {
2502
+ createOrGetBoundary: async (project, expectedGroupId) => {
2503
+ if (expectedGroupId !== groupId) throw new Error("operation_scope_mismatch");
2504
+ const readExisting = async () => (await coordinatorListScopesAction({
2505
+ groupId,
2506
+ includeInactive: true,
2507
+ remoteUrl: config.syncCoordinatorUrl || null,
2508
+ adminSecret: config.syncCoordinatorAdminSecret || null
2509
+ })).find((scope) => scope.scope_id === project.boundaryId) ?? null;
2510
+ const existing = await readExisting();
2511
+ if (existing) return existing;
2512
+ try {
2513
+ return await coordinatorCreateScopeAction({
2514
+ groupId,
2515
+ scopeId: project.boundaryId,
2516
+ label: project.displayName,
2517
+ kind: "managed_project",
2518
+ authorityType: "coordinator",
2519
+ coordinatorId,
2520
+ membershipEpoch: 1,
2521
+ status: "active",
2522
+ remoteUrl: config.syncCoordinatorUrl || null,
2523
+ adminSecret: config.syncCoordinatorAdminSecret || null
2524
+ });
2525
+ } catch (error) {
2526
+ const reread = await readExisting();
2527
+ if (reread) return reread;
2528
+ throw error;
2529
+ }
2530
+ },
2531
+ grantMembership: ({ effectId, groupId: expectedGroupId, scopeId, deviceId, role }) => {
2532
+ if (expectedGroupId !== groupId) throw new Error("operation_scope_mismatch");
2533
+ return coordinatorGrantScopeMembershipAction({
2534
+ effectId,
2535
+ groupId,
2536
+ scopeId,
2537
+ deviceId,
2538
+ role,
2539
+ membershipEpoch: 1,
2540
+ coordinatorId,
2541
+ remoteUrl: config.syncCoordinatorUrl || null,
2542
+ adminSecret: config.syncCoordinatorAdminSecret || null
2543
+ });
2544
+ },
2545
+ supportsReassignScope: (deviceId) => peerSupportsReassignScope(store, deviceId),
2546
+ refreshAuthorization: async (expectedGroupId) => {
2547
+ if (expectedGroupId !== groupId) throw new Error("operation_scope_mismatch");
2548
+ if ((await refreshConfiguredScopeMembershipCache(store.db, config, { keysDir: syncKeysDir() })).groups.find((item) => item.groupId === groupId)?.status !== "refreshed") throw new Error("authorization_refresh_failed");
2549
+ },
2550
+ runInitialSync: (recipientDeviceId) => runSyncPass(store.db, recipientDeviceId, {
2551
+ keysDir: syncKeysDir(),
2552
+ scanner: store.scanner,
2553
+ refreshAuthorization: true
2554
+ })
2555
+ });
2556
+ }
2557
+ async function advanceProjectShareOperation(store, operationId) {
2558
+ const operation = store.db.prepare("SELECT state, inviter_actor_id, recipient_device_id FROM share_operations WHERE operation_id = ?").get(operationId);
2559
+ if (!operation) throw new Error("operation_not_found");
2560
+ if (operation.inviter_actor_id !== store.actorId) throw new Error("operation_scope_mismatch");
2561
+ if (operation.state === "cancelled") return {
2562
+ advanced: false,
2563
+ state: "cancelled"
2564
+ };
2565
+ if (!operation.recipient_device_id) {
2566
+ if (!(await reconcileProjectInviteAcceptance(store, operationId)).accepted) return {
2567
+ advanced: false,
2568
+ state: "waiting_for_acceptance"
2569
+ };
2570
+ }
2571
+ if ((await executeProjectShareProvisioning(store, operationId)).superseded) return {
2572
+ advanced: false,
2573
+ state: "cancelled"
2574
+ };
2575
+ return {
2576
+ advanced: true,
2577
+ state: "active"
2578
+ };
2579
+ }
2580
+ var AUTOMATIC_SHARE_OPERATION_STATES = [
2581
+ "waiting_for_acceptance",
2582
+ "accepted",
2583
+ "provisioning",
2584
+ "initial_sync",
2585
+ "waiting_for_device"
2586
+ ];
2587
+ var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 30 * 1e3;
2588
+ var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 300 * 1e3;
2589
+ var TERMINAL_SHARE_MAINTENANCE_ERRORS = new Set([
2590
+ "coordinator_not_configured",
2591
+ "team_sharing_not_configured",
2592
+ "team_selection_ambiguous",
2593
+ "initiating_device_not_reviewed",
2594
+ "inviter_project_access_ambiguous",
2595
+ "managed_boundary_plan_missing",
2596
+ "operation_device_binding_missing",
2597
+ "operation_intent_invalid",
2598
+ "provisioning_membership_plan_invalid",
2599
+ "device_binding_conflict",
2600
+ "intent_conflict",
2601
+ "inviter_identity_conflict"
2602
+ ]);
2603
+ var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = new Set([
2604
+ "operation_scope_mismatch",
2605
+ "operation_state_invalid",
2606
+ "operation_acceptance_invalid",
2607
+ "operation_trust_state_invalid",
2608
+ "operation_intent_invalid",
2609
+ "operation_intent_mismatch",
2610
+ "recipient_fingerprint_mismatch",
2611
+ "recipient_device_identity_conflict",
2612
+ "recipient_actor_conflict",
2613
+ "pending_person_identity_conflict",
2614
+ "device_binding_conflict",
2615
+ "intent_conflict",
2616
+ "inviter_identity_conflict"
2617
+ ]);
2618
+ var TERMINAL_RECONCILIATION_ERRORS = new Set([
2619
+ "coordinator_not_configured",
2620
+ "team_sharing_not_configured",
2621
+ "team_selection_ambiguous",
2622
+ "operation_not_found",
2623
+ ...PROJECT_INVITE_OWNER_CONFLICT_ERRORS
2624
+ ]);
2625
+ function errorStatus(error) {
2626
+ const value = error && typeof error === "object" ? error.status : null;
2627
+ return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
2628
+ }
2629
+ function projectInviteOwnerErrorResponse(error, fallback) {
2630
+ const message = error instanceof Error ? error.message : "";
2631
+ const status = errorStatus(error);
2632
+ if (message === "operation_not_found" && (status == null || status === 404)) return {
2633
+ code: message,
2634
+ status: 404
2635
+ };
2636
+ if (PROJECT_INVITE_OWNER_CONFLICT_ERRORS.has(message) && (status == null || status === 409)) return {
2637
+ code: message,
2638
+ status: 409
2639
+ };
2640
+ if (status != null && status >= 500 && message !== "coordinator_unavailable") return {
2641
+ code: "coordinator_upstream_failed",
2642
+ status: 502
2643
+ };
2644
+ if (message === "coordinator_unavailable" || status === 408 || status === 429 || status === 503) return {
2645
+ code: "coordinator_unavailable",
2646
+ status: 503
2647
+ };
2648
+ return fallback;
2649
+ }
2650
+ function isRetryableReconciliationError(error) {
2651
+ const message = error instanceof Error ? error.message : String(error);
2652
+ if (TERMINAL_RECONCILIATION_ERRORS.has(message)) return false;
2653
+ const status = errorStatus(error);
2654
+ return status == null || status === 408 || status === 429 || status >= 500;
2655
+ }
2656
+ function safeReconciliationErrorCode(error) {
2657
+ const message = error instanceof Error ? error.message : String(error);
2658
+ return TERMINAL_RECONCILIATION_ERRORS.has(message) ? message : "operation_read_failed";
2659
+ }
2660
+ function recordInviteReconciliationFailure(store, operationId, error, now) {
2661
+ const safeErrorCode = safeReconciliationErrorCode(error);
2662
+ const retryable = isRetryableReconciliationError(error);
2663
+ store.db.prepare(`UPDATE share_operation_steps SET
2664
+ status = CASE WHEN ? = 1 THEN 'pending' ELSE 'failed' END,
2665
+ attempt_count = attempt_count + 1, last_attempt_at = ?, safe_error_code = ?, updated_at = ?
2666
+ WHERE operation_id = ? AND step_key = 'invite_consumption'`).run(retryable ? 1 : 0, now, safeErrorCode, now, operationId);
2667
+ const outcome = retryable ? "retry_scheduled" : "needs_attention";
2668
+ store.db.prepare("UPDATE share_operations SET state = ?, updated_at = ? WHERE operation_id = ?").run(outcome === "needs_attention" ? "needs_attention" : "waiting_for_acceptance", now, operationId);
2669
+ return outcome;
2670
+ }
2671
+ async function advancePendingProjectShares(store, options = {}) {
2672
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 3), 10));
2673
+ const placeholders = AUTOMATIC_SHARE_OPERATION_STATES.map(() => "?").join(", ");
2674
+ const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
2675
+ const waitingAcceptanceRetryBefore = (/* @__PURE__ */ new Date(maintenanceNow.getTime() - AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS)).toISOString();
2676
+ const waitingRetryBefore = (/* @__PURE__ */ new Date(maintenanceNow.getTime() - AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS)).toISOString();
2677
+ const rows = store.db.prepare(`SELECT operation_id FROM share_operations
2678
+ WHERE inviter_actor_id = ?
2679
+ AND state IN (${placeholders})
2680
+ AND (state <> 'waiting_for_acceptance' OR updated_at <= ?)
2681
+ AND (state <> 'waiting_for_device' OR updated_at <= ?)
2682
+ ORDER BY CASE WHEN state IN ('accepted', 'provisioning', 'initial_sync') THEN 0 ELSE 1 END,
2683
+ CASE WHEN state IN ('waiting_for_acceptance', 'waiting_for_device')
2684
+ THEN updated_at ELSE created_at END ASC,
2685
+ created_at ASC, operation_id ASC
2686
+ LIMIT ?`).all(store.actorId, ...AUTOMATIC_SHARE_OPERATION_STATES, waitingAcceptanceRetryBefore, waitingRetryBefore, limit);
2687
+ const advanceOperation = options.advanceOperation ?? advanceProjectShareOperation;
2688
+ const result = {
2689
+ processed: 0,
2690
+ advanced: 0,
2691
+ waiting: 0,
2692
+ attention: 0,
2693
+ failed: 0,
2694
+ items: []
2695
+ };
2696
+ for (const row of rows) {
2697
+ result.processed += 1;
2698
+ try {
2699
+ const advanced = await advanceOperation(store, row.operation_id);
2700
+ if (!advanced.advanced) {
2701
+ if (advanced.state === "cancelled") {
2702
+ result.items.push({
2703
+ operationId: row.operation_id,
2704
+ outcome: "superseded"
2705
+ });
2706
+ continue;
2707
+ }
2708
+ store.db.prepare("UPDATE share_operations SET updated_at = ? WHERE operation_id = ?").run(maintenanceNow.toISOString(), row.operation_id);
2709
+ result.waiting += 1;
2710
+ result.items.push({
2711
+ operationId: row.operation_id,
2712
+ outcome: "waiting_for_acceptance"
2713
+ });
2714
+ continue;
2715
+ }
2716
+ result.advanced += 1;
2717
+ result.items.push({
2718
+ operationId: row.operation_id,
2719
+ outcome: "advanced"
2720
+ });
2721
+ } catch (error) {
2722
+ const message = error instanceof Error ? error.message : String(error);
2723
+ const state = store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(row.operation_id);
2724
+ if (state === "cancelled") {
2725
+ result.items.push({
2726
+ operationId: row.operation_id,
2727
+ outcome: "superseded"
2728
+ });
2729
+ continue;
2730
+ }
2731
+ if (message === "waiting_for_device" || state === "waiting_for_device") {
2732
+ result.waiting += 1;
2733
+ result.items.push({
2734
+ operationId: row.operation_id,
2735
+ outcome: "waiting_for_device"
2736
+ });
2737
+ continue;
2738
+ }
2739
+ if (state === "needs_attention") {
2740
+ result.attention += 1;
2741
+ result.items.push({
2742
+ operationId: row.operation_id,
2743
+ outcome: "needs_attention",
2744
+ error: message
2745
+ });
2746
+ continue;
2747
+ }
2748
+ if (state === "waiting_for_acceptance") {
2749
+ const outcome = recordInviteReconciliationFailure(store, row.operation_id, error, maintenanceNow.toISOString());
2750
+ if (outcome === "needs_attention") result.attention += 1;
2751
+ else result.waiting += 1;
2752
+ result.items.push({
2753
+ operationId: row.operation_id,
2754
+ outcome,
2755
+ error: message
2756
+ });
2757
+ continue;
2758
+ }
2759
+ if (TERMINAL_SHARE_MAINTENANCE_ERRORS.has(message)) {
2760
+ store.db.prepare("UPDATE share_operations SET state = 'needs_attention', updated_at = ? WHERE operation_id = ?").run(maintenanceNow.toISOString(), row.operation_id);
2761
+ result.attention += 1;
2762
+ result.items.push({
2763
+ operationId: row.operation_id,
2764
+ outcome: "needs_attention",
2765
+ error: message
2766
+ });
2767
+ continue;
2768
+ }
2769
+ result.failed += 1;
2770
+ result.items.push({
2771
+ operationId: row.operation_id,
2772
+ outcome: "failed",
2773
+ error: message
2774
+ });
2775
+ }
2776
+ }
2777
+ return result;
2778
+ }
2779
+ var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
2780
+ var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
2781
+ var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
2782
+ var RECIPIENT_POLICY_WAITING_ERRORS = new Set([
2783
+ "recipient_policy_capability_undetermined",
2784
+ "recipient_policy_parity_incomplete",
2785
+ "recipient_policy_snapshot_not_fresh"
2786
+ ]);
2787
+ function recipientPolicyCoordinatorBoundary(store, scopeId) {
2788
+ const row = store.db.prepare(`SELECT coordinator_id, group_id, membership_epoch FROM replication_scopes
2789
+ WHERE scope_id = ? AND kind = 'managed_project' AND authority_type = 'coordinator'
2790
+ AND status = 'active'`).get(scopeId);
2791
+ const coordinatorId = String(row?.coordinator_id ?? "").trim();
2792
+ const groupId = String(row?.group_id ?? "").trim();
2793
+ const membershipEpoch = Number(row?.membership_epoch);
2794
+ if (!coordinatorId || !groupId || !Number.isSafeInteger(membershipEpoch) || membershipEpoch < 0) throw new Error("recipient_policy_active_managed_scope_required");
2795
+ return {
2796
+ coordinatorId,
2797
+ groupId,
2798
+ membershipEpoch
2799
+ };
2800
+ }
2801
+ function recipientPolicySnapshotFingerprint(scopeId, scopeMembershipEpoch, memberships) {
2802
+ 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("");
2803
+ return `recipient-policy-coordinator-snapshot-v2:${createHash("sha256").update(`${scopeId}\u0000${scopeMembershipEpoch}\u0000${canonical}`).digest("hex")}`;
2804
+ }
2805
+ function createRecipientPolicyReconcilerEffects(store, options = {}) {
2806
+ const config = options.config ?? readCoordinatorSyncConfig();
2807
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
2808
+ const target = (scopeId) => recipientPolicyCoordinatorBoundary(store, scopeId);
2809
+ const coordinatorOptions = (scopeId) => {
2810
+ const boundary = target(scopeId);
2811
+ const remoteUrl = config.syncCoordinatorUrl?.trim();
2812
+ const adminSecret = config.syncCoordinatorAdminSecret?.trim();
2813
+ if (!remoteUrl || !adminSecret) throw new Error("recipient_policy_effect_failed");
2814
+ if (!config.syncCoordinatorGroups.includes(boundary.groupId)) throw new Error("recipient_policy_effect_failed");
2815
+ try {
2816
+ if (buildBaseUrl(boundary.coordinatorId) !== buildBaseUrl(remoteUrl)) throw new Error("recipient_policy_effect_failed");
2817
+ } catch {
2818
+ throw new Error("recipient_policy_effect_failed");
2819
+ }
2820
+ return {
2821
+ ...boundary,
2822
+ remoteUrl,
2823
+ adminSecret
2824
+ };
2825
+ };
2826
+ return {
2827
+ now,
2828
+ snapshot: async ({ scopeId }) => {
2829
+ const targetOptions = coordinatorOptions(scopeId);
2830
+ const snapshotMemberships = (await coordinatorListScopeMembershipsAction({
2831
+ groupId: targetOptions.groupId,
2832
+ scopeId,
2833
+ includeRevoked: true,
2834
+ remoteUrl: targetOptions.remoteUrl,
2835
+ adminSecret: targetOptions.adminSecret
2836
+ }).catch(() => {
2837
+ throw new Error("recipient_policy_snapshot_not_fresh");
2838
+ })).map((membership) => {
2839
+ if (membership.status !== "active" && membership.status !== "revoked") throw new Error("recipient_policy_snapshot_invalid");
2840
+ const status = membership.status;
2841
+ return {
2842
+ deviceId: membership.device_id,
2843
+ status,
2844
+ membershipEpoch: membership.membership_epoch
2845
+ };
2846
+ });
2847
+ return {
2848
+ authoritative: true,
2849
+ scopeId,
2850
+ scopeMembershipEpoch: targetOptions.membershipEpoch,
2851
+ fingerprint: recipientPolicySnapshotFingerprint(scopeId, targetOptions.membershipEpoch, snapshotMemberships),
2852
+ observedAt: now(),
2853
+ memberships: snapshotMemberships
2854
+ };
2855
+ },
2856
+ probeCapability: (deviceId) => peerSupportsSyncRequirements(store, deviceId, {
2857
+ scoped: true,
2858
+ reassignScope: true
2859
+ }),
2860
+ revoke: async (input) => {
2861
+ const targetOptions = coordinatorOptions(input.scopeId);
2862
+ await coordinatorRevokeScopeMembershipAction({
2863
+ effectId: input.effectId,
2864
+ groupId: targetOptions.groupId,
2865
+ scopeId: input.scopeId,
2866
+ deviceId: input.deviceId,
2867
+ remoteUrl: targetOptions.remoteUrl,
2868
+ adminSecret: targetOptions.adminSecret
2869
+ });
2870
+ return {
2871
+ effectId: input.effectId,
2872
+ scopeId: input.scopeId,
2873
+ deviceId: input.deviceId,
2874
+ status: "revoked"
2875
+ };
2876
+ },
2877
+ grant: async (input) => {
2878
+ const targetOptions = coordinatorOptions(input.scopeId);
2879
+ const membership = await coordinatorGrantScopeMembershipAction({
2880
+ effectId: input.effectId,
2881
+ groupId: targetOptions.groupId,
2882
+ scopeId: input.scopeId,
2883
+ deviceId: input.deviceId,
2884
+ role: input.role,
2885
+ coordinatorId: targetOptions.coordinatorId,
2886
+ remoteUrl: targetOptions.remoteUrl,
2887
+ adminSecret: targetOptions.adminSecret
2888
+ });
2889
+ return {
2890
+ effectId: input.effectId,
2891
+ scopeId: membership.scope_id,
2892
+ deviceId: membership.device_id,
2893
+ status: membership.status === "active" ? "active" : "revoked"
2894
+ };
2895
+ },
2896
+ refresh: async ({ scopeId }) => {
2897
+ const boundary = target(scopeId);
2898
+ if ((await refreshConfiguredScopeMembershipCache(store.db, config, { keysDir: syncKeysDir() })).groups.find((item) => item.groupId === boundary.groupId)?.status !== "refreshed") throw new Error("recipient_policy_effect_failed");
2899
+ }
2900
+ };
2901
+ }
2902
+ async function reconcileRecipientPolicyProjects(store, options = {}) {
2903
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT), RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT));
2904
+ const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
2905
+ const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS));
2906
+ const retryBefore = new Date(maintenanceNow.getTime() - backoffMs).toISOString();
2907
+ const rows = store.db.prepare(`WITH projects AS (
2908
+ SELECT DISTINCT canonical_project_identity FROM project_recipients
2909
+ UNION
2910
+ SELECT canonical_project_identity FROM recipient_policy_authority_states
2911
+ )
2912
+ SELECT projects.canonical_project_identity
2913
+ FROM projects
2914
+ LEFT JOIN recipient_policy_authority_states authority
2915
+ ON authority.canonical_project_identity = projects.canonical_project_identity
2916
+ WHERE authority.safe_error_code IS NULL OR authority.last_attempt_at IS NULL
2917
+ OR authority.last_attempt_at <= ?
2918
+ ORDER BY CASE WHEN authority.last_attempt_at IS NULL THEN 0 ELSE 1 END,
2919
+ authority.last_attempt_at, projects.canonical_project_identity
2920
+ LIMIT ?`).all(retryBefore, limit);
2921
+ const effects = options.effects ?? createRecipientPolicyReconcilerEffects(store);
2922
+ const reconcileProject = options.reconcileProject ?? reconcileRecipientPolicyProject;
2923
+ const result = {
2924
+ processed: 0,
2925
+ active: 0,
2926
+ waiting: 0,
2927
+ attention: 0,
2928
+ failed: 0,
2929
+ items: []
2930
+ };
2931
+ for (const row of rows) {
2932
+ result.processed += 1;
2933
+ try {
2934
+ const outcome = await reconcileProject(store.db, {
2935
+ canonicalProjectIdentity: row.canonical_project_identity,
2936
+ leaseOwner: options.leaseOwner ?? `recipient-policy-maintenance:${store.deviceId || process.pid}`
2937
+ }, effects);
2938
+ if (outcome.status === "active") result.active += 1;
2939
+ else if (outcome.status === "needs_attention") result.attention += 1;
2940
+ else result.waiting += 1;
2941
+ result.items.push({
2942
+ canonicalProjectIdentity: row.canonical_project_identity,
2943
+ status: outcome.status,
2944
+ safeErrorCode: outcome.safeErrorCode
2945
+ });
2946
+ } catch {
2947
+ result.failed += 1;
2948
+ result.items.push({
2949
+ canonicalProjectIdentity: row.canonical_project_identity,
2950
+ status: "failed",
2951
+ safeErrorCode: "recipient_policy_reconciliation_failed"
2952
+ });
2953
+ }
2954
+ }
2955
+ return result;
2956
+ }
2957
+ function recipientPolicyReadState(authority) {
2958
+ if (!authority) return "pending";
2959
+ if (authority.safeErrorCode && RECIPIENT_POLICY_WAITING_ERRORS.has(authority.safeErrorCode)) return "waiting";
2960
+ if (authority.safeErrorCode || authority.authorityState === "rolled_back") return "needs_attention";
2961
+ if (authority.authorityState === "active") return "active";
2962
+ if (authority.authorityState === "eligible") return "verifying";
2963
+ return "pending";
2964
+ }
2965
+ function recipientPolicyReadCopy(state) {
2966
+ if (state === "active") return {
2967
+ label: "Recipient policy active",
2968
+ explanation: "Recipient policy now controls future access for this Project."
2969
+ };
2970
+ if (state === "verifying") return {
2971
+ label: "Verifying recipient policy",
2972
+ explanation: "Current access matches recipient policy and needs one more stable check."
2973
+ };
2974
+ if (state === "waiting") return {
2975
+ label: "Waiting to reconcile",
2976
+ explanation: "Waiting for devices or a fresh coordinator snapshot. No partial grant is applied."
2977
+ };
2978
+ if (state === "needs_attention") return {
2979
+ label: "Reconciliation needs attention",
2980
+ explanation: "Legacy scope enforcement remains in control until this Project is safe to retry."
2981
+ };
2982
+ return {
2983
+ label: "Recipient policy pending",
2984
+ explanation: "Legacy scope enforcement remains in control while reconciliation is pending."
2985
+ };
2986
+ }
2987
+ function listRecipientPolicyReconciliationStatus(store) {
2988
+ return {
2989
+ version: 1,
2990
+ items: store.db.prepare(`SELECT DISTINCT canonical_project_identity FROM project_recipients WHERE status = 'active'
2991
+ UNION SELECT canonical_project_identity FROM recipient_policy_authority_states
2992
+ ORDER BY canonical_project_identity`).all().map((row) => row.canonical_project_identity).map((canonicalProjectIdentity) => {
2993
+ const state = recipientPolicyReadState(getRecipientPolicyAuthorityState(store.db, canonicalProjectIdentity));
2994
+ return {
2995
+ canonicalProjectIdentity,
2996
+ state,
2997
+ ...recipientPolicyReadCopy(state),
2998
+ deliveredCopiesMayRemain: true,
2999
+ revocationWarning: SCOPE_MEMBERSHIP_REVOCATION_LIMITATION
3000
+ };
3001
+ })
3002
+ };
3003
+ }
1979
3004
  function sortActiveMaintenanceJobs(jobs) {
1980
3005
  return [...jobs].sort((a, b) => {
1981
3006
  const aTime = a.started_at ?? a.updated_at;
@@ -2017,6 +3042,11 @@ function redactSemanticIndexDiagnostics(diagnostics, showDiagnostics) {
2017
3042
  function syncKeysDir() {
2018
3043
  return process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
2019
3044
  }
3045
+ /** Strip control/format characters and cap length so attacker-controlled
3046
+ * headers cannot forge log lines when included in diagnostics. */
3047
+ function loggableHeaderValue(value) {
3048
+ return (value ?? "").replace(/[\p{Cc}\p{Cf}]/gu, "").slice(0, 64).trim() || "unknown";
3049
+ }
2020
3050
  function intEnvOr(name, fallback) {
2021
3051
  const value = Number.parseInt(process.env[name] ?? "", 10);
2022
3052
  return Number.isFinite(value) ? value : fallback;
@@ -2221,6 +3251,13 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2221
3251
  reason: "bootstrap_grant_worker_disabled",
2222
3252
  deviceId
2223
3253
  };
3254
+ const workerPublicKey = String(workerEnrollment.public_key ?? "").trim();
3255
+ const workerFingerprint = String(workerEnrollment.fingerprint ?? "").trim();
3256
+ if (!workerPublicKey || fingerprintPublicKey(workerPublicKey) !== workerFingerprint) return {
3257
+ ok: false,
3258
+ reason: "bootstrap_grant_worker_identity_invalid",
3259
+ deviceId
3260
+ };
2224
3261
  if (grant.worker_device_id !== deviceId) return {
2225
3262
  ok: false,
2226
3263
  reason: "bootstrap_grant_worker_mismatch",
@@ -2245,7 +3282,7 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2245
3282
  timestamp,
2246
3283
  nonce,
2247
3284
  signature,
2248
- publicKey: String(workerEnrollment.public_key),
3285
+ publicKey: workerPublicKey,
2249
3286
  deviceId
2250
3287
  });
2251
3288
  } catch {
@@ -2262,6 +3299,12 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
2262
3299
  };
2263
3300
  const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
2264
3301
  if (nonceResult) return nonceResult;
3302
+ updatePeerAddresses(store.db, deviceId, [], {
3303
+ name: String(workerEnrollment.display_name ?? "").trim() || void 0,
3304
+ pinnedFingerprint: workerFingerprint,
3305
+ publicKey: workerPublicKey,
3306
+ replaceTrust: true
3307
+ });
2265
3308
  return {
2266
3309
  ok: true,
2267
3310
  reason: "ok",
@@ -2348,8 +3391,12 @@ function ensureLocalActorRecord(store) {
2348
3391
  is_local: 0,
2349
3392
  updated_at: now
2350
3393
  }).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({
3394
+ const existing = d.select({
3395
+ actor_id: schema.actors.actor_id,
3396
+ is_local: schema.actors.is_local
3397
+ }).from(schema.actors).where(eq(schema.actors.actor_id, store.actorId)).get();
3398
+ if (existing) {
3399
+ if (existing.is_local !== 1) d.update(schema.actors).set({
2353
3400
  is_local: 1,
2354
3401
  updated_at: now
2355
3402
  }).where(eq(schema.actors.actor_id, store.actorId)).run();
@@ -2365,6 +3412,16 @@ function ensureLocalActorRecord(store) {
2365
3412
  updated_at: now
2366
3413
  }).run();
2367
3414
  }
3415
+ function ensureStableStoreIdentity(store) {
3416
+ const deviceId = ensureStableStoreDeviceIdentity(store);
3417
+ ensureLocalActorRecord(store);
3418
+ return deviceId;
3419
+ }
3420
+ function ensureStableStoreDeviceIdentity(store) {
3421
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3422
+ store.adoptEnsuredDeviceIdentity(deviceId);
3423
+ return deviceId;
3424
+ }
2368
3425
  function findPeerDeviceIdForAddress(store, address) {
2369
3426
  const rows = store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all();
2370
3427
  for (const row of rows) if (safeJsonList(row.addresses_json).includes(address)) return String(row.peer_device_id ?? "").trim() || null;
@@ -2615,7 +3672,8 @@ async function acceptDiscoveredPeer(store, input) {
2615
3672
  function filterOpsForPeer(store, peerDeviceId, localDeviceId, ops, options = {}) {
2616
3673
  const [allowed, , skipped] = filterReplicationOpsForSyncWithStatus(store.db, ops, peerDeviceId, {
2617
3674
  localDeviceId,
2618
- applyScopeFilter: options.applyScopeFilter
3675
+ applyScopeFilter: options.applyScopeFilter,
3676
+ supportsReassignScope: options.supportsReassignScope
2619
3677
  });
2620
3678
  return {
2621
3679
  allowed,
@@ -3132,6 +4190,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
3132
4190
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
3133
4191
  let preauthChecked = false;
3134
4192
  let bootstrapAttempted = false;
4193
+ let deviceAuthReason = null;
3135
4194
  if (!auth.ok) {
3136
4195
  if ((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()) {
3137
4196
  preauthChecked = true;
@@ -3142,19 +4201,21 @@ function syncProtocolRoutes(getStore, opts = {}) {
3142
4201
  const unauthLimited = rateLimitedResponse(c, c.req.path, false);
3143
4202
  if (unauthLimited) return unauthLimited;
3144
4203
  }
4204
+ deviceAuthReason = auth.reason;
3145
4205
  auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
3146
4206
  bootstrapAttempted = true;
3147
4207
  }
3148
4208
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
3149
4209
  if (!auth.ok) {
3150
4210
  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;
4211
+ 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"))}`);
4212
+ const wireReason = bootstrapAttempted && grantPresent ? "bootstrap_grant_invalid" : deviceAuthReason ?? auth.reason;
3153
4213
  return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason, true), 401);
3154
4214
  }
3155
4215
  const limited = rateLimitedResponse(c, auth.deviceId, true);
3156
4216
  if (limited) return limited;
3157
4217
  try {
4218
+ 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
4219
  const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3159
4220
  const syncReset = getSyncResetState(store.db);
3160
4221
  const authorizedScopes = isScopedSyncCapability(negotiatedSyncCapability(c)) ? listAuthorizedScopesForPeer(store.db, {
@@ -3166,7 +4227,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
3166
4227
  protocol_version: SYNC_PROTOCOL_VERSION,
3167
4228
  fingerprint,
3168
4229
  sync_reset: addSyncScopeToBoundary(syncReset, null),
3169
- sync_capability: LOCAL_SYNC_CAPABILITY
4230
+ sync_capability: LOCAL_SYNC_CAPABILITY,
4231
+ sync_features: LOCAL_SYNC_FEATURES
3170
4232
  };
3171
4233
  if (authorizedScopes !== null) response.authorized_scopes = authorizedScopes;
3172
4234
  return c.json(response);
@@ -3216,7 +4278,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
3216
4278
  ...addSyncScopeToBoundary(result.reset, scopeRequest.scope_id)
3217
4279
  }, 409);
3218
4280
  const { ops, nextCursor, boundary } = result;
3219
- const filtered = filterOpsForPeer(store, peerDeviceId, localDeviceId, ops);
4281
+ const filtered = filterOpsForPeer(store, peerDeviceId, localDeviceId, ops, { supportsReassignScope: supportsSyncFeature(c.req.header(SYNC_FEATURES_HEADER), "reassign_scope") });
3220
4282
  return c.json({
3221
4283
  reset_required: false,
3222
4284
  sync_capability: LOCAL_SYNC_CAPABILITY,
@@ -3241,6 +4303,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
3241
4303
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
3242
4304
  let preauthChecked = false;
3243
4305
  let bootstrapAttempted = false;
4306
+ let deviceAuthReason = null;
3244
4307
  if (!auth.ok) {
3245
4308
  if ((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()) {
3246
4309
  preauthChecked = true;
@@ -3251,14 +4314,15 @@ function syncProtocolRoutes(getStore, opts = {}) {
3251
4314
  const unauthLimited = rateLimitedResponse(c, c.req.path, false);
3252
4315
  if (unauthLimited) return unauthLimited;
3253
4316
  }
4317
+ deviceAuthReason = auth.reason;
3254
4318
  auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
3255
4319
  bootstrapAttempted = true;
3256
4320
  }
3257
4321
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
3258
4322
  if (!auth.ok) {
3259
4323
  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;
4324
+ 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"))}`);
4325
+ const wireReason = bootstrapAttempted && grantPresent ? "bootstrap_grant_invalid" : deviceAuthReason ?? auth.reason;
3262
4326
  return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason, true), 401);
3263
4327
  }
3264
4328
  const limited = rateLimitedResponse(c, auth.deviceId, true);
@@ -3340,12 +4404,23 @@ function syncProtocolRoutes(getStore, opts = {}) {
3340
4404
  if (!Array.isArray(body.ops)) return c.json({ error: "invalid_ops" }, 400);
3341
4405
  if (body.ops.length > MAX_SYNC_OPS) return c.json({ error: "too_many_ops" }, 413);
3342
4406
  const normalizedOps = extractReplicationOps(body);
4407
+ const peerFeatures = normalizeSyncFeatures(body.sync_features);
4408
+ const reassignOps = normalizedOps.filter((op) => op.op_type === "reassign_scope");
4409
+ if (reassignOps.length > 0 && !peerFeatures.includes("reassign_scope")) return c.json({ error: "reassign_capability_required" }, 409);
4410
+ try {
4411
+ for (const op of reassignOps) parseReassignScopePayload(op);
4412
+ } catch {
4413
+ return c.json({ error: "reassign_payload_invalid" }, 400);
4414
+ }
3343
4415
  for (const op of normalizedOps) if (op.device_id !== peerDeviceId || op.clock_device_id !== peerDeviceId) return c.json({
3344
4416
  error: "invalid_op_device",
3345
4417
  reason: "device_id_mismatch",
3346
4418
  op_id: op.op_id
3347
4419
  }, 400);
3348
- const filtered = filterOpsForPeer(store, peerDeviceId, localDeviceId, normalizedOps, { applyScopeFilter: false });
4420
+ const filtered = filterOpsForPeer(store, peerDeviceId, localDeviceId, normalizedOps, {
4421
+ applyScopeFilter: false,
4422
+ supportsReassignScope: peerFeatures.includes("reassign_scope")
4423
+ });
3349
4424
  const rejected = rejectInboundScopeFailures(store.db, normalizedOps, localDeviceId, {
3350
4425
  enabled: negotiated !== "unsupported",
3351
4426
  peerDeviceId
@@ -3510,6 +4585,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3510
4585
  const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
3511
4586
  const legacyReview = traceSync("legacySharedReview", () => legacySharedReviewSummary(store));
3512
4587
  const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
4588
+ const recipientPolicyReconciliation = traceSync("recipientPolicyReconciliation", () => listRecipientPolicyReconciliationStatus(store));
3513
4589
  let joinRequests = [];
3514
4590
  if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
3515
4591
  joinRequests = await listCoordinatorJoinRequests(config);
@@ -3545,6 +4621,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3545
4621
  legacy_devices: legacyDevices,
3546
4622
  legacy_shared_review: legacyReview,
3547
4623
  sharing_review: sharingReview,
4624
+ recipient_policy_reconciliation: recipientPolicyReconciliation,
3548
4625
  coordinator
3549
4626
  };
3550
4627
  if (includeJoinRequests && showDiag) responsePayload.join_requests = joinRequests;
@@ -3698,7 +4775,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3698
4775
  else peerIds = store.db.prepare("SELECT peer_device_id FROM sync_peers").all().map((row) => String(row.peer_device_id ?? "").trim()).filter(Boolean);
3699
4776
  const items = [];
3700
4777
  for (const peerId of peerIds) {
3701
- const result = await runSyncPass(store.db, peerId, { scanner: store.scanner });
4778
+ const result = await runSyncPass(store.db, peerId, {
4779
+ keysDir: syncKeysDir(),
4780
+ scanner: store.scanner
4781
+ });
3702
4782
  items.push({
3703
4783
  peer_device_id: peerId,
3704
4784
  ...result,
@@ -3747,18 +4827,244 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3747
4827
  local_default_scope_id: LOCAL_DEFAULT_SCOPE_ID
3748
4828
  });
3749
4829
  });
3750
- app.get("/api/sync/projects", (c) => {
4830
+ app.get("/api/sync/recipient-policy/v1/projection", (c) => {
4831
+ const store = getStore();
4832
+ return c.json(listLegacyRecipientPolicyProjections(store.db, {
4833
+ localActorId: store.actorId,
4834
+ localDeviceId: store.deviceId
4835
+ }));
4836
+ });
4837
+ app.get("/api/sync/recipient-policy/v1/review", (c) => {
4838
+ const store = getStore();
4839
+ ensureLocalActorRecord(store);
4840
+ return c.json(listRecipientPolicyReview(store.db, {
4841
+ localActorId: store.actorId,
4842
+ localDeviceId: store.deviceId
4843
+ }));
4844
+ });
4845
+ app.get("/api/sync/recipient-policy/v1/intent", (c) => {
4846
+ const store = getStore();
4847
+ return c.json(listRecipientPolicyIntent(store.db));
4848
+ });
4849
+ app.get("/api/sync/recipient-policy/v1/reconciliation-status", (c) => {
4850
+ return c.json(listRecipientPolicyReconciliationStatus(getStore()));
4851
+ });
4852
+ app.post("/api/sync/recipient-policy/v1/edges/preview", async (c) => {
4853
+ const store = getStore();
4854
+ const body = await parseViewerJsonBody(c);
4855
+ try {
4856
+ return c.json(previewRecipientPolicyEdges(store.db, body));
4857
+ } catch (error) {
4858
+ if (error instanceof RecipientPolicyEdgeRequestError) {
4859
+ if (error.status === "not_found") return c.json({ error: error.errorCode }, 404);
4860
+ return c.json({ error: error.errorCode }, 400);
4861
+ }
4862
+ if (isSqliteBusy(error)) {
4863
+ c.header("Retry-After", "1");
4864
+ return c.json({ error: "edge_preview_busy" }, 503);
4865
+ }
4866
+ return c.json({ error: "edge_preview_failed" }, 500);
4867
+ }
4868
+ });
4869
+ app.post("/api/sync/recipient-policy/v1/edges/commit", async (c) => {
4870
+ const store = getStore();
4871
+ const body = await parseViewerJsonBody(c);
4872
+ try {
4873
+ const result = commitRecipientPolicyEdges(store.db, body);
4874
+ if (result.status === "invalid") return c.json(result, 400);
4875
+ if (result.status === "not_found") return c.json(result, 404);
4876
+ if (result.status === "stale" || result.status === "conflict") return c.json(result, 409);
4877
+ return c.json(result);
4878
+ } catch (error) {
4879
+ if (isSqliteBusy(error)) {
4880
+ c.header("Retry-After", "1");
4881
+ return c.json({ error: "edge_commit_busy" }, 503);
4882
+ }
4883
+ return c.json({
4884
+ version: 1,
4885
+ status: "conflict",
4886
+ reviewedPolicyDigest: "",
4887
+ errorCode: "edge_commit_conflict",
4888
+ outcomes: [],
4889
+ writeCount: 0,
4890
+ idempotent: false
4891
+ }, 409);
4892
+ }
4893
+ });
4894
+ app.post("/api/sync/recipient-policy/v1/invites/preview", async (c) => {
4895
+ const store = getStore();
4896
+ const body = await parseViewerJsonBody(c);
4897
+ if (!body) return c.json({ error: "request_invalid" }, 400);
4898
+ try {
4899
+ const kind = recipientInviteKind(body.kind ?? body.invite_kind);
4900
+ const unavailable = recipientInviteCreationUnavailable(coordinatorAdminStatusPayload(readCoordinatorSyncConfig()), kind);
4901
+ if (unavailable) return c.json(unavailable.body, unavailable.httpStatus);
4902
+ const targetId = kind === "team_member" ? optionalViewerStrictString(body, "policy_team_id") : optionalViewerStrictString(body, "target_identity_id");
4903
+ if (!targetId) throw new Error("recipient_invite_metadata_invalid");
4904
+ if (kind === "add_device" && targetId !== store.actorId) throw new Error("invite_identity_conflict");
4905
+ const preview = recipientInviteOnboardingPreview(store, body, recipientInvitePreviewId(kind, targetId));
4906
+ return c.json({
4907
+ kind,
4908
+ preview
4909
+ });
4910
+ } catch (error) {
4911
+ return c.json({ error: error instanceof Error ? error.message : "recipient_invite_invalid" }, 400);
4912
+ }
4913
+ });
4914
+ app.post("/api/sync/recipient-policy/v1/invites", async (c) => {
4915
+ const store = getStore();
4916
+ const body = await parseViewerJsonBody(c);
4917
+ if (!body) return c.json({ error: "request_invalid" }, 400);
4918
+ try {
4919
+ const kind = recipientInviteKind(body.kind ?? body.invite_kind);
4920
+ const config = readCoordinatorSyncConfig();
4921
+ const status = coordinatorAdminStatusPayload(config);
4922
+ const unavailable = recipientInviteCreationUnavailable(status, kind);
4923
+ if (unavailable) return c.json(unavailable.body, unavailable.httpStatus);
4924
+ const targetId = kind === "team_member" ? optionalViewerStrictString(body, "policy_team_id") : optionalViewerStrictString(body, "target_identity_id");
4925
+ if (!targetId) throw new Error("recipient_invite_metadata_invalid");
4926
+ if (kind === "add_device" && targetId !== store.actorId) throw new Error("invite_identity_conflict");
4927
+ const preview = recipientInviteOnboardingPreview(store, body, recipientInvitePreviewId(kind, targetId));
4928
+ const reviewedDigest = optionalViewerStrictString(body, "reviewed_onboarding_digest");
4929
+ if (!reviewedDigest) throw new Error("reviewed_onboarding_digest_required");
4930
+ if (reviewedDigest !== preview.reviewedOnboardingDigest) return c.json({
4931
+ error: "reviewed_onboarding_stale",
4932
+ preview
4933
+ }, 409);
4934
+ const reviewedIntent = recipientInviteReviewedIntent(store, kind, targetId, preview);
4935
+ const reviewedPreviewDigest = await recipientReviewedIntentDigest(reviewedIntent);
4936
+ const groupId = resolveCoordinatorAdminGroup(optionalViewerString(body, "group_id"), status);
4937
+ const ttlHours = parseInviteTtlHours(body.ttl_hours);
4938
+ if (!groupId) throw new Error("group_id_required");
4939
+ if (ttlHours == null) throw new Error("ttl_hours_invalid");
4940
+ const result = kind === "team_member" || status.has_admin_secret ? await coordinatorCreateInviteAction({
4941
+ groupId,
4942
+ coordinatorUrl: config.syncCoordinatorUrl || null,
4943
+ policy: "auto_admit",
4944
+ ttlHours,
4945
+ createdBy: store.actorId,
4946
+ remoteUrl: config.syncCoordinatorUrl || null,
4947
+ adminSecret: config.syncCoordinatorAdminSecret || null,
4948
+ inviteKind: kind,
4949
+ policyTeamId: kind === "team_member" ? targetId : null,
4950
+ targetIdentityId: kind === "add_device" ? targetId : null,
4951
+ reviewedPreviewDigest,
4952
+ reviewedIntent
4953
+ }) : await coordinatorCreateAddDeviceInviteAction({
4954
+ groupId,
4955
+ coordinatorUrl: config.syncCoordinatorUrl || null,
4956
+ ttlHours,
4957
+ deviceId: preview.binding.deviceId,
4958
+ keysDir: syncKeysDir(),
4959
+ remoteUrl: config.syncCoordinatorUrl || null,
4960
+ reviewedPreviewDigest,
4961
+ reviewedIntent
4962
+ });
4963
+ return c.json({
4964
+ ok: true,
4965
+ kind,
4966
+ preview,
4967
+ invite: result
4968
+ });
4969
+ } catch (error) {
4970
+ return c.json({ error: error instanceof Error ? error.message : "recipient_invite_failed" }, 400);
4971
+ }
4972
+ });
4973
+ app.post("/api/sync/recipient-policy/v1/migrate", async (c) => {
4974
+ const store = getStore();
4975
+ const value = await parseViewerJsonBody(c);
4976
+ if (!value || typeof value !== "object" || Array.isArray(value)) return c.json({ error: "request_invalid" }, 400);
4977
+ const body = value;
4978
+ if (Object.keys(body).some((key) => key !== "dryRun") || Object.hasOwn(body, "dryRun") && typeof body.dryRun !== "boolean") return c.json({ error: "request_invalid" }, 400);
4979
+ return c.json(migrateRecipientPolicyIntent(store.db, {
4980
+ localActorId: store.actorId,
4981
+ localDeviceId: store.deviceId
4982
+ }, { dryRun: body.dryRun === true }));
4983
+ });
4984
+ const parseReviewRequest = (value) => {
4985
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
4986
+ const body = value;
4987
+ const allowedKeys = new Set([
4988
+ "reviewItemId",
4989
+ "sourceFingerprint",
4990
+ "decision",
4991
+ "decisionInput"
4992
+ ]);
4993
+ if (Object.keys(body).some((key) => !allowedKeys.has(key)) || typeof body.reviewItemId !== "string" || typeof body.sourceFingerprint !== "string" || typeof body.decision !== "string") return null;
4994
+ return {
4995
+ reviewItemId: body.reviewItemId,
4996
+ sourceFingerprint: body.sourceFingerprint,
4997
+ decision: body.decision,
4998
+ ...Object.hasOwn(body, "decisionInput") ? { decisionInput: body.decisionInput } : {}
4999
+ };
5000
+ };
5001
+ app.post("/api/sync/recipient-policy/v1/review/resolve", async (c) => {
5002
+ const store = getStore();
5003
+ const request = parseReviewRequest(await parseViewerJsonBody(c));
5004
+ if (!request) return c.json({ error: "request_invalid" }, 400);
5005
+ ensureLocalActorRecord(store);
5006
+ const result = resolveRecipientPolicyReview(store.db, {
5007
+ localActorId: store.actorId,
5008
+ localDeviceId: store.deviceId
5009
+ }, request);
5010
+ if (result.status === "stale" || result.status === "conflict") return c.json(result, 409);
5011
+ if (result.status === "not_found") return c.json(result, 404);
5012
+ if (result.status === "invalid") return c.json(result, 400);
5013
+ return c.json(result);
5014
+ });
5015
+ app.post("/api/sync/recipient-policy/v1/review/resolve-bulk", async (c) => {
5016
+ const store = getStore();
5017
+ const body = await parseViewerJsonBody(c);
5018
+ if (!body || !Array.isArray(body.requests) || body.requests.length === 0 || body.requests.length > 100) return c.json({ error: "request_invalid" }, 400);
5019
+ const requests = body.requests.map(parseReviewRequest);
5020
+ if (requests.some((request) => request == null)) return c.json({ error: "request_invalid" }, 400);
5021
+ ensureLocalActorRecord(store);
5022
+ const result = resolveRecipientPolicyReviewBulk(store.db, {
5023
+ localActorId: store.actorId,
5024
+ localDeviceId: store.deviceId
5025
+ }, requests);
5026
+ return c.json(result, result.results.some((item) => item.status !== "applied") ? 207 : 200);
5027
+ });
5028
+ app.get("/api/sync/projects", async (c) => {
3751
5029
  const store = getStore();
3752
5030
  const limit = Math.max(1, queryInt(c.req.query("limit"), 50));
3753
5031
  const offset = Math.max(0, queryInt(c.req.query("offset"), 0));
3754
- return c.json(listProjectScopeInventory(store.db, {
5032
+ const inventory = listProjectScopeInventory(store.db, {
3755
5033
  identitySource: c.req.query("identity_source"),
3756
5034
  limit,
3757
5035
  offset,
3758
5036
  query: c.req.query("q"),
3759
5037
  scopeId: c.req.query("scope_id"),
3760
5038
  status: c.req.query("status")
3761
- }));
5039
+ });
5040
+ const operations = await shareOperationReadModels(store, void 0, false);
5041
+ const operationById = new Map(operations.map((operation) => [operation.operation_id, operation]));
5042
+ const sharingByProject = /* @__PURE__ */ new Map();
5043
+ const reviewed = store.db.prepare(`SELECT p.operation_id, p.canonical_project_identity
5044
+ FROM share_operation_projects p
5045
+ JOIN share_operations o ON o.operation_id = p.operation_id
5046
+ WHERE o.inviter_actor_id = ? ORDER BY o.created_at, p.ordinal`).all(store.actorId);
5047
+ for (const item of reviewed) {
5048
+ const operation = operationById.get(item.operation_id);
5049
+ if (!operation) continue;
5050
+ const current = sharingByProject.get(item.canonical_project_identity) ?? [];
5051
+ current.push({
5052
+ person: operation.person,
5053
+ lifecycle: {
5054
+ state: operation.lifecycle.state,
5055
+ label: operation.lifecycle.label,
5056
+ explanation: operation.lifecycle.explanation
5057
+ }
5058
+ });
5059
+ sharingByProject.set(item.canonical_project_identity, current);
5060
+ }
5061
+ return c.json({
5062
+ ...inventory,
5063
+ projects: inventory.projects.map((project) => ({
5064
+ ...project,
5065
+ sharing: sharingByProject.get(project.workspace_identity) ?? []
5066
+ }))
5067
+ });
3762
5068
  });
3763
5069
  app.post("/api/sync/projects/reassign-project", async (c) => {
3764
5070
  const store = getStore();
@@ -3808,6 +5114,175 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
3808
5114
  return c.json({ error: message }, message.includes("not found") ? 404 : 400);
3809
5115
  }
3810
5116
  });
5117
+ app.post("/api/sync/project-invites/preview", async (c) => {
5118
+ const store = getStore();
5119
+ const body = await parseViewerJsonBody(c);
5120
+ if (!body) return c.json({ error: "invalid_json" }, 400);
5121
+ try {
5122
+ assertProjectInviteBody(body, false);
5123
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
5124
+ const { plan } = projectInvitePlan(store, body, createdAt, new Date(new Date(createdAt).getTime() + PROJECT_INVITE_TTL_HOURS * 3600 * 1e3).toISOString());
5125
+ return c.json(projectInvitePreview(plan));
5126
+ } catch (error) {
5127
+ return c.json({ error: error instanceof Error ? error.message : "project_invite_invalid" }, 400);
5128
+ }
5129
+ });
5130
+ app.post("/api/sync/project-invites", async (c) => {
5131
+ const store = getStore();
5132
+ const body = await parseViewerJsonBody(c);
5133
+ if (!body) return c.json({ error: "invalid_json" }, 400);
5134
+ try {
5135
+ assertProjectInviteBody(body, true);
5136
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
5137
+ const { plan, config } = projectInvitePlan(store, body, createdAt, new Date(new Date(createdAt).getTime() + PROJECT_INVITE_TTL_HOURS * 3600 * 1e3).toISOString());
5138
+ if (String(body.reviewed_project_set_digest).trim() !== plan.reviewedProjectSetDigest) return c.json({ error: "reviewed_project_set_changed" }, 409);
5139
+ const existingState = (() => {
5140
+ try {
5141
+ return store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(plan.operationId);
5142
+ } catch {
5143
+ return;
5144
+ }
5145
+ })();
5146
+ if (existingState && existingState !== "waiting_for_acceptance") return c.json({ error: "operation_state_invalid" }, 409);
5147
+ const result = await coordinatorCreateInviteAction({
5148
+ groupId: plan.coordinatorGroupId,
5149
+ coordinatorUrl: config.syncCoordinatorUrl || null,
5150
+ policy: "auto_admit",
5151
+ ttlHours: PROJECT_INVITE_TTL_HOURS,
5152
+ createdBy: store.actorId,
5153
+ remoteUrl: config.syncCoordinatorUrl || null,
5154
+ adminSecret: config.syncCoordinatorAdminSecret || null,
5155
+ operationId: plan.operationId,
5156
+ reviewedProjectSetDigest: plan.reviewedProjectSetDigest,
5157
+ inviterActorId: plan.inviterActorId,
5158
+ inviterDisplayName: store.actorDisplayName,
5159
+ inviterDeviceId: plan.inviterDeviceIds[0],
5160
+ pendingPersonId: plan.personId,
5161
+ projectSummaries: plan.projects.map((project) => ({
5162
+ display_name: project.displayName,
5163
+ existing_memory_count: project.existingMemoryCount
5164
+ })),
5165
+ projectIntent: plan.projects.map((project) => ({
5166
+ canonical_identity: project.canonicalIdentity,
5167
+ display_name: project.displayName,
5168
+ existing_memory_count: project.existingMemoryCount
5169
+ }))
5170
+ });
5171
+ if (String(result.operation_id ?? "").trim() !== plan.operationId || String(result.reviewed_project_set_digest ?? "").trim() !== plan.reviewedProjectSetDigest) throw new Error("coordinator_invite_intent_mismatch");
5172
+ const payload = result.payload;
5173
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("coordinator_invite_payload_missing");
5174
+ const token = String(payload.token ?? "").trim();
5175
+ const expiresAt = String(payload.expires_at ?? "").trim();
5176
+ const link = String(result.link ?? "").trim();
5177
+ const encoded = String(result.encoded ?? "").trim();
5178
+ if (!token || !expiresAt || !link || !encoded) throw new Error("coordinator_invite_payload_invalid");
5179
+ const persistedPlan = {
5180
+ ...plan,
5181
+ inviteExpiresAt: new Date(expiresAt).toISOString()
5182
+ };
5183
+ persistShareOperation(store.db, persistedPlan, {
5184
+ inviteId: String(result.invite_id ?? "").trim() || null,
5185
+ tokenDigest: inviteTokenDigest(token)
5186
+ });
5187
+ return c.json({
5188
+ ok: true,
5189
+ ...projectInvitePreview(persistedPlan),
5190
+ invite: {
5191
+ link,
5192
+ encoded,
5193
+ expires_at: persistedPlan.inviteExpiresAt
5194
+ }
5195
+ });
5196
+ } catch (error) {
5197
+ return c.json({ error: error instanceof Error ? error.message : "project_invite_failed" }, 400);
5198
+ }
5199
+ });
5200
+ app.get("/api/sync/project-invites/:operationId", async (c) => {
5201
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5202
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5203
+ try {
5204
+ const { payload } = await coordinatorProjectInvitePayload(operationId);
5205
+ return c.json(payload.state === "accepted" ? {
5206
+ ...payload,
5207
+ state: "pending_setup"
5208
+ } : payload);
5209
+ } catch (error) {
5210
+ const mapped = projectInviteOwnerErrorResponse(error, {
5211
+ code: "operation_read_failed",
5212
+ status: 400
5213
+ });
5214
+ return c.json({ error: mapped.code }, mapped.status);
5215
+ }
5216
+ });
5217
+ app.post("/api/sync/project-invites/:operationId/reconcile", async (c) => {
5218
+ const store = getStore();
5219
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5220
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5221
+ try {
5222
+ const result = await reconcileProjectInviteAcceptance(store, operationId);
5223
+ return c.json({
5224
+ ok: true,
5225
+ operation_id: operationId,
5226
+ reconciled: result.accepted,
5227
+ state: result.accepted ? "pending_setup" : "waiting_for_acceptance"
5228
+ });
5229
+ } catch (error) {
5230
+ const mapped = projectInviteOwnerErrorResponse(error, {
5231
+ code: "operation_reconcile_failed",
5232
+ status: 409
5233
+ });
5234
+ return c.json({ error: mapped.code }, mapped.status);
5235
+ }
5236
+ });
5237
+ app.get("/api/sync/share-operations", async (c) => {
5238
+ const items = await shareOperationReadModels(getStore(), void 0, false);
5239
+ return c.json({ items });
5240
+ });
5241
+ app.get("/api/sync/share-operations/:operationId", async (c) => {
5242
+ const store = getStore();
5243
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5244
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5245
+ const [item] = await shareOperationReadModels(store, operationId);
5246
+ return item ? c.json(item) : c.json({ error: "operation_not_found" }, 404);
5247
+ });
5248
+ const advanceProjectShare = async (c) => {
5249
+ const store = getStore();
5250
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5251
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5252
+ try {
5253
+ const advanced = await advanceProjectShareOperation(store, operationId);
5254
+ if (!advanced.advanced) return c.json({ error: advanced.state === "cancelled" ? "operation_superseded" : "invitation_not_accepted" }, 409);
5255
+ const [operation] = await shareOperationReadModels(store, operationId);
5256
+ return c.json({
5257
+ ok: true,
5258
+ operation
5259
+ });
5260
+ } catch (error) {
5261
+ const code = error instanceof Error ? error.message : "provisioning_failed";
5262
+ if (store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(operationId) === "cancelled") return c.json({ error: "operation_superseded" }, 409);
5263
+ return c.json({ error: code }, code === "operation_not_found" ? 404 : 409);
5264
+ }
5265
+ };
5266
+ app.post("/api/sync/share-operations/:operationId/advance", advanceProjectShare);
5267
+ app.post("/api/sync/project-invites/:operationId/provision", async (c) => {
5268
+ const store = getStore();
5269
+ const operationId = String(c.req.param("operationId") ?? "").trim();
5270
+ if (!/^share_[a-f0-9]{40}$/u.test(operationId)) return c.json({ error: "operation_id_invalid" }, 400);
5271
+ try {
5272
+ const plan = await executeProjectShareProvisioning(store, operationId);
5273
+ if (plan.superseded) return c.json({ error: "operation_superseded" }, 409);
5274
+ return c.json({
5275
+ ok: true,
5276
+ operation_id: operationId,
5277
+ state: "active",
5278
+ plan
5279
+ });
5280
+ } catch (error) {
5281
+ const code = error instanceof Error ? error.message : "provisioning_failed";
5282
+ if (store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(operationId) === "cancelled") return c.json({ error: "operation_superseded" }, 409);
5283
+ return c.json({ error: code }, code === "operation_not_found" ? 404 : 409);
5284
+ }
5285
+ });
3811
5286
  app.post("/api/sync/legacy-shared-review/reassign", async (c) => {
3812
5287
  const store = getStore();
3813
5288
  const body = await parseViewerJsonBody(c);
@@ -4173,6 +5648,73 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4173
5648
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
4174
5649
  }
4175
5650
  });
5651
+ app.post("/api/sync/invites/inspect", async (c) => {
5652
+ const store = getStore();
5653
+ const body = await parseViewerJsonBody(c);
5654
+ if (!body || typeof body.invite !== "string") return c.json({ error: "invite_invalid" }, 400);
5655
+ try {
5656
+ const payload = decodeInvitePayload(extractInvitePayload(body.invite));
5657
+ const recipientInvite = payload.kind === "team_member" || payload.kind === "add_device";
5658
+ if (recipientInvite) ensureStableStoreDeviceIdentity(store);
5659
+ else ensureStableStoreIdentity(store);
5660
+ if (!payload.operation_id && !recipientInvite) return c.json({ kind: "legacy_team_invite" });
5661
+ const [status, inspected] = await requestJson("POST", `${buildBaseUrl(payload.coordinator_url)}/v1/invites/inspect`, {
5662
+ body: { token: payload.token },
5663
+ timeoutS: 10
5664
+ });
5665
+ if (status < 200 || status >= 300) return c.json({ error: recipientInvite ? safeRecipientInviteError(inspected?.error) : String(inspected?.error ?? "invite_invalid") }, status === 410 ? 410 : 400);
5666
+ const config = readCodememConfigFile();
5667
+ const identityId = recipientInvite ? recipientInviteAuthoritativeIdentityId({
5668
+ kind: payload.kind,
5669
+ assigned_identity_id: inspected?.assigned_identity_id,
5670
+ target_identity_id: inspected?.target_identity_id
5671
+ }) : store.actorId;
5672
+ const expectedTarget = payload.kind === "team_member" ? String(inspected?.policy_team_id ?? "").trim() : String(inspected?.target_identity_id ?? "").trim();
5673
+ if (recipientInvite) {
5674
+ 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);
5675
+ if (inspected?.reviewed_intent == null) return c.json({ error: "recipient_invite_review_unavailable" }, 409);
5676
+ let reviewedIntent;
5677
+ try {
5678
+ reviewedIntent = await verifyRecipientReviewedIntent(inspected.reviewed_intent, {
5679
+ target: payload.kind === "team_member" ? {
5680
+ kind: "team_member",
5681
+ policyTeamId: expectedTarget
5682
+ } : {
5683
+ kind: "add_device",
5684
+ targetIdentityId: expectedTarget
5685
+ },
5686
+ digest: String(payload.reviewed_preview_digest ?? "").trim()
5687
+ });
5688
+ } catch {
5689
+ return c.json({ error: "recipient_invite_intent_mismatch" }, 409);
5690
+ }
5691
+ const preview = recipientInviteOnboardingPreviewFromReviewedIntent(store, {
5692
+ kind: payload.kind,
5693
+ policy_team_id: payload.kind === "team_member" ? expectedTarget : null,
5694
+ assigned_identity_id: payload.kind === "team_member" ? identityId : null,
5695
+ target_identity_id: payload.kind === "add_device" ? identityId : null,
5696
+ device_name: optionalViewerString(body, "device_name")
5697
+ }, String(payload.token), reviewedIntent);
5698
+ return c.json({
5699
+ ...inspected,
5700
+ recipient_name: reviewedIntent.journey === "add_device" ? reviewedIntent.targetIdentity.displayName : store.actorDisplayName,
5701
+ device_name: preview.binding.deviceDisplayName,
5702
+ onboarding: preview
5703
+ });
5704
+ }
5705
+ return c.json({
5706
+ ...inspected,
5707
+ recipient_name: store.actorDisplayName,
5708
+ device_name: friendlyDeviceName({
5709
+ explicitName: String(config.sync_device_name ?? ""),
5710
+ osName: hostname(),
5711
+ fallbackSeed: store.deviceId
5712
+ })
5713
+ });
5714
+ } catch (error) {
5715
+ return c.json({ error: safeRecipientInviteError(error) }, 400);
5716
+ }
5717
+ });
4176
5718
  app.post("/api/sync/invites/import", async (c) => {
4177
5719
  const store = getStore();
4178
5720
  let body;
@@ -4205,16 +5747,49 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4205
5747
  } catch (error) {
4206
5748
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
4207
5749
  }
5750
+ let projectInvite = false;
5751
+ let recipientInvite = false;
4208
5752
  try {
5753
+ const decoded = decodeInvitePayload(extractInvitePayload(rawValue));
5754
+ projectInvite = Boolean(decoded.operation_id);
5755
+ recipientInvite = decoded.kind === "team_member" || decoded.kind === "add_device";
5756
+ if (recipientInvite) ensureStableStoreDeviceIdentity(store);
5757
+ else ensureStableStoreIdentity(store);
5758
+ const reviewedOnboardingDigest = typeof body.reviewed_onboarding_digest === "string" ? body.reviewed_onboarding_digest.trim() : "";
5759
+ if (recipientInvite && !reviewedOnboardingDigest) return c.json({ error: "reviewed_onboarding_digest_required" }, 400);
4209
5760
  const result = await coordinatorImportInviteAction({
4210
5761
  inviteValue: rawValue,
4211
- dbPath: store.dbPath
5762
+ dbPath: store.dbPath,
5763
+ recipientActorId: recipientInvite ? null : store.actorId,
5764
+ recipientDisplayName: typeof body.recipient_name === "string" ? body.recipient_name : store.actorDisplayName,
5765
+ deviceDisplayName: typeof body.device_name === "string" ? body.device_name : null,
5766
+ reviewedOnboardingDigest: reviewedOnboardingDigest || null
4212
5767
  });
5768
+ const type = recipientInvite ? "recipient_onboarding" : projectInvite ? "project_share" : "team_join";
5769
+ if (recipientInvite) {
5770
+ const persistedIdentityId = String(result.identity_id ?? "").trim();
5771
+ if (!store.refreshPersistedLocalIdentity(persistedIdentityId)) return c.json({
5772
+ ...result,
5773
+ type,
5774
+ setup_state: "restart_required",
5775
+ restart_required: true,
5776
+ detail: "The invitation was accepted and the reviewed Identity was persisted, but this process could not safely adopt it. Restart codemem before continuing."
5777
+ }, 200);
5778
+ }
5779
+ if ((projectInvite || recipientInvite) && getSyncRuntimeStatus?.()?.phase === "disabled") return c.json({
5780
+ ...result,
5781
+ type,
5782
+ setup_state: "restart_required",
5783
+ restart_required: true,
5784
+ 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."
5785
+ }, 200);
4213
5786
  return c.json({
4214
5787
  ...result,
4215
- type: "team_join"
5788
+ type
4216
5789
  });
4217
5790
  } catch (error) {
5791
+ if (projectInvite) return c.json(projectInviteAcceptanceFailure(error), 400);
5792
+ if (recipientInvite) return c.json({ error: safeRecipientInviteError(error) }, 400);
4218
5793
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
4219
5794
  }
4220
5795
  });
@@ -4659,17 +6234,34 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4659
6234
  error: "membership_epoch must be number",
4660
6235
  status
4661
6236
  }, 400);
6237
+ const role = optionalViewerString(body, "role");
6238
+ const coordinatorId = optionalViewerString(body, "coordinator_id");
6239
+ const manifestIssuerDeviceId = optionalViewerString(body, "manifest_issuer_device_id");
6240
+ const manifestHash = optionalViewerString(body, "manifest_hash");
6241
+ const signedManifestJson = optionalViewerString(body, "signed_manifest_json");
6242
+ const effectId = optionalViewerString(body, "effect_id") ?? coordinatorAdminMembershipEffectId("grant", randomUUID(), {
6243
+ groupId,
6244
+ scopeId,
6245
+ deviceId,
6246
+ role,
6247
+ membershipEpoch,
6248
+ coordinatorId,
6249
+ manifestIssuerDeviceId,
6250
+ manifestHash,
6251
+ signedManifestJson
6252
+ });
4662
6253
  try {
4663
6254
  const membership = await coordinatorGrantScopeMembershipAction({
6255
+ effectId,
4664
6256
  groupId,
4665
6257
  scopeId,
4666
6258
  deviceId,
4667
- role: optionalViewerString(body, "role"),
6259
+ role,
4668
6260
  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"),
6261
+ coordinatorId,
6262
+ manifestIssuerDeviceId,
6263
+ manifestHash,
6264
+ signedManifestJson,
4673
6265
  remoteUrl: config.syncCoordinatorUrl || null,
4674
6266
  adminSecret: config.syncCoordinatorAdminSecret || null
4675
6267
  });
@@ -4718,14 +6310,25 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4718
6310
  error: "membership_epoch must be number",
4719
6311
  status
4720
6312
  }, 400);
6313
+ const manifestHash = optionalViewerString(body, "manifest_hash");
6314
+ const signedManifestJson = optionalViewerString(body, "signed_manifest_json");
6315
+ const effectId = optionalViewerString(body, "effect_id") ?? coordinatorAdminMembershipEffectId("revoke", randomUUID(), {
6316
+ groupId,
6317
+ scopeId,
6318
+ deviceId,
6319
+ membershipEpoch,
6320
+ manifestHash,
6321
+ signedManifestJson
6322
+ });
4721
6323
  try {
4722
6324
  if (!await coordinatorRevokeScopeMembershipAction({
6325
+ effectId,
4723
6326
  groupId,
4724
6327
  scopeId,
4725
6328
  deviceId,
4726
6329
  membershipEpoch,
4727
- manifestHash: optionalViewerString(body, "manifest_hash"),
4728
- signedManifestJson: optionalViewerString(body, "signed_manifest_json"),
6330
+ manifestHash,
6331
+ signedManifestJson,
4729
6332
  remoteUrl: config.syncCoordinatorUrl || null,
4730
6333
  adminSecret: config.syncCoordinatorAdminSecret || null
4731
6334
  })) return c.json({
@@ -4878,6 +6481,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
4878
6481
  store: getStore(),
4879
6482
  config,
4880
6483
  groupId: result.group_id,
6484
+ requestId,
4881
6485
  deviceId: result.device_id
4882
6486
  });
4883
6487
  } catch (grantError) {
@@ -5519,6 +7123,6 @@ function createSyncApp(opts) {
5519
7123
  return app;
5520
7124
  }
5521
7125
  //#endregion
5522
- export { VERSION, closeStore, createApp, createSyncApp, getStore };
7126
+ export { VERSION, advancePendingProjectShares, advanceProjectShareOperation, closeStore, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileRecipientPolicyProjects };
5523
7127
 
5524
7128
  //# sourceMappingURL=index.js.map