@codemem/server 0.42.0 → 0.43.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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +889 -18
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +7 -3
- package/dist/middleware.d.ts.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/dist/routes/team-setup.d.ts +171 -0
- package/dist/routes/team-setup.d.ts.map +1 -0
- package/package.json +2 -2
- package/static/app.js +57 -57
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/index.html +134 -0
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, 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, 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, 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, diagnoseStalePeerReceivedRows, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inviteTokenDigest, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, 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, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, rejectInboundScopeFailures, requestJson, resolveDbPath, resolveProject, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, summarizeInboundScopeRejections, supportsSyncFeature, syncScopeResetRequiredPayload, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
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, 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, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, finishLegacyTeamSetupActivation, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, 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, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, requestJson, resolveDbPath, resolveProject, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, summarizeInboundScopeRejections, supportsSyncFeature, syncScopeResetRequiredPayload, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, 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";
|
|
@@ -28,7 +28,8 @@ function isLoopbackOrigin(origin) {
|
|
|
28
28
|
}
|
|
29
29
|
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
30
30
|
if (url.username || url.password) return false;
|
|
31
|
-
|
|
31
|
+
const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
|
|
32
|
+
return LOOPBACK_HOSTS.has(hostname);
|
|
32
33
|
}
|
|
33
34
|
/** HTTP methods that mutate state and require origin validation. */
|
|
34
35
|
var UNSAFE_METHODS = new Set([
|
|
@@ -61,7 +62,8 @@ function isUnsafeMissingOrigin(c) {
|
|
|
61
62
|
*
|
|
62
63
|
* Ports Python's `reject_cross_origin(missing_origin_policy="reject_if_unsafe")`:
|
|
63
64
|
*
|
|
64
|
-
* - GET/HEAD/OPTIONS: allowed from any origin
|
|
65
|
+
* - GET/HEAD/OPTIONS: allowed from any origin unless the caller marks a
|
|
66
|
+
* side-effecting GET path prefix as unsafe.
|
|
65
67
|
* - POST/DELETE/PATCH/PUT:
|
|
66
68
|
* - Origin present + loopback → allowed (browser on localhost)
|
|
67
69
|
* - Origin present + non-loopback → rejected 403
|
|
@@ -72,20 +74,21 @@ function isUnsafeMissingOrigin(c) {
|
|
|
72
74
|
* Access-Control-Allow-Origin is set — the browser doesn't need it.
|
|
73
75
|
* For valid loopback origins, ACAO is echoed back.
|
|
74
76
|
*/
|
|
75
|
-
function originGuard() {
|
|
77
|
+
function originGuard(options) {
|
|
76
78
|
return createMiddleware(async (c, next) => {
|
|
77
79
|
const origin = c.req.header("Origin");
|
|
78
80
|
const method = c.req.method;
|
|
79
|
-
|
|
81
|
+
const unsafeGet = (method === "GET" || method === "HEAD") && options?.unsafeGetPathPrefixes?.some((prefix) => c.req.path.startsWith(prefix));
|
|
82
|
+
if (UNSAFE_METHODS.has(method) || unsafeGet) {
|
|
80
83
|
if (origin) {
|
|
81
84
|
if (!isLoopbackOrigin(origin)) return c.json({ error: "forbidden" }, 403);
|
|
82
85
|
c.header("Access-Control-Allow-Origin", origin);
|
|
83
|
-
c.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
86
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
84
87
|
c.header("Access-Control-Allow-Headers", "Content-Type");
|
|
85
88
|
} else if (isUnsafeMissingOrigin(c)) return c.json({ error: "forbidden" }, 403);
|
|
86
89
|
} else if (origin && isLoopbackOrigin(origin)) {
|
|
87
90
|
c.header("Access-Control-Allow-Origin", origin);
|
|
88
|
-
c.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
91
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
89
92
|
c.header("Access-Control-Allow-Headers", "Content-Type");
|
|
90
93
|
}
|
|
91
94
|
await next();
|
|
@@ -104,7 +107,7 @@ function preflightHandler() {
|
|
|
104
107
|
const origin = c.req.header("Origin");
|
|
105
108
|
if (origin && isLoopbackOrigin(origin)) {
|
|
106
109
|
c.header("Access-Control-Allow-Origin", origin);
|
|
107
|
-
c.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
110
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
108
111
|
c.header("Access-Control-Allow-Headers", "Content-Type");
|
|
109
112
|
c.header("Access-Control-Max-Age", "86400");
|
|
110
113
|
return c.body(null, 204);
|
|
@@ -3124,13 +3127,13 @@ var TERMINAL_RECONCILIATION_ERRORS = new Set([
|
|
|
3124
3127
|
"operation_not_found",
|
|
3125
3128
|
...PROJECT_INVITE_OWNER_CONFLICT_ERRORS
|
|
3126
3129
|
]);
|
|
3127
|
-
function errorStatus(error) {
|
|
3130
|
+
function errorStatus$1(error) {
|
|
3128
3131
|
const value = error && typeof error === "object" ? error.status : null;
|
|
3129
3132
|
return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
|
|
3130
3133
|
}
|
|
3131
3134
|
function projectInviteOwnerErrorResponse(error, fallback) {
|
|
3132
3135
|
const message = error instanceof Error ? error.message : "";
|
|
3133
|
-
const status = errorStatus(error);
|
|
3136
|
+
const status = errorStatus$1(error);
|
|
3134
3137
|
if (message === "operation_not_found" && (status == null || status === 404)) return {
|
|
3135
3138
|
code: message,
|
|
3136
3139
|
status: 404
|
|
@@ -3152,7 +3155,7 @@ function projectInviteOwnerErrorResponse(error, fallback) {
|
|
|
3152
3155
|
function isRetryableReconciliationError(error) {
|
|
3153
3156
|
const message = error instanceof Error ? error.message : String(error);
|
|
3154
3157
|
if (TERMINAL_RECONCILIATION_ERRORS.has(message)) return false;
|
|
3155
|
-
const status = errorStatus(error);
|
|
3158
|
+
const status = errorStatus$1(error);
|
|
3156
3159
|
return status == null || status === 408 || status === 429 || status >= 500;
|
|
3157
3160
|
}
|
|
3158
3161
|
function safeReconciliationErrorCode(error) {
|
|
@@ -4409,7 +4412,8 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
4409
4412
|
const addressesJson = JSON.stringify(mergeAddresses(nextAddresses, existingAddresses));
|
|
4410
4413
|
await createCoordinatorReciprocalApproval(store, config, {
|
|
4411
4414
|
groupId,
|
|
4412
|
-
requestedDeviceId: input.peerDeviceId
|
|
4415
|
+
requestedDeviceId: input.peerDeviceId,
|
|
4416
|
+
...input.expectedIncomingRequestId ? { expectedIncomingRequestId: input.expectedIncomingRequestId } : {}
|
|
4413
4417
|
});
|
|
4414
4418
|
const coordinatorUrl = config.syncCoordinatorUrl || null;
|
|
4415
4419
|
let scopeInclude;
|
|
@@ -5973,10 +5977,18 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
5973
5977
|
if (!value || typeof value !== "object" || Array.isArray(value)) return c.json({ error: "request_invalid" }, 400);
|
|
5974
5978
|
const body = value;
|
|
5975
5979
|
if (Object.keys(body).some((key) => key !== "dryRun") || Object.hasOwn(body, "dryRun") && typeof body.dryRun !== "boolean") return c.json({ error: "request_invalid" }, 400);
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
+
try {
|
|
5981
|
+
return c.json(migrateRecipientPolicyIntent(store.db, {
|
|
5982
|
+
localActorId: store.actorId,
|
|
5983
|
+
localDeviceId: store.deviceId
|
|
5984
|
+
}, { dryRun: body.dryRun === true }));
|
|
5985
|
+
} catch (error) {
|
|
5986
|
+
if (isSqliteBusy(error)) {
|
|
5987
|
+
c.header("Retry-After", "1");
|
|
5988
|
+
return c.json({ error: "migration_busy" }, 503);
|
|
5989
|
+
}
|
|
5990
|
+
return c.json({ error: "migration_failed" }, 500);
|
|
5991
|
+
}
|
|
5980
5992
|
});
|
|
5981
5993
|
const parseReviewRequest = (value) => {
|
|
5982
5994
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
@@ -6718,11 +6730,18 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6718
6730
|
}
|
|
6719
6731
|
const peerDeviceId = String(body.peer_device_id ?? "").trim();
|
|
6720
6732
|
const fingerprint = String(body.fingerprint ?? "").trim();
|
|
6733
|
+
const expectedGroupId = String(body.expected_group_id ?? "").trim();
|
|
6734
|
+
const hasExpectedIncomingRequestId = Object.hasOwn(body, "expected_incoming_request_id");
|
|
6735
|
+
const expectedIncomingRequestId = String(body.expected_incoming_request_id ?? "").trim();
|
|
6721
6736
|
if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
|
|
6737
|
+
if (hasExpectedIncomingRequestId && !expectedIncomingRequestId) return c.json({ error: "expected_incoming_request_id must not be empty" }, 400);
|
|
6738
|
+
if (expectedIncomingRequestId && !expectedGroupId) return c.json({ error: "expected_group_id required" }, 400);
|
|
6722
6739
|
try {
|
|
6723
6740
|
const result = await acceptDiscoveredPeer(store, {
|
|
6724
6741
|
peerDeviceId,
|
|
6725
|
-
fingerprint: fingerprint || void 0
|
|
6742
|
+
fingerprint: fingerprint || void 0,
|
|
6743
|
+
expectedGroupId: expectedGroupId || void 0,
|
|
6744
|
+
expectedIncomingRequestId: expectedIncomingRequestId || void 0
|
|
6726
6745
|
});
|
|
6727
6746
|
if (!result.ok) return c.json({
|
|
6728
6747
|
error: result.error,
|
|
@@ -6738,6 +6757,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
6738
6757
|
needs_scope_review: true
|
|
6739
6758
|
});
|
|
6740
6759
|
} catch (error) {
|
|
6760
|
+
if (error instanceof CoordinatorReciprocalApprovalRequestChangedError) return c.json({
|
|
6761
|
+
error: error.code,
|
|
6762
|
+
detail: "This approval request changed before it was submitted. Refresh sync status and review the current request."
|
|
6763
|
+
}, { status: 409 });
|
|
6741
6764
|
return c.json({
|
|
6742
6765
|
error: "coordinator_lookup_failed",
|
|
6743
6766
|
detail: error instanceof Error ? error.message : String(error)
|
|
@@ -8166,6 +8189,850 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
|
|
|
8166
8189
|
return app;
|
|
8167
8190
|
}
|
|
8168
8191
|
//#endregion
|
|
8192
|
+
//#region src/routes/team-setup.ts
|
|
8193
|
+
var TEAM_SETUP_VERSION = 1;
|
|
8194
|
+
var MAX_CONFIGURED_GROUPS = 25;
|
|
8195
|
+
var MAX_DEVICES = 500;
|
|
8196
|
+
var MAX_PROJECTS = 500;
|
|
8197
|
+
var MAX_ACCESS_DELTA_ENTRIES = 1e4;
|
|
8198
|
+
var MAX_IDENTITY_CHOICES = 500;
|
|
8199
|
+
var MAX_COMPLETED_IDENTITY_CHOICES = MAX_DEVICES * 4;
|
|
8200
|
+
var MAX_PROJECT_MAPPING_CHOICES = 500;
|
|
8201
|
+
var MAX_TOTAL_PROJECT_MAPPING_CHOICES = 1e4;
|
|
8202
|
+
var MAX_PROJECT_MAPPING_SCAN_ROWS = 1e4;
|
|
8203
|
+
var MAX_PROJECT_MAPPING_METADATA_ROWS = 1e4;
|
|
8204
|
+
var MAX_MUTATION_BODY_BYTES = 8192;
|
|
8205
|
+
var SUMMARY_SNAPSHOT_CACHE_TTL_MS = 3e4;
|
|
8206
|
+
var TEAM_SETUP_ROUTE_PREFIX = "/api/sync/team-setup/v1";
|
|
8207
|
+
var CANDIDATE_REF_PATTERN = /^legacy-team-candidate:[0-9a-f]{32}$/u;
|
|
8208
|
+
var DEVICE_REF_PATTERN = /^legacy-team-device-ref-v1:[0-9a-f]{64}$/u;
|
|
8209
|
+
var PROJECT_REF_PATTERN = /^legacy-team-project-ref-v1:[0-9a-f]{64}$/u;
|
|
8210
|
+
var IDENTITY_REF_PATTERN = /^legacy-team-viewer-identity-ref-v1:[0-9a-f]{64}$/u;
|
|
8211
|
+
var RESOLVED_PROJECT_REF_PATTERN = /^legacy-team-resolved-project-ref-v1:[0-9a-f]{64}$/u;
|
|
8212
|
+
var ATTEMPT_ID_PATTERN = /^legacy-team-attempt:[0-9a-f-]{36}$/u;
|
|
8213
|
+
var FINISH_DIGEST_PATTERN = /^legacy-team-activation-finish-v1:[0-9a-f]{64}$/u;
|
|
8214
|
+
var ACCESS_DELTA_DIGEST_PATTERN = /^legacy-team-access-delta:[0-9a-f]{64}$/u;
|
|
8215
|
+
var VIEWER_ACCESS_DELTA_DIGEST_PATTERN = /^legacy-team-viewer-access-delta-v1:[0-9a-f]{64}$/u;
|
|
8216
|
+
var defaultSnapshotLoaderDependencies = {
|
|
8217
|
+
readConfig: readCoordinatorSyncConfig,
|
|
8218
|
+
listGroups: coordinatorListGroupsAction,
|
|
8219
|
+
listDevices: coordinatorListDevicesAction
|
|
8220
|
+
};
|
|
8221
|
+
function safeCoordinatorError() {
|
|
8222
|
+
return /* @__PURE__ */ new Error("team_setup_roster_unavailable");
|
|
8223
|
+
}
|
|
8224
|
+
function isCoordinatorRosterTooLargeError(error) {
|
|
8225
|
+
return error instanceof Error && error.message === "coordinator_response_too_large";
|
|
8226
|
+
}
|
|
8227
|
+
function configuredGroupIds(groups) {
|
|
8228
|
+
const unique = [...new Set(groups.map((group) => group.trim()).filter(Boolean))];
|
|
8229
|
+
if (unique.length > MAX_CONFIGURED_GROUPS) throw safeCoordinatorError();
|
|
8230
|
+
return unique;
|
|
8231
|
+
}
|
|
8232
|
+
async function loadConfiguredLegacyTeamGroupSnapshotsWith(dependencies, options = {}) {
|
|
8233
|
+
let config;
|
|
8234
|
+
try {
|
|
8235
|
+
config = dependencies.readConfig();
|
|
8236
|
+
} catch {
|
|
8237
|
+
throw safeCoordinatorError();
|
|
8238
|
+
}
|
|
8239
|
+
const groupIds = configuredGroupIds(config.syncCoordinatorGroups);
|
|
8240
|
+
const hasUrl = Boolean(config.syncCoordinatorUrl);
|
|
8241
|
+
const hasSecret = Boolean(config.syncCoordinatorAdminSecret);
|
|
8242
|
+
if (!hasUrl && !hasSecret && groupIds.length === 0) return [];
|
|
8243
|
+
if (!hasUrl || !hasSecret || groupIds.length === 0) throw safeCoordinatorError();
|
|
8244
|
+
try {
|
|
8245
|
+
const remoteUrl = buildBaseUrl(config.syncCoordinatorUrl);
|
|
8246
|
+
const coordinatorId = remoteUrl;
|
|
8247
|
+
const timeoutS = Math.max(1, config.syncCoordinatorTimeoutS);
|
|
8248
|
+
const requestedGroupIds = options.candidateRef ? groupIds.filter((groupId) => legacyTeamCandidateId(coordinatorId, groupId) === options.candidateRef) : groupIds;
|
|
8249
|
+
const groups = await dependencies.listGroups({
|
|
8250
|
+
remoteUrl,
|
|
8251
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
8252
|
+
includeArchived: false,
|
|
8253
|
+
timeoutS
|
|
8254
|
+
});
|
|
8255
|
+
const groupById = new Map(groups.map((group) => [group.group_id, group]));
|
|
8256
|
+
return (await Promise.all(requestedGroupIds.map(async (groupId) => {
|
|
8257
|
+
const group = groupById.get(groupId);
|
|
8258
|
+
if (!group || group.group_id !== groupId || group.display_name !== null && typeof group.display_name !== "string" || group.archived_at != null) throw safeCoordinatorError();
|
|
8259
|
+
let devices;
|
|
8260
|
+
try {
|
|
8261
|
+
devices = await dependencies.listDevices({
|
|
8262
|
+
groupId,
|
|
8263
|
+
includeDisabled: true,
|
|
8264
|
+
remoteUrl,
|
|
8265
|
+
adminSecret: config.syncCoordinatorAdminSecret,
|
|
8266
|
+
timeoutS
|
|
8267
|
+
});
|
|
8268
|
+
} catch (error) {
|
|
8269
|
+
if (isCoordinatorRosterTooLargeError(error)) {
|
|
8270
|
+
if (options.candidateRef) throw new Error("legacy_team_setup_roster_too_large");
|
|
8271
|
+
return null;
|
|
8272
|
+
}
|
|
8273
|
+
throw error;
|
|
8274
|
+
}
|
|
8275
|
+
if (devices.length > MAX_DEVICES) {
|
|
8276
|
+
if (options.candidateRef) throw new Error("legacy_team_setup_roster_too_large");
|
|
8277
|
+
return null;
|
|
8278
|
+
}
|
|
8279
|
+
if (devices.some((device) => fingerprintPublicKey(device.public_key) !== device.fingerprint || device.enabled !== 0 && device.enabled !== 1)) throw safeCoordinatorError();
|
|
8280
|
+
return {
|
|
8281
|
+
coordinatorId,
|
|
8282
|
+
groupId,
|
|
8283
|
+
displayName: group.display_name ?? "Legacy Team",
|
|
8284
|
+
devices: devices.map((device) => ({
|
|
8285
|
+
deviceId: device.device_id,
|
|
8286
|
+
fingerprint: device.fingerprint,
|
|
8287
|
+
displayName: device.display_name ?? "Device",
|
|
8288
|
+
enabled: device.enabled === 1,
|
|
8289
|
+
labelRedactionIds: [device.identity_id ?? "", device.public_key].filter(Boolean)
|
|
8290
|
+
}))
|
|
8291
|
+
};
|
|
8292
|
+
}))).filter((snapshot) => snapshot !== null);
|
|
8293
|
+
} catch (error) {
|
|
8294
|
+
if (error instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
|
|
8295
|
+
throw safeCoordinatorError();
|
|
8296
|
+
}
|
|
8297
|
+
}
|
|
8298
|
+
async function loadConfiguredLegacyTeamGroupSnapshots(options = {}) {
|
|
8299
|
+
return loadConfiguredLegacyTeamGroupSnapshotsWith(defaultSnapshotLoaderDependencies, options);
|
|
8300
|
+
}
|
|
8301
|
+
function requireBoundedSnapshots(groups) {
|
|
8302
|
+
if (groups.length > MAX_CONFIGURED_GROUPS) throw new Error("legacy_team_setup_roster_too_large");
|
|
8303
|
+
}
|
|
8304
|
+
function projectionOptions(store) {
|
|
8305
|
+
return {
|
|
8306
|
+
localActorId: store.actorId,
|
|
8307
|
+
localDeviceId: store.deviceId
|
|
8308
|
+
};
|
|
8309
|
+
}
|
|
8310
|
+
function discoverCandidates(store, groups) {
|
|
8311
|
+
requireBoundedSnapshots(groups);
|
|
8312
|
+
return discoverLegacyTeamCandidates(store.db, {
|
|
8313
|
+
projection: projectionOptions(store),
|
|
8314
|
+
groups
|
|
8315
|
+
});
|
|
8316
|
+
}
|
|
8317
|
+
function candidateSummary(candidate) {
|
|
8318
|
+
return {
|
|
8319
|
+
candidateRef: candidate.candidateRef,
|
|
8320
|
+
displayName: candidate.displayName,
|
|
8321
|
+
status: candidate.status,
|
|
8322
|
+
deviceCount: candidate.deviceCount,
|
|
8323
|
+
projectCount: candidate.projectCount,
|
|
8324
|
+
unresolvedDeviceCount: candidate.unresolvedDeviceCount,
|
|
8325
|
+
unresolvedProjectCount: candidate.unresolvedProjectCount
|
|
8326
|
+
};
|
|
8327
|
+
}
|
|
8328
|
+
function candidateSummaryForDraft(candidate, draft) {
|
|
8329
|
+
return {
|
|
8330
|
+
...candidateSummary(candidate),
|
|
8331
|
+
status: draft.state === "completed" ? "ready" : draft.state,
|
|
8332
|
+
unresolvedDeviceCount: draft.unresolvedDeviceCount,
|
|
8333
|
+
unresolvedProjectCount: draft.unresolvedProjectCount
|
|
8334
|
+
};
|
|
8335
|
+
}
|
|
8336
|
+
function completedCandidateFromDraft(draft) {
|
|
8337
|
+
return {
|
|
8338
|
+
candidateRef: draft.candidateRef,
|
|
8339
|
+
displayName: draft.displayName,
|
|
8340
|
+
status: "ready",
|
|
8341
|
+
deviceCount: draft.devices.length,
|
|
8342
|
+
projectCount: draft.projects.length,
|
|
8343
|
+
unresolvedDeviceCount: draft.unresolvedDeviceCount,
|
|
8344
|
+
unresolvedProjectCount: draft.unresolvedProjectCount
|
|
8345
|
+
};
|
|
8346
|
+
}
|
|
8347
|
+
function identityRef(candidateRef, identityId) {
|
|
8348
|
+
return identityId ? recipientPolicyDigest("legacy-team-viewer-identity-ref-v1", [candidateRef, identityId]) : null;
|
|
8349
|
+
}
|
|
8350
|
+
function requiredIdentityRef(candidateRef, identityId) {
|
|
8351
|
+
return recipientPolicyDigest("legacy-team-viewer-identity-ref-v1", [candidateRef, identityId]);
|
|
8352
|
+
}
|
|
8353
|
+
function teamRef(candidateRef, teamId) {
|
|
8354
|
+
return recipientPolicyDigest("legacy-team-viewer-team-ref-v1", [candidateRef, teamId]);
|
|
8355
|
+
}
|
|
8356
|
+
function resolvedProjectRef(projectRef, projectIdentity) {
|
|
8357
|
+
return projectIdentity ? legacyTeamResolvedProjectRef(projectRef, projectIdentity) : null;
|
|
8358
|
+
}
|
|
8359
|
+
function viewerSafeAccessDelta(candidateRef, delta, labels) {
|
|
8360
|
+
const fallbackLabels = /* @__PURE__ */ new Map();
|
|
8361
|
+
const fallbackLabelCounts = /* @__PURE__ */ new Map();
|
|
8362
|
+
const reservedLabels = new Set([
|
|
8363
|
+
labels?.teamDisplayName,
|
|
8364
|
+
...labels?.devices.map((device) => device.displayName) ?? [],
|
|
8365
|
+
...labels?.projects.flatMap((project) => [project.displayName, ...project.mappingChoices.map((choice) => choice.displayName)]) ?? [],
|
|
8366
|
+
...labels?.identityChoices.map((identity) => identity.displayName) ?? []
|
|
8367
|
+
].filter((label) => Boolean(label)).map((label) => normalizeChoiceLabelText(label).toLowerCase()));
|
|
8368
|
+
const fallbackLabel = (kind, ref) => {
|
|
8369
|
+
const key = `${kind}:${ref}`;
|
|
8370
|
+
const existing = fallbackLabels.get(key);
|
|
8371
|
+
if (existing) return existing;
|
|
8372
|
+
let index = (fallbackLabelCounts.get(kind) ?? 0) + 1;
|
|
8373
|
+
let label = `${kind} outside this setup (${index})`;
|
|
8374
|
+
while (reservedLabels.has(normalizeChoiceLabelText(label).toLowerCase())) {
|
|
8375
|
+
index += 1;
|
|
8376
|
+
label = `${kind} outside this setup (${index})`;
|
|
8377
|
+
}
|
|
8378
|
+
fallbackLabelCounts.set(kind, index);
|
|
8379
|
+
reservedLabels.add(normalizeChoiceLabelText(label).toLowerCase());
|
|
8380
|
+
fallbackLabels.set(key, label);
|
|
8381
|
+
return label;
|
|
8382
|
+
};
|
|
8383
|
+
const projectsByRef = new Map(labels?.projects.map((project) => [project.projectRef, project]));
|
|
8384
|
+
const projectsByCanonicalRef = new Map(labels?.projects.flatMap((project) => project.canonicalProjectRef ? [[project.canonicalProjectRef, project]] : []));
|
|
8385
|
+
const identitiesByRef = new Map(labels?.identityChoices.map((identity) => [identity.identityRef, identity]));
|
|
8386
|
+
const devicesByRef = new Map(labels?.devices.map((device) => [device.deviceRef, device]));
|
|
8387
|
+
const resolvedDisplayName = (projectRef, ref) => {
|
|
8388
|
+
if (!ref) return null;
|
|
8389
|
+
const project = projectsByRef.get(projectRef);
|
|
8390
|
+
const choice = project?.mappingChoices.find((item) => item.resolvedProjectRef === ref);
|
|
8391
|
+
if (choice) return choice.displayName;
|
|
8392
|
+
if (project?.resolvedProjectRef === ref && project.resolution === "deterministic") return `${project.displayName} (automatic match)`;
|
|
8393
|
+
return fallbackLabel("Project", ref);
|
|
8394
|
+
};
|
|
8395
|
+
return {
|
|
8396
|
+
teamChanges: delta.teamChanges.map((change) => ({
|
|
8397
|
+
teamRef: teamRef(candidateRef, change.teamId),
|
|
8398
|
+
teamDisplayName: labels?.teamDisplayName ?? fallbackLabel("Team", teamRef(candidateRef, change.teamId)),
|
|
8399
|
+
change: change.change,
|
|
8400
|
+
fromDeviceEligibilityMode: change.fromDeviceEligibilityMode,
|
|
8401
|
+
toDeviceEligibilityMode: change.toDeviceEligibilityMode
|
|
8402
|
+
})),
|
|
8403
|
+
membershipChanges: delta.membershipChanges.map((change) => {
|
|
8404
|
+
const identityRef = requiredIdentityRef(candidateRef, change.identityId);
|
|
8405
|
+
const membershipTeamRef = teamRef(candidateRef, change.teamId);
|
|
8406
|
+
return {
|
|
8407
|
+
teamRef: membershipTeamRef,
|
|
8408
|
+
teamDisplayName: labels?.teamDisplayName ?? fallbackLabel("Team", membershipTeamRef),
|
|
8409
|
+
identityRef,
|
|
8410
|
+
identityDisplayName: identitiesByRef.get(identityRef)?.displayName ?? fallbackLabel("Person", identityRef),
|
|
8411
|
+
change: change.change
|
|
8412
|
+
};
|
|
8413
|
+
}),
|
|
8414
|
+
projectChanges: delta.projectChanges.map((change) => {
|
|
8415
|
+
const fromRef = resolvedProjectRef(change.projectRef, change.fromProjectIdentity);
|
|
8416
|
+
const toRef = resolvedProjectRef(change.projectRef, change.toProjectIdentity);
|
|
8417
|
+
return {
|
|
8418
|
+
projectRef: change.projectRef,
|
|
8419
|
+
projectDisplayName: projectsByRef.get(change.projectRef)?.displayName ?? fallbackLabel("Project", change.projectRef),
|
|
8420
|
+
fromResolvedProjectRef: fromRef,
|
|
8421
|
+
fromResolvedProjectDisplayName: resolvedDisplayName(change.projectRef, fromRef),
|
|
8422
|
+
toResolvedProjectRef: toRef,
|
|
8423
|
+
toResolvedProjectDisplayName: resolvedDisplayName(change.projectRef, toRef),
|
|
8424
|
+
change: change.change
|
|
8425
|
+
};
|
|
8426
|
+
}),
|
|
8427
|
+
recipientChanges: delta.recipientChanges.map((change) => {
|
|
8428
|
+
const canonicalProjectRef = legacyTeamCanonicalProjectRef(candidateRef, change.canonicalProjectIdentity);
|
|
8429
|
+
const recipientRef = teamRef(candidateRef, change.recipientId);
|
|
8430
|
+
return {
|
|
8431
|
+
canonicalProjectRef,
|
|
8432
|
+
canonicalProjectDisplayName: projectsByCanonicalRef.get(canonicalProjectRef)?.displayName ?? fallbackLabel("Project", canonicalProjectRef),
|
|
8433
|
+
recipientKind: change.recipientKind,
|
|
8434
|
+
recipientRef,
|
|
8435
|
+
recipientDisplayName: labels?.teamDisplayName ?? fallbackLabel("Team", recipientRef),
|
|
8436
|
+
change: change.change
|
|
8437
|
+
};
|
|
8438
|
+
}),
|
|
8439
|
+
deviceAccessChanges: delta.deviceAccessChanges.map((change) => {
|
|
8440
|
+
const canonicalProjectRef = legacyTeamCanonicalProjectRef(candidateRef, change.canonicalProjectIdentity);
|
|
8441
|
+
const deviceRef = legacyTeamDeviceRef(candidateRef, change.deviceId);
|
|
8442
|
+
return {
|
|
8443
|
+
canonicalProjectRef,
|
|
8444
|
+
canonicalProjectDisplayName: projectsByCanonicalRef.get(canonicalProjectRef)?.displayName ?? fallbackLabel("Project", canonicalProjectRef),
|
|
8445
|
+
deviceRef,
|
|
8446
|
+
deviceDisplayName: devicesByRef.get(deviceRef)?.displayName ?? fallbackLabel("Device", deviceRef),
|
|
8447
|
+
change: change.change
|
|
8448
|
+
};
|
|
8449
|
+
})
|
|
8450
|
+
};
|
|
8451
|
+
}
|
|
8452
|
+
function viewerAccessDeltaDigest(delta) {
|
|
8453
|
+
return recipientPolicyDigest("legacy-team-viewer-access-delta-v1", delta);
|
|
8454
|
+
}
|
|
8455
|
+
function requireBoundedAccessDelta(delta) {
|
|
8456
|
+
const total = delta.teamChanges.length + delta.membershipChanges.length + delta.projectChanges.length + delta.recipientChanges.length + delta.deviceAccessChanges.length;
|
|
8457
|
+
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");
|
|
8458
|
+
}
|
|
8459
|
+
var SAFE_CHOICE_LABEL_PATTERN = /^[\p{L}\p{N} '&,.()_-]*$/u;
|
|
8460
|
+
function normalizeChoiceLabelText(value) {
|
|
8461
|
+
return value.normalize("NFKC").replace(/\p{Cf}/gu, "").replace(/\p{Cc}/gu, " ").replace(/\s+/gu, " ").trim();
|
|
8462
|
+
}
|
|
8463
|
+
function safeChoiceLabel(value, fallback, forbiddenIds, opaqueRef) {
|
|
8464
|
+
const normalized = normalizeChoiceLabelText(value.slice(0, 512));
|
|
8465
|
+
const label = normalized.slice(0, 120).trim();
|
|
8466
|
+
const forbiddenComparisons = forbiddenIds.map((forbiddenId) => normalizeChoiceLabelText(forbiddenId).toLowerCase()).filter(Boolean);
|
|
8467
|
+
const comparable = normalized.toLowerCase();
|
|
8468
|
+
if (!label || !SAFE_CHOICE_LABEL_PATTERN.test(label) || /[\p{L}\p{N}]\.[\p{L}\p{N}]/u.test(label) || /-----|\b(?:ssh|ecdsa|sk)-[\p{L}\p{N}-]+ /iu.test(label) || forbiddenComparisons.some((forbiddenComparison) => comparable.includes(forbiddenComparison))) {
|
|
8469
|
+
const suffix = opaqueRef?.slice(-6);
|
|
8470
|
+
return suffix ? `${fallback} ${suffix}` : fallback;
|
|
8471
|
+
}
|
|
8472
|
+
return label;
|
|
8473
|
+
}
|
|
8474
|
+
function disambiguateChoiceLabels(choices, choiceRef) {
|
|
8475
|
+
const counts = /* @__PURE__ */ new Map();
|
|
8476
|
+
const originalLabels = /* @__PURE__ */ new Set();
|
|
8477
|
+
for (const choice of choices) {
|
|
8478
|
+
const comparable = normalizeChoiceLabelText(choice.displayName).toLowerCase();
|
|
8479
|
+
counts.set(comparable, (counts.get(comparable) ?? 0) + 1);
|
|
8480
|
+
originalLabels.add(comparable);
|
|
8481
|
+
}
|
|
8482
|
+
const usedLabels = /* @__PURE__ */ new Set();
|
|
8483
|
+
return choices.map((choice, index) => {
|
|
8484
|
+
const comparable = normalizeChoiceLabelText(choice.displayName).toLowerCase();
|
|
8485
|
+
if ((counts.get(comparable) ?? 0) < 2 && !usedLabels.has(comparable)) {
|
|
8486
|
+
usedLabels.add(comparable);
|
|
8487
|
+
return choice;
|
|
8488
|
+
}
|
|
8489
|
+
const ref = choiceRef(choice);
|
|
8490
|
+
let suffixLength = Math.min(6, ref.length);
|
|
8491
|
+
let suffix = ref.slice(-suffixLength);
|
|
8492
|
+
const maxAttempts = ref.length + originalLabels.size + usedLabels.size + 2;
|
|
8493
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
8494
|
+
const baseLength = Math.max(0, 119 - suffix.length);
|
|
8495
|
+
const base = choice.displayName.slice(0, baseLength).trimEnd();
|
|
8496
|
+
const displayName = base ? `${base} ${suffix}` : suffix.slice(-120);
|
|
8497
|
+
const finalComparable = normalizeChoiceLabelText(displayName).toLowerCase();
|
|
8498
|
+
if (!usedLabels.has(finalComparable) && !originalLabels.has(finalComparable)) {
|
|
8499
|
+
usedLabels.add(finalComparable);
|
|
8500
|
+
return {
|
|
8501
|
+
...choice,
|
|
8502
|
+
displayName
|
|
8503
|
+
};
|
|
8504
|
+
}
|
|
8505
|
+
if (suffixLength < ref.length) {
|
|
8506
|
+
suffixLength = Math.min(ref.length, suffixLength + 2);
|
|
8507
|
+
suffix = ref.slice(-suffixLength);
|
|
8508
|
+
continue;
|
|
8509
|
+
}
|
|
8510
|
+
suffix = `${ref.slice(-Math.min(96, ref.length))}-${index + 1}-${attempt + 1}`;
|
|
8511
|
+
}
|
|
8512
|
+
throw new Error("legacy_team_setup_roster_too_large");
|
|
8513
|
+
});
|
|
8514
|
+
}
|
|
8515
|
+
function requireCompleteMappingChoices(projectCount, choiceCount) {
|
|
8516
|
+
if (projectCount * choiceCount > MAX_TOTAL_PROJECT_MAPPING_CHOICES) throw new Error("legacy_team_setup_roster_too_large");
|
|
8517
|
+
}
|
|
8518
|
+
function createCachedSnapshotLoader(load, ttlMs, now = Date.now) {
|
|
8519
|
+
let cached = null;
|
|
8520
|
+
let inFlight = null;
|
|
8521
|
+
let generation = 0;
|
|
8522
|
+
const cachedLoad = () => {
|
|
8523
|
+
if (cached && now() < cached.expiresAt) return Promise.resolve(cached.snapshots);
|
|
8524
|
+
if (inFlight) return inFlight;
|
|
8525
|
+
const operationGeneration = generation;
|
|
8526
|
+
let operation;
|
|
8527
|
+
operation = load().then((snapshots) => {
|
|
8528
|
+
if (generation !== operationGeneration) return cachedLoad();
|
|
8529
|
+
cached = {
|
|
8530
|
+
expiresAt: now() + ttlMs,
|
|
8531
|
+
snapshots
|
|
8532
|
+
};
|
|
8533
|
+
return snapshots;
|
|
8534
|
+
}).finally(() => {
|
|
8535
|
+
if (inFlight === operation) inFlight = null;
|
|
8536
|
+
});
|
|
8537
|
+
inFlight = operation;
|
|
8538
|
+
return operation;
|
|
8539
|
+
};
|
|
8540
|
+
cachedLoad.invalidate = () => {
|
|
8541
|
+
generation += 1;
|
|
8542
|
+
cached = null;
|
|
8543
|
+
inFlight = null;
|
|
8544
|
+
};
|
|
8545
|
+
return cachedLoad;
|
|
8546
|
+
}
|
|
8547
|
+
function identityChoicesFromRows(rows, candidateRef, maxChoices = MAX_IDENTITY_CHOICES) {
|
|
8548
|
+
if (rows.length > maxChoices) throw new Error("legacy_team_setup_roster_too_large");
|
|
8549
|
+
const actorIds = rows.map((row) => row.actor_id);
|
|
8550
|
+
return disambiguateChoiceLabels(rows.map((row) => {
|
|
8551
|
+
const opaqueIdentityRef = requiredIdentityRef(candidateRef, row.actor_id);
|
|
8552
|
+
return {
|
|
8553
|
+
identityId: row.actor_id,
|
|
8554
|
+
identityRef: opaqueIdentityRef,
|
|
8555
|
+
displayName: safeChoiceLabel(row.display_name, "Person", actorIds, opaqueIdentityRef)
|
|
8556
|
+
};
|
|
8557
|
+
}), (choice) => choice.identityRef);
|
|
8558
|
+
}
|
|
8559
|
+
function identityChoices(store, candidateRef) {
|
|
8560
|
+
return identityChoicesFromRows(store.db.prepare(`SELECT actor_id, display_name FROM actors
|
|
8561
|
+
WHERE status = 'active' AND merged_into_actor_id IS NULL
|
|
8562
|
+
ORDER BY display_name, actor_id LIMIT ?`).all(501), candidateRef);
|
|
8563
|
+
}
|
|
8564
|
+
function completedDraftIdentityChoices(store, draft) {
|
|
8565
|
+
const actorIds = [...new Set(draft.devices.flatMap((device) => [
|
|
8566
|
+
device.existingIdentityId,
|
|
8567
|
+
device.suggestedIdentityId,
|
|
8568
|
+
device.targetIdentityId,
|
|
8569
|
+
device.expectation.kind === "existing" ? device.expectation.identityId : null
|
|
8570
|
+
]))].filter((actorId) => actorId !== null);
|
|
8571
|
+
if (actorIds.length === 0) return [];
|
|
8572
|
+
const placeholders = actorIds.map(() => "?").join(", ");
|
|
8573
|
+
return identityChoicesFromRows(store.db.prepare(`SELECT actor_id, display_name FROM actors
|
|
8574
|
+
WHERE status = 'active' AND merged_into_actor_id IS NULL
|
|
8575
|
+
AND actor_id IN (${placeholders})
|
|
8576
|
+
ORDER BY display_name, actor_id`).all(...actorIds), draft.candidateRef, MAX_COMPLETED_IDENTITY_CHOICES);
|
|
8577
|
+
}
|
|
8578
|
+
function projectMappingChoices(store) {
|
|
8579
|
+
let candidates;
|
|
8580
|
+
try {
|
|
8581
|
+
candidates = listProjectScopeCandidates(store.db, {
|
|
8582
|
+
limit: null,
|
|
8583
|
+
maxScannedRows: MAX_PROJECT_MAPPING_SCAN_ROWS,
|
|
8584
|
+
maxMetadataRows: MAX_PROJECT_MAPPING_METADATA_ROWS,
|
|
8585
|
+
excludePeerReceived: true
|
|
8586
|
+
}).filter((candidate) => !candidate.read_only && isLegacyTeamSetupProjectMappingIdentity(candidate.workspace_identity));
|
|
8587
|
+
} catch (error) {
|
|
8588
|
+
if (error instanceof Error && (error.message === "project_scope_candidate_scan_too_large" || error.message === "project_scope_candidate_metadata_too_large")) throw new Error("legacy_team_setup_roster_too_large");
|
|
8589
|
+
throw error;
|
|
8590
|
+
}
|
|
8591
|
+
if (candidates.length > MAX_PROJECT_MAPPING_CHOICES) throw new Error("legacy_team_setup_roster_too_large");
|
|
8592
|
+
return candidates.map((candidate) => ({
|
|
8593
|
+
projectIdentity: candidate.workspace_identity,
|
|
8594
|
+
sourceDisplayName: candidate.display_project
|
|
8595
|
+
}));
|
|
8596
|
+
}
|
|
8597
|
+
function viewerSafeDraft(store, draft) {
|
|
8598
|
+
if (draft.devices.length > MAX_DEVICES || draft.projects.length > MAX_PROJECTS) throw new Error("legacy_team_setup_roster_too_large");
|
|
8599
|
+
const completed = draft.state === "completed";
|
|
8600
|
+
const identities = completed ? completedDraftIdentityChoices(store, draft) : identityChoices(store, draft.candidateRef);
|
|
8601
|
+
const mappableProjectCount = completed ? 0 : draft.projects.filter((project) => project.resolution !== "deterministic").length;
|
|
8602
|
+
const mappingChoices = mappableProjectCount === 0 ? [] : projectMappingChoices(store);
|
|
8603
|
+
requireCompleteMappingChoices(mappableProjectCount, mappingChoices.length);
|
|
8604
|
+
const mappingChoiceIdentities = mappingChoices.map((choice) => choice.projectIdentity);
|
|
8605
|
+
return {
|
|
8606
|
+
devices: draft.devices.map((device) => ({
|
|
8607
|
+
deviceRef: device.deviceRef,
|
|
8608
|
+
displayName: device.displayName,
|
|
8609
|
+
enabled: device.enabled,
|
|
8610
|
+
existingIdentityRef: identityRef(draft.candidateRef, device.existingIdentityId),
|
|
8611
|
+
suggestedIdentityRef: identityRef(draft.candidateRef, device.suggestedIdentityId),
|
|
8612
|
+
verifiedEvidenceKind: device.verifiedEvidenceKind,
|
|
8613
|
+
decision: device.decision,
|
|
8614
|
+
targetIdentityRef: identityRef(draft.candidateRef, device.targetIdentityId),
|
|
8615
|
+
expectation: device.expectation.kind === "existing" ? {
|
|
8616
|
+
kind: "existing",
|
|
8617
|
+
assignmentVersion: device.expectation.assignmentVersion,
|
|
8618
|
+
identityRef: requiredIdentityRef(draft.candidateRef, device.expectation.identityId)
|
|
8619
|
+
} : { kind: "absent" }
|
|
8620
|
+
})),
|
|
8621
|
+
projects: draft.projects.map((project) => ({
|
|
8622
|
+
projectRef: project.projectRef,
|
|
8623
|
+
displayName: project.displayName,
|
|
8624
|
+
resolution: project.resolution,
|
|
8625
|
+
canonicalProjectRef: project.canonicalProjectRef,
|
|
8626
|
+
resolvedProjectRef: project.resolvedProjectRef,
|
|
8627
|
+
mappingChoices: project.resolution === "deterministic" ? [] : disambiguateChoiceLabels(mappingChoices.map((choice) => {
|
|
8628
|
+
const opaqueResolvedProjectRef = legacyTeamResolvedProjectRef(project.projectRef, choice.projectIdentity);
|
|
8629
|
+
return {
|
|
8630
|
+
resolvedProjectRef: opaqueResolvedProjectRef,
|
|
8631
|
+
displayName: safeChoiceLabel(choice.sourceDisplayName, "Project", mappingChoiceIdentities, opaqueResolvedProjectRef)
|
|
8632
|
+
};
|
|
8633
|
+
}), (choice) => choice.resolvedProjectRef)
|
|
8634
|
+
})),
|
|
8635
|
+
identityChoices: identities.map(({ identityRef, displayName }) => ({
|
|
8636
|
+
identityRef,
|
|
8637
|
+
displayName
|
|
8638
|
+
}))
|
|
8639
|
+
};
|
|
8640
|
+
}
|
|
8641
|
+
function errorStatus(code) {
|
|
8642
|
+
if (code === "team_setup_roster_unavailable") return 503;
|
|
8643
|
+
if (code === "team_setup_failed") return 503;
|
|
8644
|
+
if (code === "team_setup_incomplete") return 400;
|
|
8645
|
+
return 409;
|
|
8646
|
+
}
|
|
8647
|
+
function apiErrorCode(error) {
|
|
8648
|
+
const code = legacyTeamSetupApiErrorCode(error);
|
|
8649
|
+
return code === "team_setup_projection_changed" ? "team_setup_conflict" : code;
|
|
8650
|
+
}
|
|
8651
|
+
async function parseBoundedJsonObject(c) {
|
|
8652
|
+
if (c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return { ok: false };
|
|
8653
|
+
const lengthHeader = c.req.header("content-length");
|
|
8654
|
+
if (lengthHeader != null && !/^\d+$/u.test(lengthHeader.trim())) return { ok: false };
|
|
8655
|
+
const contentLength = lengthHeader == null ? null : Number(lengthHeader);
|
|
8656
|
+
if (contentLength != null && contentLength > MAX_MUTATION_BODY_BYTES) return { ok: false };
|
|
8657
|
+
const body = c.req.raw.body;
|
|
8658
|
+
if (!body) return { ok: false };
|
|
8659
|
+
const reader = body.getReader();
|
|
8660
|
+
const chunks = [];
|
|
8661
|
+
let total = 0;
|
|
8662
|
+
try {
|
|
8663
|
+
while (true) {
|
|
8664
|
+
const { done, value } = await reader.read();
|
|
8665
|
+
if (done) break;
|
|
8666
|
+
if (!value) continue;
|
|
8667
|
+
total += value.byteLength;
|
|
8668
|
+
if (total > MAX_MUTATION_BODY_BYTES) {
|
|
8669
|
+
await reader.cancel().catch(() => void 0);
|
|
8670
|
+
return { ok: false };
|
|
8671
|
+
}
|
|
8672
|
+
chunks.push(value);
|
|
8673
|
+
}
|
|
8674
|
+
} catch {
|
|
8675
|
+
return { ok: false };
|
|
8676
|
+
} finally {
|
|
8677
|
+
reader.releaseLock();
|
|
8678
|
+
}
|
|
8679
|
+
try {
|
|
8680
|
+
const bytes = new Uint8Array(total);
|
|
8681
|
+
let offset = 0;
|
|
8682
|
+
for (const chunk of chunks) {
|
|
8683
|
+
bytes.set(chunk, offset);
|
|
8684
|
+
offset += chunk.byteLength;
|
|
8685
|
+
}
|
|
8686
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
8687
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? {
|
|
8688
|
+
ok: true,
|
|
8689
|
+
value: parsed
|
|
8690
|
+
} : { ok: false };
|
|
8691
|
+
} catch {
|
|
8692
|
+
return { ok: false };
|
|
8693
|
+
}
|
|
8694
|
+
}
|
|
8695
|
+
function hasExactKeys(value, keys) {
|
|
8696
|
+
const actual = Object.keys(value).toSorted();
|
|
8697
|
+
const expected = keys.toSorted();
|
|
8698
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
8699
|
+
}
|
|
8700
|
+
function mutationResponse(draft) {
|
|
8701
|
+
return {
|
|
8702
|
+
version: TEAM_SETUP_VERSION,
|
|
8703
|
+
candidateRef: draft.candidateRef,
|
|
8704
|
+
attemptId: draft.attemptId,
|
|
8705
|
+
draftState: draft.state,
|
|
8706
|
+
canFinish: draft.canFinish,
|
|
8707
|
+
unresolvedDeviceCount: draft.unresolvedDeviceCount,
|
|
8708
|
+
unresolvedProjectCount: draft.unresolvedProjectCount
|
|
8709
|
+
};
|
|
8710
|
+
}
|
|
8711
|
+
function finishResponse(candidateRef, result) {
|
|
8712
|
+
return {
|
|
8713
|
+
version: TEAM_SETUP_VERSION,
|
|
8714
|
+
status: result.status,
|
|
8715
|
+
teamRef: teamRef(candidateRef, result.teamId),
|
|
8716
|
+
attemptId: result.attemptId,
|
|
8717
|
+
accessDeltaDigest: result.accessDeltaDigest,
|
|
8718
|
+
completedAt: result.completedAt
|
|
8719
|
+
};
|
|
8720
|
+
}
|
|
8721
|
+
function teamSetupRoutes(options) {
|
|
8722
|
+
const app = new Hono();
|
|
8723
|
+
const loadSnapshots = options.loadLegacyTeamConfiguredGroupSnapshots ?? loadConfiguredLegacyTeamGroupSnapshots;
|
|
8724
|
+
async function loadedSnapshots(candidateRef) {
|
|
8725
|
+
try {
|
|
8726
|
+
return await loadSnapshots(candidateRef ? { candidateRef } : void 0);
|
|
8727
|
+
} catch (error) {
|
|
8728
|
+
if (error instanceof Error && error.message === "legacy_team_setup_roster_too_large") throw error;
|
|
8729
|
+
throw safeCoordinatorError();
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
8732
|
+
const loadedSummarySnapshots = createCachedSnapshotLoader(() => loadedSnapshots(), SUMMARY_SNAPSHOT_CACHE_TTL_MS);
|
|
8733
|
+
async function loadedCandidateSnapshots(candidateRef) {
|
|
8734
|
+
loadedSummarySnapshots.invalidate();
|
|
8735
|
+
try {
|
|
8736
|
+
return await loadedSnapshots(candidateRef);
|
|
8737
|
+
} finally {
|
|
8738
|
+
loadedSummarySnapshots.invalidate();
|
|
8739
|
+
}
|
|
8740
|
+
}
|
|
8741
|
+
app.get("/api/sync/team-setup/v1", async (c) => {
|
|
8742
|
+
let groups;
|
|
8743
|
+
try {
|
|
8744
|
+
groups = await loadedSummarySnapshots();
|
|
8745
|
+
} catch (error) {
|
|
8746
|
+
const code = apiErrorCode(error);
|
|
8747
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8748
|
+
}
|
|
8749
|
+
try {
|
|
8750
|
+
const response = {
|
|
8751
|
+
version: TEAM_SETUP_VERSION,
|
|
8752
|
+
candidates: discoverCandidates(options.getStore(), groups).map(candidateSummary)
|
|
8753
|
+
};
|
|
8754
|
+
return c.json(response);
|
|
8755
|
+
} catch (error) {
|
|
8756
|
+
const code = apiErrorCode(error);
|
|
8757
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8758
|
+
}
|
|
8759
|
+
});
|
|
8760
|
+
app.get("/api/sync/team-setup/v1/:candidateRef", async (c) => {
|
|
8761
|
+
const candidateRef = c.req.param("candidateRef");
|
|
8762
|
+
if (!CANDIDATE_REF_PATTERN.test(candidateRef)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8763
|
+
try {
|
|
8764
|
+
const store = options.getStore();
|
|
8765
|
+
let draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
8766
|
+
let candidate;
|
|
8767
|
+
if (draft?.state === "completed" && isLegacyTeamCandidateSelectable(store.db, candidateRef, { projects: legacyTeamCandidateProjectInventory(store.db, projectionOptions(store), candidateRef) })) candidate = completedCandidateFromDraft(draft);
|
|
8768
|
+
else {
|
|
8769
|
+
const discoveredCandidate = discoverCandidates(store, (await loadedCandidateSnapshots(candidateRef)).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef)).find((item) => item.candidateRef === candidateRef);
|
|
8770
|
+
if (!discoveredCandidate) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8771
|
+
candidate = discoveredCandidate;
|
|
8772
|
+
draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
8773
|
+
}
|
|
8774
|
+
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8775
|
+
let conflictState = null;
|
|
8776
|
+
let preview = null;
|
|
8777
|
+
if (draft.state !== "completed") try {
|
|
8778
|
+
preview = previewLegacyTeamSetupActivation(store.db, {
|
|
8779
|
+
candidateRef,
|
|
8780
|
+
attemptId: draft.attemptId
|
|
8781
|
+
});
|
|
8782
|
+
} catch (error) {
|
|
8783
|
+
const code = apiErrorCode(error);
|
|
8784
|
+
if (code === "team_setup_failed") return c.json({ error: code }, 503);
|
|
8785
|
+
preview = null;
|
|
8786
|
+
conflictState = code;
|
|
8787
|
+
}
|
|
8788
|
+
if (preview) requireBoundedAccessDelta(preview.accessDelta);
|
|
8789
|
+
const viewDraft = conflictState ? getLegacyTeamSetupDraft(store.db, candidateRef) ?? draft : draft;
|
|
8790
|
+
const safeDraft = viewerSafeDraft(store, viewDraft);
|
|
8791
|
+
const responseBase = {
|
|
8792
|
+
version: TEAM_SETUP_VERSION,
|
|
8793
|
+
candidate: candidateSummaryForDraft(candidate, viewDraft),
|
|
8794
|
+
attemptId: viewDraft.attemptId,
|
|
8795
|
+
draftState: viewDraft.state,
|
|
8796
|
+
unresolvedDeviceCount: viewDraft.unresolvedDeviceCount,
|
|
8797
|
+
unresolvedProjectCount: viewDraft.unresolvedProjectCount,
|
|
8798
|
+
...safeDraft
|
|
8799
|
+
};
|
|
8800
|
+
if (!preview) {
|
|
8801
|
+
const response = {
|
|
8802
|
+
...responseBase,
|
|
8803
|
+
canFinish: false,
|
|
8804
|
+
conflictState
|
|
8805
|
+
};
|
|
8806
|
+
return c.json(response);
|
|
8807
|
+
}
|
|
8808
|
+
const accessDelta = viewerSafeAccessDelta(candidateRef, preview.accessDelta, {
|
|
8809
|
+
teamDisplayName: viewDraft.displayName,
|
|
8810
|
+
...safeDraft
|
|
8811
|
+
});
|
|
8812
|
+
const response = {
|
|
8813
|
+
...responseBase,
|
|
8814
|
+
canFinish: true,
|
|
8815
|
+
conflictState: null,
|
|
8816
|
+
finishDigest: preview.finishDigest,
|
|
8817
|
+
accessDeltaDigest: preview.accessDeltaDigest,
|
|
8818
|
+
viewerAccessDeltaDigest: viewerAccessDeltaDigest(accessDelta),
|
|
8819
|
+
accessDelta
|
|
8820
|
+
};
|
|
8821
|
+
return c.json(response);
|
|
8822
|
+
} catch (error) {
|
|
8823
|
+
const code = apiErrorCode(error);
|
|
8824
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8825
|
+
}
|
|
8826
|
+
});
|
|
8827
|
+
app.put("/api/sync/team-setup/v1/:candidateRef/devices/:deviceRef/assignment", async (c) => {
|
|
8828
|
+
const candidateRef = c.req.param("candidateRef");
|
|
8829
|
+
const deviceRef = c.req.param("deviceRef");
|
|
8830
|
+
if (!CANDIDATE_REF_PATTERN.test(candidateRef) || !DEVICE_REF_PATTERN.test(deviceRef)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8831
|
+
const parsed = await parseBoundedJsonObject(c);
|
|
8832
|
+
if (!parsed.ok || !hasExactKeys(parsed.value, [
|
|
8833
|
+
"attemptId",
|
|
8834
|
+
"expectation",
|
|
8835
|
+
"targetIdentityRef"
|
|
8836
|
+
])) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8837
|
+
const { attemptId, expectation, targetIdentityRef } = parsed.value;
|
|
8838
|
+
if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId) || typeof targetIdentityRef !== "string" || !IDENTITY_REF_PATTERN.test(targetIdentityRef) || !expectation || typeof expectation !== "object" || Array.isArray(expectation)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8839
|
+
try {
|
|
8840
|
+
const store = options.getStore();
|
|
8841
|
+
const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
8842
|
+
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8843
|
+
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
8844
|
+
const device = draft.devices.find((item) => item.deviceRef === deviceRef);
|
|
8845
|
+
if (!device) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8846
|
+
const submittedExpectation = expectation;
|
|
8847
|
+
if (!(device.expectation.kind === "absent" ? hasExactKeys(submittedExpectation, ["kind"]) && submittedExpectation.kind === "absent" : hasExactKeys(submittedExpectation, [
|
|
8848
|
+
"assignmentVersion",
|
|
8849
|
+
"identityRef",
|
|
8850
|
+
"kind"
|
|
8851
|
+
]) && submittedExpectation.kind === "existing" && submittedExpectation.assignmentVersion === device.expectation.assignmentVersion && submittedExpectation.identityRef === requiredIdentityRef(candidateRef, device.expectation.identityId))) return c.json({ error: "team_setup_assignment_changed" }, 409);
|
|
8852
|
+
const target = identityChoices(store, candidateRef).find((choice) => choice.identityRef === targetIdentityRef);
|
|
8853
|
+
if (!target) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8854
|
+
return c.json(mutationResponse(setLegacyTeamSetupDeviceAssignment(store.db, {
|
|
8855
|
+
attemptId,
|
|
8856
|
+
deviceRef,
|
|
8857
|
+
targetIdentityId: target.identityId,
|
|
8858
|
+
expectation: device.expectation
|
|
8859
|
+
})));
|
|
8860
|
+
} catch (error) {
|
|
8861
|
+
const code = apiErrorCode(error);
|
|
8862
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8863
|
+
}
|
|
8864
|
+
});
|
|
8865
|
+
app.put("/api/sync/team-setup/v1/:candidateRef/devices/:deviceRef/decision", async (c) => {
|
|
8866
|
+
const candidateRef = c.req.param("candidateRef");
|
|
8867
|
+
const deviceRef = c.req.param("deviceRef");
|
|
8868
|
+
if (!CANDIDATE_REF_PATTERN.test(candidateRef) || !DEVICE_REF_PATTERN.test(deviceRef)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8869
|
+
const parsed = await parseBoundedJsonObject(c);
|
|
8870
|
+
if (!parsed.ok) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8871
|
+
const { attemptId, decision, expectedTargetIdentityRef } = parsed.value;
|
|
8872
|
+
if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId) || ![
|
|
8873
|
+
"included",
|
|
8874
|
+
"excluded",
|
|
8875
|
+
"removed"
|
|
8876
|
+
].includes(decision) || (decision === "included" ? !hasExactKeys(parsed.value, [
|
|
8877
|
+
"attemptId",
|
|
8878
|
+
"decision",
|
|
8879
|
+
"expectedTargetIdentityRef"
|
|
8880
|
+
]) || typeof expectedTargetIdentityRef !== "string" || !IDENTITY_REF_PATTERN.test(expectedTargetIdentityRef) : !hasExactKeys(parsed.value, ["attemptId", "decision"]))) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8881
|
+
try {
|
|
8882
|
+
const store = options.getStore();
|
|
8883
|
+
const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
8884
|
+
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8885
|
+
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
8886
|
+
const device = draft.devices.find((item) => item.deviceRef === deviceRef);
|
|
8887
|
+
if (!device) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8888
|
+
if (decision === "included" && (device.targetIdentityId === null || requiredIdentityRef(candidateRef, device.targetIdentityId) !== expectedTargetIdentityRef)) return c.json({ error: "team_setup_assignment_changed" }, 409);
|
|
8889
|
+
return c.json(mutationResponse(setLegacyTeamSetupDeviceDecision(store.db, {
|
|
8890
|
+
attemptId,
|
|
8891
|
+
deviceRef,
|
|
8892
|
+
decision
|
|
8893
|
+
})));
|
|
8894
|
+
} catch (error) {
|
|
8895
|
+
const code = apiErrorCode(error);
|
|
8896
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8897
|
+
}
|
|
8898
|
+
});
|
|
8899
|
+
app.delete("/api/sync/team-setup/v1/:candidateRef/devices/:deviceRef/decision", async (c) => {
|
|
8900
|
+
const candidateRef = c.req.param("candidateRef");
|
|
8901
|
+
const deviceRef = c.req.param("deviceRef");
|
|
8902
|
+
if (!CANDIDATE_REF_PATTERN.test(candidateRef) || !DEVICE_REF_PATTERN.test(deviceRef)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8903
|
+
const parsed = await parseBoundedJsonObject(c);
|
|
8904
|
+
if (!parsed.ok || !hasExactKeys(parsed.value, ["attemptId"])) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8905
|
+
const { attemptId } = parsed.value;
|
|
8906
|
+
if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8907
|
+
try {
|
|
8908
|
+
const store = options.getStore();
|
|
8909
|
+
const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
8910
|
+
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8911
|
+
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
8912
|
+
if (!draft.devices.some((item) => item.deviceRef === deviceRef)) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8913
|
+
return c.json(mutationResponse(clearLegacyTeamSetupDeviceDecision(store.db, {
|
|
8914
|
+
attemptId,
|
|
8915
|
+
deviceRef
|
|
8916
|
+
})));
|
|
8917
|
+
} catch (error) {
|
|
8918
|
+
const code = apiErrorCode(error);
|
|
8919
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8920
|
+
}
|
|
8921
|
+
});
|
|
8922
|
+
app.put("/api/sync/team-setup/v1/:candidateRef/projects/:projectRef/mapping", async (c) => {
|
|
8923
|
+
const candidateRef = c.req.param("candidateRef");
|
|
8924
|
+
const projectRef = c.req.param("projectRef");
|
|
8925
|
+
if (!CANDIDATE_REF_PATTERN.test(candidateRef) || !PROJECT_REF_PATTERN.test(projectRef)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8926
|
+
const parsed = await parseBoundedJsonObject(c);
|
|
8927
|
+
if (!parsed.ok || !hasExactKeys(parsed.value, ["attemptId", "resolvedProjectRef"])) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8928
|
+
const { attemptId, resolvedProjectRef } = parsed.value;
|
|
8929
|
+
if (typeof attemptId !== "string" || !ATTEMPT_ID_PATTERN.test(attemptId) || typeof resolvedProjectRef !== "string" || !RESOLVED_PROJECT_REF_PATTERN.test(resolvedProjectRef)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8930
|
+
try {
|
|
8931
|
+
const store = options.getStore();
|
|
8932
|
+
const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
8933
|
+
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8934
|
+
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
8935
|
+
const project = draft.projects.find((item) => item.projectRef === projectRef);
|
|
8936
|
+
if (!project) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8937
|
+
if (project.resolution === "deterministic") return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8938
|
+
const target = projectMappingChoices(store).find((choice) => legacyTeamResolvedProjectRef(projectRef, choice.projectIdentity) === resolvedProjectRef);
|
|
8939
|
+
if (!target) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8940
|
+
return c.json(mutationResponse(setLegacyTeamSetupProjectMapping(store.db, {
|
|
8941
|
+
attemptId,
|
|
8942
|
+
projectRef,
|
|
8943
|
+
resolvedProjectIdentity: target.projectIdentity
|
|
8944
|
+
})));
|
|
8945
|
+
} catch (error) {
|
|
8946
|
+
const code = apiErrorCode(error);
|
|
8947
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8948
|
+
}
|
|
8949
|
+
});
|
|
8950
|
+
app.post("/api/sync/team-setup/v1/:candidateRef/refresh", async (c) => {
|
|
8951
|
+
const candidateRef = c.req.param("candidateRef");
|
|
8952
|
+
if (!CANDIDATE_REF_PATTERN.test(candidateRef)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8953
|
+
const parsed = await parseBoundedJsonObject(c);
|
|
8954
|
+
if (!parsed.ok || !hasExactKeys(parsed.value, [])) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8955
|
+
let groups;
|
|
8956
|
+
try {
|
|
8957
|
+
groups = await loadedCandidateSnapshots(candidateRef);
|
|
8958
|
+
} catch (error) {
|
|
8959
|
+
const code = apiErrorCode(error);
|
|
8960
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8961
|
+
}
|
|
8962
|
+
if (groups.filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef).length === 0) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8963
|
+
try {
|
|
8964
|
+
const store = options.getStore();
|
|
8965
|
+
return c.json(mutationResponse(refreshLegacyTeamCandidate(store.db, {
|
|
8966
|
+
projection: projectionOptions(store),
|
|
8967
|
+
groups
|
|
8968
|
+
}, candidateRef)));
|
|
8969
|
+
} catch (error) {
|
|
8970
|
+
const code = apiErrorCode(error);
|
|
8971
|
+
return c.json({ error: code }, errorStatus(code));
|
|
8972
|
+
}
|
|
8973
|
+
});
|
|
8974
|
+
app.post("/api/sync/team-setup/v1/:candidateRef/finish", async (c) => {
|
|
8975
|
+
const candidateRef = c.req.param("candidateRef");
|
|
8976
|
+
if (!CANDIDATE_REF_PATTERN.test(candidateRef)) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8977
|
+
const parsed = await parseBoundedJsonObject(c);
|
|
8978
|
+
if (!parsed.ok) return c.json({ error: "team_setup_incomplete" }, 400);
|
|
8979
|
+
const finishKeys = [
|
|
8980
|
+
"attemptId",
|
|
8981
|
+
"confirmedAccessDeltaDigest",
|
|
8982
|
+
"confirmedViewerAccessDeltaDigest",
|
|
8983
|
+
"finishDigest"
|
|
8984
|
+
];
|
|
8985
|
+
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);
|
|
8986
|
+
const { attemptId, confirmedAccessDeltaDigest, confirmedViewerAccessDeltaDigest, finishDigest } = parsed.value;
|
|
8987
|
+
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);
|
|
8988
|
+
try {
|
|
8989
|
+
const store = options.getStore();
|
|
8990
|
+
const draft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
8991
|
+
if (!draft) return c.json({ error: "team_setup_confirmation_stale" }, 404);
|
|
8992
|
+
if (draft.attemptId !== attemptId) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
8993
|
+
if (draft.state !== "completed") {
|
|
8994
|
+
const preview = previewLegacyTeamSetupActivation(store.db, {
|
|
8995
|
+
candidateRef,
|
|
8996
|
+
attemptId: draft.attemptId
|
|
8997
|
+
});
|
|
8998
|
+
requireBoundedAccessDelta(preview.accessDelta);
|
|
8999
|
+
const safeDraft = viewerSafeDraft(store, draft);
|
|
9000
|
+
if (viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, preview.accessDelta, {
|
|
9001
|
+
teamDisplayName: draft.displayName,
|
|
9002
|
+
...safeDraft
|
|
9003
|
+
})) !== confirmedViewerAccessDeltaDigest) return c.json({ error: "team_setup_confirmation_stale" }, 409);
|
|
9004
|
+
}
|
|
9005
|
+
const result = await finishLegacyTeamSetupActivation(store.db, {
|
|
9006
|
+
candidateRef,
|
|
9007
|
+
attemptId,
|
|
9008
|
+
finishDigest,
|
|
9009
|
+
confirmedAccessDeltaDigest,
|
|
9010
|
+
loadFreshRoster: async () => {
|
|
9011
|
+
const matching = (await loadedCandidateSnapshots(candidateRef)).filter((group) => legacyTeamCandidateId(group.coordinatorId, group.groupId) === candidateRef);
|
|
9012
|
+
if (matching.length !== 1) throw safeCoordinatorError();
|
|
9013
|
+
return matching[0]?.devices ?? [];
|
|
9014
|
+
},
|
|
9015
|
+
loadProjectInventory: () => legacyTeamCandidateProjectInventory(store.db, projectionOptions(store), candidateRef),
|
|
9016
|
+
validateLockedPreview: (lockedPreview) => {
|
|
9017
|
+
requireBoundedAccessDelta(lockedPreview.accessDelta);
|
|
9018
|
+
const lockedDraft = getLegacyTeamSetupDraft(store.db, candidateRef);
|
|
9019
|
+
if (!lockedDraft || lockedDraft.attemptId !== attemptId) return false;
|
|
9020
|
+
const safeDraft = viewerSafeDraft(store, lockedDraft);
|
|
9021
|
+
return viewerAccessDeltaDigest(viewerSafeAccessDelta(candidateRef, lockedPreview.accessDelta, {
|
|
9022
|
+
teamDisplayName: lockedDraft.displayName,
|
|
9023
|
+
...safeDraft
|
|
9024
|
+
})) === confirmedViewerAccessDeltaDigest;
|
|
9025
|
+
}
|
|
9026
|
+
});
|
|
9027
|
+
return c.json(finishResponse(candidateRef, result));
|
|
9028
|
+
} catch (error) {
|
|
9029
|
+
const code = apiErrorCode(error);
|
|
9030
|
+
return c.json({ error: code }, errorStatus(code));
|
|
9031
|
+
}
|
|
9032
|
+
});
|
|
9033
|
+
return app;
|
|
9034
|
+
}
|
|
9035
|
+
//#endregion
|
|
8169
9036
|
//#region src/routes/update-status.ts
|
|
8170
9037
|
function updateStatusRoutes(options = {}) {
|
|
8171
9038
|
const getUpdateStatus$1 = options.getUpdateStatus ?? getUpdateStatus;
|
|
@@ -8216,7 +9083,7 @@ function createApp(opts) {
|
|
|
8216
9083
|
const getSyncRuntimeStatus = opts?.getSyncRuntimeStatus ?? (() => null);
|
|
8217
9084
|
const app = new Hono();
|
|
8218
9085
|
app.use("*", preflightHandler());
|
|
8219
|
-
app.use("*", originGuard());
|
|
9086
|
+
app.use("*", originGuard({ unsafeGetPathPrefixes: [TEAM_SETUP_ROUTE_PREFIX] }));
|
|
8220
9087
|
app.route("/", healthRoutes(storeFactory));
|
|
8221
9088
|
app.route("/", statsRoutes(storeFactory));
|
|
8222
9089
|
app.route("/", memoryRoutes(storeFactory));
|
|
@@ -8229,6 +9096,10 @@ function createApp(opts) {
|
|
|
8229
9096
|
app.route("/", configRoutes({ getSweeper: () => sweeper }));
|
|
8230
9097
|
app.route("/", rawEventsRoutes(storeFactory, sweeper));
|
|
8231
9098
|
app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus, { loadDeviceIdentityCoordinatorEvidence: opts?.loadDeviceIdentityCoordinatorEvidence }));
|
|
9099
|
+
app.route("/", teamSetupRoutes({
|
|
9100
|
+
getStore: storeFactory,
|
|
9101
|
+
loadLegacyTeamConfiguredGroupSnapshots: opts?.loadLegacyTeamConfiguredGroupSnapshots
|
|
9102
|
+
}));
|
|
8232
9103
|
app.route("/", updateStatusRoutes({ getUpdateStatus: opts?.getUpdateStatus }));
|
|
8233
9104
|
const staticRoot = process.env.CODEMEM_VIEWER_STATIC_DIR ?? join(import.meta.dirname ?? ".", "../static");
|
|
8234
9105
|
app.use("/assets/*", async (c, next) => {
|