@codemem/server 0.46.0-alpha.2 → 0.46.0-beta.2
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/application/coordinator-maintenance.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +478 -182
- package/dist/index.js.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/sync.d.ts +36 -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 +64 -56
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/index.html +934 -298
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, analyzeProjectScopeMappingChangesGuardrails, analyzeProjectScopeMappingDeletionGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterClausesWithContext, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalRepositoryProjectIdentity, 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, repositoryIdentitiesByWorkspace, repositoryIdentityForWorkspace, 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, upsertProjectScopeSettingsMappings, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, validateRawEvents, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, wakeRecipientPoliciesForIdentities, wakeRecipientPoliciesForProjectIdentities } 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;
|
|
@@ -3789,6 +3847,8 @@ var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 3e4;
|
|
|
3789
3847
|
var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 3e5;
|
|
3790
3848
|
var TERMINAL_SHARE_MAINTENANCE_ERRORS = /* @__PURE__ */ new Set([
|
|
3791
3849
|
"coordinator_not_configured",
|
|
3850
|
+
"conflicting_repository_mappings",
|
|
3851
|
+
"project_mapping_conflict",
|
|
3792
3852
|
"team_sharing_not_configured",
|
|
3793
3853
|
"team_selection_ambiguous",
|
|
3794
3854
|
"initiating_device_not_reviewed",
|
|
@@ -3990,25 +4050,69 @@ async function advancePendingProjectSharesOperation(store, options) {
|
|
|
3990
4050
|
var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
|
|
3991
4051
|
var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
|
|
3992
4052
|
var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
|
|
4053
|
+
function recipientPolicyMaintenanceCandidates(store, retryBefore, limit) {
|
|
4054
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
4055
|
+
const canonicalize = (projectId) => canonicalRepositoryProjectIdentity(repositoryIdentities, projectId);
|
|
4056
|
+
const projectIds = store.db.prepare(`SELECT canonical_project_identity FROM project_recipients
|
|
4057
|
+
UNION SELECT canonical_project_identity FROM recipient_policy_authority_states`).all();
|
|
4058
|
+
const authorityRows = store.db.prepare(`SELECT authority_state, canonical_project_identity, last_attempt_at, safe_error_code
|
|
4059
|
+
FROM recipient_policy_authority_states`).all();
|
|
4060
|
+
const authorityByProject = /* @__PURE__ */ new Map();
|
|
4061
|
+
for (const row of authorityRows) {
|
|
4062
|
+
const projectId = canonicalize(row.canonical_project_identity);
|
|
4063
|
+
const current = authorityByProject.get(projectId);
|
|
4064
|
+
if (!current) {
|
|
4065
|
+
authorityByProject.set(projectId, row);
|
|
4066
|
+
continue;
|
|
4067
|
+
}
|
|
4068
|
+
if (current.canonical_project_identity === projectId) continue;
|
|
4069
|
+
if (row.canonical_project_identity === projectId) {
|
|
4070
|
+
authorityByProject.set(projectId, row);
|
|
4071
|
+
continue;
|
|
4072
|
+
}
|
|
4073
|
+
if (String(row.last_attempt_at ?? "") > String(current.last_attempt_at ?? "")) authorityByProject.set(projectId, row);
|
|
4074
|
+
}
|
|
4075
|
+
return [...new Set(projectIds.map((row) => canonicalize(row.canonical_project_identity)))].filter((projectId) => {
|
|
4076
|
+
const authority = authorityByProject.get(projectId);
|
|
4077
|
+
return authority == null || authority.last_attempt_at == null || authority.safe_error_code == null && authority.authority_state !== "active" || authority.last_attempt_at <= retryBefore;
|
|
4078
|
+
}).toSorted((left, right) => {
|
|
4079
|
+
const leftAttempt = authorityByProject.get(left)?.last_attempt_at;
|
|
4080
|
+
const rightAttempt = authorityByProject.get(right)?.last_attempt_at;
|
|
4081
|
+
if (leftAttempt == null && rightAttempt != null) return -1;
|
|
4082
|
+
if (leftAttempt != null && rightAttempt == null) return 1;
|
|
4083
|
+
return String(leftAttempt ?? "").localeCompare(String(rightAttempt ?? "")) || left.localeCompare(right);
|
|
4084
|
+
}).slice(0, limit);
|
|
4085
|
+
}
|
|
4086
|
+
function recordRecipientPolicyMaintenanceFailure(store, projectId, attemptedAt) {
|
|
4087
|
+
store.db.prepare(`INSERT INTO recipient_policy_authority_states(
|
|
4088
|
+
canonical_project_identity, state_changed_at, created_at, updated_at,
|
|
4089
|
+
attempt_count, last_attempt_at, safe_error_code, last_error_at
|
|
4090
|
+
) VALUES (?, ?, ?, ?, 1, ?, 'recipient_policy_reconciliation_failed', ?)
|
|
4091
|
+
ON CONFLICT(canonical_project_identity) DO UPDATE SET
|
|
4092
|
+
attempt_count = attempt_count + 1, last_attempt_at = excluded.last_attempt_at,
|
|
4093
|
+
safe_error_code = excluded.safe_error_code, last_error_at = excluded.last_error_at,
|
|
4094
|
+
updated_at = excluded.updated_at`).run(projectId, attemptedAt, attemptedAt, attemptedAt, attemptedAt, attemptedAt);
|
|
4095
|
+
}
|
|
4096
|
+
function isSqliteBusy$1(error) {
|
|
4097
|
+
if (!error || typeof error !== "object" || !("code" in error)) return false;
|
|
4098
|
+
const code = error.code;
|
|
4099
|
+
return typeof code === "string" && (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_LOCKED"));
|
|
4100
|
+
}
|
|
4101
|
+
function recordMaintenanceFailureIfAvailable(store, projectId, attemptedAt, error) {
|
|
4102
|
+
if (isSqliteBusy$1(error)) return;
|
|
4103
|
+
try {
|
|
4104
|
+
recordRecipientPolicyMaintenanceFailure(store, projectId, attemptedAt);
|
|
4105
|
+
} catch (bookkeepingError) {
|
|
4106
|
+
if (!isSqliteBusy$1(bookkeepingError)) throw bookkeepingError;
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
3993
4109
|
async function reconcileRecipientPolicyProjectsOperation(store, options) {
|
|
3994
4110
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT), RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT));
|
|
3995
4111
|
const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
|
|
3996
4112
|
const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS));
|
|
3997
4113
|
const retryBefore = new Date(maintenanceNow.getTime() - backoffMs).toISOString();
|
|
3998
|
-
const
|
|
3999
|
-
|
|
4000
|
-
UNION
|
|
4001
|
-
SELECT canonical_project_identity FROM recipient_policy_authority_states
|
|
4002
|
-
)
|
|
4003
|
-
SELECT projects.canonical_project_identity
|
|
4004
|
-
FROM projects
|
|
4005
|
-
LEFT JOIN recipient_policy_authority_states authority
|
|
4006
|
-
ON authority.canonical_project_identity = projects.canonical_project_identity
|
|
4007
|
-
WHERE authority.safe_error_code IS NULL OR authority.last_attempt_at IS NULL
|
|
4008
|
-
OR authority.last_attempt_at <= ?
|
|
4009
|
-
ORDER BY CASE WHEN authority.last_attempt_at IS NULL THEN 0 ELSE 1 END,
|
|
4010
|
-
authority.last_attempt_at, projects.canonical_project_identity
|
|
4011
|
-
LIMIT ?`).all(retryBefore, limit);
|
|
4114
|
+
const attemptedAt = maintenanceNow.toISOString();
|
|
4115
|
+
const projectIds = recipientPolicyMaintenanceCandidates(store, retryBefore, limit);
|
|
4012
4116
|
const reconcileProject = options.reconcileProject ?? reconcileRecipientPolicyProject;
|
|
4013
4117
|
const result = {
|
|
4014
4118
|
processed: 0,
|
|
@@ -4018,25 +4122,26 @@ async function reconcileRecipientPolicyProjectsOperation(store, options) {
|
|
|
4018
4122
|
failed: 0,
|
|
4019
4123
|
items: []
|
|
4020
4124
|
};
|
|
4021
|
-
for (const
|
|
4125
|
+
for (const projectId of projectIds) {
|
|
4022
4126
|
result.processed += 1;
|
|
4023
4127
|
try {
|
|
4024
4128
|
const outcome = await reconcileProject(store.db, {
|
|
4025
|
-
canonicalProjectIdentity:
|
|
4129
|
+
canonicalProjectIdentity: projectId,
|
|
4026
4130
|
leaseOwner: options.leaseOwner ?? `recipient-policy-maintenance:${store.deviceId || process.pid}`
|
|
4027
4131
|
}, options.effects);
|
|
4028
4132
|
if (outcome.status === "active") result.active += 1;
|
|
4029
4133
|
else if (outcome.status === "needs_attention") result.attention += 1;
|
|
4030
4134
|
else result.waiting += 1;
|
|
4031
4135
|
result.items.push({
|
|
4032
|
-
canonicalProjectIdentity:
|
|
4136
|
+
canonicalProjectIdentity: projectId,
|
|
4033
4137
|
status: outcome.status,
|
|
4034
4138
|
safeErrorCode: outcome.safeErrorCode
|
|
4035
4139
|
});
|
|
4036
|
-
} catch {
|
|
4140
|
+
} catch (error) {
|
|
4141
|
+
recordMaintenanceFailureIfAvailable(store, projectId, attemptedAt, error);
|
|
4037
4142
|
result.failed += 1;
|
|
4038
4143
|
result.items.push({
|
|
4039
|
-
canonicalProjectIdentity:
|
|
4144
|
+
canonicalProjectIdentity: projectId,
|
|
4040
4145
|
status: "failed",
|
|
4041
4146
|
safeErrorCode: "recipient_policy_reconciliation_failed"
|
|
4042
4147
|
});
|
|
@@ -4319,6 +4424,40 @@ var PEERS_QUERY$1 = `
|
|
|
4319
4424
|
ORDER BY name, peer_device_id
|
|
4320
4425
|
`;
|
|
4321
4426
|
//#endregion
|
|
4427
|
+
//#region src/snapshot-source.ts
|
|
4428
|
+
function snapshotGeneration(raw) {
|
|
4429
|
+
if (!raw?.trim()) return null;
|
|
4430
|
+
return Number.parseInt(raw, 10);
|
|
4431
|
+
}
|
|
4432
|
+
function snapshotSourceDevice(requested, localDeviceId) {
|
|
4433
|
+
if (requested !== void 0 && requested !== localDeviceId) throw new Error("retirement_snapshot_source_required");
|
|
4434
|
+
return requested;
|
|
4435
|
+
}
|
|
4436
|
+
function snapshotErrorStatus(message) {
|
|
4437
|
+
if ([
|
|
4438
|
+
"retirement_snapshot_source_required",
|
|
4439
|
+
"missing_generation",
|
|
4440
|
+
"missing_snapshot_id"
|
|
4441
|
+
].includes(message)) return 400;
|
|
4442
|
+
if (message === "generation_mismatch" || message === "boundary_mismatch") return 409;
|
|
4443
|
+
return null;
|
|
4444
|
+
}
|
|
4445
|
+
function snapshotPageRequest(query, localDeviceId) {
|
|
4446
|
+
const generation = snapshotGeneration(query("generation"));
|
|
4447
|
+
const snapshotId = query("snapshot_id");
|
|
4448
|
+
if (generation === null || !Number.isFinite(generation)) throw new Error("missing_generation");
|
|
4449
|
+
if (!snapshotId) throw new Error("missing_snapshot_id");
|
|
4450
|
+
const rawLimit = Number.parseInt(query("limit") ?? "200", 10);
|
|
4451
|
+
return {
|
|
4452
|
+
generation,
|
|
4453
|
+
snapshotId,
|
|
4454
|
+
baselineCursor: query("baseline_cursor") ?? null,
|
|
4455
|
+
pageToken: query("page_token") ?? null,
|
|
4456
|
+
sourceDeviceId: snapshotSourceDevice(query("source_device_id"), localDeviceId),
|
|
4457
|
+
limit: Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 5e3)) : 200
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
//#endregion
|
|
4322
4461
|
//#region src/routes/team-setup-view.ts
|
|
4323
4462
|
var enabled = {
|
|
4324
4463
|
enabled: true,
|
|
@@ -6264,6 +6403,26 @@ function tryParsePairingPayload(value) {
|
|
|
6264
6403
|
addresses
|
|
6265
6404
|
};
|
|
6266
6405
|
}
|
|
6406
|
+
async function inspectPairingInvite(c, next) {
|
|
6407
|
+
const body = await parseViewerJsonBody(c);
|
|
6408
|
+
if (typeof body?.invite !== "string") return c.json({ error: "invite_invalid" }, 400);
|
|
6409
|
+
try {
|
|
6410
|
+
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());
|
|
6411
|
+
if (pairing?.kind === "invalid-pair") return c.json({ error: pairing.error }, 400);
|
|
6412
|
+
if (pairing?.kind !== "pair") {
|
|
6413
|
+
decodeInvitePayload(extractInvitePayload(body.invite));
|
|
6414
|
+
return next();
|
|
6415
|
+
}
|
|
6416
|
+
return c.json({
|
|
6417
|
+
kind: "pair",
|
|
6418
|
+
device_id: pairing.device_id,
|
|
6419
|
+
fingerprint: pairing.fingerprint,
|
|
6420
|
+
addresses: pairing.addresses
|
|
6421
|
+
});
|
|
6422
|
+
} catch (error) {
|
|
6423
|
+
return c.json({ error: safeRecipientInviteError(error) }, 400);
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6267
6426
|
function coordinatorAdminStatusPayload(config = readCoordinatorSyncConfig()) {
|
|
6268
6427
|
const groups = config.syncCoordinatorGroups;
|
|
6269
6428
|
const activeGroup = groups[0] ?? null;
|
|
@@ -6482,6 +6641,14 @@ function missingGuardrailConfirmations(warnings, confirmedTokens) {
|
|
|
6482
6641
|
const confirmed = new Set(confirmedTokens);
|
|
6483
6642
|
return warnings.filter((warning) => warning.requires_confirmation && (!warning.confirmation_token || !confirmed.has(warning.confirmation_token)));
|
|
6484
6643
|
}
|
|
6644
|
+
function mappingGuardrailChallenge(warnings, missing) {
|
|
6645
|
+
return {
|
|
6646
|
+
error: "guardrail_confirmation_required",
|
|
6647
|
+
required_guardrails: [...new Set(missing.map((warning) => warning.code))],
|
|
6648
|
+
required_guardrail_tokens: [...new Set(missing.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0),
|
|
6649
|
+
guardrail_warnings: warnings
|
|
6650
|
+
};
|
|
6651
|
+
}
|
|
6485
6652
|
function parseViewerProjectMappingInput(body) {
|
|
6486
6653
|
const id = optionalViewerInteger(body, "id");
|
|
6487
6654
|
const priority = optionalViewerInteger(body, "priority");
|
|
@@ -6496,6 +6663,14 @@ function parseViewerProjectMappingInput(body) {
|
|
|
6496
6663
|
source: optionalViewerStrictString(body, "source") ?? "user"
|
|
6497
6664
|
};
|
|
6498
6665
|
}
|
|
6666
|
+
function parseViewerProjectMappingInputs(body) {
|
|
6667
|
+
const rawMappings = body.mappings;
|
|
6668
|
+
if (!Array.isArray(rawMappings) || rawMappings.length === 0) throw new Error("mappings must be a non-empty array");
|
|
6669
|
+
return rawMappings.map((raw) => {
|
|
6670
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
|
|
6671
|
+
return parseViewerProjectMappingInput(raw);
|
|
6672
|
+
});
|
|
6673
|
+
}
|
|
6499
6674
|
function coordinatorAdminMutationStatus(message) {
|
|
6500
6675
|
if (message.includes("scope_not_found") || message.includes("membership_not_found") || message.includes("group_not_found") || message.includes("Group not found") || message.includes("Scope not found") || message.includes("device_not_enrolled_for_scope_group") || message.includes("device must be enrolled")) return 404;
|
|
6501
6676
|
if (message.includes("not active") || message.includes("scope_not_active") || message.includes("scope_membership_effect_conflict") || message.includes("group_archived") || message.includes("Group is archived")) return 409;
|
|
@@ -7453,13 +7628,25 @@ function recipientPolicyReadCopy(state) {
|
|
|
7453
7628
|
explanation: "Legacy scope enforcement remains in control while reconciliation is pending."
|
|
7454
7629
|
};
|
|
7455
7630
|
}
|
|
7631
|
+
function recipientPolicyAuthorityForAliases(store, canonicalProjectIdentity, aliases) {
|
|
7632
|
+
const canonical = getRecipientPolicyAuthorityState(store.db, canonicalProjectIdentity);
|
|
7633
|
+
if (canonical) return canonical;
|
|
7634
|
+
return aliases.filter((identity) => identity !== canonicalProjectIdentity).map((identity) => getRecipientPolicyAuthorityState(store.db, identity)).filter((state) => state != null).toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? null;
|
|
7635
|
+
}
|
|
7456
7636
|
function listRecipientPolicyReconciliationStatus(store) {
|
|
7637
|
+
const projectIds = store.db.prepare(`SELECT DISTINCT canonical_project_identity FROM project_recipients WHERE status = 'active'
|
|
7638
|
+
UNION SELECT canonical_project_identity FROM recipient_policy_authority_states
|
|
7639
|
+
ORDER BY canonical_project_identity`).all().map((row) => row.canonical_project_identity);
|
|
7640
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
7641
|
+
const aliasesByProject = /* @__PURE__ */ new Map();
|
|
7642
|
+
for (const projectId of projectIds) {
|
|
7643
|
+
const canonical = canonicalRepositoryProjectIdentity(repositoryIdentities, projectId);
|
|
7644
|
+
aliasesByProject.set(canonical, [...aliasesByProject.get(canonical) ?? [], projectId]);
|
|
7645
|
+
}
|
|
7457
7646
|
return {
|
|
7458
7647
|
version: 1,
|
|
7459
|
-
items:
|
|
7460
|
-
|
|
7461
|
-
ORDER BY canonical_project_identity`).all().map((row) => row.canonical_project_identity).map((canonicalProjectIdentity) => {
|
|
7462
|
-
const state = recipientPolicyReadState(getRecipientPolicyAuthorityState(store.db, canonicalProjectIdentity));
|
|
7648
|
+
items: [...aliasesByProject.keys()].toSorted().map((canonicalProjectIdentity) => {
|
|
7649
|
+
const state = recipientPolicyReadState(recipientPolicyAuthorityForAliases(store, canonicalProjectIdentity, aliasesByProject.get(canonicalProjectIdentity) ?? []));
|
|
7463
7650
|
return {
|
|
7464
7651
|
canonicalProjectIdentity,
|
|
7465
7652
|
state,
|
|
@@ -8453,6 +8640,7 @@ function addLegacySharedReviewRow(groups, row, identity, candidate, isOwnedBySel
|
|
|
8453
8640
|
}
|
|
8454
8641
|
function collectLegacySharedReviewGroups(store) {
|
|
8455
8642
|
const ownedBySelf = store.buildOwnershipPredicate();
|
|
8643
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
8456
8644
|
const candidatesByIdentity = new Map(listProjectScopeCandidates(store.db, { limit: null }).map((candidate) => [candidate.workspace_identity, candidate]));
|
|
8457
8645
|
const groups = /* @__PURE__ */ new Map();
|
|
8458
8646
|
let memoryCount = 0;
|
|
@@ -8464,6 +8652,11 @@ function collectLegacySharedReviewGroups(store) {
|
|
|
8464
8652
|
cwd: row.cwd,
|
|
8465
8653
|
gitBranch: row.git_branch,
|
|
8466
8654
|
gitRemote: row.git_remote,
|
|
8655
|
+
repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
|
|
8656
|
+
cwd: row.cwd,
|
|
8657
|
+
gitRemote: row.git_remote,
|
|
8658
|
+
metadataJson: row.session_metadata_json
|
|
8659
|
+
}),
|
|
8467
8660
|
project: row.project,
|
|
8468
8661
|
workspaceId: row.workspace_id
|
|
8469
8662
|
});
|
|
@@ -8538,7 +8731,8 @@ function legacySharedReviewSummaryRows(store) {
|
|
|
8538
8731
|
m.workspace_id,
|
|
8539
8732
|
m.actor_id,
|
|
8540
8733
|
m.origin_device_id,
|
|
8541
|
-
m.metadata_json
|
|
8734
|
+
m.metadata_json,
|
|
8735
|
+
s.metadata_json AS session_metadata_json
|
|
8542
8736
|
FROM memory_items m
|
|
8543
8737
|
LEFT JOIN sessions s ON s.id = m.session_id
|
|
8544
8738
|
WHERE m.scope_id = ?
|
|
@@ -8578,7 +8772,8 @@ function legacySharedReviewRows(store) {
|
|
|
8578
8772
|
m.workspace_id,
|
|
8579
8773
|
m.actor_id,
|
|
8580
8774
|
m.origin_device_id,
|
|
8581
|
-
m.metadata_json
|
|
8775
|
+
m.metadata_json,
|
|
8776
|
+
s.metadata_json AS session_metadata_json
|
|
8582
8777
|
FROM memory_items m
|
|
8583
8778
|
LEFT JOIN sessions s ON s.id = m.session_id
|
|
8584
8779
|
WHERE m.scope_id = ?
|
|
@@ -8595,7 +8790,7 @@ function legacySharedReviewConfirmationToken(input) {
|
|
|
8595
8790
|
return Buffer.from(`${input.workspaceIdentity}|${input.scopeId}|${payload}`, "utf8").toString("base64url");
|
|
8596
8791
|
}
|
|
8597
8792
|
function projectInventoryRowsForWorkspace(store, workspaceIdentity) {
|
|
8598
|
-
|
|
8793
|
+
const rows = store.db.prepare(`SELECT m.id,
|
|
8599
8794
|
m.rev,
|
|
8600
8795
|
s.project,
|
|
8601
8796
|
s.cwd,
|
|
@@ -8604,15 +8799,23 @@ function projectInventoryRowsForWorkspace(store, workspaceIdentity) {
|
|
|
8604
8799
|
m.workspace_id,
|
|
8605
8800
|
m.actor_id,
|
|
8606
8801
|
m.origin_device_id,
|
|
8607
|
-
m.metadata_json
|
|
8802
|
+
m.metadata_json,
|
|
8803
|
+
s.metadata_json AS session_metadata_json
|
|
8608
8804
|
FROM memory_items m
|
|
8609
8805
|
LEFT JOIN sessions s ON s.id = m.session_id
|
|
8610
8806
|
WHERE m.active = 1
|
|
8611
|
-
AND m.deleted_at IS NULL`).all()
|
|
8807
|
+
AND m.deleted_at IS NULL`).all();
|
|
8808
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
8809
|
+
return rows.filter((row) => {
|
|
8612
8810
|
return canonicalWorkspaceIdentity({
|
|
8613
8811
|
cwd: row.cwd,
|
|
8614
8812
|
gitBranch: row.git_branch,
|
|
8615
8813
|
gitRemote: row.git_remote,
|
|
8814
|
+
repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
|
|
8815
|
+
cwd: row.cwd,
|
|
8816
|
+
gitRemote: row.git_remote,
|
|
8817
|
+
metadataJson: row.session_metadata_json
|
|
8818
|
+
}),
|
|
8616
8819
|
project: row.project,
|
|
8617
8820
|
workspaceId: row.workspace_id
|
|
8618
8821
|
}).value === workspaceIdentity;
|
|
@@ -8666,11 +8869,17 @@ function assertLocalDeviceScopeMembership(store, scopeId) {
|
|
|
8666
8869
|
LIMIT 1`).get(scopeId, store.deviceId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
|
|
8667
8870
|
}
|
|
8668
8871
|
function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
|
|
8872
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
|
|
8669
8873
|
return legacySharedReviewRows(store).filter((row) => {
|
|
8670
8874
|
return canonicalWorkspaceIdentity({
|
|
8671
8875
|
cwd: row.cwd,
|
|
8672
8876
|
gitBranch: row.git_branch,
|
|
8673
8877
|
gitRemote: row.git_remote,
|
|
8878
|
+
repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
|
|
8879
|
+
cwd: row.cwd,
|
|
8880
|
+
gitRemote: row.git_remote,
|
|
8881
|
+
metadataJson: row.session_metadata_json
|
|
8882
|
+
}),
|
|
8674
8883
|
project: row.project,
|
|
8675
8884
|
workspaceId: row.workspace_id
|
|
8676
8885
|
}).value === workspaceIdentity;
|
|
@@ -8838,6 +9047,42 @@ function negotiatedSyncCapability(c) {
|
|
|
8838
9047
|
* network-accessible. All requests are auth-gated via signature
|
|
8839
9048
|
* verification so unauthenticated callers are rejected.
|
|
8840
9049
|
*/
|
|
9050
|
+
function serveAuthorizedSnapshot(c, store, peerDeviceId) {
|
|
9051
|
+
try {
|
|
9052
|
+
const rawScopeId = c.req.query(SYNC_SCOPE_QUERY_PARAM);
|
|
9053
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
9054
|
+
const scopeRequest = parseSyncScopeRequest(rawScopeId, rawScopeId !== void 0, {
|
|
9055
|
+
db: store.db,
|
|
9056
|
+
localDeviceId,
|
|
9057
|
+
negotiatedCapability: negotiatedSyncCapability(c),
|
|
9058
|
+
peerDeviceId
|
|
9059
|
+
});
|
|
9060
|
+
if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
|
|
9061
|
+
const pageRequest = snapshotPageRequest((name) => c.req.query(name), localDeviceId);
|
|
9062
|
+
const result = loadMemorySnapshotPageForPeer(store.db, {
|
|
9063
|
+
...pageRequest,
|
|
9064
|
+
peerDeviceId,
|
|
9065
|
+
scopeId: scopeRequest.mode === "scoped" ? scopeRequest.scope_id : void 0
|
|
9066
|
+
});
|
|
9067
|
+
return c.json({
|
|
9068
|
+
source_device_id: pageRequest.sourceDeviceId,
|
|
9069
|
+
scope_id: scopeRequest.scope_id,
|
|
9070
|
+
generation: result.boundary.generation,
|
|
9071
|
+
snapshot_id: result.boundary.snapshot_id,
|
|
9072
|
+
baseline_cursor: result.boundary.baseline_cursor,
|
|
9073
|
+
retained_floor_cursor: result.boundary.retained_floor_cursor,
|
|
9074
|
+
sync_capability: LOCAL_SYNC_CAPABILITY,
|
|
9075
|
+
items: result.items,
|
|
9076
|
+
next_page_token: result.nextPageToken,
|
|
9077
|
+
has_more: result.hasMore
|
|
9078
|
+
});
|
|
9079
|
+
} catch (err) {
|
|
9080
|
+
const message = err instanceof Error ? err.message : "";
|
|
9081
|
+
const status = snapshotErrorStatus(message);
|
|
9082
|
+
if (status) return c.json({ error: message }, status);
|
|
9083
|
+
return c.json({ error: "internal_error" }, 500);
|
|
9084
|
+
}
|
|
9085
|
+
}
|
|
8841
9086
|
function syncProtocolRoutes(getStore, opts = {}) {
|
|
8842
9087
|
const app = new Hono();
|
|
8843
9088
|
const routeRateLimit = opts.routeRateLimit ?? null;
|
|
@@ -9004,51 +9249,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
9004
9249
|
}
|
|
9005
9250
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
9006
9251
|
if (limited) return limited;
|
|
9007
|
-
|
|
9008
|
-
const rawScopeId = c.req.query(SYNC_SCOPE_QUERY_PARAM);
|
|
9009
|
-
const negotiated = negotiatedSyncCapability(c);
|
|
9010
|
-
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
9011
|
-
const scopeRequest = parseSyncScopeRequest(rawScopeId, rawScopeId !== void 0, {
|
|
9012
|
-
db: store.db,
|
|
9013
|
-
localDeviceId,
|
|
9014
|
-
negotiatedCapability: negotiated,
|
|
9015
|
-
peerDeviceId: auth.deviceId
|
|
9016
|
-
});
|
|
9017
|
-
if (!scopeRequest.ok) return c.json(syncScopeResetRequiredPayload(getSyncResetState(store.db), scopeRequest.reason, LOCAL_SYNC_CAPABILITY, null), 409);
|
|
9018
|
-
const rawGeneration = c.req.query("generation");
|
|
9019
|
-
const generation = rawGeneration != null && rawGeneration.trim().length > 0 ? Number.parseInt(rawGeneration, 10) : null;
|
|
9020
|
-
const snapshotId = c.req.query("snapshot_id") ?? null;
|
|
9021
|
-
const baselineCursor = c.req.query("baseline_cursor") ?? null;
|
|
9022
|
-
const pageToken = c.req.query("page_token") ?? null;
|
|
9023
|
-
const rawLimit = Number.parseInt(c.req.query("limit") ?? "200", 10);
|
|
9024
|
-
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 5e3)) : 200;
|
|
9025
|
-
if (generation == null || !Number.isFinite(generation)) return c.json({ error: "missing_generation" }, 400);
|
|
9026
|
-
if (!snapshotId) return c.json({ error: "missing_snapshot_id" }, 400);
|
|
9027
|
-
const result = loadMemorySnapshotPageForPeer(store.db, {
|
|
9028
|
-
generation,
|
|
9029
|
-
snapshotId,
|
|
9030
|
-
baselineCursor,
|
|
9031
|
-
pageToken,
|
|
9032
|
-
limit,
|
|
9033
|
-
peerDeviceId: auth.deviceId,
|
|
9034
|
-
scopeId: scopeRequest.mode === "scoped" ? scopeRequest.scope_id : void 0
|
|
9035
|
-
});
|
|
9036
|
-
return c.json({
|
|
9037
|
-
scope_id: scopeRequest.scope_id,
|
|
9038
|
-
generation: result.boundary.generation,
|
|
9039
|
-
snapshot_id: result.boundary.snapshot_id,
|
|
9040
|
-
baseline_cursor: result.boundary.baseline_cursor,
|
|
9041
|
-
retained_floor_cursor: result.boundary.retained_floor_cursor,
|
|
9042
|
-
sync_capability: LOCAL_SYNC_CAPABILITY,
|
|
9043
|
-
items: result.items,
|
|
9044
|
-
next_page_token: result.nextPageToken,
|
|
9045
|
-
has_more: result.hasMore
|
|
9046
|
-
});
|
|
9047
|
-
} catch (err) {
|
|
9048
|
-
const message = err instanceof Error ? err.message : "";
|
|
9049
|
-
if (message === "generation_mismatch" || message === "boundary_mismatch") return c.json({ error: message }, 409);
|
|
9050
|
-
return c.json({ error: "internal_error" }, 500);
|
|
9051
|
-
}
|
|
9252
|
+
return serveAuthorizedSnapshot(c, store, auth.deviceId);
|
|
9052
9253
|
})();
|
|
9053
9254
|
});
|
|
9054
9255
|
app.post("/v1/ops", async (c) => {
|
|
@@ -9126,7 +9327,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
9126
9327
|
}
|
|
9127
9328
|
async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
9128
9329
|
const config = readCoordinatorSyncConfig();
|
|
9129
|
-
const hasCoordinatorConfiguration = config.syncCoordinatorUrl.length > 0 || config.syncCoordinatorGroups.length > 0;
|
|
9330
|
+
const hasCoordinatorConfiguration = config.syncCoordinatorUrl.length > 0 || config.syncCoordinatorGroups.length > 0 || config.syncCoordinatorAdminSecret.length > 0;
|
|
9130
9331
|
const remoteUrl = config.syncCoordinatorUrl.trim();
|
|
9131
9332
|
const adminSecret = config.syncCoordinatorAdminSecret.trim();
|
|
9132
9333
|
const groupIds = [...new Set(config.syncCoordinatorGroups.map((groupId) => groupId.trim()))].filter(Boolean);
|
|
@@ -9137,7 +9338,7 @@ async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
|
9137
9338
|
};
|
|
9138
9339
|
if (!remoteUrl || !adminSecret || groupIds.length === 0) return {
|
|
9139
9340
|
availability: "unavailable",
|
|
9140
|
-
safeErrorCode: "
|
|
9341
|
+
safeErrorCode: "coordinator_configuration_required",
|
|
9141
9342
|
enrollments: []
|
|
9142
9343
|
};
|
|
9143
9344
|
if (groupIds.length > 25) return {
|
|
@@ -9170,6 +9371,174 @@ async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
|
9170
9371
|
};
|
|
9171
9372
|
}
|
|
9172
9373
|
}
|
|
9374
|
+
var ORIGIN_DEVICE_NAME_LOOKUP_BATCH_SIZE = 400;
|
|
9375
|
+
function originDeviceNameKey(value) {
|
|
9376
|
+
return value.normalize("NFKC").toLowerCase().toUpperCase().toLowerCase();
|
|
9377
|
+
}
|
|
9378
|
+
function originDeviceDisplayName(value, deviceId) {
|
|
9379
|
+
if (typeof value !== "string") return null;
|
|
9380
|
+
try {
|
|
9381
|
+
const displayName = normalizeHumanPresentationName(value, "display_name");
|
|
9382
|
+
let normalizedDeviceId = deviceId;
|
|
9383
|
+
try {
|
|
9384
|
+
normalizedDeviceId = normalizeHumanPresentationName(deviceId, "device_id");
|
|
9385
|
+
} catch {}
|
|
9386
|
+
return originDeviceNameKey(displayName) === originDeviceNameKey(normalizedDeviceId) ? null : displayName;
|
|
9387
|
+
} catch {
|
|
9388
|
+
return null;
|
|
9389
|
+
}
|
|
9390
|
+
}
|
|
9391
|
+
function resolveOriginDeviceDisplayNames(store, projects) {
|
|
9392
|
+
const deviceIds = [...new Set(projects.flatMap((project) => project.origin_devices.map((device) => device.device_id)))];
|
|
9393
|
+
const names = /* @__PURE__ */ new Map();
|
|
9394
|
+
for (let offset = 0; offset < deviceIds.length; offset += ORIGIN_DEVICE_NAME_LOOKUP_BATCH_SIZE) {
|
|
9395
|
+
const batch = deviceIds.slice(offset, offset + ORIGIN_DEVICE_NAME_LOOKUP_BATCH_SIZE);
|
|
9396
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
9397
|
+
const peers = store.db.prepare(`SELECT peer_device_id AS device_id, name AS display_name
|
|
9398
|
+
FROM sync_peers
|
|
9399
|
+
WHERE peer_device_id IN (${placeholders})
|
|
9400
|
+
AND TRIM(COALESCE(pinned_fingerprint, '')) <> ''`).all(...batch);
|
|
9401
|
+
for (const peer of peers) {
|
|
9402
|
+
const displayName = originDeviceDisplayName(peer.display_name, peer.device_id);
|
|
9403
|
+
if (displayName) names.set(peer.device_id, displayName);
|
|
9404
|
+
}
|
|
9405
|
+
const devices = store.db.prepare(`SELECT device_id, display_name
|
|
9406
|
+
FROM identity_devices
|
|
9407
|
+
WHERE device_id IN (${placeholders}) AND status = 'active'`).all(...batch);
|
|
9408
|
+
for (const device of devices) {
|
|
9409
|
+
const displayName = originDeviceDisplayName(device.display_name, device.device_id);
|
|
9410
|
+
if (displayName) names.set(device.device_id, displayName);
|
|
9411
|
+
}
|
|
9412
|
+
}
|
|
9413
|
+
return names;
|
|
9414
|
+
}
|
|
9415
|
+
function serializeProjectScopeInventory(store, inventory, sharingByProject) {
|
|
9416
|
+
const originDeviceNames = resolveOriginDeviceDisplayNames(store, inventory.projects);
|
|
9417
|
+
return {
|
|
9418
|
+
...inventory,
|
|
9419
|
+
projects: inventory.projects.map((project) => ({
|
|
9420
|
+
...project,
|
|
9421
|
+
origin_devices: project.origin_devices.map(({ device_id }) => ({
|
|
9422
|
+
device_id,
|
|
9423
|
+
display_name: originDeviceNames.get(device_id) ?? null
|
|
9424
|
+
})),
|
|
9425
|
+
sharing: sharingByProject.get(project.workspace_identity) ?? []
|
|
9426
|
+
}))
|
|
9427
|
+
};
|
|
9428
|
+
}
|
|
9429
|
+
async function deleteProjectScopeMappingRoute(c, store) {
|
|
9430
|
+
const id = Number(c.req.param("id"));
|
|
9431
|
+
let releasePublicationMutation;
|
|
9432
|
+
try {
|
|
9433
|
+
const body = c.req.header("content-type")?.includes("application/json") ? await parseViewerJsonBody(c) : {};
|
|
9434
|
+
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
9435
|
+
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
9436
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
9437
|
+
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
9438
|
+
const warnings = analyzeProjectScopeMappingDeletionGuardrails(store.db, id, deviceId);
|
|
9439
|
+
const missing = missingGuardrailConfirmations(warnings, confirmedGuardrailTokens);
|
|
9440
|
+
if (missing.length > 0) return c.json({
|
|
9441
|
+
error: "guardrail_confirmation_required",
|
|
9442
|
+
required_guardrails: [...new Set(missing.map((warning) => warning.code))],
|
|
9443
|
+
required_guardrail_tokens: missing.map((warning) => warning.confirmation_token),
|
|
9444
|
+
guardrail_warnings: warnings
|
|
9445
|
+
}, 409);
|
|
9446
|
+
const deleted = deleteProjectMappingAndWakePolicies(store, id, deviceId, confirmedGuardrailTokens);
|
|
9447
|
+
return c.json({
|
|
9448
|
+
ok: true,
|
|
9449
|
+
deleted
|
|
9450
|
+
});
|
|
9451
|
+
} catch (error) {
|
|
9452
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
9453
|
+
} finally {
|
|
9454
|
+
releasePublicationMutation?.();
|
|
9455
|
+
}
|
|
9456
|
+
}
|
|
9457
|
+
function projectSharingByRepository(store, operations) {
|
|
9458
|
+
const operationById = new Map(operations.map((operation) => [operation.operation_id, operation]));
|
|
9459
|
+
const sharingByProject = /* @__PURE__ */ new Map();
|
|
9460
|
+
const mappings = store.db.prepare("SELECT workspace_identity, project_pattern FROM project_scope_mappings").all();
|
|
9461
|
+
const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db, { knownRepositoryIdentities: mappings.flatMap((mapping) => [mapping.workspace_identity, mapping.project_pattern]) });
|
|
9462
|
+
const seenOperationProjects = /* @__PURE__ */ new Set();
|
|
9463
|
+
const reviewed = store.db.prepare(`SELECT p.operation_id, p.canonical_project_identity
|
|
9464
|
+
FROM share_operation_projects p
|
|
9465
|
+
JOIN share_operations o ON o.operation_id = p.operation_id
|
|
9466
|
+
WHERE o.inviter_actor_id = ? ORDER BY o.created_at, p.ordinal`).all(store.actorId);
|
|
9467
|
+
for (const item of reviewed) {
|
|
9468
|
+
const operation = operationById.get(item.operation_id);
|
|
9469
|
+
if (!operation) continue;
|
|
9470
|
+
const projectId = canonicalRepositoryProjectIdentity(repositoryIdentities, item.canonical_project_identity);
|
|
9471
|
+
const operationProjectKey = `${item.operation_id}\u0000${projectId}`;
|
|
9472
|
+
if (seenOperationProjects.has(operationProjectKey)) continue;
|
|
9473
|
+
seenOperationProjects.add(operationProjectKey);
|
|
9474
|
+
const current = sharingByProject.get(projectId) ?? [];
|
|
9475
|
+
current.push({
|
|
9476
|
+
person: operation.person,
|
|
9477
|
+
lifecycle: {
|
|
9478
|
+
state: operation.lifecycle.state,
|
|
9479
|
+
label: operation.lifecycle.label,
|
|
9480
|
+
explanation: operation.lifecycle.explanation
|
|
9481
|
+
}
|
|
9482
|
+
});
|
|
9483
|
+
sharingByProject.set(projectId, current);
|
|
9484
|
+
}
|
|
9485
|
+
return sharingByProject;
|
|
9486
|
+
}
|
|
9487
|
+
function deactivateActorAndWakePolicies(store, actorId, now) {
|
|
9488
|
+
store.db.transaction(() => {
|
|
9489
|
+
store.db.prepare("UPDATE actors SET status = 'deactivated', updated_at = ? WHERE actor_id = ?").run(now, actorId);
|
|
9490
|
+
store.db.prepare("UPDATE sync_peers SET actor_id = NULL WHERE actor_id = ?").run(actorId);
|
|
9491
|
+
wakeRecipientPoliciesForIdentities(store.db, [actorId], now);
|
|
9492
|
+
}).immediate();
|
|
9493
|
+
}
|
|
9494
|
+
function finishActorMerge(store, primaryActorId, secondaryActorId, now) {
|
|
9495
|
+
store.db.prepare("UPDATE actors SET status = 'merged', merged_into_actor_id = ?, updated_at = ? WHERE actor_id = ?").run(primaryActorId, now, secondaryActorId);
|
|
9496
|
+
wakeRecipientPoliciesForIdentities(store.db, [primaryActorId, secondaryActorId], now);
|
|
9497
|
+
}
|
|
9498
|
+
function policyWakeMappings(store) {
|
|
9499
|
+
return store.db.prepare("SELECT id, scope_id, workspace_identity FROM project_scope_mappings ORDER BY id").all();
|
|
9500
|
+
}
|
|
9501
|
+
function existingRequestedMappings(mappings, inputs) {
|
|
9502
|
+
const requestedIds = new Set(inputs.map((input) => input.id).filter((id) => id != null));
|
|
9503
|
+
const requestedIdentities = new Set(inputs.map((input) => input.workspace_identity).filter((identity) => identity != null).map((identity) => canonicalWorkspaceIdentity({ cwd: identity }).value));
|
|
9504
|
+
return mappings.filter((mapping) => {
|
|
9505
|
+
if (requestedIds.has(mapping.id)) return true;
|
|
9506
|
+
if (!mapping.workspace_identity) return false;
|
|
9507
|
+
return requestedIdentities.has(canonicalWorkspaceIdentity({ cwd: mapping.workspace_identity }).value);
|
|
9508
|
+
});
|
|
9509
|
+
}
|
|
9510
|
+
function mappedProjectsInScopes(mappings, scopeIds) {
|
|
9511
|
+
return mappings.filter((mapping) => scopeIds.has(mapping.scope_id)).map((mapping) => mapping.workspace_identity).filter((identity) => identity != null);
|
|
9512
|
+
}
|
|
9513
|
+
function wakeRecipientPolicyProjects(store, projectIdentities, now) {
|
|
9514
|
+
wakeRecipientPoliciesForProjectIdentities(store.db, [...new Set(projectIdentities)], now);
|
|
9515
|
+
}
|
|
9516
|
+
function saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs) {
|
|
9517
|
+
return store.db.transaction(() => {
|
|
9518
|
+
const before = policyWakeMappings(store);
|
|
9519
|
+
const previousMappings = existingRequestedMappings(before, mappingInputs);
|
|
9520
|
+
const mappings = upsertProjectScopeSettingsMappings(store.db, mappingInputs, { deviceId });
|
|
9521
|
+
const after = policyWakeMappings(store);
|
|
9522
|
+
const changedScopeIds = /* @__PURE__ */ new Set([...previousMappings.map((mapping) => mapping.scope_id), ...mappings.map((mapping) => mapping.scope_id)]);
|
|
9523
|
+
wakeRecipientPolicyProjects(store, [...mappedProjectsInScopes(before, changedScopeIds), ...mappedProjectsInScopes(after, changedScopeIds)], (/* @__PURE__ */ new Date()).toISOString());
|
|
9524
|
+
return mappings;
|
|
9525
|
+
}).immediate();
|
|
9526
|
+
}
|
|
9527
|
+
function deleteProjectMappingAndWakePolicies(store, mappingId, deviceId, confirmedGuardrailTokens) {
|
|
9528
|
+
return store.db.transaction(() => {
|
|
9529
|
+
const before = policyWakeMappings(store);
|
|
9530
|
+
const removed = before.find((mapping) => mapping.id === mappingId);
|
|
9531
|
+
const deleted = deleteProjectScopeSettingsMapping(store.db, mappingId, {
|
|
9532
|
+
deviceId,
|
|
9533
|
+
confirmedGuardrailTokens
|
|
9534
|
+
});
|
|
9535
|
+
if (deleted && removed) {
|
|
9536
|
+
const changedScopeIds = /* @__PURE__ */ new Set([removed.scope_id]);
|
|
9537
|
+
wakeRecipientPolicyProjects(store, [...mappedProjectsInScopes(before, changedScopeIds), ...mappedProjectsInScopes(policyWakeMappings(store), changedScopeIds)], (/* @__PURE__ */ new Date()).toISOString());
|
|
9538
|
+
}
|
|
9539
|
+
return deleted;
|
|
9540
|
+
}).immediate();
|
|
9541
|
+
}
|
|
9173
9542
|
/**
|
|
9174
9543
|
* Viewer-facing sync management routes (/api/sync/*).
|
|
9175
9544
|
*
|
|
@@ -9178,7 +9547,7 @@ async function loadConfiguredDeviceIdentityCoordinatorEvidence() {
|
|
|
9178
9547
|
* local viewer.
|
|
9179
9548
|
*/
|
|
9180
9549
|
function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
9181
|
-
const app = new Hono();
|
|
9550
|
+
const app = new Hono().post("/api/sync/invites/inspect", inspectPairingInvite);
|
|
9182
9551
|
const readCoordinatorConfig = options.readCoordinatorConfig ?? readCoordinatorSyncConfig;
|
|
9183
9552
|
const renameCoordinatorGroup = options.renameCoordinatorGroup ?? coordinatorRenameGroupAction;
|
|
9184
9553
|
const loadCoordinatorEvidence = options.loadDeviceIdentityCoordinatorEvidence ?? loadConfiguredDeviceIdentityCoordinatorEvidence;
|
|
@@ -9802,34 +10171,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9802
10171
|
scopeId: c.req.query("scope_id"),
|
|
9803
10172
|
status: c.req.query("status")
|
|
9804
10173
|
});
|
|
9805
|
-
const
|
|
9806
|
-
|
|
9807
|
-
const sharingByProject = /* @__PURE__ */ new Map();
|
|
9808
|
-
const reviewed = store.db.prepare(`SELECT p.operation_id, p.canonical_project_identity
|
|
9809
|
-
FROM share_operation_projects p
|
|
9810
|
-
JOIN share_operations o ON o.operation_id = p.operation_id
|
|
9811
|
-
WHERE o.inviter_actor_id = ? ORDER BY o.created_at, p.ordinal`).all(store.actorId);
|
|
9812
|
-
for (const item of reviewed) {
|
|
9813
|
-
const operation = operationById.get(item.operation_id);
|
|
9814
|
-
if (!operation) continue;
|
|
9815
|
-
const current = sharingByProject.get(item.canonical_project_identity) ?? [];
|
|
9816
|
-
current.push({
|
|
9817
|
-
person: operation.person,
|
|
9818
|
-
lifecycle: {
|
|
9819
|
-
state: operation.lifecycle.state,
|
|
9820
|
-
label: operation.lifecycle.label,
|
|
9821
|
-
explanation: operation.lifecycle.explanation
|
|
9822
|
-
}
|
|
9823
|
-
});
|
|
9824
|
-
sharingByProject.set(item.canonical_project_identity, current);
|
|
9825
|
-
}
|
|
9826
|
-
return c.json({
|
|
9827
|
-
...inventory,
|
|
9828
|
-
projects: inventory.projects.map((project) => ({
|
|
9829
|
-
...project,
|
|
9830
|
-
sharing: sharingByProject.get(project.workspace_identity) ?? []
|
|
9831
|
-
}))
|
|
9832
|
-
});
|
|
10174
|
+
const sharingByProject = projectSharingByRepository(store, await shareOperationReadModels(store, void 0, false));
|
|
10175
|
+
return c.json(serializeProjectScopeInventory(store, inventory, sharingByProject));
|
|
9833
10176
|
});
|
|
9834
10177
|
app.post("/api/sync/projects/reassign-project", async (c) => {
|
|
9835
10178
|
const store = getStore();
|
|
@@ -10085,28 +10428,19 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10085
10428
|
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
10086
10429
|
const mappingInput = parseViewerProjectMappingInput(body);
|
|
10087
10430
|
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
10088
|
-
const
|
|
10431
|
+
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10432
|
+
const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, {
|
|
10433
|
+
...mappingInput,
|
|
10434
|
+
deviceId
|
|
10435
|
+
});
|
|
10089
10436
|
if (analysis.requested_workspace_identity?.startsWith("unmapped:")) return c.json({
|
|
10090
10437
|
error: "unmapped_project_local_only",
|
|
10091
10438
|
message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a Sharing domain.",
|
|
10092
10439
|
guardrail_warnings: analysis.warnings
|
|
10093
10440
|
}, 400);
|
|
10094
10441
|
const missingConfirmations = missingGuardrailConfirmations(analysis.warnings, confirmedGuardrailTokens);
|
|
10095
|
-
if (missingConfirmations.length > 0)
|
|
10096
|
-
|
|
10097
|
-
const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
|
|
10098
|
-
return c.json({
|
|
10099
|
-
error: "guardrail_confirmation_required",
|
|
10100
|
-
required_guardrails: missingCodes,
|
|
10101
|
-
required_guardrail_tokens: missingTokens,
|
|
10102
|
-
guardrail_warnings: analysis.warnings
|
|
10103
|
-
}, 409);
|
|
10104
|
-
}
|
|
10105
|
-
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10106
|
-
const mapping = upsertProjectScopeSettingsMapping(store.db, {
|
|
10107
|
-
deviceId,
|
|
10108
|
-
...mappingInput
|
|
10109
|
-
});
|
|
10442
|
+
if (missingConfirmations.length > 0) return c.json(mappingGuardrailChallenge(analysis.warnings, missingConfirmations), 409);
|
|
10443
|
+
const [mapping] = saveProjectMappingsAndWakePolicies(store, deviceId, [mappingInput]);
|
|
10110
10444
|
return c.json({
|
|
10111
10445
|
ok: true,
|
|
10112
10446
|
mapping,
|
|
@@ -10124,14 +10458,13 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10124
10458
|
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
10125
10459
|
let releasePublicationMutation;
|
|
10126
10460
|
try {
|
|
10127
|
-
const
|
|
10128
|
-
if (!Array.isArray(rawMappings) || rawMappings.length === 0) return c.json({ error: "mappings must be a non-empty array" }, 400);
|
|
10129
|
-
const mappingInputs = rawMappings.map((raw) => {
|
|
10130
|
-
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
|
|
10131
|
-
return parseViewerProjectMappingInput(raw);
|
|
10132
|
-
});
|
|
10461
|
+
const mappingInputs = parseViewerProjectMappingInputs(body);
|
|
10133
10462
|
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
10134
|
-
const
|
|
10463
|
+
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10464
|
+
const analyses = analyzeProjectScopeMappingChangesGuardrails(store.db, mappingInputs.map((input) => ({
|
|
10465
|
+
...input,
|
|
10466
|
+
deviceId
|
|
10467
|
+
})));
|
|
10135
10468
|
const unmapped = analyses.find((analysis) => analysis.requested_workspace_identity?.startsWith("unmapped:"));
|
|
10136
10469
|
if (unmapped) return c.json({
|
|
10137
10470
|
error: "unmapped_project_local_only",
|
|
@@ -10139,24 +10472,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10139
10472
|
guardrail_warnings: unmapped.warnings
|
|
10140
10473
|
}, 400);
|
|
10141
10474
|
const missingConfirmations = analyses.flatMap((analysis) => missingGuardrailConfirmations(analysis.warnings, []));
|
|
10142
|
-
if (missingConfirmations.length > 0)
|
|
10143
|
-
const missingCodes = [...new Set(missingConfirmations.map((warning) => warning.code))];
|
|
10144
|
-
const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
|
|
10145
|
-
return c.json({
|
|
10146
|
-
error: "guardrail_confirmation_required",
|
|
10147
|
-
required_guardrails: missingCodes,
|
|
10148
|
-
required_guardrail_tokens: missingTokens,
|
|
10149
|
-
guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
|
|
10150
|
-
}, 409);
|
|
10151
|
-
}
|
|
10152
|
-
const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
10153
|
-
const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, {
|
|
10154
|
-
deviceId,
|
|
10155
|
-
...mappingInput
|
|
10156
|
-
})));
|
|
10475
|
+
if (missingConfirmations.length > 0) return c.json(mappingGuardrailChallenge(analyses.flatMap((analysis) => analysis.warnings), missingConfirmations), 409);
|
|
10157
10476
|
return c.json({
|
|
10158
10477
|
ok: true,
|
|
10159
|
-
mappings:
|
|
10478
|
+
mappings: saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs),
|
|
10160
10479
|
guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
|
|
10161
10480
|
});
|
|
10162
10481
|
} catch (error) {
|
|
@@ -10165,23 +10484,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10165
10484
|
releasePublicationMutation?.();
|
|
10166
10485
|
}
|
|
10167
10486
|
});
|
|
10168
|
-
app.delete("/api/sync/sharing-domains/project-mappings/:id",
|
|
10169
|
-
const store = getStore();
|
|
10170
|
-
const id = Number(c.req.param("id"));
|
|
10171
|
-
let releasePublicationMutation;
|
|
10172
|
-
try {
|
|
10173
|
-
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
10174
|
-
const deleted = deleteProjectScopeSettingsMapping(store.db, id);
|
|
10175
|
-
return c.json({
|
|
10176
|
-
ok: true,
|
|
10177
|
-
deleted
|
|
10178
|
-
});
|
|
10179
|
-
} catch (error) {
|
|
10180
|
-
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
10181
|
-
} finally {
|
|
10182
|
-
releasePublicationMutation?.();
|
|
10183
|
-
}
|
|
10184
|
-
});
|
|
10487
|
+
app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => deleteProjectScopeMappingRoute(c, getStore()));
|
|
10185
10488
|
app.post("/api/sync/peers/identity", async (c) => {
|
|
10186
10489
|
const store = getStore();
|
|
10187
10490
|
ensureLocalActorRecord(store);
|
|
@@ -10439,11 +10742,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10439
10742
|
WHERE inviter_actor_id = ? OR recipient_actor_id = ? OR person_id = ?`).run(secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, now, secondaryActorId, secondaryActorId, secondaryActorId);
|
|
10440
10743
|
const peerUpdate = d.update(schema.syncPeers).set({ actor_id: primaryActorId }).where(eq(schema.syncPeers.actor_id, secondaryActorId)).run();
|
|
10441
10744
|
if (primary.is_local) d.update(schema.syncPeers).set({ claimed_local_actor: 1 }).where(eq(schema.syncPeers.actor_id, primaryActorId)).run();
|
|
10442
|
-
|
|
10443
|
-
status: "merged",
|
|
10444
|
-
merged_into_actor_id: primaryActorId,
|
|
10445
|
-
updated_at: now
|
|
10446
|
-
}).where(eq(schema.actors.actor_id, secondaryActorId)).run();
|
|
10745
|
+
finishActorMerge(store, primaryActorId, secondaryActorId, now);
|
|
10447
10746
|
return { mergedCount: Number(peerUpdate.changes ?? 0) };
|
|
10448
10747
|
}).immediate();
|
|
10449
10748
|
if ("error" in result) return c.json({ error: result.error }, result.status);
|
|
@@ -10462,16 +10761,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
10462
10761
|
if (actorId === store.actorId) return c.json({ error: "cannot deactivate this device's own local actor" }, 409);
|
|
10463
10762
|
const releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, [actorId]);
|
|
10464
10763
|
queueMicrotask(releaseActorMutations);
|
|
10465
|
-
const
|
|
10466
|
-
const actor = d.select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
|
|
10764
|
+
const actor = drizzle(store.db, { schema }).select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
|
|
10467
10765
|
if (!actor) return c.json({ error: "actor not found" }, 404);
|
|
10468
10766
|
if (actor.status !== "active") return c.json({ error: "actor not active" }, 409);
|
|
10469
|
-
|
|
10470
|
-
d.update(schema.actors).set({
|
|
10471
|
-
status: "deactivated",
|
|
10472
|
-
updated_at: now
|
|
10473
|
-
}).where(eq(schema.actors.actor_id, actorId)).run();
|
|
10474
|
-
d.update(schema.syncPeers).set({ actor_id: null }).where(eq(schema.syncPeers.actor_id, actorId)).run();
|
|
10767
|
+
deactivateActorAndWakePolicies(store, actorId, (/* @__PURE__ */ new Date()).toISOString());
|
|
10475
10768
|
return c.json({ ok: true });
|
|
10476
10769
|
});
|
|
10477
10770
|
app.post("/api/sync/legacy-devices/claim", async (c) => {
|
|
@@ -12028,6 +12321,9 @@ function createApp(opts) {
|
|
|
12028
12321
|
const app = new Hono();
|
|
12029
12322
|
app.use("*", preflightHandler());
|
|
12030
12323
|
app.use("*", originGuard({ unsafeGetPathPrefixes: [TEAM_SETUP_ROUTE_PREFIX] }));
|
|
12324
|
+
const viewerTargetGuard = viewerTargetQueryGuard(storeFactory);
|
|
12325
|
+
app.use("/api/memories/*", viewerTargetGuard);
|
|
12326
|
+
app.use("/api/memory", viewerTargetGuard);
|
|
12031
12327
|
app.route("/", healthRoutes(storeFactory));
|
|
12032
12328
|
app.route("/", diagnosticsRoutes(storeFactory));
|
|
12033
12329
|
app.route("/", statsRoutes(storeFactory));
|