@codemem/server 0.43.2 → 0.44.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +914 -137
- 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-view.d.ts +2 -1
- package/dist/routes/team-setup-view.d.ts.map +1 -1
- package/dist/routes/team-setup.d.ts +31 -2
- 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 +3 -3
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, LegacyTeamSetupRosterCapacityError, MemoryStore, PROMPT_TRANSPORT_PROTOCOL_RANGE, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, 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();
|
|
@@ -2853,10 +2922,10 @@ function projectMappingChoices(store) {
|
|
|
2853
2922
|
excludePeerReceived: true
|
|
2854
2923
|
}).filter((candidate) => !candidate.read_only && isLegacyTeamSetupProjectMappingIdentity(candidate.workspace_identity, store.db));
|
|
2855
2924
|
} catch (error) {
|
|
2856
|
-
if (error instanceof Error && (error.message === "project_scope_candidate_scan_too_large" || error.message === "project_scope_candidate_metadata_too_large")) throw new
|
|
2925
|
+
if (error instanceof Error && (error.message === "project_scope_candidate_scan_too_large" || error.message === "project_scope_candidate_metadata_too_large")) throw new LocalProjectScanBudgetError(error);
|
|
2857
2926
|
throw error;
|
|
2858
2927
|
}
|
|
2859
|
-
if (candidates.length > MAX_PROJECT_MAPPING_CHOICES) throw new
|
|
2928
|
+
if (candidates.length > MAX_PROJECT_MAPPING_CHOICES) throw new LocalProjectScanBudgetError();
|
|
2860
2929
|
return candidates.map((candidate) => ({
|
|
2861
2930
|
projectIdentity: candidate.workspace_identity,
|
|
2862
2931
|
sourceDisplayName: candidate.display_project
|
|
@@ -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,160 @@ 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
|
+
}
|
|
3004
|
+
var TeamSetupFinishError = class extends Error {
|
|
3005
|
+
reason;
|
|
3006
|
+
alreadyLogged;
|
|
3007
|
+
constructor(code, reason, alreadyLogged, cause) {
|
|
3008
|
+
super(code, cause === void 0 ? void 0 : { cause });
|
|
3009
|
+
this.reason = reason;
|
|
3010
|
+
this.alreadyLogged = alreadyLogged;
|
|
3011
|
+
this.name = "TeamSetupFinishError";
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
/**
|
|
3015
|
+
* The generic `legacy_team_setup_roster_too_large` string is thrown by ten
|
|
3016
|
+
* sites for unrelated limits (coordinator roster size, draft device count,
|
|
3017
|
+
* local project choices). Only the local project-candidate scan is a
|
|
3018
|
+
* "this device's history is too large" condition, so it carries a distinct
|
|
3019
|
+
* type. The message is kept identical so the frozen API-code mapping and every
|
|
3020
|
+
* existing caller that matches on it are unchanged.
|
|
3021
|
+
*/
|
|
3022
|
+
var LocalProjectScanBudgetError = class extends Error {
|
|
3023
|
+
constructor(cause) {
|
|
3024
|
+
super("legacy_team_setup_roster_too_large", cause === void 0 ? void 0 : { cause });
|
|
3025
|
+
this.name = "LocalProjectScanBudgetError";
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
/**
|
|
3029
|
+
* Remote error codes the finish path can legitimately receive from the
|
|
3030
|
+
* coordinator. Anything else — including identifier-shaped values, which a
|
|
3031
|
+
* hostile or misconfigured upstream could use to smuggle a credential through
|
|
3032
|
+
* a JSON `error` field — is reduced to a fixed classification before logging.
|
|
3033
|
+
* A shape check is not an allowlist; only this set is echoed verbatim.
|
|
3034
|
+
*/
|
|
3035
|
+
var LOGGABLE_REMOTE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3036
|
+
"body_too_large",
|
|
3037
|
+
"completion_manifest_invalid",
|
|
3038
|
+
"completion_manifest_unavailable",
|
|
3039
|
+
"completion_not_found",
|
|
3040
|
+
"completion_query_invalid",
|
|
3041
|
+
"group_archived",
|
|
3042
|
+
"group_id_and_candidate_ref_required",
|
|
3043
|
+
"group_not_found",
|
|
3044
|
+
"invalid_json",
|
|
3045
|
+
"missing_admin_header",
|
|
3046
|
+
"not_found",
|
|
3047
|
+
"rate_limited",
|
|
3048
|
+
"response_too_large",
|
|
3049
|
+
"unauthorized"
|
|
3050
|
+
]);
|
|
3051
|
+
function loggableRemoteCode(code) {
|
|
3052
|
+
if (LOGGABLE_REMOTE_ERROR_CODES.has(code)) return code;
|
|
3053
|
+
if (code.startsWith("non_json_response")) return "non_json_response";
|
|
3054
|
+
return "unclassified_remote_error";
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Local causes the finish path can produce. Same allowlist discipline as
|
|
3058
|
+
* remote codes: everything else, including transport messages that embed the
|
|
3059
|
+
* target URL, is classified rather than echoed.
|
|
3060
|
+
*/
|
|
3061
|
+
var LOGGABLE_LOCAL_CAUSES = /* @__PURE__ */ new Set([
|
|
3062
|
+
"completion_conflict",
|
|
3063
|
+
"legacy_team_setup_roster_too_large",
|
|
3064
|
+
"project_scope_candidate_metadata_too_large",
|
|
3065
|
+
"project_scope_candidate_scan_too_large",
|
|
3066
|
+
"team_setup_completion_conflict",
|
|
3067
|
+
"team_setup_completion_invalid",
|
|
3068
|
+
"team_setup_completion_unavailable",
|
|
3069
|
+
"team_setup_confirmation_stale",
|
|
3070
|
+
"team_setup_roster_unavailable"
|
|
3071
|
+
]);
|
|
3072
|
+
function loggableLocalCause(error) {
|
|
3073
|
+
if (LOGGABLE_LOCAL_CAUSES.has(error.message)) return error.message;
|
|
3074
|
+
if (error.name === "AbortError" || error.name === "TimeoutError") return error.name;
|
|
3075
|
+
if (/fetch failed/iu.test(error.message)) return "fetch_failed";
|
|
3076
|
+
if (/timed out|timeout/iu.test(error.message)) return "timed_out";
|
|
3077
|
+
return `unclassified_error:${error.name || "Error"}`;
|
|
3078
|
+
}
|
|
3079
|
+
function errorChain(error) {
|
|
3080
|
+
const chain = [];
|
|
3081
|
+
let current = error;
|
|
3082
|
+
while (current && !chain.includes(current)) {
|
|
3083
|
+
chain.push(current);
|
|
3084
|
+
current = current instanceof Error ? current.cause : void 0;
|
|
3085
|
+
}
|
|
3086
|
+
return chain;
|
|
3087
|
+
}
|
|
3088
|
+
function remoteFailureReason(remote, fallback) {
|
|
3089
|
+
if (remote.status === 404 && (remote.code === "not_found" || remote.code.startsWith("non_json"))) return "coordinator_route_missing";
|
|
3090
|
+
if (remote.status === 400 && remote.code === "completion_manifest_invalid") return "coordinator_rejected_manifest";
|
|
3091
|
+
return fallback;
|
|
3092
|
+
}
|
|
3093
|
+
function teamSetupFinishFailureDetails(error) {
|
|
3094
|
+
const chain = errorChain(error);
|
|
3095
|
+
const wrapper = chain.find((item) => item instanceof TeamSetupFinishError);
|
|
3096
|
+
const remote = chain.find((item) => item instanceof RemoteCoordinatorRequestError);
|
|
3097
|
+
if (remote) {
|
|
3098
|
+
const reason = remoteFailureReason(remote, wrapper?.reason);
|
|
3099
|
+
return {
|
|
3100
|
+
...reason ? { reason } : {},
|
|
3101
|
+
cause: "remote coordinator request failed",
|
|
3102
|
+
status: remote.status,
|
|
3103
|
+
code: loggableRemoteCode(remote.code)
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
if (chain.some((item) => item instanceof LegacyTeamSetupRosterCapacityError || item instanceof LocalProjectScanBudgetError)) return {
|
|
3107
|
+
reason: "local_candidate_scan_budget_exceeded",
|
|
3108
|
+
cause: "local project candidate scan exceeded budget"
|
|
3109
|
+
};
|
|
3110
|
+
const errorCause = chain.find((item) => item instanceof Error && !(item instanceof TeamSetupFinishError));
|
|
3111
|
+
const cause = errorCause ? loggableLocalCause(errorCause) : "unclassified_non_error";
|
|
3112
|
+
if (wrapper?.reason) return {
|
|
3113
|
+
reason: wrapper.reason,
|
|
3114
|
+
cause
|
|
3115
|
+
};
|
|
3116
|
+
if (errorCause && (errorCause.name === "AbortError" || errorCause.name === "TimeoutError" || /fetch failed|timed out|timeout/iu.test(errorCause.message))) return {
|
|
3117
|
+
reason: "coordinator_unreachable",
|
|
3118
|
+
cause
|
|
3119
|
+
};
|
|
3120
|
+
return { cause };
|
|
3121
|
+
}
|
|
3122
|
+
function warnTeamSetupFinishFailure(error, details, context) {
|
|
3123
|
+
if (errorChain(error).some((item) => item instanceof TeamSetupFinishError && item.alreadyLogged)) return;
|
|
3124
|
+
const safe = (value) => {
|
|
3125
|
+
let sanitized = String(value).replace(/[\r\n\t]+/gu, " ");
|
|
3126
|
+
for (const secret of context.redactValues ?? []) if (secret) sanitized = sanitized.replaceAll(secret, "[redacted]");
|
|
3127
|
+
return sanitized.replace(/https?:\/\/[^\s]+/giu, (value) => {
|
|
3128
|
+
try {
|
|
3129
|
+
const url = new URL(value);
|
|
3130
|
+
return `${url.protocol}//${url.host}`;
|
|
3131
|
+
} catch {
|
|
3132
|
+
return "[redacted-url]";
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
3135
|
+
};
|
|
3136
|
+
const fields = [
|
|
3137
|
+
`candidateRef=${safe(context.candidateRef)}`,
|
|
3138
|
+
`attemptId=${safe(context.attemptId)}`,
|
|
3139
|
+
`apiCode=${safe(context.apiCode)}`,
|
|
3140
|
+
`cause=${safe(details.cause)}`,
|
|
3141
|
+
...details.reason ? [`reason=${safe(details.reason)}`] : [],
|
|
3142
|
+
...details.status == null ? [] : [`status=${safe(details.status)}`],
|
|
3143
|
+
...details.code == null ? [] : [`code=${safe(details.code)}`]
|
|
3144
|
+
];
|
|
3145
|
+
console.warn(`[team-setup finish] ${fields.join(" ")}`);
|
|
3146
|
+
}
|
|
3147
|
+
function finishErrorResponse(code, details) {
|
|
3148
|
+
const reason = details.reason;
|
|
3149
|
+
return reason ? {
|
|
3150
|
+
error: code,
|
|
3151
|
+
reason
|
|
3152
|
+
} : { error: code };
|
|
3153
|
+
}
|
|
2931
3154
|
async function parseBoundedJsonObject(c) {
|
|
2932
3155
|
if (c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return { ok: false };
|
|
2933
3156
|
const lengthHeader = c.req.header("content-length");
|
|
@@ -2991,7 +3214,7 @@ function detailResponse(store, draft, candidate = candidateFromDraft(draft)) {
|
|
|
2991
3214
|
if (code === "team_setup_projection_changed") unavailableReason = "team_setup_conflict";
|
|
2992
3215
|
else if (code !== "team_setup_incomplete") unavailableReason = code;
|
|
2993
3216
|
}
|
|
2994
|
-
if (preview)
|
|
3217
|
+
if (preview) requireLegacyTeamSetupAccessDeltaWithinLimit(preview.accessDelta);
|
|
2995
3218
|
const safeDraft = viewerSafeDraft(store, draft);
|
|
2996
3219
|
const safePreview = preview ? viewerSafePreview(draft, safeDraft, preview) : null;
|
|
2997
3220
|
return projectLegacyTeamSetupView({
|
|
@@ -3019,9 +3242,12 @@ function finishResponse(candidateRef, result) {
|
|
|
3019
3242
|
function teamSetupRoutes(options) {
|
|
3020
3243
|
const app = new Hono();
|
|
3021
3244
|
const loadSnapshots = options.loadLegacyTeamConfiguredGroupSnapshots ?? ((loadOptions) => loadConfiguredLegacyTeamGroupSnapshotsWith(options.snapshotLoaderDependencies ?? defaultSnapshotLoaderDependencies, loadOptions, options.getStore));
|
|
3022
|
-
async function loadedSnapshots(candidateRef) {
|
|
3245
|
+
async function loadedSnapshots(candidateRef, deadlineMs) {
|
|
3023
3246
|
try {
|
|
3024
|
-
return await loadSnapshots(candidateRef
|
|
3247
|
+
return await loadSnapshots(candidateRef || deadlineMs != null ? {
|
|
3248
|
+
candidateRef,
|
|
3249
|
+
deadlineMs
|
|
3250
|
+
} : void 0);
|
|
3025
3251
|
} catch (error) {
|
|
3026
3252
|
if (error instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
|
|
3027
3253
|
throw safeCoordinatorError();
|
|
@@ -3029,12 +3255,341 @@ function teamSetupRoutes(options) {
|
|
|
3029
3255
|
}
|
|
3030
3256
|
const loadedSummarySnapshots = createCachedSnapshotLoader(() => loadedSnapshots(), SUMMARY_SNAPSHOT_CACHE_TTL_MS);
|
|
3031
3257
|
options.registerSummaryInvalidator?.(() => loadedSummarySnapshots.invalidate());
|
|
3258
|
+
const completionDependencies = options.completionDependencies === void 0 ? defaultCompletionDependencies : options.completionDependencies;
|
|
3259
|
+
/**
|
|
3260
|
+
* Reads the live coordinator configuration and the groups it currently
|
|
3261
|
+
* authorizes. Returns null when completion I/O is not configured or current
|
|
3262
|
+
* authorization cannot be proven.
|
|
3263
|
+
*/
|
|
3264
|
+
function currentCompletionAuthorization() {
|
|
3265
|
+
const config = (options.readCoordinatorConfig ?? options.snapshotLoaderDependencies?.readConfig ?? readCoordinatorSyncConfig)();
|
|
3266
|
+
const remoteUrl = buildBaseUrl(config.syncCoordinatorUrl);
|
|
3267
|
+
if (!remoteUrl || !config.syncCoordinatorAdminSecret) return null;
|
|
3268
|
+
const configuredCoordinatorId = normalizedCoordinatorId(remoteUrl);
|
|
3269
|
+
if (!configuredCoordinatorId) return null;
|
|
3270
|
+
let currentGroupIds;
|
|
3271
|
+
try {
|
|
3272
|
+
currentGroupIds = new Set(configuredGroupIds(config.syncCoordinatorGroups));
|
|
3273
|
+
} catch {
|
|
3274
|
+
return null;
|
|
3275
|
+
}
|
|
3276
|
+
return {
|
|
3277
|
+
config,
|
|
3278
|
+
remoteUrl,
|
|
3279
|
+
configuredCoordinatorId,
|
|
3280
|
+
currentGroupIds
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
function authorizesGroup(authorization, group) {
|
|
3284
|
+
return authorization.currentGroupIds.has(group.groupId) && normalizedCoordinatorId(group.coordinatorId) === authorization.configuredCoordinatorId;
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Configuration may change while completion-list or roster requests are in
|
|
3288
|
+
* flight. Re-prove authorization for the exact coordinator and group right
|
|
3289
|
+
* before applying canonical policy, as the finish publication path does.
|
|
3290
|
+
*/
|
|
3291
|
+
function stillAuthorizesGroup(initial, group) {
|
|
3292
|
+
let current;
|
|
3293
|
+
try {
|
|
3294
|
+
current = currentCompletionAuthorization();
|
|
3295
|
+
} catch {
|
|
3296
|
+
return false;
|
|
3297
|
+
}
|
|
3298
|
+
return current !== null && current.configuredCoordinatorId === initial.configuredCoordinatorId && current.remoteUrl === initial.remoteUrl && current.config.syncCoordinatorAdminSecret === initial.config.syncCoordinatorAdminSecret && authorizesGroup(current, group);
|
|
3299
|
+
}
|
|
3300
|
+
function matchesGroupSnapshot(snapshot, group) {
|
|
3301
|
+
return snapshot.groupId === group.groupId && normalizedCoordinatorId(snapshot.coordinatorId) === normalizedCoordinatorId(group.coordinatorId);
|
|
3302
|
+
}
|
|
3303
|
+
function freshGroupKey(group) {
|
|
3304
|
+
return `${normalizedCoordinatorId(group.coordinatorId) ?? group.coordinatorId}\0${group.groupId}`;
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* The snapshot loader keys candidates by the configured coordinator spelling,
|
|
3308
|
+
* while a persisted group may carry an equivalent but different spelling.
|
|
3309
|
+
* Ask for the roster under each spelling and match on normalized identity,
|
|
3310
|
+
* preserving the persisted candidate identity for everything else.
|
|
3311
|
+
*/
|
|
3312
|
+
async function loadFreshGroupSnapshot(group, remoteUrl, deadlineMs) {
|
|
3313
|
+
const candidateRefs = [.../* @__PURE__ */ new Set([legacyTeamCandidateId(remoteUrl, group.groupId), legacyTeamCandidateId(group.coordinatorId, group.groupId)])];
|
|
3314
|
+
for (const candidateRef of candidateRefs) {
|
|
3315
|
+
const matches = (await loadedSnapshots(candidateRef, deadlineMs)).filter((snapshot) => matchesGroupSnapshot(snapshot, group));
|
|
3316
|
+
if (matches.length > 1) throw new Error("team_setup_completion_invalid");
|
|
3317
|
+
if (matches[0]) return matches[0];
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
function completionRecordCandidateRef(record) {
|
|
3321
|
+
const manifest = record.manifest;
|
|
3322
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return null;
|
|
3323
|
+
const candidateRef = manifest.candidate_ref;
|
|
3324
|
+
return typeof candidateRef === "string" ? candidateRef : null;
|
|
3325
|
+
}
|
|
3326
|
+
function belongsToGroupCandidate(record, group) {
|
|
3327
|
+
const candidateRef = completionRecordCandidateRef(record);
|
|
3328
|
+
return candidateRef === null || candidateRef === legacyTeamCandidateId(group.coordinatorId, group.groupId);
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Applies a canonical manifest with authorization re-proven inside the same
|
|
3332
|
+
* serialized section that guards the apply. Checking before the locks would
|
|
3333
|
+
* let a group removed while reconciliation waited on another policy
|
|
3334
|
+
* mutation still be applied once the queues open.
|
|
3335
|
+
*/
|
|
3336
|
+
async function applyCanonicalManifestIfStillAuthorized(store, authorization, group, manifest, freshRoster) {
|
|
3337
|
+
let outcome = "unauthorized";
|
|
3338
|
+
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 () => {
|
|
3339
|
+
if (!stillAuthorizesGroup(authorization, group)) return;
|
|
3340
|
+
await applyLegacyTeamSetupCompletionManifest(store.db, {
|
|
3341
|
+
coordinatorId: group.coordinatorId,
|
|
3342
|
+
groupId: group.groupId,
|
|
3343
|
+
manifest,
|
|
3344
|
+
freshRoster,
|
|
3345
|
+
recipientPolicyLocksHeld: {
|
|
3346
|
+
publication: true,
|
|
3347
|
+
team: true,
|
|
3348
|
+
coordinatorGroup: true
|
|
3349
|
+
}
|
|
3350
|
+
});
|
|
3351
|
+
outcome = "applied";
|
|
3352
|
+
})))) ? outcome : "claimed";
|
|
3353
|
+
}
|
|
3354
|
+
async function reconcileCompletions(store, groups, reconciliationOptions = {}) {
|
|
3355
|
+
if (!completionDependencies || groups.length === 0) return;
|
|
3356
|
+
let authorization;
|
|
3357
|
+
try {
|
|
3358
|
+
authorization = currentCompletionAuthorization();
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
throw new Error(completionApiError(error));
|
|
3361
|
+
}
|
|
3362
|
+
if (!authorization) return;
|
|
3363
|
+
const { config, remoteUrl } = authorization;
|
|
3364
|
+
const scopedGroups = groups.filter((group) => authorizesGroup(authorization, group));
|
|
3365
|
+
if (scopedGroups.length === 0) return;
|
|
3366
|
+
const timeoutS = Math.max(1, config.syncCoordinatorTimeoutS);
|
|
3367
|
+
try {
|
|
3368
|
+
const reconciliationDeadlineMs = Date.now() + COORDINATOR_ROSTER_READ_BUDGET_MS;
|
|
3369
|
+
const groupById = new Map(scopedGroups.map((group) => [group.groupId, group]));
|
|
3370
|
+
const listCompletionRecords = (groupIds) => retryTransientCoordinatorRead((attemptTimeoutS) => completionDependencies.list({
|
|
3371
|
+
groupIds,
|
|
3372
|
+
remoteUrl,
|
|
3373
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3374
|
+
timeoutS: attemptTimeoutS
|
|
3375
|
+
}), timeoutS, reconciliationDeadlineMs);
|
|
3376
|
+
const scopedGroupIds = [...new Set(scopedGroups.map((group) => group.groupId))];
|
|
3377
|
+
let records;
|
|
3378
|
+
try {
|
|
3379
|
+
records = await listCompletionRecords(scopedGroupIds);
|
|
3380
|
+
} catch (error) {
|
|
3381
|
+
if (isUnsupportedCompletionQuery(error)) records = [];
|
|
3382
|
+
else if (reconciliationOptions.isolateApplyFailures && isUnattributableCompletionBatchError(error)) records = await listCompletionRecordsPerGroup(scopedGroupIds, listCompletionRecords);
|
|
3383
|
+
else throw error;
|
|
3384
|
+
}
|
|
3385
|
+
const freshGroupByKey = /* @__PURE__ */ new Map();
|
|
3386
|
+
if (records.length > 0) try {
|
|
3387
|
+
const onlyGroup = scopedGroups.length === 1 ? scopedGroups[0] : void 0;
|
|
3388
|
+
const freshGroups = onlyGroup ? [await loadFreshGroupSnapshot(onlyGroup, remoteUrl, reconciliationDeadlineMs)] : await loadedSnapshots(void 0, reconciliationDeadlineMs);
|
|
3389
|
+
for (const freshGroup of freshGroups) {
|
|
3390
|
+
if (!freshGroup) continue;
|
|
3391
|
+
const key = freshGroupKey(freshGroup);
|
|
3392
|
+
if (freshGroupByKey.has(key)) throw new Error("team_setup_completion_invalid");
|
|
3393
|
+
freshGroupByKey.set(key, freshGroup);
|
|
3394
|
+
}
|
|
3395
|
+
} catch (error) {
|
|
3396
|
+
for (const record of records) {
|
|
3397
|
+
const group = groupById.get(record.group_id);
|
|
3398
|
+
if (!group || !belongsToGroupCandidate(record, group)) continue;
|
|
3399
|
+
try {
|
|
3400
|
+
validateLegacyTeamSetupCompletionManifestBinding(record.manifest, {
|
|
3401
|
+
coordinatorId: group.coordinatorId,
|
|
3402
|
+
groupId: group.groupId
|
|
3403
|
+
});
|
|
3404
|
+
} catch (bindingError) {
|
|
3405
|
+
if (!reconciliationOptions.isolateApplyFailures) throw bindingError;
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
let canonicalManifest;
|
|
3409
|
+
try {
|
|
3410
|
+
canonicalManifest = validateLegacyTeamSetupCompletionManifest(record.manifest, {
|
|
3411
|
+
coordinatorId: group.coordinatorId,
|
|
3412
|
+
groupId: group.groupId
|
|
3413
|
+
});
|
|
3414
|
+
} catch (validationError) {
|
|
3415
|
+
try {
|
|
3416
|
+
if (!await runCandidateMutation(store.db, legacyTeamCandidateId(group.coordinatorId, group.groupId), () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3417
|
+
coordinatorId: group.coordinatorId,
|
|
3418
|
+
groupId: group.groupId
|
|
3419
|
+
}))) {
|
|
3420
|
+
if (!reconciliationOptions.isolateApplyFailures) throw validationError;
|
|
3421
|
+
continue;
|
|
3422
|
+
}
|
|
3423
|
+
} catch (containmentError) {
|
|
3424
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3425
|
+
}
|
|
3426
|
+
if (!reconciliationOptions.isolateApplyFailures) throw validationError;
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
3429
|
+
try {
|
|
3430
|
+
const localManifest = reconstructLegacyTeamSetupCompletionManifest(store.db, { candidateRef: canonicalManifest.candidate_ref });
|
|
3431
|
+
if (canonicalCoordinatorLegacyTeamCompletionManifestJson(localManifest) === canonicalCoordinatorLegacyTeamCompletionManifestJson(canonicalManifest)) continue;
|
|
3432
|
+
if (areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible(localManifest, canonicalManifest)) continue;
|
|
3433
|
+
} catch {}
|
|
3434
|
+
try {
|
|
3435
|
+
await runCandidateMutation(store.db, canonicalManifest.candidate_ref, () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3436
|
+
coordinatorId: group.coordinatorId,
|
|
3437
|
+
groupId: group.groupId
|
|
3438
|
+
}));
|
|
3439
|
+
} catch (containmentError) {
|
|
3440
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
if (!reconciliationOptions.isolateApplyFailures) throw error;
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
const coordinatorCompletionGroupIds = /* @__PURE__ */ new Set();
|
|
3447
|
+
for (const record of records) {
|
|
3448
|
+
const group = groupById.get(record.group_id);
|
|
3449
|
+
if (!group) throw new Error("team_setup_completion_invalid");
|
|
3450
|
+
if (!belongsToGroupCandidate(record, group)) continue;
|
|
3451
|
+
try {
|
|
3452
|
+
validateLegacyTeamSetupCompletionManifestBinding(record.manifest, {
|
|
3453
|
+
coordinatorId: group.coordinatorId,
|
|
3454
|
+
groupId: group.groupId
|
|
3455
|
+
});
|
|
3456
|
+
} catch (bindingError) {
|
|
3457
|
+
if (!reconciliationOptions.isolateApplyFailures) throw bindingError;
|
|
3458
|
+
continue;
|
|
3459
|
+
}
|
|
3460
|
+
let canonicalManifest;
|
|
3461
|
+
try {
|
|
3462
|
+
canonicalManifest = validateLegacyTeamSetupCompletionManifest(record.manifest, {
|
|
3463
|
+
coordinatorId: group.coordinatorId,
|
|
3464
|
+
groupId: group.groupId
|
|
3465
|
+
});
|
|
3466
|
+
} catch (validationError) {
|
|
3467
|
+
try {
|
|
3468
|
+
await runCandidateMutation(store.db, legacyTeamCandidateId(group.coordinatorId, group.groupId), () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3469
|
+
coordinatorId: group.coordinatorId,
|
|
3470
|
+
groupId: group.groupId
|
|
3471
|
+
}));
|
|
3472
|
+
} catch (containmentError) {
|
|
3473
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3474
|
+
}
|
|
3475
|
+
if (!reconciliationOptions.isolateApplyFailures) throw validationError;
|
|
3476
|
+
continue;
|
|
3477
|
+
}
|
|
3478
|
+
coordinatorCompletionGroupIds.add(record.group_id);
|
|
3479
|
+
try {
|
|
3480
|
+
let freshGroup = freshGroupByKey.get(freshGroupKey(group));
|
|
3481
|
+
if (!freshGroup && scopedGroups.length > 1) {
|
|
3482
|
+
if (remainingCoordinatorTimeoutS(timeoutS, reconciliationDeadlineMs) == null) throw new Error("team_setup_roster_unavailable");
|
|
3483
|
+
freshGroup = await loadFreshGroupSnapshot(group, remoteUrl, reconciliationDeadlineMs);
|
|
3484
|
+
}
|
|
3485
|
+
if (!freshGroup) {
|
|
3486
|
+
if (!await runCandidateMutation(store.db, legacyTeamCandidateId(group.coordinatorId, group.groupId), () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3487
|
+
coordinatorId: group.coordinatorId,
|
|
3488
|
+
groupId: group.groupId
|
|
3489
|
+
}))) {
|
|
3490
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_invalid");
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
throw new Error("team_setup_completion_invalid");
|
|
3494
|
+
}
|
|
3495
|
+
if (await applyCanonicalManifestIfStillAuthorized(store, authorization, group, canonicalManifest, freshGroup.devices) !== "applied") continue;
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
if (!reconciliationOptions.isolateApplyFailures) throw error;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
for (const group of scopedGroups) {
|
|
3501
|
+
if (coordinatorCompletionGroupIds.has(group.groupId)) continue;
|
|
3502
|
+
const publicationTimeoutS = remainingCoordinatorTimeoutS(timeoutS, reconciliationDeadlineMs);
|
|
3503
|
+
if (publicationTimeoutS == null) break;
|
|
3504
|
+
if (!hasCompletedLegacyTeamSetup(store, legacyTeamCandidateId(group.coordinatorId, group.groupId))) continue;
|
|
3505
|
+
const candidateRef = legacyTeamCandidateId(group.coordinatorId, group.groupId);
|
|
3506
|
+
let reconstructionFailed = false;
|
|
3507
|
+
try {
|
|
3508
|
+
if (!await runCandidateMutation(store.db, candidateRef, () => serializeRecipientPolicyPublicationMutation(store.db, () => serializeRecipientPolicyTeamMutation(store.db, deterministicPolicyTeamId(candidateRef), async () => {
|
|
3509
|
+
let manifest;
|
|
3510
|
+
try {
|
|
3511
|
+
manifest = reconstructLegacyTeamSetupCompletionManifest(store.db, { candidateRef });
|
|
3512
|
+
} catch {
|
|
3513
|
+
reconstructionFailed = true;
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
if (!stillAuthorizesGroup(authorization, group)) {
|
|
3517
|
+
reconstructionFailed = true;
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
await completionDependencies.create({
|
|
3521
|
+
groupId: group.groupId,
|
|
3522
|
+
manifest,
|
|
3523
|
+
remoteUrl,
|
|
3524
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3525
|
+
timeoutS: publicationTimeoutS
|
|
3526
|
+
});
|
|
3527
|
+
}))) || reconstructionFailed) continue;
|
|
3528
|
+
} catch (error) {
|
|
3529
|
+
if (completionApiError(error) !== "team_setup_completion_conflict") continue;
|
|
3530
|
+
const getCompletion = completionDependencies.get;
|
|
3531
|
+
let canonicalManifest = null;
|
|
3532
|
+
try {
|
|
3533
|
+
if (!getCompletion) throw new Error("team_setup_completion_conflict");
|
|
3534
|
+
canonicalManifest = await retryTransientCoordinatorRead((attemptTimeoutS) => getCompletion({
|
|
3535
|
+
groupId: group.groupId,
|
|
3536
|
+
candidateRef,
|
|
3537
|
+
remoteUrl,
|
|
3538
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3539
|
+
timeoutS: attemptTimeoutS
|
|
3540
|
+
}), timeoutS, reconciliationDeadlineMs);
|
|
3541
|
+
if (!canonicalManifest) throw new Error("team_setup_completion_conflict");
|
|
3542
|
+
} catch {
|
|
3543
|
+
try {
|
|
3544
|
+
if (!await runCandidateMutation(store.db, candidateRef, () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3545
|
+
coordinatorId: group.coordinatorId,
|
|
3546
|
+
groupId: group.groupId
|
|
3547
|
+
}))) {
|
|
3548
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_conflict");
|
|
3549
|
+
continue;
|
|
3550
|
+
}
|
|
3551
|
+
} catch (containmentError) {
|
|
3552
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3553
|
+
continue;
|
|
3554
|
+
}
|
|
3555
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_conflict");
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
try {
|
|
3559
|
+
const freshGroup = await loadFreshGroupSnapshot(group, remoteUrl, reconciliationDeadlineMs);
|
|
3560
|
+
if (!freshGroup) throw new Error("team_setup_roster_unavailable");
|
|
3561
|
+
if (await applyCanonicalManifestIfStillAuthorized(store, authorization, group, canonicalManifest, freshGroup.devices) !== "applied") continue;
|
|
3562
|
+
} catch (applyError) {
|
|
3563
|
+
if (applyError instanceof LegacyTeamSetupAdditiveConvergenceError) {
|
|
3564
|
+
if (!reconciliationOptions.isolateApplyFailures) throw applyError;
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
try {
|
|
3568
|
+
if (!await runCandidateMutation(store.db, candidateRef, () => containLegacyTeamSetupCompletionConflict(store.db, {
|
|
3569
|
+
coordinatorId: group.coordinatorId,
|
|
3570
|
+
groupId: group.groupId
|
|
3571
|
+
}))) {
|
|
3572
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_conflict", { cause: applyError });
|
|
3573
|
+
continue;
|
|
3574
|
+
}
|
|
3575
|
+
} catch (containmentError) {
|
|
3576
|
+
if (!reconciliationOptions.isolateApplyFailures) throw containmentError;
|
|
3577
|
+
continue;
|
|
3578
|
+
}
|
|
3579
|
+
if (!reconciliationOptions.isolateApplyFailures) throw new Error("team_setup_completion_conflict", { cause: applyError });
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
} catch (error) {
|
|
3584
|
+
throw new Error(completionApiError(error));
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3032
3587
|
async function loadedCandidateSnapshots(candidateRef, loadOptions = {}) {
|
|
3033
3588
|
const allowCachedSummaryFallback = loadOptions.allowCachedSummaryFallback ?? false;
|
|
3034
3589
|
const invalidateSummaryCache = loadOptions.invalidateSummaryCache ?? true;
|
|
3035
3590
|
if (invalidateSummaryCache) loadedSummarySnapshots.invalidate();
|
|
3036
3591
|
try {
|
|
3037
|
-
return await loadedSnapshots(candidateRef);
|
|
3592
|
+
return await loadedSnapshots(candidateRef, loadOptions.deadlineMs);
|
|
3038
3593
|
} catch (error) {
|
|
3039
3594
|
if (!allowCachedSummaryFallback) throw error;
|
|
3040
3595
|
const cachedGroups = loadedSummarySnapshots.peek();
|
|
@@ -3046,6 +3601,15 @@ function teamSetupRoutes(options) {
|
|
|
3046
3601
|
if (invalidateSummaryCache) loadedSummarySnapshots.invalidate();
|
|
3047
3602
|
}
|
|
3048
3603
|
}
|
|
3604
|
+
function persistedCompletionGroup(store, candidateRef) {
|
|
3605
|
+
const row = store.db.prepare(`SELECT coordinator_id, group_id FROM legacy_team_setup_drafts
|
|
3606
|
+
WHERE candidate_id = ? ORDER BY rowid DESC LIMIT 1`).get(candidateRef);
|
|
3607
|
+
if (!row || legacyTeamCandidateId(row.coordinator_id, row.group_id) !== candidateRef) return null;
|
|
3608
|
+
return {
|
|
3609
|
+
coordinatorId: row.coordinator_id,
|
|
3610
|
+
groupId: row.group_id
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3049
3613
|
app.get("/api/sync/team-setup/v1", async (c) => {
|
|
3050
3614
|
let groups;
|
|
3051
3615
|
try {
|
|
@@ -3055,9 +3619,12 @@ function teamSetupRoutes(options) {
|
|
|
3055
3619
|
return c.json({ error: code }, errorStatus$1(code));
|
|
3056
3620
|
}
|
|
3057
3621
|
try {
|
|
3622
|
+
const store = options.getStore();
|
|
3623
|
+
readPureCandidateViews(store, groups);
|
|
3624
|
+
await reconcileCompletions(store, groups, { isolateApplyFailures: true });
|
|
3058
3625
|
const response = {
|
|
3059
3626
|
version: TEAM_SETUP_VERSION,
|
|
3060
|
-
candidates: readPureCandidateViews(
|
|
3627
|
+
candidates: readPureCandidateViews(store, groups).map(candidateSummary).filter((candidate) => candidate.status !== "ready")
|
|
3061
3628
|
};
|
|
3062
3629
|
return c.json(response);
|
|
3063
3630
|
} catch (error) {
|
|
@@ -3072,6 +3639,7 @@ function teamSetupRoutes(options) {
|
|
|
3072
3639
|
const store = options.getStore();
|
|
3073
3640
|
let draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3074
3641
|
const terminal = hasCompletedLegacyTeamSetup(store, candidateRef);
|
|
3642
|
+
let candidateGroupsForCompletion = null;
|
|
3075
3643
|
if (terminal && draft?.state !== "completed") return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3076
3644
|
let candidate;
|
|
3077
3645
|
if (draft && (draft.state !== "completed" || terminal)) candidate = candidateFromDraft(draft);
|
|
@@ -3080,6 +3648,7 @@ function teamSetupRoutes(options) {
|
|
|
3080
3648
|
allowCachedSummaryFallback: draft == null,
|
|
3081
3649
|
invalidateSummaryCache: false
|
|
3082
3650
|
})).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
|
|
3651
|
+
candidateGroupsForCompletion = candidateGroups;
|
|
3083
3652
|
const postLoadDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3084
3653
|
if (hasCompletedLegacyTeamSetup(store, candidateRef)) {
|
|
3085
3654
|
if (postLoadDraft?.state !== "completed") return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
@@ -3093,6 +3662,17 @@ function teamSetupRoutes(options) {
|
|
|
3093
3662
|
}
|
|
3094
3663
|
}
|
|
3095
3664
|
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3665
|
+
const cachedCandidateGroups = loadedSummarySnapshots.peek()?.filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
|
|
3666
|
+
let completionGroups = candidateGroupsForCompletion ?? (cachedCandidateGroups?.length ? cachedCandidateGroups : null);
|
|
3667
|
+
if (!completionGroups) {
|
|
3668
|
+
const persistedGroup = persistedCompletionGroup(store, candidateRef);
|
|
3669
|
+
completionGroups = persistedGroup ? [persistedGroup] : null;
|
|
3670
|
+
}
|
|
3671
|
+
const terminalBeforeReconciliation = hasCompletedLegacyTeamSetup(store, candidateRef);
|
|
3672
|
+
if (completionGroups) await reconcileCompletions(store, completionGroups);
|
|
3673
|
+
draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3674
|
+
if (!draft || !terminalBeforeReconciliation && hasCompletedLegacyTeamSetup(store, candidateRef)) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3675
|
+
if (completionGroups) candidate = candidateFromDraft(draft);
|
|
3096
3676
|
return c.json(detailResponse(store, draft, candidate));
|
|
3097
3677
|
} catch (error) {
|
|
3098
3678
|
const code = apiErrorCode(error);
|
|
@@ -3128,12 +3708,18 @@ function teamSetupRoutes(options) {
|
|
|
3128
3708
|
]) && 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
3709
|
const target = identityChoices(store, candidateRef).find((choice) => choice.identityRef === targetIdentityRef);
|
|
3130
3710
|
if (!target) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3711
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3712
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3713
|
+
try {
|
|
3714
|
+
return c.json(projectMutationAtomically(store, () => setLegacyTeamSetupDeviceAssignment(store.db, {
|
|
3715
|
+
attemptId,
|
|
3716
|
+
deviceRef,
|
|
3717
|
+
targetIdentityId: target.identityId,
|
|
3718
|
+
expectation: device.expectation
|
|
3719
|
+
}), (draft) => detailResponse(store, draft)));
|
|
3720
|
+
} finally {
|
|
3721
|
+
releaseMutation();
|
|
3722
|
+
}
|
|
3137
3723
|
} catch (error) {
|
|
3138
3724
|
const code = apiErrorCode(error);
|
|
3139
3725
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3165,11 +3751,17 @@ function teamSetupRoutes(options) {
|
|
|
3165
3751
|
const device = draft.devices.find((item) => item.deviceRef === deviceRef);
|
|
3166
3752
|
if (!device) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3167
3753
|
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
|
-
|
|
3754
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3755
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3756
|
+
try {
|
|
3757
|
+
return c.json(projectMutationAtomically(store, () => setLegacyTeamSetupDeviceDecision(store.db, {
|
|
3758
|
+
attemptId,
|
|
3759
|
+
deviceRef,
|
|
3760
|
+
decision
|
|
3761
|
+
}), (draft) => detailResponse(store, draft)));
|
|
3762
|
+
} finally {
|
|
3763
|
+
releaseMutation();
|
|
3764
|
+
}
|
|
3173
3765
|
} catch (error) {
|
|
3174
3766
|
const code = apiErrorCode(error);
|
|
3175
3767
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3191,10 +3783,16 @@ function teamSetupRoutes(options) {
|
|
|
3191
3783
|
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3192
3784
|
if (draft.state === "completed") return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3193
3785
|
if (!draft.devices.some((item) => item.deviceRef === deviceRef)) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3786
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3787
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3788
|
+
try {
|
|
3789
|
+
return c.json(projectMutationAtomically(store, () => clearLegacyTeamSetupDeviceDecision(store.db, {
|
|
3790
|
+
attemptId,
|
|
3791
|
+
deviceRef
|
|
3792
|
+
}), (draft) => detailResponse(store, draft)));
|
|
3793
|
+
} finally {
|
|
3794
|
+
releaseMutation();
|
|
3795
|
+
}
|
|
3198
3796
|
} catch (error) {
|
|
3199
3797
|
const code = apiErrorCode(error);
|
|
3200
3798
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3220,11 +3818,17 @@ function teamSetupRoutes(options) {
|
|
|
3220
3818
|
if (project.resolution === "deterministic") return c.json({ error: "team_setup_incomplete" }, 400);
|
|
3221
3819
|
const target = projectMappingChoices(store).find((choice) => legacyTeamResolvedProjectRef(projectRef, choice.projectIdentity) === resolvedProjectRef);
|
|
3222
3820
|
if (!target) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3821
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3822
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3823
|
+
try {
|
|
3824
|
+
return c.json(projectMutationAtomically(store, () => setLegacyTeamSetupProjectMapping(store.db, {
|
|
3825
|
+
attemptId,
|
|
3826
|
+
projectRef,
|
|
3827
|
+
resolvedProjectIdentity: target.projectIdentity
|
|
3828
|
+
}), (draft) => detailResponse(store, draft)));
|
|
3829
|
+
} finally {
|
|
3830
|
+
releaseMutation();
|
|
3831
|
+
}
|
|
3228
3832
|
} catch (error) {
|
|
3229
3833
|
const code = apiErrorCode(error);
|
|
3230
3834
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3252,13 +3856,19 @@ function teamSetupRoutes(options) {
|
|
|
3252
3856
|
}
|
|
3253
3857
|
if (groups.filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef).length === 0) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3254
3858
|
try {
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3859
|
+
const releaseMutation = claimCandidateMutation(store.db, candidateRef);
|
|
3860
|
+
if (!releaseMutation) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3861
|
+
try {
|
|
3862
|
+
return c.json(projectMutationAtomically(store, () => {
|
|
3863
|
+
if (getLegacyTeamSetupDraft(store.db, candidateRef)?.state === "completed" || hasCompletedLegacyTeamSetup(store, candidateRef)) throw new Error("team_setup_confirmation_stale");
|
|
3864
|
+
return refreshLegacyTeamCandidate(store.db, {
|
|
3865
|
+
projection: projectionOptions(store),
|
|
3866
|
+
groups
|
|
3867
|
+
}, candidateRef);
|
|
3868
|
+
}, (draft) => detailResponse(store, draft)));
|
|
3869
|
+
} finally {
|
|
3870
|
+
releaseMutation();
|
|
3871
|
+
}
|
|
3262
3872
|
} catch (error) {
|
|
3263
3873
|
const code = apiErrorCode(error);
|
|
3264
3874
|
return c.json({ error: code }, errorStatus$1(code));
|
|
@@ -3278,50 +3888,180 @@ function teamSetupRoutes(options) {
|
|
|
3278
3888
|
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
3889
|
const { attemptId, confirmedAccessDeltaDigest, confirmedViewerAccessDeltaDigest, finishDigest } = parsed.value;
|
|
3280
3890
|
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);
|
|
3891
|
+
let releaseMutation;
|
|
3892
|
+
let releasePublicationMutation;
|
|
3893
|
+
let releaseActorMutations;
|
|
3894
|
+
let finishLogRedactValues = [];
|
|
3281
3895
|
try {
|
|
3282
3896
|
const store = options.getStore();
|
|
3283
3897
|
const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3284
3898
|
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
3285
3899
|
if (draft.state !== "completed" && hasCompletedLegacyTeamSetup(store, candidateRef)) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3286
3900
|
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3287
|
-
|
|
3288
|
-
|
|
3901
|
+
const replay = replayLegacyTeamSetupActivation(store.db, {
|
|
3902
|
+
candidateRef,
|
|
3903
|
+
attemptId,
|
|
3904
|
+
finishDigest,
|
|
3905
|
+
confirmedAccessDeltaDigest
|
|
3906
|
+
});
|
|
3907
|
+
if (replay) return c.json(finishResponse(candidateRef, replay));
|
|
3908
|
+
if (draft.state === "completed") return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3909
|
+
const preview = previewLegacyTeamSetupActivation(store.db, {
|
|
3910
|
+
candidateRef,
|
|
3911
|
+
attemptId: draft.attemptId
|
|
3912
|
+
});
|
|
3913
|
+
requireLegacyTeamSetupAccessDeltaWithinLimit(preview.accessDelta);
|
|
3914
|
+
const safeDraft = viewerSafeDraft(store, draft);
|
|
3915
|
+
if (viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, preview.accessDelta, {
|
|
3916
|
+
teamDisplayName: draft.displayName,
|
|
3917
|
+
...safeDraft
|
|
3918
|
+
})) !== confirmedViewerAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3919
|
+
const matchingGroups = (await loadedCandidateSnapshots(candidateRef)).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
|
|
3920
|
+
if (matchingGroups.length !== 1) throw safeCoordinatorError();
|
|
3921
|
+
const group = matchingGroups[0];
|
|
3922
|
+
if (!group) throw safeCoordinatorError();
|
|
3923
|
+
releaseMutation = claimCandidateMutation(store.db, candidateRef) ?? void 0;
|
|
3924
|
+
if (!releaseMutation) {
|
|
3925
|
+
await candidateMutationSettled(store.db, candidateRef);
|
|
3926
|
+
const overlappingReplay = replayLegacyTeamSetupActivation(store.db, {
|
|
3289
3927
|
candidateRef,
|
|
3290
|
-
attemptId
|
|
3928
|
+
attemptId,
|
|
3929
|
+
finishDigest,
|
|
3930
|
+
confirmedAccessDeltaDigest
|
|
3291
3931
|
});
|
|
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);
|
|
3932
|
+
if (overlappingReplay) return c.json(finishResponse(candidateRef, overlappingReplay));
|
|
3933
|
+
return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3298
3934
|
}
|
|
3299
|
-
const
|
|
3935
|
+
const claimedReplay = replayLegacyTeamSetupActivation(store.db, {
|
|
3300
3936
|
candidateRef,
|
|
3301
3937
|
attemptId,
|
|
3302
3938
|
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
|
-
}
|
|
3939
|
+
confirmedAccessDeltaDigest
|
|
3320
3940
|
});
|
|
3321
|
-
return c.json(finishResponse(candidateRef,
|
|
3941
|
+
if (claimedReplay) return c.json(finishResponse(candidateRef, claimedReplay));
|
|
3942
|
+
const claimedDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3943
|
+
if (!claimedDraft || claimedDraft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3944
|
+
const publicationDeadlineMs = Date.now() + COORDINATOR_ROSTER_READ_BUDGET_MS;
|
|
3945
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
3946
|
+
releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, claimedDraft.devices.flatMap((device) => [...device.decision === "included" && device.targetIdentityId ? [device.targetIdentityId] : [], ...device.existingIdentityId ? [device.existingIdentityId] : []]));
|
|
3947
|
+
return await serializeRecipientPolicyTeamMutation(store.db, deterministicPolicyTeamId(candidateRef), () => serializeRecipientPolicyCoordinatorGroupMutation(store.db, group.groupId, async () => {
|
|
3948
|
+
let authorization;
|
|
3949
|
+
try {
|
|
3950
|
+
authorization = currentCompletionAuthorization();
|
|
3951
|
+
} catch (error) {
|
|
3952
|
+
throw new Error(completionApiError(error));
|
|
3953
|
+
}
|
|
3954
|
+
if (!completionDependencies || !authorization) throw new Error("team_setup_completion_unavailable");
|
|
3955
|
+
if (!authorizesGroup(authorization, group)) throw new Error("team_setup_completion_invalid");
|
|
3956
|
+
const { config, remoteUrl } = authorization;
|
|
3957
|
+
finishLogRedactValues = [config.syncCoordinatorAdminSecret];
|
|
3958
|
+
const freshGroup = await loadFreshGroupSnapshot(group, remoteUrl, publicationDeadlineMs);
|
|
3959
|
+
if (!freshGroup) throw new TeamSetupFinishError("team_setup_roster_unavailable", "coordinator_roster_unavailable", false, /* @__PURE__ */ new Error("coordinator roster unavailable"));
|
|
3960
|
+
if (!stillAuthorizesGroup(authorization, group)) throw new Error("team_setup_completion_invalid");
|
|
3961
|
+
const projectInventory = legacyTeamCandidateProjectInventory(store.db, projectionOptions(store), candidateRef);
|
|
3962
|
+
const freshPreview = inspectFreshLegacyTeamSetupActivation(store.db, {
|
|
3963
|
+
candidateRef,
|
|
3964
|
+
attemptId,
|
|
3965
|
+
freshRoster: freshGroup.devices,
|
|
3966
|
+
projectInventory
|
|
3967
|
+
});
|
|
3968
|
+
if (freshPreview.finishDigest !== finishDigest || freshPreview.accessDeltaDigest !== confirmedAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3969
|
+
const freshDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
3970
|
+
if (!freshDraft || freshDraft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3971
|
+
const safeFreshDraft = viewerSafeDraft(store, freshDraft);
|
|
3972
|
+
if (viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, freshPreview.accessDelta, {
|
|
3973
|
+
teamDisplayName: freshDraft.displayName,
|
|
3974
|
+
...safeFreshDraft
|
|
3975
|
+
})) !== confirmedViewerAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
3976
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3977
|
+
const proposedManifest = deriveLegacyTeamSetupCompletionManifest(store.db, {
|
|
3978
|
+
candidateRef,
|
|
3979
|
+
attemptId,
|
|
3980
|
+
completedAt
|
|
3981
|
+
});
|
|
3982
|
+
const timeoutS = Math.max(1, config.syncCoordinatorTimeoutS);
|
|
3983
|
+
const publicationTimeoutS = remainingCoordinatorTimeoutS(timeoutS, publicationDeadlineMs);
|
|
3984
|
+
if (publicationTimeoutS == null) throw new Error("team_setup_completion_unavailable");
|
|
3985
|
+
let canonicalManifest;
|
|
3986
|
+
try {
|
|
3987
|
+
canonicalManifest = validateLegacyTeamSetupCompletionManifest((await completionDependencies.create({
|
|
3988
|
+
groupId: group.groupId,
|
|
3989
|
+
manifest: proposedManifest,
|
|
3990
|
+
remoteUrl,
|
|
3991
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
3992
|
+
timeoutS: publicationTimeoutS
|
|
3993
|
+
})).manifest, {
|
|
3994
|
+
coordinatorId: group.coordinatorId,
|
|
3995
|
+
groupId: group.groupId
|
|
3996
|
+
});
|
|
3997
|
+
if (canonicalCoordinatorLegacyTeamCompletionManifestJson(canonicalManifest) !== canonicalCoordinatorLegacyTeamCompletionManifestJson(proposedManifest)) throw new Error("team_setup_completion_conflict");
|
|
3998
|
+
} catch (error) {
|
|
3999
|
+
const code = completionApiError(error);
|
|
4000
|
+
const details = teamSetupFinishFailureDetails(error);
|
|
4001
|
+
const getCompletion = completionDependencies.get;
|
|
4002
|
+
if (code !== "team_setup_completion_conflict" || !getCompletion) {
|
|
4003
|
+
warnTeamSetupFinishFailure(error, details, {
|
|
4004
|
+
candidateRef,
|
|
4005
|
+
attemptId,
|
|
4006
|
+
apiCode: code,
|
|
4007
|
+
redactValues: finishLogRedactValues
|
|
4008
|
+
});
|
|
4009
|
+
throw new TeamSetupFinishError(code, details.reason, true, error);
|
|
4010
|
+
}
|
|
4011
|
+
try {
|
|
4012
|
+
const existing = await retryTransientCoordinatorRead((attemptTimeoutS) => getCompletion({
|
|
4013
|
+
groupId: group.groupId,
|
|
4014
|
+
candidateRef,
|
|
4015
|
+
remoteUrl,
|
|
4016
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
4017
|
+
timeoutS: attemptTimeoutS
|
|
4018
|
+
}), timeoutS, publicationDeadlineMs);
|
|
4019
|
+
if (!existing) throw new Error("team_setup_completion_conflict");
|
|
4020
|
+
canonicalManifest = validateLegacyTeamSetupCompletionManifest(existing, {
|
|
4021
|
+
coordinatorId: group.coordinatorId,
|
|
4022
|
+
groupId: group.groupId
|
|
4023
|
+
});
|
|
4024
|
+
if (canonicalManifest.finish_digest !== proposedManifest.finish_digest) throw new Error("team_setup_completion_conflict");
|
|
4025
|
+
} catch (recoveryError) {
|
|
4026
|
+
throw new TeamSetupFinishError(completionApiError(recoveryError), teamSetupFinishFailureDetails(recoveryError).reason, false, recoveryError);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
let postPublicationGroup;
|
|
4030
|
+
try {
|
|
4031
|
+
postPublicationGroup = await loadFreshGroupSnapshot(group, remoteUrl, publicationDeadlineMs);
|
|
4032
|
+
} catch (error) {
|
|
4033
|
+
if (legacyTeamSetupApiErrorCode(error) === "team_setup_completion_invalid") throw new TeamSetupFinishError("team_setup_roster_unavailable", "coordinator_roster_unavailable", false, error);
|
|
4034
|
+
throw error;
|
|
4035
|
+
}
|
|
4036
|
+
if (!postPublicationGroup) throw new TeamSetupFinishError("team_setup_roster_unavailable", "coordinator_roster_unavailable", false, /* @__PURE__ */ new Error("coordinator roster unavailable"));
|
|
4037
|
+
const result = await applyLegacyTeamSetupCompletionManifestAndReturnActivation(store.db, {
|
|
4038
|
+
coordinatorId: group.coordinatorId,
|
|
4039
|
+
groupId: group.groupId,
|
|
4040
|
+
freshRoster: postPublicationGroup.devices,
|
|
4041
|
+
manifest: canonicalManifest,
|
|
4042
|
+
expectedDraftManifest: proposedManifest,
|
|
4043
|
+
recipientPolicyLocksHeld: {
|
|
4044
|
+
publication: true,
|
|
4045
|
+
team: true,
|
|
4046
|
+
coordinatorGroup: true
|
|
4047
|
+
}
|
|
4048
|
+
});
|
|
4049
|
+
return c.json(finishResponse(candidateRef, result));
|
|
4050
|
+
}));
|
|
3322
4051
|
} catch (error) {
|
|
3323
4052
|
const code = apiErrorCode(error);
|
|
3324
|
-
|
|
4053
|
+
const details = teamSetupFinishFailureDetails(error);
|
|
4054
|
+
warnTeamSetupFinishFailure(error, details, {
|
|
4055
|
+
candidateRef,
|
|
4056
|
+
attemptId,
|
|
4057
|
+
apiCode: code,
|
|
4058
|
+
redactValues: finishLogRedactValues
|
|
4059
|
+
});
|
|
4060
|
+
return c.json(finishErrorResponse(code, details), errorStatus$1(code));
|
|
4061
|
+
} finally {
|
|
4062
|
+
releaseActorMutations?.();
|
|
4063
|
+
releasePublicationMutation?.();
|
|
4064
|
+
releaseMutation?.();
|
|
3325
4065
|
}
|
|
3326
4066
|
});
|
|
3327
4067
|
return app;
|
|
@@ -3670,7 +4410,7 @@ function projectInviteAcceptanceFailure(error) {
|
|
|
3670
4410
|
}
|
|
3671
4411
|
var PROJECT_INVITE_TTL_HOURS = 168;
|
|
3672
4412
|
var PROJECT_INVITE_OWNER_FALLBACK = "Project owner";
|
|
3673
|
-
var PROJECT_INVITE_BODY_KEYS = new Set([
|
|
4413
|
+
var PROJECT_INVITE_BODY_KEYS = /* @__PURE__ */ new Set([
|
|
3674
4414
|
"teammate_name",
|
|
3675
4415
|
"project_ids",
|
|
3676
4416
|
"reviewed_project_set_digest"
|
|
@@ -3695,7 +4435,7 @@ function projectInviteStringList(body, key) {
|
|
|
3695
4435
|
return items;
|
|
3696
4436
|
}
|
|
3697
4437
|
function assertProjectInviteBody(body, creating) {
|
|
3698
|
-
const allowed = creating ? PROJECT_INVITE_BODY_KEYS : new Set(["teammate_name", "project_ids"]);
|
|
4438
|
+
const allowed = creating ? PROJECT_INVITE_BODY_KEYS : /* @__PURE__ */ new Set(["teammate_name", "project_ids"]);
|
|
3699
4439
|
if (Object.keys(body).some((key) => !allowed.has(key))) throw new Error("unexpected_project_invite_fields");
|
|
3700
4440
|
if (typeof body.teammate_name !== "string") throw new Error("teammate_name_must_be_string");
|
|
3701
4441
|
if (creating && typeof body.reviewed_project_set_digest !== "string") throw new Error("reviewed_project_set_digest_required");
|
|
@@ -3807,7 +4547,7 @@ function recipientInviteOnboardingPreviewFromReviewedIntent(store, body, invitat
|
|
|
3807
4547
|
...binding
|
|
3808
4548
|
});
|
|
3809
4549
|
}
|
|
3810
|
-
var SAFE_RECIPIENT_INVITE_ERRORS = new Set([
|
|
4550
|
+
var SAFE_RECIPIENT_INVITE_ERRORS = /* @__PURE__ */ new Set([
|
|
3811
4551
|
"device_display_name_invalid",
|
|
3812
4552
|
"device_display_name_required",
|
|
3813
4553
|
"device_display_name_too_long",
|
|
@@ -3841,20 +4581,21 @@ async function peerSupportsSyncRequirements(store, deviceId, requirements) {
|
|
|
3841
4581
|
const addresses = safeJsonList(row?.addresses_json ?? null);
|
|
3842
4582
|
for (const address of addresses) try {
|
|
3843
4583
|
const url = `${buildBaseUrl(address)}/v1/status`;
|
|
4584
|
+
const headers = {
|
|
4585
|
+
...buildDirectPeerAuthHeaders({
|
|
4586
|
+
deviceId: localDeviceId,
|
|
4587
|
+
recipientId: deviceId,
|
|
4588
|
+
dbPath: store.dbPath,
|
|
4589
|
+
method: "GET",
|
|
4590
|
+
url,
|
|
4591
|
+
bodyBytes: Buffer.alloc(0),
|
|
4592
|
+
keysDir: syncKeysDir()
|
|
4593
|
+
}),
|
|
4594
|
+
[SYNC_CAPABILITY_HEADER]: LOCAL_SYNC_CAPABILITY,
|
|
4595
|
+
[SYNC_FEATURES_HEADER]: LOCAL_SYNC_FEATURES.join(",")
|
|
4596
|
+
};
|
|
3844
4597
|
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
|
-
},
|
|
4598
|
+
headers,
|
|
3858
4599
|
timeoutS: 5
|
|
3859
4600
|
});
|
|
3860
4601
|
if (status < 200 || status >= 300) continue;
|
|
@@ -4226,9 +4967,9 @@ var AUTOMATIC_SHARE_OPERATION_STATES = [
|
|
|
4226
4967
|
"initial_sync",
|
|
4227
4968
|
"waiting_for_device"
|
|
4228
4969
|
];
|
|
4229
|
-
var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS =
|
|
4230
|
-
var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS =
|
|
4231
|
-
var TERMINAL_SHARE_MAINTENANCE_ERRORS = new Set([
|
|
4970
|
+
var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 3e4;
|
|
4971
|
+
var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 3e5;
|
|
4972
|
+
var TERMINAL_SHARE_MAINTENANCE_ERRORS = /* @__PURE__ */ new Set([
|
|
4232
4973
|
"coordinator_not_configured",
|
|
4233
4974
|
"team_sharing_not_configured",
|
|
4234
4975
|
"team_selection_ambiguous",
|
|
@@ -4242,7 +4983,7 @@ var TERMINAL_SHARE_MAINTENANCE_ERRORS = new Set([
|
|
|
4242
4983
|
"intent_conflict",
|
|
4243
4984
|
"inviter_identity_conflict"
|
|
4244
4985
|
]);
|
|
4245
|
-
var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = new Set([
|
|
4986
|
+
var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = /* @__PURE__ */ new Set([
|
|
4246
4987
|
"operation_scope_mismatch",
|
|
4247
4988
|
"operation_state_invalid",
|
|
4248
4989
|
"operation_acceptance_invalid",
|
|
@@ -4257,7 +4998,7 @@ var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = new Set([
|
|
|
4257
4998
|
"intent_conflict",
|
|
4258
4999
|
"inviter_identity_conflict"
|
|
4259
5000
|
]);
|
|
4260
|
-
var TERMINAL_RECONCILIATION_ERRORS = new Set([
|
|
5001
|
+
var TERMINAL_RECONCILIATION_ERRORS = /* @__PURE__ */ new Set([
|
|
4261
5002
|
"coordinator_not_configured",
|
|
4262
5003
|
"team_sharing_not_configured",
|
|
4263
5004
|
"team_selection_ambiguous",
|
|
@@ -4451,7 +5192,7 @@ function coordinatorEnrollmentFailureCode(error) {
|
|
|
4451
5192
|
const message = (error instanceof Error ? error.message : String(error)).trim();
|
|
4452
5193
|
const httpStatus = message.match(/\((\d{3})(?::|\))/u)?.[1];
|
|
4453
5194
|
if (httpStatus) return `http_${httpStatus}`;
|
|
4454
|
-
if (new Set([
|
|
5195
|
+
if ((/* @__PURE__ */ new Set([
|
|
4455
5196
|
"coordinator_consumed_team_invite_invalid",
|
|
4456
5197
|
"coordinator_device_list_malformed",
|
|
4457
5198
|
"coordinator_group_id_invalid",
|
|
@@ -4459,7 +5200,7 @@ function coordinatorEnrollmentFailureCode(error) {
|
|
|
4459
5200
|
"coordinator_invite_group_mismatch",
|
|
4460
5201
|
"coordinator_invite_list_malformed",
|
|
4461
5202
|
"reconciliation_time_invalid"
|
|
4462
|
-
]).has(message)) return message;
|
|
5203
|
+
])).has(message)) return message;
|
|
4463
5204
|
if (/timeout|timed out|abort/iu.test(message)) return "request_timeout";
|
|
4464
5205
|
if (/fetch failed|network|econn|enotfound/iu.test(message)) return "network_error";
|
|
4465
5206
|
return "unexpected_error";
|
|
@@ -4517,7 +5258,7 @@ async function reconcileConfiguredCoordinatorEnrollment(store, options = {}) {
|
|
|
4517
5258
|
continue;
|
|
4518
5259
|
}
|
|
4519
5260
|
try {
|
|
4520
|
-
const result = reconcileSnapshot({
|
|
5261
|
+
const result = await reconcileSnapshot({
|
|
4521
5262
|
coordinatorId: buildBaseUrl(remoteUrl),
|
|
4522
5263
|
groupId,
|
|
4523
5264
|
enrollments: enrollmentsResult.value,
|
|
@@ -4540,7 +5281,7 @@ async function reconcileConfiguredCoordinatorEnrollment(store, options = {}) {
|
|
|
4540
5281
|
var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
|
|
4541
5282
|
var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
|
|
4542
5283
|
var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
|
|
4543
|
-
var RECIPIENT_POLICY_WAITING_ERRORS = new Set([
|
|
5284
|
+
var RECIPIENT_POLICY_WAITING_ERRORS = /* @__PURE__ */ new Set([
|
|
4544
5285
|
"recipient_policy_capability_undetermined",
|
|
4545
5286
|
"recipient_policy_parity_incomplete",
|
|
4546
5287
|
"recipient_policy_snapshot_not_fresh"
|
|
@@ -5709,7 +6450,7 @@ function mapSyncAttemptRow(row, showDiag, addresses) {
|
|
|
5709
6450
|
address: showDiag && addresses?.length ? addresses[0] : null
|
|
5710
6451
|
};
|
|
5711
6452
|
}
|
|
5712
|
-
var SYNC_SCOPE_REJECTION_WINDOW_SECONDS =
|
|
6453
|
+
var SYNC_SCOPE_REJECTION_WINDOW_SECONDS = 86400;
|
|
5713
6454
|
var CLEANUP_DIAGNOSTICS_MAX_ROWS = 250;
|
|
5714
6455
|
function recentScopeRejectionsByPeer(store) {
|
|
5715
6456
|
const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_SCOPE_REJECTION_WINDOW_SECONDS * 1e3)).toISOString();
|
|
@@ -6017,7 +6758,7 @@ function reassignLegacySharedReviewGroup(store, input) {
|
|
|
6017
6758
|
legacy_shared_review: legacySharedReviewSummary(store)
|
|
6018
6759
|
};
|
|
6019
6760
|
}
|
|
6020
|
-
var SYNC_DIRECTION_WINDOW_SECONDS =
|
|
6761
|
+
var SYNC_DIRECTION_WINDOW_SECONDS = 86400;
|
|
6021
6762
|
function recentPeerOps(store) {
|
|
6022
6763
|
const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_DIRECTION_WINDOW_SECONDS * 1e3)).toISOString();
|
|
6023
6764
|
const rows = store.db.prepare(`SELECT peer_device_id,
|
|
@@ -6112,7 +6853,8 @@ var PEERS_QUERY = `
|
|
|
6112
6853
|
* scope contents.
|
|
6113
6854
|
*/
|
|
6114
6855
|
function negotiatedSyncCapability(c) {
|
|
6115
|
-
|
|
6856
|
+
const header = c.req.header(SYNC_CAPABILITY_HEADER) ?? null;
|
|
6857
|
+
return negotiateSyncCapability(LOCAL_SYNC_CAPABILITY, normalizeSyncCapability(header));
|
|
6116
6858
|
}
|
|
6117
6859
|
/**
|
|
6118
6860
|
* Peer-to-peer sync protocol routes (/v1/*).
|
|
@@ -6173,7 +6915,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
6173
6915
|
});
|
|
6174
6916
|
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
6175
6917
|
const syncReset = getSyncResetState(store.db);
|
|
6176
|
-
const
|
|
6918
|
+
const negotiated = negotiatedSyncCapability(c);
|
|
6919
|
+
const authorizedScopes = isScopedSyncCapability(negotiated) ? listAuthorizedScopesForPeer(store.db, {
|
|
6177
6920
|
localDeviceId: deviceId,
|
|
6178
6921
|
peerDeviceId: auth.deviceId
|
|
6179
6922
|
}) : null;
|
|
@@ -6560,7 +7303,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6560
7303
|
const bindings = body.bindings.map((value) => {
|
|
6561
7304
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
6562
7305
|
const binding = value;
|
|
6563
|
-
const bindingKeys = new Set([
|
|
7306
|
+
const bindingKeys = /* @__PURE__ */ new Set([
|
|
6564
7307
|
"deviceId",
|
|
6565
7308
|
"targetIdentityId",
|
|
6566
7309
|
"confirmed",
|
|
@@ -6604,7 +7347,9 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6604
7347
|
const body = parseDeviceBindingRequest(await parseViewerJsonBody(c), true);
|
|
6605
7348
|
if (!body || !("reviewedInventoryDigest" in body)) return c.json({ error: "binding_request_invalid" }, 400);
|
|
6606
7349
|
const store = getStore();
|
|
7350
|
+
let releasePublicationMutation;
|
|
6607
7351
|
try {
|
|
7352
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
6608
7353
|
ensureStableStoreIdentity(store);
|
|
6609
7354
|
const result = commitDeviceIdentityBindings(store.db, {
|
|
6610
7355
|
localActorId: store.actorId,
|
|
@@ -6620,6 +7365,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6620
7365
|
return c.json({ error: "binding_commit_busy" }, 503);
|
|
6621
7366
|
}
|
|
6622
7367
|
return c.json({ error: "binding_commit_failed" }, 500);
|
|
7368
|
+
} finally {
|
|
7369
|
+
releasePublicationMutation?.();
|
|
6623
7370
|
}
|
|
6624
7371
|
});
|
|
6625
7372
|
app.get("/api/sync/status", async (c) => {
|
|
@@ -6758,7 +7505,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6758
7505
|
if (addrs.length) peerAddressMap.set(String(peerRow.peer_device_id ?? ""), addrs);
|
|
6759
7506
|
});
|
|
6760
7507
|
const attemptsItems = attemptRows.map((row) => {
|
|
6761
|
-
|
|
7508
|
+
const addrs = showDiag ? peerAddressMap.get(String(row.peer_device_id ?? "")) : void 0;
|
|
7509
|
+
return mapSyncAttemptRow(row, showDiag, addrs);
|
|
6762
7510
|
});
|
|
6763
7511
|
const latestAttemptError = String(attemptRows[0]?.error || "").trim();
|
|
6764
7512
|
const statusBlock = {
|
|
@@ -6837,7 +7585,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6837
7585
|
const limit = queryInt(c.req.query("limit"), 100);
|
|
6838
7586
|
if (limit <= 0) return c.json({ error: "invalid_limit" }, 400);
|
|
6839
7587
|
const sinceMinutesRaw = queryInt(c.req.query("sinceMinutes"), 1440);
|
|
6840
|
-
const sinceMinutes = Math.min(Math.max(sinceMinutesRaw, 0),
|
|
7588
|
+
const sinceMinutes = Math.min(Math.max(sinceMinutesRaw, 0), 5256e3);
|
|
6841
7589
|
const sinceIso = sinceMinutes > 0 ? (/* @__PURE__ */ new Date(Date.now() - sinceMinutes * 6e4)).toISOString() : void 0;
|
|
6842
7590
|
const records = listInboundScopeRejections(store.db, {
|
|
6843
7591
|
peerDeviceId,
|
|
@@ -7064,7 +7812,14 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7064
7812
|
const store = getStore();
|
|
7065
7813
|
const body = await parseViewerJsonBody(c);
|
|
7066
7814
|
try {
|
|
7067
|
-
const
|
|
7815
|
+
const request = parseRecipientPolicyEdgeCommitRequest(body);
|
|
7816
|
+
const teamIds = request ? [...new Set(request.changes.flatMap((change) => change.recipient.recipientKind === "team" ? [change.recipient.teamId] : []))].toSorted() : [];
|
|
7817
|
+
const commit = async (index) => {
|
|
7818
|
+
const teamId = teamIds[index];
|
|
7819
|
+
if (!teamId) return commitRecipientPolicyEdges(store.db, request ?? body);
|
|
7820
|
+
return serializeRecipientPolicyTeamMutation(store.db, teamId, () => commit(index + 1));
|
|
7821
|
+
};
|
|
7822
|
+
const result = await commit(0);
|
|
7068
7823
|
if (result.status === "invalid") return c.json(result, 400);
|
|
7069
7824
|
if (result.status === "not_found") return c.json(result, 404);
|
|
7070
7825
|
if (result.status === "stale" || result.status === "conflict") return c.json(result, 409);
|
|
@@ -7187,7 +7942,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7187
7942
|
const parseReviewRequest = (value) => {
|
|
7188
7943
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
7189
7944
|
const body = value;
|
|
7190
|
-
const allowedKeys = new Set([
|
|
7945
|
+
const allowedKeys = /* @__PURE__ */ new Set([
|
|
7191
7946
|
"reviewItemId",
|
|
7192
7947
|
"sourceFingerprint",
|
|
7193
7948
|
"decision",
|
|
@@ -7203,7 +7958,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7203
7958
|
};
|
|
7204
7959
|
app.post("/api/sync/recipient-policy/v1/review/resolve", async (c) => {
|
|
7205
7960
|
const store = getStore();
|
|
7206
|
-
const
|
|
7961
|
+
const body = await parseViewerJsonBody(c);
|
|
7962
|
+
const request = parseReviewRequest(body);
|
|
7207
7963
|
if (!request) return c.json({ error: "request_invalid" }, 400);
|
|
7208
7964
|
ensureLocalActorRecord(store);
|
|
7209
7965
|
const result = resolveRecipientPolicyReview(store.db, {
|
|
@@ -7518,9 +8274,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7518
8274
|
const store = getStore();
|
|
7519
8275
|
const body = await parseViewerJsonBody(c);
|
|
7520
8276
|
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
8277
|
+
let releasePublicationMutation;
|
|
7521
8278
|
try {
|
|
7522
8279
|
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
7523
8280
|
const mappingInput = parseViewerProjectMappingInput(body);
|
|
8281
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
7524
8282
|
const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput);
|
|
7525
8283
|
if (analysis.requested_workspace_identity?.startsWith("unmapped:")) return c.json({
|
|
7526
8284
|
error: "unmapped_project_local_only",
|
|
@@ -7550,12 +8308,15 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7550
8308
|
});
|
|
7551
8309
|
} catch (error) {
|
|
7552
8310
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
8311
|
+
} finally {
|
|
8312
|
+
releasePublicationMutation?.();
|
|
7553
8313
|
}
|
|
7554
8314
|
});
|
|
7555
8315
|
app.put("/api/sync/sharing-domains/project-mappings/bulk", async (c) => {
|
|
7556
8316
|
const store = getStore();
|
|
7557
8317
|
const body = await parseViewerJsonBody(c);
|
|
7558
8318
|
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
8319
|
+
let releasePublicationMutation;
|
|
7559
8320
|
try {
|
|
7560
8321
|
const rawMappings = body.mappings;
|
|
7561
8322
|
if (!Array.isArray(rawMappings) || rawMappings.length === 0) return c.json({ error: "mappings must be a non-empty array" }, 400);
|
|
@@ -7563,6 +8324,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7563
8324
|
if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
|
|
7564
8325
|
return parseViewerProjectMappingInput(raw);
|
|
7565
8326
|
});
|
|
8327
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
7566
8328
|
const analyses = mappingInputs.map((mappingInput) => analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput));
|
|
7567
8329
|
const unmapped = analyses.find((analysis) => analysis.requested_workspace_identity?.startsWith("unmapped:"));
|
|
7568
8330
|
if (unmapped) return c.json({
|
|
@@ -7593,12 +8355,16 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7593
8355
|
});
|
|
7594
8356
|
} catch (error) {
|
|
7595
8357
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
8358
|
+
} finally {
|
|
8359
|
+
releasePublicationMutation?.();
|
|
7596
8360
|
}
|
|
7597
8361
|
});
|
|
7598
|
-
app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => {
|
|
8362
|
+
app.delete("/api/sync/sharing-domains/project-mappings/:id", async (c) => {
|
|
7599
8363
|
const store = getStore();
|
|
7600
8364
|
const id = Number(c.req.param("id"));
|
|
8365
|
+
let releasePublicationMutation;
|
|
7601
8366
|
try {
|
|
8367
|
+
releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
|
|
7602
8368
|
const deleted = deleteProjectScopeSettingsMapping(store.db, id);
|
|
7603
8369
|
return c.json({
|
|
7604
8370
|
ok: true,
|
|
@@ -7606,6 +8372,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7606
8372
|
});
|
|
7607
8373
|
} catch (error) {
|
|
7608
8374
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
8375
|
+
} finally {
|
|
8376
|
+
releasePublicationMutation?.();
|
|
7609
8377
|
}
|
|
7610
8378
|
});
|
|
7611
8379
|
app.post("/api/sync/peers/identity", async (c) => {
|
|
@@ -7734,6 +8502,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7734
8502
|
if (!secondaryActorId) return c.json({ error: "secondary_actor_id required" }, 400);
|
|
7735
8503
|
if (primaryActorId === secondaryActorId) return c.json({ error: "actor ids must differ" }, 400);
|
|
7736
8504
|
const d = drizzle(store.db, { schema });
|
|
8505
|
+
const releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, [primaryActorId, secondaryActorId]);
|
|
8506
|
+
queueMicrotask(releaseActorMutations);
|
|
7737
8507
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7738
8508
|
const result = store.db.transaction(() => {
|
|
7739
8509
|
const primary = d.select().from(schema.actors).where(eq(schema.actors.actor_id, primaryActorId)).get();
|
|
@@ -7884,6 +8654,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
7884
8654
|
const actorId = String(body.actor_id ?? "").trim();
|
|
7885
8655
|
if (!actorId) return c.json({ error: "actor_id required" }, 400);
|
|
7886
8656
|
if (actorId === store.actorId) return c.json({ error: "cannot deactivate this device's own local actor" }, 409);
|
|
8657
|
+
const releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, [actorId]);
|
|
8658
|
+
queueMicrotask(releaseActorMutations);
|
|
7887
8659
|
const d = drizzle(store.db, { schema });
|
|
7888
8660
|
const actor = d.select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
|
|
7889
8661
|
if (!actor) return c.json({ error: "actor not found" }, 404);
|
|
@@ -9114,7 +9886,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9114
9886
|
const publicKey = String(body.peer_public_key ?? "").trim();
|
|
9115
9887
|
const name = String(body.name ?? "").trim() || null;
|
|
9116
9888
|
const requestedFingerprint = String(body.fingerprint ?? "").trim();
|
|
9117
|
-
const
|
|
9889
|
+
const addressesInput = Array.isArray(body.peer_addresses) ? body.peer_addresses : [];
|
|
9890
|
+
const addresses = mergeAddresses([], addressesInput.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0));
|
|
9118
9891
|
if (!peerDeviceId || !publicKey) return c.json({ error: "peer_device_id_and_public_key_required" }, 400);
|
|
9119
9892
|
const fingerprint = requestedFingerprint || fingerprintPublicKey(publicKey);
|
|
9120
9893
|
if (fingerprintPublicKey(publicKey) !== fingerprint) return c.json({ error: "fingerprint_mismatch" }, 400);
|
|
@@ -9203,7 +9976,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
9203
9976
|
error: "coordinator_not_configured",
|
|
9204
9977
|
status
|
|
9205
9978
|
}, 400);
|
|
9206
|
-
const
|
|
9979
|
+
const store = getStore();
|
|
9980
|
+
const existing = getCoordinatorGroupPreference(store.db, coordinatorId, groupId);
|
|
9207
9981
|
if (existing) return c.json({
|
|
9208
9982
|
preferences: existing,
|
|
9209
9983
|
status
|
|
@@ -9389,12 +10163,13 @@ function updateStatusRoutes(options = {}) {
|
|
|
9389
10163
|
const app = new Hono();
|
|
9390
10164
|
app.get("/api/update-status", async (c) => {
|
|
9391
10165
|
try {
|
|
10166
|
+
const installKind = detectInstallKind({
|
|
10167
|
+
entryPath: process.argv[1] ?? "",
|
|
10168
|
+
env: process.env
|
|
10169
|
+
});
|
|
9392
10170
|
const status = await getUpdateStatus$1({
|
|
9393
10171
|
currentVersion: VERSION$1,
|
|
9394
|
-
installKind
|
|
9395
|
-
entryPath: process.argv[1] ?? "",
|
|
9396
|
-
env: process.env
|
|
9397
|
-
})
|
|
10172
|
+
installKind
|
|
9398
10173
|
});
|
|
9399
10174
|
return c.json(status);
|
|
9400
10175
|
} catch {
|
|
@@ -9455,6 +10230,8 @@ function createApp(opts) {
|
|
|
9455
10230
|
app.route("/", teamSetupRoutes({
|
|
9456
10231
|
getStore: storeFactory,
|
|
9457
10232
|
loadLegacyTeamConfiguredGroupSnapshots: opts?.loadLegacyTeamConfiguredGroupSnapshots,
|
|
10233
|
+
completionDependencies: opts?.teamSetupCompletionDependencies,
|
|
10234
|
+
readCoordinatorConfig: opts?.readCoordinatorConfig,
|
|
9458
10235
|
registerSummaryInvalidator: (invalidate) => {
|
|
9459
10236
|
invalidateTeamSetupSummary = invalidate;
|
|
9460
10237
|
}
|