@codemem/server 0.43.2 → 0.44.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 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +743 -134
- package/dist/index.js.map +1 -1
- package/dist/routes/sync.d.ts +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/dist/routes/target-validation.d.ts.map +1 -1
- package/dist/routes/team-setup.d.ts +11 -1
- package/dist/routes/team-setup.d.ts.map +1 -1
- package/package.json +11 -11
- package/static/app.js +58 -61
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/index.html +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
3
|
-
import { ACCESS_CLEANUP_OP_TYPE, CODEMEM_CONFIG_ENV_OVERRIDES, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, MemoryStore, PROMPT_TRANSPORT_PROTOCOL_RANGE, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildFilterClausesWithContext, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildViewerIdentityTarget, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey,
|
|
3
|
+
import { ACCESS_CLEANUP_OP_TYPE, CODEMEM_CONFIG_ENV_OVERRIDES, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, LegacyTeamSetupAdditiveConvergenceError, MemoryStore, PROMPT_TRANSPORT_PROTOCOL_RANGE, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildFilterClausesWithContext, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalWorkspaceIdentity, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, deriveLegacyTeamSetupCompletionManifest, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, migrateRecipientPolicyIntent, negotiateSyncCapability, normalizeAddress, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveDbPath, resolveProject, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, summarizeInboundScopeRejections, supportsSyncFeature, syncScopeResetRequiredPayload, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
4
4
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { createMiddleware } from "hono/factory";
|
|
@@ -10,7 +10,7 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
|
10
10
|
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
12
|
//#region src/middleware.ts
|
|
13
|
-
var LOOPBACK_HOSTS = new Set([
|
|
13
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
14
14
|
"127.0.0.1",
|
|
15
15
|
"localhost",
|
|
16
16
|
"::1"
|
|
@@ -32,7 +32,7 @@ function isLoopbackOrigin(origin) {
|
|
|
32
32
|
return LOOPBACK_HOSTS.has(hostname);
|
|
33
33
|
}
|
|
34
34
|
/** HTTP methods that mutate state and require origin validation. */
|
|
35
|
-
var UNSAFE_METHODS = new Set([
|
|
35
|
+
var UNSAFE_METHODS = /* @__PURE__ */ new Set([
|
|
36
36
|
"POST",
|
|
37
37
|
"DELETE",
|
|
38
38
|
"PATCH",
|
|
@@ -167,20 +167,20 @@ function createInMemoryRequestRateLimiter(windowMs = 6e4) {
|
|
|
167
167
|
* codemem/viewer_routes/config.py, scoped to the TS runtime's current needs.
|
|
168
168
|
*/
|
|
169
169
|
var REDACTED_VALUE = "[redacted]";
|
|
170
|
-
var RUNTIMES = new Set([
|
|
170
|
+
var RUNTIMES = /* @__PURE__ */ new Set([
|
|
171
171
|
"api_http",
|
|
172
172
|
"claude_sidecar",
|
|
173
173
|
"codex_sidecar"
|
|
174
174
|
]);
|
|
175
|
-
var AUTH_SOURCES = new Set([
|
|
175
|
+
var AUTH_SOURCES = /* @__PURE__ */ new Set([
|
|
176
176
|
"auto",
|
|
177
177
|
"env",
|
|
178
178
|
"file",
|
|
179
179
|
"command",
|
|
180
180
|
"none"
|
|
181
181
|
]);
|
|
182
|
-
var HOT_RELOAD_KEYS = new Set(["raw_events_sweeper_interval_s"]);
|
|
183
|
-
var PROTECTED_WRITE_KEYS = new Set([
|
|
182
|
+
var HOT_RELOAD_KEYS = /* @__PURE__ */ new Set(["raw_events_sweeper_interval_s"]);
|
|
183
|
+
var PROTECTED_WRITE_KEYS = /* @__PURE__ */ new Set([
|
|
184
184
|
"claude_command",
|
|
185
185
|
"codex_command",
|
|
186
186
|
"observer_base_url",
|
|
@@ -189,13 +189,13 @@ var PROTECTED_WRITE_KEYS = new Set([
|
|
|
189
189
|
"observer_headers",
|
|
190
190
|
"sync_coordinator_url"
|
|
191
191
|
]);
|
|
192
|
-
var SECRET_CONFIG_KEYS = new Set([
|
|
192
|
+
var SECRET_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
193
193
|
"observer_auth_file",
|
|
194
194
|
"observer_auth_command",
|
|
195
195
|
"observer_headers",
|
|
196
196
|
"sync_coordinator_admin_secret"
|
|
197
197
|
]);
|
|
198
|
-
var REMOVED_CONFIG_KEYS = new Set(["observer_rich_openai_use_responses"]);
|
|
198
|
+
var REMOVED_CONFIG_KEYS = /* @__PURE__ */ new Set(["observer_rich_openai_use_responses"]);
|
|
199
199
|
var ALLOWED_KEYS = [
|
|
200
200
|
"claude_command",
|
|
201
201
|
"codex_command",
|
|
@@ -489,7 +489,7 @@ function configRoutes(opts = {}) {
|
|
|
489
489
|
const effectiveChangedKeys = ALLOWED_KEYS.filter((key) => !configValuesEqual(beforeEffective[key], afterEffective[key]));
|
|
490
490
|
const envOverrides = getCodememEnvOverrides();
|
|
491
491
|
const ignoredByEnvKeys = savedChangedKeys.filter((key) => !effectiveChangedKeys.includes(key) && key in envOverrides);
|
|
492
|
-
const hotReloadedKeys = applyRuntimeEffects([
|
|
492
|
+
const hotReloadedKeys = applyRuntimeEffects([.../* @__PURE__ */ new Set([
|
|
493
493
|
...touchedKeys,
|
|
494
494
|
...savedChangedKeys,
|
|
495
495
|
...effectiveChangedKeys
|
|
@@ -689,7 +689,7 @@ function attachResolvedIdentityFieldsUnsafe(store, items) {
|
|
|
689
689
|
AND TRIM(COALESCE(pinned_fingerprint, '')) <> ''`);
|
|
690
690
|
const peerByDevice = new Map(peers.map((peer) => [peer.peer_device_id, peer]));
|
|
691
691
|
const peerActorIds = peers.map((peer) => String(peer.actor_id ?? "").trim()).filter((actorId) => actorId.length > 0);
|
|
692
|
-
const actorRows = batchedLookup(store, [
|
|
692
|
+
const actorRows = batchedLookup(store, [.../* @__PURE__ */ new Set([...actorIds, ...peerActorIds])], (placeholders) => `SELECT actor_id, display_name, status, merged_into_actor_id FROM actors
|
|
693
693
|
WHERE actor_id IN (${placeholders})`);
|
|
694
694
|
const devices = batchedLookup(store, deviceIds, (placeholders) => `SELECT device_id, display_name FROM identity_devices
|
|
695
695
|
WHERE device_id IN (${placeholders}) AND status = 'active'`);
|
|
@@ -1266,7 +1266,11 @@ function observerStatusRoutes(deps) {
|
|
|
1266
1266
|
//#endregion
|
|
1267
1267
|
//#region src/routes/target-validation.ts
|
|
1268
1268
|
var IDENTITY_TARGET_KEYS = new Set(VIEWER_IDENTITY_TARGET_KEYS);
|
|
1269
|
-
var BOOLEAN_IDENTITY_TARGET_KEYS = new Set([
|
|
1269
|
+
var BOOLEAN_IDENTITY_TARGET_KEYS = /* @__PURE__ */ new Set([
|
|
1270
|
+
"actor_id_present",
|
|
1271
|
+
"embedding_disabled",
|
|
1272
|
+
"embedding_offline"
|
|
1273
|
+
]);
|
|
1270
1274
|
function invalidRequest$1(message) {
|
|
1271
1275
|
return {
|
|
1272
1276
|
ok: false,
|
|
@@ -1330,12 +1334,12 @@ function validateViewerTarget(store, payload, options = {}) {
|
|
|
1330
1334
|
}
|
|
1331
1335
|
//#endregion
|
|
1332
1336
|
//#region src/routes/pack.ts
|
|
1333
|
-
var MAX_LEDGER_PAYLOAD_BYTES =
|
|
1337
|
+
var MAX_LEDGER_PAYLOAD_BYTES = 16384;
|
|
1334
1338
|
var MAX_METADATA_FIELD_CHARS = 512;
|
|
1335
1339
|
var MAX_WORKING_SET_FILES = 50;
|
|
1336
1340
|
var MAX_WORKING_SET_PATH_CHARS = 512;
|
|
1337
1341
|
var INVALID_JSON = Symbol("invalid-json");
|
|
1338
|
-
var PACK_KEYS = new Set([
|
|
1342
|
+
var PACK_KEYS = /* @__PURE__ */ new Set([
|
|
1339
1343
|
"context",
|
|
1340
1344
|
"limit",
|
|
1341
1345
|
"token_budget",
|
|
@@ -1349,7 +1353,7 @@ var PACK_KEYS = new Set([
|
|
|
1349
1353
|
"identity_target",
|
|
1350
1354
|
"attempt"
|
|
1351
1355
|
]);
|
|
1352
|
-
var LEDGER_KEYS = new Set([
|
|
1356
|
+
var LEDGER_KEYS = /* @__PURE__ */ new Set([
|
|
1353
1357
|
"action",
|
|
1354
1358
|
"attempt_id",
|
|
1355
1359
|
"started_at",
|
|
@@ -1366,7 +1370,7 @@ var LEDGER_KEYS = new Set([
|
|
|
1366
1370
|
"db_path",
|
|
1367
1371
|
"identity_target"
|
|
1368
1372
|
]);
|
|
1369
|
-
var ATTEMPT_KEYS = new Set([
|
|
1373
|
+
var ATTEMPT_KEYS = /* @__PURE__ */ new Set([
|
|
1370
1374
|
"attempt_id",
|
|
1371
1375
|
"started_at",
|
|
1372
1376
|
"source",
|
|
@@ -1375,7 +1379,7 @@ var ATTEMPT_KEYS = new Set([
|
|
|
1375
1379
|
"prompt_number",
|
|
1376
1380
|
"request_id"
|
|
1377
1381
|
]);
|
|
1378
|
-
var FORBIDDEN_LEDGER_KEYS = new Set([
|
|
1382
|
+
var FORBIDDEN_LEDGER_KEYS = /* @__PURE__ */ new Set([
|
|
1379
1383
|
"body",
|
|
1380
1384
|
"context",
|
|
1381
1385
|
"pack",
|
|
@@ -1698,7 +1702,7 @@ function isClaudeBoundaryEnvelope(envelope) {
|
|
|
1698
1702
|
const hookEventName = meta.hook_event_name;
|
|
1699
1703
|
return hookEventName === "SessionEnd" || hookEventName === "Stop";
|
|
1700
1704
|
}
|
|
1701
|
-
var SAFE_INGEST_VALIDATION_ERRORS = new Set([
|
|
1705
|
+
var SAFE_INGEST_VALIDATION_ERRORS = /* @__PURE__ */ new Set([
|
|
1702
1706
|
"source must be string",
|
|
1703
1707
|
"source is required",
|
|
1704
1708
|
"source must use 1-64 letters, digits, dots, underscores, or hyphens",
|
|
@@ -2295,7 +2299,6 @@ var MAX_CONFIGURED_GROUPS = 25;
|
|
|
2295
2299
|
var MAX_SCOPE_EVIDENCE_COORDINATORS = 100;
|
|
2296
2300
|
var MAX_DEVICES = 500;
|
|
2297
2301
|
var MAX_PROJECTS = 500;
|
|
2298
|
-
var MAX_ACCESS_DELTA_ENTRIES = 1e4;
|
|
2299
2302
|
var MAX_IDENTITY_CHOICES = 500;
|
|
2300
2303
|
var MAX_COMPLETED_IDENTITY_CHOICES = MAX_DEVICES * 4;
|
|
2301
2304
|
var MAX_PROJECT_MAPPING_CHOICES = 500;
|
|
@@ -2315,6 +2318,46 @@ var ATTEMPT_ID_PATTERN = /^legacy-team-attempt:[0-9a-f-]{36}$/u;
|
|
|
2315
2318
|
var FINISH_DIGEST_PATTERN = /^legacy-team-activation-finish-v1:[0-9a-f]{64}$/u;
|
|
2316
2319
|
var ACCESS_DELTA_DIGEST_PATTERN = /^legacy-team-access-delta:[0-9a-f]{64}$/u;
|
|
2317
2320
|
var VIEWER_ACCESS_DELTA_DIGEST_PATTERN = /^legacy-team-viewer-access-delta-v1:[0-9a-f]{64}$/u;
|
|
2321
|
+
var activeCandidateMutations = /* @__PURE__ */ new WeakMap();
|
|
2322
|
+
function claimCandidateMutation(db, candidateRef) {
|
|
2323
|
+
let active = activeCandidateMutations.get(db);
|
|
2324
|
+
if (!active) {
|
|
2325
|
+
active = /* @__PURE__ */ new Map();
|
|
2326
|
+
activeCandidateMutations.set(db, active);
|
|
2327
|
+
}
|
|
2328
|
+
if (active.has(candidateRef)) return null;
|
|
2329
|
+
let settle = () => void 0;
|
|
2330
|
+
const settled = new Promise((resolve) => {
|
|
2331
|
+
settle = resolve;
|
|
2332
|
+
});
|
|
2333
|
+
active.set(candidateRef, settled);
|
|
2334
|
+
let released = false;
|
|
2335
|
+
return () => {
|
|
2336
|
+
if (released) return;
|
|
2337
|
+
released = true;
|
|
2338
|
+
active?.delete(candidateRef);
|
|
2339
|
+
settle();
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
/** Resolves once the current holder of the candidate claim (if any) releases it. */
|
|
2343
|
+
function candidateMutationSettled(db, candidateRef) {
|
|
2344
|
+
return activeCandidateMutations.get(db)?.get(candidateRef) ?? Promise.resolve();
|
|
2345
|
+
}
|
|
2346
|
+
async function runCandidateMutation(db, candidateRef, operation) {
|
|
2347
|
+
const release = claimCandidateMutation(db, candidateRef);
|
|
2348
|
+
if (!release) return false;
|
|
2349
|
+
try {
|
|
2350
|
+
await operation();
|
|
2351
|
+
return true;
|
|
2352
|
+
} finally {
|
|
2353
|
+
release();
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
var defaultCompletionDependencies = {
|
|
2357
|
+
create: coordinatorCreateLegacyTeamCompletionAction,
|
|
2358
|
+
get: coordinatorGetLegacyTeamCompletionAction,
|
|
2359
|
+
list: coordinatorListLegacyTeamCompletionsAction
|
|
2360
|
+
};
|
|
2318
2361
|
var defaultSnapshotLoaderDependencies = {
|
|
2319
2362
|
readConfig: readCoordinatorSyncConfig,
|
|
2320
2363
|
listGroups: coordinatorListGroupsAction,
|
|
@@ -2326,19 +2369,45 @@ function safeCoordinatorError() {
|
|
|
2326
2369
|
function isCoordinatorRosterTooLargeError(error) {
|
|
2327
2370
|
return error instanceof Error && error.message === "coordinator_response_too_large";
|
|
2328
2371
|
}
|
|
2372
|
+
function remoteCoordinatorHttpStatus(error) {
|
|
2373
|
+
if (!(error instanceof Error)) return null;
|
|
2374
|
+
const status = /Remote coordinator request failed \((\d{3})\):/u.exec(error.message)?.[1];
|
|
2375
|
+
return status ? Number(status) : null;
|
|
2376
|
+
}
|
|
2329
2377
|
function isTransientCoordinatorReadError(error) {
|
|
2330
2378
|
if (!(error instanceof Error)) return false;
|
|
2331
|
-
const remoteStatus =
|
|
2379
|
+
const remoteStatus = remoteCoordinatorHttpStatus(error);
|
|
2332
2380
|
if (remoteStatus) return [
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2381
|
+
408,
|
|
2382
|
+
429,
|
|
2383
|
+
500,
|
|
2384
|
+
502,
|
|
2385
|
+
503,
|
|
2386
|
+
504
|
|
2339
2387
|
].includes(remoteStatus);
|
|
2340
2388
|
return error.name === "TimeoutError" || error.name === "AbortError" || /request_timeout|fetch failed|timed out|timeout/iu.test(error.message);
|
|
2341
2389
|
}
|
|
2390
|
+
function isUnsupportedCompletionQuery(error) {
|
|
2391
|
+
const status = remoteCoordinatorHttpStatus(error);
|
|
2392
|
+
return status === 404 || status === 405;
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Batch completion decoding rejects the whole payload when any one record is
|
|
2396
|
+
* malformed or misattributed, so the failure cannot be pinned to a group.
|
|
2397
|
+
*/
|
|
2398
|
+
function isUnattributableCompletionBatchError(error) {
|
|
2399
|
+
return error instanceof Error && (error.message === "coordinator_completion_response_malformed" || error.message === "coordinator_completion_group_mismatch");
|
|
2400
|
+
}
|
|
2401
|
+
async function listCompletionRecordsPerGroup(groupIds, list) {
|
|
2402
|
+
if (groupIds.length < 2) return [];
|
|
2403
|
+
const records = [];
|
|
2404
|
+
for (const groupId of groupIds) try {
|
|
2405
|
+
records.push(...await list([groupId]));
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
if (!isUnattributableCompletionBatchError(error)) throw error;
|
|
2408
|
+
}
|
|
2409
|
+
return records;
|
|
2410
|
+
}
|
|
2342
2411
|
async function retryTransientCoordinatorRead(read, configuredTimeoutS, deadlineMs) {
|
|
2343
2412
|
let lastError;
|
|
2344
2413
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
@@ -2356,6 +2425,10 @@ async function retryTransientCoordinatorRead(read, configuredTimeoutS, deadlineM
|
|
|
2356
2425
|
}
|
|
2357
2426
|
throw lastError;
|
|
2358
2427
|
}
|
|
2428
|
+
function remainingCoordinatorTimeoutS(configuredTimeoutS, deadlineMs, nowMs = Date.now()) {
|
|
2429
|
+
const remainingMs = deadlineMs - nowMs;
|
|
2430
|
+
return remainingMs <= 0 ? null : Math.max(.1, Math.min(configuredTimeoutS, remainingMs / 1e3));
|
|
2431
|
+
}
|
|
2359
2432
|
function configuredGroupIds(groups) {
|
|
2360
2433
|
const unique = [...new Set(groups.map((group) => group.trim()).filter(Boolean))];
|
|
2361
2434
|
if (unique.length > MAX_CONFIGURED_GROUPS) throw safeCoordinatorError();
|
|
@@ -2456,7 +2529,7 @@ async function loadConfiguredLegacyTeamGroupSnapshotsWith(dependencies, options
|
|
|
2456
2529
|
const groupDescriptors = legacyTeamCandidateGroupDescriptors(store, config.syncCoordinatorUrl, config.syncCoordinatorGroups);
|
|
2457
2530
|
if (groupDescriptors.length === 0) throw safeCoordinatorError();
|
|
2458
2531
|
const timeoutS = Math.max(1, config.syncCoordinatorTimeoutS);
|
|
2459
|
-
const deadlineMs = Date.now() + COORDINATOR_ROSTER_READ_BUDGET_MS;
|
|
2532
|
+
const deadlineMs = options.deadlineMs ?? Date.now() + COORDINATOR_ROSTER_READ_BUDGET_MS;
|
|
2460
2533
|
const requestedGroupDescriptors = options.candidateRef ? groupDescriptors.filter(({ coordinatorId, groupId }) => legacyTeamCandidateId(coordinatorId, groupId) === options.candidateRef) : groupDescriptors;
|
|
2461
2534
|
const groups = await retryTransientCoordinatorRead((attemptTimeoutS) => dependencies.listGroups({
|
|
2462
2535
|
remoteUrl,
|
|
@@ -2712,10 +2785,6 @@ function viewerSafeAccessDelta(candidateRef, delta, labels) {
|
|
|
2712
2785
|
function viewerAccessDeltaDigest(delta) {
|
|
2713
2786
|
return recipientPolicyDigest("legacy-team-viewer-access-delta-v1", delta);
|
|
2714
2787
|
}
|
|
2715
|
-
function requireBoundedAccessDelta(delta) {
|
|
2716
|
-
const total = delta.teamChanges.length + delta.membershipChanges.length + delta.projectChanges.length + delta.recipientChanges.length + delta.deviceAccessChanges.length;
|
|
2717
|
-
if (delta.teamChanges.length > MAX_ACCESS_DELTA_ENTRIES || delta.membershipChanges.length > MAX_ACCESS_DELTA_ENTRIES || delta.projectChanges.length > MAX_ACCESS_DELTA_ENTRIES || delta.recipientChanges.length > MAX_ACCESS_DELTA_ENTRIES || delta.deviceAccessChanges.length > MAX_ACCESS_DELTA_ENTRIES || total > MAX_ACCESS_DELTA_ENTRIES) throw new Error("legacy_team_setup_roster_too_large");
|
|
2718
|
-
}
|
|
2719
2788
|
var SAFE_CHOICE_LABEL_PATTERN = /^[\p{L}\p{N} '&,.()_-]*$/u;
|
|
2720
2789
|
function normalizeChoiceLabelText(value) {
|
|
2721
2790
|
return value.normalize("NFKC").replace(/\p{Cf}/gu, "").replace(/\p{Cc}/gu, " ").replace(/\s+/gu, " ").trim();
|
|
@@ -2919,7 +2988,7 @@ function viewerSafePreview(draft, safeDraft, preview) {
|
|
|
2919
2988
|
};
|
|
2920
2989
|
}
|
|
2921
2990
|
function errorStatus$1(code) {
|
|
2922
|
-
if (code === "team_setup_roster_unavailable") return 503;
|
|
2991
|
+
if (code === "team_setup_roster_unavailable" || code === "team_setup_completion_unavailable") return 503;
|
|
2923
2992
|
if (code === "team_setup_failed") return 503;
|
|
2924
2993
|
if (code === "team_setup_incomplete") return 400;
|
|
2925
2994
|
return 409;
|
|
@@ -2928,6 +2997,10 @@ function apiErrorCode(error) {
|
|
|
2928
2997
|
const code = legacyTeamSetupApiErrorCode(error);
|
|
2929
2998
|
return code === "team_setup_projection_changed" ? "team_setup_conflict" : code;
|
|
2930
2999
|
}
|
|
3000
|
+
function completionApiError(error) {
|
|
3001
|
+
const code = legacyTeamSetupApiErrorCode(error);
|
|
3002
|
+
return code === "team_setup_failed" ? "team_setup_completion_unavailable" : code;
|
|
3003
|
+
}
|
|
2931
3004
|
async function parseBoundedJsonObject(c) {
|
|
2932
3005
|
if (c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return { ok: false };
|
|
2933
3006
|
const lengthHeader = c.req.header("content-length");
|
|
@@ -2991,7 +3064,7 @@ function detailResponse(store, draft, candidate = candidateFromDraft(draft)) {
|
|
|
2991
3064
|
if (code === "team_setup_projection_changed") unavailableReason = "team_setup_conflict";
|
|
2992
3065
|
else if (code !== "team_setup_incomplete") unavailableReason = code;
|
|
2993
3066
|
}
|
|
2994
|
-
if (preview)
|
|
3067
|
+
if (preview) requireLegacyTeamSetupAccessDeltaWithinLimit(preview.accessDelta);
|
|
2995
3068
|
const safeDraft = viewerSafeDraft(store, draft);
|
|
2996
3069
|
const safePreview = preview ? viewerSafePreview(draft, safeDraft, preview) : null;
|
|
2997
3070
|
return projectLegacyTeamSetupView({
|
|
@@ -3019,9 +3092,12 @@ function finishResponse(candidateRef, result) {
|
|
|
3019
3092
|
function teamSetupRoutes(options) {
|
|
3020
3093
|
const app = new Hono();
|
|
3021
3094
|
const loadSnapshots = options.loadLegacyTeamConfiguredGroupSnapshots ?? ((loadOptions) => loadConfiguredLegacyTeamGroupSnapshotsWith(options.snapshotLoaderDependencies ?? defaultSnapshotLoaderDependencies, loadOptions, options.getStore));
|
|
3022
|
-
async function loadedSnapshots(candidateRef) {
|
|
3095
|
+
async function loadedSnapshots(candidateRef, deadlineMs) {
|
|
3023
3096
|
try {
|
|
3024
|
-
return await loadSnapshots(candidateRef
|
|
3097
|
+
return await loadSnapshots(candidateRef || deadlineMs != null ? {
|
|
3098
|
+
candidateRef,
|
|
3099
|
+
deadlineMs
|
|
3100
|
+
} : void 0);
|
|
3025
3101
|
} catch (error) {
|
|
3026
3102
|
if (error instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
|
|
3027
3103
|
throw safeCoordinatorError();
|
|
@@ -3029,12 +3105,341 @@ function teamSetupRoutes(options) {
|
|
|
3029
3105
|
}
|
|
3030
3106
|
const loadedSummarySnapshots = createCachedSnapshotLoader(() => loadedSnapshots(), SUMMARY_SNAPSHOT_CACHE_TTL_MS);
|
|
3031
3107
|
options.registerSummaryInvalidator?.(() => loadedSummarySnapshots.invalidate());
|
|
3108
|
+
const completionDependencies = options.completionDependencies === void 0 ? defaultCompletionDependencies : options.completionDependencies;
|
|
3109
|
+
/**
|
|
3110
|
+
* Reads the live coordinator configuration and the groups it currently
|
|
3111
|
+
* authorizes. Returns null when completion I/O is not configured or current
|
|
3112
|
+
* authorization cannot be proven.
|
|
3113
|
+
*/
|
|
3114
|
+
function currentCompletionAuthorization() {
|
|
3115
|
+
const config = (options.readCoordinatorConfig ?? options.snapshotLoaderDependencies?.readConfig ?? readCoordinatorSyncConfig)();
|
|
3116
|
+
const remoteUrl = buildBaseUrl(config.syncCoordinatorUrl);
|
|
3117
|
+
if (!remoteUrl || !config.syncCoordinatorAdminSecret) return null;
|
|
3118
|
+
const configuredCoordinatorId = normalizedCoordinatorId(remoteUrl);
|
|
3119
|
+
if (!configuredCoordinatorId) return null;
|
|
3120
|
+
let currentGroupIds;
|
|
3121
|
+
try {
|
|
3122
|
+
currentGroupIds = new Set(configuredGroupIds(config.syncCoordinatorGroups));
|
|
3123
|
+
} catch {
|
|
3124
|
+
return null;
|
|
3125
|
+
}
|
|
3126
|
+
return {
|
|
3127
|
+
config,
|
|
3128
|
+
remoteUrl,
|
|
3129
|
+
configuredCoordinatorId,
|
|
3130
|
+
currentGroupIds
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
function authorizesGroup(authorization, group) {
|
|
3134
|
+
return authorization.currentGroupIds.has(group.groupId) && normalizedCoordinatorId(group.coordinatorId) === authorization.configuredCoordinatorId;
|
|
3135
|
+
}
|
|
3136
|
+
/**
|
|
3137
|
+
* Configuration may change while completion-list or roster requests are in
|
|
3138
|
+
* flight. Re-prove authorization for the exact coordinator and group right
|
|
3139
|
+
* before applying canonical policy, as the finish publication path does.
|
|
3140
|
+
*/
|
|
3141
|
+
function stillAuthorizesGroup(initial, group) {
|
|
3142
|
+
let current;
|
|
3143
|
+
try {
|
|
3144
|
+
current = currentCompletionAuthorization();
|
|
3145
|
+
} catch {
|
|
3146
|
+
return false;
|
|
3147
|
+
}
|
|
3148
|
+
return current !== null && current.configuredCoordinatorId === initial.configuredCoordinatorId && current.remoteUrl === initial.remoteUrl && current.config.syncCoordinatorAdminSecret === initial.config.syncCoordinatorAdminSecret && authorizesGroup(current, group);
|
|
3149
|
+
}
|
|
3150
|
+
function matchesGroupSnapshot(snapshot, group) {
|
|
3151
|
+
return snapshot.groupId === group.groupId && normalizedCoordinatorId(snapshot.coordinatorId) === normalizedCoordinatorId(group.coordinatorId);
|
|
3152
|
+
}
|
|
3153
|
+
function freshGroupKey(group) {
|
|
3154
|
+
return `${normalizedCoordinatorId(group.coordinatorId) ?? group.coordinatorId}\0${group.groupId}`;
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* The snapshot loader keys candidates by the configured coordinator spelling,
|
|
3158
|
+
* while a persisted group may carry an equivalent but different spelling.
|
|
3159
|
+
* Ask for the roster under each spelling and match on normalized identity,
|
|
3160
|
+
* preserving the persisted candidate identity for everything else.
|
|
3161
|
+
*/
|
|
3162
|
+
async function loadFreshGroupSnapshot(group, remoteUrl, deadlineMs) {
|
|
3163
|
+
const candidateRefs = [.../* @__PURE__ */ new Set([legacyTeamCandidateId(remoteUrl, group.groupId), legacyTeamCandidateId(group.coordinatorId, group.groupId)])];
|
|
3164
|
+
for (const candidateRef of candidateRefs) {
|
|
3165
|
+
const matches = (await loadedSnapshots(candidateRef, deadlineMs)).filter((snapshot) => matchesGroupSnapshot(snapshot, group));
|
|
3166
|
+
if (matches.length > 1) throw new Error("team_setup_completion_invalid");
|
|
3167
|
+
if (matches[0]) return matches[0];
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
function completionRecordCandidateRef(record) {
|
|
3171
|
+
const manifest = record.manifest;
|
|
3172
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return null;
|
|
3173
|
+
const candidateRef = manifest.candidate_ref;
|
|
3174
|
+
return typeof candidateRef === "string" ? candidateRef : null;
|
|
3175
|
+
}
|
|
3176
|
+
function belongsToGroupCandidate(record, group) {
|
|
3177
|
+
const candidateRef = completionRecordCandidateRef(record);
|
|
3178
|
+
return candidateRef === null || candidateRef === legacyTeamCandidateId(group.coordinatorId, group.groupId);
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Applies a canonical manifest with authorization re-proven inside the same
|
|
3182
|
+
* serialized section that guards the apply. Checking before the locks would
|
|
3183
|
+
* let a group removed while reconciliation waited on another policy
|
|
3184
|
+
* mutation still be applied once the queues open.
|
|
3185
|
+
*/
|
|
3186
|
+
async function applyCanonicalManifestIfStillAuthorized(store, authorization, group, manifest, freshRoster) {
|
|
3187
|
+
let outcome = "unauthorized";
|
|
3188
|
+
return await runCandidateMutation(store.db, legacyTeamCandidateId(group.coordinatorId, group.groupId), () => serializeRecipientPolicyPublicationMutation(store.db, () => serializeRecipientPolicyTeamMutation(store.db, manifest.team_id, () => serializeRecipientPolicyCoordinatorGroupMutation(store.db, group.groupId, async () => {
|
|
3189
|
+
if (!stillAuthorizesGroup(authorization, group)) return;
|
|
3190
|
+
await applyLegacyTeamSetupCompletionManifest(store.db, {
|
|
3191
|
+
coordinatorId: group.coordinatorId,
|
|
3192
|
+
groupId: group.groupId,
|
|
3193
|
+
manifest,
|
|
3194
|
+
freshRoster,
|
|
3195
|
+
recipientPolicyLocksHeld: {
|
|
3196
|
+
publication: true,
|
|
3197
|
+
team: true,
|
|
3198
|
+
coordinatorGroup: true
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
outcome = "applied";
|
|
3202
|
+
})))) ? outcome : "claimed";
|
|
3203
|
+
}
|
|
3204
|
+
async function reconcileCompletions(store, groups, reconciliationOptions = {}) {
|
|
3205
|
+
if (!completionDependencies || groups.length === 0) return;
|
|
3206
|
+
let authorization;
|
|
3207
|
+
try {
|
|
3208
|
+
authorization = currentCompletionAuthorization();
|
|
3209
|
+
} catch (error) {
|
|
3210
|
+
throw new Error(completionApiError(error));
|
|
3211
|
+
}
|
|
3212
|
+
if (!authorization) return;
|
|
3213
|
+
const { config, remoteUrl } = authorization;
|
|
3214
|
+
const scopedGroups = groups.filter((group) => authorizesGroup(authorization, group));
|
|
3215
|
+
if (scopedGroups.length === 0) return;
|
|
3216
|
+
const timeoutS = Math.max(1, config.syncCoordinatorTimeoutS);
|
|
3217
|
+
try {
|
|
3218
|
+
const reconciliationDeadlineMs = Date.now() + COORDINATOR_ROSTER_READ_BUDGET_MS;
|
|
3219
|
+
const groupById = new Map(scopedGroups.map((group) => [group.groupId, group]));
|
|
3220
|
+
const listCompletionRecords = (groupIds) => retryTransientCoordinatorRead((attemptTimeoutS) => completionDependencies.list({
|
|
3221
|
+
groupIds,
|
|
3222
|
+
remoteUrl,
|
|
3223
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3224
|
+
timeoutS: attemptTimeoutS
|
|
3225
|
+
}), timeoutS, reconciliationDeadlineMs);
|
|
3226
|
+
const scopedGroupIds = [...new Set(scopedGroups.map((group) => group.groupId))];
|
|
3227
|
+
let records;
|
|
3228
|
+
try {
|
|
3229
|
+
records = await listCompletionRecords(scopedGroupIds);
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
if (isUnsupportedCompletionQuery(error)) records = [];
|
|
3232
|
+
else if (reconciliationOptions.isolateApplyFailures && isUnattributableCompletionBatchError(error)) records = await listCompletionRecordsPerGroup(scopedGroupIds, listCompletionRecords);
|
|
3233
|
+
else throw error;
|
|
3234
|
+
}
|
|
3235
|
+
const freshGroupByKey = /* @__PURE__ */ new Map();
|
|
3236
|
+
if (records.length > 0) try {
|
|
3237
|
+
const onlyGroup = scopedGroups.length === 1 ? scopedGroups[0] : void 0;
|
|
3238
|
+
const freshGroups = onlyGroup ? [await loadFreshGroupSnapshot(onlyGroup, remoteUrl, reconciliationDeadlineMs)] : await loadedSnapshots(void 0, reconciliationDeadlineMs);
|
|
3239
|
+
for (const freshGroup of freshGroups) {
|
|
3240
|
+
if (!freshGroup) continue;
|
|
3241
|
+
const key = freshGroupKey(freshGroup);
|
|
3242
|
+
if (freshGroupByKey.has(key)) throw new Error("team_setup_completion_invalid");
|
|
3243
|
+
freshGroupByKey.set(key, freshGroup);
|
|
3244
|
+
}
|
|
3245
|
+
} catch (error) {
|
|
3246
|
+
for (const record of records) {
|
|
3247
|
+
const group = groupById.get(record.group_id);
|
|
3248
|
+
if (!group || !belongsToGroupCandidate(record, group)) continue;
|
|
3249
|
+
try {
|
|
3250
|
+
validateLegacyTeamSetupCompletionManifestBinding(record.manifest, {
|
|
3251
|
+
coordinatorId: group.coordinatorId,
|
|
3252
|
+
groupId: group.groupId
|
|
3253
|
+
});
|
|
3254
|
+
} catch (bindingError) {
|
|
3255
|
+
if (!reconciliationOptions.isolateApplyFailures) throw bindingError;
|
|
3256
|
+
continue;
|
|
3257
|
+
}
|
|
3258
|
+
let canonicalManifest;
|
|
3259
|
+
try {
|
|
3260
|
+
canonicalManifest = validateLegacyTeamSetupCompletionManifest(record.manifest, {
|
|
3261
|
+
coordinatorId: group.coordinatorId,
|
|
3262
|
+
groupId: group.groupId
|
|
3263
|
+
});
|
|
3264
|
+
} catch (validationError) {
|
|
3265
|
+
try {
|
|
3266
|
+
if (!await runCandidateMutation(store.db, legacyTeamCandidateId(group.coordinatorId, group.groupId), () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3267
|
+
coordinatorId: group.coordinatorId,
|
|
3268
|
+
groupId: group.groupId
|
|
3269
|
+
}))) {
|
|
3270
|
+
if (!reconciliationOptions.isolateApplyFailures) throw validationError;
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
} catch (containmentError) {
|
|
3274
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3275
|
+
}
|
|
3276
|
+
if (!reconciliationOptions.isolateApplyFailures) throw validationError;
|
|
3277
|
+
continue;
|
|
3278
|
+
}
|
|
3279
|
+
try {
|
|
3280
|
+
const localManifest = reconstructLegacyTeamSetupCompletionManifest(store.db, { candidateRef: canonicalManifest.candidate_ref });
|
|
3281
|
+
if (canonicalCoordinatorLegacyTeamCompletionManifestJson(localManifest) === canonicalCoordinatorLegacyTeamCompletionManifestJson(canonicalManifest)) continue;
|
|
3282
|
+
if (areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible(localManifest, canonicalManifest)) continue;
|
|
3283
|
+
} catch {}
|
|
3284
|
+
try {
|
|
3285
|
+
await runCandidateMutation(store.db, canonicalManifest.candidate_ref, () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3286
|
+
coordinatorId: group.coordinatorId,
|
|
3287
|
+
groupId: group.groupId
|
|
3288
|
+
}));
|
|
3289
|
+
} catch (containmentError) {
|
|
3290
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
if (!reconciliationOptions.isolateApplyFailures) throw error;
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
const coordinatorCompletionGroupIds = /* @__PURE__ */ new Set();
|
|
3297
|
+
for (const record of records) {
|
|
3298
|
+
const group = groupById.get(record.group_id);
|
|
3299
|
+
if (!group) throw new Error("team_setup_completion_invalid");
|
|
3300
|
+
if (!belongsToGroupCandidate(record, group)) continue;
|
|
3301
|
+
try {
|
|
3302
|
+
validateLegacyTeamSetupCompletionManifestBinding(record.manifest, {
|
|
3303
|
+
coordinatorId: group.coordinatorId,
|
|
3304
|
+
groupId: group.groupId
|
|
3305
|
+
});
|
|
3306
|
+
} catch (bindingError) {
|
|
3307
|
+
if (!reconciliationOptions.isolateApplyFailures) throw bindingError;
|
|
3308
|
+
continue;
|
|
3309
|
+
}
|
|
3310
|
+
let canonicalManifest;
|
|
3311
|
+
try {
|
|
3312
|
+
canonicalManifest = validateLegacyTeamSetupCompletionManifest(record.manifest, {
|
|
3313
|
+
coordinatorId: group.coordinatorId,
|
|
3314
|
+
groupId: group.groupId
|
|
3315
|
+
});
|
|
3316
|
+
} catch (validationError) {
|
|
3317
|
+
try {
|
|
3318
|
+
await runCandidateMutation(store.db, legacyTeamCandidateId(group.coordinatorId, group.groupId), () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3319
|
+
coordinatorId: group.coordinatorId,
|
|
3320
|
+
groupId: group.groupId
|
|
3321
|
+
}));
|
|
3322
|
+
} catch (containmentError) {
|
|
3323
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3324
|
+
}
|
|
3325
|
+
if (!reconciliationOptions.isolateApplyFailures) throw validationError;
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
coordinatorCompletionGroupIds.add(record.group_id);
|
|
3329
|
+
try {
|
|
3330
|
+
let freshGroup = freshGroupByKey.get(freshGroupKey(group));
|
|
3331
|
+
if (!freshGroup && scopedGroups.length > 1) {
|
|
3332
|
+
if (remainingCoordinatorTimeoutS(timeoutS, reconciliationDeadlineMs) == null) throw new Error("team_setup_roster_unavailable");
|
|
3333
|
+
freshGroup = await loadFreshGroupSnapshot(group, remoteUrl, reconciliationDeadlineMs);
|
|
3334
|
+
}
|
|
3335
|
+
if (!freshGroup) {
|
|
3336
|
+
if (!await runCandidateMutation(store.db, legacyTeamCandidateId(group.coordinatorId, group.groupId), () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3337
|
+
coordinatorId: group.coordinatorId,
|
|
3338
|
+
groupId: group.groupId
|
|
3339
|
+
}))) {
|
|
3340
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_invalid");
|
|
3341
|
+
continue;
|
|
3342
|
+
}
|
|
3343
|
+
throw new Error("team_setup_completion_invalid");
|
|
3344
|
+
}
|
|
3345
|
+
if (await applyCanonicalManifestIfStillAuthorized(store, authorization, group, canonicalManifest, freshGroup.devices) !== "applied") continue;
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
if (!reconciliationOptions.isolateApplyFailures) throw error;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
for (const group of scopedGroups) {
|
|
3351
|
+
if (coordinatorCompletionGroupIds.has(group.groupId)) continue;
|
|
3352
|
+
const publicationTimeoutS = remainingCoordinatorTimeoutS(timeoutS, reconciliationDeadlineMs);
|
|
3353
|
+
if (publicationTimeoutS == null) break;
|
|
3354
|
+
if (!hasCompletedLegacyTeamSetup(store, legacyTeamCandidateId(group.coordinatorId, group.groupId))) continue;
|
|
3355
|
+
const candidateRef = legacyTeamCandidateId(group.coordinatorId, group.groupId);
|
|
3356
|
+
let reconstructionFailed = false;
|
|
3357
|
+
try {
|
|
3358
|
+
if (!await runCandidateMutation(store.db, candidateRef, () => serializeRecipientPolicyPublicationMutation(store.db, () => serializeRecipientPolicyTeamMutation(store.db, deterministicPolicyTeamId(candidateRef), async () => {
|
|
3359
|
+
let manifest;
|
|
3360
|
+
try {
|
|
3361
|
+
manifest = reconstructLegacyTeamSetupCompletionManifest(store.db, { candidateRef });
|
|
3362
|
+
} catch {
|
|
3363
|
+
reconstructionFailed = true;
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
if (!stillAuthorizesGroup(authorization, group)) {
|
|
3367
|
+
reconstructionFailed = true;
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
await completionDependencies.create({
|
|
3371
|
+
groupId: group.groupId,
|
|
3372
|
+
manifest,
|
|
3373
|
+
remoteUrl,
|
|
3374
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3375
|
+
timeoutS: publicationTimeoutS
|
|
3376
|
+
});
|
|
3377
|
+
}))) || reconstructionFailed) continue;
|
|
3378
|
+
} catch (error) {
|
|
3379
|
+
if (completionApiError(error) !== "team_setup_completion_conflict") continue;
|
|
3380
|
+
const getCompletion = completionDependencies.get;
|
|
3381
|
+
let canonicalManifest = null;
|
|
3382
|
+
try {
|
|
3383
|
+
if (!getCompletion) throw new Error("team_setup_completion_conflict");
|
|
3384
|
+
canonicalManifest = await retryTransientCoordinatorRead((attemptTimeoutS) => getCompletion({
|
|
3385
|
+
groupId: group.groupId,
|
|
3386
|
+
candidateRef,
|
|
3387
|
+
remoteUrl,
|
|
3388
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3389
|
+
timeoutS: attemptTimeoutS
|
|
3390
|
+
}), timeoutS, reconciliationDeadlineMs);
|
|
3391
|
+
if (!canonicalManifest) throw new Error("team_setup_completion_conflict");
|
|
3392
|
+
} catch {
|
|
3393
|
+
try {
|
|
3394
|
+
if (!await runCandidateMutation(store.db, candidateRef, () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3395
|
+
coordinatorId: group.coordinatorId,
|
|
3396
|
+
groupId: group.groupId
|
|
3397
|
+
}))) {
|
|
3398
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_conflict");
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
} catch (containmentError) {
|
|
3402
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_conflict");
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
try {
|
|
3409
|
+
const freshGroup = await loadFreshGroupSnapshot(group, remoteUrl, reconciliationDeadlineMs);
|
|
3410
|
+
if (!freshGroup) throw new Error("team_setup_roster_unavailable");
|
|
3411
|
+
if (await applyCanonicalManifestIfStillAuthorized(store, authorization, group, canonicalManifest, freshGroup.devices) !== "applied") continue;
|
|
3412
|
+
} catch (applyError) {
|
|
3413
|
+
if (applyError instanceof LegacyTeamSetupAdditiveConvergenceError) {
|
|
3414
|
+
if (!reconciliationOptions.isolateApplyFailures) throw applyError;
|
|
3415
|
+
continue;
|
|
3416
|
+
}
|
|
3417
|
+
try {
|
|
3418
|
+
if (!await runCandidateMutation(store.db, candidateRef, () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3419
|
+
coordinatorId: group.coordinatorId,
|
|
3420
|
+
groupId: group.groupId
|
|
3421
|
+
}))) {
|
|
3422
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_conflict", { cause: applyError });
|
|
3423
|
+
continue;
|
|
3424
|
+
}
|
|
3425
|
+
} catch (containmentError) {
|
|
3426
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
3429
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_conflict", { cause: applyError });
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
throw new Error(completionApiError(error));
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3032
3437
|
async function loadedCandidateSnapshots(candidateRef, loadOptions = {}) {
|
|
3033
3438
|
const allowCachedSummaryFallback = loadOptions.allowCachedSummaryFallback ?? false;
|
|
3034
3439
|
const invalidateSummaryCache = loadOptions.invalidateSummaryCache ?? true;
|
|
3035
3440
|
if (invalidateSummaryCache) loadedSummarySnapshots.invalidate();
|
|
3036
3441
|
try {
|
|
3037
|
-
return await loadedSnapshots(candidateRef);
|
|
3442
|
+
return await loadedSnapshots(candidateRef, loadOptions.deadlineMs);
|
|
3038
3443
|
} catch (error) {
|
|
3039
3444
|
if (!allowCachedSummaryFallback) throw error;
|
|
3040
3445
|
const cachedGroups = loadedSummarySnapshots.peek();
|
|
@@ -3046,6 +3451,15 @@ function teamSetupRoutes(options) {
|
|
|
3046
3451
|
if (invalidateSummaryCache) loadedSummarySnapshots.invalidate();
|
|
3047
3452
|
}
|
|
3048
3453
|
}
|
|
3454
|
+
function persistedCompletionGroup(store, candidateRef) {
|
|
3455
|
+
const row = store.db.prepare(`SELECT coordinator_id, group_id FROM legacy_team_setup_drafts
|
|
3456
|
+
WHERE candidate_id = ? ORDER BY rowid DESC LIMIT 1`).get(candidateRef);
|
|
3457
|
+
if (!row || legacyTeamCandidateId(row.coordinator_id, row.group_id) !== candidateRef) return null;
|
|
3458
|
+
return {
|
|
3459
|
+
coordinatorId: row.coordinator_id,
|
|
3460
|
+
groupId: row.group_id
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3049
3463
|
app.get("/api/sync/team-setup/v1", async (c) => {
|
|
3050
3464
|
let groups;
|
|
3051
3465
|
try {
|
|
@@ -3055,9 +3469,12 @@ function teamSetupRoutes(options) {
|
|
|
3055
3469
|
return c.json({ error: code }, errorStatus$1(code));
|
|
3056
3470
|
}
|
|
3057
3471
|
try {
|
|
3472
|
+
const store = options.getStore();
|
|
3473
|
+
readPureCandidateViews(store, groups);
|
|
3474
|
+
await reconcileCompletions(store, groups, { isolateApplyFailures: true });
|
|
3058
3475
|
const response = {
|
|
3059
3476
|
version: TEAM_SETUP_VERSION,
|
|
3060
|
-
candidates: readPureCandidateViews(
|
|
3477
|
+
candidates: readPureCandidateViews(store, groups).map(candidateSummary).filter((candidate) => candidate.status !== "ready")
|
|
3061
3478
|
};
|
|
3062
3479
|
return c.json(response);
|
|
3063
3480
|
} catch (error) {
|
|
@@ -3072,6 +3489,7 @@ function teamSetupRoutes(options) {
|
|
|
3072
3489
|
const store = options.getStore();
|
|
3073
3490
|
let draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3074
3491
|
const terminal = hasCompletedLegacyTeamSetup(store, candidateRef);
|
|
3492
|
+
let candidateGroupsForCompletion = null;
|
|
3075
3493
|
if (terminal && draft?.state !== "completed") return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3076
3494
|
let candidate;
|
|
3077
3495
|
if (draft && (draft.state !== "completed" || terminal)) candidate = candidateFromDraft(draft);
|
|
@@ -3080,6 +3498,7 @@ function teamSetupRoutes(options) {
|
|
|
3080
3498
|
allowCachedSummaryFallback: draft == null,
|
|
3081
3499
|
invalidateSummaryCache: false
|
|
3082
3500
|
})).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
|
|
3501
|
+
candidateGroupsForCompletion = candidateGroups;
|
|
3083
3502
|
const postLoadDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3084
3503
|
if (hasCompletedLegacyTeamSetup(store, candidateRef)) {
|
|
3085
3504
|
if (postLoadDraft?.state !== "completed") return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
@@ -3093,6 +3512,17 @@ function teamSetupRoutes(options) {
|
|
|
3093
3512
|
}
|
|
3094
3513
|
}
|
|
3095
3514
|
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3515
|
+
const cachedCandidateGroups = loadedSummarySnapshots.peek()?.filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
|
|
3516
|
+
let completionGroups = candidateGroupsForCompletion ?? (cachedCandidateGroups?.length ? cachedCandidateGroups : null);
|
|
3517
|
+
if (!completionGroups) {
|
|
3518
|
+
const persistedGroup = persistedCompletionGroup(store, candidateRef);
|
|
3519
|
+
completionGroups = persistedGroup ? [persistedGroup] : null;
|
|
3520
|
+
}
|
|
3521
|
+
const terminalBeforeReconciliation = hasCompletedLegacyTeamSetup(store, candidateRef);
|
|
3522
|
+
if (completionGroups) await reconcileCompletions(store, completionGroups);
|
|
3523
|
+
draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3524
|
+
if (!draft || !terminalBeforeReconciliation && hasCompletedLegacyTeamSetup(store, candidateRef)) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3525
|
+
if (completionGroups) candidate = candidateFromDraft(draft);
|
|
3096
3526
|
return c.json(detailResponse(store, draft, candidate));
|
|
3097
3527
|
} catch (error) {
|
|
3098
3528
|
const code = apiErrorCode(error);
|
|
@@ -3128,12 +3558,18 @@ function teamSetupRoutes(options) {
|
|
|
3128
3558
|
]) && submittedExpectation.kind === "existing" && submittedExpectation.assignmentVersion === device.expectation.assignmentVersion && submittedExpectation.identityRef === requiredIdentityRef(candidateRef, device.expectation.identityId))) return c.json({ error: "team_setup_assignment_changed" }, 409);
|
|
3129
3559
|
const target = identityChoices(store, candidateRef).find((choice) => choice.identityRef === targetIdentityRef);
|
|
3130
3560
|
if (!target) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3561
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3562
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3563
|
+
try {
|
|
3564
|
+
return c.json(projectMutationAtomically(store, () => setLegacyTeamSetupDeviceAssignment(store.db, {
|
|
3565
|
+
attemptId,
|
|
3566
|
+
deviceRef,
|
|
3567
|
+
targetIdentityId: target.identityId,
|
|
3568
|
+
expectation: device.expectation
|
|
3569
|
+
}), (draft) => detailResponse(store, draft)));
|
|
3570
|
+
} finally {
|
|
3571
|
+
releaseMutation();
|
|
3572
|
+
}
|
|
3137
3573
|
} catch (error) {
|
|
3138
3574
|
const code = apiErrorCode(error);
|
|
3139
3575
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3165,11 +3601,17 @@ function teamSetupRoutes(options) {
|
|
|
3165
3601
|
const device = draft.devices.find((item) => item.deviceRef === deviceRef);
|
|
3166
3602
|
if (!device) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3167
3603
|
if (decision === "included" && (device.targetIdentityId === null || requiredIdentityRef(candidateRef, device.targetIdentityId) !== expectedTargetIdentityRef)) return c.json({ error: "team_setup_assignment_changed" }, 409);
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3604
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3605
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3606
|
+
try {
|
|
3607
|
+
return c.json(projectMutationAtomically(store, () => setLegacyTeamSetupDeviceDecision(store.db, {
|
|
3608
|
+
attemptId,
|
|
3609
|
+
deviceRef,
|
|
3610
|
+
decision
|
|
3611
|
+
}), (draft) => detailResponse(store, draft)));
|
|
3612
|
+
} finally {
|
|
3613
|
+
releaseMutation();
|
|
3614
|
+
}
|
|
3173
3615
|
} catch (error) {
|
|
3174
3616
|
const code = apiErrorCode(error);
|
|
3175
3617
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3191,10 +3633,16 @@ function teamSetupRoutes(options) {
|
|
|
3191
3633
|
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3192
3634
|
if (draft.state === "completed") return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3193
3635
|
if (!draft.devices.some((item) => item.deviceRef === deviceRef)) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3636
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3637
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3638
|
+
try {
|
|
3639
|
+
return c.json(projectMutationAtomically(store, () => clearLegacyTeamSetupDeviceDecision(store.db, {
|
|
3640
|
+
attemptId,
|
|
3641
|
+
deviceRef
|
|
3642
|
+
}), (draft) => detailResponse(store, draft)));
|
|
3643
|
+
} finally {
|
|
3644
|
+
releaseMutation();
|
|
3645
|
+
}
|
|
3198
3646
|
} catch (error) {
|
|
3199
3647
|
const code = apiErrorCode(error);
|
|
3200
3648
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3220,11 +3668,17 @@ function teamSetupRoutes(options) {
|
|
|
3220
3668
|
if (project.resolution === "deterministic") return c.json({ error: "team_setup_incomplete" }, 400);
|
|
3221
3669
|
const target = projectMappingChoices(store).find((choice) => legacyTeamResolvedProjectRef(projectRef, choice.projectIdentity) === resolvedProjectRef);
|
|
3222
3670
|
if (!target) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3671
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3672
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3673
|
+
try {
|
|
3674
|
+
return c.json(projectMutationAtomically(store, () => setLegacyTeamSetupProjectMapping(store.db, {
|
|
3675
|
+
attemptId,
|
|
3676
|
+
projectRef,
|
|
3677
|
+
resolvedProjectIdentity: target.projectIdentity
|
|
3678
|
+
}), (draft) => detailResponse(store, draft)));
|
|
3679
|
+
} finally {
|
|
3680
|
+
releaseMutation();
|
|
3681
|
+
}
|
|
3228
3682
|
} catch (error) {
|
|
3229
3683
|
const code = apiErrorCode(error);
|
|
3230
3684
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3252,13 +3706,19 @@ function teamSetupRoutes(options) {
|
|
|
3252
3706
|
}
|
|
3253
3707
|
if (groups.filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef).length === 0) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3254
3708
|
try {
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3709
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3710
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3711
|
+
try {
|
|
3712
|
+
return c.json(projectMutationAtomically(store, () => {
|
|
3713
|
+
if (getLegacyTeamSetupDraft(store.db, candidateRef)?.state === "completed" || hasCompletedLegacyTeamSetup(store, candidateRef)) throw new Error("team_setup_confirmation_stale");
|
|
3714
|
+
return refreshLegacyTeamCandidate(store.db, {
|
|
3715
|
+
projection: projectionOptions(store),
|
|
3716
|
+
groups
|
|
3717
|
+
}, candidateRef);
|
|
3718
|
+
}, (draft) => detailResponse(store, draft)));
|
|
3719
|
+
} finally {
|
|
3720
|
+
releaseMutation();
|
|
3721
|
+
}
|
|
3262
3722
|
} catch (error) {
|
|
3263
3723
|
const code = apiErrorCode(error);
|
|
3264
3724
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3278,50 +3738,162 @@ function teamSetupRoutes(options) {
|
|
|
3278
3738
|
if (!hasExactKeys(parsed.value, finishKeys)) return Object.keys(parsed.value).every((key) => finishKeys.includes(key)) ? c.json({ error: "team_setup_confirmation_stale" }, 409) : c.json({ error: "team_setup_incomplete" }, 400);
|
|
3279
3739
|
const { attemptId, confirmedAccessDeltaDigest, confirmedViewerAccessDeltaDigest, finishDigest } = parsed.value;
|
|
3280
3740
|
if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId) || typeof finishDigest !== "string" || !FINISH_DIGEST_PATTERN.test(finishDigest) || typeof confirmedAccessDeltaDigest !== "string" || !ACCESS_DELTA_DIGEST_PATTERN.test(confirmedAccessDeltaDigest) || typeof confirmedViewerAccessDeltaDigest !== "string" || !VIEWER_ACCESS_DELTA_DIGEST_PATTERN.test(confirmedViewerAccessDeltaDigest)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
3741
|
+
let releaseMutation;
|
|
3742
|
+
let releasePublicationMutation;
|
|
3743
|
+
let releaseActorMutations;
|
|
3281
3744
|
try {
|
|
3282
3745
|
const store = options.getStore();
|
|
3283
3746
|
const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3284
3747
|
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3285
3748
|
if (draft.state !== "completed" && hasCompletedLegacyTeamSetup(store, candidateRef)) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3286
3749
|
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3287
|
-
|
|
3288
|
-
|
|
3750
|
+
const replay = replayLegacyTeamSetupActivation(store.db, {
|
|
3751
|
+
candidateRef,
|
|
3752
|
+
attemptId,
|
|
3753
|
+
finishDigest,
|
|
3754
|
+
confirmedAccessDeltaDigest
|
|
3755
|
+
});
|
|
3756
|
+
if (replay) return c.json(finishResponse(candidateRef, replay));
|
|
3757
|
+
if (draft.state === "completed") return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3758
|
+
const preview = previewLegacyTeamSetupActivation(store.db, {
|
|
3759
|
+
candidateRef,
|
|
3760
|
+
attemptId: draft.attemptId
|
|
3761
|
+
});
|
|
3762
|
+
requireLegacyTeamSetupAccessDeltaWithinLimit(preview.accessDelta);
|
|
3763
|
+
const safeDraft = viewerSafeDraft(store, draft);
|
|
3764
|
+
if (viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, preview.accessDelta, {
|
|
3765
|
+
teamDisplayName: draft.displayName,
|
|
3766
|
+
...safeDraft
|
|
3767
|
+
})) !== confirmedViewerAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3768
|
+
const matchingGroups = (await loadedCandidateSnapshots(candidateRef)).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
|
|
3769
|
+
if (matchingGroups.length !== 1) throw safeCoordinatorError();
|
|
3770
|
+
const group = matchingGroups[0];
|
|
3771
|
+
if (!group) throw safeCoordinatorError();
|
|
3772
|
+
releaseMutation = claimCandidateMutation(store.db, candidateRef) ?? void 0;
|
|
3773
|
+
if (!releaseMutation) {
|
|
3774
|
+
await candidateMutationSettled(store.db, candidateRef);
|
|
3775
|
+
const overlappingReplay = replayLegacyTeamSetupActivation(store.db, {
|
|
3289
3776
|
candidateRef,
|
|
3290
|
-
attemptId
|
|
3777
|
+
attemptId,
|
|
3778
|
+
finishDigest,
|
|
3779
|
+
confirmedAccessDeltaDigest
|
|
3291
3780
|
});
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
if (viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, preview.accessDelta, {
|
|
3295
|
-
teamDisplayName: draft.displayName,
|
|
3296
|
-
...safeDraft
|
|
3297
|
-
})) !== confirmedViewerAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3781
|
+
if (overlappingReplay) return c.json(finishResponse(candidateRef, overlappingReplay));
|
|
3782
|
+
return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3298
3783
|
}
|
|
3299
|
-
const
|
|
3784
|
+
const claimedReplay = replayLegacyTeamSetupActivation(store.db, {
|
|
3300
3785
|
candidateRef,
|
|
3301
3786
|
attemptId,
|
|
3302
3787
|
finishDigest,
|
|
3303
|
-
confirmedAccessDeltaDigest
|
|
3304
|
-
loadFreshRoster: async () => {
|
|
3305
|
-
const matching = (await loadedCandidateSnapshots(candidateRef)).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
|
|
3306
|
-
if (matching.length !== 1) throw safeCoordinatorError();
|
|
3307
|
-
return matching[0]?.devices ?? [];
|
|
3308
|
-
},
|
|
3309
|
-
loadProjectInventory: () => legacyTeamCandidateProjectInventory(store.db, projectionOptions(store), candidateRef),
|
|
3310
|
-
validateLockedPreview: (lockedPreview) => {
|
|
3311
|
-
requireBoundedAccessDelta(lockedPreview.accessDelta);
|
|
3312
|
-
const lockedDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3313
|
-
if (!lockedDraft || lockedDraft.attemptId !== attemptId) return false;
|
|
3314
|
-
const safeDraft = viewerSafeDraft(store, lockedDraft);
|
|
3315
|
-
return viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, lockedPreview.accessDelta, {
|
|
3316
|
-
teamDisplayName: lockedDraft.displayName,
|
|
3317
|
-
...safeDraft
|
|
3318
|
-
})) === confirmedViewerAccessDeltaDigest;
|
|
3319
|
-
}
|
|
3788
|
+
confirmedAccessDeltaDigest
|
|
3320
3789
|
});
|
|
3321
|
-
return c.json(finishResponse(candidateRef,
|
|
3790
|
+
if (claimedReplay) return c.json(finishResponse(candidateRef, claimedReplay));
|
|
3791
|
+
const claimedDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3792
|
+
if (!claimedDraft || claimedDraft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3793
|
+
const publicationDeadlineMs = Date.now() + COORDINATOR_ROSTER_READ_BUDGET_MS;
|
|
3794
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
3795
|
+
releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, claimedDraft.devices.flatMap((device) => [...device.decision === "included" && device.targetIdentityId ? [device.targetIdentityId] : [], ...device.existingIdentityId ? [device.existingIdentityId] : []]));
|
|
3796
|
+
return await serializeRecipientPolicyTeamMutation(store.db, deterministicPolicyTeamId(candidateRef), () => serializeRecipientPolicyCoordinatorGroupMutation(store.db, group.groupId, async () => {
|
|
3797
|
+
let authorization;
|
|
3798
|
+
try {
|
|
3799
|
+
authorization = currentCompletionAuthorization();
|
|
3800
|
+
} catch (error) {
|
|
3801
|
+
throw new Error(completionApiError(error));
|
|
3802
|
+
}
|
|
3803
|
+
if (!completionDependencies || !authorization) throw new Error("team_setup_completion_unavailable");
|
|
3804
|
+
if (!authorizesGroup(authorization, group)) throw new Error("team_setup_completion_invalid");
|
|
3805
|
+
const { config, remoteUrl } = authorization;
|
|
3806
|
+
const freshGroup = await loadFreshGroupSnapshot(group, remoteUrl, publicationDeadlineMs);
|
|
3807
|
+
if (!freshGroup) throw new Error("team_setup_roster_unavailable");
|
|
3808
|
+
if (!stillAuthorizesGroup(authorization, group)) throw new Error("team_setup_completion_invalid");
|
|
3809
|
+
const projectInventory = legacyTeamCandidateProjectInventory(store.db, projectionOptions(store), candidateRef);
|
|
3810
|
+
const freshPreview = inspectFreshLegacyTeamSetupActivation(store.db, {
|
|
3811
|
+
candidateRef,
|
|
3812
|
+
attemptId,
|
|
3813
|
+
freshRoster: freshGroup.devices,
|
|
3814
|
+
projectInventory
|
|
3815
|
+
});
|
|
3816
|
+
if (freshPreview.finishDigest !== finishDigest || freshPreview.accessDeltaDigest !== confirmedAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3817
|
+
const freshDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3818
|
+
if (!freshDraft || freshDraft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3819
|
+
const safeFreshDraft = viewerSafeDraft(store, freshDraft);
|
|
3820
|
+
if (viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, freshPreview.accessDelta, {
|
|
3821
|
+
teamDisplayName: freshDraft.displayName,
|
|
3822
|
+
...safeFreshDraft
|
|
3823
|
+
})) !== confirmedViewerAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3824
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3825
|
+
const proposedManifest = deriveLegacyTeamSetupCompletionManifest(store.db, {
|
|
3826
|
+
candidateRef,
|
|
3827
|
+
attemptId,
|
|
3828
|
+
completedAt
|
|
3829
|
+
});
|
|
3830
|
+
const timeoutS = Math.max(1, config.syncCoordinatorTimeoutS);
|
|
3831
|
+
const publicationTimeoutS = remainingCoordinatorTimeoutS(timeoutS, publicationDeadlineMs);
|
|
3832
|
+
if (publicationTimeoutS == null) throw new Error("team_setup_completion_unavailable");
|
|
3833
|
+
let canonicalManifest;
|
|
3834
|
+
try {
|
|
3835
|
+
canonicalManifest = validateLegacyTeamSetupCompletionManifest((await completionDependencies.create({
|
|
3836
|
+
groupId: group.groupId,
|
|
3837
|
+
manifest: proposedManifest,
|
|
3838
|
+
remoteUrl,
|
|
3839
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3840
|
+
timeoutS: publicationTimeoutS
|
|
3841
|
+
})).manifest, {
|
|
3842
|
+
coordinatorId: group.coordinatorId,
|
|
3843
|
+
groupId: group.groupId
|
|
3844
|
+
});
|
|
3845
|
+
if (canonicalCoordinatorLegacyTeamCompletionManifestJson(canonicalManifest) !== canonicalCoordinatorLegacyTeamCompletionManifestJson(proposedManifest)) throw new Error("team_setup_completion_conflict");
|
|
3846
|
+
} catch (error) {
|
|
3847
|
+
const code = completionApiError(error);
|
|
3848
|
+
const getCompletion = completionDependencies.get;
|
|
3849
|
+
if (code !== "team_setup_completion_conflict" || !getCompletion) throw new Error(code);
|
|
3850
|
+
try {
|
|
3851
|
+
const existing = await retryTransientCoordinatorRead((attemptTimeoutS) => getCompletion({
|
|
3852
|
+
groupId: group.groupId,
|
|
3853
|
+
candidateRef,
|
|
3854
|
+
remoteUrl,
|
|
3855
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3856
|
+
timeoutS: attemptTimeoutS
|
|
3857
|
+
}), timeoutS, publicationDeadlineMs);
|
|
3858
|
+
if (!existing) throw new Error("team_setup_completion_conflict");
|
|
3859
|
+
canonicalManifest = validateLegacyTeamSetupCompletionManifest(existing, {
|
|
3860
|
+
coordinatorId: group.coordinatorId,
|
|
3861
|
+
groupId: group.groupId
|
|
3862
|
+
});
|
|
3863
|
+
if (canonicalManifest.finish_digest !== proposedManifest.finish_digest) throw new Error("team_setup_completion_conflict");
|
|
3864
|
+
} catch (recoveryError) {
|
|
3865
|
+
throw new Error(completionApiError(recoveryError));
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
let postPublicationGroup;
|
|
3869
|
+
try {
|
|
3870
|
+
postPublicationGroup = await loadFreshGroupSnapshot(group, remoteUrl, publicationDeadlineMs);
|
|
3871
|
+
} catch (error) {
|
|
3872
|
+
if (legacyTeamSetupApiErrorCode(error) === "team_setup_completion_invalid") throw new Error("team_setup_roster_unavailable");
|
|
3873
|
+
throw error;
|
|
3874
|
+
}
|
|
3875
|
+
if (!postPublicationGroup) throw new Error("team_setup_roster_unavailable");
|
|
3876
|
+
const result = await applyLegacyTeamSetupCompletionManifestAndReturnActivation(store.db, {
|
|
3877
|
+
coordinatorId: group.coordinatorId,
|
|
3878
|
+
groupId: group.groupId,
|
|
3879
|
+
freshRoster: postPublicationGroup.devices,
|
|
3880
|
+
manifest: canonicalManifest,
|
|
3881
|
+
expectedDraftManifest: proposedManifest,
|
|
3882
|
+
recipientPolicyLocksHeld: {
|
|
3883
|
+
publication: true,
|
|
3884
|
+
team: true,
|
|
3885
|
+
coordinatorGroup: true
|
|
3886
|
+
}
|
|
3887
|
+
});
|
|
3888
|
+
return c.json(finishResponse(candidateRef, result));
|
|
3889
|
+
}));
|
|
3322
3890
|
} catch (error) {
|
|
3323
3891
|
const code = apiErrorCode(error);
|
|
3324
3892
|
return c.json({ error: code }, errorStatus$1(code));
|
|
3893
|
+
} finally {
|
|
3894
|
+
releaseActorMutations?.();
|
|
3895
|
+
releasePublicationMutation?.();
|
|
3896
|
+
releaseMutation?.();
|
|
3325
3897
|
}
|
|
3326
3898
|
});
|
|
3327
3899
|
return app;
|
|
@@ -3670,7 +4242,7 @@ function projectInviteAcceptanceFailure(error) {
|
|
|
3670
4242
|
}
|
|
3671
4243
|
var PROJECT_INVITE_TTL_HOURS = 168;
|
|
3672
4244
|
var PROJECT_INVITE_OWNER_FALLBACK = "Project owner";
|
|
3673
|
-
var PROJECT_INVITE_BODY_KEYS = new Set([
|
|
4245
|
+
var PROJECT_INVITE_BODY_KEYS = /* @__PURE__ */ new Set([
|
|
3674
4246
|
"teammate_name",
|
|
3675
4247
|
"project_ids",
|
|
3676
4248
|
"reviewed_project_set_digest"
|
|
@@ -3695,7 +4267,7 @@ function projectInviteStringList(body, key) {
|
|
|
3695
4267
|
return items;
|
|
3696
4268
|
}
|
|
3697
4269
|
function assertProjectInviteBody(body, creating) {
|
|
3698
|
-
const allowed = creating ? PROJECT_INVITE_BODY_KEYS : new Set(["teammate_name", "project_ids"]);
|
|
4270
|
+
const allowed = creating ? PROJECT_INVITE_BODY_KEYS : /* @__PURE__ */ new Set(["teammate_name", "project_ids"]);
|
|
3699
4271
|
if (Object.keys(body).some((key) => !allowed.has(key))) throw new Error("unexpected_project_invite_fields");
|
|
3700
4272
|
if (typeof body.teammate_name !== "string") throw new Error("teammate_name_must_be_string");
|
|
3701
4273
|
if (creating && typeof body.reviewed_project_set_digest !== "string") throw new Error("reviewed_project_set_digest_required");
|
|
@@ -3807,7 +4379,7 @@ function recipientInviteOnboardingPreviewFromReviewedIntent(store, body, invitat
|
|
|
3807
4379
|
...binding
|
|
3808
4380
|
});
|
|
3809
4381
|
}
|
|
3810
|
-
var SAFE_RECIPIENT_INVITE_ERRORS = new Set([
|
|
4382
|
+
var SAFE_RECIPIENT_INVITE_ERRORS = /* @__PURE__ */ new Set([
|
|
3811
4383
|
"device_display_name_invalid",
|
|
3812
4384
|
"device_display_name_required",
|
|
3813
4385
|
"device_display_name_too_long",
|
|
@@ -3841,20 +4413,21 @@ async function peerSupportsSyncRequirements(store, deviceId, requirements) {
|
|
|
3841
4413
|
const addresses = safeJsonList(row?.addresses_json ?? null);
|
|
3842
4414
|
for (const address of addresses) try {
|
|
3843
4415
|
const url = `${buildBaseUrl(address)}/v1/status`;
|
|
4416
|
+
const headers = {
|
|
4417
|
+
...buildDirectPeerAuthHeaders({
|
|
4418
|
+
deviceId: localDeviceId,
|
|
4419
|
+
recipientId: deviceId,
|
|
4420
|
+
dbPath: store.dbPath,
|
|
4421
|
+
method: "GET",
|
|
4422
|
+
url,
|
|
4423
|
+
bodyBytes: Buffer.alloc(0),
|
|
4424
|
+
keysDir: syncKeysDir()
|
|
4425
|
+
}),
|
|
4426
|
+
[SYNC_CAPABILITY_HEADER]: LOCAL_SYNC_CAPABILITY,
|
|
4427
|
+
[SYNC_FEATURES_HEADER]: LOCAL_SYNC_FEATURES.join(",")
|
|
4428
|
+
};
|
|
3844
4429
|
const [status, payload] = await requestJson("GET", url, {
|
|
3845
|
-
headers
|
|
3846
|
-
...buildDirectPeerAuthHeaders({
|
|
3847
|
-
deviceId: localDeviceId,
|
|
3848
|
-
recipientId: deviceId,
|
|
3849
|
-
dbPath: store.dbPath,
|
|
3850
|
-
method: "GET",
|
|
3851
|
-
url,
|
|
3852
|
-
bodyBytes: Buffer.alloc(0),
|
|
3853
|
-
keysDir: syncKeysDir()
|
|
3854
|
-
}),
|
|
3855
|
-
[SYNC_CAPABILITY_HEADER]: LOCAL_SYNC_CAPABILITY,
|
|
3856
|
-
[SYNC_FEATURES_HEADER]: LOCAL_SYNC_FEATURES.join(",")
|
|
3857
|
-
},
|
|
4430
|
+
headers,
|
|
3858
4431
|
timeoutS: 5
|
|
3859
4432
|
});
|
|
3860
4433
|
if (status < 200 || status >= 300) continue;
|
|
@@ -4226,9 +4799,9 @@ var AUTOMATIC_SHARE_OPERATION_STATES = [
|
|
|
4226
4799
|
"initial_sync",
|
|
4227
4800
|
"waiting_for_device"
|
|
4228
4801
|
];
|
|
4229
|
-
var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS =
|
|
4230
|
-
var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS =
|
|
4231
|
-
var TERMINAL_SHARE_MAINTENANCE_ERRORS = new Set([
|
|
4802
|
+
var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 3e4;
|
|
4803
|
+
var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 3e5;
|
|
4804
|
+
var TERMINAL_SHARE_MAINTENANCE_ERRORS = /* @__PURE__ */ new Set([
|
|
4232
4805
|
"coordinator_not_configured",
|
|
4233
4806
|
"team_sharing_not_configured",
|
|
4234
4807
|
"team_selection_ambiguous",
|
|
@@ -4242,7 +4815,7 @@ var TERMINAL_SHARE_MAINTENANCE_ERRORS = new Set([
|
|
|
4242
4815
|
"intent_conflict",
|
|
4243
4816
|
"inviter_identity_conflict"
|
|
4244
4817
|
]);
|
|
4245
|
-
var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = new Set([
|
|
4818
|
+
var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = /* @__PURE__ */ new Set([
|
|
4246
4819
|
"operation_scope_mismatch",
|
|
4247
4820
|
"operation_state_invalid",
|
|
4248
4821
|
"operation_acceptance_invalid",
|
|
@@ -4257,7 +4830,7 @@ var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = new Set([
|
|
|
4257
4830
|
"intent_conflict",
|
|
4258
4831
|
"inviter_identity_conflict"
|
|
4259
4832
|
]);
|
|
4260
|
-
var TERMINAL_RECONCILIATION_ERRORS = new Set([
|
|
4833
|
+
var TERMINAL_RECONCILIATION_ERRORS = /* @__PURE__ */ new Set([
|
|
4261
4834
|
"coordinator_not_configured",
|
|
4262
4835
|
"team_sharing_not_configured",
|
|
4263
4836
|
"team_selection_ambiguous",
|
|
@@ -4451,7 +5024,7 @@ function coordinatorEnrollmentFailureCode(error) {
|
|
|
4451
5024
|
const message = (error instanceof Error ? error.message : String(error)).trim();
|
|
4452
5025
|
const httpStatus = message.match(/\((\d{3})(?::|\))/u)?.[1];
|
|
4453
5026
|
if (httpStatus) return `http_${httpStatus}`;
|
|
4454
|
-
if (new Set([
|
|
5027
|
+
if ((/* @__PURE__ */ new Set([
|
|
4455
5028
|
"coordinator_consumed_team_invite_invalid",
|
|
4456
5029
|
"coordinator_device_list_malformed",
|
|
4457
5030
|
"coordinator_group_id_invalid",
|
|
@@ -4459,7 +5032,7 @@ function coordinatorEnrollmentFailureCode(error) {
|
|
|
4459
5032
|
"coordinator_invite_group_mismatch",
|
|
4460
5033
|
"coordinator_invite_list_malformed",
|
|
4461
5034
|
"reconciliation_time_invalid"
|
|
4462
|
-
]).has(message)) return message;
|
|
5035
|
+
])).has(message)) return message;
|
|
4463
5036
|
if (/timeout|timed out|abort/iu.test(message)) return "request_timeout";
|
|
4464
5037
|
if (/fetch failed|network|econn|enotfound/iu.test(message)) return "network_error";
|
|
4465
5038
|
return "unexpected_error";
|
|
@@ -4517,7 +5090,7 @@ async function reconcileConfiguredCoordinatorEnrollment(store, options = {}) {
|
|
|
4517
5090
|
continue;
|
|
4518
5091
|
}
|
|
4519
5092
|
try {
|
|
4520
|
-
const result = reconcileSnapshot({
|
|
5093
|
+
const result = await reconcileSnapshot({
|
|
4521
5094
|
coordinatorId: buildBaseUrl(remoteUrl),
|
|
4522
5095
|
groupId,
|
|
4523
5096
|
enrollments: enrollmentsResult.value,
|
|
@@ -4540,7 +5113,7 @@ async function reconcileConfiguredCoordinatorEnrollment(store, options = {}) {
|
|
|
4540
5113
|
var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
|
|
4541
5114
|
var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
|
|
4542
5115
|
var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
|
|
4543
|
-
var RECIPIENT_POLICY_WAITING_ERRORS = new Set([
|
|
5116
|
+
var RECIPIENT_POLICY_WAITING_ERRORS = /* @__PURE__ */ new Set([
|
|
4544
5117
|
"recipient_policy_capability_undetermined",
|
|
4545
5118
|
"recipient_policy_parity_incomplete",
|
|
4546
5119
|
"recipient_policy_snapshot_not_fresh"
|
|
@@ -5709,7 +6282,7 @@ function mapSyncAttemptRow(row, showDiag, addresses) {
|
|
|
5709
6282
|
address: showDiag && addresses?.length ? addresses[0] : null
|
|
5710
6283
|
};
|
|
5711
6284
|
}
|
|
5712
|
-
var SYNC_SCOPE_REJECTION_WINDOW_SECONDS =
|
|
6285
|
+
var SYNC_SCOPE_REJECTION_WINDOW_SECONDS = 86400;
|
|
5713
6286
|
var CLEANUP_DIAGNOSTICS_MAX_ROWS = 250;
|
|
5714
6287
|
function recentScopeRejectionsByPeer(store) {
|
|
5715
6288
|
const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_SCOPE_REJECTION_WINDOW_SECONDS * 1e3)).toISOString();
|
|
@@ -6017,7 +6590,7 @@ function reassignLegacySharedReviewGroup(store, input) {
|
|
|
6017
6590
|
legacy_shared_review: legacySharedReviewSummary(store)
|
|
6018
6591
|
};
|
|
6019
6592
|
}
|
|
6020
|
-
var SYNC_DIRECTION_WINDOW_SECONDS =
|
|
6593
|
+
var SYNC_DIRECTION_WINDOW_SECONDS = 86400;
|
|
6021
6594
|
function recentPeerOps(store) {
|
|
6022
6595
|
const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_DIRECTION_WINDOW_SECONDS * 1e3)).toISOString();
|
|
6023
6596
|
const rows = store.db.prepare(`SELECT peer_device_id,
|
|
@@ -6112,7 +6685,8 @@ var PEERS_QUERY = `
|
|
|
6112
6685
|
* scope contents.
|
|
6113
6686
|
*/
|
|
6114
6687
|
function negotiatedSyncCapability(c) {
|
|
6115
|
-
|
|
6688
|
+
const header = c.req.header(SYNC_CAPABILITY_HEADER) ?? null;
|
|
6689
|
+
return negotiateSyncCapability(LOCAL_SYNC_CAPABILITY, normalizeSyncCapability(header));
|
|
6116
6690
|
}
|
|
6117
6691
|
/**
|
|
6118
6692
|
* Peer-to-peer sync protocol routes (/v1/*).
|
|
@@ -6173,7 +6747,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
6173
6747
|
});
|
|
6174
6748
|
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
6175
6749
|
const syncReset = getSyncResetState(store.db);
|
|
6176
|
-
const
|
|
6750
|
+
const negotiated = negotiatedSyncCapability(c);
|
|
6751
|
+
const authorizedScopes = isScopedSyncCapability(negotiated) ? listAuthorizedScopesForPeer(store.db, {
|
|
6177
6752
|
localDeviceId: deviceId,
|
|
6178
6753
|
peerDeviceId: auth.deviceId
|
|
6179
6754
|
}) : null;
|
|
@@ -6560,7 +7135,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6560
7135
|
const bindings = body.bindings.map((value) => {
|
|
6561
7136
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
6562
7137
|
const binding = value;
|
|
6563
|
-
const bindingKeys = new Set([
|
|
7138
|
+
const bindingKeys = /* @__PURE__ */ new Set([
|
|
6564
7139
|
"deviceId",
|
|
6565
7140
|
"targetIdentityId",
|
|
6566
7141
|
"confirmed",
|
|
@@ -6604,7 +7179,9 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6604
7179
|
const body = parseDeviceBindingRequest(await parseViewerJsonBody(c), true);
|
|
6605
7180
|
if (!body || !("reviewedInventoryDigest" in body)) return c.json({ error: "binding_request_invalid" }, 400);
|
|
6606
7181
|
const store = getStore();
|
|
7182
|
+
let releasePublicationMutation;
|
|
6607
7183
|
try {
|
|
7184
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
6608
7185
|
ensureStableStoreIdentity(store);
|
|
6609
7186
|
const result = commitDeviceIdentityBindings(store.db, {
|
|
6610
7187
|
localActorId: store.actorId,
|
|
@@ -6620,6 +7197,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6620
7197
|
return c.json({ error: "binding_commit_busy" }, 503);
|
|
6621
7198
|
}
|
|
6622
7199
|
return c.json({ error: "binding_commit_failed" }, 500);
|
|
7200
|
+
} finally {
|
|
7201
|
+
releasePublicationMutation?.();
|
|
6623
7202
|
}
|
|
6624
7203
|
});
|
|
6625
7204
|
app.get("/api/sync/status", async (c) => {
|
|
@@ -6758,7 +7337,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6758
7337
|
if (addrs.length) peerAddressMap.set(String(peerRow.peer_device_id ?? ""), addrs);
|
|
6759
7338
|
});
|
|
6760
7339
|
const attemptsItems = attemptRows.map((row) => {
|
|
6761
|
-
|
|
7340
|
+
const addrs = showDiag ? peerAddressMap.get(String(row.peer_device_id ?? "")) : void 0;
|
|
7341
|
+
return mapSyncAttemptRow(row, showDiag, addrs);
|
|
6762
7342
|
});
|
|
6763
7343
|
const latestAttemptError = String(attemptRows[0]?.error || "").trim();
|
|
6764
7344
|
const statusBlock = {
|
|
@@ -6837,7 +7417,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6837
7417
|
const limit = queryInt(c.req.query("limit"), 100);
|
|
6838
7418
|
if (limit <= 0) return c.json({ error: "invalid_limit" }, 400);
|
|
6839
7419
|
const sinceMinutesRaw = queryInt(c.req.query("sinceMinutes"), 1440);
|
|
6840
|
-
const sinceMinutes = Math.min(Math.max(sinceMinutesRaw, 0),
|
|
7420
|
+
const sinceMinutes = Math.min(Math.max(sinceMinutesRaw, 0), 5256e3);
|
|
6841
7421
|
const sinceIso = sinceMinutes > 0 ? (/* @__PURE__ */ new Date(Date.now() - sinceMinutes * 6e4)).toISOString() : void 0;
|
|
6842
7422
|
const records = listInboundScopeRejections(store.db, {
|
|
6843
7423
|
peerDeviceId,
|
|
@@ -7064,7 +7644,14 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7064
7644
|
const store = getStore();
|
|
7065
7645
|
const body = await parseViewerJsonBody(c);
|
|
7066
7646
|
try {
|
|
7067
|
-
const
|
|
7647
|
+
const request = parseRecipientPolicyEdgeCommitRequest(body);
|
|
7648
|
+
const teamIds = request ? [...new Set(request.changes.flatMap((change) => change.recipient.recipientKind === "team" ? [change.recipient.teamId] : []))].toSorted() : [];
|
|
7649
|
+
const commit = async (index) => {
|
|
7650
|
+
const teamId = teamIds[index];
|
|
7651
|
+
if (!teamId) return commitRecipientPolicyEdges(store.db, request ?? body);
|
|
7652
|
+
return serializeRecipientPolicyTeamMutation(store.db, teamId, () => commit(index + 1));
|
|
7653
|
+
};
|
|
7654
|
+
const result = await commit(0);
|
|
7068
7655
|
if (result.status === "invalid") return c.json(result, 400);
|
|
7069
7656
|
if (result.status === "not_found") return c.json(result, 404);
|
|
7070
7657
|
if (result.status === "stale" || result.status === "conflict") return c.json(result, 409);
|
|
@@ -7187,7 +7774,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7187
7774
|
const parseReviewRequest = (value) => {
|
|
7188
7775
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
7189
7776
|
const body = value;
|
|
7190
|
-
const allowedKeys = new Set([
|
|
7777
|
+
const allowedKeys = /* @__PURE__ */ new Set([
|
|
7191
7778
|
"reviewItemId",
|
|
7192
7779
|
"sourceFingerprint",
|
|
7193
7780
|
"decision",
|
|
@@ -7203,7 +7790,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7203
7790
|
};
|
|
7204
7791
|
app.post("/api/sync/recipient-policy/v1/review/resolve", async (c) => {
|
|
7205
7792
|
const store = getStore();
|
|
7206
|
-
const
|
|
7793
|
+
const body = await parseViewerJsonBody(c);
|
|
7794
|
+
const request = parseReviewRequest(body);
|
|
7207
7795
|
if (!request) return c.json({ error: "request_invalid" }, 400);
|
|
7208
7796
|
ensureLocalActorRecord(store);
|
|
7209
7797
|
const result = resolveRecipientPolicyReview(store.db, {
|
|
@@ -7518,9 +8106,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7518
8106
|
const store = getStore();
|
|
7519
8107
|
const body = await parseViewerJsonBody(c);
|
|
7520
8108
|
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
8109
|
+
let releasePublicationMutation;
|
|
7521
8110
|
try {
|
|
7522
8111
|
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
7523
8112
|
const mappingInput = parseViewerProjectMappingInput(body);
|
|
8113
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
7524
8114
|
const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput);
|
|
7525
8115
|
if (analysis.requested_workspace_identity?.startsWith("unmapped:")) return c.json({
|
|
7526
8116
|
error: "unmapped_project_local_only",
|
|
@@ -7550,12 +8140,15 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7550
8140
|
});
|
|
7551
8141
|
} catch (error) {
|
|
7552
8142
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
8143
|
+
} finally {
|
|
8144
|
+
releasePublicationMutation?.();
|
|
7553
8145
|
}
|
|
7554
8146
|
});
|
|
7555
8147
|
app.put("/api/sync/sharing-domains/project-mappings/bulk", async (c) => {
|
|
7556
8148
|
const store = getStore();
|
|
7557
8149
|
const body = await parseViewerJsonBody(c);
|
|
7558
8150
|
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
8151
|
+
let releasePublicationMutation;
|
|
7559
8152
|
try {
|
|
7560
8153
|
const rawMappings = body.mappings;
|
|
7561
8154
|
if (!Array.isArray(rawMappings) || rawMappings.length === 0) return c.json({ error: "mappings must be a non-empty array" }, 400);
|
|
@@ -7563,6 +8156,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7563
8156
|
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
|
|
7564
8157
|
return parseViewerProjectMappingInput(raw);
|
|
7565
8158
|
});
|
|
8159
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
7566
8160
|
const analyses = mappingInputs.map((mappingInput) => analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput));
|
|
7567
8161
|
const unmapped = analyses.find((analysis) => analysis.requested_workspace_identity?.startsWith("unmapped:"));
|
|
7568
8162
|
if (unmapped) return c.json({
|
|
@@ -7593,12 +8187,16 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7593
8187
|
});
|
|
7594
8188
|
} catch (error) {
|
|
7595
8189
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
8190
|
+
} finally {
|
|
8191
|
+
releasePublicationMutation?.();
|
|
7596
8192
|
}
|
|
7597
8193
|
});
|
|
7598
|
-
app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => {
|
|
8194
|
+
app.delete("/api/sync/sharing-domains/project-mappings/:id", async (c) => {
|
|
7599
8195
|
const store = getStore();
|
|
7600
8196
|
const id = Number(c.req.param("id"));
|
|
8197
|
+
let releasePublicationMutation;
|
|
7601
8198
|
try {
|
|
8199
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
7602
8200
|
const deleted = deleteProjectScopeSettingsMapping(store.db, id);
|
|
7603
8201
|
return c.json({
|
|
7604
8202
|
ok: true,
|
|
@@ -7606,6 +8204,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7606
8204
|
});
|
|
7607
8205
|
} catch (error) {
|
|
7608
8206
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
8207
|
+
} finally {
|
|
8208
|
+
releasePublicationMutation?.();
|
|
7609
8209
|
}
|
|
7610
8210
|
});
|
|
7611
8211
|
app.post("/api/sync/peers/identity", async (c) => {
|
|
@@ -7734,6 +8334,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7734
8334
|
if (!secondaryActorId) return c.json({ error: "secondary_actor_id required" }, 400);
|
|
7735
8335
|
if (primaryActorId === secondaryActorId) return c.json({ error: "actor ids must differ" }, 400);
|
|
7736
8336
|
const d = drizzle(store.db, { schema });
|
|
8337
|
+
const releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, [primaryActorId, secondaryActorId]);
|
|
8338
|
+
queueMicrotask(releaseActorMutations);
|
|
7737
8339
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7738
8340
|
const result = store.db.transaction(() => {
|
|
7739
8341
|
const primary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, primaryActorId)).get();
|
|
@@ -7884,6 +8486,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7884
8486
|
const actorId = String(body.actor_id ?? "").trim();
|
|
7885
8487
|
if (!actorId) return c.json({ error: "actor_id required" }, 400);
|
|
7886
8488
|
if (actorId === store.actorId) return c.json({ error: "cannot deactivate this device's own local actor" }, 409);
|
|
8489
|
+
const releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, [actorId]);
|
|
8490
|
+
queueMicrotask(releaseActorMutations);
|
|
7887
8491
|
const d = drizzle(store.db, { schema });
|
|
7888
8492
|
const actor = d.select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
|
|
7889
8493
|
if (!actor) return c.json({ error: "actor not found" }, 404);
|
|
@@ -9114,7 +9718,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9114
9718
|
const publicKey = String(body.peer_public_key ?? "").trim();
|
|
9115
9719
|
const name = String(body.name ?? "").trim() || null;
|
|
9116
9720
|
const requestedFingerprint = String(body.fingerprint ?? "").trim();
|
|
9117
|
-
const
|
|
9721
|
+
const addressesInput = Array.isArray(body.peer_addresses) ? body.peer_addresses : [];
|
|
9722
|
+
const addresses = mergeAddresses([], addressesInput.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0));
|
|
9118
9723
|
if (!peerDeviceId || !publicKey) return c.json({ error: "peer_device_id_and_public_key_required" }, 400);
|
|
9119
9724
|
const fingerprint = requestedFingerprint || fingerprintPublicKey(publicKey);
|
|
9120
9725
|
if (fingerprintPublicKey(publicKey) !== fingerprint) return c.json({ error: "fingerprint_mismatch" }, 400);
|
|
@@ -9203,7 +9808,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9203
9808
|
error: "coordinator_not_configured",
|
|
9204
9809
|
status
|
|
9205
9810
|
}, 400);
|
|
9206
|
-
const
|
|
9811
|
+
const store = getStore();
|
|
9812
|
+
const existing = getCoordinatorGroupPreference(store.db, coordinatorId, groupId);
|
|
9207
9813
|
if (existing) return c.json({
|
|
9208
9814
|
preferences: existing,
|
|
9209
9815
|
status
|
|
@@ -9389,12 +9995,13 @@ function updateStatusRoutes(options = {}) {
|
|
|
9389
9995
|
const app = new Hono();
|
|
9390
9996
|
app.get("/api/update-status", async (c) => {
|
|
9391
9997
|
try {
|
|
9998
|
+
const installKind = detectInstallKind({
|
|
9999
|
+
entryPath: process.argv[1] ?? "",
|
|
10000
|
+
env: process.env
|
|
10001
|
+
});
|
|
9392
10002
|
const status = await getUpdateStatus$1({
|
|
9393
10003
|
currentVersion: VERSION$1,
|
|
9394
|
-
installKind
|
|
9395
|
-
entryPath: process.argv[1] ?? "",
|
|
9396
|
-
env: process.env
|
|
9397
|
-
})
|
|
10004
|
+
installKind
|
|
9398
10005
|
});
|
|
9399
10006
|
return c.json(status);
|
|
9400
10007
|
} catch {
|
|
@@ -9455,6 +10062,8 @@ function createApp(opts) {
|
|
|
9455
10062
|
app.route("/", teamSetupRoutes({
|
|
9456
10063
|
getStore: storeFactory,
|
|
9457
10064
|
loadLegacyTeamConfiguredGroupSnapshots: opts?.loadLegacyTeamConfiguredGroupSnapshots,
|
|
10065
|
+
completionDependencies: opts?.teamSetupCompletionDependencies,
|
|
10066
|
+
readCoordinatorConfig: opts?.readCoordinatorConfig,
|
|
9458
10067
|
registerSummaryInvalidator: (invalidate) => {
|
|
9459
10068
|
invalidateTeamSetupSummary = invalidate;
|
|
9460
10069
|
}
|