@codemem/server 0.41.0 → 0.42.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.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +890 -406
- package/dist/index.js.map +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/pack.d.ts.map +1 -1
- package/dist/routes/raw-events.d.ts.map +1 -1
- package/dist/routes/sync.d.ts +9 -2
- package/dist/routes/sync.d.ts.map +1 -1
- package/dist/routes/target-validation.d.ts +19 -0
- package/dist/routes/target-validation.d.ts.map +1 -0
- package/package.json +2 -2
- package/static/app.js +44 -44
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/controls.css +196 -0
- package/static/controls.css.br +0 -0
- package/static/controls.css.gz +0 -0
- package/static/index.html +87 -35
- package/static/themes.css +4 -0
- package/static/themes.css.br +0 -0
- package/static/themes.css.gz +0 -0
- package/static/tokens.css +2 -0
- package/static/tokens.css.br +0 -0
- package/static/tokens.css.gz +0 -0
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,
|
|
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, 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, 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, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inviteTokenDigest, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, 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, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, rejectInboundScopeFailures, requestJson, resolveDbPath, resolveProject, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, 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";
|
|
@@ -573,6 +573,102 @@ function queryBool(value) {
|
|
|
573
573
|
}
|
|
574
574
|
//#endregion
|
|
575
575
|
//#region src/routes/memory.ts
|
|
576
|
+
var MAX_FEED_QUERY_CODE_UNITS = 256;
|
|
577
|
+
var searchFunctionsRegisteredStores = /* @__PURE__ */ new WeakSet();
|
|
578
|
+
function casefold(value) {
|
|
579
|
+
return String(value ?? "").toLowerCase();
|
|
580
|
+
}
|
|
581
|
+
function normalizeMemorySearchQuery(value) {
|
|
582
|
+
return casefold(value?.trim()).slice(0, MAX_FEED_QUERY_CODE_UNITS) || void 0;
|
|
583
|
+
}
|
|
584
|
+
function ensureMemorySearchFunctions(store) {
|
|
585
|
+
if (searchFunctionsRegisteredStores.has(store)) return;
|
|
586
|
+
store.db.function("codemem_casefold", { deterministic: true }, casefold);
|
|
587
|
+
store.db.function("codemem_project_basename", { deterministic: true }, (value) => projectBasename(String(value ?? "").trim()));
|
|
588
|
+
searchFunctionsRegisteredStores.add(store);
|
|
589
|
+
}
|
|
590
|
+
var IDENTITY_LOOKUP_BATCH_SIZE = 500;
|
|
591
|
+
function identityValue(item, key) {
|
|
592
|
+
const direct = String(item[key] ?? "").trim();
|
|
593
|
+
if (direct) return direct;
|
|
594
|
+
const rawMetadata = item.metadata ?? item.metadata_json;
|
|
595
|
+
const metadata = typeof rawMetadata === "string" ? fromJson(rawMetadata) : rawMetadata;
|
|
596
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return "";
|
|
597
|
+
return String(metadata[key] ?? "").trim();
|
|
598
|
+
}
|
|
599
|
+
function uniqueStrings(items, key) {
|
|
600
|
+
return [...new Set(items.map((item) => identityValue(item, key)).filter(Boolean))];
|
|
601
|
+
}
|
|
602
|
+
function batchedLookup(store, values, query) {
|
|
603
|
+
const rows = [];
|
|
604
|
+
for (let offset = 0; offset < values.length; offset += IDENTITY_LOOKUP_BATCH_SIZE) {
|
|
605
|
+
const batch = values.slice(offset, offset + IDENTITY_LOOKUP_BATCH_SIZE);
|
|
606
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
607
|
+
rows.push(...store.db.prepare(query(placeholders)).all(...batch));
|
|
608
|
+
}
|
|
609
|
+
return rows;
|
|
610
|
+
}
|
|
611
|
+
function humanName(value) {
|
|
612
|
+
if (typeof value !== "string") return void 0;
|
|
613
|
+
try {
|
|
614
|
+
return normalizeHumanPresentationName(value, "display_name");
|
|
615
|
+
} catch {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Add presentation-only identity labels without changing persisted provenance.
|
|
621
|
+
* Resolved names are display hints, not authority or ownership evidence; legacy
|
|
622
|
+
* metadata receives the same top-level-first fallback as existing read paths.
|
|
623
|
+
*/
|
|
624
|
+
function attachResolvedIdentityFieldsUnsafe(store, items) {
|
|
625
|
+
const actorIds = uniqueStrings(items, "actor_id");
|
|
626
|
+
const deviceIds = uniqueStrings(items, "origin_device_id");
|
|
627
|
+
const peers = batchedLookup(store, deviceIds, (placeholders) => `SELECT peer_device_id, name, actor_id FROM sync_peers
|
|
628
|
+
WHERE peer_device_id IN (${placeholders})
|
|
629
|
+
AND TRIM(COALESCE(pinned_fingerprint, '')) <> ''`);
|
|
630
|
+
const peerByDevice = new Map(peers.map((peer) => [peer.peer_device_id, peer]));
|
|
631
|
+
const peerActorIds = peers.map((peer) => String(peer.actor_id ?? "").trim()).filter((actorId) => actorId.length > 0);
|
|
632
|
+
const actorRows = batchedLookup(store, [...new Set([...actorIds, ...peerActorIds])], (placeholders) => `SELECT actor_id, display_name, status, merged_into_actor_id FROM actors
|
|
633
|
+
WHERE actor_id IN (${placeholders})`);
|
|
634
|
+
const devices = batchedLookup(store, deviceIds, (placeholders) => `SELECT device_id, display_name FROM identity_devices
|
|
635
|
+
WHERE device_id IN (${placeholders}) AND status = 'active'`);
|
|
636
|
+
const actorById = new Map(actorRows.map((actor) => [actor.actor_id, actor]));
|
|
637
|
+
let pendingMergeTargets = [...new Set(actorRows.map((actor) => String(actor.merged_into_actor_id ?? "").trim()).filter((actorId) => actorId && !actorById.has(actorId)))];
|
|
638
|
+
while (pendingMergeTargets.length > 0) {
|
|
639
|
+
const targetRows = batchedLookup(store, pendingMergeTargets, (placeholders) => `SELECT actor_id, display_name, status, merged_into_actor_id FROM actors
|
|
640
|
+
WHERE actor_id IN (${placeholders})`);
|
|
641
|
+
for (const actor of targetRows) actorById.set(actor.actor_id, actor);
|
|
642
|
+
pendingMergeTargets = [...new Set(targetRows.map((actor) => String(actor.merged_into_actor_id ?? "").trim()).filter((actorId) => actorId && !actorById.has(actorId)))];
|
|
643
|
+
}
|
|
644
|
+
const resolvedActorName = (actorId) => {
|
|
645
|
+
const seen = /* @__PURE__ */ new Set();
|
|
646
|
+
let currentActorId = actorId;
|
|
647
|
+
while (currentActorId && !seen.has(currentActorId)) {
|
|
648
|
+
seen.add(currentActorId);
|
|
649
|
+
const actor = actorById.get(currentActorId);
|
|
650
|
+
if (!actor) return void 0;
|
|
651
|
+
if (actor.status === "active" && actor.merged_into_actor_id === null) return humanName(actor.display_name);
|
|
652
|
+
currentActorId = String(actor.merged_into_actor_id ?? "").trim();
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
const deviceNameById = new Map(devices.map((device) => [device.device_id, humanName(device.display_name)]).filter((entry) => entry[1] !== void 0));
|
|
656
|
+
for (const item of items) {
|
|
657
|
+
const actorId = identityValue(item, "actor_id");
|
|
658
|
+
const deviceId = identityValue(item, "origin_device_id");
|
|
659
|
+
const peer = peerByDevice.get(deviceId);
|
|
660
|
+
const peerActorId = String(peer?.actor_id ?? "").trim();
|
|
661
|
+
const actorName = actorById.has(actorId) ? resolvedActorName(actorId) ?? humanName(item.actor_display_name) : resolvedActorName(peerActorId) ?? humanName(item.actor_display_name);
|
|
662
|
+
const deviceName = deviceNameById.get(deviceId) ?? humanName(peer?.name);
|
|
663
|
+
if (actorName) item.resolved_actor_display_name = actorName;
|
|
664
|
+
if (deviceName) item.resolved_device_display_name = deviceName;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function attachResolvedIdentityFields(store, items) {
|
|
668
|
+
try {
|
|
669
|
+
attachResolvedIdentityFieldsUnsafe(store, items);
|
|
670
|
+
} catch {}
|
|
671
|
+
}
|
|
576
672
|
function serializeMemoryRow(ownedBySelf, row) {
|
|
577
673
|
const metadata = fromJson(row.metadata_json ?? null);
|
|
578
674
|
const owned_by_self = ownedBySelf(row);
|
|
@@ -651,12 +747,49 @@ function summaryLikeSqlPredicate() {
|
|
|
651
747
|
OR (
|
|
652
748
|
json_valid(COALESCE(memory_items.metadata_json, ''))
|
|
653
749
|
AND (
|
|
654
|
-
COALESCE(
|
|
750
|
+
COALESCE(json_type(memory_items.metadata_json, '$.is_summary') = 'true', 0)
|
|
655
751
|
OR LOWER(TRIM(COALESCE(json_extract(memory_items.metadata_json, '$.source'), ''))) = 'observer_summary'
|
|
656
752
|
)
|
|
657
753
|
)
|
|
658
754
|
)`;
|
|
659
755
|
}
|
|
756
|
+
function memorySearchSql(query) {
|
|
757
|
+
const textClause = `INSTR(codemem_casefold(
|
|
758
|
+
COALESCE(memory_items.title, '') || CHAR(10) ||
|
|
759
|
+
COALESCE(memory_items.body_text, '') || CHAR(10) ||
|
|
760
|
+
CASE WHEN ${summaryLikeSqlPredicate()} THEN 'session_summary' ELSE
|
|
761
|
+
COALESCE(NULLIF(LOWER(TRIM(COALESCE(memory_items.kind, ''))), ''), 'change')
|
|
762
|
+
END || CHAR(10) ||
|
|
763
|
+
COALESCE(
|
|
764
|
+
memory_items.project,
|
|
765
|
+
codemem_project_basename(
|
|
766
|
+
(SELECT sessions.project FROM sessions WHERE sessions.id = memory_items.session_id)
|
|
767
|
+
),
|
|
768
|
+
''
|
|
769
|
+
) || CHAR(10) ||
|
|
770
|
+
COALESCE(memory_items.tags_text, '') || CHAR(10) ||
|
|
771
|
+
COALESCE(memory_items.facts, '') || CHAR(10) ||
|
|
772
|
+
COALESCE(memory_items.subtitle, '') || CHAR(10) ||
|
|
773
|
+
COALESCE(memory_items.narrative, '') || CHAR(10) ||
|
|
774
|
+
CASE WHEN json_valid(COALESCE(memory_items.metadata_json, '')) THEN
|
|
775
|
+
COALESCE(json_extract(memory_items.metadata_json, '$.request'), '') || CHAR(10) ||
|
|
776
|
+
COALESCE(json_extract(memory_items.metadata_json, '$.subtitle'), '') || CHAR(10) ||
|
|
777
|
+
COALESCE(json_extract(memory_items.metadata_json, '$.narrative'), '') || CHAR(10) ||
|
|
778
|
+
COALESCE(json_extract(memory_items.metadata_json, '$.facts'), '') || CHAR(10) ||
|
|
779
|
+
COALESCE(json_extract(memory_items.metadata_json, '$.summary'), '')
|
|
780
|
+
ELSE '' END
|
|
781
|
+
), codemem_casefold(?)) > 0`;
|
|
782
|
+
const parsedMemoryId = parsePositiveMemoryId(query);
|
|
783
|
+
const memoryId = parsedMemoryId != null && String(parsedMemoryId) === query ? parsedMemoryId : null;
|
|
784
|
+
if (memoryId != null) return {
|
|
785
|
+
clause: `(memory_items.id = ? OR ${textClause})`,
|
|
786
|
+
params: [memoryId, query]
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
clause: textClause,
|
|
790
|
+
params: [query]
|
|
791
|
+
};
|
|
792
|
+
}
|
|
660
793
|
function countVisibleObservationRows(store, filters) {
|
|
661
794
|
const filterResult = buildViewerMemoryFilters(store, filters);
|
|
662
795
|
const clauses = [
|
|
@@ -709,43 +842,32 @@ function countVisibleArtifactRows(store, project) {
|
|
|
709
842
|
return Number(row?.total ?? 0);
|
|
710
843
|
}
|
|
711
844
|
function queryMemoryPage(store, options) {
|
|
845
|
+
ensureMemorySearchFunctions(store);
|
|
712
846
|
const filters = {};
|
|
713
847
|
if (options.project) filters.project = options.project;
|
|
714
848
|
if (options.scope) filters.ownership_scope = options.scope;
|
|
715
849
|
const filterResult = buildViewerMemoryFilters(store, filters);
|
|
716
|
-
const
|
|
850
|
+
const summaryPredicate = summaryLikeSqlPredicate();
|
|
851
|
+
const clauses = [
|
|
852
|
+
"memory_items.active = 1",
|
|
853
|
+
options.classification === "summary" ? summaryPredicate : `NOT ${summaryPredicate}`,
|
|
854
|
+
...filterResult.clauses
|
|
855
|
+
];
|
|
856
|
+
const params = [...filterResult.params];
|
|
857
|
+
if (options.q) {
|
|
858
|
+
const search = memorySearchSql(options.q);
|
|
859
|
+
clauses.push(search.clause);
|
|
860
|
+
params.push(...search.params);
|
|
861
|
+
}
|
|
862
|
+
const where = clauses.join(" AND ");
|
|
717
863
|
const from = filterResult.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
|
|
718
864
|
const rows = store.db.prepare(`SELECT memory_items.* FROM ${from}
|
|
719
865
|
WHERE ${where}
|
|
720
|
-
ORDER BY memory_items.created_at DESC
|
|
721
|
-
LIMIT ? OFFSET ?`).all(...
|
|
866
|
+
ORDER BY memory_items.created_at DESC, memory_items.id DESC
|
|
867
|
+
LIMIT ? OFFSET ?`).all(...params, options.limit + 1, options.offset);
|
|
722
868
|
const ownedBySelf = store.buildOwnershipPredicate();
|
|
723
869
|
return rows.map((row) => serializeMemoryRow(ownedBySelf, row));
|
|
724
870
|
}
|
|
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
871
|
function memoryRoutes(getStore) {
|
|
750
872
|
const app = new Hono();
|
|
751
873
|
app.get("/api/sessions", (c) => {
|
|
@@ -794,17 +916,19 @@ function memoryRoutes(getStore) {
|
|
|
794
916
|
{
|
|
795
917
|
const limit = Math.max(1, queryInt(c.req.query("limit"), 20));
|
|
796
918
|
const offset = Math.max(0, queryInt(c.req.query("offset"), 0));
|
|
797
|
-
const items =
|
|
919
|
+
const items = queryMemoryPage(store, {
|
|
798
920
|
limit,
|
|
799
921
|
offset,
|
|
800
922
|
project: c.req.query("project") || void 0,
|
|
801
923
|
scope: normalizeScope(c.req.query("scope")),
|
|
802
|
-
|
|
924
|
+
q: normalizeMemorySearchQuery(c.req.query("q")),
|
|
925
|
+
classification: "observation"
|
|
803
926
|
});
|
|
804
927
|
const hasMore = items.length > limit;
|
|
805
928
|
const result = hasMore ? items.slice(0, limit) : items;
|
|
806
929
|
const asRecords = result;
|
|
807
930
|
attachSessionFields(store, asRecords);
|
|
931
|
+
attachResolvedIdentityFields(store, asRecords);
|
|
808
932
|
return c.json({
|
|
809
933
|
items: asRecords,
|
|
810
934
|
pagination: {
|
|
@@ -821,17 +945,19 @@ function memoryRoutes(getStore) {
|
|
|
821
945
|
{
|
|
822
946
|
const limit = Math.max(1, queryInt(c.req.query("limit"), 50));
|
|
823
947
|
const offset = Math.max(0, queryInt(c.req.query("offset"), 0));
|
|
824
|
-
const items =
|
|
948
|
+
const items = queryMemoryPage(store, {
|
|
825
949
|
limit,
|
|
826
950
|
offset,
|
|
827
951
|
project: c.req.query("project") || void 0,
|
|
828
952
|
scope: normalizeScope(c.req.query("scope")),
|
|
829
|
-
|
|
953
|
+
q: normalizeMemorySearchQuery(c.req.query("q")),
|
|
954
|
+
classification: "summary"
|
|
830
955
|
});
|
|
831
956
|
const hasMore = items.length > limit;
|
|
832
957
|
const result = hasMore ? items.slice(0, limit) : items;
|
|
833
958
|
const asRecords = result;
|
|
834
959
|
attachSessionFields(store, asRecords);
|
|
960
|
+
attachResolvedIdentityFields(store, asRecords);
|
|
835
961
|
return c.json({
|
|
836
962
|
items: asRecords,
|
|
837
963
|
pagination: {
|
|
@@ -926,6 +1052,7 @@ function memoryRoutes(getStore) {
|
|
|
926
1052
|
if (project) filters.project = project;
|
|
927
1053
|
const asRecords = store.recent(limit, filters);
|
|
928
1054
|
attachSessionFields(store, asRecords);
|
|
1055
|
+
attachResolvedIdentityFields(store, asRecords);
|
|
929
1056
|
return c.json({ items: asRecords });
|
|
930
1057
|
}
|
|
931
1058
|
});
|
|
@@ -1077,6 +1204,71 @@ function observerStatusRoutes(deps) {
|
|
|
1077
1204
|
return app;
|
|
1078
1205
|
}
|
|
1079
1206
|
//#endregion
|
|
1207
|
+
//#region src/routes/target-validation.ts
|
|
1208
|
+
var IDENTITY_TARGET_KEYS = new Set(VIEWER_IDENTITY_TARGET_KEYS);
|
|
1209
|
+
var BOOLEAN_IDENTITY_TARGET_KEYS = new Set(["actor_id_present", "embedding_disabled"]);
|
|
1210
|
+
function invalidRequest$1(message) {
|
|
1211
|
+
return {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
status: 400,
|
|
1214
|
+
body: { error: {
|
|
1215
|
+
code: "invalid_request",
|
|
1216
|
+
message
|
|
1217
|
+
} }
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function conflict(code, message) {
|
|
1221
|
+
return {
|
|
1222
|
+
ok: false,
|
|
1223
|
+
status: 409,
|
|
1224
|
+
body: { error: {
|
|
1225
|
+
code,
|
|
1226
|
+
message
|
|
1227
|
+
} }
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function isRecord$1(value) {
|
|
1231
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1232
|
+
}
|
|
1233
|
+
function currentIdentityTarget() {
|
|
1234
|
+
return buildViewerIdentityTarget();
|
|
1235
|
+
}
|
|
1236
|
+
function requestedDbMatches(store, value) {
|
|
1237
|
+
if (value == null) return true;
|
|
1238
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
1239
|
+
return resolve(resolveDbPath(value.trim())) === resolve(store.dbPath);
|
|
1240
|
+
}
|
|
1241
|
+
function requestedIdentityMatches(value) {
|
|
1242
|
+
if (value == null) return true;
|
|
1243
|
+
if (!isRecord$1(value)) return null;
|
|
1244
|
+
if (Object.keys(value).length !== IDENTITY_TARGET_KEYS.size || Object.keys(value).some((key) => !IDENTITY_TARGET_KEYS.has(key))) return "unsupported";
|
|
1245
|
+
const expected = currentIdentityTarget();
|
|
1246
|
+
for (const key of VIEWER_IDENTITY_TARGET_KEYS) {
|
|
1247
|
+
const requested = value[key];
|
|
1248
|
+
if (BOOLEAN_IDENTITY_TARGET_KEYS.has(key)) {
|
|
1249
|
+
if (typeof requested !== "boolean") return null;
|
|
1250
|
+
} else if (requested !== null && typeof requested !== "string") return null;
|
|
1251
|
+
if (requested !== expected[key]) return false;
|
|
1252
|
+
}
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
function validateViewerTarget(store, payload, options = {}) {
|
|
1256
|
+
if (options.requirePairedTargets) {
|
|
1257
|
+
const hasDbPath = Object.hasOwn(payload, "db_path");
|
|
1258
|
+
if (hasDbPath !== Object.hasOwn(payload, "identity_target")) return invalidRequest$1("db_path and identity_target must be provided together");
|
|
1259
|
+
if (hasDbPath && (payload.db_path == null || payload.identity_target == null)) return invalidRequest$1("db_path and identity_target must be valid when provided");
|
|
1260
|
+
}
|
|
1261
|
+
const dbMatches = requestedDbMatches(store, payload.db_path);
|
|
1262
|
+
if (dbMatches == null) return invalidRequest$1("db_path must be a non-empty string");
|
|
1263
|
+
if (!dbMatches) return conflict("viewer_db_mismatch", "viewer database does not match request");
|
|
1264
|
+
const identityMatches = requestedIdentityMatches(payload.identity_target);
|
|
1265
|
+
if (identityMatches === "unsupported") return conflict("viewer_contract_unsupported", "viewer request contract is incompatible");
|
|
1266
|
+
if (identityMatches == null) return invalidRequest$1("identity_target is invalid");
|
|
1267
|
+
if (!identityMatches) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
|
|
1268
|
+
if ((options.requireCurrentIdentity || payload.identity_target != null) && !store.hasCurrentIdentity()) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
|
|
1269
|
+
return { ok: true };
|
|
1270
|
+
}
|
|
1271
|
+
//#endregion
|
|
1080
1272
|
//#region src/routes/pack.ts
|
|
1081
1273
|
var MAX_LEDGER_PAYLOAD_BYTES = 16 * 1024;
|
|
1082
1274
|
var MAX_METADATA_FIELD_CHARS = 512;
|
|
@@ -1114,19 +1306,6 @@ var LEDGER_KEYS = new Set([
|
|
|
1114
1306
|
"db_path",
|
|
1115
1307
|
"identity_target"
|
|
1116
1308
|
]);
|
|
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
1309
|
var ATTEMPT_KEYS = new Set([
|
|
1131
1310
|
"attempt_id",
|
|
1132
1311
|
"started_at",
|
|
@@ -1154,12 +1333,6 @@ function invalidRequest(message) {
|
|
|
1154
1333
|
message
|
|
1155
1334
|
} };
|
|
1156
1335
|
}
|
|
1157
|
-
function viewerDbMismatch() {
|
|
1158
|
-
return { error: {
|
|
1159
|
-
code: "viewer_db_mismatch",
|
|
1160
|
-
message: "viewer database does not match request"
|
|
1161
|
-
} };
|
|
1162
|
-
}
|
|
1163
1336
|
function viewerIdentityMismatch() {
|
|
1164
1337
|
return { error: {
|
|
1165
1338
|
code: "viewer_identity_mismatch",
|
|
@@ -1185,47 +1358,6 @@ function normalizeWorkingSetPath(value) {
|
|
|
1185
1358
|
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) return null;
|
|
1186
1359
|
return normalized;
|
|
1187
1360
|
}
|
|
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
1361
|
function validateMetadataFields(payload, allowedKeys) {
|
|
1230
1362
|
if (Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_LEDGER_PAYLOAD_BYTES) return "ledger metadata exceeds 16384 bytes";
|
|
1231
1363
|
for (const key of Object.keys(payload)) {
|
|
@@ -1347,7 +1479,8 @@ function packTransportRoutes(getStore) {
|
|
|
1347
1479
|
if (!store.hasCurrentIdentity()) return c.json(viewerIdentityMismatch(), 409);
|
|
1348
1480
|
return c.json({
|
|
1349
1481
|
service: "codemem-viewer",
|
|
1350
|
-
protocol_version:
|
|
1482
|
+
protocol_version: PROMPT_TRANSPORT_PROTOCOL_RANGE.protocolVersion,
|
|
1483
|
+
min_supported_protocol_version: PROMPT_TRANSPORT_PROTOCOL_RANGE.minSupportedProtocolVersion,
|
|
1351
1484
|
db_path: resolve(store.dbPath),
|
|
1352
1485
|
identity_target: currentIdentityTarget()
|
|
1353
1486
|
});
|
|
@@ -1368,14 +1501,8 @@ function packTransportRoutes(getStore) {
|
|
|
1368
1501
|
}
|
|
1369
1502
|
try {
|
|
1370
1503
|
const store = getStore();
|
|
1371
|
-
const
|
|
1372
|
-
if (
|
|
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);
|
|
1504
|
+
const target = validateViewerTarget(store, parsed, { requireCurrentIdentity: true });
|
|
1505
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
1379
1506
|
if (!request.attempt) {
|
|
1380
1507
|
const pack = await store.buildMemoryPackAsync(request.context, request.limit, request.tokenBudget, request.filters, request.renderOptions);
|
|
1381
1508
|
return c.json(pack);
|
|
@@ -1412,14 +1539,8 @@ function packTransportRoutes(getStore) {
|
|
|
1412
1539
|
}
|
|
1413
1540
|
try {
|
|
1414
1541
|
const store = getStore();
|
|
1415
|
-
const
|
|
1416
|
-
if (
|
|
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);
|
|
1542
|
+
const target = validateViewerTarget(store, parsed, { requireCurrentIdentity: true });
|
|
1543
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
1423
1544
|
const outcome = dispatchLedger(store, parsed);
|
|
1424
1545
|
if (typeof outcome === "string") return c.json(invalidRequest(outcome), 400);
|
|
1425
1546
|
if (outcome.ok) return c.json(outcome);
|
|
@@ -1439,34 +1560,60 @@ function packTransportRoutes(getStore) {
|
|
|
1439
1560
|
* Raw events routes — GET & POST /api/raw-events, GET /api/raw-events/status,
|
|
1440
1561
|
* POST /api/claude-hooks, POST /api/codex-hooks.
|
|
1441
1562
|
*/
|
|
1442
|
-
var
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1563
|
+
var DEFAULT_MAX_RAW_EVENTS_BODY_BYTES = 1048576;
|
|
1564
|
+
function configuredMaxRawEventsBodyBytes() {
|
|
1565
|
+
const parsed = Number(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES?.trim() ?? "");
|
|
1566
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_RAW_EVENTS_BODY_BYTES;
|
|
1567
|
+
}
|
|
1568
|
+
var MAX_RAW_EVENTS_BODY_BYTES = configuredMaxRawEventsBodyBytes();
|
|
1569
|
+
function emptyTranscriptOutcomeCounts() {
|
|
1570
|
+
return {
|
|
1571
|
+
ok: 0,
|
|
1572
|
+
not_provided: 0,
|
|
1573
|
+
path_rejected: 0,
|
|
1574
|
+
unreadable: 0,
|
|
1575
|
+
no_complete_record: 0,
|
|
1576
|
+
no_assistant_record: 0
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
function createTranscriptDiagnostics() {
|
|
1580
|
+
const counts = {
|
|
1581
|
+
claude: emptyTranscriptOutcomeCounts(),
|
|
1582
|
+
codex: emptyTranscriptOutcomeCounts()
|
|
1583
|
+
};
|
|
1584
|
+
return {
|
|
1585
|
+
record(source, outcome) {
|
|
1586
|
+
counts[source][outcome] += 1;
|
|
1587
|
+
},
|
|
1588
|
+
snapshot() {
|
|
1589
|
+
return {
|
|
1590
|
+
scope: "legacy_compatibility_routes",
|
|
1591
|
+
counts: {
|
|
1592
|
+
claude: { ...counts.claude },
|
|
1593
|
+
codex: { ...counts.codex }
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function transcriptSkipResponse(outcome) {
|
|
1600
|
+
if (outcome !== null && outcome !== "ok") return {
|
|
1601
|
+
inserted: 0,
|
|
1602
|
+
skipped: 1,
|
|
1603
|
+
skip_reason: "transcript_unavailable",
|
|
1604
|
+
skip_detail: outcome
|
|
1605
|
+
};
|
|
1606
|
+
return {
|
|
1607
|
+
inserted: 0,
|
|
1608
|
+
skipped: 1,
|
|
1609
|
+
skip_reason: "unsupported_hook"
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
function claudeTranscriptRoot() {
|
|
1613
|
+
return join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "projects");
|
|
1614
|
+
}
|
|
1615
|
+
function codexTranscriptRoot() {
|
|
1616
|
+
return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions");
|
|
1470
1617
|
}
|
|
1471
1618
|
/**
|
|
1472
1619
|
* Parse and validate a JSON object body, enforcing size limits.
|
|
@@ -1479,16 +1626,43 @@ async function parseJsonObjectBody(c, maxBytes) {
|
|
|
1479
1626
|
error: "payload too large",
|
|
1480
1627
|
max_bytes: maxBytes
|
|
1481
1628
|
}, 413);
|
|
1482
|
-
let raw;
|
|
1629
|
+
let raw = "";
|
|
1483
1630
|
try {
|
|
1484
|
-
|
|
1631
|
+
const body = c.req.raw.body;
|
|
1632
|
+
if (body !== null) {
|
|
1633
|
+
const reader = body.getReader();
|
|
1634
|
+
const chunks = [];
|
|
1635
|
+
let totalBytes = 0;
|
|
1636
|
+
try {
|
|
1637
|
+
while (true) {
|
|
1638
|
+
const { done, value } = await reader.read();
|
|
1639
|
+
if (done) break;
|
|
1640
|
+
totalBytes += value.byteLength;
|
|
1641
|
+
if (totalBytes > maxBytes) {
|
|
1642
|
+
try {
|
|
1643
|
+
await reader.cancel();
|
|
1644
|
+
} catch {}
|
|
1645
|
+
return c.json({
|
|
1646
|
+
error: "payload too large",
|
|
1647
|
+
max_bytes: maxBytes
|
|
1648
|
+
}, 413);
|
|
1649
|
+
}
|
|
1650
|
+
chunks.push(value);
|
|
1651
|
+
}
|
|
1652
|
+
} finally {
|
|
1653
|
+
reader.releaseLock();
|
|
1654
|
+
}
|
|
1655
|
+
const bytes = new Uint8Array(totalBytes);
|
|
1656
|
+
let offset = 0;
|
|
1657
|
+
for (const chunk of chunks) {
|
|
1658
|
+
bytes.set(chunk, offset);
|
|
1659
|
+
offset += chunk.byteLength;
|
|
1660
|
+
}
|
|
1661
|
+
raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1662
|
+
}
|
|
1485
1663
|
} catch {
|
|
1486
1664
|
return c.json({ error: "invalid json" }, 400);
|
|
1487
1665
|
}
|
|
1488
|
-
if (Buffer.byteLength(raw, "utf-8") > maxBytes) return c.json({
|
|
1489
|
-
error: "payload too large",
|
|
1490
|
-
max_bytes: maxBytes
|
|
1491
|
-
}, 413);
|
|
1492
1666
|
let parsed;
|
|
1493
1667
|
try {
|
|
1494
1668
|
parsed = raw ? JSON.parse(raw) : {};
|
|
@@ -1499,13 +1673,78 @@ async function parseJsonObjectBody(c, maxBytes) {
|
|
|
1499
1673
|
return parsed;
|
|
1500
1674
|
}
|
|
1501
1675
|
/** Nudge the sweeper safely — never crashes the caller. */
|
|
1502
|
-
function nudgeSweeper(sweeper,
|
|
1503
|
-
try {
|
|
1504
|
-
|
|
1676
|
+
function nudgeSweeper(sweeper, sessions) {
|
|
1677
|
+
for (const session of sessions) try {
|
|
1678
|
+
sweeper?.nudge(session.streamId, session.source);
|
|
1679
|
+
} catch {}
|
|
1680
|
+
}
|
|
1681
|
+
async function flushBoundarySessions(sweeper, sessions) {
|
|
1682
|
+
for (const session of sessions) try {
|
|
1683
|
+
await sweeper?.flushBoundary(session.streamId, session.source);
|
|
1505
1684
|
} catch {}
|
|
1506
1685
|
}
|
|
1686
|
+
function isClaudeBoundaryEnvelope(envelope) {
|
|
1687
|
+
const record = envelope;
|
|
1688
|
+
if (String(record.source ?? "").trim().toLowerCase() !== "claude") return false;
|
|
1689
|
+
const payload = record.payload;
|
|
1690
|
+
if (payload == null || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
1691
|
+
const adapter = payload._adapter;
|
|
1692
|
+
if (adapter == null || typeof adapter !== "object" || Array.isArray(adapter)) return false;
|
|
1693
|
+
const meta = adapter.meta;
|
|
1694
|
+
if (meta == null || typeof meta !== "object" || Array.isArray(meta)) return false;
|
|
1695
|
+
const hookEventName = meta.hook_event_name;
|
|
1696
|
+
return hookEventName === "SessionEnd" || hookEventName === "Stop";
|
|
1697
|
+
}
|
|
1698
|
+
var SAFE_INGEST_VALIDATION_ERRORS = new Set([
|
|
1699
|
+
"source must be string",
|
|
1700
|
+
"source is required",
|
|
1701
|
+
"source must use 1-64 letters, digits, dots, underscores, or hyphens",
|
|
1702
|
+
"session_stream_id must be string",
|
|
1703
|
+
"session_id must be string",
|
|
1704
|
+
"stream_id must be string",
|
|
1705
|
+
"opencode_session_id must be string",
|
|
1706
|
+
"conflicting session id fields",
|
|
1707
|
+
"invalid session id",
|
|
1708
|
+
"event source conflicts with request source",
|
|
1709
|
+
"session id required",
|
|
1710
|
+
"event_type must be string",
|
|
1711
|
+
"event_type required",
|
|
1712
|
+
"event_type has invalid syntax",
|
|
1713
|
+
"event_id must be string",
|
|
1714
|
+
"event_id required",
|
|
1715
|
+
"event_id has invalid syntax",
|
|
1716
|
+
"event_seq must be int",
|
|
1717
|
+
"ts_wall_ms must be number",
|
|
1718
|
+
"ts_mono_ms must be number",
|
|
1719
|
+
"payload must be an object",
|
|
1720
|
+
"cwd must be string",
|
|
1721
|
+
"project must be string",
|
|
1722
|
+
"started_at must be string",
|
|
1723
|
+
"events must be a list",
|
|
1724
|
+
"event must be an object"
|
|
1725
|
+
]);
|
|
1726
|
+
function boundedIngestValidationMessage(error) {
|
|
1727
|
+
return SAFE_INGEST_VALIDATION_ERRORS.has(error.message) ? error.message : "invalid raw event request";
|
|
1728
|
+
}
|
|
1729
|
+
function boundedIngestErrorResponse(c, error) {
|
|
1730
|
+
if (error instanceof RawEventIngestValidationError) return c.json({ error: boundedIngestValidationMessage(error) }, 400);
|
|
1731
|
+
const response = { error: "internal server error" };
|
|
1732
|
+
if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = error instanceof Error ? error.message : String(error);
|
|
1733
|
+
return c.json(response, 500);
|
|
1734
|
+
}
|
|
1735
|
+
function untargetedPayload(payload) {
|
|
1736
|
+
const { db_path: _dbPath, identity_target: _identityTarget, ...body } = payload;
|
|
1737
|
+
return body;
|
|
1738
|
+
}
|
|
1739
|
+
async function ingestNormalizedEnvelope(store, sweeper, envelope, flushBoundary = false) {
|
|
1740
|
+
const result = ingestRawEvents(store, envelope);
|
|
1741
|
+
nudgeSweeper(sweeper, result.sessions);
|
|
1742
|
+
if (flushBoundary && isClaudeBoundaryEnvelope(envelope)) await flushBoundarySessions(sweeper, result.sessions);
|
|
1743
|
+
return result;
|
|
1744
|
+
}
|
|
1507
1745
|
function rawEventsRoutes(getStore, sweeper) {
|
|
1508
1746
|
const app = new Hono();
|
|
1747
|
+
const transcriptDiagnostics = createTranscriptDiagnostics();
|
|
1509
1748
|
app.get("/api/raw-events", (c) => {
|
|
1510
1749
|
const totals = getStore().rawEventBacklogTotals();
|
|
1511
1750
|
return c.json(totals);
|
|
@@ -1540,233 +1779,83 @@ function rawEventsRoutes(getStore, sweeper) {
|
|
|
1540
1779
|
available: true,
|
|
1541
1780
|
mode: "stream_queue",
|
|
1542
1781
|
max_body_bytes: MAX_RAW_EVENTS_BODY_BYTES
|
|
1543
|
-
}
|
|
1782
|
+
},
|
|
1783
|
+
transcript_diagnostics: transcriptDiagnostics.snapshot()
|
|
1544
1784
|
});
|
|
1545
1785
|
});
|
|
1546
1786
|
app.post("/api/raw-events", async (c) => {
|
|
1547
1787
|
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
1548
1788
|
if (result instanceof Response) return result;
|
|
1549
|
-
const payload = result;
|
|
1550
|
-
const store = getStore();
|
|
1551
1789
|
try {
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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);
|
|
1790
|
+
const store = getStore();
|
|
1791
|
+
const target = validateViewerTarget(store, result, { requirePairedTargets: true });
|
|
1792
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
1793
|
+
const ingestResult = await ingestNormalizedEnvelope(store, sweeper, untargetedPayload(result), c.req.header("x-codemem-boundary-flush") === "1");
|
|
1682
1794
|
return c.json({
|
|
1683
|
-
inserted,
|
|
1684
|
-
|
|
1795
|
+
inserted: ingestResult.inserted,
|
|
1796
|
+
skipped: ingestResult.skipped,
|
|
1797
|
+
received: ingestResult.received
|
|
1685
1798
|
});
|
|
1686
1799
|
} catch (err) {
|
|
1687
|
-
|
|
1688
|
-
if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = err.message;
|
|
1689
|
-
return c.json(response, 500);
|
|
1800
|
+
return boundedIngestErrorResponse(c, err);
|
|
1690
1801
|
}
|
|
1691
1802
|
});
|
|
1692
1803
|
app.post("/api/claude-hooks", async (c) => {
|
|
1693
1804
|
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
1694
1805
|
if (result instanceof Response) return result;
|
|
1695
|
-
const
|
|
1696
|
-
if (envelope === null) return c.json({
|
|
1697
|
-
inserted: 0,
|
|
1698
|
-
skipped: 1
|
|
1699
|
-
});
|
|
1700
|
-
const store = getStore();
|
|
1806
|
+
const payload = result;
|
|
1701
1807
|
try {
|
|
1702
|
-
const
|
|
1703
|
-
const
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
source,
|
|
1716
|
-
cwd: envelope.cwd,
|
|
1717
|
-
project: envelope.project,
|
|
1718
|
-
startedAt: envelope.started_at,
|
|
1719
|
-
lastSeenTsWallMs: envelope.ts_wall_ms
|
|
1808
|
+
const store = getStore();
|
|
1809
|
+
const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
|
|
1810
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
1811
|
+
let transcriptOutcome = null;
|
|
1812
|
+
const envelope = buildRawEventEnvelopeFromHook(untargetedPayload(payload), {
|
|
1813
|
+
transcriptPolicy: {
|
|
1814
|
+
trust: "restricted",
|
|
1815
|
+
approvedRoots: [claudeTranscriptRoot()]
|
|
1816
|
+
},
|
|
1817
|
+
onTranscriptOutcome: (outcome) => {
|
|
1818
|
+
transcriptOutcome = outcome;
|
|
1819
|
+
transcriptDiagnostics.record("claude", outcome);
|
|
1820
|
+
}
|
|
1720
1821
|
});
|
|
1721
|
-
|
|
1822
|
+
if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
|
|
1823
|
+
const ingestResult = await ingestNormalizedEnvelope(store, sweeper, envelope, c.req.header("x-codemem-boundary-flush") === "1");
|
|
1722
1824
|
return c.json({
|
|
1723
|
-
inserted: inserted
|
|
1724
|
-
skipped:
|
|
1825
|
+
inserted: ingestResult.inserted,
|
|
1826
|
+
skipped: ingestResult.skipped
|
|
1725
1827
|
});
|
|
1726
1828
|
} catch (err) {
|
|
1727
|
-
|
|
1728
|
-
if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = err.message;
|
|
1729
|
-
return c.json(response, 500);
|
|
1829
|
+
return boundedIngestErrorResponse(c, err);
|
|
1730
1830
|
}
|
|
1731
1831
|
});
|
|
1732
1832
|
app.post("/api/codex-hooks", async (c) => {
|
|
1733
1833
|
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
1734
1834
|
if (result instanceof Response) return result;
|
|
1735
|
-
const
|
|
1736
|
-
if (envelope === null) return c.json({
|
|
1737
|
-
inserted: 0,
|
|
1738
|
-
skipped: 1
|
|
1739
|
-
});
|
|
1740
|
-
const store = getStore();
|
|
1835
|
+
const payload = result;
|
|
1741
1836
|
try {
|
|
1742
|
-
const
|
|
1743
|
-
const
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
source,
|
|
1756
|
-
cwd: envelope.cwd,
|
|
1757
|
-
project: envelope.project,
|
|
1758
|
-
startedAt: envelope.started_at,
|
|
1759
|
-
lastSeenTsWallMs: envelope.ts_wall_ms
|
|
1837
|
+
const store = getStore();
|
|
1838
|
+
const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
|
|
1839
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
1840
|
+
let transcriptOutcome = null;
|
|
1841
|
+
const envelope = buildRawEventEnvelopeFromCodexHook(untargetedPayload(payload), {
|
|
1842
|
+
transcriptPolicy: {
|
|
1843
|
+
trust: "restricted",
|
|
1844
|
+
approvedRoots: [codexTranscriptRoot()]
|
|
1845
|
+
},
|
|
1846
|
+
onTranscriptOutcome: (outcome) => {
|
|
1847
|
+
transcriptOutcome = outcome;
|
|
1848
|
+
transcriptDiagnostics.record("codex", outcome);
|
|
1849
|
+
}
|
|
1760
1850
|
});
|
|
1761
|
-
|
|
1851
|
+
if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
|
|
1852
|
+
const ingestResult = await ingestNormalizedEnvelope(store, sweeper, envelope);
|
|
1762
1853
|
return c.json({
|
|
1763
|
-
inserted: inserted
|
|
1764
|
-
skipped:
|
|
1854
|
+
inserted: ingestResult.inserted,
|
|
1855
|
+
skipped: ingestResult.skipped
|
|
1765
1856
|
});
|
|
1766
1857
|
} catch (err) {
|
|
1767
|
-
|
|
1768
|
-
if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = err.message;
|
|
1769
|
-
return c.json(response, 500);
|
|
1858
|
+
return boundedIngestErrorResponse(c, err);
|
|
1770
1859
|
}
|
|
1771
1860
|
});
|
|
1772
1861
|
return app;
|
|
@@ -2426,7 +2515,13 @@ function projectInviteAcceptanceFailure(error) {
|
|
|
2426
2515
|
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
2516
|
project_invite_self_acceptance_forbidden: "The owner cannot accept this recipient invitation on the owner's device.",
|
|
2428
2517
|
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."
|
|
2518
|
+
project_invite_trust_state_invalid: "The owner's trust setup could not be verified. Ask the owner to review the share.",
|
|
2519
|
+
recipient_display_name_invalid: "Enter a human-readable Identity name instead of an internal identifier.",
|
|
2520
|
+
recipient_display_name_required: "Enter an Identity display name.",
|
|
2521
|
+
recipient_display_name_too_long: "Use an Identity display name with 120 characters or fewer.",
|
|
2522
|
+
device_display_name_invalid: "Enter a human-readable device name instead of an internal identifier.",
|
|
2523
|
+
device_display_name_required: "Enter a device display name.",
|
|
2524
|
+
device_display_name_too_long: "Use a device display name with 120 characters or fewer."
|
|
2430
2525
|
};
|
|
2431
2526
|
return {
|
|
2432
2527
|
error: Object.hasOwn(detailByCode, code) ? code : "project_invite_acceptance_failed",
|
|
@@ -2434,11 +2529,19 @@ function projectInviteAcceptanceFailure(error) {
|
|
|
2434
2529
|
};
|
|
2435
2530
|
}
|
|
2436
2531
|
var PROJECT_INVITE_TTL_HOURS = 168;
|
|
2532
|
+
var PROJECT_INVITE_OWNER_FALLBACK = "Project owner";
|
|
2437
2533
|
var PROJECT_INVITE_BODY_KEYS = new Set([
|
|
2438
2534
|
"teammate_name",
|
|
2439
2535
|
"project_ids",
|
|
2440
2536
|
"reviewed_project_set_digest"
|
|
2441
2537
|
]);
|
|
2538
|
+
function projectInviteInviterDisplayName(value) {
|
|
2539
|
+
try {
|
|
2540
|
+
return normalizeHumanPresentationName(value, "inviter_display_name");
|
|
2541
|
+
} catch {
|
|
2542
|
+
return PROJECT_INVITE_OWNER_FALLBACK;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2442
2545
|
function projectInviteStringList(body, key) {
|
|
2443
2546
|
const value = body[key];
|
|
2444
2547
|
if (!Array.isArray(value)) throw new Error(`${key}_must_be_string_array`);
|
|
@@ -2565,10 +2668,16 @@ function recipientInviteOnboardingPreviewFromReviewedIntent(store, body, invitat
|
|
|
2565
2668
|
});
|
|
2566
2669
|
}
|
|
2567
2670
|
var SAFE_RECIPIENT_INVITE_ERRORS = new Set([
|
|
2671
|
+
"device_display_name_invalid",
|
|
2672
|
+
"device_display_name_required",
|
|
2673
|
+
"device_display_name_too_long",
|
|
2568
2674
|
"invite_already_bound",
|
|
2569
2675
|
"invite_expired",
|
|
2570
2676
|
"invite_identity_conflict",
|
|
2571
2677
|
"invite_invalid",
|
|
2678
|
+
"recipient_display_name_invalid",
|
|
2679
|
+
"recipient_display_name_required",
|
|
2680
|
+
"recipient_display_name_too_long",
|
|
2572
2681
|
"recipient_invite_intent_mismatch",
|
|
2573
2682
|
"recipient_invite_review_unavailable",
|
|
2574
2683
|
"reviewed_onboarding_stale"
|
|
@@ -2594,8 +2703,9 @@ async function peerSupportsSyncRequirements(store, deviceId, requirements) {
|
|
|
2594
2703
|
const url = `${buildBaseUrl(address)}/v1/status`;
|
|
2595
2704
|
const [status, payload] = await requestJson("GET", url, {
|
|
2596
2705
|
headers: {
|
|
2597
|
-
...
|
|
2706
|
+
...buildDirectPeerAuthHeaders({
|
|
2598
2707
|
deviceId: localDeviceId,
|
|
2708
|
+
recipientId: deviceId,
|
|
2599
2709
|
dbPath: store.dbPath,
|
|
2600
2710
|
method: "GET",
|
|
2601
2711
|
url,
|
|
@@ -3672,14 +3782,15 @@ function pathWithQuery(url) {
|
|
|
3672
3782
|
const parsed = new URL(url);
|
|
3673
3783
|
return parsed.search ? `${parsed.pathname}${parsed.search}` : parsed.pathname;
|
|
3674
3784
|
}
|
|
3675
|
-
function unauthorizedPayload(
|
|
3676
|
-
if (exposeReason && process.env.CODEMEM_SYNC_AUTH_DIAGNOSTICS === "1") return {
|
|
3677
|
-
error: "unauthorized",
|
|
3678
|
-
reason
|
|
3679
|
-
};
|
|
3785
|
+
function unauthorizedPayload() {
|
|
3680
3786
|
return { error: "unauthorized" };
|
|
3681
3787
|
}
|
|
3682
3788
|
var SYNC_AUTH_STORE_BUSY_REASON = "sync_auth_store_busy";
|
|
3789
|
+
var DIRECT_PEER_SIGNATURE_VERSION_NUMBER = 3;
|
|
3790
|
+
function logDirectPeerAuthDiagnostic(...reasons) {
|
|
3791
|
+
const reason = reasons.find((candidate) => candidate === "recipient_mismatch" || candidate === "signature_downgrade");
|
|
3792
|
+
if (reason) console.warn(`[sync] peer auth failed: reason=${reason}`);
|
|
3793
|
+
}
|
|
3683
3794
|
function isSqliteBusy(err) {
|
|
3684
3795
|
if (!err || typeof err !== "object") return false;
|
|
3685
3796
|
const code = "code" in err ? String(err.code ?? "") : "";
|
|
@@ -3716,6 +3827,76 @@ function syncAuthStoreBusyResponse(c) {
|
|
|
3716
3827
|
c.header("Retry-After", "1");
|
|
3717
3828
|
return c.json({ error: SYNC_AUTH_STORE_BUSY_REASON }, 503);
|
|
3718
3829
|
}
|
|
3830
|
+
function verifyDirectPeerRequestSignature(request, body, options) {
|
|
3831
|
+
const timestamp = request.header("X-Opencode-Timestamp") ?? "";
|
|
3832
|
+
const nonce = request.header("X-Opencode-Nonce") ?? "";
|
|
3833
|
+
const legacySignature = request.header("X-Opencode-Signature") ?? "";
|
|
3834
|
+
const recipientHeader = request.header("X-Codemem-Recipient");
|
|
3835
|
+
const directSignatureHeader = request.header("X-Codemem-Signature");
|
|
3836
|
+
const hasDirectMaterial = recipientHeader !== void 0 || directSignatureHeader !== void 0;
|
|
3837
|
+
const direct = verifyDirectPeerSignature({
|
|
3838
|
+
method: request.method,
|
|
3839
|
+
pathWithQuery: pathWithQuery(request.url),
|
|
3840
|
+
bodyBytes: body,
|
|
3841
|
+
timestamp,
|
|
3842
|
+
nonce,
|
|
3843
|
+
recipientId: recipientHeader,
|
|
3844
|
+
expectedRecipientId: options.expectedRecipientId,
|
|
3845
|
+
signature: directSignatureHeader,
|
|
3846
|
+
publicKey: options.publicKey
|
|
3847
|
+
});
|
|
3848
|
+
if (hasDirectMaterial && direct.status === "absent") return {
|
|
3849
|
+
ok: false,
|
|
3850
|
+
reason: "invalid_direct_signature"
|
|
3851
|
+
};
|
|
3852
|
+
if (direct.status === "invalid") return {
|
|
3853
|
+
ok: false,
|
|
3854
|
+
reason: direct.reason === "recipient_mismatch" ? direct.reason : "invalid_direct_signature"
|
|
3855
|
+
};
|
|
3856
|
+
if (!verifySignature({
|
|
3857
|
+
method: request.method,
|
|
3858
|
+
pathWithQuery: pathWithQuery(request.url),
|
|
3859
|
+
bodyBytes: body,
|
|
3860
|
+
timestamp,
|
|
3861
|
+
nonce,
|
|
3862
|
+
signature: legacySignature,
|
|
3863
|
+
publicKey: options.publicKey,
|
|
3864
|
+
deviceId: options.deviceId
|
|
3865
|
+
})) return {
|
|
3866
|
+
ok: false,
|
|
3867
|
+
reason: "invalid_signature"
|
|
3868
|
+
};
|
|
3869
|
+
if (direct.status === "absent" && (options.highestObservedVersion ?? 0) >= DIRECT_PEER_SIGNATURE_VERSION_NUMBER) return {
|
|
3870
|
+
ok: false,
|
|
3871
|
+
reason: "signature_downgrade"
|
|
3872
|
+
};
|
|
3873
|
+
return {
|
|
3874
|
+
ok: true,
|
|
3875
|
+
version: direct.status === "valid" ? 3 : 2
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
function recordDirectPeerSignatureVersion(store, deviceId, version) {
|
|
3879
|
+
try {
|
|
3880
|
+
if (!recordHighestObservedDirectSignatureVersion(store.db, deviceId, version)) {
|
|
3881
|
+
if (store.db.prepare(`SELECT sync_peer_signature_state.highest_observed_direct_signature_version
|
|
3882
|
+
FROM sync_peers
|
|
3883
|
+
LEFT JOIN sync_peer_signature_state USING (peer_device_id)
|
|
3884
|
+
WHERE sync_peers.peer_device_id = ? LIMIT 1`).pluck().get(deviceId) === void 0) return {
|
|
3885
|
+
ok: false,
|
|
3886
|
+
reason: "unknown_peer",
|
|
3887
|
+
deviceId
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3890
|
+
return null;
|
|
3891
|
+
} catch (err) {
|
|
3892
|
+
if (isSqliteBusy(err)) return {
|
|
3893
|
+
ok: false,
|
|
3894
|
+
reason: SYNC_AUTH_STORE_BUSY_REASON,
|
|
3895
|
+
deviceId
|
|
3896
|
+
};
|
|
3897
|
+
throw err;
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3719
3900
|
function authorizeSyncRequest(store, request, body) {
|
|
3720
3901
|
const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
|
|
3721
3902
|
const signature = request.header("X-Opencode-Signature") ?? "";
|
|
@@ -3726,7 +3907,11 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
3726
3907
|
reason: "missing_headers",
|
|
3727
3908
|
deviceId
|
|
3728
3909
|
};
|
|
3729
|
-
const peerRow = store.db.prepare(
|
|
3910
|
+
const peerRow = store.db.prepare(`SELECT sync_peers.pinned_fingerprint, sync_peers.public_key,
|
|
3911
|
+
sync_peer_signature_state.highest_observed_direct_signature_version
|
|
3912
|
+
FROM sync_peers
|
|
3913
|
+
LEFT JOIN sync_peer_signature_state USING (peer_device_id)
|
|
3914
|
+
WHERE sync_peers.peer_device_id = ? LIMIT 1`).get(deviceId);
|
|
3730
3915
|
if (!peerRow) return {
|
|
3731
3916
|
ok: false,
|
|
3732
3917
|
reason: "unknown_peer",
|
|
@@ -3744,17 +3929,14 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
3744
3929
|
reason: "fingerprint_mismatch",
|
|
3745
3930
|
deviceId
|
|
3746
3931
|
};
|
|
3747
|
-
let
|
|
3932
|
+
let verification;
|
|
3748
3933
|
try {
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
bodyBytes: body,
|
|
3753
|
-
timestamp,
|
|
3754
|
-
nonce,
|
|
3755
|
-
signature,
|
|
3934
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
3935
|
+
verification = verifyDirectPeerRequestSignature(request, body, {
|
|
3936
|
+
deviceId,
|
|
3756
3937
|
publicKey,
|
|
3757
|
-
|
|
3938
|
+
expectedRecipientId: localDeviceId,
|
|
3939
|
+
highestObservedVersion: peerRow.highest_observed_direct_signature_version
|
|
3758
3940
|
});
|
|
3759
3941
|
} catch {
|
|
3760
3942
|
return {
|
|
@@ -3763,13 +3945,17 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
3763
3945
|
deviceId
|
|
3764
3946
|
};
|
|
3765
3947
|
}
|
|
3766
|
-
if (!
|
|
3948
|
+
if (!verification.ok) return {
|
|
3767
3949
|
ok: false,
|
|
3768
|
-
reason:
|
|
3950
|
+
reason: verification.reason,
|
|
3769
3951
|
deviceId
|
|
3770
3952
|
};
|
|
3771
3953
|
const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
|
|
3772
3954
|
if (nonceResult) return nonceResult;
|
|
3955
|
+
if (verification.version === DIRECT_PEER_SIGNATURE_VERSION_NUMBER) {
|
|
3956
|
+
const versionResult = recordDirectPeerSignatureVersion(store, deviceId, verification.version);
|
|
3957
|
+
if (versionResult) return versionResult;
|
|
3958
|
+
}
|
|
3773
3959
|
return {
|
|
3774
3960
|
ok: true,
|
|
3775
3961
|
reason: "ok",
|
|
@@ -3888,17 +4074,15 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
3888
4074
|
reason: "bootstrap_grant_expired",
|
|
3889
4075
|
deviceId
|
|
3890
4076
|
};
|
|
3891
|
-
|
|
4077
|
+
const existingPeer = store.db.prepare(`SELECT highest_observed_direct_signature_version
|
|
4078
|
+
FROM sync_peer_signature_state WHERE peer_device_id = ? LIMIT 1`).get(deviceId);
|
|
4079
|
+
let signatureVerification;
|
|
3892
4080
|
try {
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
pathWithQuery: pathWithQuery(request.url),
|
|
3896
|
-
bodyBytes: body,
|
|
3897
|
-
timestamp,
|
|
3898
|
-
nonce,
|
|
3899
|
-
signature,
|
|
4081
|
+
signatureVerification = verifyDirectPeerRequestSignature(request, body, {
|
|
4082
|
+
deviceId,
|
|
3900
4083
|
publicKey: workerPublicKey,
|
|
3901
|
-
|
|
4084
|
+
expectedRecipientId: localDeviceId,
|
|
4085
|
+
highestObservedVersion: existingPeer?.highest_observed_direct_signature_version ?? null
|
|
3902
4086
|
});
|
|
3903
4087
|
} catch {
|
|
3904
4088
|
return {
|
|
@@ -3907,9 +4091,9 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
3907
4091
|
deviceId
|
|
3908
4092
|
};
|
|
3909
4093
|
}
|
|
3910
|
-
if (!
|
|
4094
|
+
if (!signatureVerification.ok) return {
|
|
3911
4095
|
ok: false,
|
|
3912
|
-
reason:
|
|
4096
|
+
reason: signatureVerification.reason,
|
|
3913
4097
|
deviceId
|
|
3914
4098
|
};
|
|
3915
4099
|
if (!isPeerTrustBindingCompatible(store.db, deviceId, workerPublicKey, workerFingerprint)) return {
|
|
@@ -3925,6 +4109,10 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
3925
4109
|
publicKey: workerPublicKey,
|
|
3926
4110
|
replaceTrust: true
|
|
3927
4111
|
});
|
|
4112
|
+
if (signatureVerification.version === DIRECT_PEER_SIGNATURE_VERSION_NUMBER) {
|
|
4113
|
+
const versionResult = recordDirectPeerSignatureVersion(store, deviceId, signatureVerification.version);
|
|
4114
|
+
if (versionResult) return versionResult;
|
|
4115
|
+
}
|
|
3928
4116
|
return {
|
|
3929
4117
|
ok: true,
|
|
3930
4118
|
reason: "ok",
|
|
@@ -4832,8 +5020,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
4832
5020
|
if (!auth.ok) {
|
|
4833
5021
|
const grantPresent = Boolean((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim());
|
|
4834
5022
|
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
|
-
|
|
4836
|
-
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(
|
|
5023
|
+
else logDirectPeerAuthDiagnostic(deviceAuthReason, auth.reason);
|
|
5024
|
+
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(), 401);
|
|
4837
5025
|
}
|
|
4838
5026
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
4839
5027
|
if (limited) return limited;
|
|
@@ -4868,7 +5056,10 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
4868
5056
|
const store = getStore();
|
|
4869
5057
|
const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
4870
5058
|
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
4871
|
-
if (!auth.ok)
|
|
5059
|
+
if (!auth.ok) {
|
|
5060
|
+
logDirectPeerAuthDiagnostic(auth.reason);
|
|
5061
|
+
return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(), 401);
|
|
5062
|
+
}
|
|
4872
5063
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
4873
5064
|
if (limited) return limited;
|
|
4874
5065
|
const peerDeviceId = auth.deviceId;
|
|
@@ -4949,8 +5140,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
4949
5140
|
if (!auth.ok) {
|
|
4950
5141
|
const grantPresent = Boolean((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim());
|
|
4951
5142
|
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
|
-
|
|
4953
|
-
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(
|
|
5143
|
+
else logDirectPeerAuthDiagnostic(deviceAuthReason, auth.reason);
|
|
5144
|
+
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(), 401);
|
|
4954
5145
|
}
|
|
4955
5146
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
4956
5147
|
if (limited) return limited;
|
|
@@ -5007,7 +5198,10 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
5007
5198
|
if (raw == null) return c.json({ error: "payload_too_large" }, 413);
|
|
5008
5199
|
const auth = authorizeSyncRequest(store, c.req, raw);
|
|
5009
5200
|
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
5010
|
-
if (!auth.ok)
|
|
5201
|
+
if (!auth.ok) {
|
|
5202
|
+
logDirectPeerAuthDiagnostic(auth.reason);
|
|
5203
|
+
return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(), 401);
|
|
5204
|
+
}
|
|
5011
5205
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
5012
5206
|
if (limited) return limited;
|
|
5013
5207
|
const peerDeviceId = auth.deviceId;
|
|
@@ -5071,6 +5265,52 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
5071
5265
|
});
|
|
5072
5266
|
return app;
|
|
5073
5267
|
}
|
|
5268
|
+
async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
5269
|
+
const config = readCoordinatorSyncConfig();
|
|
5270
|
+
const hasCoordinatorConfiguration = config.syncCoordinatorUrl.length > 0 || config.syncCoordinatorGroups.length > 0;
|
|
5271
|
+
const remoteUrl = config.syncCoordinatorUrl.trim();
|
|
5272
|
+
const adminSecret = config.syncCoordinatorAdminSecret.trim();
|
|
5273
|
+
const groupIds = [...new Set(config.syncCoordinatorGroups.map((groupId) => groupId.trim()))].filter(Boolean);
|
|
5274
|
+
if (!hasCoordinatorConfiguration) return {
|
|
5275
|
+
availability: "unavailable",
|
|
5276
|
+
safeErrorCode: "coordinator_not_configured",
|
|
5277
|
+
enrollments: []
|
|
5278
|
+
};
|
|
5279
|
+
if (!remoteUrl || !adminSecret || groupIds.length === 0) return {
|
|
5280
|
+
availability: "unavailable",
|
|
5281
|
+
safeErrorCode: "coordinator_unavailable",
|
|
5282
|
+
enrollments: []
|
|
5283
|
+
};
|
|
5284
|
+
if (groupIds.length > 25) return {
|
|
5285
|
+
availability: "unavailable",
|
|
5286
|
+
safeErrorCode: "coordinator_evidence_too_large",
|
|
5287
|
+
enrollments: []
|
|
5288
|
+
};
|
|
5289
|
+
try {
|
|
5290
|
+
const enrollments = (await Promise.all(groupIds.map((groupId) => coordinatorListDevicesAction({
|
|
5291
|
+
groupId,
|
|
5292
|
+
includeDisabled: true,
|
|
5293
|
+
remoteUrl,
|
|
5294
|
+
adminSecret
|
|
5295
|
+
})))).flat();
|
|
5296
|
+
if (enrollments.length > 500) return {
|
|
5297
|
+
availability: "unavailable",
|
|
5298
|
+
safeErrorCode: "coordinator_evidence_too_large",
|
|
5299
|
+
enrollments: []
|
|
5300
|
+
};
|
|
5301
|
+
return {
|
|
5302
|
+
availability: "available",
|
|
5303
|
+
safeErrorCode: null,
|
|
5304
|
+
enrollments
|
|
5305
|
+
};
|
|
5306
|
+
} catch {
|
|
5307
|
+
return {
|
|
5308
|
+
availability: "unavailable",
|
|
5309
|
+
safeErrorCode: "coordinator_unavailable",
|
|
5310
|
+
enrollments: []
|
|
5311
|
+
};
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5074
5314
|
/**
|
|
5075
5315
|
* Viewer-facing sync management routes (/api/sync/*).
|
|
5076
5316
|
*
|
|
@@ -5078,8 +5318,112 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
5078
5318
|
* provide sync status, peer management, and coordinator UI for the
|
|
5079
5319
|
* local viewer.
|
|
5080
5320
|
*/
|
|
5081
|
-
function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
5321
|
+
function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
5082
5322
|
const app = new Hono();
|
|
5323
|
+
const loadCoordinatorEvidence = options.loadDeviceIdentityCoordinatorEvidence ?? loadConfiguredDeviceIdentityCoordinatorEvidence;
|
|
5324
|
+
const currentCoordinatorEvidence = async () => {
|
|
5325
|
+
try {
|
|
5326
|
+
return await loadCoordinatorEvidence();
|
|
5327
|
+
} catch {
|
|
5328
|
+
return {
|
|
5329
|
+
availability: "unavailable",
|
|
5330
|
+
safeErrorCode: "coordinator_unavailable",
|
|
5331
|
+
enrollments: []
|
|
5332
|
+
};
|
|
5333
|
+
}
|
|
5334
|
+
};
|
|
5335
|
+
const localInventoryInput = async (store) => {
|
|
5336
|
+
let explicitDeviceName = "";
|
|
5337
|
+
try {
|
|
5338
|
+
explicitDeviceName = String(readCodememConfigFile().sync_device_name ?? "");
|
|
5339
|
+
} catch {
|
|
5340
|
+
explicitDeviceName = "";
|
|
5341
|
+
}
|
|
5342
|
+
return {
|
|
5343
|
+
localDeviceId: store.deviceId,
|
|
5344
|
+
localDisplayName: friendlyDeviceName({
|
|
5345
|
+
explicitName: explicitDeviceName,
|
|
5346
|
+
osName: hostname(),
|
|
5347
|
+
fallbackSeed: store.deviceId
|
|
5348
|
+
}),
|
|
5349
|
+
coordinator: await currentCoordinatorEvidence()
|
|
5350
|
+
};
|
|
5351
|
+
};
|
|
5352
|
+
app.get("/api/sync/recipient-policy/v1/device-inventory", async (c) => {
|
|
5353
|
+
const store = getStore();
|
|
5354
|
+
ensureStableStoreIdentity(store);
|
|
5355
|
+
return c.json(listDeviceIdentityInventory(store.db, await localInventoryInput(store)));
|
|
5356
|
+
});
|
|
5357
|
+
const parseDeviceBindingRequest = (value, commit) => {
|
|
5358
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
5359
|
+
const body = value;
|
|
5360
|
+
const allowed = new Set(commit ? ["bindings", "reviewedInventoryDigest"] : ["bindings"]);
|
|
5361
|
+
if (Object.keys(body).some((key) => !allowed.has(key)) || !Array.isArray(body.bindings)) return null;
|
|
5362
|
+
const bindings = body.bindings.map((value) => {
|
|
5363
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
5364
|
+
const binding = value;
|
|
5365
|
+
const bindingKeys = new Set([
|
|
5366
|
+
"deviceId",
|
|
5367
|
+
"targetIdentityId",
|
|
5368
|
+
"confirmed",
|
|
5369
|
+
"allowRebind"
|
|
5370
|
+
]);
|
|
5371
|
+
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;
|
|
5372
|
+
return {
|
|
5373
|
+
deviceId: binding.deviceId,
|
|
5374
|
+
targetIdentityId: binding.targetIdentityId,
|
|
5375
|
+
confirmed: binding.confirmed,
|
|
5376
|
+
...typeof binding.allowRebind === "boolean" ? { allowRebind: binding.allowRebind } : {}
|
|
5377
|
+
};
|
|
5378
|
+
});
|
|
5379
|
+
if (bindings.some((binding) => binding == null)) return null;
|
|
5380
|
+
if (commit && typeof body.reviewedInventoryDigest !== "string") return null;
|
|
5381
|
+
return {
|
|
5382
|
+
bindings,
|
|
5383
|
+
...commit ? { reviewedInventoryDigest: body.reviewedInventoryDigest } : {}
|
|
5384
|
+
};
|
|
5385
|
+
};
|
|
5386
|
+
app.post("/api/sync/recipient-policy/v1/device-bindings/preview", async (c) => {
|
|
5387
|
+
const body = parseDeviceBindingRequest(await parseViewerJsonBody(c), false);
|
|
5388
|
+
if (!body) return c.json({ error: "binding_request_invalid" }, 400);
|
|
5389
|
+
const store = getStore();
|
|
5390
|
+
try {
|
|
5391
|
+
ensureStableStoreIdentity(store);
|
|
5392
|
+
const result = previewDeviceIdentityBindings(store.db, await localInventoryInput(store), body);
|
|
5393
|
+
if (result.status === "invalid") return c.json(result, 400);
|
|
5394
|
+
if (result.status === "not_found") return c.json(result, 404);
|
|
5395
|
+
if (result.status === "conflict") return c.json(result, 409);
|
|
5396
|
+
return c.json(result);
|
|
5397
|
+
} catch (error) {
|
|
5398
|
+
if (isSqliteBusy(error)) {
|
|
5399
|
+
c.header("Retry-After", "1");
|
|
5400
|
+
return c.json({ error: "binding_preview_busy" }, 503);
|
|
5401
|
+
}
|
|
5402
|
+
return c.json({ error: "binding_preview_failed" }, 500);
|
|
5403
|
+
}
|
|
5404
|
+
});
|
|
5405
|
+
app.post("/api/sync/recipient-policy/v1/device-bindings/commit", async (c) => {
|
|
5406
|
+
const body = parseDeviceBindingRequest(await parseViewerJsonBody(c), true);
|
|
5407
|
+
if (!body || !("reviewedInventoryDigest" in body)) return c.json({ error: "binding_request_invalid" }, 400);
|
|
5408
|
+
const store = getStore();
|
|
5409
|
+
try {
|
|
5410
|
+
ensureStableStoreIdentity(store);
|
|
5411
|
+
const result = commitDeviceIdentityBindings(store.db, {
|
|
5412
|
+
localActorId: store.actorId,
|
|
5413
|
+
localDeviceId: store.deviceId
|
|
5414
|
+
}, await localInventoryInput(store), body);
|
|
5415
|
+
if (result.status === "invalid") return c.json(result, 400);
|
|
5416
|
+
if (result.status === "not_found") return c.json(result, 404);
|
|
5417
|
+
if (result.status === "stale" || result.status === "conflict") return c.json(result, 409);
|
|
5418
|
+
return c.json(result);
|
|
5419
|
+
} catch (error) {
|
|
5420
|
+
if (isSqliteBusy(error)) {
|
|
5421
|
+
c.header("Retry-After", "1");
|
|
5422
|
+
return c.json({ error: "binding_commit_busy" }, 503);
|
|
5423
|
+
}
|
|
5424
|
+
return c.json({ error: "binding_commit_failed" }, 500);
|
|
5425
|
+
}
|
|
5426
|
+
});
|
|
5083
5427
|
app.get("/api/sync/status", async (c) => {
|
|
5084
5428
|
const store = getStore();
|
|
5085
5429
|
{
|
|
@@ -5392,9 +5736,15 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
5392
5736
|
return c.json({ error: "invalid json" }, 400);
|
|
5393
5737
|
}
|
|
5394
5738
|
const peerDeviceId = String(body.peer_device_id ?? "").trim();
|
|
5395
|
-
const
|
|
5739
|
+
const rawName = String(body.name ?? "");
|
|
5396
5740
|
if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
|
|
5397
|
-
if (!
|
|
5741
|
+
if (!rawName.trim()) return c.json({ error: "name required" }, 400);
|
|
5742
|
+
let name;
|
|
5743
|
+
try {
|
|
5744
|
+
name = normalizeHumanPresentationName(rawName, "name");
|
|
5745
|
+
} catch (error) {
|
|
5746
|
+
return c.json({ error: error instanceof Error ? error.message : "name_invalid" }, 400);
|
|
5747
|
+
}
|
|
5398
5748
|
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
5749
|
d.update(schema.syncPeers).set({ name }).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();
|
|
5400
5750
|
return c.json({ ok: true });
|
|
@@ -5802,7 +6152,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
5802
6152
|
operationId: plan.operationId,
|
|
5803
6153
|
reviewedProjectSetDigest: plan.reviewedProjectSetDigest,
|
|
5804
6154
|
inviterActorId: plan.inviterActorId,
|
|
5805
|
-
inviterDisplayName: store.actorDisplayName,
|
|
6155
|
+
inviterDisplayName: projectInviteInviterDisplayName(store.actorDisplayName),
|
|
5806
6156
|
inviterDeviceId: plan.inviterDeviceIds[0],
|
|
5807
6157
|
pendingPersonId: plan.personId,
|
|
5808
6158
|
projectSummaries: plan.projects.map((project) => ({
|
|
@@ -6110,8 +6460,14 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
6110
6460
|
} catch {
|
|
6111
6461
|
return c.json({ error: "invalid json" }, 400);
|
|
6112
6462
|
}
|
|
6113
|
-
const
|
|
6114
|
-
if (!
|
|
6463
|
+
const rawDisplayName = String(body.display_name ?? "");
|
|
6464
|
+
if (!rawDisplayName.trim()) return c.json({ error: "display_name required" }, 400);
|
|
6465
|
+
let displayName;
|
|
6466
|
+
try {
|
|
6467
|
+
displayName = normalizeHumanPresentationName(rawDisplayName, "display_name");
|
|
6468
|
+
} catch (error) {
|
|
6469
|
+
return c.json({ error: error instanceof Error ? error.message : "display_name_invalid" }, 400);
|
|
6470
|
+
}
|
|
6115
6471
|
const d = drizzle(store.db, { schema });
|
|
6116
6472
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6117
6473
|
const actor = {
|
|
@@ -6136,9 +6492,13 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
6136
6492
|
return c.json({ error: "invalid json" }, 400);
|
|
6137
6493
|
}
|
|
6138
6494
|
const actorId = String(body.actor_id ?? "").trim();
|
|
6139
|
-
const displayName = String(body.display_name ?? "").trim();
|
|
6140
6495
|
if (!actorId) return c.json({ error: "actor_id required" }, 400);
|
|
6141
|
-
|
|
6496
|
+
let displayName;
|
|
6497
|
+
try {
|
|
6498
|
+
displayName = normalizeHumanPresentationName(String(body.display_name ?? ""), "display_name");
|
|
6499
|
+
} catch (error) {
|
|
6500
|
+
return c.json({ error: error instanceof Error ? error.message : "display_name_invalid" }, 400);
|
|
6501
|
+
}
|
|
6142
6502
|
const d = drizzle(store.db, { schema });
|
|
6143
6503
|
const actor = d.select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
|
|
6144
6504
|
if (!actor) return c.json({ error: "actor not found" }, 404);
|
|
@@ -6168,14 +6528,133 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
6168
6528
|
if (!secondaryActorId) return c.json({ error: "secondary_actor_id required" }, 400);
|
|
6169
6529
|
if (primaryActorId === secondaryActorId) return c.json({ error: "actor ids must differ" }, 400);
|
|
6170
6530
|
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
6531
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6178
|
-
const
|
|
6532
|
+
const result = store.db.transaction(() => {
|
|
6533
|
+
const primary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, primaryActorId)).get();
|
|
6534
|
+
const secondary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, secondaryActorId)).get();
|
|
6535
|
+
if (!primary || !secondary) return {
|
|
6536
|
+
error: "actor not found",
|
|
6537
|
+
status: 404
|
|
6538
|
+
};
|
|
6539
|
+
if (primary.status !== "active") return {
|
|
6540
|
+
error: "primary actor not active",
|
|
6541
|
+
status: 409
|
|
6542
|
+
};
|
|
6543
|
+
if (secondary.status !== "active") return {
|
|
6544
|
+
error: "secondary actor not active",
|
|
6545
|
+
status: 409
|
|
6546
|
+
};
|
|
6547
|
+
if (secondary.is_local && secondary.actor_id === store.actorId) return {
|
|
6548
|
+
error: "cannot merge this device's own local actor",
|
|
6549
|
+
status: 409
|
|
6550
|
+
};
|
|
6551
|
+
const memberships = d.select().from(schema.policyTeamMemberships).where(eq(schema.policyTeamMemberships.identity_id, secondaryActorId)).all();
|
|
6552
|
+
const primaryMemberships = /* @__PURE__ */ new Map();
|
|
6553
|
+
for (const membership of memberships) {
|
|
6554
|
+
const primaryMembership = d.select().from(schema.policyTeamMemberships).where(and(eq(schema.policyTeamMemberships.team_id, membership.team_id), eq(schema.policyTeamMemberships.identity_id, primaryActorId))).get();
|
|
6555
|
+
if (primaryMembership && (primaryMembership.role !== membership.role || primaryMembership.status !== membership.status)) return {
|
|
6556
|
+
error: "actor team memberships conflict",
|
|
6557
|
+
status: 409
|
|
6558
|
+
};
|
|
6559
|
+
primaryMemberships.set(membership.team_id, primaryMembership);
|
|
6560
|
+
}
|
|
6561
|
+
const directRecipients = d.select().from(schema.projectRecipients).where(and(eq(schema.projectRecipients.recipient_kind, "identity"), eq(schema.projectRecipients.recipient_id, secondaryActorId))).all();
|
|
6562
|
+
const primaryRecipients = /* @__PURE__ */ new Map();
|
|
6563
|
+
for (const recipient of directRecipients) {
|
|
6564
|
+
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();
|
|
6565
|
+
if (primaryRecipient && primaryRecipient.status !== recipient.status) return {
|
|
6566
|
+
error: "actor project recipients conflict",
|
|
6567
|
+
status: 409
|
|
6568
|
+
};
|
|
6569
|
+
primaryRecipients.set(recipient.canonical_project_identity, primaryRecipient);
|
|
6570
|
+
}
|
|
6571
|
+
const mergeDigest = createHash("sha256").update(JSON.stringify({
|
|
6572
|
+
kind: "identity-merge-v1",
|
|
6573
|
+
primaryActorId,
|
|
6574
|
+
secondaryActorId,
|
|
6575
|
+
nonce: randomUUID()
|
|
6576
|
+
})).digest("hex");
|
|
6577
|
+
const bindings = d.select({
|
|
6578
|
+
deviceId: schema.identityDevices.device_id,
|
|
6579
|
+
assignmentVersion: schema.identityDevices.assignment_version
|
|
6580
|
+
}).from(schema.identityDevices).where(eq(schema.identityDevices.identity_id, secondaryActorId)).all();
|
|
6581
|
+
if (bindings.length > 0) {
|
|
6582
|
+
const commitDigest = mergeDigest;
|
|
6583
|
+
const outcomes = bindings.map((binding) => ({
|
|
6584
|
+
deviceId: binding.deviceId,
|
|
6585
|
+
previousIdentityId: secondaryActorId,
|
|
6586
|
+
targetIdentityId: primaryActorId,
|
|
6587
|
+
action: "rebind"
|
|
6588
|
+
}));
|
|
6589
|
+
store.db.prepare(`INSERT INTO device_identity_binding_commits(
|
|
6590
|
+
commit_digest, reviewed_inventory_digest, request_json, outcomes_json, write_count,
|
|
6591
|
+
decided_by_identity_id, decided_by_device_id, created_at
|
|
6592
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(commitDigest, `identity-merge:${secondaryActorId}:${primaryActorId}`, JSON.stringify({
|
|
6593
|
+
primaryActorId,
|
|
6594
|
+
secondaryActorId
|
|
6595
|
+
}), JSON.stringify(outcomes), bindings.length, store.actorId, store.deviceId, now);
|
|
6596
|
+
for (const binding of bindings) {
|
|
6597
|
+
const revision = createHash("sha256").update(`identity-merge-revision-v1\0${commitDigest}\0${binding.deviceId}`).digest("hex");
|
|
6598
|
+
const idempotencyKey = createHash("sha256").update(`identity-merge-row-v1\0${commitDigest}\0${binding.deviceId}`).digest("hex");
|
|
6599
|
+
if (store.db.prepare(`UPDATE identity_devices SET
|
|
6600
|
+
identity_id = ?, provenance = 'review_resolution', revision = ?,
|
|
6601
|
+
migration_state = 'user_managed', source_fingerprint = ?,
|
|
6602
|
+
idempotency_key = ?, updated_at = ?
|
|
6603
|
+
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");
|
|
6604
|
+
const rebound = store.db.prepare("SELECT assignment_version FROM identity_devices WHERE device_id = ?").get(binding.deviceId);
|
|
6605
|
+
if (!rebound || rebound.assignment_version !== binding.assignmentVersion + 1) throw new Error("identity_merge_assignment_version_unexpected");
|
|
6606
|
+
store.db.prepare("DELETE FROM policy_team_device_decisions WHERE device_id = ?").run(binding.deviceId);
|
|
6607
|
+
store.db.prepare(`INSERT INTO device_identity_binding_audit(
|
|
6608
|
+
event_id, commit_digest, device_id, previous_identity_id, target_identity_id,
|
|
6609
|
+
action, previous_assignment_version, resulting_assignment_version,
|
|
6610
|
+
decided_by_identity_id, decided_by_device_id, created_at
|
|
6611
|
+
) 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);
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
for (const membership of memberships) {
|
|
6615
|
+
if (primaryMemberships.get(membership.team_id)) {
|
|
6616
|
+
d.delete(schema.policyTeamMemberships).where(and(eq(schema.policyTeamMemberships.team_id, membership.team_id), eq(schema.policyTeamMemberships.identity_id, secondaryActorId))).run();
|
|
6617
|
+
continue;
|
|
6618
|
+
}
|
|
6619
|
+
const revision = createHash("sha256").update(`identity-merge-membership-revision-v1\0${mergeDigest}\0${membership.team_id}`).digest("hex");
|
|
6620
|
+
const idempotencyKey = createHash("sha256").update(`identity-merge-membership-row-v1\0${mergeDigest}\0${membership.team_id}`).digest("hex");
|
|
6621
|
+
d.update(schema.policyTeamMemberships).set({
|
|
6622
|
+
identity_id: primaryActorId,
|
|
6623
|
+
provenance: "review_resolution",
|
|
6624
|
+
revision,
|
|
6625
|
+
migration_state: "user_managed",
|
|
6626
|
+
source_fingerprint: mergeDigest,
|
|
6627
|
+
idempotency_key: idempotencyKey,
|
|
6628
|
+
updated_at: now
|
|
6629
|
+
}).where(and(eq(schema.policyTeamMemberships.team_id, membership.team_id), eq(schema.policyTeamMemberships.identity_id, secondaryActorId))).run();
|
|
6630
|
+
}
|
|
6631
|
+
for (const recipient of directRecipients) {
|
|
6632
|
+
if (primaryRecipients.get(recipient.canonical_project_identity)) {
|
|
6633
|
+
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();
|
|
6634
|
+
continue;
|
|
6635
|
+
}
|
|
6636
|
+
const policyRevision = createHash("sha256").update(`identity-merge-recipient-revision-v1\0${mergeDigest}\0${recipient.canonical_project_identity}`).digest("hex");
|
|
6637
|
+
const idempotencyKey = createHash("sha256").update(`identity-merge-recipient-row-v1\0${mergeDigest}\0${recipient.canonical_project_identity}`).digest("hex");
|
|
6638
|
+
d.update(schema.projectRecipients).set({
|
|
6639
|
+
recipient_id: primaryActorId,
|
|
6640
|
+
provenance: "review_resolution",
|
|
6641
|
+
policy_revision: policyRevision,
|
|
6642
|
+
migration_state: "user_managed",
|
|
6643
|
+
source_fingerprint: mergeDigest,
|
|
6644
|
+
idempotency_key: idempotencyKey,
|
|
6645
|
+
updated_at: now
|
|
6646
|
+
}).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();
|
|
6647
|
+
}
|
|
6648
|
+
d.update(schema.recipientManagedProjectProjections).set({
|
|
6649
|
+
recipient_identity_id: primaryActorId,
|
|
6650
|
+
updated_at: now
|
|
6651
|
+
}).where(eq(schema.recipientManagedProjectProjections.recipient_identity_id, secondaryActorId)).run();
|
|
6652
|
+
store.db.prepare(`UPDATE share_operations SET
|
|
6653
|
+
inviter_actor_id = CASE WHEN inviter_actor_id = ? THEN ? ELSE inviter_actor_id END,
|
|
6654
|
+
recipient_actor_id = CASE WHEN recipient_actor_id = ? THEN ? ELSE recipient_actor_id END,
|
|
6655
|
+
person_id = CASE WHEN person_id = ? THEN ? ELSE person_id END,
|
|
6656
|
+
updated_at = ?
|
|
6657
|
+
WHERE inviter_actor_id = ? OR recipient_actor_id = ? OR person_id = ?`).run(secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, now, secondaryActorId, secondaryActorId, secondaryActorId);
|
|
6179
6658
|
const peerUpdate = d.update(schema.syncPeers).set({ actor_id: primaryActorId }).where(eq(schema.syncPeers.actor_id, secondaryActorId)).run();
|
|
6180
6659
|
if (primary.is_local) d.update(schema.syncPeers).set({ claimed_local_actor: 1 }).where(eq(schema.syncPeers.actor_id, primaryActorId)).run();
|
|
6181
6660
|
d.update(schema.actors).set({
|
|
@@ -6183,9 +6662,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
6183
6662
|
merged_into_actor_id: primaryActorId,
|
|
6184
6663
|
updated_at: now
|
|
6185
6664
|
}).where(eq(schema.actors.actor_id, secondaryActorId)).run();
|
|
6186
|
-
return Number(peerUpdate.changes ?? 0);
|
|
6187
|
-
})();
|
|
6188
|
-
return c.json({
|
|
6665
|
+
return { mergedCount: Number(peerUpdate.changes ?? 0) };
|
|
6666
|
+
}).immediate();
|
|
6667
|
+
if ("error" in result) return c.json({ error: result.error }, result.status);
|
|
6668
|
+
return c.json({ merged_count: result.mergedCount });
|
|
6189
6669
|
});
|
|
6190
6670
|
app.post("/api/sync/actors/deactivate", async (c) => {
|
|
6191
6671
|
const store = getStore();
|
|
@@ -6408,7 +6888,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
6408
6888
|
inviteValue: rawValue,
|
|
6409
6889
|
dbPath: store.dbPath,
|
|
6410
6890
|
recipientActorId: recipientInvite ? null : store.actorId,
|
|
6411
|
-
recipientDisplayName: typeof body.recipient_name === "string" ? body.recipient_name : store.actorDisplayName,
|
|
6891
|
+
recipientDisplayName: decoded.kind === "add_device" ? null : typeof body.recipient_name === "string" ? body.recipient_name : store.actorDisplayName,
|
|
6412
6892
|
deviceDisplayName: typeof body.device_name === "string" ? body.device_name : null,
|
|
6413
6893
|
reviewedOnboardingDigest: reviewedOnboardingDigest || null
|
|
6414
6894
|
});
|
|
@@ -7255,15 +7735,19 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
7255
7735
|
}, 400);
|
|
7256
7736
|
}
|
|
7257
7737
|
const groupId = resolveCoordinatorAdminGroup(String(body.group_id ?? "").trim(), status);
|
|
7258
|
-
const displayName = String(body.display_name ?? "").trim();
|
|
7259
7738
|
if (!groupId) return c.json({
|
|
7260
7739
|
error: "group_id required",
|
|
7261
7740
|
status
|
|
7262
7741
|
}, 400);
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
}
|
|
7742
|
+
let displayName;
|
|
7743
|
+
try {
|
|
7744
|
+
displayName = normalizeHumanPresentationName(String(body.display_name ?? ""), "display_name");
|
|
7745
|
+
} catch (error) {
|
|
7746
|
+
return c.json({
|
|
7747
|
+
error: error instanceof Error ? error.message : "display_name_invalid",
|
|
7748
|
+
status
|
|
7749
|
+
}, 400);
|
|
7750
|
+
}
|
|
7267
7751
|
try {
|
|
7268
7752
|
const device = await coordinatorRenameDeviceAction({
|
|
7269
7753
|
groupId,
|
|
@@ -7744,7 +8228,7 @@ function createApp(opts) {
|
|
|
7744
8228
|
}));
|
|
7745
8229
|
app.route("/", configRoutes({ getSweeper: () => sweeper }));
|
|
7746
8230
|
app.route("/", rawEventsRoutes(storeFactory, sweeper));
|
|
7747
|
-
app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus));
|
|
8231
|
+
app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus, { loadDeviceIdentityCoordinatorEvidence: opts?.loadDeviceIdentityCoordinatorEvidence }));
|
|
7748
8232
|
app.route("/", updateStatusRoutes({ getUpdateStatus: opts?.getUpdateStatus }));
|
|
7749
8233
|
const staticRoot = process.env.CODEMEM_VIEWER_STATIC_DIR ?? join(import.meta.dirname ?? ".", "../static");
|
|
7750
8234
|
app.use("/assets/*", async (c, next) => {
|