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