@codemem/server 0.47.0-beta.4 → 0.47.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/account/link-registry.d.ts +15 -3
- package/dist/account/link-registry.d.ts.map +1 -1
- package/dist/account/owner-recovery-candidates.d.ts +31 -0
- package/dist/account/owner-recovery-candidates.d.ts.map +1 -0
- package/dist/account/owner-recovery-evidence.d.ts +5 -0
- package/dist/account/owner-recovery-evidence.d.ts.map +1 -1
- package/dist/account/owner-recovery-service.d.ts +12 -1
- package/dist/account/owner-recovery-service.d.ts.map +1 -1
- package/dist/account/owner-status.d.ts.map +1 -1
- package/dist/account/rebind.d.ts +41 -0
- package/dist/account/rebind.d.ts.map +1 -0
- package/dist/account/status.d.ts +23 -5
- package/dist/account/status.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +560 -48
- package/dist/index.js.map +1 -1
- package/dist/routes/account-operations.d.ts.map +1 -1
- package/dist/routes/account-owner-recovery.d.ts.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/dist/routes/team-setup-view.d.ts +2 -0
- package/dist/routes/team-setup-view.d.ts.map +1 -1
- package/dist/routes/team-setup.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +30 -30
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/index.html +4 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { closeSync, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
3
|
-
import { ACCESS_CLEANUP_OP_TYPE, CodememConfigMutationError, CoordinatorAccountLinkError, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, FILE_CONTEXT_TRANSPORT_MAX_BYTES, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, LegacyTeamSetupAdditiveConvergenceError, LegacyTeamSetupRosterCapacityError, MEMORY_FILTER_FIELD_TYPES, MEMORY_FILTER_NAMES, MEMORY_KIND_DESCRIPTIONS, MemoryStore, ObserverClient, PROMPT_TRANSPORT_PROTOCOL_RANGE, REMEMBER_MEMORY_KINDS, RICH_TIER_ANTHROPIC_DEFAULTS, RICH_TIER_DEFAULTS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SIMPLE_TIER_ANTHROPIC_DEFAULTS, SIMPLE_TIER_DEFAULTS, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, analyzeProjectScopeMappingChangesGuardrails, analyzeProjectScopeMappingDeletionGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterBranchesWithContext, buildFilterClausesWithContext, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalRepositoryProjectIdentity, canonicalWorkspaceIdentity, captureAccountOwnerProfilePins, changeCoordinatorAccount, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, classifyRecordedSyncFailure, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorAuthControllerReviewAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, dedupeOrderedIds, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, deriveLegacyTeamSetupCompletionManifest, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, dispatchFileContextTransport, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, generateWithOpenCodeV2, getCodememConfigPath, getCodememEnvOverrideValues, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getEffectiveCachedScopeAuthorization, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isAutomaticRecallMeasurement, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, judgeDistillReport, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, linkCoordinatorAccount, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listOpenCodeV2Models, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadPublicKey, loadReplicationOpsForPeer, loadRuntimeSigningPublicKey, lookupCoordinatorPeers, memoryFilterValueMatchesType, mergeAddresses, migrateRecipientPolicyIntent, mutateCodememConfigFile, negotiateSyncCapability, normalizeAddress, normalizeCoordinatorAccountLinkOrigin, normalizeCoordinatorLegacyTeamCompletionManifest, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parseBootstrapExpiry, parseFileContextTransportRequest, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectMatchesFilter, projectShareLifecycle, promptPackArtifactFingerprint, readAccountLinkDevice, readAccountOwnerAssociation, readCodememConfigFile, readCoordinatorAccountChangeCapabilities, readCoordinatorAccountLinkStatus, readCoordinatorResolvedAccountOwnerReceiptStatus, readCoordinatorResolvedAccountOwnerStatus, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordAutomaticRecall, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, repositoryIdentitiesByWorkspace, repositoryIdentityForWorkspace, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveCodememConfigPath, resolveDbPath, resolveObserverDefaultModel, resolveObserverRuntime, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, resolveVisibleScopeIds, runSyncPass, schema, scopeAuthorizationFailureReason, searchPiSessions, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMappings, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, validateRawEvents, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, wakeRecipientPoliciesForIdentities, wakeRecipientPoliciesForProjectIdentities } from "@codemem/core";
|
|
3
|
+
import { ACCESS_CLEANUP_OP_TYPE, CodememConfigMutationError, CoordinatorAccountLinkError, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, FILE_CONTEXT_TRANSPORT_MAX_BYTES, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, LegacyTeamSetupAdditiveConvergenceError, LegacyTeamSetupRosterCapacityError, MEMORY_FILTER_FIELD_TYPES, MEMORY_FILTER_NAMES, MEMORY_KIND_DESCRIPTIONS, MemoryStore, ObserverClient, PROMPT_TRANSPORT_PROTOCOL_RANGE, REMEMBER_MEMORY_KINDS, RICH_TIER_ANTHROPIC_DEFAULTS, RICH_TIER_DEFAULTS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SIMPLE_TIER_ANTHROPIC_DEFAULTS, SIMPLE_TIER_DEFAULTS, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, analyzeProjectScopeMappingChangesGuardrails, analyzeProjectScopeMappingDeletionGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyLocalIdentitySwitch, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, assertNoLocalIdentityOverride, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterBranchesWithContext, buildFilterClausesWithContext, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalRepositoryProjectIdentity, canonicalWorkspaceIdentity, captureAccountOwnerProfilePins, changeCoordinatorAccount, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, classifyRecordedSyncFailure, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, completeLocalIdentitySwitch, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorAuthControllerReviewAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, dedupeOrderedIds, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, deriveLegacyTeamSetupCompletionManifest, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, dispatchFileContextTransport, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, generateWithOpenCodeV2, getCodememConfigPath, getCodememEnvOverrideValues, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getEffectiveCachedScopeAuthorization, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isAutomaticRecallMeasurement, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, judgeDistillReport, legacyDecisionCountsAsOwner, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, linkCoordinatorAccount, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listOpenCodeV2Models, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadPublicKey, loadReplicationOpsForPeer, loadRuntimeSigningPublicKey, lookupCoordinatorPeers, markLocalIdentitySwitchBound, markLocalIdentitySwitchNotCommitted, memoryFilterValueMatchesType, mergeAddresses, migrateRecipientPolicyIntent, mutateCodememConfigFile, negotiateSyncCapability, normalizeAddress, normalizeCoordinatorAccountLinkOrigin, normalizeCoordinatorLegacyTeamCompletionManifest, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parseBootstrapExpiry, parseFileContextTransportRequest, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectMatchesFilter, projectShareLifecycle, promptPackArtifactFingerprint, readAccountLinkDevice, readAccountOwnerAssociation, readAccountOwnerAssociationRetainedAtMs, readCodememConfigFile, readCompletedLocalIdentitySwitches, readCoordinatorAccountChangeCapabilities, readCoordinatorAccountLinkStatus, readCoordinatorResolvedAccountOwnerReceiptStatus, readCoordinatorResolvedAccountOwnerStatus, readCoordinatorSyncConfig, readOpenLocalIdentitySwitch, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordAutomaticRecall, recordHighestObservedDirectSignatureVersion, recordLocalIdentitySwitch, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, repositoryIdentitiesByWorkspace, repositoryIdentityForWorkspace, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveCodememConfigPath, resolveDbPath, resolveObserverDefaultModel, resolveObserverRuntime, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, resolveVisibleScopeIds, resumeDeviceRebind, runDeviceRebind, runSyncPass, schema, scopeAuthorizationFailureReason, searchPiSessions, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMappings, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, validateRawEvents, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, wakeRecipientPoliciesForIdentities, wakeRecipientPoliciesForProjectIdentities } from "@codemem/core";
|
|
4
4
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -316,10 +316,31 @@ function safeFailure(error) {
|
|
|
316
316
|
default: return "request_failed";
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
|
+
function browserPath(intent) {
|
|
320
|
+
if (intent === "refresh" || intent === "disconnect") return "/auth/account/change/start";
|
|
321
|
+
if (intent === "sign_in") return "/auth/rebind/start";
|
|
322
|
+
return "/auth/link/start";
|
|
323
|
+
}
|
|
324
|
+
var REBIND_REFUSALS = {
|
|
325
|
+
account_not_linked: "account_not_linked",
|
|
326
|
+
already_this_identity: "already_this_identity",
|
|
327
|
+
current_identity_linked: "current_identity_linked",
|
|
328
|
+
device_already_bound: "device_already_bound",
|
|
329
|
+
daily_limit: "daily_limit"
|
|
330
|
+
};
|
|
331
|
+
/** Maps a rebind result to the operation's terminal state and failure. */
|
|
332
|
+
function rebindOutcome(result) {
|
|
333
|
+
if (result.kind === "switched" || result.kind === "bound") return ["succeeded", null];
|
|
334
|
+
if (result.kind === "bind_unknown") return ["outcome_unknown", "bind_unconfirmed"];
|
|
335
|
+
if (result.kind === "pending") return ["outcome_unknown", "switch_pending"];
|
|
336
|
+
if (result.kind === "refused" && result.reason === "unavailable" && result.beforeBrowser) return ["failed", "sign_in_unavailable"];
|
|
337
|
+
if (result.kind === "refused") return ["failed", REBIND_REFUSALS[result.reason] ?? "request_failed"];
|
|
338
|
+
return ["failed", "request_failed"];
|
|
339
|
+
}
|
|
319
340
|
function validBrowserUrl(value, origin, intent) {
|
|
320
341
|
try {
|
|
321
342
|
const url = new URL(value);
|
|
322
|
-
return url.origin === origin && url.pathname === (intent
|
|
343
|
+
return url.origin === origin && url.pathname === browserPath(intent) && !url.username && !url.password && !url.hash;
|
|
323
344
|
} catch {
|
|
324
345
|
return false;
|
|
325
346
|
}
|
|
@@ -413,6 +434,7 @@ var InProcessAccountLinkRegistry = class {
|
|
|
413
434
|
onBrowserStart: (url) => this.#browserStart(job, url)
|
|
414
435
|
};
|
|
415
436
|
const intent = job.operation.intent ?? "connect";
|
|
437
|
+
if (intent === "sign_in") return await this.#runSignIn(job, options.onBrowserStart);
|
|
416
438
|
if (intent === "connect") await (this.#options.link ?? linkCoordinatorAccount)({
|
|
417
439
|
...options,
|
|
418
440
|
expectedAuthority
|
|
@@ -430,6 +452,25 @@ var InProcessAccountLinkRegistry = class {
|
|
|
430
452
|
this.#fail(job, error);
|
|
431
453
|
}
|
|
432
454
|
}
|
|
455
|
+
/** Sign-in may switch this device's Identity; its result maps to one terminal state. */
|
|
456
|
+
async #runSignIn(job, onBrowserStart) {
|
|
457
|
+
const rebind = this.#options.rebind;
|
|
458
|
+
if (!rebind) throw new CoordinatorAccountLinkError("invalid_options");
|
|
459
|
+
const [state, failure] = rebindOutcome(await rebind({
|
|
460
|
+
context: job.context,
|
|
461
|
+
signal: job.controller.signal,
|
|
462
|
+
onBrowserStart,
|
|
463
|
+
isCurrent: () => {
|
|
464
|
+
const current = this.#current(job.context);
|
|
465
|
+
if (!current) job.abortFailure = "context_changed";
|
|
466
|
+
return current;
|
|
467
|
+
}
|
|
468
|
+
}));
|
|
469
|
+
if (state !== "failed") this.#finish(job, state, failure);
|
|
470
|
+
else if (job.abortFailure) this.#finish(job, "failed", job.abortFailure);
|
|
471
|
+
else if (job.cancelled) this.#finish(job, "cancelled", "link_stopped");
|
|
472
|
+
else this.#finish(job, "failed", failure);
|
|
473
|
+
}
|
|
433
474
|
#fail(job, error) {
|
|
434
475
|
let failure = safeFailure(error);
|
|
435
476
|
if (job.abortFailure && (failure === "link_stopped" || failure === "request_failed")) failure = job.abortFailure;
|
|
@@ -484,6 +525,15 @@ var InProcessAccountLinkRegistry = class {
|
|
|
484
525
|
});
|
|
485
526
|
return { operation: this.#snapshot(job) };
|
|
486
527
|
}
|
|
528
|
+
supportsSignIn() {
|
|
529
|
+
if (!this.#options.rebind) return false;
|
|
530
|
+
try {
|
|
531
|
+
assertNoLocalIdentityOverride();
|
|
532
|
+
return true;
|
|
533
|
+
} catch {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
487
537
|
lifecycleToken() {
|
|
488
538
|
busySlot.lifecycle ??= Symbol();
|
|
489
539
|
return busySlot.lifecycle;
|
|
@@ -584,18 +634,69 @@ function readOwnerAssociation(context, coordinatorId) {
|
|
|
584
634
|
if (context.publicKeyHash === void 0) return null;
|
|
585
635
|
const before = statSync(context.dbPath, { bigint: true });
|
|
586
636
|
const device = readAccountLinkDevice(context.dbPath);
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
637
|
+
const profileFor = (actorId) => {
|
|
638
|
+
const profile = {
|
|
639
|
+
...context,
|
|
640
|
+
publicKey: device.publicKey,
|
|
641
|
+
actorId,
|
|
642
|
+
coordinatorId
|
|
643
|
+
};
|
|
644
|
+
return {
|
|
645
|
+
profile,
|
|
646
|
+
expectedProfilePins: captureAccountOwnerProfilePins(profile)
|
|
647
|
+
};
|
|
592
648
|
};
|
|
593
|
-
const
|
|
594
|
-
|
|
649
|
+
const association = readCurrentAssociation(context, {
|
|
650
|
+
generation: (actorId) => {
|
|
651
|
+
const { profile, expectedProfilePins } = profileFor(actorId);
|
|
652
|
+
return readAccountOwnerAssociation(profile, { expectedProfilePins });
|
|
653
|
+
},
|
|
654
|
+
generationRetainedAtMs: (actorId) => {
|
|
655
|
+
const { profile, expectedProfilePins } = profileFor(actorId);
|
|
656
|
+
return readAccountOwnerAssociationRetainedAtMs(profile, { expectedProfilePins });
|
|
657
|
+
},
|
|
658
|
+
legacy: (actorId) => readAccountOwnerAssociation(profileFor(actorId).profile),
|
|
659
|
+
legacyRetainedAtMs: (actorId) => readAccountOwnerAssociationRetainedAtMs(profileFor(actorId).profile)
|
|
660
|
+
});
|
|
595
661
|
const after = statSync(context.dbPath, { bigint: true });
|
|
596
662
|
if (!before.isFile() || before.dev !== after.dev || before.ino !== after.ino || after.dev.toString() !== context.fileIdentity.dev || after.ino.toString() !== context.fileIdentity.ino || device.deviceId !== context.deviceId || createHash("sha256").update(device.publicKey).digest("hex") !== context.publicKeyHash || device.fingerprint !== context.fingerprint) throw new Error("account_owner_association_invalid");
|
|
597
663
|
return association;
|
|
598
664
|
}
|
|
665
|
+
/** A switch away from `actorId` completed after evidence published at `retainedAtMs`. */
|
|
666
|
+
function switchedAwayAfter(switches, actorId, retainedAtMs) {
|
|
667
|
+
return retainedAtMs !== null && switches.some((s) => s.previousIdentityId === actorId && s.completedAtMs > retainedAtMs);
|
|
668
|
+
}
|
|
669
|
+
/** The shared legacy file pins a previous Identity whose switch superseded it. */
|
|
670
|
+
function legacySuperseded(evidence, switches) {
|
|
671
|
+
return [...new Set(switches.map((s) => s.previousIdentityId))].some((actorId) => {
|
|
672
|
+
try {
|
|
673
|
+
return evidence.legacy(actorId) !== null && switchedAwayAfter(switches, actorId, evidence.legacyRetainedAtMs(actorId));
|
|
674
|
+
} catch {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* A switch away from the pinned Identity, completed after the association was
|
|
681
|
+
* published, supersedes it for good. Read failures are suppressed only for the
|
|
682
|
+
* shared legacy file, and only when it pins such a superseded Identity; a
|
|
683
|
+
* failing exact generation for the current Identity is always reported.
|
|
684
|
+
*/
|
|
685
|
+
function readCurrentAssociation(context, evidence) {
|
|
686
|
+
const current = context.identityId;
|
|
687
|
+
const generation = evidence.generation(current);
|
|
688
|
+
const switches = () => readCompletedLocalIdentitySwitches(context.dbPath);
|
|
689
|
+
if (generation) return switchedAwayAfter(switches(), current, evidence.generationRetainedAtMs(current)) ? null : generation;
|
|
690
|
+
let legacy;
|
|
691
|
+
try {
|
|
692
|
+
legacy = evidence.legacy(current);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (legacySuperseded(evidence, switches())) return null;
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
if (!legacy) return null;
|
|
698
|
+
return switchedAwayAfter(switches(), current, evidence.legacyRetainedAtMs(current)) ? null : legacy;
|
|
699
|
+
}
|
|
599
700
|
function ownerUnavailable(view, code) {
|
|
600
701
|
return {
|
|
601
702
|
...withoutOwnerCapabilities(view),
|
|
@@ -688,7 +789,26 @@ function associatedOwnerView(view, context, association, remote) {
|
|
|
688
789
|
}
|
|
689
790
|
//#endregion
|
|
690
791
|
//#region src/account/status.ts
|
|
691
|
-
|
|
792
|
+
/**
|
|
793
|
+
* The owner-only state offers sign-in when the owner's Google account is
|
|
794
|
+
* linked and this device's own Identity has none: signing in switches the
|
|
795
|
+
* device to the owner's Identity.
|
|
796
|
+
*/
|
|
797
|
+
function withOwnerSignIn(owner, own, context, remote) {
|
|
798
|
+
if (owner.ownerProjection?.status.profileState !== "linked" || own.canSignIn !== true || own.remote.kind !== "status" || own.remote.state !== "unlinked") return owner;
|
|
799
|
+
return {
|
|
800
|
+
...owner,
|
|
801
|
+
canSignIn: true,
|
|
802
|
+
signIn: {
|
|
803
|
+
contextId: context.contextId,
|
|
804
|
+
coordinatorId: remote.status.coordinatorId,
|
|
805
|
+
authority: remote.status.authority
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function attachOperation(input, registry, operationContextId = input.contextId) {
|
|
810
|
+
const { canSignIn, signIn, ...rest } = input;
|
|
811
|
+
const view = registry?.supportsSignIn?.() ? input : rest;
|
|
692
812
|
const operation = operationContextId ? registry?.forContext(operationContextId) ?? null : null;
|
|
693
813
|
const operationContext = operation && operationContextId && operationContextId !== view.contextId ? { operationContextId } : {};
|
|
694
814
|
if (registry?.hasActive()) return {
|
|
@@ -696,6 +816,7 @@ function attachOperation(view, registry, operationContextId = view.contextId) {
|
|
|
696
816
|
...operationContext,
|
|
697
817
|
operation,
|
|
698
818
|
canLink: false,
|
|
819
|
+
..."canSignIn" in view ? { canSignIn: false } : {},
|
|
699
820
|
blockedReason: "operation_active"
|
|
700
821
|
};
|
|
701
822
|
return {
|
|
@@ -722,6 +843,7 @@ function accountLocalView(result) {
|
|
|
722
843
|
} : null,
|
|
723
844
|
remote: { kind: "not_checked" },
|
|
724
845
|
canLink: false,
|
|
846
|
+
canSignIn: false,
|
|
725
847
|
blockedReason: "setup_incomplete",
|
|
726
848
|
operation: null,
|
|
727
849
|
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -755,12 +877,84 @@ function applyRemote(view, remote) {
|
|
|
755
877
|
account: identityMatches && status.state === "linked" ? status.account : null
|
|
756
878
|
},
|
|
757
879
|
canLink: status.state === "unlinked" && identityMatches,
|
|
880
|
+
canSignIn: identityMatches && signInState(status),
|
|
758
881
|
blockedReason
|
|
759
882
|
};
|
|
760
883
|
}
|
|
884
|
+
/** Sign-in starts unlinked, or linked when this device has no proof of its own (a self-bind). */
|
|
885
|
+
function signInState(status) {
|
|
886
|
+
return status.state === "unlinked" || status.state === "linked" && !status.account;
|
|
887
|
+
}
|
|
761
888
|
async function readAccountStatus(options, groupId, signal, readOptions = {}) {
|
|
762
889
|
const initial = options.readContext(groupId);
|
|
763
890
|
if ("error" in initial) return initial;
|
|
891
|
+
if (groupId === void 0 && readOptions.expectedContext === void 0 && initial.setup === "group_selection_required" && initial.groups.length > 0) return withPeopleNames(options, await readFirstUsableGroupStatus(options, initial.groups, signal));
|
|
892
|
+
return withPeopleNames(options, await readStatusFromContext(options, initial, groupId, signal, readOptions));
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Names come from the local People list at view time. They stay out of the
|
|
896
|
+
* context, so renaming someone in Sharing cannot abort an account operation.
|
|
897
|
+
*/
|
|
898
|
+
function withPeopleNames(options, result) {
|
|
899
|
+
if ("error" in result) return result;
|
|
900
|
+
const name = (identityId) => {
|
|
901
|
+
try {
|
|
902
|
+
return options.identityName?.(identityId) || void 0;
|
|
903
|
+
} catch {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
let view = result;
|
|
908
|
+
const localName = view.local ? name(view.local.identityId) : void 0;
|
|
909
|
+
if (view.local && localName) view = {
|
|
910
|
+
...view,
|
|
911
|
+
local: {
|
|
912
|
+
...view.local,
|
|
913
|
+
identityLabel: localName
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
const ownerName = view.accountOwner ? name(view.accountOwner.identityId) : void 0;
|
|
917
|
+
if (view.accountOwner && ownerName) view = {
|
|
918
|
+
...view,
|
|
919
|
+
accountOwner: {
|
|
920
|
+
...view.accountOwner,
|
|
921
|
+
displayName: ownerName
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
return view;
|
|
925
|
+
}
|
|
926
|
+
/** 2: use this group. 1: review, keep looking. 0: this enrollment was refused. -1: stop. */
|
|
927
|
+
function usableStatusRank(result) {
|
|
928
|
+
if ("error" in result) return -1;
|
|
929
|
+
if (result.ownerProjection) return 2;
|
|
930
|
+
if (result.remote.kind === "status") return result.remote.state === "review_required" ? 1 : 2;
|
|
931
|
+
if (result.remote.kind === "unavailable" && result.remote.code === "coordinator_rejected") return 0;
|
|
932
|
+
return -1;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Account status is per coordinator, and the group only picks which enrollment
|
|
936
|
+
* signs the request. Try configured groups in order and keep the first one
|
|
937
|
+
* whose enrollment the coordinator accepts, preferring a decided status over
|
|
938
|
+
* review. Each attempt is a full signed read with its own context checks.
|
|
939
|
+
*/
|
|
940
|
+
async function readFirstUsableGroupStatus(options, groups, signal) {
|
|
941
|
+
let best = null;
|
|
942
|
+
for (const groupId of groups) {
|
|
943
|
+
if (signal.aborted) break;
|
|
944
|
+
const result = await readGroupAccountStatus(options, groupId, signal);
|
|
945
|
+
const rank = usableStatusRank(result);
|
|
946
|
+
if (rank === 2) return result;
|
|
947
|
+
if (rank === -1) return best && usableStatusRank(best) === 1 ? best : result;
|
|
948
|
+
if (!best || rank > usableStatusRank(best)) best = result;
|
|
949
|
+
}
|
|
950
|
+
return best ?? readGroupAccountStatus(options, void 0, signal);
|
|
951
|
+
}
|
|
952
|
+
async function readGroupAccountStatus(options, groupId, signal) {
|
|
953
|
+
const initial = options.readContext(groupId);
|
|
954
|
+
if ("error" in initial) return initial;
|
|
955
|
+
return readStatusFromContext(options, initial, groupId, signal);
|
|
956
|
+
}
|
|
957
|
+
async function readStatusFromContext(options, initial, groupId, signal, readOptions = {}) {
|
|
764
958
|
const view = accountLocalView(initial);
|
|
765
959
|
if (initial.setup !== "ready" || !initial.context) return attachOperation(view, options.registry);
|
|
766
960
|
const snapshot = JSON.stringify(initial);
|
|
@@ -804,13 +998,21 @@ async function readAccountStatus(options, groupId, signal, readOptions = {}) {
|
|
|
804
998
|
},
|
|
805
999
|
blockedReason: "status_unavailable"
|
|
806
1000
|
}, options.registry);
|
|
807
|
-
return finishAccountStatus(options, initial.context, fresh, remote,
|
|
1001
|
+
return finishAccountStatus(options, initial.context, fresh, remote, {
|
|
1002
|
+
signal,
|
|
1003
|
+
ownIdentityOnly: readOptions.ownIdentityOnly === true
|
|
1004
|
+
});
|
|
808
1005
|
}
|
|
809
|
-
async function finishAccountStatus(options, context, fresh, remote,
|
|
1006
|
+
async function finishAccountStatus(options, context, fresh, remote, read) {
|
|
1007
|
+
const { signal } = read;
|
|
810
1008
|
const currentView = applyRemote(accountLocalView(fresh), remote);
|
|
811
1009
|
if (remote.kind === "status") {
|
|
812
1010
|
const associated = await readAssociatedOwner(options, context, currentView, remote, signal);
|
|
813
|
-
if (associated)
|
|
1011
|
+
if (associated) {
|
|
1012
|
+
const owner = withOwnerSignIn(associated, currentView, context, remote);
|
|
1013
|
+
if (read.ownIdentityOnly && owner.signIn?.contextId === context.contextId) return attachOperation(currentView, options.registry);
|
|
1014
|
+
return attachOperation(owner, options.registry, context.contextId);
|
|
1015
|
+
}
|
|
814
1016
|
}
|
|
815
1017
|
await attachManagementActions(options, context, currentView, signal);
|
|
816
1018
|
if (remote.kind === "status") try {
|
|
@@ -824,7 +1026,8 @@ async function readAssociatedOwner(options, context, view, actorRemote, signal)
|
|
|
824
1026
|
const local = {
|
|
825
1027
|
...view,
|
|
826
1028
|
remote: { kind: "not_checked" },
|
|
827
|
-
canLink: false
|
|
1029
|
+
canLink: false,
|
|
1030
|
+
canSignIn: false
|
|
828
1031
|
};
|
|
829
1032
|
try {
|
|
830
1033
|
const association = readOwnerAssociation(context, actorRemote.status.coordinatorId);
|
|
@@ -944,7 +1147,7 @@ function isCurrent(options, operation) {
|
|
|
944
1147
|
return !("error" in fresh) && fresh.setup === "ready" && fresh.context?.groupId === operation.groupId && fresh.context.contextId === operation.contextId;
|
|
945
1148
|
}
|
|
946
1149
|
function validStartBody(body) {
|
|
947
|
-
return body !== null && validString(body.groupId) && validString(body.contextId) && validString(body.coordinatorId) && (body.intent !== "disconnect" || validString(body.expectedLinkId)) && (body.intent === void 0 || body.intent === "connect" || body.intent === "refresh" || body.intent === "disconnect") && (body.authority === "controller_attestation" || body.authority === "device_claim");
|
|
1150
|
+
return body !== null && validString(body.groupId) && validString(body.contextId) && validString(body.coordinatorId) && (body.intent !== "disconnect" || validString(body.expectedLinkId)) && (body.intent === void 0 || body.intent === "connect" || body.intent === "refresh" || body.intent === "disconnect" || body.intent === "sign_in") && (body.authority === "controller_attestation" || body.authority === "device_claim");
|
|
948
1151
|
}
|
|
949
1152
|
function unlinkTargetChanged(body, view) {
|
|
950
1153
|
return body.intent === "disconnect" && body.expectedLinkId !== view.unlinkTarget;
|
|
@@ -962,7 +1165,10 @@ async function startOperation(c, options) {
|
|
|
962
1165
|
if ("error" in initial || initial.setup !== "ready" || !initial.context || initial.context?.contextId !== body.contextId) return c.json({ error: "account_context_changed" }, 409);
|
|
963
1166
|
const checkedContext = structuredClone(initial.context);
|
|
964
1167
|
const expectedLifecycleToken = options.registry.lifecycleToken();
|
|
965
|
-
const view = await readAccountStatus(options, body.groupId, c.req.raw.signal, {
|
|
1168
|
+
const view = await readAccountStatus(options, body.groupId, c.req.raw.signal, {
|
|
1169
|
+
expectedContext: checkedContext,
|
|
1170
|
+
ownIdentityOnly: body.intent === "sign_in"
|
|
1171
|
+
});
|
|
966
1172
|
if ("error" in view || view.setup !== "ready" || view.contextId !== body.contextId || view.remote.kind === "unavailable" && view.remote.code === "context_changed") return c.json({ error: "account_context_changed" }, 409);
|
|
967
1173
|
if (view.operation && view.operation.finishedAt === null) return c.json({
|
|
968
1174
|
error: "account_link_in_progress",
|
|
@@ -982,6 +1188,7 @@ async function startOperation(c, options) {
|
|
|
982
1188
|
}
|
|
983
1189
|
function actionAllowed(view, intent = "connect") {
|
|
984
1190
|
if (intent === "connect") return view.canLink;
|
|
1191
|
+
if (intent === "sign_in") return view.canSignIn === true;
|
|
985
1192
|
if (intent === "refresh" || intent === "disconnect") return view.managementActions?.[intent] === true;
|
|
986
1193
|
return false;
|
|
987
1194
|
}
|
|
@@ -1059,6 +1266,14 @@ var AccountOwnerRecoveryError = class extends Error {
|
|
|
1059
1266
|
this.code = code;
|
|
1060
1267
|
}
|
|
1061
1268
|
};
|
|
1269
|
+
/**
|
|
1270
|
+
* The fix for an unsafe local state directory, or null for any other error. Matches the
|
|
1271
|
+
* core error by code because the bundle can carry a second copy of the core class.
|
|
1272
|
+
*/
|
|
1273
|
+
function unsafeStorageMessage(error) {
|
|
1274
|
+
if (!(error instanceof Error) || !("code" in error)) return null;
|
|
1275
|
+
return error.code === "account_owner_association_directory_unsafe" ? error.message : null;
|
|
1276
|
+
}
|
|
1062
1277
|
function requireRecovery(condition, code = "invalid") {
|
|
1063
1278
|
if (!condition) throw new AccountOwnerRecoveryError(code);
|
|
1064
1279
|
}
|
|
@@ -1100,7 +1315,7 @@ function readWeakLabel(value) {
|
|
|
1100
1315
|
effect: row.effect
|
|
1101
1316
|
};
|
|
1102
1317
|
}
|
|
1103
|
-
function historyRows(value, selection, labels) {
|
|
1318
|
+
function historyRows$1(value, selection, labels) {
|
|
1104
1319
|
const row = recoveryRecord(value);
|
|
1105
1320
|
requireRecovery(recoveryId(row.group_id) && recoveryId(row.candidate_ref) && timestamp(row.created_at));
|
|
1106
1321
|
const manifest = normalizeCoordinatorLegacyTeamCompletionManifest(row.manifest);
|
|
@@ -1118,7 +1333,7 @@ function historyRows(value, selection, labels) {
|
|
|
1118
1333
|
identityId: decision.identity_id ?? "",
|
|
1119
1334
|
assignment: decision.decision,
|
|
1120
1335
|
relation,
|
|
1121
|
-
reason: "
|
|
1336
|
+
reason: legacyDecisionCountsAsOwner(decision) ? "Recorded in Team history." : "Guessed by another device in Team history."
|
|
1122
1337
|
};
|
|
1123
1338
|
});
|
|
1124
1339
|
}
|
|
@@ -1156,7 +1371,7 @@ function validateRecoveryReview(value, selection, groupId) {
|
|
|
1156
1371
|
requireRecovery(Array.isArray(row.legacy_history) && row.legacy_history.length <= 500 && Array.isArray(row.reviewed_weak_labels) && row.reviewed_weak_labels.length <= 256 && Array.isArray(row.reasons) && row.reasons.every((reason) => typeof reason === "string"));
|
|
1157
1372
|
const labels = row.reviewed_weak_labels.map(readWeakLabel);
|
|
1158
1373
|
requireRecovery(new Set(labels.map((label) => JSON.stringify(label))).size === labels.length);
|
|
1159
|
-
const rows = row.legacy_history.flatMap((history) => historyRows(history, selection, labels));
|
|
1374
|
+
const rows = row.legacy_history.flatMap((history) => historyRows$1(history, selection, labels));
|
|
1160
1375
|
requireRecovery(row.reasons.length <= 8 && row.reasons.every((reason) => typeof reason === "string" && RECOVERY_REASON.test(reason)));
|
|
1161
1376
|
const reasons = row.reasons;
|
|
1162
1377
|
if (row.state === "needs_review") {
|
|
@@ -1367,6 +1582,11 @@ async function dispatch(c, service) {
|
|
|
1367
1582
|
function accountOwnerRecoveryRoutes(service) {
|
|
1368
1583
|
const app = new Hono();
|
|
1369
1584
|
app.onError((error, c) => {
|
|
1585
|
+
const storage = unsafeStorageMessage(error);
|
|
1586
|
+
if (storage) return c.json({
|
|
1587
|
+
error: "account_owner_recovery_storage_unsafe",
|
|
1588
|
+
detail: storage
|
|
1589
|
+
}, 409);
|
|
1370
1590
|
if (!(error instanceof AccountOwnerRecoveryError)) return c.json({ error: "account_owner_recovery_unavailable" }, 503);
|
|
1371
1591
|
return c.json({ error: `account_owner_recovery_${error.code}` }, {
|
|
1372
1592
|
invalid: 400,
|
|
@@ -6804,6 +7024,24 @@ function projectMappingChoices(store) {
|
|
|
6804
7024
|
sourceDisplayName: candidate.display_project
|
|
6805
7025
|
}));
|
|
6806
7026
|
}
|
|
7027
|
+
function viewerSafeDevice(candidateRef, device) {
|
|
7028
|
+
return {
|
|
7029
|
+
deviceRef: device.deviceRef,
|
|
7030
|
+
displayName: device.displayName,
|
|
7031
|
+
enabled: device.enabled,
|
|
7032
|
+
existingIdentityRef: identityRef(candidateRef, device.existingIdentityId),
|
|
7033
|
+
suggestedIdentityRef: identityRef(candidateRef, device.suggestedIdentityId),
|
|
7034
|
+
verifiedEvidenceKind: device.verifiedEvidenceKind,
|
|
7035
|
+
identityEvidence: device.identityEvidence,
|
|
7036
|
+
decision: device.decision,
|
|
7037
|
+
targetIdentityRef: identityRef(candidateRef, device.targetIdentityId),
|
|
7038
|
+
expectation: device.expectation.kind === "existing" ? {
|
|
7039
|
+
kind: "existing",
|
|
7040
|
+
assignmentVersion: device.expectation.assignmentVersion,
|
|
7041
|
+
identityRef: requiredIdentityRef(candidateRef, device.expectation.identityId)
|
|
7042
|
+
} : { kind: "absent" }
|
|
7043
|
+
};
|
|
7044
|
+
}
|
|
6807
7045
|
function viewerSafeDraft(store, draft) {
|
|
6808
7046
|
if (draft.devices.length > MAX_DEVICES || draft.projects.length > MAX_PROJECTS) throw new Error("legacy_team_setup_roster_too_large");
|
|
6809
7047
|
const completed = draft.state === "completed";
|
|
@@ -6813,21 +7051,7 @@ function viewerSafeDraft(store, draft) {
|
|
|
6813
7051
|
requireCompleteMappingChoices(mappableProjectCount, mappingChoices.length);
|
|
6814
7052
|
const mappingChoiceIdentities = mappingChoices.map((choice) => choice.projectIdentity);
|
|
6815
7053
|
return {
|
|
6816
|
-
devices: draft.devices.map((device) => (
|
|
6817
|
-
deviceRef: device.deviceRef,
|
|
6818
|
-
displayName: device.displayName,
|
|
6819
|
-
enabled: device.enabled,
|
|
6820
|
-
existingIdentityRef: identityRef(draft.candidateRef, device.existingIdentityId),
|
|
6821
|
-
suggestedIdentityRef: identityRef(draft.candidateRef, device.suggestedIdentityId),
|
|
6822
|
-
verifiedEvidenceKind: device.verifiedEvidenceKind,
|
|
6823
|
-
decision: device.decision,
|
|
6824
|
-
targetIdentityRef: identityRef(draft.candidateRef, device.targetIdentityId),
|
|
6825
|
-
expectation: device.expectation.kind === "existing" ? {
|
|
6826
|
-
kind: "existing",
|
|
6827
|
-
assignmentVersion: device.expectation.assignmentVersion,
|
|
6828
|
-
identityRef: requiredIdentityRef(draft.candidateRef, device.expectation.identityId)
|
|
6829
|
-
} : { kind: "absent" }
|
|
6830
|
-
})),
|
|
7054
|
+
devices: draft.devices.map((device) => viewerSafeDevice(draft.candidateRef, device)),
|
|
6831
7055
|
projects: draft.projects.map((project) => ({
|
|
6832
7056
|
projectRef: project.projectRef,
|
|
6833
7057
|
displayName: project.displayName,
|
|
@@ -9799,6 +10023,7 @@ function isPendingCaptureActorConflict(error) {
|
|
|
9799
10023
|
/** False leaves the conflict untouched; callers must check before claiming or merging identity. */
|
|
9800
10024
|
function ensureLocalActorRecord(store) {
|
|
9801
10025
|
return store.db.transaction(() => {
|
|
10026
|
+
store.assertLocalIdentityCurrent();
|
|
9802
10027
|
try {
|
|
9803
10028
|
store.assertPendingLocalCaptureAdoptionActors();
|
|
9804
10029
|
} catch (error) {
|
|
@@ -15358,6 +15583,7 @@ sqliteTable("legacy_team_setup_draft_devices", {
|
|
|
15358
15583
|
existing_identity_id: text("existing_identity_id"),
|
|
15359
15584
|
existing_assignment_version: integer("existing_assignment_version"),
|
|
15360
15585
|
verified_evidence_kind: text("verified_evidence_kind"),
|
|
15586
|
+
identity_evidence: text("identity_evidence"),
|
|
15361
15587
|
decision: text("decision").notNull().default("unresolved"),
|
|
15362
15588
|
target_identity_id: text("target_identity_id"),
|
|
15363
15589
|
expected_assignment_kind: text("expected_assignment_kind"),
|
|
@@ -15561,16 +15787,44 @@ function assertPublishedConfigLock(path, staged) {
|
|
|
15561
15787
|
function accountOwnerAssociationDirectory() {
|
|
15562
15788
|
return join(homedir(), ".codemem", "account-owner-associations");
|
|
15563
15789
|
}
|
|
15790
|
+
/**
|
|
15791
|
+
* A state directory that other local users could change. The message names the path and
|
|
15792
|
+
* the command that fixes it, because callers show it to the person who owns the directory.
|
|
15793
|
+
*/
|
|
15794
|
+
var AccountOwnerAssociationDirectoryError = class extends Error {
|
|
15795
|
+
path;
|
|
15796
|
+
problem;
|
|
15797
|
+
code = "account_owner_association_directory_unsafe";
|
|
15798
|
+
constructor(path, problem) {
|
|
15799
|
+
super(directoryProblemMessage(path, problem));
|
|
15800
|
+
this.path = path;
|
|
15801
|
+
this.problem = problem;
|
|
15802
|
+
this.name = "AccountOwnerAssociationDirectoryError";
|
|
15803
|
+
}
|
|
15804
|
+
};
|
|
15805
|
+
function directoryProblemMessage(path, problem) {
|
|
15806
|
+
if (problem === "shared_write") return `${path} is writable by other users. Run \`chmod go-w ${path}\`, then try again.`;
|
|
15807
|
+
if (problem === "not_private") return `${path} must be private to you. Run \`chmod 700 ${path}\`, then try again.`;
|
|
15808
|
+
if (problem === "wrong_owner") return `${path} is owned by another user. Make it yours.`;
|
|
15809
|
+
return `${path} must be a real directory, not a file or symlink.`;
|
|
15810
|
+
}
|
|
15811
|
+
function directoryProblem(stat, options) {
|
|
15812
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return "not_directory";
|
|
15813
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) return "wrong_owner";
|
|
15814
|
+
if (options.private) return (stat.mode & 511) === 448 ? null : "not_private";
|
|
15815
|
+
return (stat.mode & 18) === 0 ? null : "shared_write";
|
|
15816
|
+
}
|
|
15564
15817
|
function checkDirectory(path, options = {}) {
|
|
15818
|
+
let stat;
|
|
15565
15819
|
try {
|
|
15566
|
-
|
|
15567
|
-
const unsafeMode = options.private ? (stat.mode & 511) !== 448 : (stat.mode & 18) !== 0;
|
|
15568
|
-
if (!stat.isDirectory() || stat.isSymbolicLink() || unsafeMode || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("account_owner_association_invalid");
|
|
15569
|
-
return true;
|
|
15820
|
+
stat = lstatSync(path);
|
|
15570
15821
|
} catch (error) {
|
|
15571
15822
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
|
|
15572
15823
|
throw error;
|
|
15573
15824
|
}
|
|
15825
|
+
const problem = directoryProblem(stat, options);
|
|
15826
|
+
if (problem) throw new AccountOwnerAssociationDirectoryError(path, problem);
|
|
15827
|
+
return true;
|
|
15574
15828
|
}
|
|
15575
15829
|
/** Missing directories stay missing during reads. Never follow a state-directory symlink. */
|
|
15576
15830
|
function readAccountOwnerAssociationDirectory(path) {
|
|
@@ -15877,6 +16131,97 @@ function retainGeneration(profile, review, context) {
|
|
|
15877
16131
|
return retained.record;
|
|
15878
16132
|
}
|
|
15879
16133
|
//#endregion
|
|
16134
|
+
//#region src/account/owner-recovery-candidates.ts
|
|
16135
|
+
function historyRows(response) {
|
|
16136
|
+
const history = response?.legacy_history;
|
|
16137
|
+
return Array.isArray(history) ? history : [];
|
|
16138
|
+
}
|
|
16139
|
+
/** Return [identity, group] pairs for this device, or none when the record is unreadable. */
|
|
16140
|
+
function recordedOwners(row, deviceId) {
|
|
16141
|
+
const record = row;
|
|
16142
|
+
const groupId = record?.group_id;
|
|
16143
|
+
if (!recoveryId(groupId)) return [];
|
|
16144
|
+
let manifest;
|
|
16145
|
+
try {
|
|
16146
|
+
manifest = normalizeCoordinatorLegacyTeamCompletionManifest(record?.manifest);
|
|
16147
|
+
} catch {
|
|
16148
|
+
return [];
|
|
16149
|
+
}
|
|
16150
|
+
return manifest.device_decisions.flatMap((decision) => {
|
|
16151
|
+
const identityId = decision.identity_id;
|
|
16152
|
+
if (decision.device_id !== deviceId || !recoveryId(identityId)) return [];
|
|
16153
|
+
if (!legacyDecisionCountsAsOwner(decision)) return [];
|
|
16154
|
+
return [[identityId, groupId]];
|
|
16155
|
+
});
|
|
16156
|
+
}
|
|
16157
|
+
/** Map each Identity recorded for this device to the groups whose Team history records it. */
|
|
16158
|
+
function recordedOwnerGroups(responses, device) {
|
|
16159
|
+
const owners = /* @__PURE__ */ new Map();
|
|
16160
|
+
const pairs = responses.flatMap(historyRows).flatMap((row) => recordedOwners(row, device.deviceId));
|
|
16161
|
+
for (const [identityId, groupId] of pairs) owners.set(identityId, /* @__PURE__ */ new Set([...owners.get(identityId) ?? [], groupId]));
|
|
16162
|
+
return owners;
|
|
16163
|
+
}
|
|
16164
|
+
var MAX_LABEL_LENGTH = 256;
|
|
16165
|
+
var LISTED_GROUPS = 3;
|
|
16166
|
+
function candidateLabel(prefix, groups) {
|
|
16167
|
+
const sorted = [...groups].sort();
|
|
16168
|
+
const listed = sorted.slice(0, LISTED_GROUPS).join(", ");
|
|
16169
|
+
const more = sorted.length - LISTED_GROUPS;
|
|
16170
|
+
const label = `${prefix} recorded for this device in ${more > 0 ? `${listed} and ${more} more` : listed}`;
|
|
16171
|
+
if (label.length <= MAX_LABEL_LENGTH) return label;
|
|
16172
|
+
return `${prefix} recorded for this device in ${sorted.length === 1 ? "1 group" : `${sorted.length} groups`}`;
|
|
16173
|
+
}
|
|
16174
|
+
var MAX_CANDIDATES = 256;
|
|
16175
|
+
function candidateList(owners, currentIdentityId) {
|
|
16176
|
+
const rank = (id) => id === currentIdentityId ? 0 : 1;
|
|
16177
|
+
return [...owners.entries()].sort(([left], [right]) => rank(left) - rank(right) || left.localeCompare(right)).slice(0, MAX_CANDIDATES).map(([id, groups]) => {
|
|
16178
|
+
return {
|
|
16179
|
+
id,
|
|
16180
|
+
label: candidateLabel(id === currentIdentityId ? "This profile's Identity," : "Identity", [...groups])
|
|
16181
|
+
};
|
|
16182
|
+
});
|
|
16183
|
+
}
|
|
16184
|
+
/**
|
|
16185
|
+
* The coordinator returns this device's history across all groups once it finds an
|
|
16186
|
+
* enrollment. It hides the enrollment on an identity mismatch but still sends the
|
|
16187
|
+
* history, so non-empty history also counts as complete.
|
|
16188
|
+
*/
|
|
16189
|
+
function hasCompleteHistory(response) {
|
|
16190
|
+
const row = response;
|
|
16191
|
+
return row?.enrollment !== null && typeof row?.enrollment === "object" || historyRows(response).length > 0;
|
|
16192
|
+
}
|
|
16193
|
+
/**
|
|
16194
|
+
* Suggest owners from read-only coordinator previews of this device's current review.
|
|
16195
|
+
* Suggestions are display hints: the scoped owner preview still checks the chosen ID.
|
|
16196
|
+
*
|
|
16197
|
+
* Previews share the coordinator's admin review rate limit with the owner preview and
|
|
16198
|
+
* confirmation. History is filtered by device, not group, so this asks one group at a
|
|
16199
|
+
* time and stops at the first enrolled group instead of spending one request per group.
|
|
16200
|
+
*/
|
|
16201
|
+
async function readRecoveryCandidates(options) {
|
|
16202
|
+
const { device } = options;
|
|
16203
|
+
const responses = [];
|
|
16204
|
+
for (const groupId of options.groups) {
|
|
16205
|
+
let response;
|
|
16206
|
+
try {
|
|
16207
|
+
response = await options.previewDeviceOwner({
|
|
16208
|
+
remoteUrl: options.remoteUrl,
|
|
16209
|
+
adminSecret: options.adminSecret,
|
|
16210
|
+
groupId,
|
|
16211
|
+
deviceId: device.deviceId,
|
|
16212
|
+
fingerprint: device.fingerprint,
|
|
16213
|
+
identityId: device.identityId,
|
|
16214
|
+
timeoutS: options.timeoutS
|
|
16215
|
+
});
|
|
16216
|
+
} catch {
|
|
16217
|
+
continue;
|
|
16218
|
+
}
|
|
16219
|
+
responses.push(response);
|
|
16220
|
+
if (hasCompleteHistory(response)) break;
|
|
16221
|
+
}
|
|
16222
|
+
return candidateList(recordedOwnerGroups(responses, device), device.identityId);
|
|
16223
|
+
}
|
|
16224
|
+
//#endregion
|
|
15880
16225
|
//#region src/account/owner-recovery-context.ts
|
|
15881
16226
|
function recoverySha(value) {
|
|
15882
16227
|
return createHash("sha256").update(value).digest("hex");
|
|
@@ -15954,8 +16299,7 @@ function assertNoCompetingOwner(destinations, ownerIdentityId) {
|
|
|
15954
16299
|
requireRecovery(!existing || existing.ownerIdentityId === ownerIdentityId);
|
|
15955
16300
|
}
|
|
15956
16301
|
}
|
|
15957
|
-
|
|
15958
|
-
//#region src/account/owner-recovery-service.ts
|
|
16302
|
+
var CANDIDATE_TTL_MS = 6e4;
|
|
15959
16303
|
function groupAction(state) {
|
|
15960
16304
|
if (state === "ready") return "new";
|
|
15961
16305
|
if (state === "restored") return "reuse";
|
|
@@ -15971,6 +16315,7 @@ function staleDenial(error) {
|
|
|
15971
16315
|
var AccountOwnerRecoveryService = class {
|
|
15972
16316
|
#options;
|
|
15973
16317
|
#handles = /* @__PURE__ */ new Map();
|
|
16318
|
+
#candidateCache;
|
|
15974
16319
|
#reservations = /* @__PURE__ */ new Set();
|
|
15975
16320
|
#controller = new AbortController();
|
|
15976
16321
|
#closed = false;
|
|
@@ -15980,6 +16325,10 @@ var AccountOwnerRecoveryService = class {
|
|
|
15980
16325
|
#now() {
|
|
15981
16326
|
return (this.#options.now ?? Date.now)();
|
|
15982
16327
|
}
|
|
16328
|
+
#timeoutS() {
|
|
16329
|
+
const configured = this.#options.readCoordinatorTimeoutS?.() ?? 0;
|
|
16330
|
+
return Math.max(10, Number.isFinite(configured) ? configured : 0);
|
|
16331
|
+
}
|
|
15983
16332
|
#assertCurrent(capture, reservation) {
|
|
15984
16333
|
requireRecovery(!this.#closed, "unavailable");
|
|
15985
16334
|
requireRecovery(!reservation || reservation.isCurrent(), "context_changed");
|
|
@@ -16010,14 +16359,39 @@ var AccountOwnerRecoveryService = class {
|
|
|
16010
16359
|
const capture = this.#capture();
|
|
16011
16360
|
const coordinatorId = await this.#discover(capture);
|
|
16012
16361
|
this.#assertCurrent(capture);
|
|
16013
|
-
const
|
|
16362
|
+
const adminSecret = this.#options.readAdminSecret();
|
|
16363
|
+
const candidates = adminSecret ? await this.#candidates(capture, adminSecret) : [];
|
|
16014
16364
|
this.#assertCurrent(capture);
|
|
16015
16365
|
return {
|
|
16016
16366
|
version: 1,
|
|
16017
16367
|
profile: recoveryProfile(capture, coordinatorId),
|
|
16018
|
-
reviewAccess,
|
|
16019
|
-
candidates
|
|
16368
|
+
reviewAccess: adminSecret ? "operator" : "request",
|
|
16369
|
+
candidates
|
|
16370
|
+
};
|
|
16371
|
+
}
|
|
16372
|
+
async #candidates(capture, adminSecret) {
|
|
16373
|
+
const cached = this.#candidateCache;
|
|
16374
|
+
if (cached?.semanticId === capture.semanticId && this.#now() - cached.at < CANDIDATE_TTL_MS) return structuredClone(cached.candidates);
|
|
16375
|
+
const c = capture.context;
|
|
16376
|
+
const candidates = await readRecoveryCandidates({
|
|
16377
|
+
previewDeviceOwner: this.#options.previewDeviceOwner ?? coordinatorAuthControllerReviewAction,
|
|
16378
|
+
remoteUrl: c.coordinatorUrl,
|
|
16379
|
+
adminSecret,
|
|
16380
|
+
groups: capture.groups,
|
|
16381
|
+
device: {
|
|
16382
|
+
deviceId: c.deviceId,
|
|
16383
|
+
fingerprint: c.fingerprint,
|
|
16384
|
+
identityId: c.identityId
|
|
16385
|
+
},
|
|
16386
|
+
timeoutS: this.#timeoutS()
|
|
16387
|
+
});
|
|
16388
|
+
this.#assertCurrent(capture);
|
|
16389
|
+
this.#candidateCache = {
|
|
16390
|
+
semanticId: capture.semanticId,
|
|
16391
|
+
at: this.#now(),
|
|
16392
|
+
candidates
|
|
16020
16393
|
};
|
|
16394
|
+
return structuredClone(candidates);
|
|
16021
16395
|
}
|
|
16022
16396
|
#reviewInput(capture, selection, groupId) {
|
|
16023
16397
|
const adminSecret = this.#options.readAdminSecret();
|
|
@@ -16031,7 +16405,8 @@ var AccountOwnerRecoveryService = class {
|
|
|
16031
16405
|
groupId,
|
|
16032
16406
|
deviceId: capture.context.deviceId,
|
|
16033
16407
|
fingerprint: capture.context.fingerprint,
|
|
16034
|
-
identityId: selection.existingOwnerId
|
|
16408
|
+
identityId: selection.existingOwnerId,
|
|
16409
|
+
timeoutS: this.#timeoutS()
|
|
16035
16410
|
};
|
|
16036
16411
|
}
|
|
16037
16412
|
async #previewReview(entry, groupId) {
|
|
@@ -16197,7 +16572,7 @@ var AccountOwnerRecoveryService = class {
|
|
|
16197
16572
|
return { outcome: {
|
|
16198
16573
|
groupId,
|
|
16199
16574
|
status: staleDenial(error) ? "failed" : "unknown",
|
|
16200
|
-
message: "Check coordinator status or import original receipts before trying again."
|
|
16575
|
+
message: unsafeStorageMessage(error) ?? "Check coordinator status or import original receipts before trying again."
|
|
16201
16576
|
} };
|
|
16202
16577
|
}
|
|
16203
16578
|
}
|
|
@@ -16272,11 +16647,11 @@ var AccountOwnerRecoveryService = class {
|
|
|
16272
16647
|
async #importGroup(entry, groupId, receipt, reservation) {
|
|
16273
16648
|
try {
|
|
16274
16649
|
return await this.#verifyAndRetain(entry, groupId, receipt, reservation);
|
|
16275
|
-
} catch {
|
|
16650
|
+
} catch (error) {
|
|
16276
16651
|
return {
|
|
16277
16652
|
groupId,
|
|
16278
16653
|
status: "unknown",
|
|
16279
|
-
message: "Context or proof outcome changed; check status before importing again."
|
|
16654
|
+
message: unsafeStorageMessage(error) ?? "Context or proof outcome changed; check status before importing again."
|
|
16280
16655
|
};
|
|
16281
16656
|
}
|
|
16282
16657
|
}
|
|
@@ -16291,6 +16666,143 @@ function createAccountOwnerRecoveryService(options) {
|
|
|
16291
16666
|
return new AccountOwnerRecoveryService(options);
|
|
16292
16667
|
}
|
|
16293
16668
|
//#endregion
|
|
16669
|
+
//#region src/account/rebind.ts
|
|
16670
|
+
/**
|
|
16671
|
+
* Viewer side of "Sign in with Google" when the account belongs to another
|
|
16672
|
+
* Identity (ADR 0005 Section 8): run the coordinator rebind, then switch this
|
|
16673
|
+
* device's local Identity. An unclear outcome stays recorded and resumes.
|
|
16674
|
+
*/
|
|
16675
|
+
/** Pins the local facts the person reviewed; the coordinator stores and signs it. */
|
|
16676
|
+
function reviewDigest(store, context) {
|
|
16677
|
+
const binding = store.db.prepare("SELECT identity_id, assignment_version FROM identity_devices WHERE device_id = ?").get(context.deviceId);
|
|
16678
|
+
const localActors = store.db.prepare("SELECT actor_id FROM actors WHERE is_local = 1 ORDER BY actor_id").pluck().all();
|
|
16679
|
+
return createHash("sha256").update(JSON.stringify([
|
|
16680
|
+
"codemem-device-rebind-review-v1",
|
|
16681
|
+
context.deviceId,
|
|
16682
|
+
context.fingerprint,
|
|
16683
|
+
context.identityId,
|
|
16684
|
+
binding ?? null,
|
|
16685
|
+
localActors
|
|
16686
|
+
])).digest("hex");
|
|
16687
|
+
}
|
|
16688
|
+
/**
|
|
16689
|
+
* Finishes the local half from a coordinator receipt. Each step is
|
|
16690
|
+
* idempotent, so a later resume can repeat it.
|
|
16691
|
+
*/
|
|
16692
|
+
function finishLocalSwitch(store, paths, attemptId, receipt) {
|
|
16693
|
+
try {
|
|
16694
|
+
if (receipt) markLocalIdentitySwitchBound(store.db, receipt);
|
|
16695
|
+
const context = { configPath: paths.configPath };
|
|
16696
|
+
applyLocalIdentitySwitch(store.db, {
|
|
16697
|
+
attemptId,
|
|
16698
|
+
displayName: store.actorDisplayName,
|
|
16699
|
+
expectedPublicKey: loadPublicKey(paths.keysDir) ?? ""
|
|
16700
|
+
}, context);
|
|
16701
|
+
completeLocalIdentitySwitch(store.db, {
|
|
16702
|
+
attemptId,
|
|
16703
|
+
configPath: paths.configPath
|
|
16704
|
+
});
|
|
16705
|
+
const target = readTarget(store, attemptId);
|
|
16706
|
+
if (!target || !store.refreshPersistedLocalIdentity(target)) return { kind: "pending" };
|
|
16707
|
+
return {
|
|
16708
|
+
kind: "switched",
|
|
16709
|
+
targetIdentityId: target
|
|
16710
|
+
};
|
|
16711
|
+
} catch {
|
|
16712
|
+
return { kind: "pending" };
|
|
16713
|
+
}
|
|
16714
|
+
}
|
|
16715
|
+
function readTarget(store, attemptId) {
|
|
16716
|
+
const row = store.db.prepare("SELECT target_identity_id FROM local_identity_switches WHERE attempt_id = ? AND state = 'completed'").pluck().get(attemptId);
|
|
16717
|
+
return typeof row === "string" ? row : null;
|
|
16718
|
+
}
|
|
16719
|
+
function createAccountRebindRunner(store) {
|
|
16720
|
+
return async ({ context, signal, onBrowserStart, isCurrent = () => true }) => {
|
|
16721
|
+
try {
|
|
16722
|
+
assertNoLocalIdentityOverride();
|
|
16723
|
+
} catch {
|
|
16724
|
+
return { kind: "failed" };
|
|
16725
|
+
}
|
|
16726
|
+
const earlier = await resolveOpenSwitch(store, context);
|
|
16727
|
+
if (earlier) return earlier;
|
|
16728
|
+
const recorded = {};
|
|
16729
|
+
return settleOutcome(store, context, await runRebind(store, context, {
|
|
16730
|
+
signal,
|
|
16731
|
+
onBrowserStart,
|
|
16732
|
+
recorded,
|
|
16733
|
+
isCurrent
|
|
16734
|
+
}), recorded.attemptId);
|
|
16735
|
+
};
|
|
16736
|
+
}
|
|
16737
|
+
/** An earlier switch left open is resolved first; no new Google trip until it is. */
|
|
16738
|
+
async function resolveOpenSwitch(store, context) {
|
|
16739
|
+
if (!readOpenLocalIdentitySwitch(store.db)) return null;
|
|
16740
|
+
const resumed = await resumeAccountRebind(store, context);
|
|
16741
|
+
if (resumed?.kind === "switched") return resumed;
|
|
16742
|
+
return readOpenLocalIdentitySwitch(store.db) ? { kind: "pending" } : null;
|
|
16743
|
+
}
|
|
16744
|
+
/**
|
|
16745
|
+
* A refusal or failure means this run's finalize did not commit, and nothing
|
|
16746
|
+
* else sends it, so the attempt this run recorded can close at once.
|
|
16747
|
+
*/
|
|
16748
|
+
function settleOutcome(store, context, outcome, recordedAttemptId) {
|
|
16749
|
+
if ((outcome.kind === "refused" || outcome.kind === "failed") && recordedAttemptId) markLocalIdentitySwitchNotCommitted(store.db, recordedAttemptId);
|
|
16750
|
+
if (outcome.kind === "refused") return {
|
|
16751
|
+
kind: "refused",
|
|
16752
|
+
reason: outcome.reason,
|
|
16753
|
+
...outcome.beforeBrowser ? { beforeBrowser: true } : {}
|
|
16754
|
+
};
|
|
16755
|
+
if (outcome.kind === "failed") return { kind: "failed" };
|
|
16756
|
+
if (outcome.kind === "unknown") return isSelfBind(outcome.pending) ? { kind: "bind_unknown" } : { kind: "pending" };
|
|
16757
|
+
if (isSelfBind(outcome.receipt)) return { kind: "bound" };
|
|
16758
|
+
return finishLocalSwitch(store, context, outcome.receipt.attemptId, outcome.receipt);
|
|
16759
|
+
}
|
|
16760
|
+
/** A self-bind keeps this device's Identity, so it has no local switch. */
|
|
16761
|
+
function isSelfBind(attempt) {
|
|
16762
|
+
return attempt.targetIdentityId === attempt.previousIdentityId;
|
|
16763
|
+
}
|
|
16764
|
+
function runRebind(store, context, run) {
|
|
16765
|
+
return runDeviceRebind({
|
|
16766
|
+
dbPath: context.dbPath,
|
|
16767
|
+
keysDir: context.keysDir,
|
|
16768
|
+
coordinatorUrl: context.coordinatorUrl,
|
|
16769
|
+
groupId: context.groupId,
|
|
16770
|
+
currentIdentityId: context.identityId,
|
|
16771
|
+
reviewDigest: reviewDigest(store, context),
|
|
16772
|
+
openBrowser: run.onBrowserStart,
|
|
16773
|
+
recordPending: (pending) => {
|
|
16774
|
+
if (!run.isCurrent()) throw new Error("device_rebind_context_changed");
|
|
16775
|
+
if (reviewDigest(store, context) !== pending.reviewDigest) throw new Error("device_rebind_review_changed");
|
|
16776
|
+
if (isSelfBind(pending)) return;
|
|
16777
|
+
recordLocalIdentitySwitch(store.db, pending, { configPath: context.configPath });
|
|
16778
|
+
run.recorded.attemptId = pending.attemptId;
|
|
16779
|
+
},
|
|
16780
|
+
signal: run.signal
|
|
16781
|
+
});
|
|
16782
|
+
}
|
|
16783
|
+
/**
|
|
16784
|
+
* Picks up a switch an earlier run left open. A recorded attempt is resolved
|
|
16785
|
+
* by reading coordinator status, never by sending finalize again.
|
|
16786
|
+
*/
|
|
16787
|
+
async function resumeAccountRebind(store, paths) {
|
|
16788
|
+
const open = readOpenLocalIdentitySwitch(store.db);
|
|
16789
|
+
if (!open) return null;
|
|
16790
|
+
if (open.state !== "pending") return finishLocalSwitch(store, paths, open.attemptId);
|
|
16791
|
+
const resumed = await resumeDeviceRebind({
|
|
16792
|
+
dbPath: store.dbPath,
|
|
16793
|
+
keysDir: paths.keysDir,
|
|
16794
|
+
coordinatorUrl: open.coordinatorOrigin,
|
|
16795
|
+
groupId: open.groupId,
|
|
16796
|
+
pending: open
|
|
16797
|
+
});
|
|
16798
|
+
if (resumed.kind === "finalized") return finishLocalSwitch(store, paths, open.attemptId, resumed.receipt);
|
|
16799
|
+
if (resumed.kind === "not_committed") {
|
|
16800
|
+
markLocalIdentitySwitchNotCommitted(store.db, open.attemptId);
|
|
16801
|
+
return { kind: "failed" };
|
|
16802
|
+
}
|
|
16803
|
+
return { kind: "pending" };
|
|
16804
|
+
}
|
|
16805
|
+
//#endregion
|
|
16294
16806
|
//#region src/index.ts
|
|
16295
16807
|
/**
|
|
16296
16808
|
* @codemem/server — HTTP server (viewer, sync, API).
|
|
@@ -16443,6 +16955,6 @@ function createSyncApp(opts) {
|
|
|
16443
16955
|
return app;
|
|
16444
16956
|
}
|
|
16445
16957
|
//#endregion
|
|
16446
|
-
export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, closeStoreAsync, createAccountContextReader, createAccountLinkRegistry, createAccountOwnerRecoveryService, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory };
|
|
16958
|
+
export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, closeStoreAsync, createAccountContextReader, createAccountLinkRegistry, createAccountOwnerRecoveryService, createAccountRebindRunner, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory, resumeAccountRebind };
|
|
16447
16959
|
|
|
16448
16960
|
//# sourceMappingURL=index.js.map
|