@codemem/server 0.46.0-alpha.1 → 0.46.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +404 -160
- package/dist/index.js.map +1 -1
- package/dist/raw-event-inbox.d.ts +8 -0
- package/dist/raw-event-inbox.d.ts.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/memory-ownership.d.ts +5 -0
- package/dist/routes/memory-ownership.d.ts.map +1 -0
- package/dist/routes/observer-status.d.ts.map +1 -1
- package/dist/routes/raw-events.d.ts.map +1 -1
- package/dist/routes/sync.d.ts +22 -8
- package/dist/routes/sync.d.ts.map +1 -1
- package/dist/routes/target-validation.d.ts +13 -0
- package/dist/routes/target-validation.d.ts.map +1 -1
- package/dist/snapshot-source.d.ts +12 -0
- package/dist/snapshot-source.d.ts.map +1 -0
- package/package.json +2 -2
- package/static/app.js +76 -57
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/index.html +838 -280
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,
|
|
3
|
+
import { ACCESS_CLEANUP_OP_TYPE, CodememConfigMutationError, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, LegacyTeamSetupAdditiveConvergenceError, LegacyTeamSetupRosterCapacityError, MEMORY_FILTER_FIELD_TYPES, MEMORY_FILTER_NAMES, MEMORY_KIND_DESCRIPTIONS, MemoryStore, ObserverClient, PROMPT_TRANSPORT_PROTOCOL_RANGE, REMEMBER_MEMORY_KINDS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, 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, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterClausesWithContext, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalWorkspaceIdentity, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, dedupeOrderedIds, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, deriveLegacyTeamSetupCompletionManifest, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrideValues, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isAutomaticRecallMeasurement, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, judgeDistillReport, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, memoryFilterValueMatchesType, mergeAddresses, migrateRecipientPolicyIntent, mutateCodememConfigFile, negotiateSyncCapability, normalizeAddress, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectMatchesFilter, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordAutomaticRecall, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveDbPath, resolveObserverRuntime, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, storeVectors, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, validateRawEvents, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature } 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";
|
|
@@ -284,7 +284,7 @@ var DEFAULTS = {
|
|
|
284
284
|
sync_host: "0.0.0.0",
|
|
285
285
|
sync_port: 7337,
|
|
286
286
|
sync_interval_s: 120,
|
|
287
|
-
sync_mdns:
|
|
287
|
+
sync_mdns: false,
|
|
288
288
|
sync_coordinator_timeout_s: 3,
|
|
289
289
|
sync_coordinator_presence_ttl_s: 180,
|
|
290
290
|
raw_events_sweeper_interval_s: 30
|
|
@@ -303,14 +303,18 @@ function getEffectiveConfig(configData) {
|
|
|
303
303
|
...configData
|
|
304
304
|
};
|
|
305
305
|
for (const key of ["claude_command", "codex_command"]) effective[key] = coerceObserverCommand(effective[key]) ?? DEFAULTS[key];
|
|
306
|
-
|
|
307
|
-
const val = process.env[envVar];
|
|
308
|
-
if (val == null || val === "") continue;
|
|
309
|
-
if (key === "claude_command" || key === "codex_command") effective[key] = coerceObserverCommand(val) ?? effective[key];
|
|
310
|
-
else effective[key] = val;
|
|
311
|
-
}
|
|
306
|
+
Object.assign(effective, getCodememEnvOverrideValues());
|
|
312
307
|
return effective;
|
|
313
308
|
}
|
|
309
|
+
function observerRuntimeMetadata(configData) {
|
|
310
|
+
return {
|
|
311
|
+
resolved_observer_runtime: resolveObserverRuntime(configData),
|
|
312
|
+
observer_runtime_by_auth_source: Object.fromEntries([...AUTH_SOURCES].map((source) => [source, resolveObserverRuntime({
|
|
313
|
+
...configData,
|
|
314
|
+
observer_auth_source: source
|
|
315
|
+
})]))
|
|
316
|
+
};
|
|
317
|
+
}
|
|
314
318
|
function redactConfigValue(key, value) {
|
|
315
319
|
if (value == null || !SECRET_CONFIG_KEYS.has(key)) return value;
|
|
316
320
|
if (Array.isArray(value)) return value.length > 0 ? REDACTED_VALUE : [];
|
|
@@ -530,6 +534,7 @@ function viewerConfigSavePayload(saved, opts) {
|
|
|
530
534
|
path: savedPath,
|
|
531
535
|
config: sanitizeConfigForResponse(nextConfig),
|
|
532
536
|
effective: sanitizeConfigForResponse(afterEffective),
|
|
537
|
+
...observerRuntimeMetadata(nextConfig),
|
|
533
538
|
protected_keys: [...PROTECTED_WRITE_KEYS].sort(),
|
|
534
539
|
effects: {
|
|
535
540
|
saved_keys: savedChangedKeys,
|
|
@@ -563,6 +568,7 @@ function configRoutes(opts = {}) {
|
|
|
563
568
|
config: sanitizeConfigForResponse(configData),
|
|
564
569
|
defaults: DEFAULTS,
|
|
565
570
|
effective: sanitizeConfigForResponse(effective),
|
|
571
|
+
...observerRuntimeMetadata(configData),
|
|
566
572
|
env_overrides: getCodememEnvOverrides(),
|
|
567
573
|
protected_keys: [...PROTECTED_WRITE_KEYS].sort(),
|
|
568
574
|
providers: loadProviderOptions()
|
|
@@ -1594,7 +1600,7 @@ function memoryRoutes(getStore) {
|
|
|
1594
1600
|
}
|
|
1595
1601
|
const memoryId = parseStrictInteger(typeof body.memory_id === "string" ? body.memory_id : String(body.memory_id ?? ""));
|
|
1596
1602
|
if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
|
|
1597
|
-
if (!store
|
|
1603
|
+
if (!forgetTargetExists(store, memoryId, forgetSafetyFilters(body))) return c.json(...forgetNotFound(forgetSafetyFilters(body)));
|
|
1598
1604
|
const row = drizzle(store.db, { schema }).select().from(schema.memoryItems).where(eq(schema.memoryItems.id, memoryId)).get();
|
|
1599
1605
|
if (!row) return c.json({ error: "memory not found" }, 404);
|
|
1600
1606
|
if (!store.memoryOwnedBySelf(row)) return c.json({ error: "memory not owned by this device" }, 403);
|
|
@@ -1612,6 +1618,19 @@ function memoryRoutes(getStore) {
|
|
|
1612
1618
|
});
|
|
1613
1619
|
return app;
|
|
1614
1620
|
}
|
|
1621
|
+
function forgetNotFound(filters) {
|
|
1622
|
+
return [{ error: Object.keys(filters).length > 0 ? "not_found" : "memory not found" }, 404];
|
|
1623
|
+
}
|
|
1624
|
+
function forgetSafetyFilters(body) {
|
|
1625
|
+
const filters = {};
|
|
1626
|
+
if (typeof body.kind === "string" && body.kind.trim()) filters.kind = body.kind.trim();
|
|
1627
|
+
if (typeof body.project === "string" && body.project.trim()) filters.project = body.project.trim();
|
|
1628
|
+
return filters;
|
|
1629
|
+
}
|
|
1630
|
+
function forgetTargetExists(store, memoryId, filters) {
|
|
1631
|
+
if (Object.keys(filters).length > 0) return store.timeline(null, memoryId, 0, 0, filters).some((row) => row.id === memoryId);
|
|
1632
|
+
return store.get(memoryId) != null;
|
|
1633
|
+
}
|
|
1615
1634
|
//#endregion
|
|
1616
1635
|
//#region src/routes/memory-tools.ts
|
|
1617
1636
|
/**
|
|
@@ -2129,6 +2148,15 @@ function buildFailureImpact(latestFailure, queueTotals, authBackoff) {
|
|
|
2129
2148
|
if (queueTotals.pending > 0) return `${queueTotals.pending} queued raw events across ${queueTotals.sessions} session(s) are waiting on a successful flush.`;
|
|
2130
2149
|
return "Failed flush batches are pending retry.";
|
|
2131
2150
|
}
|
|
2151
|
+
function readCaptureEnabled() {
|
|
2152
|
+
const value = process.env.CODEMEM_RAW_EVENTS?.trim().toLowerCase();
|
|
2153
|
+
if (!value) return null;
|
|
2154
|
+
return ![
|
|
2155
|
+
"0",
|
|
2156
|
+
"false",
|
|
2157
|
+
"off"
|
|
2158
|
+
].includes(value);
|
|
2159
|
+
}
|
|
2132
2160
|
function observerStatusRoutes(deps) {
|
|
2133
2161
|
const app = new Hono();
|
|
2134
2162
|
app.get("/api/observer-status", (c) => {
|
|
@@ -2138,6 +2166,7 @@ function observerStatusRoutes(deps) {
|
|
|
2138
2166
|
if (!store || typeof store.rawEventBacklogTotals !== "function") return c.json({
|
|
2139
2167
|
active: null,
|
|
2140
2168
|
available_credentials: {},
|
|
2169
|
+
capture_enabled: readCaptureEnabled(),
|
|
2141
2170
|
latest_failure: null,
|
|
2142
2171
|
queue: {
|
|
2143
2172
|
pending: 0,
|
|
@@ -2161,6 +2190,7 @@ function observerStatusRoutes(deps) {
|
|
|
2161
2190
|
return c.json({
|
|
2162
2191
|
active,
|
|
2163
2192
|
available_credentials: availableCredentials,
|
|
2193
|
+
capture_enabled: readCaptureEnabled(),
|
|
2164
2194
|
latest_failure: failureWithImpact,
|
|
2165
2195
|
queue: {
|
|
2166
2196
|
...queueTotals,
|
|
@@ -2240,6 +2270,34 @@ function validateViewerTarget(store, payload, options = {}) {
|
|
|
2240
2270
|
if ((options.requireCurrentIdentity || payload.identity_target != null) && !store.hasCurrentIdentity()) return conflict("viewer_identity_mismatch", "viewer identity does not match request");
|
|
2241
2271
|
return { ok: true };
|
|
2242
2272
|
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Middleware read of db_path / identity_target from the query string.
|
|
2275
|
+
* Native tools (pi extension) send targets as query params so a pre-handler
|
|
2276
|
+
* guard can reject a foreign viewer without consuming POST bodies. Lenient:
|
|
2277
|
+
* clients that omit targets (UI, MCP, curl) are unaffected.
|
|
2278
|
+
*/
|
|
2279
|
+
function viewerTargetQueryGuard(getStore) {
|
|
2280
|
+
return async (c, next) => {
|
|
2281
|
+
const dbPath = c.req.query("db_path");
|
|
2282
|
+
const rawIdentity = c.req.query("identity_target");
|
|
2283
|
+
if (dbPath == null && rawIdentity == null) return next();
|
|
2284
|
+
let identityTarget;
|
|
2285
|
+
if (rawIdentity != null) try {
|
|
2286
|
+
identityTarget = JSON.parse(rawIdentity);
|
|
2287
|
+
} catch {
|
|
2288
|
+
return c.json({ error: {
|
|
2289
|
+
code: "invalid_request",
|
|
2290
|
+
message: "identity_target must be JSON"
|
|
2291
|
+
} }, 400);
|
|
2292
|
+
}
|
|
2293
|
+
const target = validateViewerTarget(getStore(), {
|
|
2294
|
+
db_path: dbPath ?? void 0,
|
|
2295
|
+
identity_target: identityTarget
|
|
2296
|
+
});
|
|
2297
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
2298
|
+
return next();
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2243
2301
|
//#endregion
|
|
2244
2302
|
//#region src/routes/pack.ts
|
|
2245
2303
|
var MAX_LEDGER_PAYLOAD_BYTES = 16384;
|
|
@@ -2675,21 +2733,26 @@ function resolveRawEventInboxDirectory(dbPath, homeDir = homedir()) {
|
|
|
2675
2733
|
const target = createHash("sha256").update(resolve(dbPath)).digest("hex").slice(0, 24);
|
|
2676
2734
|
return join(homeDir, ".codemem", INBOX_DIRECTORY_NAME, target);
|
|
2677
2735
|
}
|
|
2678
|
-
function entryContent(request, flushBoundary) {
|
|
2736
|
+
function entryContent(request, flushBoundary, boundary) {
|
|
2679
2737
|
return {
|
|
2680
2738
|
version: 1,
|
|
2681
2739
|
request,
|
|
2682
|
-
flush_boundary: flushBoundary
|
|
2740
|
+
flush_boundary: flushBoundary,
|
|
2741
|
+
...boundary ? { boundary: {
|
|
2742
|
+
source: boundary.source,
|
|
2743
|
+
stream_id: boundary.streamId,
|
|
2744
|
+
...boundary.id ? { id: boundary.id } : {}
|
|
2745
|
+
} } : {}
|
|
2683
2746
|
};
|
|
2684
2747
|
}
|
|
2685
|
-
function storedEntry(request, flushBoundary, enqueueOrder) {
|
|
2748
|
+
function storedEntry(request, flushBoundary, boundary, enqueueOrder) {
|
|
2686
2749
|
return {
|
|
2687
|
-
...entryContent(request, flushBoundary),
|
|
2750
|
+
...entryContent(request, flushBoundary, boundary),
|
|
2688
2751
|
enqueue_order: enqueueOrder.toString()
|
|
2689
2752
|
};
|
|
2690
2753
|
}
|
|
2691
2754
|
function entryContentId(entry) {
|
|
2692
|
-
return contentId(JSON.stringify(entryContent(entry.request, entry.flushBoundary)));
|
|
2755
|
+
return contentId(JSON.stringify(entryContent(entry.request, entry.flushBoundary, entry.boundary)));
|
|
2693
2756
|
}
|
|
2694
2757
|
function inboxFileName(enqueueOrder, id) {
|
|
2695
2758
|
return `${enqueueOrder.toString().padStart(24, "0")}-${id}.json`;
|
|
@@ -2749,10 +2812,17 @@ async function ensurePrivateDirectory(path) {
|
|
|
2749
2812
|
function parseStoredEntry(serialized) {
|
|
2750
2813
|
const parsed = JSON.parse(serialized);
|
|
2751
2814
|
if (parsed.version !== 1 || typeof parsed.enqueue_order !== "string" || !/^[1-9]\d*$/u.test(parsed.enqueue_order) || parsed.request == null || typeof parsed.request !== "object" || Array.isArray(parsed.request) || typeof parsed.flush_boundary !== "boolean") throw new Error("invalid raw-event inbox entry");
|
|
2815
|
+
const storedBoundary = parsed.boundary;
|
|
2816
|
+
if (storedBoundary !== void 0 && (typeof storedBoundary.source !== "string" || storedBoundary.source.trim() === "" || typeof storedBoundary.stream_id !== "string" || storedBoundary.stream_id.trim() === "" || storedBoundary.id !== void 0 && (typeof storedBoundary.id !== "string" || storedBoundary.id.trim() === ""))) throw new Error("invalid raw-event inbox boundary");
|
|
2752
2817
|
return {
|
|
2753
2818
|
entry: {
|
|
2754
2819
|
request: parsed.request,
|
|
2755
|
-
flushBoundary: parsed.flush_boundary
|
|
2820
|
+
flushBoundary: parsed.flush_boundary,
|
|
2821
|
+
...storedBoundary ? { boundary: {
|
|
2822
|
+
source: storedBoundary.source,
|
|
2823
|
+
streamId: storedBoundary.stream_id,
|
|
2824
|
+
...storedBoundary.id ? { id: storedBoundary.id } : {}
|
|
2825
|
+
} } : {}
|
|
2756
2826
|
},
|
|
2757
2827
|
enqueueOrder: BigInt(parsed.enqueue_order)
|
|
2758
2828
|
};
|
|
@@ -2803,7 +2873,8 @@ var FileRawEventInbox = class {
|
|
|
2803
2873
|
const flushBoundary = options.flushBoundary === true;
|
|
2804
2874
|
const id = entryContentId({
|
|
2805
2875
|
request,
|
|
2806
|
-
flushBoundary
|
|
2876
|
+
flushBoundary,
|
|
2877
|
+
boundary: options.boundary
|
|
2807
2878
|
});
|
|
2808
2879
|
await ensurePrivateDirectory(this.directory);
|
|
2809
2880
|
const entries = await readdir(this.directory, { withFileTypes: true });
|
|
@@ -2823,7 +2894,7 @@ var FileRawEventInbox = class {
|
|
|
2823
2894
|
throw error;
|
|
2824
2895
|
}
|
|
2825
2896
|
const enqueueOrder = await this.allocateEnqueueOrder(entries);
|
|
2826
|
-
const serialized = JSON.stringify(storedEntry(request, flushBoundary, enqueueOrder));
|
|
2897
|
+
const serialized = JSON.stringify(storedEntry(request, flushBoundary, options.boundary, enqueueOrder));
|
|
2827
2898
|
const destination = join(this.directory, inboxFileName(enqueueOrder, id));
|
|
2828
2899
|
const temporary = join(this.directory, `.${id}.${process.pid}.${randomUUID()}.tmp`);
|
|
2829
2900
|
try {
|
|
@@ -3024,10 +3095,11 @@ function createViewerRawEventInbox(options) {
|
|
|
3024
3095
|
onCorruptEntries: options.onCorruptEntries,
|
|
3025
3096
|
onBacklog: options.onBacklog,
|
|
3026
3097
|
onBacklogRecovered: options.onBacklogRecovered,
|
|
3027
|
-
processEntry: async ({ request, flushBoundary }) => {
|
|
3098
|
+
processEntry: async ({ request, flushBoundary, boundary }) => {
|
|
3028
3099
|
const result = ingestRawEvents(store, request);
|
|
3029
3100
|
nudgeRawEventSessions(options.sweeper, result.sessions);
|
|
3030
|
-
if (
|
|
3101
|
+
if (boundary) await options.sweeper?.flushBoundary(boundary.streamId, boundary.source);
|
|
3102
|
+
else if (flushBoundary && isClaudeBoundaryEnvelope(request)) await flushRawEventBoundarySessions(options.sweeper, result.sessions);
|
|
3031
3103
|
}
|
|
3032
3104
|
});
|
|
3033
3105
|
return {
|
|
@@ -3155,15 +3227,94 @@ async function enqueueRawEventRequest(options) {
|
|
|
3155
3227
|
if (!target.ok) return options.c.json(target.body, target.status);
|
|
3156
3228
|
const validation = validateRawEvents(options.request);
|
|
3157
3229
|
try {
|
|
3158
|
-
await options.inbox.enqueue(validation.request,
|
|
3230
|
+
await options.inbox.enqueue(validation.request, options.boundary ? {
|
|
3231
|
+
flushBoundary: options.flushBoundary,
|
|
3232
|
+
boundary: options.boundary
|
|
3233
|
+
} : { flushBoundary: options.flushBoundary });
|
|
3159
3234
|
} catch (error) {
|
|
3160
3235
|
return boundedInboxErrorResponse(options.c, error);
|
|
3161
3236
|
}
|
|
3237
|
+
const accepted = options.acceptedCount ?? validation.received;
|
|
3162
3238
|
return options.c.json({
|
|
3163
|
-
accepted
|
|
3164
|
-
queued:
|
|
3239
|
+
accepted,
|
|
3240
|
+
queued: accepted
|
|
3165
3241
|
}, 202);
|
|
3166
3242
|
}
|
|
3243
|
+
function piEventName(payload) {
|
|
3244
|
+
for (const key of [
|
|
3245
|
+
"piEvent",
|
|
3246
|
+
"pi_event",
|
|
3247
|
+
"event",
|
|
3248
|
+
"type"
|
|
3249
|
+
]) {
|
|
3250
|
+
const value = payload[key];
|
|
3251
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
3252
|
+
}
|
|
3253
|
+
return "";
|
|
3254
|
+
}
|
|
3255
|
+
function piInboxBoundary(payload, envelope, signal) {
|
|
3256
|
+
if (!["session_before_compact", "session_shutdown"].includes(piEventName(payload))) return;
|
|
3257
|
+
const streamId = envelope?.session_stream_id ?? signal?.session_id;
|
|
3258
|
+
if (!streamId) return;
|
|
3259
|
+
const envelopeTimestamp = envelope?.payload.timestamp;
|
|
3260
|
+
const id = typeof envelopeTimestamp === "string" ? envelopeTimestamp : signal?.ts;
|
|
3261
|
+
return {
|
|
3262
|
+
source: "pi",
|
|
3263
|
+
streamId,
|
|
3264
|
+
...id ? { id } : {}
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
async function postPiHookRequest(c, getStore, sweeper, inbox, inboxTarget) {
|
|
3268
|
+
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
3269
|
+
if (result instanceof Response) return result;
|
|
3270
|
+
const payload = result;
|
|
3271
|
+
try {
|
|
3272
|
+
const target = validateViewerTarget(inbox && inboxTarget ? inboxTarget : getStore(), payload, { requirePairedTargets: true });
|
|
3273
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
3274
|
+
const untargeted = untargetedPayload(payload);
|
|
3275
|
+
const envelope = buildRawEventEnvelopeFromPiEvent(untargeted);
|
|
3276
|
+
const signal = buildPiFlushSignalFromEvent(untargeted);
|
|
3277
|
+
const boundary = piInboxBoundary(untargeted, envelope, signal);
|
|
3278
|
+
if (envelope === null && signal === null) return c.json({
|
|
3279
|
+
inserted: 0,
|
|
3280
|
+
skipped: 1
|
|
3281
|
+
});
|
|
3282
|
+
if (inbox) return await enqueueRawEventRequest({
|
|
3283
|
+
c,
|
|
3284
|
+
getStore,
|
|
3285
|
+
inbox,
|
|
3286
|
+
inboxTarget,
|
|
3287
|
+
payload,
|
|
3288
|
+
request: envelope ? {
|
|
3289
|
+
...envelope,
|
|
3290
|
+
source: "pi"
|
|
3291
|
+
} : {
|
|
3292
|
+
source: "pi",
|
|
3293
|
+
session_stream_id: signal?.session_id,
|
|
3294
|
+
events: []
|
|
3295
|
+
},
|
|
3296
|
+
flushBoundary: false,
|
|
3297
|
+
...boundary ? {
|
|
3298
|
+
boundary,
|
|
3299
|
+
acceptedCount: 1
|
|
3300
|
+
} : {}
|
|
3301
|
+
});
|
|
3302
|
+
if (envelope === null) return c.json({
|
|
3303
|
+
inserted: 0,
|
|
3304
|
+
skipped: 1
|
|
3305
|
+
});
|
|
3306
|
+
const ingestResult = await ingestNormalizedEnvelope(getStore(), sweeper, {
|
|
3307
|
+
...envelope,
|
|
3308
|
+
source: "pi"
|
|
3309
|
+
});
|
|
3310
|
+
return c.json({
|
|
3311
|
+
inserted: ingestResult.inserted,
|
|
3312
|
+
skipped: ingestResult.skipped
|
|
3313
|
+
});
|
|
3314
|
+
} catch (error) {
|
|
3315
|
+
return boundedIngestErrorResponse(c, error);
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3167
3318
|
async function postRawEventRequest(c, getStore, sweeper, inbox, inboxTarget) {
|
|
3168
3319
|
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
3169
3320
|
if (result instanceof Response) return result;
|
|
@@ -3197,6 +3348,81 @@ async function ingestNormalizedEnvelope(store, sweeper, envelope, flushBoundary
|
|
|
3197
3348
|
if (flushBoundary && isClaudeBoundaryEnvelope(envelope)) await flushRawEventBoundarySessions(sweeper, result.sessions);
|
|
3198
3349
|
return result;
|
|
3199
3350
|
}
|
|
3351
|
+
async function postClaudeHookRequest(c, getStore, sweeper, inbox, inboxTarget, transcriptDiagnostics) {
|
|
3352
|
+
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
3353
|
+
if (result instanceof Response) return result;
|
|
3354
|
+
const payload = result;
|
|
3355
|
+
try {
|
|
3356
|
+
const target = validateViewerTarget(inbox && inboxTarget ? inboxTarget : getStore(), payload, { requirePairedTargets: true });
|
|
3357
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
3358
|
+
let transcriptOutcome = null;
|
|
3359
|
+
const envelope = buildRawEventEnvelopeFromHook(untargetedPayload(payload), {
|
|
3360
|
+
transcriptPolicy: {
|
|
3361
|
+
trust: "restricted",
|
|
3362
|
+
approvedRoots: [claudeTranscriptRoot()]
|
|
3363
|
+
},
|
|
3364
|
+
onTranscriptOutcome: (outcome) => {
|
|
3365
|
+
transcriptOutcome = outcome;
|
|
3366
|
+
transcriptDiagnostics.record("claude", outcome);
|
|
3367
|
+
}
|
|
3368
|
+
});
|
|
3369
|
+
if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
|
|
3370
|
+
const flushBoundary = c.req.header("x-codemem-boundary-flush") === "1";
|
|
3371
|
+
if (inbox) return await enqueueRawEventRequest({
|
|
3372
|
+
c,
|
|
3373
|
+
getStore,
|
|
3374
|
+
inbox,
|
|
3375
|
+
inboxTarget,
|
|
3376
|
+
payload,
|
|
3377
|
+
request: { ...envelope },
|
|
3378
|
+
flushBoundary
|
|
3379
|
+
});
|
|
3380
|
+
const ingestResult = await ingestNormalizedEnvelope(getStore(), sweeper, envelope, flushBoundary);
|
|
3381
|
+
return c.json({
|
|
3382
|
+
inserted: ingestResult.inserted,
|
|
3383
|
+
skipped: ingestResult.skipped
|
|
3384
|
+
});
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
return boundedIngestErrorResponse(c, error);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
async function postCodexHookRequest(c, getStore, sweeper, inbox, inboxTarget, transcriptDiagnostics) {
|
|
3390
|
+
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
3391
|
+
if (result instanceof Response) return result;
|
|
3392
|
+
const payload = result;
|
|
3393
|
+
try {
|
|
3394
|
+
const target = validateViewerTarget(inbox && inboxTarget ? inboxTarget : getStore(), payload, { requirePairedTargets: true });
|
|
3395
|
+
if (!target.ok) return c.json(target.body, target.status);
|
|
3396
|
+
let transcriptOutcome = null;
|
|
3397
|
+
const envelope = buildRawEventEnvelopeFromCodexHook(untargetedPayload(payload), {
|
|
3398
|
+
transcriptPolicy: {
|
|
3399
|
+
trust: "restricted",
|
|
3400
|
+
approvedRoots: [codexTranscriptRoot()]
|
|
3401
|
+
},
|
|
3402
|
+
onTranscriptOutcome: (outcome) => {
|
|
3403
|
+
transcriptOutcome = outcome;
|
|
3404
|
+
transcriptDiagnostics.record("codex", outcome);
|
|
3405
|
+
}
|
|
3406
|
+
});
|
|
3407
|
+
if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
|
|
3408
|
+
if (inbox) return await enqueueRawEventRequest({
|
|
3409
|
+
c,
|
|
3410
|
+
getStore,
|
|
3411
|
+
inbox,
|
|
3412
|
+
inboxTarget,
|
|
3413
|
+
payload,
|
|
3414
|
+
request: { ...envelope },
|
|
3415
|
+
flushBoundary: false
|
|
3416
|
+
});
|
|
3417
|
+
const ingestResult = await ingestNormalizedEnvelope(getStore(), sweeper, envelope);
|
|
3418
|
+
return c.json({
|
|
3419
|
+
inserted: ingestResult.inserted,
|
|
3420
|
+
skipped: ingestResult.skipped
|
|
3421
|
+
});
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
return boundedIngestErrorResponse(c, error);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3200
3426
|
function rawEventsRoutes(getStore, sweeper, inbox, inboxTarget) {
|
|
3201
3427
|
const app = new Hono();
|
|
3202
3428
|
const transcriptDiagnostics = createTranscriptDiagnostics();
|
|
@@ -3248,89 +3474,9 @@ function rawEventsRoutes(getStore, sweeper, inbox, inboxTarget) {
|
|
|
3248
3474
|
});
|
|
3249
3475
|
});
|
|
3250
3476
|
app.post("/api/raw-events", (c) => postRawEventRequest(c, getStore, sweeper, inbox, inboxTarget));
|
|
3251
|
-
app.post("/api/claude-hooks",
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
const payload = result;
|
|
3255
|
-
try {
|
|
3256
|
-
const store = getStore();
|
|
3257
|
-
const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
|
|
3258
|
-
if (!target.ok) return c.json(target.body, target.status);
|
|
3259
|
-
let transcriptOutcome = null;
|
|
3260
|
-
const envelope = buildRawEventEnvelopeFromHook(untargetedPayload(payload), {
|
|
3261
|
-
transcriptPolicy: {
|
|
3262
|
-
trust: "restricted",
|
|
3263
|
-
approvedRoots: [claudeTranscriptRoot()]
|
|
3264
|
-
},
|
|
3265
|
-
onTranscriptOutcome: (outcome) => {
|
|
3266
|
-
transcriptOutcome = outcome;
|
|
3267
|
-
transcriptDiagnostics.record("claude", outcome);
|
|
3268
|
-
}
|
|
3269
|
-
});
|
|
3270
|
-
if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
|
|
3271
|
-
const ingestResult = await ingestNormalizedEnvelope(store, sweeper, envelope, c.req.header("x-codemem-boundary-flush") === "1");
|
|
3272
|
-
return c.json({
|
|
3273
|
-
inserted: ingestResult.inserted,
|
|
3274
|
-
skipped: ingestResult.skipped
|
|
3275
|
-
});
|
|
3276
|
-
} catch (err) {
|
|
3277
|
-
return boundedIngestErrorResponse(c, err);
|
|
3278
|
-
}
|
|
3279
|
-
});
|
|
3280
|
-
app.post("/api/codex-hooks", async (c) => {
|
|
3281
|
-
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
3282
|
-
if (result instanceof Response) return result;
|
|
3283
|
-
const payload = result;
|
|
3284
|
-
try {
|
|
3285
|
-
const store = getStore();
|
|
3286
|
-
const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
|
|
3287
|
-
if (!target.ok) return c.json(target.body, target.status);
|
|
3288
|
-
let transcriptOutcome = null;
|
|
3289
|
-
const envelope = buildRawEventEnvelopeFromCodexHook(untargetedPayload(payload), {
|
|
3290
|
-
transcriptPolicy: {
|
|
3291
|
-
trust: "restricted",
|
|
3292
|
-
approvedRoots: [codexTranscriptRoot()]
|
|
3293
|
-
},
|
|
3294
|
-
onTranscriptOutcome: (outcome) => {
|
|
3295
|
-
transcriptOutcome = outcome;
|
|
3296
|
-
transcriptDiagnostics.record("codex", outcome);
|
|
3297
|
-
}
|
|
3298
|
-
});
|
|
3299
|
-
if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
|
|
3300
|
-
const ingestResult = await ingestNormalizedEnvelope(store, sweeper, envelope);
|
|
3301
|
-
return c.json({
|
|
3302
|
-
inserted: ingestResult.inserted,
|
|
3303
|
-
skipped: ingestResult.skipped
|
|
3304
|
-
});
|
|
3305
|
-
} catch (err) {
|
|
3306
|
-
return boundedIngestErrorResponse(c, err);
|
|
3307
|
-
}
|
|
3308
|
-
});
|
|
3309
|
-
app.post("/api/pi-hooks", async (c) => {
|
|
3310
|
-
const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
|
|
3311
|
-
if (result instanceof Response) return result;
|
|
3312
|
-
const payload = result;
|
|
3313
|
-
try {
|
|
3314
|
-
const store = getStore();
|
|
3315
|
-
const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
|
|
3316
|
-
if (!target.ok) return c.json(target.body, target.status);
|
|
3317
|
-
const envelope = buildRawEventEnvelopeFromPiEvent(untargetedPayload(payload));
|
|
3318
|
-
if (envelope === null) return c.json({
|
|
3319
|
-
inserted: 0,
|
|
3320
|
-
skipped: 1
|
|
3321
|
-
});
|
|
3322
|
-
const ingestResult = await ingestNormalizedEnvelope(store, sweeper, {
|
|
3323
|
-
...envelope,
|
|
3324
|
-
source: "pi"
|
|
3325
|
-
});
|
|
3326
|
-
return c.json({
|
|
3327
|
-
inserted: ingestResult.inserted,
|
|
3328
|
-
skipped: ingestResult.skipped
|
|
3329
|
-
});
|
|
3330
|
-
} catch (err) {
|
|
3331
|
-
return boundedIngestErrorResponse(c, err);
|
|
3332
|
-
}
|
|
3333
|
-
});
|
|
3477
|
+
app.post("/api/claude-hooks", (c) => postClaudeHookRequest(c, getStore, sweeper, inbox, inboxTarget, transcriptDiagnostics));
|
|
3478
|
+
app.post("/api/codex-hooks", (c) => postCodexHookRequest(c, getStore, sweeper, inbox, inboxTarget, transcriptDiagnostics));
|
|
3479
|
+
app.post("/api/pi-hooks", (c) => postPiHookRequest(c, getStore, sweeper, inbox, inboxTarget));
|
|
3334
3480
|
return app;
|
|
3335
3481
|
}
|
|
3336
3482
|
//#endregion
|
|
@@ -4231,6 +4377,40 @@ var PEERS_QUERY$1 = `
|
|
|
4231
4377
|
ORDER BY name, peer_device_id
|
|
4232
4378
|
`;
|
|
4233
4379
|
//#endregion
|
|
4380
|
+
//#region src/snapshot-source.ts
|
|
4381
|
+
function snapshotGeneration(raw) {
|
|
4382
|
+
if (!raw?.trim()) return null;
|
|
4383
|
+
return Number.parseInt(raw, 10);
|
|
4384
|
+
}
|
|
4385
|
+
function snapshotSourceDevice(requested, localDeviceId) {
|
|
4386
|
+
if (requested !== void 0 && requested !== localDeviceId) throw new Error("retirement_snapshot_source_required");
|
|
4387
|
+
return requested;
|
|
4388
|
+
}
|
|
4389
|
+
function snapshotErrorStatus(message) {
|
|
4390
|
+
if ([
|
|
4391
|
+
"retirement_snapshot_source_required",
|
|
4392
|
+
"missing_generation",
|
|
4393
|
+
"missing_snapshot_id"
|
|
4394
|
+
].includes(message)) return 400;
|
|
4395
|
+
if (message === "generation_mismatch" || message === "boundary_mismatch") return 409;
|
|
4396
|
+
return null;
|
|
4397
|
+
}
|
|
4398
|
+
function snapshotPageRequest(query, localDeviceId) {
|
|
4399
|
+
const generation = snapshotGeneration(query("generation"));
|
|
4400
|
+
const snapshotId = query("snapshot_id");
|
|
4401
|
+
if (generation === null || !Number.isFinite(generation)) throw new Error("missing_generation");
|
|
4402
|
+
if (!snapshotId) throw new Error("missing_snapshot_id");
|
|
4403
|
+
const rawLimit = Number.parseInt(query("limit") ?? "200", 10);
|
|
4404
|
+
return {
|
|
4405
|
+
generation,
|
|
4406
|
+
snapshotId,
|
|
4407
|
+
baselineCursor: query("baseline_cursor") ?? null,
|
|
4408
|
+
pageToken: query("page_token") ?? null,
|
|
4409
|
+
sourceDeviceId: snapshotSourceDevice(query("source_device_id"), localDeviceId),
|
|
4410
|
+
limit: Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 5e3)) : 200
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4413
|
+
//#endregion
|
|
4234
4414
|
//#region src/routes/team-setup-view.ts
|
|
4235
4415
|
var enabled = {
|
|
4236
4416
|
enabled: true,
|
|
@@ -6176,6 +6356,26 @@ function tryParsePairingPayload(value) {
|
|
|
6176
6356
|
addresses
|
|
6177
6357
|
};
|
|
6178
6358
|
}
|
|
6359
|
+
async function inspectPairingInvite(c, next) {
|
|
6360
|
+
const body = await parseViewerJsonBody(c);
|
|
6361
|
+
if (typeof body?.invite !== "string") return c.json({ error: "invite_invalid" }, 400);
|
|
6362
|
+
try {
|
|
6363
|
+
const pairing = tryParsePairingPayload(body.invite.match(/echo\s+['"]([A-Za-z0-9+/=]+)['"]\s*\|\s*base64\s+-d\s*\|\s*codemem/)?.[1]?.trim() || body.invite.trim());
|
|
6364
|
+
if (pairing?.kind === "invalid-pair") return c.json({ error: pairing.error }, 400);
|
|
6365
|
+
if (pairing?.kind !== "pair") {
|
|
6366
|
+
decodeInvitePayload(extractInvitePayload(body.invite));
|
|
6367
|
+
return next();
|
|
6368
|
+
}
|
|
6369
|
+
return c.json({
|
|
6370
|
+
kind: "pair",
|
|
6371
|
+
device_id: pairing.device_id,
|
|
6372
|
+
fingerprint: pairing.fingerprint,
|
|
6373
|
+
addresses: pairing.addresses
|
|
6374
|
+
});
|
|
6375
|
+
} catch (error) {
|
|
6376
|
+
return c.json({ error: safeRecipientInviteError(error) }, 400);
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6179
6379
|
function coordinatorAdminStatusPayload(config = readCoordinatorSyncConfig()) {
|
|
6180
6380
|
const groups = config.syncCoordinatorGroups;
|
|
6181
6381
|
const activeGroup = groups[0] ?? null;
|
|
@@ -8750,6 +8950,42 @@ function negotiatedSyncCapability(c) {
|
|
|
8750
8950
|
* network-accessible. All requests are auth-gated via signature
|
|
8751
8951
|
* verification so unauthenticated callers are rejected.
|
|
8752
8952
|
*/
|
|
8953
|
+
function serveAuthorizedSnapshot(c, store, peerDeviceId) {
|
|
8954
|
+
try {
|
|
8955
|
+
const rawScopeId = c.req.query(SYNC_SCOPE_QUERY_PARAM);
|
|
8956
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
8957
|
+
const scopeRequest = parseSyncScopeRequest(rawScopeId, rawScopeId !== void 0, {
|
|
8958
|
+
db: store.db,
|
|
8959
|
+
localDeviceId,
|
|
8960
|
+
negotiatedCapability: negotiatedSyncCapability(c),
|
|
8961
|
+
peerDeviceId
|
|
8962
|
+
});
|
|
8963
|
+
if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
|
|
8964
|
+
const pageRequest = snapshotPageRequest((name) => c.req.query(name), localDeviceId);
|
|
8965
|
+
const result = loadMemorySnapshotPageForPeer(store.db, {
|
|
8966
|
+
...pageRequest,
|
|
8967
|
+
peerDeviceId,
|
|
8968
|
+
scopeId: scopeRequest.mode === "scoped" ? scopeRequest.scope_id : void 0
|
|
8969
|
+
});
|
|
8970
|
+
return c.json({
|
|
8971
|
+
source_device_id: pageRequest.sourceDeviceId,
|
|
8972
|
+
scope_id: scopeRequest.scope_id,
|
|
8973
|
+
generation: result.boundary.generation,
|
|
8974
|
+
snapshot_id: result.boundary.snapshot_id,
|
|
8975
|
+
baseline_cursor: result.boundary.baseline_cursor,
|
|
8976
|
+
retained_floor_cursor: result.boundary.retained_floor_cursor,
|
|
8977
|
+
sync_capability: LOCAL_SYNC_CAPABILITY,
|
|
8978
|
+
items: result.items,
|
|
8979
|
+
next_page_token: result.nextPageToken,
|
|
8980
|
+
has_more: result.hasMore
|
|
8981
|
+
});
|
|
8982
|
+
} catch (err) {
|
|
8983
|
+
const message = err instanceof Error ? err.message : "";
|
|
8984
|
+
const status = snapshotErrorStatus(message);
|
|
8985
|
+
if (status) return c.json({ error: message }, status);
|
|
8986
|
+
return c.json({ error: "internal_error" }, 500);
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8753
8989
|
function syncProtocolRoutes(getStore, opts = {}) {
|
|
8754
8990
|
const app = new Hono();
|
|
8755
8991
|
const routeRateLimit = opts.routeRateLimit ?? null;
|
|
@@ -8916,51 +9152,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
8916
9152
|
}
|
|
8917
9153
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
8918
9154
|
if (limited) return limited;
|
|
8919
|
-
|
|
8920
|
-
const rawScopeId = c.req.query(SYNC_SCOPE_QUERY_PARAM);
|
|
8921
|
-
const negotiated = negotiatedSyncCapability(c);
|
|
8922
|
-
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
8923
|
-
const scopeRequest = parseSyncScopeRequest(rawScopeId, rawScopeId !== void 0, {
|
|
8924
|
-
db: store.db,
|
|
8925
|
-
localDeviceId,
|
|
8926
|
-
negotiatedCapability: negotiated,
|
|
8927
|
-
peerDeviceId: auth.deviceId
|
|
8928
|
-
});
|
|
8929
|
-
if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
|
|
8930
|
-
const rawGeneration = c.req.query("generation");
|
|
8931
|
-
const generation = rawGeneration != null && rawGeneration.trim().length > 0 ? Number.parseInt(rawGeneration, 10) : null;
|
|
8932
|
-
const snapshotId = c.req.query("snapshot_id") ?? null;
|
|
8933
|
-
const baselineCursor = c.req.query("baseline_cursor") ?? null;
|
|
8934
|
-
const pageToken = c.req.query("page_token") ?? null;
|
|
8935
|
-
const rawLimit = Number.parseInt(c.req.query("limit") ?? "200", 10);
|
|
8936
|
-
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 5e3)) : 200;
|
|
8937
|
-
if (generation == null || !Number.isFinite(generation)) return c.json({ error: "missing_generation" }, 400);
|
|
8938
|
-
if (!snapshotId) return c.json({ error: "missing_snapshot_id" }, 400);
|
|
8939
|
-
const result = loadMemorySnapshotPageForPeer(store.db, {
|
|
8940
|
-
generation,
|
|
8941
|
-
snapshotId,
|
|
8942
|
-
baselineCursor,
|
|
8943
|
-
pageToken,
|
|
8944
|
-
limit,
|
|
8945
|
-
peerDeviceId: auth.deviceId,
|
|
8946
|
-
scopeId: scopeRequest.mode === "scoped" ? scopeRequest.scope_id : void 0
|
|
8947
|
-
});
|
|
8948
|
-
return c.json({
|
|
8949
|
-
scope_id: scopeRequest.scope_id,
|
|
8950
|
-
generation: result.boundary.generation,
|
|
8951
|
-
snapshot_id: result.boundary.snapshot_id,
|
|
8952
|
-
baseline_cursor: result.boundary.baseline_cursor,
|
|
8953
|
-
retained_floor_cursor: result.boundary.retained_floor_cursor,
|
|
8954
|
-
sync_capability: LOCAL_SYNC_CAPABILITY,
|
|
8955
|
-
items: result.items,
|
|
8956
|
-
next_page_token: result.nextPageToken,
|
|
8957
|
-
has_more: result.hasMore
|
|
8958
|
-
});
|
|
8959
|
-
} catch (err) {
|
|
8960
|
-
const message = err instanceof Error ? err.message : "";
|
|
8961
|
-
if (message === "generation_mismatch" || message === "boundary_mismatch") return c.json({ error: message }, 409);
|
|
8962
|
-
return c.json({ error: "internal_error" }, 500);
|
|
8963
|
-
}
|
|
9155
|
+
return serveAuthorizedSnapshot(c, store, auth.deviceId);
|
|
8964
9156
|
})();
|
|
8965
9157
|
});
|
|
8966
9158
|
app.post("/v1/ops", async (c) => {
|
|
@@ -9038,7 +9230,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
9038
9230
|
}
|
|
9039
9231
|
async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
9040
9232
|
const config = readCoordinatorSyncConfig();
|
|
9041
|
-
const hasCoordinatorConfiguration = config.syncCoordinatorUrl.length > 0 || config.syncCoordinatorGroups.length > 0;
|
|
9233
|
+
const hasCoordinatorConfiguration = config.syncCoordinatorUrl.length > 0 || config.syncCoordinatorGroups.length > 0 || config.syncCoordinatorAdminSecret.length > 0;
|
|
9042
9234
|
const remoteUrl = config.syncCoordinatorUrl.trim();
|
|
9043
9235
|
const adminSecret = config.syncCoordinatorAdminSecret.trim();
|
|
9044
9236
|
const groupIds = [...new Set(config.syncCoordinatorGroups.map((groupId) => groupId.trim()))].filter(Boolean);
|
|
@@ -9049,7 +9241,7 @@ async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
|
9049
9241
|
};
|
|
9050
9242
|
if (!remoteUrl || !adminSecret || groupIds.length === 0) return {
|
|
9051
9243
|
availability: "unavailable",
|
|
9052
|
-
safeErrorCode: "
|
|
9244
|
+
safeErrorCode: "coordinator_configuration_required",
|
|
9053
9245
|
enrollments: []
|
|
9054
9246
|
};
|
|
9055
9247
|
if (groupIds.length > 25) return {
|
|
@@ -9082,6 +9274,61 @@ async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
|
9082
9274
|
};
|
|
9083
9275
|
}
|
|
9084
9276
|
}
|
|
9277
|
+
var ORIGIN_DEVICE_NAME_LOOKUP_BATCH_SIZE = 400;
|
|
9278
|
+
function originDeviceNameKey(value) {
|
|
9279
|
+
return value.normalize("NFKC").toLowerCase().toUpperCase().toLowerCase();
|
|
9280
|
+
}
|
|
9281
|
+
function originDeviceDisplayName(value, deviceId) {
|
|
9282
|
+
if (typeof value !== "string") return null;
|
|
9283
|
+
try {
|
|
9284
|
+
const displayName = normalizeHumanPresentationName(value, "display_name");
|
|
9285
|
+
let normalizedDeviceId = deviceId;
|
|
9286
|
+
try {
|
|
9287
|
+
normalizedDeviceId = normalizeHumanPresentationName(deviceId, "device_id");
|
|
9288
|
+
} catch {}
|
|
9289
|
+
return originDeviceNameKey(displayName) === originDeviceNameKey(normalizedDeviceId) ? null : displayName;
|
|
9290
|
+
} catch {
|
|
9291
|
+
return null;
|
|
9292
|
+
}
|
|
9293
|
+
}
|
|
9294
|
+
function resolveOriginDeviceDisplayNames(store, projects) {
|
|
9295
|
+
const deviceIds = [...new Set(projects.flatMap((project) => project.origin_devices.map((device) => device.device_id)))];
|
|
9296
|
+
const names = /* @__PURE__ */ new Map();
|
|
9297
|
+
for (let offset = 0; offset < deviceIds.length; offset += ORIGIN_DEVICE_NAME_LOOKUP_BATCH_SIZE) {
|
|
9298
|
+
const batch = deviceIds.slice(offset, offset + ORIGIN_DEVICE_NAME_LOOKUP_BATCH_SIZE);
|
|
9299
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
9300
|
+
const peers = store.db.prepare(`SELECT peer_device_id AS device_id, name AS display_name
|
|
9301
|
+
FROM sync_peers
|
|
9302
|
+
WHERE peer_device_id IN (${placeholders})
|
|
9303
|
+
AND TRIM(COALESCE(pinned_fingerprint, '')) <> ''`).all(...batch);
|
|
9304
|
+
for (const peer of peers) {
|
|
9305
|
+
const displayName = originDeviceDisplayName(peer.display_name, peer.device_id);
|
|
9306
|
+
if (displayName) names.set(peer.device_id, displayName);
|
|
9307
|
+
}
|
|
9308
|
+
const devices = store.db.prepare(`SELECT device_id, display_name
|
|
9309
|
+
FROM identity_devices
|
|
9310
|
+
WHERE device_id IN (${placeholders}) AND status = 'active'`).all(...batch);
|
|
9311
|
+
for (const device of devices) {
|
|
9312
|
+
const displayName = originDeviceDisplayName(device.display_name, device.device_id);
|
|
9313
|
+
if (displayName) names.set(device.device_id, displayName);
|
|
9314
|
+
}
|
|
9315
|
+
}
|
|
9316
|
+
return names;
|
|
9317
|
+
}
|
|
9318
|
+
function serializeProjectScopeInventory(store, inventory, sharingByProject) {
|
|
9319
|
+
const originDeviceNames = resolveOriginDeviceDisplayNames(store, inventory.projects);
|
|
9320
|
+
return {
|
|
9321
|
+
...inventory,
|
|
9322
|
+
projects: inventory.projects.map((project) => ({
|
|
9323
|
+
...project,
|
|
9324
|
+
origin_devices: project.origin_devices.map(({ device_id }) => ({
|
|
9325
|
+
device_id,
|
|
9326
|
+
display_name: originDeviceNames.get(device_id) ?? null
|
|
9327
|
+
})),
|
|
9328
|
+
sharing: sharingByProject.get(project.workspace_identity) ?? []
|
|
9329
|
+
}))
|
|
9330
|
+
};
|
|
9331
|
+
}
|
|
9085
9332
|
/**
|
|
9086
9333
|
* Viewer-facing sync management routes (/api/sync/*).
|
|
9087
9334
|
*
|
|
@@ -9090,7 +9337,7 @@ async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
|
9090
9337
|
* local viewer.
|
|
9091
9338
|
*/
|
|
9092
9339
|
function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
9093
|
-
const app = new Hono();
|
|
9340
|
+
const app = new Hono().post("/api/sync/invites/inspect", inspectPairingInvite);
|
|
9094
9341
|
const readCoordinatorConfig = options.readCoordinatorConfig ?? readCoordinatorSyncConfig;
|
|
9095
9342
|
const renameCoordinatorGroup = options.renameCoordinatorGroup ?? coordinatorRenameGroupAction;
|
|
9096
9343
|
const loadCoordinatorEvidence = options.loadDeviceIdentityCoordinatorEvidence ?? loadConfiguredDeviceIdentityCoordinatorEvidence;
|
|
@@ -9735,13 +9982,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9735
9982
|
});
|
|
9736
9983
|
sharingByProject.set(item.canonical_project_identity, current);
|
|
9737
9984
|
}
|
|
9738
|
-
return c.json(
|
|
9739
|
-
...inventory,
|
|
9740
|
-
projects: inventory.projects.map((project) => ({
|
|
9741
|
-
...project,
|
|
9742
|
-
sharing: sharingByProject.get(project.workspace_identity) ?? []
|
|
9743
|
-
}))
|
|
9744
|
-
});
|
|
9985
|
+
return c.json(serializeProjectScopeInventory(store, inventory, sharingByProject));
|
|
9745
9986
|
});
|
|
9746
9987
|
app.post("/api/sync/projects/reassign-project", async (c) => {
|
|
9747
9988
|
const store = getStore();
|
|
@@ -11940,6 +12181,9 @@ function createApp(opts) {
|
|
|
11940
12181
|
const app = new Hono();
|
|
11941
12182
|
app.use("*", preflightHandler());
|
|
11942
12183
|
app.use("*", originGuard({ unsafeGetPathPrefixes: [TEAM_SETUP_ROUTE_PREFIX] }));
|
|
12184
|
+
const viewerTargetGuard = viewerTargetQueryGuard(storeFactory);
|
|
12185
|
+
app.use("/api/memories/*", viewerTargetGuard);
|
|
12186
|
+
app.use("/api/memory", viewerTargetGuard);
|
|
11943
12187
|
app.route("/", healthRoutes(storeFactory));
|
|
11944
12188
|
app.route("/", diagnosticsRoutes(storeFactory));
|
|
11945
12189
|
app.route("/", statsRoutes(storeFactory));
|