@codemem/server 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, posix, resolve, win32 } 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, 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, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildAuthHeaders, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, clonePromptPackAttempt, coerceObserverCommand, commitRecipientPolicyEdges, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, detectInstallKind, diagnoseStalePeerReceivedRows, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, inviteTokenDigest, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, isSummaryLikeMemory, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, migrateRecipientPolicyIntent, negotiateSyncCapability, normalizeAddress, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parseReassignScopePayload, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, rejectInboundScopeFailures, requestJson, resolveDbPath, resolveProject, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, stripPrivateObj, summarizeInboundScopeRejections, supportsSyncFeature, syncScopeResetRequiredPayload, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifyRecipientReviewedIntent, verifySignature, writeCodememConfigFile } from "@codemem/core";
3
+ import { ACCESS_CLEANUP_OP_TYPE, CODEMEM_CONFIG_ENV_OVERRIDES, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, MemoryStore, PROMPT_TRANSPORT_PROTOCOL_RANGE, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildFilterClausesWithContext, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildViewerIdentityTarget, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, detectInstallKind, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, finishLegacyTeamSetupActivation, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inviteTokenDigest, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, migrateRecipientPolicyIntent, negotiateSyncCapability, normalizeAddress, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parsePositiveMemoryId, parseReassignScopePayload, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, requestJson, resolveDbPath, resolveProject, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, summarizeInboundScopeRejections, supportsSyncFeature, syncScopeResetRequiredPayload, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifyDirectPeerSignature, 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";
@@ -28,7 +28,8 @@ function isLoopbackOrigin(origin) {
28
28
  }
29
29
  if (url.protocol !== "http:" && url.protocol !== "https:") return false;
30
30
  if (url.username || url.password) return false;
31
- return LOOPBACK_HOSTS.has(url.hostname);
31
+ const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
32
+ return LOOPBACK_HOSTS.has(hostname);
32
33
  }
33
34
  /** HTTP methods that mutate state and require origin validation. */
34
35
  var UNSAFE_METHODS = new Set([
@@ -61,7 +62,8 @@ function isUnsafeMissingOrigin(c) {
61
62
  *
62
63
  * Ports Python's `reject_cross_origin(missing_origin_policy="reject_if_unsafe")`:
63
64
  *
64
- * - GET/HEAD/OPTIONS: allowed from any origin (viewer is local-only).
65
+ * - GET/HEAD/OPTIONS: allowed from any origin unless the caller marks a
66
+ * side-effecting GET path prefix as unsafe.
65
67
  * - POST/DELETE/PATCH/PUT:
66
68
  * - Origin present + loopback → allowed (browser on localhost)
67
69
  * - Origin present + non-loopback → rejected 403
@@ -72,20 +74,21 @@ function isUnsafeMissingOrigin(c) {
72
74
  * Access-Control-Allow-Origin is set — the browser doesn't need it.
73
75
  * For valid loopback origins, ACAO is echoed back.
74
76
  */
75
- function originGuard() {
77
+ function originGuard(options) {
76
78
  return createMiddleware(async (c, next) => {
77
79
  const origin = c.req.header("Origin");
78
80
  const method = c.req.method;
79
- if (UNSAFE_METHODS.has(method)) {
81
+ const unsafeGet = (method === "GET" || method === "HEAD") && options?.unsafeGetPathPrefixes?.some((prefix) => c.req.path.startsWith(prefix));
82
+ if (UNSAFE_METHODS.has(method) || unsafeGet) {
80
83
  if (origin) {
81
84
  if (!isLoopbackOrigin(origin)) return c.json({ error: "forbidden" }, 403);
82
85
  c.header("Access-Control-Allow-Origin", origin);
83
- c.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
86
+ c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
84
87
  c.header("Access-Control-Allow-Headers", "Content-Type");
85
88
  } else if (isUnsafeMissingOrigin(c)) return c.json({ error: "forbidden" }, 403);
86
89
  } else if (origin && isLoopbackOrigin(origin)) {
87
90
  c.header("Access-Control-Allow-Origin", origin);
88
- c.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
91
+ c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
89
92
  c.header("Access-Control-Allow-Headers", "Content-Type");
90
93
  }
91
94
  await next();
@@ -104,7 +107,7 @@ function preflightHandler() {
104
107
  const origin = c.req.header("Origin");
105
108
  if (origin && isLoopbackOrigin(origin)) {
106
109
  c.header("Access-Control-Allow-Origin", origin);
107
- c.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
110
+ c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
108
111
  c.header("Access-Control-Allow-Headers", "Content-Type");
109
112
  c.header("Access-Control-Max-Age", "86400");
110
113
  return c.body(null, 204);
@@ -573,6 +576,102 @@ function queryBool(value) {
573
576
  }
574
577
  //#endregion
575
578
  //#region src/routes/memory.ts
579
+ var MAX_FEED_QUERY_CODE_UNITS = 256;
580
+ var searchFunctionsRegisteredStores = /* @__PURE__ */ new WeakSet();
581
+ function casefold(value) {
582
+ return String(value ?? "").toLowerCase();
583
+ }
584
+ function normalizeMemorySearchQuery(value) {
585
+ return casefold(value?.trim()).slice(0, MAX_FEED_QUERY_CODE_UNITS) || void 0;
586
+ }
587
+ function ensureMemorySearchFunctions(store) {
588
+ if (searchFunctionsRegisteredStores.has(store)) return;
589
+ store.db.function("codemem_casefold", { deterministic: true }, casefold);
590
+ store.db.function("codemem_project_basename", { deterministic: true }, (value) => projectBasename(String(value ?? "").trim()));
591
+ searchFunctionsRegisteredStores.add(store);
592
+ }
593
+ var IDENTITY_LOOKUP_BATCH_SIZE = 500;
594
+ function identityValue(item, key) {
595
+ const direct = String(item[key] ?? "").trim();
596
+ if (direct) return direct;
597
+ const rawMetadata = item.metadata ?? item.metadata_json;
598
+ const metadata = typeof rawMetadata === "string" ? fromJson(rawMetadata) : rawMetadata;
599
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return "";
600
+ return String(metadata[key] ?? "").trim();
601
+ }
602
+ function uniqueStrings(items, key) {
603
+ return [...new Set(items.map((item) => identityValue(item, key)).filter(Boolean))];
604
+ }
605
+ function batchedLookup(store, values, query) {
606
+ const rows = [];
607
+ for (let offset = 0; offset < values.length; offset += IDENTITY_LOOKUP_BATCH_SIZE) {
608
+ const batch = values.slice(offset, offset + IDENTITY_LOOKUP_BATCH_SIZE);
609
+ const placeholders = batch.map(() => "?").join(", ");
610
+ rows.push(...store.db.prepare(query(placeholders)).all(...batch));
611
+ }
612
+ return rows;
613
+ }
614
+ function humanName(value) {
615
+ if (typeof value !== "string") return void 0;
616
+ try {
617
+ return normalizeHumanPresentationName(value, "display_name");
618
+ } catch {
619
+ return;
620
+ }
621
+ }
622
+ /**
623
+ * Add presentation-only identity labels without changing persisted provenance.
624
+ * Resolved names are display hints, not authority or ownership evidence; legacy
625
+ * metadata receives the same top-level-first fallback as existing read paths.
626
+ */
627
+ function attachResolvedIdentityFieldsUnsafe(store, items) {
628
+ const actorIds = uniqueStrings(items, "actor_id");
629
+ const deviceIds = uniqueStrings(items, "origin_device_id");
630
+ const peers = batchedLookup(store, deviceIds, (placeholders) => `SELECT peer_device_id, name, actor_id FROM sync_peers
631
+ WHERE peer_device_id IN (${placeholders})
632
+ AND TRIM(COALESCE(pinned_fingerprint, '')) <> ''`);
633
+ const peerByDevice = new Map(peers.map((peer) => [peer.peer_device_id, peer]));
634
+ const peerActorIds = peers.map((peer) => String(peer.actor_id ?? "").trim()).filter((actorId) => actorId.length > 0);
635
+ const actorRows = batchedLookup(store, [...new Set([...actorIds, ...peerActorIds])], (placeholders) => `SELECT actor_id, display_name, status, merged_into_actor_id FROM actors
636
+ WHERE actor_id IN (${placeholders})`);
637
+ const devices = batchedLookup(store, deviceIds, (placeholders) => `SELECT device_id, display_name FROM identity_devices
638
+ WHERE device_id IN (${placeholders}) AND status = 'active'`);
639
+ const actorById = new Map(actorRows.map((actor) => [actor.actor_id, actor]));
640
+ let pendingMergeTargets = [...new Set(actorRows.map((actor) => String(actor.merged_into_actor_id ?? "").trim()).filter((actorId) => actorId && !actorById.has(actorId)))];
641
+ while (pendingMergeTargets.length > 0) {
642
+ const targetRows = batchedLookup(store, pendingMergeTargets, (placeholders) => `SELECT actor_id, display_name, status, merged_into_actor_id FROM actors
643
+ WHERE actor_id IN (${placeholders})`);
644
+ for (const actor of targetRows) actorById.set(actor.actor_id, actor);
645
+ pendingMergeTargets = [...new Set(targetRows.map((actor) => String(actor.merged_into_actor_id ?? "").trim()).filter((actorId) => actorId && !actorById.has(actorId)))];
646
+ }
647
+ const resolvedActorName = (actorId) => {
648
+ const seen = /* @__PURE__ */ new Set();
649
+ let currentActorId = actorId;
650
+ while (currentActorId && !seen.has(currentActorId)) {
651
+ seen.add(currentActorId);
652
+ const actor = actorById.get(currentActorId);
653
+ if (!actor) return void 0;
654
+ if (actor.status === "active" && actor.merged_into_actor_id === null) return humanName(actor.display_name);
655
+ currentActorId = String(actor.merged_into_actor_id ?? "").trim();
656
+ }
657
+ };
658
+ const deviceNameById = new Map(devices.map((device) => [device.device_id, humanName(device.display_name)]).filter((entry) => entry[1] !== void 0));
659
+ for (const item of items) {
660
+ const actorId = identityValue(item, "actor_id");
661
+ const deviceId = identityValue(item, "origin_device_id");
662
+ const peer = peerByDevice.get(deviceId);
663
+ const peerActorId = String(peer?.actor_id ?? "").trim();
664
+ const actorName = actorById.has(actorId) ? resolvedActorName(actorId) ?? humanName(item.actor_display_name) : resolvedActorName(peerActorId) ?? humanName(item.actor_display_name);
665
+ const deviceName = deviceNameById.get(deviceId) ?? humanName(peer?.name);
666
+ if (actorName) item.resolved_actor_display_name = actorName;
667
+ if (deviceName) item.resolved_device_display_name = deviceName;
668
+ }
669
+ }
670
+ function attachResolvedIdentityFields(store, items) {
671
+ try {
672
+ attachResolvedIdentityFieldsUnsafe(store, items);
673
+ } catch {}
674
+ }
576
675
  function serializeMemoryRow(ownedBySelf, row) {
577
676
  const metadata = fromJson(row.metadata_json ?? null);
578
677
  const owned_by_self = ownedBySelf(row);
@@ -651,12 +750,49 @@ function summaryLikeSqlPredicate() {
651
750
  OR (
652
751
  json_valid(COALESCE(memory_items.metadata_json, ''))
653
752
  AND (
654
- COALESCE(json_extract(memory_items.metadata_json, '$.is_summary') = 1, 0)
753
+ COALESCE(json_type(memory_items.metadata_json, '$.is_summary') = 'true', 0)
655
754
  OR LOWER(TRIM(COALESCE(json_extract(memory_items.metadata_json, '$.source'), ''))) = 'observer_summary'
656
755
  )
657
756
  )
658
757
  )`;
659
758
  }
759
+ function memorySearchSql(query) {
760
+ const textClause = `INSTR(codemem_casefold(
761
+ COALESCE(memory_items.title, '') || CHAR(10) ||
762
+ COALESCE(memory_items.body_text, '') || CHAR(10) ||
763
+ CASE WHEN ${summaryLikeSqlPredicate()} THEN 'session_summary' ELSE
764
+ COALESCE(NULLIF(LOWER(TRIM(COALESCE(memory_items.kind, ''))), ''), 'change')
765
+ END || CHAR(10) ||
766
+ COALESCE(
767
+ memory_items.project,
768
+ codemem_project_basename(
769
+ (SELECT sessions.project FROM sessions WHERE sessions.id = memory_items.session_id)
770
+ ),
771
+ ''
772
+ ) || CHAR(10) ||
773
+ COALESCE(memory_items.tags_text, '') || CHAR(10) ||
774
+ COALESCE(memory_items.facts, '') || CHAR(10) ||
775
+ COALESCE(memory_items.subtitle, '') || CHAR(10) ||
776
+ COALESCE(memory_items.narrative, '') || CHAR(10) ||
777
+ CASE WHEN json_valid(COALESCE(memory_items.metadata_json, '')) THEN
778
+ COALESCE(json_extract(memory_items.metadata_json, '$.request'), '') || CHAR(10) ||
779
+ COALESCE(json_extract(memory_items.metadata_json, '$.subtitle'), '') || CHAR(10) ||
780
+ COALESCE(json_extract(memory_items.metadata_json, '$.narrative'), '') || CHAR(10) ||
781
+ COALESCE(json_extract(memory_items.metadata_json, '$.facts'), '') || CHAR(10) ||
782
+ COALESCE(json_extract(memory_items.metadata_json, '$.summary'), '')
783
+ ELSE '' END
784
+ ), codemem_casefold(?)) > 0`;
785
+ const parsedMemoryId = parsePositiveMemoryId(query);
786
+ const memoryId = parsedMemoryId != null && String(parsedMemoryId) === query ? parsedMemoryId : null;
787
+ if (memoryId != null) return {
788
+ clause: `(memory_items.id = ? OR ${textClause})`,
789
+ params: [memoryId, query]
790
+ };
791
+ return {
792
+ clause: textClause,
793
+ params: [query]
794
+ };
795
+ }
660
796
  function countVisibleObservationRows(store, filters) {
661
797
  const filterResult = buildViewerMemoryFilters(store, filters);
662
798
  const clauses = [
@@ -709,43 +845,32 @@ function countVisibleArtifactRows(store, project) {
709
845
  return Number(row?.total ?? 0);
710
846
  }
711
847
  function queryMemoryPage(store, options) {
848
+ ensureMemorySearchFunctions(store);
712
849
  const filters = {};
713
850
  if (options.project) filters.project = options.project;
714
851
  if (options.scope) filters.ownership_scope = options.scope;
715
852
  const filterResult = buildViewerMemoryFilters(store, filters);
716
- const where = ["memory_items.active = 1", ...filterResult.clauses].join(" AND ");
853
+ const summaryPredicate = summaryLikeSqlPredicate();
854
+ const clauses = [
855
+ "memory_items.active = 1",
856
+ options.classification === "summary" ? summaryPredicate : `NOT ${summaryPredicate}`,
857
+ ...filterResult.clauses
858
+ ];
859
+ const params = [...filterResult.params];
860
+ if (options.q) {
861
+ const search = memorySearchSql(options.q);
862
+ clauses.push(search.clause);
863
+ params.push(...search.params);
864
+ }
865
+ const where = clauses.join(" AND ");
717
866
  const from = filterResult.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
718
867
  const rows = store.db.prepare(`SELECT memory_items.* FROM ${from}
719
868
  WHERE ${where}
720
- ORDER BY memory_items.created_at DESC
721
- LIMIT ? OFFSET ?`).all(...filterResult.params, options.limit + 1, options.offset);
869
+ ORDER BY memory_items.created_at DESC, memory_items.id DESC
870
+ LIMIT ? OFFSET ?`).all(...params, options.limit + 1, options.offset);
722
871
  const ownedBySelf = store.buildOwnershipPredicate();
723
872
  return rows.map((row) => serializeMemoryRow(ownedBySelf, row));
724
873
  }
725
- function isSummaryLikeMemory$1(item) {
726
- return isSummaryLikeMemory({
727
- kind: item.kind,
728
- metadata: item.metadata_json
729
- });
730
- }
731
- function selectMemoryPage(store, options) {
732
- const pageSize = Math.max(options.limit + options.offset + 10, 50);
733
- let rawOffset = 0;
734
- const matched = [];
735
- while (matched.length < options.offset + options.limit + 1) {
736
- const page = queryMemoryPage(store, {
737
- limit: pageSize,
738
- offset: rawOffset,
739
- project: options.project,
740
- scope: options.scope
741
- });
742
- if (page.length === 0) break;
743
- matched.push(...page.filter(options.matcher));
744
- if (page.length < pageSize) break;
745
- rawOffset += page.length;
746
- }
747
- return matched.slice(options.offset, options.offset + options.limit + 1);
748
- }
749
874
  function memoryRoutes(getStore) {
750
875
  const app = new Hono();
751
876
  app.get("/api/sessions", (c) => {
@@ -794,17 +919,19 @@ function memoryRoutes(getStore) {
794
919
  {
795
920
  const limit = Math.max(1, queryInt(c.req.query("limit"), 20));
796
921
  const offset = Math.max(0, queryInt(c.req.query("offset"), 0));
797
- const items = selectMemoryPage(store, {
922
+ const items = queryMemoryPage(store, {
798
923
  limit,
799
924
  offset,
800
925
  project: c.req.query("project") || void 0,
801
926
  scope: normalizeScope(c.req.query("scope")),
802
- matcher: (item) => !isSummaryLikeMemory$1(item)
927
+ q: normalizeMemorySearchQuery(c.req.query("q")),
928
+ classification: "observation"
803
929
  });
804
930
  const hasMore = items.length > limit;
805
931
  const result = hasMore ? items.slice(0, limit) : items;
806
932
  const asRecords = result;
807
933
  attachSessionFields(store, asRecords);
934
+ attachResolvedIdentityFields(store, asRecords);
808
935
  return c.json({
809
936
  items: asRecords,
810
937
  pagination: {
@@ -821,17 +948,19 @@ function memoryRoutes(getStore) {
821
948
  {
822
949
  const limit = Math.max(1, queryInt(c.req.query("limit"), 50));
823
950
  const offset = Math.max(0, queryInt(c.req.query("offset"), 0));
824
- const items = selectMemoryPage(store, {
951
+ const items = queryMemoryPage(store, {
825
952
  limit,
826
953
  offset,
827
954
  project: c.req.query("project") || void 0,
828
955
  scope: normalizeScope(c.req.query("scope")),
829
- matcher: (item) => isSummaryLikeMemory$1(item)
956
+ q: normalizeMemorySearchQuery(c.req.query("q")),
957
+ classification: "summary"
830
958
  });
831
959
  const hasMore = items.length > limit;
832
960
  const result = hasMore ? items.slice(0, limit) : items;
833
961
  const asRecords = result;
834
962
  attachSessionFields(store, asRecords);
963
+ attachResolvedIdentityFields(store, asRecords);
835
964
  return c.json({
836
965
  items: asRecords,
837
966
  pagination: {
@@ -926,6 +1055,7 @@ function memoryRoutes(getStore) {
926
1055
  if (project) filters.project = project;
927
1056
  const asRecords = store.recent(limit, filters);
928
1057
  attachSessionFields(store, asRecords);
1058
+ attachResolvedIdentityFields(store, asRecords);
929
1059
  return c.json({ items: asRecords });
930
1060
  }
931
1061
  });
@@ -1077,6 +1207,71 @@ function observerStatusRoutes(deps) {
1077
1207
  return app;
1078
1208
  }
1079
1209
  //#endregion
1210
+ //#region src/routes/target-validation.ts
1211
+ var IDENTITY_TARGET_KEYS = new Set(VIEWER_IDENTITY_TARGET_KEYS);
1212
+ var BOOLEAN_IDENTITY_TARGET_KEYS = new Set(["actor_id_present", "embedding_disabled"]);
1213
+ function invalidRequest$1(message) {
1214
+ return {
1215
+ ok: false,
1216
+ status: 400,
1217
+ body: { error: {
1218
+ code: "invalid_request",
1219
+ message
1220
+ } }
1221
+ };
1222
+ }
1223
+ function conflict(code, message) {
1224
+ return {
1225
+ ok: false,
1226
+ status: 409,
1227
+ body: { error: {
1228
+ code,
1229
+ message
1230
+ } }
1231
+ };
1232
+ }
1233
+ function isRecord$1(value) {
1234
+ return value != null && typeof value === "object" && !Array.isArray(value);
1235
+ }
1236
+ function currentIdentityTarget() {
1237
+ return buildViewerIdentityTarget();
1238
+ }
1239
+ function requestedDbMatches(store, value) {
1240
+ if (value == null) return true;
1241
+ if (typeof value !== "string" || !value.trim()) return null;
1242
+ return resolve(resolveDbPath(value.trim())) === resolve(store.dbPath);
1243
+ }
1244
+ function requestedIdentityMatches(value) {
1245
+ if (value == null) return true;
1246
+ if (!isRecord$1(value)) return null;
1247
+ if (Object.keys(value).length !== IDENTITY_TARGET_KEYS.size || Object.keys(value).some((key) => !IDENTITY_TARGET_KEYS.has(key))) return "unsupported";
1248
+ const expected = currentIdentityTarget();
1249
+ for (const key of VIEWER_IDENTITY_TARGET_KEYS) {
1250
+ const requested = value[key];
1251
+ if (BOOLEAN_IDENTITY_TARGET_KEYS.has(key)) {
1252
+ if (typeof requested !== "boolean") return null;
1253
+ } else if (requested !== null && typeof requested !== "string") return null;
1254
+ if (requested !== expected[key]) return false;
1255
+ }
1256
+ return true;
1257
+ }
1258
+ function validateViewerTarget(store, payload, options = {}) {
1259
+ if (options.requirePairedTargets) {
1260
+ const hasDbPath = Object.hasOwn(payload, "db_path");
1261
+ if (hasDbPath !== Object.hasOwn(payload, "identity_target")) return invalidRequest$1("db_path and identity_target must be provided together");
1262
+ if (hasDbPath && (payload.db_path == null || payload.identity_target == null)) return invalidRequest$1("db_path and identity_target must be valid when provided");
1263
+ }
1264
+ const dbMatches = requestedDbMatches(store, payload.db_path);
1265
+ if (dbMatches == null) return invalidRequest$1("db_path must be a non-empty string");
1266
+ if (!dbMatches) return conflict("viewer_db_mismatch", "viewer database does not match request");
1267
+ const identityMatches = requestedIdentityMatches(payload.identity_target);
1268
+ if (identityMatches === "unsupported") return conflict("viewer_contract_unsupported", "viewer request contract is incompatible");
1269
+ if (identityMatches == null) return invalidRequest$1("identity_target is invalid");
1270
+ if (!identityMatches) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
1271
+ if ((options.requireCurrentIdentity || payload.identity_target != null) && !store.hasCurrentIdentity()) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
1272
+ return { ok: true };
1273
+ }
1274
+ //#endregion
1080
1275
  //#region src/routes/pack.ts
1081
1276
  var MAX_LEDGER_PAYLOAD_BYTES = 16 * 1024;
1082
1277
  var MAX_METADATA_FIELD_CHARS = 512;
@@ -1114,19 +1309,6 @@ var LEDGER_KEYS = new Set([
1114
1309
  "db_path",
1115
1310
  "identity_target"
1116
1311
  ]);
1117
- var IDENTITY_TARGET_KEYS = new Set([
1118
- "device_id",
1119
- "actor_id_present",
1120
- "actor_id",
1121
- "config_path",
1122
- "runtime_root",
1123
- "workspace_id",
1124
- "home_dir",
1125
- "pack_compression",
1126
- "embedding_disabled",
1127
- "embedding_model"
1128
- ]);
1129
- var BOOLEAN_IDENTITY_TARGET_KEYS = new Set(["actor_id_present", "embedding_disabled"]);
1130
1312
  var ATTEMPT_KEYS = new Set([
1131
1313
  "attempt_id",
1132
1314
  "started_at",
@@ -1154,12 +1336,6 @@ function invalidRequest(message) {
1154
1336
  message
1155
1337
  } };
1156
1338
  }
1157
- function viewerDbMismatch() {
1158
- return { error: {
1159
- code: "viewer_db_mismatch",
1160
- message: "viewer database does not match request"
1161
- } };
1162
- }
1163
1339
  function viewerIdentityMismatch() {
1164
1340
  return { error: {
1165
1341
  code: "viewer_identity_mismatch",
@@ -1185,47 +1361,6 @@ function normalizeWorkingSetPath(value) {
1185
1361
  if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) return null;
1186
1362
  return normalized;
1187
1363
  }
1188
- function requestedDbMatches(store, value) {
1189
- if (value == null) return true;
1190
- if (typeof value !== "string" || !value.trim()) return null;
1191
- return resolve(resolveDbPath(value)) === resolve(store.dbPath);
1192
- }
1193
- function normalizeIdentityPath(value) {
1194
- if (!value?.trim()) return null;
1195
- return resolve(resolveDbPath(value.trim()));
1196
- }
1197
- function currentIdentityTarget() {
1198
- return {
1199
- device_id: process.env.CODEMEM_DEVICE_ID?.trim() || null,
1200
- actor_id_present: Object.hasOwn(process.env, "CODEMEM_ACTOR_ID"),
1201
- actor_id: process.env.CODEMEM_ACTOR_ID?.trim() || null,
1202
- config_path: normalizeIdentityPath(process.env.CODEMEM_CONFIG),
1203
- runtime_root: normalizeIdentityPath(process.env.CODEMEM_RUNTIME_ROOT),
1204
- workspace_id: process.env.CODEMEM_WORKSPACE_ID?.trim() || null,
1205
- home_dir: normalizeIdentityPath(process.env.HOME || homedir()),
1206
- pack_compression: process.env.CODEMEM_PACK_COMPRESSION?.trim() || null,
1207
- embedding_disabled: [
1208
- "1",
1209
- "true",
1210
- "yes"
1211
- ].includes(String(process.env.CODEMEM_EMBEDDING_DISABLED || "").toLowerCase()),
1212
- embedding_model: process.env.CODEMEM_EMBEDDING_MODEL || "Xenova/bge-small-en-v1.5"
1213
- };
1214
- }
1215
- function requestedIdentityMatches(value) {
1216
- if (value == null) return true;
1217
- if (!isRecord(value)) return null;
1218
- if (Object.keys(value).length !== IDENTITY_TARGET_KEYS.size || Object.keys(value).some((key) => !IDENTITY_TARGET_KEYS.has(key))) return "unsupported";
1219
- const expected = currentIdentityTarget();
1220
- for (const key of IDENTITY_TARGET_KEYS) {
1221
- const requested = value[key];
1222
- if (BOOLEAN_IDENTITY_TARGET_KEYS.has(key)) {
1223
- if (typeof requested !== "boolean") return null;
1224
- } else if (requested !== null && typeof requested !== "string") return null;
1225
- if (requested !== expected[key]) return false;
1226
- }
1227
- return true;
1228
- }
1229
1364
  function validateMetadataFields(payload, allowedKeys) {
1230
1365
  if (Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_LEDGER_PAYLOAD_BYTES) return "ledger metadata exceeds 16384 bytes";
1231
1366
  for (const key of Object.keys(payload)) {
@@ -1347,7 +1482,8 @@ function packTransportRoutes(getStore) {
1347
1482
  if (!store.hasCurrentIdentity()) return c.json(viewerIdentityMismatch(), 409);
1348
1483
  return c.json({
1349
1484
  service: "codemem-viewer",
1350
- protocol_version: 1,
1485
+ protocol_version: PROMPT_TRANSPORT_PROTOCOL_RANGE.protocolVersion,
1486
+ min_supported_protocol_version: PROMPT_TRANSPORT_PROTOCOL_RANGE.minSupportedProtocolVersion,
1351
1487
  db_path: resolve(store.dbPath),
1352
1488
  identity_target: currentIdentityTarget()
1353
1489
  });
@@ -1368,14 +1504,8 @@ function packTransportRoutes(getStore) {
1368
1504
  }
1369
1505
  try {
1370
1506
  const store = getStore();
1371
- const dbMatches = requestedDbMatches(store, parsed.db_path);
1372
- if (dbMatches == null) return c.json(invalidRequest("db_path must be a non-empty string"), 400);
1373
- if (!dbMatches) return c.json(viewerDbMismatch(), 409);
1374
- const identityMatches = requestedIdentityMatches(parsed.identity_target);
1375
- if (identityMatches === "unsupported") return c.json(viewerContractUnsupported(), 409);
1376
- if (identityMatches == null) return c.json(invalidRequest("identity_target is invalid"), 400);
1377
- if (!identityMatches) return c.json(viewerIdentityMismatch(), 409);
1378
- if (!store.hasCurrentIdentity()) return c.json(viewerIdentityMismatch(), 409);
1507
+ const target = validateViewerTarget(store, parsed, { requireCurrentIdentity: true });
1508
+ if (!target.ok) return c.json(target.body, target.status);
1379
1509
  if (!request.attempt) {
1380
1510
  const pack = await store.buildMemoryPackAsync(request.context, request.limit, request.tokenBudget, request.filters, request.renderOptions);
1381
1511
  return c.json(pack);
@@ -1412,14 +1542,8 @@ function packTransportRoutes(getStore) {
1412
1542
  }
1413
1543
  try {
1414
1544
  const store = getStore();
1415
- const dbMatches = requestedDbMatches(store, parsed.db_path);
1416
- if (dbMatches == null) return c.json(invalidRequest("db_path must be a non-empty string"), 400);
1417
- if (!dbMatches) return c.json(viewerDbMismatch(), 409);
1418
- const identityMatches = requestedIdentityMatches(parsed.identity_target);
1419
- if (identityMatches === "unsupported") return c.json(viewerContractUnsupported(), 409);
1420
- if (identityMatches == null) return c.json(invalidRequest("identity_target is invalid"), 400);
1421
- if (!identityMatches) return c.json(viewerIdentityMismatch(), 409);
1422
- if (!store.hasCurrentIdentity()) return c.json(viewerIdentityMismatch(), 409);
1545
+ const target = validateViewerTarget(store, parsed, { requireCurrentIdentity: true });
1546
+ if (!target.ok) return c.json(target.body, target.status);
1423
1547
  const outcome = dispatchLedger(store, parsed);
1424
1548
  if (typeof outcome === "string") return c.json(invalidRequest(outcome), 400);
1425
1549
  if (outcome.ok) return c.json(outcome);
@@ -1439,34 +1563,60 @@ function packTransportRoutes(getStore) {
1439
1563
  * Raw events routes — GET & POST /api/raw-events, GET /api/raw-events/status,
1440
1564
  * POST /api/claude-hooks, POST /api/codex-hooks.
1441
1565
  */
1442
- var MAX_RAW_EVENTS_BODY_BYTES = Number.parseInt(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES ?? "", 10) || 1048576;
1443
- /** Keys to check (in priority order) when resolving a session stream id. */
1444
- var SESSION_ID_KEYS = [
1445
- "session_stream_id",
1446
- "session_id",
1447
- "stream_id",
1448
- "opencode_session_id"
1449
- ];
1450
- /**
1451
- * Resolve a session stream id from a payload object.
1452
- * Checks multiple field aliases. Throws on conflicting values.
1453
- */
1454
- function resolveSessionStreamId(payload) {
1455
- const values = /* @__PURE__ */ new Map();
1456
- for (const key of SESSION_ID_KEYS) {
1457
- const value = payload[key];
1458
- if (value == null) continue;
1459
- if (typeof value !== "string") throw new Error(`${key} must be string`);
1460
- const text = value.trim();
1461
- if (text) values.set(key, text);
1462
- }
1463
- if (values.size === 0) return null;
1464
- if (new Set(values.values()).size > 1) throw new Error("conflicting session id fields");
1465
- for (const key of SESSION_ID_KEYS) {
1466
- const v = values.get(key);
1467
- if (v) return v;
1468
- }
1469
- return null;
1566
+ var DEFAULT_MAX_RAW_EVENTS_BODY_BYTES = 1048576;
1567
+ function configuredMaxRawEventsBodyBytes() {
1568
+ const parsed = Number(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES?.trim() ?? "");
1569
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_RAW_EVENTS_BODY_BYTES;
1570
+ }
1571
+ var MAX_RAW_EVENTS_BODY_BYTES = configuredMaxRawEventsBodyBytes();
1572
+ function emptyTranscriptOutcomeCounts() {
1573
+ return {
1574
+ ok: 0,
1575
+ not_provided: 0,
1576
+ path_rejected: 0,
1577
+ unreadable: 0,
1578
+ no_complete_record: 0,
1579
+ no_assistant_record: 0
1580
+ };
1581
+ }
1582
+ function createTranscriptDiagnostics() {
1583
+ const counts = {
1584
+ claude: emptyTranscriptOutcomeCounts(),
1585
+ codex: emptyTranscriptOutcomeCounts()
1586
+ };
1587
+ return {
1588
+ record(source, outcome) {
1589
+ counts[source][outcome] += 1;
1590
+ },
1591
+ snapshot() {
1592
+ return {
1593
+ scope: "legacy_compatibility_routes",
1594
+ counts: {
1595
+ claude: { ...counts.claude },
1596
+ codex: { ...counts.codex }
1597
+ }
1598
+ };
1599
+ }
1600
+ };
1601
+ }
1602
+ function transcriptSkipResponse(outcome) {
1603
+ if (outcome !== null && outcome !== "ok") return {
1604
+ inserted: 0,
1605
+ skipped: 1,
1606
+ skip_reason: "transcript_unavailable",
1607
+ skip_detail: outcome
1608
+ };
1609
+ return {
1610
+ inserted: 0,
1611
+ skipped: 1,
1612
+ skip_reason: "unsupported_hook"
1613
+ };
1614
+ }
1615
+ function claudeTranscriptRoot() {
1616
+ return join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "projects");
1617
+ }
1618
+ function codexTranscriptRoot() {
1619
+ return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions");
1470
1620
  }
1471
1621
  /**
1472
1622
  * Parse and validate a JSON object body, enforcing size limits.
@@ -1479,16 +1629,43 @@ async function parseJsonObjectBody(c, maxBytes) {
1479
1629
  error: "payload too large",
1480
1630
  max_bytes: maxBytes
1481
1631
  }, 413);
1482
- let raw;
1632
+ let raw = "";
1483
1633
  try {
1484
- raw = await c.req.text();
1634
+ const body = c.req.raw.body;
1635
+ if (body !== null) {
1636
+ const reader = body.getReader();
1637
+ const chunks = [];
1638
+ let totalBytes = 0;
1639
+ try {
1640
+ while (true) {
1641
+ const { done, value } = await reader.read();
1642
+ if (done) break;
1643
+ totalBytes += value.byteLength;
1644
+ if (totalBytes > maxBytes) {
1645
+ try {
1646
+ await reader.cancel();
1647
+ } catch {}
1648
+ return c.json({
1649
+ error: "payload too large",
1650
+ max_bytes: maxBytes
1651
+ }, 413);
1652
+ }
1653
+ chunks.push(value);
1654
+ }
1655
+ } finally {
1656
+ reader.releaseLock();
1657
+ }
1658
+ const bytes = new Uint8Array(totalBytes);
1659
+ let offset = 0;
1660
+ for (const chunk of chunks) {
1661
+ bytes.set(chunk, offset);
1662
+ offset += chunk.byteLength;
1663
+ }
1664
+ raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1665
+ }
1485
1666
  } catch {
1486
1667
  return c.json({ error: "invalid json" }, 400);
1487
1668
  }
1488
- if (Buffer.byteLength(raw, "utf-8") > maxBytes) return c.json({
1489
- error: "payload too large",
1490
- max_bytes: maxBytes
1491
- }, 413);
1492
1669
  let parsed;
1493
1670
  try {
1494
1671
  parsed = raw ? JSON.parse(raw) : {};
@@ -1499,13 +1676,78 @@ async function parseJsonObjectBody(c, maxBytes) {
1499
1676
  return parsed;
1500
1677
  }
1501
1678
  /** Nudge the sweeper safely — never crashes the caller. */
1502
- function nudgeSweeper(sweeper, sessionIds, source = "opencode") {
1503
- try {
1504
- for (const sessionId of sessionIds) sweeper?.nudge(sessionId, source);
1679
+ function nudgeSweeper(sweeper, sessions) {
1680
+ for (const session of sessions) try {
1681
+ sweeper?.nudge(session.streamId, session.source);
1682
+ } catch {}
1683
+ }
1684
+ async function flushBoundarySessions(sweeper, sessions) {
1685
+ for (const session of sessions) try {
1686
+ await sweeper?.flushBoundary(session.streamId, session.source);
1505
1687
  } catch {}
1506
1688
  }
1689
+ function isClaudeBoundaryEnvelope(envelope) {
1690
+ const record = envelope;
1691
+ if (String(record.source ?? "").trim().toLowerCase() !== "claude") return false;
1692
+ const payload = record.payload;
1693
+ if (payload == null || typeof payload !== "object" || Array.isArray(payload)) return false;
1694
+ const adapter = payload._adapter;
1695
+ if (adapter == null || typeof adapter !== "object" || Array.isArray(adapter)) return false;
1696
+ const meta = adapter.meta;
1697
+ if (meta == null || typeof meta !== "object" || Array.isArray(meta)) return false;
1698
+ const hookEventName = meta.hook_event_name;
1699
+ return hookEventName === "SessionEnd" || hookEventName === "Stop";
1700
+ }
1701
+ var SAFE_INGEST_VALIDATION_ERRORS = new Set([
1702
+ "source must be string",
1703
+ "source is required",
1704
+ "source must use 1-64 letters, digits, dots, underscores, or hyphens",
1705
+ "session_stream_id must be string",
1706
+ "session_id must be string",
1707
+ "stream_id must be string",
1708
+ "opencode_session_id must be string",
1709
+ "conflicting session id fields",
1710
+ "invalid session id",
1711
+ "event source conflicts with request source",
1712
+ "session id required",
1713
+ "event_type must be string",
1714
+ "event_type required",
1715
+ "event_type has invalid syntax",
1716
+ "event_id must be string",
1717
+ "event_id required",
1718
+ "event_id has invalid syntax",
1719
+ "event_seq must be int",
1720
+ "ts_wall_ms must be number",
1721
+ "ts_mono_ms must be number",
1722
+ "payload must be an object",
1723
+ "cwd must be string",
1724
+ "project must be string",
1725
+ "started_at must be string",
1726
+ "events must be a list",
1727
+ "event must be an object"
1728
+ ]);
1729
+ function boundedIngestValidationMessage(error) {
1730
+ return SAFE_INGEST_VALIDATION_ERRORS.has(error.message) ? error.message : "invalid raw event request";
1731
+ }
1732
+ function boundedIngestErrorResponse(c, error) {
1733
+ if (error instanceof RawEventIngestValidationError) return c.json({ error: boundedIngestValidationMessage(error) }, 400);
1734
+ const response = { error: "internal server error" };
1735
+ if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = error instanceof Error ? error.message : String(error);
1736
+ return c.json(response, 500);
1737
+ }
1738
+ function untargetedPayload(payload) {
1739
+ const { db_path: _dbPath, identity_target: _identityTarget, ...body } = payload;
1740
+ return body;
1741
+ }
1742
+ async function ingestNormalizedEnvelope(store, sweeper, envelope, flushBoundary = false) {
1743
+ const result = ingestRawEvents(store, envelope);
1744
+ nudgeSweeper(sweeper, result.sessions);
1745
+ if (flushBoundary && isClaudeBoundaryEnvelope(envelope)) await flushBoundarySessions(sweeper, result.sessions);
1746
+ return result;
1747
+ }
1507
1748
  function rawEventsRoutes(getStore, sweeper) {
1508
1749
  const app = new Hono();
1750
+ const transcriptDiagnostics = createTranscriptDiagnostics();
1509
1751
  app.get("/api/raw-events", (c) => {
1510
1752
  const totals = getStore().rawEventBacklogTotals();
1511
1753
  return c.json(totals);
@@ -1540,233 +1782,83 @@ function rawEventsRoutes(getStore, sweeper) {
1540
1782
  available: true,
1541
1783
  mode: "stream_queue",
1542
1784
  max_body_bytes: MAX_RAW_EVENTS_BODY_BYTES
1543
- }
1785
+ },
1786
+ transcript_diagnostics: transcriptDiagnostics.snapshot()
1544
1787
  });
1545
1788
  });
1546
1789
  app.post("/api/raw-events", async (c) => {
1547
1790
  const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
1548
1791
  if (result instanceof Response) return result;
1549
- const payload = result;
1550
- const store = getStore();
1551
1792
  try {
1552
- const cwd = payload.cwd;
1553
- if (cwd != null && typeof cwd !== "string") return c.json({ error: "cwd must be string" }, 400);
1554
- const project = payload.project;
1555
- if (project != null && typeof project !== "string") return c.json({ error: "project must be string" }, 400);
1556
- const startedAt = payload.started_at;
1557
- if (startedAt != null && typeof startedAt !== "string") return c.json({ error: "started_at must be string" }, 400);
1558
- let items = payload.events;
1559
- if (items == null) items = [payload];
1560
- if (!Array.isArray(items)) return c.json({ error: "events must be a list" }, 400);
1561
- let defaultSessionId;
1562
- try {
1563
- defaultSessionId = resolveSessionStreamId(payload) ?? "";
1564
- } catch (err) {
1565
- return c.json({ error: err.message }, 400);
1566
- }
1567
- if (defaultSessionId.startsWith("msg_")) return c.json({ error: "invalid session id" }, 400);
1568
- let inserted = 0;
1569
- const lastSeenBySession = /* @__PURE__ */ new Map();
1570
- const metaBySession = /* @__PURE__ */ new Map();
1571
- const sessionIds = /* @__PURE__ */ new Set();
1572
- const batchBySession = /* @__PURE__ */ new Map();
1573
- for (const item of items) {
1574
- if (item == null || typeof item !== "object" || Array.isArray(item)) return c.json({ error: "event must be an object" }, 400);
1575
- const itemObj = item;
1576
- let itemSessionId;
1577
- try {
1578
- itemSessionId = resolveSessionStreamId(itemObj);
1579
- } catch (err) {
1580
- return c.json({ error: err.message }, 400);
1581
- }
1582
- const opencodeSessionId = String(itemSessionId ?? defaultSessionId ?? "");
1583
- if (!opencodeSessionId) return c.json({ error: "session id required" }, 400);
1584
- if (opencodeSessionId.startsWith("msg_")) return c.json({ error: "invalid session id" }, 400);
1585
- let eventId = String(itemObj.event_id ?? "");
1586
- const eventType = String(itemObj.event_type ?? "");
1587
- if (!eventType) return c.json({ error: "event_type required" }, 400);
1588
- const eventSeqValue = itemObj.event_seq;
1589
- if (eventSeqValue != null) {
1590
- const parsed = Number(eventSeqValue);
1591
- if (!Number.isFinite(parsed) || parsed !== Math.floor(parsed)) return c.json({ error: "event_seq must be int" }, 400);
1592
- }
1593
- let tsWallMs = itemObj.ts_wall_ms;
1594
- if (tsWallMs != null) {
1595
- const parsed = Number(tsWallMs);
1596
- if (!Number.isFinite(parsed)) return c.json({ error: "ts_wall_ms must be int" }, 400);
1597
- tsWallMs = Math.floor(parsed);
1598
- const prev = lastSeenBySession.get(opencodeSessionId) ?? tsWallMs;
1599
- lastSeenBySession.set(opencodeSessionId, Math.max(prev, tsWallMs));
1600
- }
1601
- let tsMonoMs = itemObj.ts_mono_ms;
1602
- if (tsMonoMs != null) {
1603
- const parsed = Number(tsMonoMs);
1604
- if (!Number.isFinite(parsed)) return c.json({ error: "ts_mono_ms must be number" }, 400);
1605
- tsMonoMs = parsed;
1606
- }
1607
- let eventPayload = itemObj.payload;
1608
- if (eventPayload == null) eventPayload = {};
1609
- if (typeof eventPayload !== "object" || Array.isArray(eventPayload)) return c.json({ error: "payload must be an object" }, 400);
1610
- const itemCwd = itemObj.cwd;
1611
- if (itemCwd != null && typeof itemCwd !== "string") return c.json({ error: "cwd must be string" }, 400);
1612
- const itemProject = itemObj.project;
1613
- if (itemProject != null && typeof itemProject !== "string") return c.json({ error: "project must be string" }, 400);
1614
- const itemStartedAt = itemObj.started_at;
1615
- if (itemStartedAt != null && typeof itemStartedAt !== "string") return c.json({ error: "started_at must be string" }, 400);
1616
- eventPayload = stripPrivateObj(eventPayload);
1617
- if (!eventId) {
1618
- const sortedStringify = (obj) => JSON.stringify(obj, (_key, value) => {
1619
- if (value != null && typeof value === "object" && !Array.isArray(value)) {
1620
- const sorted = {};
1621
- for (const k of Object.keys(value).sort()) sorted[k] = value[k];
1622
- return sorted;
1623
- }
1624
- return value;
1625
- });
1626
- if (eventSeqValue != null) {
1627
- const rawId = sortedStringify({
1628
- s: eventSeqValue,
1629
- t: eventType,
1630
- p: eventPayload
1631
- });
1632
- eventId = `legacy-seq-${eventSeqValue}-${createHash("sha256").update(rawId, "utf-8").digest("hex").slice(0, 16)}`;
1633
- } else {
1634
- const rawId = sortedStringify({
1635
- m: tsMonoMs ?? null,
1636
- p: eventPayload,
1637
- t: eventType,
1638
- w: tsWallMs ?? null
1639
- });
1640
- eventId = `legacy-${createHash("sha256").update(rawId, "utf-8").digest("hex").slice(0, 16)}`;
1641
- }
1642
- }
1643
- const eventEntry = {
1644
- event_id: eventId,
1645
- event_type: eventType,
1646
- payload: eventPayload,
1647
- ts_wall_ms: tsWallMs ?? null,
1648
- ts_mono_ms: tsMonoMs ?? null
1649
- };
1650
- sessionIds.add(opencodeSessionId);
1651
- const list = batchBySession.get(opencodeSessionId) ?? [];
1652
- list.push({ ...eventEntry });
1653
- batchBySession.set(opencodeSessionId, list);
1654
- if (itemCwd || itemProject || itemStartedAt) {
1655
- const perSession = metaBySession.get(opencodeSessionId) ?? {};
1656
- if (itemCwd) perSession.cwd = itemCwd;
1657
- if (itemProject) perSession.project = itemProject;
1658
- if (itemStartedAt) perSession.started_at = itemStartedAt;
1659
- metaBySession.set(opencodeSessionId, perSession);
1660
- }
1661
- }
1662
- if (sessionIds.size === 1) {
1663
- const singleSessionId = sessionIds.values().next().value;
1664
- const batch = batchBySession.get(singleSessionId) ?? [];
1665
- inserted = store.recordRawEventsBatch(singleSessionId, batch).inserted;
1666
- } else for (const [sid, sidEvents] of batchBySession) {
1667
- const result = store.recordRawEventsBatch(sid, sidEvents);
1668
- inserted += result.inserted;
1669
- }
1670
- for (const metaSessionId of sessionIds) {
1671
- const sessionMeta = metaBySession.get(metaSessionId) ?? {};
1672
- const applyRequestMeta = sessionIds.size === 1 || metaSessionId === defaultSessionId;
1673
- store.updateRawEventSessionMeta({
1674
- opencodeSessionId: metaSessionId,
1675
- cwd: sessionMeta.cwd ?? (applyRequestMeta ? cwd : void 0) ?? null,
1676
- project: sessionMeta.project ?? (applyRequestMeta ? project : void 0) ?? null,
1677
- startedAt: sessionMeta.started_at ?? (applyRequestMeta ? startedAt : void 0) ?? null,
1678
- lastSeenTsWallMs: lastSeenBySession.get(metaSessionId) ?? null
1679
- });
1680
- }
1681
- nudgeSweeper(sweeper, sessionIds);
1793
+ const store = getStore();
1794
+ const target = validateViewerTarget(store, result, { requirePairedTargets: true });
1795
+ if (!target.ok) return c.json(target.body, target.status);
1796
+ const ingestResult = await ingestNormalizedEnvelope(store, sweeper, untargetedPayload(result), c.req.header("x-codemem-boundary-flush") === "1");
1682
1797
  return c.json({
1683
- inserted,
1684
- received: items.length
1798
+ inserted: ingestResult.inserted,
1799
+ skipped: ingestResult.skipped,
1800
+ received: ingestResult.received
1685
1801
  });
1686
1802
  } catch (err) {
1687
- const response = { error: "internal server error" };
1688
- if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = err.message;
1689
- return c.json(response, 500);
1803
+ return boundedIngestErrorResponse(c, err);
1690
1804
  }
1691
1805
  });
1692
1806
  app.post("/api/claude-hooks", async (c) => {
1693
1807
  const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
1694
1808
  if (result instanceof Response) return result;
1695
- const envelope = buildRawEventEnvelopeFromHook(result);
1696
- if (envelope === null) return c.json({
1697
- inserted: 0,
1698
- skipped: 1
1699
- });
1700
- const store = getStore();
1809
+ const payload = result;
1701
1810
  try {
1702
- const opencodeSessionId = envelope.opencode_session_id;
1703
- const source = envelope.source;
1704
- const strippedPayload = stripPrivateObj(envelope.payload);
1705
- const inserted = store.recordRawEvent({
1706
- opencodeSessionId,
1707
- source,
1708
- eventId: envelope.event_id,
1709
- eventType: "claude.hook",
1710
- payload: strippedPayload,
1711
- tsWallMs: envelope.ts_wall_ms
1712
- });
1713
- store.updateRawEventSessionMeta({
1714
- opencodeSessionId,
1715
- source,
1716
- cwd: envelope.cwd,
1717
- project: envelope.project,
1718
- startedAt: envelope.started_at,
1719
- lastSeenTsWallMs: envelope.ts_wall_ms
1811
+ const store = getStore();
1812
+ const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
1813
+ if (!target.ok) return c.json(target.body, target.status);
1814
+ let transcriptOutcome = null;
1815
+ const envelope = buildRawEventEnvelopeFromHook(untargetedPayload(payload), {
1816
+ transcriptPolicy: {
1817
+ trust: "restricted",
1818
+ approvedRoots: [claudeTranscriptRoot()]
1819
+ },
1820
+ onTranscriptOutcome: (outcome) => {
1821
+ transcriptOutcome = outcome;
1822
+ transcriptDiagnostics.record("claude", outcome);
1823
+ }
1720
1824
  });
1721
- nudgeSweeper(sweeper, [opencodeSessionId], source);
1825
+ if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
1826
+ const ingestResult = await ingestNormalizedEnvelope(store, sweeper, envelope, c.req.header("x-codemem-boundary-flush") === "1");
1722
1827
  return c.json({
1723
- inserted: inserted ? 1 : 0,
1724
- skipped: 0
1828
+ inserted: ingestResult.inserted,
1829
+ skipped: ingestResult.skipped
1725
1830
  });
1726
1831
  } catch (err) {
1727
- const response = { error: "internal server error" };
1728
- if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = err.message;
1729
- return c.json(response, 500);
1832
+ return boundedIngestErrorResponse(c, err);
1730
1833
  }
1731
1834
  });
1732
1835
  app.post("/api/codex-hooks", async (c) => {
1733
1836
  const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
1734
1837
  if (result instanceof Response) return result;
1735
- const envelope = buildRawEventEnvelopeFromCodexHook(result);
1736
- if (envelope === null) return c.json({
1737
- inserted: 0,
1738
- skipped: 1
1739
- });
1740
- const store = getStore();
1838
+ const payload = result;
1741
1839
  try {
1742
- const opencodeSessionId = envelope.opencode_session_id;
1743
- const source = envelope.source;
1744
- const strippedPayload = stripPrivateObj(envelope.payload);
1745
- const inserted = store.recordRawEvent({
1746
- opencodeSessionId,
1747
- source,
1748
- eventId: envelope.event_id,
1749
- eventType: envelope.event_type,
1750
- payload: strippedPayload,
1751
- tsWallMs: envelope.ts_wall_ms
1752
- });
1753
- store.updateRawEventSessionMeta({
1754
- opencodeSessionId,
1755
- source,
1756
- cwd: envelope.cwd,
1757
- project: envelope.project,
1758
- startedAt: envelope.started_at,
1759
- lastSeenTsWallMs: envelope.ts_wall_ms
1840
+ const store = getStore();
1841
+ const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
1842
+ if (!target.ok) return c.json(target.body, target.status);
1843
+ let transcriptOutcome = null;
1844
+ const envelope = buildRawEventEnvelopeFromCodexHook(untargetedPayload(payload), {
1845
+ transcriptPolicy: {
1846
+ trust: "restricted",
1847
+ approvedRoots: [codexTranscriptRoot()]
1848
+ },
1849
+ onTranscriptOutcome: (outcome) => {
1850
+ transcriptOutcome = outcome;
1851
+ transcriptDiagnostics.record("codex", outcome);
1852
+ }
1760
1853
  });
1761
- nudgeSweeper(sweeper, [opencodeSessionId], source);
1854
+ if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
1855
+ const ingestResult = await ingestNormalizedEnvelope(store, sweeper, envelope);
1762
1856
  return c.json({
1763
- inserted: inserted ? 1 : 0,
1764
- skipped: 0
1857
+ inserted: ingestResult.inserted,
1858
+ skipped: ingestResult.skipped
1765
1859
  });
1766
1860
  } catch (err) {
1767
- const response = { error: "internal server error" };
1768
- if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = err.message;
1769
- return c.json(response, 500);
1861
+ return boundedIngestErrorResponse(c, err);
1770
1862
  }
1771
1863
  });
1772
1864
  return app;
@@ -2426,7 +2518,13 @@ function projectInviteAcceptanceFailure(error) {
2426
2518
  inviter_identity_invalid: "The owner's device identity could not be verified. Ask the owner to review the share and create a new invitation.",
2427
2519
  project_invite_self_acceptance_forbidden: "The owner cannot accept this recipient invitation on the owner's device.",
2428
2520
  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.",
2429
- project_invite_trust_state_invalid: "The owner's trust setup could not be verified. Ask the owner to review the share."
2521
+ project_invite_trust_state_invalid: "The owner's trust setup could not be verified. Ask the owner to review the share.",
2522
+ recipient_display_name_invalid: "Enter a human-readable Identity name instead of an internal identifier.",
2523
+ recipient_display_name_required: "Enter an Identity display name.",
2524
+ recipient_display_name_too_long: "Use an Identity display name with 120 characters or fewer.",
2525
+ device_display_name_invalid: "Enter a human-readable device name instead of an internal identifier.",
2526
+ device_display_name_required: "Enter a device display name.",
2527
+ device_display_name_too_long: "Use a device display name with 120 characters or fewer."
2430
2528
  };
2431
2529
  return {
2432
2530
  error: Object.hasOwn(detailByCode, code) ? code : "project_invite_acceptance_failed",
@@ -2434,11 +2532,19 @@ function projectInviteAcceptanceFailure(error) {
2434
2532
  };
2435
2533
  }
2436
2534
  var PROJECT_INVITE_TTL_HOURS = 168;
2535
+ var PROJECT_INVITE_OWNER_FALLBACK = "Project owner";
2437
2536
  var PROJECT_INVITE_BODY_KEYS = new Set([
2438
2537
  "teammate_name",
2439
2538
  "project_ids",
2440
2539
  "reviewed_project_set_digest"
2441
2540
  ]);
2541
+ function projectInviteInviterDisplayName(value) {
2542
+ try {
2543
+ return normalizeHumanPresentationName(value, "inviter_display_name");
2544
+ } catch {
2545
+ return PROJECT_INVITE_OWNER_FALLBACK;
2546
+ }
2547
+ }
2442
2548
  function projectInviteStringList(body, key) {
2443
2549
  const value = body[key];
2444
2550
  if (!Array.isArray(value)) throw new Error(`${key}_must_be_string_array`);
@@ -2565,10 +2671,16 @@ function recipientInviteOnboardingPreviewFromReviewedIntent(store, body, invitat
2565
2671
  });
2566
2672
  }
2567
2673
  var SAFE_RECIPIENT_INVITE_ERRORS = new Set([
2674
+ "device_display_name_invalid",
2675
+ "device_display_name_required",
2676
+ "device_display_name_too_long",
2568
2677
  "invite_already_bound",
2569
2678
  "invite_expired",
2570
2679
  "invite_identity_conflict",
2571
2680
  "invite_invalid",
2681
+ "recipient_display_name_invalid",
2682
+ "recipient_display_name_required",
2683
+ "recipient_display_name_too_long",
2572
2684
  "recipient_invite_intent_mismatch",
2573
2685
  "recipient_invite_review_unavailable",
2574
2686
  "reviewed_onboarding_stale"
@@ -2594,8 +2706,9 @@ async function peerSupportsSyncRequirements(store, deviceId, requirements) {
2594
2706
  const url = `${buildBaseUrl(address)}/v1/status`;
2595
2707
  const [status, payload] = await requestJson("GET", url, {
2596
2708
  headers: {
2597
- ...buildAuthHeaders({
2709
+ ...buildDirectPeerAuthHeaders({
2598
2710
  deviceId: localDeviceId,
2711
+ recipientId: deviceId,
2599
2712
  dbPath: store.dbPath,
2600
2713
  method: "GET",
2601
2714
  url,
@@ -3014,13 +3127,13 @@ var TERMINAL_RECONCILIATION_ERRORS = new Set([
3014
3127
  "operation_not_found",
3015
3128
  ...PROJECT_INVITE_OWNER_CONFLICT_ERRORS
3016
3129
  ]);
3017
- function errorStatus(error) {
3130
+ function errorStatus$1(error) {
3018
3131
  const value = error && typeof error === "object" ? error.status : null;
3019
3132
  return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
3020
3133
  }
3021
3134
  function projectInviteOwnerErrorResponse(error, fallback) {
3022
3135
  const message = error instanceof Error ? error.message : "";
3023
- const status = errorStatus(error);
3136
+ const status = errorStatus$1(error);
3024
3137
  if (message === "operation_not_found" && (status == null || status === 404)) return {
3025
3138
  code: message,
3026
3139
  status: 404
@@ -3042,7 +3155,7 @@ function projectInviteOwnerErrorResponse(error, fallback) {
3042
3155
  function isRetryableReconciliationError(error) {
3043
3156
  const message = error instanceof Error ? error.message : String(error);
3044
3157
  if (TERMINAL_RECONCILIATION_ERRORS.has(message)) return false;
3045
- const status = errorStatus(error);
3158
+ const status = errorStatus$1(error);
3046
3159
  return status == null || status === 408 || status === 429 || status >= 500;
3047
3160
  }
3048
3161
  function safeReconciliationErrorCode(error) {
@@ -3672,14 +3785,15 @@ function pathWithQuery(url) {
3672
3785
  const parsed = new URL(url);
3673
3786
  return parsed.search ? `${parsed.pathname}${parsed.search}` : parsed.pathname;
3674
3787
  }
3675
- function unauthorizedPayload(reason, exposeReason = false) {
3676
- if (exposeReason && process.env.CODEMEM_SYNC_AUTH_DIAGNOSTICS === "1") return {
3677
- error: "unauthorized",
3678
- reason
3679
- };
3788
+ function unauthorizedPayload() {
3680
3789
  return { error: "unauthorized" };
3681
3790
  }
3682
3791
  var SYNC_AUTH_STORE_BUSY_REASON = "sync_auth_store_busy";
3792
+ var DIRECT_PEER_SIGNATURE_VERSION_NUMBER = 3;
3793
+ function logDirectPeerAuthDiagnostic(...reasons) {
3794
+ const reason = reasons.find((candidate) => candidate === "recipient_mismatch" || candidate === "signature_downgrade");
3795
+ if (reason) console.warn(`[sync] peer auth failed: reason=${reason}`);
3796
+ }
3683
3797
  function isSqliteBusy(err) {
3684
3798
  if (!err || typeof err !== "object") return false;
3685
3799
  const code = "code" in err ? String(err.code ?? "") : "";
@@ -3716,6 +3830,76 @@ function syncAuthStoreBusyResponse(c) {
3716
3830
  c.header("Retry-After", "1");
3717
3831
  return c.json({ error: SYNC_AUTH_STORE_BUSY_REASON }, 503);
3718
3832
  }
3833
+ function verifyDirectPeerRequestSignature(request, body, options) {
3834
+ const timestamp = request.header("X-Opencode-Timestamp") ?? "";
3835
+ const nonce = request.header("X-Opencode-Nonce") ?? "";
3836
+ const legacySignature = request.header("X-Opencode-Signature") ?? "";
3837
+ const recipientHeader = request.header("X-Codemem-Recipient");
3838
+ const directSignatureHeader = request.header("X-Codemem-Signature");
3839
+ const hasDirectMaterial = recipientHeader !== void 0 || directSignatureHeader !== void 0;
3840
+ const direct = verifyDirectPeerSignature({
3841
+ method: request.method,
3842
+ pathWithQuery: pathWithQuery(request.url),
3843
+ bodyBytes: body,
3844
+ timestamp,
3845
+ nonce,
3846
+ recipientId: recipientHeader,
3847
+ expectedRecipientId: options.expectedRecipientId,
3848
+ signature: directSignatureHeader,
3849
+ publicKey: options.publicKey
3850
+ });
3851
+ if (hasDirectMaterial && direct.status === "absent") return {
3852
+ ok: false,
3853
+ reason: "invalid_direct_signature"
3854
+ };
3855
+ if (direct.status === "invalid") return {
3856
+ ok: false,
3857
+ reason: direct.reason === "recipient_mismatch" ? direct.reason : "invalid_direct_signature"
3858
+ };
3859
+ if (!verifySignature({
3860
+ method: request.method,
3861
+ pathWithQuery: pathWithQuery(request.url),
3862
+ bodyBytes: body,
3863
+ timestamp,
3864
+ nonce,
3865
+ signature: legacySignature,
3866
+ publicKey: options.publicKey,
3867
+ deviceId: options.deviceId
3868
+ })) return {
3869
+ ok: false,
3870
+ reason: "invalid_signature"
3871
+ };
3872
+ if (direct.status === "absent" && (options.highestObservedVersion ?? 0) >= DIRECT_PEER_SIGNATURE_VERSION_NUMBER) return {
3873
+ ok: false,
3874
+ reason: "signature_downgrade"
3875
+ };
3876
+ return {
3877
+ ok: true,
3878
+ version: direct.status === "valid" ? 3 : 2
3879
+ };
3880
+ }
3881
+ function recordDirectPeerSignatureVersion(store, deviceId, version) {
3882
+ try {
3883
+ if (!recordHighestObservedDirectSignatureVersion(store.db, deviceId, version)) {
3884
+ if (store.db.prepare(`SELECT sync_peer_signature_state.highest_observed_direct_signature_version
3885
+ FROM sync_peers
3886
+ LEFT JOIN sync_peer_signature_state USING (peer_device_id)
3887
+ WHERE sync_peers.peer_device_id = ? LIMIT 1`).pluck().get(deviceId) === void 0) return {
3888
+ ok: false,
3889
+ reason: "unknown_peer",
3890
+ deviceId
3891
+ };
3892
+ }
3893
+ return null;
3894
+ } catch (err) {
3895
+ if (isSqliteBusy(err)) return {
3896
+ ok: false,
3897
+ reason: SYNC_AUTH_STORE_BUSY_REASON,
3898
+ deviceId
3899
+ };
3900
+ throw err;
3901
+ }
3902
+ }
3719
3903
  function authorizeSyncRequest(store, request, body) {
3720
3904
  const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
3721
3905
  const signature = request.header("X-Opencode-Signature") ?? "";
@@ -3726,7 +3910,11 @@ function authorizeSyncRequest(store, request, body) {
3726
3910
  reason: "missing_headers",
3727
3911
  deviceId
3728
3912
  };
3729
- const peerRow = store.db.prepare("SELECT pinned_fingerprint, public_key FROM sync_peers WHERE peer_device_id = ? LIMIT 1").get(deviceId);
3913
+ const peerRow = store.db.prepare(`SELECT sync_peers.pinned_fingerprint, sync_peers.public_key,
3914
+ sync_peer_signature_state.highest_observed_direct_signature_version
3915
+ FROM sync_peers
3916
+ LEFT JOIN sync_peer_signature_state USING (peer_device_id)
3917
+ WHERE sync_peers.peer_device_id = ? LIMIT 1`).get(deviceId);
3730
3918
  if (!peerRow) return {
3731
3919
  ok: false,
3732
3920
  reason: "unknown_peer",
@@ -3744,17 +3932,14 @@ function authorizeSyncRequest(store, request, body) {
3744
3932
  reason: "fingerprint_mismatch",
3745
3933
  deviceId
3746
3934
  };
3747
- let valid = false;
3935
+ let verification;
3748
3936
  try {
3749
- valid = verifySignature({
3750
- method: request.method,
3751
- pathWithQuery: pathWithQuery(request.url),
3752
- bodyBytes: body,
3753
- timestamp,
3754
- nonce,
3755
- signature,
3937
+ const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
3938
+ verification = verifyDirectPeerRequestSignature(request, body, {
3939
+ deviceId,
3756
3940
  publicKey,
3757
- deviceId
3941
+ expectedRecipientId: localDeviceId,
3942
+ highestObservedVersion: peerRow.highest_observed_direct_signature_version
3758
3943
  });
3759
3944
  } catch {
3760
3945
  return {
@@ -3763,13 +3948,17 @@ function authorizeSyncRequest(store, request, body) {
3763
3948
  deviceId
3764
3949
  };
3765
3950
  }
3766
- if (!valid) return {
3951
+ if (!verification.ok) return {
3767
3952
  ok: false,
3768
- reason: "invalid_signature",
3953
+ reason: verification.reason,
3769
3954
  deviceId
3770
3955
  };
3771
3956
  const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
3772
3957
  if (nonceResult) return nonceResult;
3958
+ if (verification.version === DIRECT_PEER_SIGNATURE_VERSION_NUMBER) {
3959
+ const versionResult = recordDirectPeerSignatureVersion(store, deviceId, verification.version);
3960
+ if (versionResult) return versionResult;
3961
+ }
3773
3962
  return {
3774
3963
  ok: true,
3775
3964
  reason: "ok",
@@ -3888,17 +4077,15 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
3888
4077
  reason: "bootstrap_grant_expired",
3889
4078
  deviceId
3890
4079
  };
3891
- let valid = false;
4080
+ const existingPeer = store.db.prepare(`SELECT highest_observed_direct_signature_version
4081
+ FROM sync_peer_signature_state WHERE peer_device_id = ? LIMIT 1`).get(deviceId);
4082
+ let signatureVerification;
3892
4083
  try {
3893
- valid = verifySignature({
3894
- method: request.method,
3895
- pathWithQuery: pathWithQuery(request.url),
3896
- bodyBytes: body,
3897
- timestamp,
3898
- nonce,
3899
- signature,
4084
+ signatureVerification = verifyDirectPeerRequestSignature(request, body, {
4085
+ deviceId,
3900
4086
  publicKey: workerPublicKey,
3901
- deviceId
4087
+ expectedRecipientId: localDeviceId,
4088
+ highestObservedVersion: existingPeer?.highest_observed_direct_signature_version ?? null
3902
4089
  });
3903
4090
  } catch {
3904
4091
  return {
@@ -3907,9 +4094,9 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
3907
4094
  deviceId
3908
4095
  };
3909
4096
  }
3910
- if (!valid) return {
4097
+ if (!signatureVerification.ok) return {
3911
4098
  ok: false,
3912
- reason: "bootstrap_grant_invalid_signature",
4099
+ reason: signatureVerification.reason,
3913
4100
  deviceId
3914
4101
  };
3915
4102
  if (!isPeerTrustBindingCompatible(store.db, deviceId, workerPublicKey, workerFingerprint)) return {
@@ -3925,6 +4112,10 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
3925
4112
  publicKey: workerPublicKey,
3926
4113
  replaceTrust: true
3927
4114
  });
4115
+ if (signatureVerification.version === DIRECT_PEER_SIGNATURE_VERSION_NUMBER) {
4116
+ const versionResult = recordDirectPeerSignatureVersion(store, deviceId, signatureVerification.version);
4117
+ if (versionResult) return versionResult;
4118
+ }
3928
4119
  return {
3929
4120
  ok: true,
3930
4121
  reason: "ok",
@@ -4221,7 +4412,8 @@ async function acceptDiscoveredPeer(store, input) {
4221
4412
  const addressesJson = JSON.stringify(mergeAddresses(nextAddresses, existingAddresses));
4222
4413
  await createCoordinatorReciprocalApproval(store, config, {
4223
4414
  groupId,
4224
- requestedDeviceId: input.peerDeviceId
4415
+ requestedDeviceId: input.peerDeviceId,
4416
+ ...input.expectedIncomingRequestId ? { expectedIncomingRequestId: input.expectedIncomingRequestId } : {}
4225
4417
  });
4226
4418
  const coordinatorUrl = config.syncCoordinatorUrl || null;
4227
4419
  let scopeInclude;
@@ -4832,8 +5024,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
4832
5024
  if (!auth.ok) {
4833
5025
  const grantPresent = Boolean((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim());
4834
5026
  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"))}`);
4835
- const wireReason = bootstrapAttempted && grantPresent ? "bootstrap_grant_invalid" : deviceAuthReason ?? auth.reason;
4836
- return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason, true), 401);
5027
+ else logDirectPeerAuthDiagnostic(deviceAuthReason, auth.reason);
5028
+ return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(), 401);
4837
5029
  }
4838
5030
  const limited = rateLimitedResponse(c, auth.deviceId, true);
4839
5031
  if (limited) return limited;
@@ -4868,7 +5060,10 @@ function syncProtocolRoutes(getStore, opts = {}) {
4868
5060
  const store = getStore();
4869
5061
  const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
4870
5062
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
4871
- if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason, true), 401);
5063
+ if (!auth.ok) {
5064
+ logDirectPeerAuthDiagnostic(auth.reason);
5065
+ return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(), 401);
5066
+ }
4872
5067
  const limited = rateLimitedResponse(c, auth.deviceId, true);
4873
5068
  if (limited) return limited;
4874
5069
  const peerDeviceId = auth.deviceId;
@@ -4949,8 +5144,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
4949
5144
  if (!auth.ok) {
4950
5145
  const grantPresent = Boolean((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim());
4951
5146
  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"))}`);
4952
- const wireReason = bootstrapAttempted && grantPresent ? "bootstrap_grant_invalid" : deviceAuthReason ?? auth.reason;
4953
- return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason, true), 401);
5147
+ else logDirectPeerAuthDiagnostic(deviceAuthReason, auth.reason);
5148
+ return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(), 401);
4954
5149
  }
4955
5150
  const limited = rateLimitedResponse(c, auth.deviceId, true);
4956
5151
  if (limited) return limited;
@@ -5007,7 +5202,10 @@ function syncProtocolRoutes(getStore, opts = {}) {
5007
5202
  if (raw == null) return c.json({ error: "payload_too_large" }, 413);
5008
5203
  const auth = authorizeSyncRequest(store, c.req, raw);
5009
5204
  if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
5010
- if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason, true), 401);
5205
+ if (!auth.ok) {
5206
+ logDirectPeerAuthDiagnostic(auth.reason);
5207
+ return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(), 401);
5208
+ }
5011
5209
  const limited = rateLimitedResponse(c, auth.deviceId, true);
5012
5210
  if (limited) return limited;
5013
5211
  const peerDeviceId = auth.deviceId;
@@ -5071,6 +5269,52 @@ function syncProtocolRoutes(getStore, opts = {}) {
5071
5269
  });
5072
5270
  return app;
5073
5271
  }
5272
+ async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
5273
+ const config = readCoordinatorSyncConfig();
5274
+ const hasCoordinatorConfiguration = config.syncCoordinatorUrl.length > 0 || config.syncCoordinatorGroups.length > 0;
5275
+ const remoteUrl = config.syncCoordinatorUrl.trim();
5276
+ const adminSecret = config.syncCoordinatorAdminSecret.trim();
5277
+ const groupIds = [...new Set(config.syncCoordinatorGroups.map((groupId) => groupId.trim()))].filter(Boolean);
5278
+ if (!hasCoordinatorConfiguration) return {
5279
+ availability: "unavailable",
5280
+ safeErrorCode: "coordinator_not_configured",
5281
+ enrollments: []
5282
+ };
5283
+ if (!remoteUrl || !adminSecret || groupIds.length === 0) return {
5284
+ availability: "unavailable",
5285
+ safeErrorCode: "coordinator_unavailable",
5286
+ enrollments: []
5287
+ };
5288
+ if (groupIds.length > 25) return {
5289
+ availability: "unavailable",
5290
+ safeErrorCode: "coordinator_evidence_too_large",
5291
+ enrollments: []
5292
+ };
5293
+ try {
5294
+ const enrollments = (await Promise.all(groupIds.map((groupId) => coordinatorListDevicesAction({
5295
+ groupId,
5296
+ includeDisabled: true,
5297
+ remoteUrl,
5298
+ adminSecret
5299
+ })))).flat();
5300
+ if (enrollments.length > 500) return {
5301
+ availability: "unavailable",
5302
+ safeErrorCode: "coordinator_evidence_too_large",
5303
+ enrollments: []
5304
+ };
5305
+ return {
5306
+ availability: "available",
5307
+ safeErrorCode: null,
5308
+ enrollments
5309
+ };
5310
+ } catch {
5311
+ return {
5312
+ availability: "unavailable",
5313
+ safeErrorCode: "coordinator_unavailable",
5314
+ enrollments: []
5315
+ };
5316
+ }
5317
+ }
5074
5318
  /**
5075
5319
  * Viewer-facing sync management routes (/api/sync/*).
5076
5320
  *
@@ -5078,8 +5322,112 @@ function syncProtocolRoutes(getStore, opts = {}) {
5078
5322
  * provide sync status, peer management, and coordinator UI for the
5079
5323
  * local viewer.
5080
5324
  */
5081
- function syncRoutes(getStore, getSyncRuntimeStatus) {
5325
+ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
5082
5326
  const app = new Hono();
5327
+ const loadCoordinatorEvidence = options.loadDeviceIdentityCoordinatorEvidence ?? loadConfiguredDeviceIdentityCoordinatorEvidence;
5328
+ const currentCoordinatorEvidence = async () => {
5329
+ try {
5330
+ return await loadCoordinatorEvidence();
5331
+ } catch {
5332
+ return {
5333
+ availability: "unavailable",
5334
+ safeErrorCode: "coordinator_unavailable",
5335
+ enrollments: []
5336
+ };
5337
+ }
5338
+ };
5339
+ const localInventoryInput = async (store) => {
5340
+ let explicitDeviceName = "";
5341
+ try {
5342
+ explicitDeviceName = String(readCodememConfigFile().sync_device_name ?? "");
5343
+ } catch {
5344
+ explicitDeviceName = "";
5345
+ }
5346
+ return {
5347
+ localDeviceId: store.deviceId,
5348
+ localDisplayName: friendlyDeviceName({
5349
+ explicitName: explicitDeviceName,
5350
+ osName: hostname(),
5351
+ fallbackSeed: store.deviceId
5352
+ }),
5353
+ coordinator: await currentCoordinatorEvidence()
5354
+ };
5355
+ };
5356
+ app.get("/api/sync/recipient-policy/v1/device-inventory", async (c) => {
5357
+ const store = getStore();
5358
+ ensureStableStoreIdentity(store);
5359
+ return c.json(listDeviceIdentityInventory(store.db, await localInventoryInput(store)));
5360
+ });
5361
+ const parseDeviceBindingRequest = (value, commit) => {
5362
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5363
+ const body = value;
5364
+ const allowed = new Set(commit ? ["bindings", "reviewedInventoryDigest"] : ["bindings"]);
5365
+ if (Object.keys(body).some((key) => !allowed.has(key)) || !Array.isArray(body.bindings)) return null;
5366
+ const bindings = body.bindings.map((value) => {
5367
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5368
+ const binding = value;
5369
+ const bindingKeys = new Set([
5370
+ "deviceId",
5371
+ "targetIdentityId",
5372
+ "confirmed",
5373
+ "allowRebind"
5374
+ ]);
5375
+ if (Object.keys(binding).some((key) => !bindingKeys.has(key)) || typeof binding.deviceId !== "string" || typeof binding.targetIdentityId !== "string" || typeof binding.confirmed !== "boolean" || Object.hasOwn(binding, "allowRebind") && typeof binding.allowRebind !== "boolean") return null;
5376
+ return {
5377
+ deviceId: binding.deviceId,
5378
+ targetIdentityId: binding.targetIdentityId,
5379
+ confirmed: binding.confirmed,
5380
+ ...typeof binding.allowRebind === "boolean" ? { allowRebind: binding.allowRebind } : {}
5381
+ };
5382
+ });
5383
+ if (bindings.some((binding) => binding == null)) return null;
5384
+ if (commit && typeof body.reviewedInventoryDigest !== "string") return null;
5385
+ return {
5386
+ bindings,
5387
+ ...commit ? { reviewedInventoryDigest: body.reviewedInventoryDigest } : {}
5388
+ };
5389
+ };
5390
+ app.post("/api/sync/recipient-policy/v1/device-bindings/preview", async (c) => {
5391
+ const body = parseDeviceBindingRequest(await parseViewerJsonBody(c), false);
5392
+ if (!body) return c.json({ error: "binding_request_invalid" }, 400);
5393
+ const store = getStore();
5394
+ try {
5395
+ ensureStableStoreIdentity(store);
5396
+ const result = previewDeviceIdentityBindings(store.db, await localInventoryInput(store), body);
5397
+ if (result.status === "invalid") return c.json(result, 400);
5398
+ if (result.status === "not_found") return c.json(result, 404);
5399
+ if (result.status === "conflict") return c.json(result, 409);
5400
+ return c.json(result);
5401
+ } catch (error) {
5402
+ if (isSqliteBusy(error)) {
5403
+ c.header("Retry-After", "1");
5404
+ return c.json({ error: "binding_preview_busy" }, 503);
5405
+ }
5406
+ return c.json({ error: "binding_preview_failed" }, 500);
5407
+ }
5408
+ });
5409
+ app.post("/api/sync/recipient-policy/v1/device-bindings/commit", async (c) => {
5410
+ const body = parseDeviceBindingRequest(await parseViewerJsonBody(c), true);
5411
+ if (!body || !("reviewedInventoryDigest" in body)) return c.json({ error: "binding_request_invalid" }, 400);
5412
+ const store = getStore();
5413
+ try {
5414
+ ensureStableStoreIdentity(store);
5415
+ const result = commitDeviceIdentityBindings(store.db, {
5416
+ localActorId: store.actorId,
5417
+ localDeviceId: store.deviceId
5418
+ }, await localInventoryInput(store), body);
5419
+ if (result.status === "invalid") return c.json(result, 400);
5420
+ if (result.status === "not_found") return c.json(result, 404);
5421
+ if (result.status === "stale" || result.status === "conflict") return c.json(result, 409);
5422
+ return c.json(result);
5423
+ } catch (error) {
5424
+ if (isSqliteBusy(error)) {
5425
+ c.header("Retry-After", "1");
5426
+ return c.json({ error: "binding_commit_busy" }, 503);
5427
+ }
5428
+ return c.json({ error: "binding_commit_failed" }, 500);
5429
+ }
5430
+ });
5083
5431
  app.get("/api/sync/status", async (c) => {
5084
5432
  const store = getStore();
5085
5433
  {
@@ -5392,9 +5740,15 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
5392
5740
  return c.json({ error: "invalid json" }, 400);
5393
5741
  }
5394
5742
  const peerDeviceId = String(body.peer_device_id ?? "").trim();
5395
- const name = String(body.name ?? "").trim();
5743
+ const rawName = String(body.name ?? "");
5396
5744
  if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
5397
- if (!name) return c.json({ error: "name required" }, 400);
5745
+ if (!rawName.trim()) return c.json({ error: "name required" }, 400);
5746
+ let name;
5747
+ try {
5748
+ name = normalizeHumanPresentationName(rawName, "name");
5749
+ } catch (error) {
5750
+ return c.json({ error: error instanceof Error ? error.message : "name_invalid" }, 400);
5751
+ }
5398
5752
  if (!d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get()) return c.json({ error: "peer not found" }, 404);
5399
5753
  d.update(schema.syncPeers).set({ name }).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();
5400
5754
  return c.json({ ok: true });
@@ -5623,10 +5977,18 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
5623
5977
  if (!value || typeof value !== "object" || Array.isArray(value)) return c.json({ error: "request_invalid" }, 400);
5624
5978
  const body = value;
5625
5979
  if (Object.keys(body).some((key) => key !== "dryRun") || Object.hasOwn(body, "dryRun") && typeof body.dryRun !== "boolean") return c.json({ error: "request_invalid" }, 400);
5626
- return c.json(migrateRecipientPolicyIntent(store.db, {
5627
- localActorId: store.actorId,
5628
- localDeviceId: store.deviceId
5629
- }, { dryRun: body.dryRun === true }));
5980
+ try {
5981
+ return c.json(migrateRecipientPolicyIntent(store.db, {
5982
+ localActorId: store.actorId,
5983
+ localDeviceId: store.deviceId
5984
+ }, { dryRun: body.dryRun === true }));
5985
+ } catch (error) {
5986
+ if (isSqliteBusy(error)) {
5987
+ c.header("Retry-After", "1");
5988
+ return c.json({ error: "migration_busy" }, 503);
5989
+ }
5990
+ return c.json({ error: "migration_failed" }, 500);
5991
+ }
5630
5992
  });
5631
5993
  const parseReviewRequest = (value) => {
5632
5994
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
@@ -5802,7 +6164,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
5802
6164
  operationId: plan.operationId,
5803
6165
  reviewedProjectSetDigest: plan.reviewedProjectSetDigest,
5804
6166
  inviterActorId: plan.inviterActorId,
5805
- inviterDisplayName: store.actorDisplayName,
6167
+ inviterDisplayName: projectInviteInviterDisplayName(store.actorDisplayName),
5806
6168
  inviterDeviceId: plan.inviterDeviceIds[0],
5807
6169
  pendingPersonId: plan.personId,
5808
6170
  projectSummaries: plan.projects.map((project) => ({
@@ -6110,8 +6472,14 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
6110
6472
  } catch {
6111
6473
  return c.json({ error: "invalid json" }, 400);
6112
6474
  }
6113
- const displayName = String(body.display_name ?? "").trim();
6114
- if (!displayName) return c.json({ error: "display_name required" }, 400);
6475
+ const rawDisplayName = String(body.display_name ?? "");
6476
+ if (!rawDisplayName.trim()) return c.json({ error: "display_name required" }, 400);
6477
+ let displayName;
6478
+ try {
6479
+ displayName = normalizeHumanPresentationName(rawDisplayName, "display_name");
6480
+ } catch (error) {
6481
+ return c.json({ error: error instanceof Error ? error.message : "display_name_invalid" }, 400);
6482
+ }
6115
6483
  const d = drizzle(store.db, { schema });
6116
6484
  const now = (/* @__PURE__ */ new Date()).toISOString();
6117
6485
  const actor = {
@@ -6136,9 +6504,13 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
6136
6504
  return c.json({ error: "invalid json" }, 400);
6137
6505
  }
6138
6506
  const actorId = String(body.actor_id ?? "").trim();
6139
- const displayName = String(body.display_name ?? "").trim();
6140
6507
  if (!actorId) return c.json({ error: "actor_id required" }, 400);
6141
- if (!displayName) return c.json({ error: "display_name required" }, 400);
6508
+ let displayName;
6509
+ try {
6510
+ displayName = normalizeHumanPresentationName(String(body.display_name ?? ""), "display_name");
6511
+ } catch (error) {
6512
+ return c.json({ error: error instanceof Error ? error.message : "display_name_invalid" }, 400);
6513
+ }
6142
6514
  const d = drizzle(store.db, { schema });
6143
6515
  const actor = d.select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
6144
6516
  if (!actor) return c.json({ error: "actor not found" }, 404);
@@ -6168,14 +6540,133 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
6168
6540
  if (!secondaryActorId) return c.json({ error: "secondary_actor_id required" }, 400);
6169
6541
  if (primaryActorId === secondaryActorId) return c.json({ error: "actor ids must differ" }, 400);
6170
6542
  const d = drizzle(store.db, { schema });
6171
- const primary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, primaryActorId)).get();
6172
- const secondary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, secondaryActorId)).get();
6173
- if (!primary || !secondary) return c.json({ error: "actor not found" }, 404);
6174
- if (primary.status !== "active") return c.json({ error: "primary actor not active" }, 409);
6175
- if (secondary.status !== "active") return c.json({ error: "secondary actor not active" }, 409);
6176
- if (secondary.is_local && secondary.actor_id === store.actorId) return c.json({ error: "cannot merge this device's own local actor" }, 409);
6177
6543
  const now = (/* @__PURE__ */ new Date()).toISOString();
6178
- const mergedCount = store.db.transaction(() => {
6544
+ const result = store.db.transaction(() => {
6545
+ const primary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, primaryActorId)).get();
6546
+ const secondary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, secondaryActorId)).get();
6547
+ if (!primary || !secondary) return {
6548
+ error: "actor not found",
6549
+ status: 404
6550
+ };
6551
+ if (primary.status !== "active") return {
6552
+ error: "primary actor not active",
6553
+ status: 409
6554
+ };
6555
+ if (secondary.status !== "active") return {
6556
+ error: "secondary actor not active",
6557
+ status: 409
6558
+ };
6559
+ if (secondary.is_local && secondary.actor_id === store.actorId) return {
6560
+ error: "cannot merge this device's own local actor",
6561
+ status: 409
6562
+ };
6563
+ const memberships = d.select().from(schema.policyTeamMemberships).where(eq(schema.policyTeamMemberships.identity_id, secondaryActorId)).all();
6564
+ const primaryMemberships = /* @__PURE__ */ new Map();
6565
+ for (const membership of memberships) {
6566
+ const primaryMembership = d.select().from(schema.policyTeamMemberships).where(and(eq(schema.policyTeamMemberships.team_id, membership.team_id), eq(schema.policyTeamMemberships.identity_id, primaryActorId))).get();
6567
+ if (primaryMembership && (primaryMembership.role !== membership.role || primaryMembership.status !== membership.status)) return {
6568
+ error: "actor team memberships conflict",
6569
+ status: 409
6570
+ };
6571
+ primaryMemberships.set(membership.team_id, primaryMembership);
6572
+ }
6573
+ const directRecipients = d.select().from(schema.projectRecipients).where(and(eq(schema.projectRecipients.recipient_kind, "identity"), eq(schema.projectRecipients.recipient_id, secondaryActorId))).all();
6574
+ const primaryRecipients = /* @__PURE__ */ new Map();
6575
+ for (const recipient of directRecipients) {
6576
+ const primaryRecipient = d.select().from(schema.projectRecipients).where(and(eq(schema.projectRecipients.canonical_project_identity, recipient.canonical_project_identity), eq(schema.projectRecipients.recipient_kind, "identity"), eq(schema.projectRecipients.recipient_id, primaryActorId))).get();
6577
+ if (primaryRecipient && primaryRecipient.status !== recipient.status) return {
6578
+ error: "actor project recipients conflict",
6579
+ status: 409
6580
+ };
6581
+ primaryRecipients.set(recipient.canonical_project_identity, primaryRecipient);
6582
+ }
6583
+ const mergeDigest = createHash("sha256").update(JSON.stringify({
6584
+ kind: "identity-merge-v1",
6585
+ primaryActorId,
6586
+ secondaryActorId,
6587
+ nonce: randomUUID()
6588
+ })).digest("hex");
6589
+ const bindings = d.select({
6590
+ deviceId: schema.identityDevices.device_id,
6591
+ assignmentVersion: schema.identityDevices.assignment_version
6592
+ }).from(schema.identityDevices).where(eq(schema.identityDevices.identity_id, secondaryActorId)).all();
6593
+ if (bindings.length > 0) {
6594
+ const commitDigest = mergeDigest;
6595
+ const outcomes = bindings.map((binding) => ({
6596
+ deviceId: binding.deviceId,
6597
+ previousIdentityId: secondaryActorId,
6598
+ targetIdentityId: primaryActorId,
6599
+ action: "rebind"
6600
+ }));
6601
+ store.db.prepare(`INSERT INTO device_identity_binding_commits(
6602
+ commit_digest, reviewed_inventory_digest, request_json, outcomes_json, write_count,
6603
+ decided_by_identity_id, decided_by_device_id, created_at
6604
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(commitDigest, `identity-merge:${secondaryActorId}:${primaryActorId}`, JSON.stringify({
6605
+ primaryActorId,
6606
+ secondaryActorId
6607
+ }), JSON.stringify(outcomes), bindings.length, store.actorId, store.deviceId, now);
6608
+ for (const binding of bindings) {
6609
+ const revision = createHash("sha256").update(`identity-merge-revision-v1\0${commitDigest}\0${binding.deviceId}`).digest("hex");
6610
+ const idempotencyKey = createHash("sha256").update(`identity-merge-row-v1\0${commitDigest}\0${binding.deviceId}`).digest("hex");
6611
+ if (store.db.prepare(`UPDATE identity_devices SET
6612
+ identity_id = ?, provenance = 'review_resolution', revision = ?,
6613
+ migration_state = 'user_managed', source_fingerprint = ?,
6614
+ idempotency_key = ?, updated_at = ?
6615
+ WHERE device_id = ? AND identity_id = ?`).run(primaryActorId, revision, commitDigest, idempotencyKey, now, binding.deviceId, secondaryActorId).changes !== 1) throw new Error("identity_merge_binding_update_failed");
6616
+ const rebound = store.db.prepare("SELECT assignment_version FROM identity_devices WHERE device_id = ?").get(binding.deviceId);
6617
+ if (!rebound || rebound.assignment_version !== binding.assignmentVersion + 1) throw new Error("identity_merge_assignment_version_unexpected");
6618
+ store.db.prepare("DELETE FROM policy_team_device_decisions WHERE device_id = ?").run(binding.deviceId);
6619
+ store.db.prepare(`INSERT INTO device_identity_binding_audit(
6620
+ event_id, commit_digest, device_id, previous_identity_id, target_identity_id,
6621
+ action, previous_assignment_version, resulting_assignment_version,
6622
+ decided_by_identity_id, decided_by_device_id, created_at
6623
+ ) VALUES (?, ?, ?, ?, ?, 'rebind', ?, ?, ?, ?, ?)`).run(createHash("sha256").update(`identity-merge-event-v1\0${commitDigest}\0${binding.deviceId}`).digest("hex"), commitDigest, binding.deviceId, secondaryActorId, primaryActorId, binding.assignmentVersion, rebound.assignment_version, store.actorId, store.deviceId, now);
6624
+ }
6625
+ }
6626
+ for (const membership of memberships) {
6627
+ if (primaryMemberships.get(membership.team_id)) {
6628
+ d.delete(schema.policyTeamMemberships).where(and(eq(schema.policyTeamMemberships.team_id, membership.team_id), eq(schema.policyTeamMemberships.identity_id, secondaryActorId))).run();
6629
+ continue;
6630
+ }
6631
+ const revision = createHash("sha256").update(`identity-merge-membership-revision-v1\0${mergeDigest}\0${membership.team_id}`).digest("hex");
6632
+ const idempotencyKey = createHash("sha256").update(`identity-merge-membership-row-v1\0${mergeDigest}\0${membership.team_id}`).digest("hex");
6633
+ d.update(schema.policyTeamMemberships).set({
6634
+ identity_id: primaryActorId,
6635
+ provenance: "review_resolution",
6636
+ revision,
6637
+ migration_state: "user_managed",
6638
+ source_fingerprint: mergeDigest,
6639
+ idempotency_key: idempotencyKey,
6640
+ updated_at: now
6641
+ }).where(and(eq(schema.policyTeamMemberships.team_id, membership.team_id), eq(schema.policyTeamMemberships.identity_id, secondaryActorId))).run();
6642
+ }
6643
+ for (const recipient of directRecipients) {
6644
+ if (primaryRecipients.get(recipient.canonical_project_identity)) {
6645
+ d.delete(schema.projectRecipients).where(and(eq(schema.projectRecipients.canonical_project_identity, recipient.canonical_project_identity), eq(schema.projectRecipients.recipient_kind, "identity"), eq(schema.projectRecipients.recipient_id, secondaryActorId))).run();
6646
+ continue;
6647
+ }
6648
+ const policyRevision = createHash("sha256").update(`identity-merge-recipient-revision-v1\0${mergeDigest}\0${recipient.canonical_project_identity}`).digest("hex");
6649
+ const idempotencyKey = createHash("sha256").update(`identity-merge-recipient-row-v1\0${mergeDigest}\0${recipient.canonical_project_identity}`).digest("hex");
6650
+ d.update(schema.projectRecipients).set({
6651
+ recipient_id: primaryActorId,
6652
+ provenance: "review_resolution",
6653
+ policy_revision: policyRevision,
6654
+ migration_state: "user_managed",
6655
+ source_fingerprint: mergeDigest,
6656
+ idempotency_key: idempotencyKey,
6657
+ updated_at: now
6658
+ }).where(and(eq(schema.projectRecipients.canonical_project_identity, recipient.canonical_project_identity), eq(schema.projectRecipients.recipient_kind, "identity"), eq(schema.projectRecipients.recipient_id, secondaryActorId))).run();
6659
+ }
6660
+ d.update(schema.recipientManagedProjectProjections).set({
6661
+ recipient_identity_id: primaryActorId,
6662
+ updated_at: now
6663
+ }).where(eq(schema.recipientManagedProjectProjections.recipient_identity_id, secondaryActorId)).run();
6664
+ store.db.prepare(`UPDATE share_operations SET
6665
+ inviter_actor_id = CASE WHEN inviter_actor_id = ? THEN ? ELSE inviter_actor_id END,
6666
+ recipient_actor_id = CASE WHEN recipient_actor_id = ? THEN ? ELSE recipient_actor_id END,
6667
+ person_id = CASE WHEN person_id = ? THEN ? ELSE person_id END,
6668
+ updated_at = ?
6669
+ WHERE inviter_actor_id = ? OR recipient_actor_id = ? OR person_id = ?`).run(secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, now, secondaryActorId, secondaryActorId, secondaryActorId);
6179
6670
  const peerUpdate = d.update(schema.syncPeers).set({ actor_id: primaryActorId }).where(eq(schema.syncPeers.actor_id, secondaryActorId)).run();
6180
6671
  if (primary.is_local) d.update(schema.syncPeers).set({ claimed_local_actor: 1 }).where(eq(schema.syncPeers.actor_id, primaryActorId)).run();
6181
6672
  d.update(schema.actors).set({
@@ -6183,9 +6674,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
6183
6674
  merged_into_actor_id: primaryActorId,
6184
6675
  updated_at: now
6185
6676
  }).where(eq(schema.actors.actor_id, secondaryActorId)).run();
6186
- return Number(peerUpdate.changes ?? 0);
6187
- })();
6188
- return c.json({ merged_count: mergedCount });
6677
+ return { mergedCount: Number(peerUpdate.changes ?? 0) };
6678
+ }).immediate();
6679
+ if ("error" in result) return c.json({ error: result.error }, result.status);
6680
+ return c.json({ merged_count: result.mergedCount });
6189
6681
  });
6190
6682
  app.post("/api/sync/actors/deactivate", async (c) => {
6191
6683
  const store = getStore();
@@ -6238,11 +6730,18 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
6238
6730
  }
6239
6731
  const peerDeviceId = String(body.peer_device_id ?? "").trim();
6240
6732
  const fingerprint = String(body.fingerprint ?? "").trim();
6733
+ const expectedGroupId = String(body.expected_group_id ?? "").trim();
6734
+ const hasExpectedIncomingRequestId = Object.hasOwn(body, "expected_incoming_request_id");
6735
+ const expectedIncomingRequestId = String(body.expected_incoming_request_id ?? "").trim();
6241
6736
  if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
6737
+ if (hasExpectedIncomingRequestId && !expectedIncomingRequestId) return c.json({ error: "expected_incoming_request_id must not be empty" }, 400);
6738
+ if (expectedIncomingRequestId && !expectedGroupId) return c.json({ error: "expected_group_id required" }, 400);
6242
6739
  try {
6243
6740
  const result = await acceptDiscoveredPeer(store, {
6244
6741
  peerDeviceId,
6245
- fingerprint: fingerprint || void 0
6742
+ fingerprint: fingerprint || void 0,
6743
+ expectedGroupId: expectedGroupId || void 0,
6744
+ expectedIncomingRequestId: expectedIncomingRequestId || void 0
6246
6745
  });
6247
6746
  if (!result.ok) return c.json({
6248
6747
  error: result.error,
@@ -6258,6 +6757,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
6258
6757
  needs_scope_review: true
6259
6758
  });
6260
6759
  } catch (error) {
6760
+ if (error instanceof CoordinatorReciprocalApprovalRequestChangedError) return c.json({
6761
+ error: error.code,
6762
+ detail: "This approval request changed before it was submitted. Refresh sync status and review the current request."
6763
+ }, { status: 409 });
6261
6764
  return c.json({
6262
6765
  error: "coordinator_lookup_failed",
6263
6766
  detail: error instanceof Error ? error.message : String(error)
@@ -6408,7 +6911,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
6408
6911
  inviteValue: rawValue,
6409
6912
  dbPath: store.dbPath,
6410
6913
  recipientActorId: recipientInvite ? null : store.actorId,
6411
- recipientDisplayName: typeof body.recipient_name === "string" ? body.recipient_name : store.actorDisplayName,
6914
+ recipientDisplayName: decoded.kind === "add_device" ? null : typeof body.recipient_name === "string" ? body.recipient_name : store.actorDisplayName,
6412
6915
  deviceDisplayName: typeof body.device_name === "string" ? body.device_name : null,
6413
6916
  reviewedOnboardingDigest: reviewedOnboardingDigest || null
6414
6917
  });
@@ -7255,15 +7758,19 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
7255
7758
  }, 400);
7256
7759
  }
7257
7760
  const groupId = resolveCoordinatorAdminGroup(String(body.group_id ?? "").trim(), status);
7258
- const displayName = String(body.display_name ?? "").trim();
7259
7761
  if (!groupId) return c.json({
7260
7762
  error: "group_id required",
7261
7763
  status
7262
7764
  }, 400);
7263
- if (!displayName) return c.json({
7264
- error: "display_name required",
7265
- status
7266
- }, 400);
7765
+ let displayName;
7766
+ try {
7767
+ displayName = normalizeHumanPresentationName(String(body.display_name ?? ""), "display_name");
7768
+ } catch (error) {
7769
+ return c.json({
7770
+ error: error instanceof Error ? error.message : "display_name_invalid",
7771
+ status
7772
+ }, 400);
7773
+ }
7267
7774
  try {
7268
7775
  const device = await coordinatorRenameDeviceAction({
7269
7776
  groupId,
@@ -7682,6 +8189,850 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
7682
8189
  return app;
7683
8190
  }
7684
8191
  //#endregion
8192
+ //#region src/routes/team-setup.ts
8193
+ var TEAM_SETUP_VERSION = 1;
8194
+ var MAX_CONFIGURED_GROUPS = 25;
8195
+ var MAX_DEVICES = 500;
8196
+ var MAX_PROJECTS = 500;
8197
+ var MAX_ACCESS_DELTA_ENTRIES = 1e4;
8198
+ var MAX_IDENTITY_CHOICES = 500;
8199
+ var MAX_COMPLETED_IDENTITY_CHOICES = MAX_DEVICES * 4;
8200
+ var MAX_PROJECT_MAPPING_CHOICES = 500;
8201
+ var MAX_TOTAL_PROJECT_MAPPING_CHOICES = 1e4;
8202
+ var MAX_PROJECT_MAPPING_SCAN_ROWS = 1e4;
8203
+ var MAX_PROJECT_MAPPING_METADATA_ROWS = 1e4;
8204
+ var MAX_MUTATION_BODY_BYTES = 8192;
8205
+ var SUMMARY_SNAPSHOT_CACHE_TTL_MS = 3e4;
8206
+ var TEAM_SETUP_ROUTE_PREFIX = "/api/sync/team-setup/v1";
8207
+ var CANDIDATE_REF_PATTERN = /^legacy-team-candidate:[0-9a-f]{32}$/u;
8208
+ var DEVICE_REF_PATTERN = /^legacy-team-device-ref-v1:[0-9a-f]{64}$/u;
8209
+ var PROJECT_REF_PATTERN = /^legacy-team-project-ref-v1:[0-9a-f]{64}$/u;
8210
+ var IDENTITY_REF_PATTERN = /^legacy-team-viewer-identity-ref-v1:[0-9a-f]{64}$/u;
8211
+ var RESOLVED_PROJECT_REF_PATTERN = /^legacy-team-resolved-project-ref-v1:[0-9a-f]{64}$/u;
8212
+ var ATTEMPT_ID_PATTERN = /^legacy-team-attempt:[0-9a-f-]{36}$/u;
8213
+ var FINISH_DIGEST_PATTERN = /^legacy-team-activation-finish-v1:[0-9a-f]{64}$/u;
8214
+ var ACCESS_DELTA_DIGEST_PATTERN = /^legacy-team-access-delta:[0-9a-f]{64}$/u;
8215
+ var VIEWER_ACCESS_DELTA_DIGEST_PATTERN = /^legacy-team-viewer-access-delta-v1:[0-9a-f]{64}$/u;
8216
+ var defaultSnapshotLoaderDependencies = {
8217
+ readConfig: readCoordinatorSyncConfig,
8218
+ listGroups: coordinatorListGroupsAction,
8219
+ listDevices: coordinatorListDevicesAction
8220
+ };
8221
+ function safeCoordinatorError() {
8222
+ return /* @__PURE__ */ new Error("team_setup_roster_unavailable");
8223
+ }
8224
+ function isCoordinatorRosterTooLargeError(error) {
8225
+ return error instanceof Error && error.message === "coordinator_response_too_large";
8226
+ }
8227
+ function configuredGroupIds(groups) {
8228
+ const unique = [...new Set(groups.map((group) => group.trim()).filter(Boolean))];
8229
+ if (unique.length > MAX_CONFIGURED_GROUPS) throw safeCoordinatorError();
8230
+ return unique;
8231
+ }
8232
+ async function loadConfiguredLegacyTeamGroupSnapshotsWith(dependencies, options = {}) {
8233
+ let config;
8234
+ try {
8235
+ config = dependencies.readConfig();
8236
+ } catch {
8237
+ throw safeCoordinatorError();
8238
+ }
8239
+ const groupIds = configuredGroupIds(config.syncCoordinatorGroups);
8240
+ const hasUrl = Boolean(config.syncCoordinatorUrl);
8241
+ const hasSecret = Boolean(config.syncCoordinatorAdminSecret);
8242
+ if (!hasUrl && !hasSecret && groupIds.length === 0) return [];
8243
+ if (!hasUrl || !hasSecret || groupIds.length === 0) throw safeCoordinatorError();
8244
+ try {
8245
+ const remoteUrl = buildBaseUrl(config.syncCoordinatorUrl);
8246
+ const coordinatorId = remoteUrl;
8247
+ const timeoutS = Math.max(1, config.syncCoordinatorTimeoutS);
8248
+ const requestedGroupIds = options.candidateRef ? groupIds.filter((groupId) => legacyTeamCandidateId(coordinatorId, groupId) === options.candidateRef) : groupIds;
8249
+ const groups = await dependencies.listGroups({
8250
+ remoteUrl,
8251
+ adminSecret: config.syncCoordinatorAdminSecret,
8252
+ includeArchived: false,
8253
+ timeoutS
8254
+ });
8255
+ const groupById = new Map(groups.map((group) => [group.group_id, group]));
8256
+ return (await Promise.all(requestedGroupIds.map(async (groupId) => {
8257
+ const group = groupById.get(groupId);
8258
+ if (!group || group.group_id !== groupId || group.display_name !== null && typeof group.display_name !== "string" || group.archived_at != null) throw safeCoordinatorError();
8259
+ let devices;
8260
+ try {
8261
+ devices = await dependencies.listDevices({
8262
+ groupId,
8263
+ includeDisabled: true,
8264
+ remoteUrl,
8265
+ adminSecret: config.syncCoordinatorAdminSecret,
8266
+ timeoutS
8267
+ });
8268
+ } catch (error) {
8269
+ if (isCoordinatorRosterTooLargeError(error)) {
8270
+ if (options.candidateRef) throw new Error("legacy_team_setup_roster_too_large");
8271
+ return null;
8272
+ }
8273
+ throw error;
8274
+ }
8275
+ if (devices.length > MAX_DEVICES) {
8276
+ if (options.candidateRef) throw new Error("legacy_team_setup_roster_too_large");
8277
+ return null;
8278
+ }
8279
+ if (devices.some((device) => fingerprintPublicKey(device.public_key) !== device.fingerprint || device.enabled !== 0 && device.enabled !== 1)) throw safeCoordinatorError();
8280
+ return {
8281
+ coordinatorId,
8282
+ groupId,
8283
+ displayName: group.display_name ?? "Legacy Team",
8284
+ devices: devices.map((device) => ({
8285
+ deviceId: device.device_id,
8286
+ fingerprint: device.fingerprint,
8287
+ displayName: device.display_name ?? "Device",
8288
+ enabled: device.enabled === 1,
8289
+ labelRedactionIds: [device.identity_id ?? "", device.public_key].filter(Boolean)
8290
+ }))
8291
+ };
8292
+ }))).filter((snapshot) => snapshot !== null);
8293
+ } catch (error) {
8294
+ if (error instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
8295
+ throw safeCoordinatorError();
8296
+ }
8297
+ }
8298
+ async function loadConfiguredLegacyTeamGroupSnapshots(options = {}) {
8299
+ return loadConfiguredLegacyTeamGroupSnapshotsWith(defaultSnapshotLoaderDependencies, options);
8300
+ }
8301
+ function requireBoundedSnapshots(groups) {
8302
+ if (groups.length > MAX_CONFIGURED_GROUPS) throw new Error("legacy_team_setup_roster_too_large");
8303
+ }
8304
+ function projectionOptions(store) {
8305
+ return {
8306
+ localActorId: store.actorId,
8307
+ localDeviceId: store.deviceId
8308
+ };
8309
+ }
8310
+ function discoverCandidates(store, groups) {
8311
+ requireBoundedSnapshots(groups);
8312
+ return discoverLegacyTeamCandidates(store.db, {
8313
+ projection: projectionOptions(store),
8314
+ groups
8315
+ });
8316
+ }
8317
+ function candidateSummary(candidate) {
8318
+ return {
8319
+ candidateRef: candidate.candidateRef,
8320
+ displayName: candidate.displayName,
8321
+ status: candidate.status,
8322
+ deviceCount: candidate.deviceCount,
8323
+ projectCount: candidate.projectCount,
8324
+ unresolvedDeviceCount: candidate.unresolvedDeviceCount,
8325
+ unresolvedProjectCount: candidate.unresolvedProjectCount
8326
+ };
8327
+ }
8328
+ function candidateSummaryForDraft(candidate, draft) {
8329
+ return {
8330
+ ...candidateSummary(candidate),
8331
+ status: draft.state === "completed" ? "ready" : draft.state,
8332
+ unresolvedDeviceCount: draft.unresolvedDeviceCount,
8333
+ unresolvedProjectCount: draft.unresolvedProjectCount
8334
+ };
8335
+ }
8336
+ function completedCandidateFromDraft(draft) {
8337
+ return {
8338
+ candidateRef: draft.candidateRef,
8339
+ displayName: draft.displayName,
8340
+ status: "ready",
8341
+ deviceCount: draft.devices.length,
8342
+ projectCount: draft.projects.length,
8343
+ unresolvedDeviceCount: draft.unresolvedDeviceCount,
8344
+ unresolvedProjectCount: draft.unresolvedProjectCount
8345
+ };
8346
+ }
8347
+ function identityRef(candidateRef, identityId) {
8348
+ return identityId ? recipientPolicyDigest("legacy-team-viewer-identity-ref-v1", [candidateRef, identityId]) : null;
8349
+ }
8350
+ function requiredIdentityRef(candidateRef, identityId) {
8351
+ return recipientPolicyDigest("legacy-team-viewer-identity-ref-v1", [candidateRef, identityId]);
8352
+ }
8353
+ function teamRef(candidateRef, teamId) {
8354
+ return recipientPolicyDigest("legacy-team-viewer-team-ref-v1", [candidateRef, teamId]);
8355
+ }
8356
+ function resolvedProjectRef(projectRef, projectIdentity) {
8357
+ return projectIdentity ? legacyTeamResolvedProjectRef(projectRef, projectIdentity) : null;
8358
+ }
8359
+ function viewerSafeAccessDelta(candidateRef, delta, labels) {
8360
+ const fallbackLabels = /* @__PURE__ */ new Map();
8361
+ const fallbackLabelCounts = /* @__PURE__ */ new Map();
8362
+ const reservedLabels = new Set([
8363
+ labels?.teamDisplayName,
8364
+ ...labels?.devices.map((device) => device.displayName) ?? [],
8365
+ ...labels?.projects.flatMap((project) => [project.displayName, ...project.mappingChoices.map((choice) => choice.displayName)]) ?? [],
8366
+ ...labels?.identityChoices.map((identity) => identity.displayName) ?? []
8367
+ ].filter((label) => Boolean(label)).map((label) => normalizeChoiceLabelText(label).toLowerCase()));
8368
+ const fallbackLabel = (kind, ref) => {
8369
+ const key = `${kind}:${ref}`;
8370
+ const existing = fallbackLabels.get(key);
8371
+ if (existing) return existing;
8372
+ let index = (fallbackLabelCounts.get(kind) ?? 0) + 1;
8373
+ let label = `${kind} outside this setup (${index})`;
8374
+ while (reservedLabels.has(normalizeChoiceLabelText(label).toLowerCase())) {
8375
+ index += 1;
8376
+ label = `${kind} outside this setup (${index})`;
8377
+ }
8378
+ fallbackLabelCounts.set(kind, index);
8379
+ reservedLabels.add(normalizeChoiceLabelText(label).toLowerCase());
8380
+ fallbackLabels.set(key, label);
8381
+ return label;
8382
+ };
8383
+ const projectsByRef = new Map(labels?.projects.map((project) => [project.projectRef, project]));
8384
+ const projectsByCanonicalRef = new Map(labels?.projects.flatMap((project) => project.canonicalProjectRef ? [[project.canonicalProjectRef, project]] : []));
8385
+ const identitiesByRef = new Map(labels?.identityChoices.map((identity) => [identity.identityRef, identity]));
8386
+ const devicesByRef = new Map(labels?.devices.map((device) => [device.deviceRef, device]));
8387
+ const resolvedDisplayName = (projectRef, ref) => {
8388
+ if (!ref) return null;
8389
+ const project = projectsByRef.get(projectRef);
8390
+ const choice = project?.mappingChoices.find((item) => item.resolvedProjectRef === ref);
8391
+ if (choice) return choice.displayName;
8392
+ if (project?.resolvedProjectRef === ref && project.resolution === "deterministic") return `${project.displayName} (automatic match)`;
8393
+ return fallbackLabel("Project", ref);
8394
+ };
8395
+ return {
8396
+ teamChanges: delta.teamChanges.map((change) => ({
8397
+ teamRef: teamRef(candidateRef, change.teamId),
8398
+ teamDisplayName: labels?.teamDisplayName ?? fallbackLabel("Team", teamRef(candidateRef, change.teamId)),
8399
+ change: change.change,
8400
+ fromDeviceEligibilityMode: change.fromDeviceEligibilityMode,
8401
+ toDeviceEligibilityMode: change.toDeviceEligibilityMode
8402
+ })),
8403
+ membershipChanges: delta.membershipChanges.map((change) => {
8404
+ const identityRef = requiredIdentityRef(candidateRef, change.identityId);
8405
+ const membershipTeamRef = teamRef(candidateRef, change.teamId);
8406
+ return {
8407
+ teamRef: membershipTeamRef,
8408
+ teamDisplayName: labels?.teamDisplayName ?? fallbackLabel("Team", membershipTeamRef),
8409
+ identityRef,
8410
+ identityDisplayName: identitiesByRef.get(identityRef)?.displayName ?? fallbackLabel("Person", identityRef),
8411
+ change: change.change
8412
+ };
8413
+ }),
8414
+ projectChanges: delta.projectChanges.map((change) => {
8415
+ const fromRef = resolvedProjectRef(change.projectRef, change.fromProjectIdentity);
8416
+ const toRef = resolvedProjectRef(change.projectRef, change.toProjectIdentity);
8417
+ return {
8418
+ projectRef: change.projectRef,
8419
+ projectDisplayName: projectsByRef.get(change.projectRef)?.displayName ?? fallbackLabel("Project", change.projectRef),
8420
+ fromResolvedProjectRef: fromRef,
8421
+ fromResolvedProjectDisplayName: resolvedDisplayName(change.projectRef, fromRef),
8422
+ toResolvedProjectRef: toRef,
8423
+ toResolvedProjectDisplayName: resolvedDisplayName(change.projectRef, toRef),
8424
+ change: change.change
8425
+ };
8426
+ }),
8427
+ recipientChanges: delta.recipientChanges.map((change) => {
8428
+ const canonicalProjectRef = legacyTeamCanonicalProjectRef(candidateRef, change.canonicalProjectIdentity);
8429
+ const recipientRef = teamRef(candidateRef, change.recipientId);
8430
+ return {
8431
+ canonicalProjectRef,
8432
+ canonicalProjectDisplayName: projectsByCanonicalRef.get(canonicalProjectRef)?.displayName ?? fallbackLabel("Project", canonicalProjectRef),
8433
+ recipientKind: change.recipientKind,
8434
+ recipientRef,
8435
+ recipientDisplayName: labels?.teamDisplayName ?? fallbackLabel("Team", recipientRef),
8436
+ change: change.change
8437
+ };
8438
+ }),
8439
+ deviceAccessChanges: delta.deviceAccessChanges.map((change) => {
8440
+ const canonicalProjectRef = legacyTeamCanonicalProjectRef(candidateRef, change.canonicalProjectIdentity);
8441
+ const deviceRef = legacyTeamDeviceRef(candidateRef, change.deviceId);
8442
+ return {
8443
+ canonicalProjectRef,
8444
+ canonicalProjectDisplayName: projectsByCanonicalRef.get(canonicalProjectRef)?.displayName ?? fallbackLabel("Project", canonicalProjectRef),
8445
+ deviceRef,
8446
+ deviceDisplayName: devicesByRef.get(deviceRef)?.displayName ?? fallbackLabel("Device", deviceRef),
8447
+ change: change.change
8448
+ };
8449
+ })
8450
+ };
8451
+ }
8452
+ function viewerAccessDeltaDigest(delta) {
8453
+ return recipientPolicyDigest("legacy-team-viewer-access-delta-v1", delta);
8454
+ }
8455
+ function requireBoundedAccessDelta(delta) {
8456
+ const total = delta.teamChanges.length + delta.membershipChanges.length + delta.projectChanges.length + delta.recipientChanges.length + delta.deviceAccessChanges.length;
8457
+ if (delta.teamChanges.length > MAX_ACCESS_DELTA_ENTRIES || delta.membershipChanges.length > MAX_ACCESS_DELTA_ENTRIES || delta.projectChanges.length > MAX_ACCESS_DELTA_ENTRIES || delta.recipientChanges.length > MAX_ACCESS_DELTA_ENTRIES || delta.deviceAccessChanges.length > MAX_ACCESS_DELTA_ENTRIES || total > MAX_ACCESS_DELTA_ENTRIES) throw new Error("legacy_team_setup_roster_too_large");
8458
+ }
8459
+ var SAFE_CHOICE_LABEL_PATTERN = /^[\p{L}\p{N} '&,.()_-]*$/u;
8460
+ function normalizeChoiceLabelText(value) {
8461
+ return value.normalize("NFKC").replace(/\p{Cf}/gu, "").replace(/\p{Cc}/gu, " ").replace(/\s+/gu, " ").trim();
8462
+ }
8463
+ function safeChoiceLabel(value, fallback, forbiddenIds, opaqueRef) {
8464
+ const normalized = normalizeChoiceLabelText(value.slice(0, 512));
8465
+ const label = normalized.slice(0, 120).trim();
8466
+ const forbiddenComparisons = forbiddenIds.map((forbiddenId) => normalizeChoiceLabelText(forbiddenId).toLowerCase()).filter(Boolean);
8467
+ const comparable = normalized.toLowerCase();
8468
+ if (!label || !SAFE_CHOICE_LABEL_PATTERN.test(label) || /[\p{L}\p{N}]\.[\p{L}\p{N}]/u.test(label) || /-----|\b(?:ssh|ecdsa|sk)-[\p{L}\p{N}-]+ /iu.test(label) || forbiddenComparisons.some((forbiddenComparison) => comparable.includes(forbiddenComparison))) {
8469
+ const suffix = opaqueRef?.slice(-6);
8470
+ return suffix ? `${fallback} ${suffix}` : fallback;
8471
+ }
8472
+ return label;
8473
+ }
8474
+ function disambiguateChoiceLabels(choices, choiceRef) {
8475
+ const counts = /* @__PURE__ */ new Map();
8476
+ const originalLabels = /* @__PURE__ */ new Set();
8477
+ for (const choice of choices) {
8478
+ const comparable = normalizeChoiceLabelText(choice.displayName).toLowerCase();
8479
+ counts.set(comparable, (counts.get(comparable) ?? 0) + 1);
8480
+ originalLabels.add(comparable);
8481
+ }
8482
+ const usedLabels = /* @__PURE__ */ new Set();
8483
+ return choices.map((choice, index) => {
8484
+ const comparable = normalizeChoiceLabelText(choice.displayName).toLowerCase();
8485
+ if ((counts.get(comparable) ?? 0) < 2 && !usedLabels.has(comparable)) {
8486
+ usedLabels.add(comparable);
8487
+ return choice;
8488
+ }
8489
+ const ref = choiceRef(choice);
8490
+ let suffixLength = Math.min(6, ref.length);
8491
+ let suffix = ref.slice(-suffixLength);
8492
+ const maxAttempts = ref.length + originalLabels.size + usedLabels.size + 2;
8493
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
8494
+ const baseLength = Math.max(0, 119 - suffix.length);
8495
+ const base = choice.displayName.slice(0, baseLength).trimEnd();
8496
+ const displayName = base ? `${base} ${suffix}` : suffix.slice(-120);
8497
+ const finalComparable = normalizeChoiceLabelText(displayName).toLowerCase();
8498
+ if (!usedLabels.has(finalComparable) && !originalLabels.has(finalComparable)) {
8499
+ usedLabels.add(finalComparable);
8500
+ return {
8501
+ ...choice,
8502
+ displayName
8503
+ };
8504
+ }
8505
+ if (suffixLength < ref.length) {
8506
+ suffixLength = Math.min(ref.length, suffixLength + 2);
8507
+ suffix = ref.slice(-suffixLength);
8508
+ continue;
8509
+ }
8510
+ suffix = `${ref.slice(-Math.min(96, ref.length))}-${index + 1}-${attempt + 1}`;
8511
+ }
8512
+ throw new Error("legacy_team_setup_roster_too_large");
8513
+ });
8514
+ }
8515
+ function requireCompleteMappingChoices(projectCount, choiceCount) {
8516
+ if (projectCount * choiceCount > MAX_TOTAL_PROJECT_MAPPING_CHOICES) throw new Error("legacy_team_setup_roster_too_large");
8517
+ }
8518
+ function createCachedSnapshotLoader(load, ttlMs, now = Date.now) {
8519
+ let cached = null;
8520
+ let inFlight = null;
8521
+ let generation = 0;
8522
+ const cachedLoad = () => {
8523
+ if (cached && now() < cached.expiresAt) return Promise.resolve(cached.snapshots);
8524
+ if (inFlight) return inFlight;
8525
+ const operationGeneration = generation;
8526
+ let operation;
8527
+ operation = load().then((snapshots) => {
8528
+ if (generation !== operationGeneration) return cachedLoad();
8529
+ cached = {
8530
+ expiresAt: now() + ttlMs,
8531
+ snapshots
8532
+ };
8533
+ return snapshots;
8534
+ }).finally(() => {
8535
+ if (inFlight === operation) inFlight = null;
8536
+ });
8537
+ inFlight = operation;
8538
+ return operation;
8539
+ };
8540
+ cachedLoad.invalidate = () => {
8541
+ generation += 1;
8542
+ cached = null;
8543
+ inFlight = null;
8544
+ };
8545
+ return cachedLoad;
8546
+ }
8547
+ function identityChoicesFromRows(rows, candidateRef, maxChoices = MAX_IDENTITY_CHOICES) {
8548
+ if (rows.length > maxChoices) throw new Error("legacy_team_setup_roster_too_large");
8549
+ const actorIds = rows.map((row) => row.actor_id);
8550
+ return disambiguateChoiceLabels(rows.map((row) => {
8551
+ const opaqueIdentityRef = requiredIdentityRef(candidateRef, row.actor_id);
8552
+ return {
8553
+ identityId: row.actor_id,
8554
+ identityRef: opaqueIdentityRef,
8555
+ displayName: safeChoiceLabel(row.display_name, "Person", actorIds, opaqueIdentityRef)
8556
+ };
8557
+ }), (choice) => choice.identityRef);
8558
+ }
8559
+ function identityChoices(store, candidateRef) {
8560
+ return identityChoicesFromRows(store.db.prepare(`SELECT actor_id, display_name FROM actors
8561
+ WHERE status = 'active' AND merged_into_actor_id IS NULL
8562
+ ORDER BY display_name, actor_id LIMIT ?`).all(501), candidateRef);
8563
+ }
8564
+ function completedDraftIdentityChoices(store, draft) {
8565
+ const actorIds = [...new Set(draft.devices.flatMap((device) => [
8566
+ device.existingIdentityId,
8567
+ device.suggestedIdentityId,
8568
+ device.targetIdentityId,
8569
+ device.expectation.kind === "existing" ? device.expectation.identityId : null
8570
+ ]))].filter((actorId) => actorId !== null);
8571
+ if (actorIds.length === 0) return [];
8572
+ const placeholders = actorIds.map(() => "?").join(", ");
8573
+ return identityChoicesFromRows(store.db.prepare(`SELECT actor_id, display_name FROM actors
8574
+ WHERE status = 'active' AND merged_into_actor_id IS NULL
8575
+ AND actor_id IN (${placeholders})
8576
+ ORDER BY display_name, actor_id`).all(...actorIds), draft.candidateRef, MAX_COMPLETED_IDENTITY_CHOICES);
8577
+ }
8578
+ function projectMappingChoices(store) {
8579
+ let candidates;
8580
+ try {
8581
+ candidates = listProjectScopeCandidates(store.db, {
8582
+ limit: null,
8583
+ maxScannedRows: MAX_PROJECT_MAPPING_SCAN_ROWS,
8584
+ maxMetadataRows: MAX_PROJECT_MAPPING_METADATA_ROWS,
8585
+ excludePeerReceived: true
8586
+ }).filter((candidate) => !candidate.read_only && isLegacyTeamSetupProjectMappingIdentity(candidate.workspace_identity));
8587
+ } catch (error) {
8588
+ if (error instanceof Error && (error.message === "project_scope_candidate_scan_too_large" || error.message === "project_scope_candidate_metadata_too_large")) throw new Error("legacy_team_setup_roster_too_large");
8589
+ throw error;
8590
+ }
8591
+ if (candidates.length > MAX_PROJECT_MAPPING_CHOICES) throw new Error("legacy_team_setup_roster_too_large");
8592
+ return candidates.map((candidate) => ({
8593
+ projectIdentity: candidate.workspace_identity,
8594
+ sourceDisplayName: candidate.display_project
8595
+ }));
8596
+ }
8597
+ function viewerSafeDraft(store, draft) {
8598
+ if (draft.devices.length > MAX_DEVICES || draft.projects.length > MAX_PROJECTS) throw new Error("legacy_team_setup_roster_too_large");
8599
+ const completed = draft.state === "completed";
8600
+ const identities = completed ? completedDraftIdentityChoices(store, draft) : identityChoices(store, draft.candidateRef);
8601
+ const mappableProjectCount = completed ? 0 : draft.projects.filter((project) => project.resolution !== "deterministic").length;
8602
+ const mappingChoices = mappableProjectCount === 0 ? [] : projectMappingChoices(store);
8603
+ requireCompleteMappingChoices(mappableProjectCount, mappingChoices.length);
8604
+ const mappingChoiceIdentities = mappingChoices.map((choice) => choice.projectIdentity);
8605
+ return {
8606
+ devices: draft.devices.map((device) => ({
8607
+ deviceRef: device.deviceRef,
8608
+ displayName: device.displayName,
8609
+ enabled: device.enabled,
8610
+ existingIdentityRef: identityRef(draft.candidateRef, device.existingIdentityId),
8611
+ suggestedIdentityRef: identityRef(draft.candidateRef, device.suggestedIdentityId),
8612
+ verifiedEvidenceKind: device.verifiedEvidenceKind,
8613
+ decision: device.decision,
8614
+ targetIdentityRef: identityRef(draft.candidateRef, device.targetIdentityId),
8615
+ expectation: device.expectation.kind === "existing" ? {
8616
+ kind: "existing",
8617
+ assignmentVersion: device.expectation.assignmentVersion,
8618
+ identityRef: requiredIdentityRef(draft.candidateRef, device.expectation.identityId)
8619
+ } : { kind: "absent" }
8620
+ })),
8621
+ projects: draft.projects.map((project) => ({
8622
+ projectRef: project.projectRef,
8623
+ displayName: project.displayName,
8624
+ resolution: project.resolution,
8625
+ canonicalProjectRef: project.canonicalProjectRef,
8626
+ resolvedProjectRef: project.resolvedProjectRef,
8627
+ mappingChoices: project.resolution === "deterministic" ? [] : disambiguateChoiceLabels(mappingChoices.map((choice) => {
8628
+ const opaqueResolvedProjectRef = legacyTeamResolvedProjectRef(project.projectRef, choice.projectIdentity);
8629
+ return {
8630
+ resolvedProjectRef: opaqueResolvedProjectRef,
8631
+ displayName: safeChoiceLabel(choice.sourceDisplayName, "Project", mappingChoiceIdentities, opaqueResolvedProjectRef)
8632
+ };
8633
+ }), (choice) => choice.resolvedProjectRef)
8634
+ })),
8635
+ identityChoices: identities.map(({ identityRef, displayName }) => ({
8636
+ identityRef,
8637
+ displayName
8638
+ }))
8639
+ };
8640
+ }
8641
+ function errorStatus(code) {
8642
+ if (code === "team_setup_roster_unavailable") return 503;
8643
+ if (code === "team_setup_failed") return 503;
8644
+ if (code === "team_setup_incomplete") return 400;
8645
+ return 409;
8646
+ }
8647
+ function apiErrorCode(error) {
8648
+ const code = legacyTeamSetupApiErrorCode(error);
8649
+ return code === "team_setup_projection_changed" ? "team_setup_conflict" : code;
8650
+ }
8651
+ async function parseBoundedJsonObject(c) {
8652
+ if (c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return { ok: false };
8653
+ const lengthHeader = c.req.header("content-length");
8654
+ if (lengthHeader != null && !/^\d+$/u.test(lengthHeader.trim())) return { ok: false };
8655
+ const contentLength = lengthHeader == null ? null : Number(lengthHeader);
8656
+ if (contentLength != null && contentLength > MAX_MUTATION_BODY_BYTES) return { ok: false };
8657
+ const body = c.req.raw.body;
8658
+ if (!body) return { ok: false };
8659
+ const reader = body.getReader();
8660
+ const chunks = [];
8661
+ let total = 0;
8662
+ try {
8663
+ while (true) {
8664
+ const { done, value } = await reader.read();
8665
+ if (done) break;
8666
+ if (!value) continue;
8667
+ total += value.byteLength;
8668
+ if (total > MAX_MUTATION_BODY_BYTES) {
8669
+ await reader.cancel().catch(() => void 0);
8670
+ return { ok: false };
8671
+ }
8672
+ chunks.push(value);
8673
+ }
8674
+ } catch {
8675
+ return { ok: false };
8676
+ } finally {
8677
+ reader.releaseLock();
8678
+ }
8679
+ try {
8680
+ const bytes = new Uint8Array(total);
8681
+ let offset = 0;
8682
+ for (const chunk of chunks) {
8683
+ bytes.set(chunk, offset);
8684
+ offset += chunk.byteLength;
8685
+ }
8686
+ const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
8687
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? {
8688
+ ok: true,
8689
+ value: parsed
8690
+ } : { ok: false };
8691
+ } catch {
8692
+ return { ok: false };
8693
+ }
8694
+ }
8695
+ function hasExactKeys(value, keys) {
8696
+ const actual = Object.keys(value).toSorted();
8697
+ const expected = keys.toSorted();
8698
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
8699
+ }
8700
+ function mutationResponse(draft) {
8701
+ return {
8702
+ version: TEAM_SETUP_VERSION,
8703
+ candidateRef: draft.candidateRef,
8704
+ attemptId: draft.attemptId,
8705
+ draftState: draft.state,
8706
+ canFinish: draft.canFinish,
8707
+ unresolvedDeviceCount: draft.unresolvedDeviceCount,
8708
+ unresolvedProjectCount: draft.unresolvedProjectCount
8709
+ };
8710
+ }
8711
+ function finishResponse(candidateRef, result) {
8712
+ return {
8713
+ version: TEAM_SETUP_VERSION,
8714
+ status: result.status,
8715
+ teamRef: teamRef(candidateRef, result.teamId),
8716
+ attemptId: result.attemptId,
8717
+ accessDeltaDigest: result.accessDeltaDigest,
8718
+ completedAt: result.completedAt
8719
+ };
8720
+ }
8721
+ function teamSetupRoutes(options) {
8722
+ const app = new Hono();
8723
+ const loadSnapshots = options.loadLegacyTeamConfiguredGroupSnapshots ?? loadConfiguredLegacyTeamGroupSnapshots;
8724
+ async function loadedSnapshots(candidateRef) {
8725
+ try {
8726
+ return await loadSnapshots(candidateRef ? { candidateRef } : void 0);
8727
+ } catch (error) {
8728
+ if (error instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
8729
+ throw safeCoordinatorError();
8730
+ }
8731
+ }
8732
+ const loadedSummarySnapshots = createCachedSnapshotLoader(() => loadedSnapshots(), SUMMARY_SNAPSHOT_CACHE_TTL_MS);
8733
+ async function loadedCandidateSnapshots(candidateRef) {
8734
+ loadedSummarySnapshots.invalidate();
8735
+ try {
8736
+ return await loadedSnapshots(candidateRef);
8737
+ } finally {
8738
+ loadedSummarySnapshots.invalidate();
8739
+ }
8740
+ }
8741
+ app.get("/api/sync/team-setup/v1", async (c) => {
8742
+ let groups;
8743
+ try {
8744
+ groups = await loadedSummarySnapshots();
8745
+ } catch (error) {
8746
+ const code = apiErrorCode(error);
8747
+ return c.json({ error: code }, errorStatus(code));
8748
+ }
8749
+ try {
8750
+ const response = {
8751
+ version: TEAM_SETUP_VERSION,
8752
+ candidates: discoverCandidates(options.getStore(), groups).map(candidateSummary)
8753
+ };
8754
+ return c.json(response);
8755
+ } catch (error) {
8756
+ const code = apiErrorCode(error);
8757
+ return c.json({ error: code }, errorStatus(code));
8758
+ }
8759
+ });
8760
+ app.get("/api/sync/team-setup/v1/:candidateRef", async (c) => {
8761
+ const candidateRef = c.req.param("candidateRef");
8762
+ if (!CANDIDATE_REF_PATTERN.test(candidateRef)) return c.json({ error: "team_setup_incomplete" }, 400);
8763
+ try {
8764
+ const store = options.getStore();
8765
+ let draft = getLegacyTeamSetupDraft(store.db, candidateRef);
8766
+ let candidate;
8767
+ if (draft?.state === "completed" && isLegacyTeamCandidateSelectable(store.db, candidateRef, { projects: legacyTeamCandidateProjectInventory(store.db, projectionOptions(store), candidateRef) })) candidate = completedCandidateFromDraft(draft);
8768
+ else {
8769
+ const discoveredCandidate = discoverCandidates(store, (await loadedCandidateSnapshots(candidateRef)).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef)).find((item) => item.candidateRef === candidateRef);
8770
+ if (!discoveredCandidate) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8771
+ candidate = discoveredCandidate;
8772
+ draft = getLegacyTeamSetupDraft(store.db, candidateRef);
8773
+ }
8774
+ if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8775
+ let conflictState = null;
8776
+ let preview = null;
8777
+ if (draft.state !== "completed") try {
8778
+ preview = previewLegacyTeamSetupActivation(store.db, {
8779
+ candidateRef,
8780
+ attemptId: draft.attemptId
8781
+ });
8782
+ } catch (error) {
8783
+ const code = apiErrorCode(error);
8784
+ if (code === "team_setup_failed") return c.json({ error: code }, 503);
8785
+ preview = null;
8786
+ conflictState = code;
8787
+ }
8788
+ if (preview) requireBoundedAccessDelta(preview.accessDelta);
8789
+ const viewDraft = conflictState ? getLegacyTeamSetupDraft(store.db, candidateRef) ?? draft : draft;
8790
+ const safeDraft = viewerSafeDraft(store, viewDraft);
8791
+ const responseBase = {
8792
+ version: TEAM_SETUP_VERSION,
8793
+ candidate: candidateSummaryForDraft(candidate, viewDraft),
8794
+ attemptId: viewDraft.attemptId,
8795
+ draftState: viewDraft.state,
8796
+ unresolvedDeviceCount: viewDraft.unresolvedDeviceCount,
8797
+ unresolvedProjectCount: viewDraft.unresolvedProjectCount,
8798
+ ...safeDraft
8799
+ };
8800
+ if (!preview) {
8801
+ const response = {
8802
+ ...responseBase,
8803
+ canFinish: false,
8804
+ conflictState
8805
+ };
8806
+ return c.json(response);
8807
+ }
8808
+ const accessDelta = viewerSafeAccessDelta(candidateRef, preview.accessDelta, {
8809
+ teamDisplayName: viewDraft.displayName,
8810
+ ...safeDraft
8811
+ });
8812
+ const response = {
8813
+ ...responseBase,
8814
+ canFinish: true,
8815
+ conflictState: null,
8816
+ finishDigest: preview.finishDigest,
8817
+ accessDeltaDigest: preview.accessDeltaDigest,
8818
+ viewerAccessDeltaDigest: viewerAccessDeltaDigest(accessDelta),
8819
+ accessDelta
8820
+ };
8821
+ return c.json(response);
8822
+ } catch (error) {
8823
+ const code = apiErrorCode(error);
8824
+ return c.json({ error: code }, errorStatus(code));
8825
+ }
8826
+ });
8827
+ app.put("/api/sync/team-setup/v1/:candidateRef/devices/:deviceRef/assignment", async (c) => {
8828
+ const candidateRef = c.req.param("candidateRef");
8829
+ const deviceRef = c.req.param("deviceRef");
8830
+ if (!CANDIDATE_REF_PATTERN.test(candidateRef) || !DEVICE_REF_PATTERN.test(deviceRef)) return c.json({ error: "team_setup_incomplete" }, 400);
8831
+ const parsed = await parseBoundedJsonObject(c);
8832
+ if (!parsed.ok || !hasExactKeys(parsed.value, [
8833
+ "attemptId",
8834
+ "expectation",
8835
+ "targetIdentityRef"
8836
+ ])) return c.json({ error: "team_setup_incomplete" }, 400);
8837
+ const { attemptId, expectation, targetIdentityRef } = parsed.value;
8838
+ if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId) || typeof targetIdentityRef !== "string" || !IDENTITY_REF_PATTERN.test(targetIdentityRef) || !expectation || typeof expectation !== "object" || Array.isArray(expectation)) return c.json({ error: "team_setup_incomplete" }, 400);
8839
+ try {
8840
+ const store = options.getStore();
8841
+ const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
8842
+ if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8843
+ if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
8844
+ const device = draft.devices.find((item) => item.deviceRef === deviceRef);
8845
+ if (!device) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8846
+ const submittedExpectation = expectation;
8847
+ if (!(device.expectation.kind === "absent" ? hasExactKeys(submittedExpectation, ["kind"]) && submittedExpectation.kind === "absent" : hasExactKeys(submittedExpectation, [
8848
+ "assignmentVersion",
8849
+ "identityRef",
8850
+ "kind"
8851
+ ]) && submittedExpectation.kind === "existing" && submittedExpectation.assignmentVersion === device.expectation.assignmentVersion && submittedExpectation.identityRef === requiredIdentityRef(candidateRef, device.expectation.identityId))) return c.json({ error: "team_setup_assignment_changed" }, 409);
8852
+ const target = identityChoices(store, candidateRef).find((choice) => choice.identityRef === targetIdentityRef);
8853
+ if (!target) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8854
+ return c.json(mutationResponse(setLegacyTeamSetupDeviceAssignment(store.db, {
8855
+ attemptId,
8856
+ deviceRef,
8857
+ targetIdentityId: target.identityId,
8858
+ expectation: device.expectation
8859
+ })));
8860
+ } catch (error) {
8861
+ const code = apiErrorCode(error);
8862
+ return c.json({ error: code }, errorStatus(code));
8863
+ }
8864
+ });
8865
+ app.put("/api/sync/team-setup/v1/:candidateRef/devices/:deviceRef/decision", async (c) => {
8866
+ const candidateRef = c.req.param("candidateRef");
8867
+ const deviceRef = c.req.param("deviceRef");
8868
+ if (!CANDIDATE_REF_PATTERN.test(candidateRef) || !DEVICE_REF_PATTERN.test(deviceRef)) return c.json({ error: "team_setup_incomplete" }, 400);
8869
+ const parsed = await parseBoundedJsonObject(c);
8870
+ if (!parsed.ok) return c.json({ error: "team_setup_incomplete" }, 400);
8871
+ const { attemptId, decision, expectedTargetIdentityRef } = parsed.value;
8872
+ if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId) || ![
8873
+ "included",
8874
+ "excluded",
8875
+ "removed"
8876
+ ].includes(decision) || (decision === "included" ? !hasExactKeys(parsed.value, [
8877
+ "attemptId",
8878
+ "decision",
8879
+ "expectedTargetIdentityRef"
8880
+ ]) || typeof expectedTargetIdentityRef !== "string" || !IDENTITY_REF_PATTERN.test(expectedTargetIdentityRef) : !hasExactKeys(parsed.value, ["attemptId", "decision"]))) return c.json({ error: "team_setup_incomplete" }, 400);
8881
+ try {
8882
+ const store = options.getStore();
8883
+ const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
8884
+ if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8885
+ if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
8886
+ const device = draft.devices.find((item) => item.deviceRef === deviceRef);
8887
+ if (!device) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8888
+ if (decision === "included" && (device.targetIdentityId === null || requiredIdentityRef(candidateRef, device.targetIdentityId) !== expectedTargetIdentityRef)) return c.json({ error: "team_setup_assignment_changed" }, 409);
8889
+ return c.json(mutationResponse(setLegacyTeamSetupDeviceDecision(store.db, {
8890
+ attemptId,
8891
+ deviceRef,
8892
+ decision
8893
+ })));
8894
+ } catch (error) {
8895
+ const code = apiErrorCode(error);
8896
+ return c.json({ error: code }, errorStatus(code));
8897
+ }
8898
+ });
8899
+ app.delete("/api/sync/team-setup/v1/:candidateRef/devices/:deviceRef/decision", async (c) => {
8900
+ const candidateRef = c.req.param("candidateRef");
8901
+ const deviceRef = c.req.param("deviceRef");
8902
+ if (!CANDIDATE_REF_PATTERN.test(candidateRef) || !DEVICE_REF_PATTERN.test(deviceRef)) return c.json({ error: "team_setup_incomplete" }, 400);
8903
+ const parsed = await parseBoundedJsonObject(c);
8904
+ if (!parsed.ok || !hasExactKeys(parsed.value, ["attemptId"])) return c.json({ error: "team_setup_incomplete" }, 400);
8905
+ const { attemptId } = parsed.value;
8906
+ if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId)) return c.json({ error: "team_setup_incomplete" }, 400);
8907
+ try {
8908
+ const store = options.getStore();
8909
+ const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
8910
+ if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8911
+ if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
8912
+ if (!draft.devices.some((item) => item.deviceRef === deviceRef)) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8913
+ return c.json(mutationResponse(clearLegacyTeamSetupDeviceDecision(store.db, {
8914
+ attemptId,
8915
+ deviceRef
8916
+ })));
8917
+ } catch (error) {
8918
+ const code = apiErrorCode(error);
8919
+ return c.json({ error: code }, errorStatus(code));
8920
+ }
8921
+ });
8922
+ app.put("/api/sync/team-setup/v1/:candidateRef/projects/:projectRef/mapping", async (c) => {
8923
+ const candidateRef = c.req.param("candidateRef");
8924
+ const projectRef = c.req.param("projectRef");
8925
+ if (!CANDIDATE_REF_PATTERN.test(candidateRef) || !PROJECT_REF_PATTERN.test(projectRef)) return c.json({ error: "team_setup_incomplete" }, 400);
8926
+ const parsed = await parseBoundedJsonObject(c);
8927
+ if (!parsed.ok || !hasExactKeys(parsed.value, ["attemptId", "resolvedProjectRef"])) return c.json({ error: "team_setup_incomplete" }, 400);
8928
+ const { attemptId, resolvedProjectRef } = parsed.value;
8929
+ if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId) || typeof resolvedProjectRef !== "string" || !RESOLVED_PROJECT_REF_PATTERN.test(resolvedProjectRef)) return c.json({ error: "team_setup_incomplete" }, 400);
8930
+ try {
8931
+ const store = options.getStore();
8932
+ const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
8933
+ if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8934
+ if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
8935
+ const project = draft.projects.find((item) => item.projectRef === projectRef);
8936
+ if (!project) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8937
+ if (project.resolution === "deterministic") return c.json({ error: "team_setup_incomplete" }, 400);
8938
+ const target = projectMappingChoices(store).find((choice) => legacyTeamResolvedProjectRef(projectRef, choice.projectIdentity) === resolvedProjectRef);
8939
+ if (!target) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8940
+ return c.json(mutationResponse(setLegacyTeamSetupProjectMapping(store.db, {
8941
+ attemptId,
8942
+ projectRef,
8943
+ resolvedProjectIdentity: target.projectIdentity
8944
+ })));
8945
+ } catch (error) {
8946
+ const code = apiErrorCode(error);
8947
+ return c.json({ error: code }, errorStatus(code));
8948
+ }
8949
+ });
8950
+ app.post("/api/sync/team-setup/v1/:candidateRef/refresh", async (c) => {
8951
+ const candidateRef = c.req.param("candidateRef");
8952
+ if (!CANDIDATE_REF_PATTERN.test(candidateRef)) return c.json({ error: "team_setup_incomplete" }, 400);
8953
+ const parsed = await parseBoundedJsonObject(c);
8954
+ if (!parsed.ok || !hasExactKeys(parsed.value, [])) return c.json({ error: "team_setup_incomplete" }, 400);
8955
+ let groups;
8956
+ try {
8957
+ groups = await loadedCandidateSnapshots(candidateRef);
8958
+ } catch (error) {
8959
+ const code = apiErrorCode(error);
8960
+ return c.json({ error: code }, errorStatus(code));
8961
+ }
8962
+ if (groups.filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef).length === 0) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8963
+ try {
8964
+ const store = options.getStore();
8965
+ return c.json(mutationResponse(refreshLegacyTeamCandidate(store.db, {
8966
+ projection: projectionOptions(store),
8967
+ groups
8968
+ }, candidateRef)));
8969
+ } catch (error) {
8970
+ const code = apiErrorCode(error);
8971
+ return c.json({ error: code }, errorStatus(code));
8972
+ }
8973
+ });
8974
+ app.post("/api/sync/team-setup/v1/:candidateRef/finish", async (c) => {
8975
+ const candidateRef = c.req.param("candidateRef");
8976
+ if (!CANDIDATE_REF_PATTERN.test(candidateRef)) return c.json({ error: "team_setup_incomplete" }, 400);
8977
+ const parsed = await parseBoundedJsonObject(c);
8978
+ if (!parsed.ok) return c.json({ error: "team_setup_incomplete" }, 400);
8979
+ const finishKeys = [
8980
+ "attemptId",
8981
+ "confirmedAccessDeltaDigest",
8982
+ "confirmedViewerAccessDeltaDigest",
8983
+ "finishDigest"
8984
+ ];
8985
+ if (!hasExactKeys(parsed.value, finishKeys)) return Object.keys(parsed.value).every((key) => finishKeys.includes(key)) ? c.json({ error: "team_setup_confirmation_stale" }, 409) : c.json({ error: "team_setup_incomplete" }, 400);
8986
+ const { attemptId, confirmedAccessDeltaDigest, confirmedViewerAccessDeltaDigest, finishDigest } = parsed.value;
8987
+ if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId) || typeof finishDigest !== "string" || !FINISH_DIGEST_PATTERN.test(finishDigest) || typeof confirmedAccessDeltaDigest !== "string" || !ACCESS_DELTA_DIGEST_PATTERN.test(confirmedAccessDeltaDigest) || typeof confirmedViewerAccessDeltaDigest !== "string" || !VIEWER_ACCESS_DELTA_DIGEST_PATTERN.test(confirmedViewerAccessDeltaDigest)) return c.json({ error: "team_setup_incomplete" }, 400);
8988
+ try {
8989
+ const store = options.getStore();
8990
+ const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
8991
+ if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
8992
+ if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
8993
+ if (draft.state !== "completed") {
8994
+ const preview = previewLegacyTeamSetupActivation(store.db, {
8995
+ candidateRef,
8996
+ attemptId: draft.attemptId
8997
+ });
8998
+ requireBoundedAccessDelta(preview.accessDelta);
8999
+ const safeDraft = viewerSafeDraft(store, draft);
9000
+ if (viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, preview.accessDelta, {
9001
+ teamDisplayName: draft.displayName,
9002
+ ...safeDraft
9003
+ })) !== confirmedViewerAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
9004
+ }
9005
+ const result = await finishLegacyTeamSetupActivation(store.db, {
9006
+ candidateRef,
9007
+ attemptId,
9008
+ finishDigest,
9009
+ confirmedAccessDeltaDigest,
9010
+ loadFreshRoster: async () => {
9011
+ const matching = (await loadedCandidateSnapshots(candidateRef)).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
9012
+ if (matching.length !== 1) throw safeCoordinatorError();
9013
+ return matching[0]?.devices ?? [];
9014
+ },
9015
+ loadProjectInventory: () => legacyTeamCandidateProjectInventory(store.db, projectionOptions(store), candidateRef),
9016
+ validateLockedPreview: (lockedPreview) => {
9017
+ requireBoundedAccessDelta(lockedPreview.accessDelta);
9018
+ const lockedDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
9019
+ if (!lockedDraft || lockedDraft.attemptId !== attemptId) return false;
9020
+ const safeDraft = viewerSafeDraft(store, lockedDraft);
9021
+ return viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, lockedPreview.accessDelta, {
9022
+ teamDisplayName: lockedDraft.displayName,
9023
+ ...safeDraft
9024
+ })) === confirmedViewerAccessDeltaDigest;
9025
+ }
9026
+ });
9027
+ return c.json(finishResponse(candidateRef, result));
9028
+ } catch (error) {
9029
+ const code = apiErrorCode(error);
9030
+ return c.json({ error: code }, errorStatus(code));
9031
+ }
9032
+ });
9033
+ return app;
9034
+ }
9035
+ //#endregion
7685
9036
  //#region src/routes/update-status.ts
7686
9037
  function updateStatusRoutes(options = {}) {
7687
9038
  const getUpdateStatus$1 = options.getUpdateStatus ?? getUpdateStatus;
@@ -7732,7 +9083,7 @@ function createApp(opts) {
7732
9083
  const getSyncRuntimeStatus = opts?.getSyncRuntimeStatus ?? (() => null);
7733
9084
  const app = new Hono();
7734
9085
  app.use("*", preflightHandler());
7735
- app.use("*", originGuard());
9086
+ app.use("*", originGuard({ unsafeGetPathPrefixes: [TEAM_SETUP_ROUTE_PREFIX] }));
7736
9087
  app.route("/", healthRoutes(storeFactory));
7737
9088
  app.route("/", statsRoutes(storeFactory));
7738
9089
  app.route("/", memoryRoutes(storeFactory));
@@ -7744,7 +9095,11 @@ function createApp(opts) {
7744
9095
  }));
7745
9096
  app.route("/", configRoutes({ getSweeper: () => sweeper }));
7746
9097
  app.route("/", rawEventsRoutes(storeFactory, sweeper));
7747
- app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus));
9098
+ app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus, { loadDeviceIdentityCoordinatorEvidence: opts?.loadDeviceIdentityCoordinatorEvidence }));
9099
+ app.route("/", teamSetupRoutes({
9100
+ getStore: storeFactory,
9101
+ loadLegacyTeamConfiguredGroupSnapshots: opts?.loadLegacyTeamConfiguredGroupSnapshots
9102
+ }));
7748
9103
  app.route("/", updateStatusRoutes({ getUpdateStatus: opts?.getUpdateStatus }));
7749
9104
  const staticRoot = process.env.CODEMEM_VIEWER_STATIC_DIR ?? join(import.meta.dirname ?? ".", "../static");
7750
9105
  app.use("/assets/*", async (c, next) => {