@codemem/server 0.47.0-beta.4 → 0.47.0
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 +30 -0
- package/dist/account/owner-recovery-candidates.d.ts.map +1 -0
- package/dist/account/owner-recovery-evidence.d.ts.map +1 -1
- package/dist/account/owner-recovery-service.d.ts +4 -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 +504 -38
- package/dist/index.js.map +1 -1
- package/dist/routes/account-operations.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 +1 -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
|
}
|
|
@@ -1100,7 +1307,7 @@ function readWeakLabel(value) {
|
|
|
1100
1307
|
effect: row.effect
|
|
1101
1308
|
};
|
|
1102
1309
|
}
|
|
1103
|
-
function historyRows(value, selection, labels) {
|
|
1310
|
+
function historyRows$1(value, selection, labels) {
|
|
1104
1311
|
const row = recoveryRecord(value);
|
|
1105
1312
|
requireRecovery(recoveryId(row.group_id) && recoveryId(row.candidate_ref) && timestamp(row.created_at));
|
|
1106
1313
|
const manifest = normalizeCoordinatorLegacyTeamCompletionManifest(row.manifest);
|
|
@@ -1118,7 +1325,7 @@ function historyRows(value, selection, labels) {
|
|
|
1118
1325
|
identityId: decision.identity_id ?? "",
|
|
1119
1326
|
assignment: decision.decision,
|
|
1120
1327
|
relation,
|
|
1121
|
-
reason: "
|
|
1328
|
+
reason: legacyDecisionCountsAsOwner(decision) ? "Recorded in Team history." : "Guessed by another device in Team history."
|
|
1122
1329
|
};
|
|
1123
1330
|
});
|
|
1124
1331
|
}
|
|
@@ -1156,7 +1363,7 @@ function validateRecoveryReview(value, selection, groupId) {
|
|
|
1156
1363
|
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
1364
|
const labels = row.reviewed_weak_labels.map(readWeakLabel);
|
|
1158
1365
|
requireRecovery(new Set(labels.map((label) => JSON.stringify(label))).size === labels.length);
|
|
1159
|
-
const rows = row.legacy_history.flatMap((history) => historyRows(history, selection, labels));
|
|
1366
|
+
const rows = row.legacy_history.flatMap((history) => historyRows$1(history, selection, labels));
|
|
1160
1367
|
requireRecovery(row.reasons.length <= 8 && row.reasons.every((reason) => typeof reason === "string" && RECOVERY_REASON.test(reason)));
|
|
1161
1368
|
const reasons = row.reasons;
|
|
1162
1369
|
if (row.state === "needs_review") {
|
|
@@ -6804,6 +7011,24 @@ function projectMappingChoices(store) {
|
|
|
6804
7011
|
sourceDisplayName: candidate.display_project
|
|
6805
7012
|
}));
|
|
6806
7013
|
}
|
|
7014
|
+
function viewerSafeDevice(candidateRef, device) {
|
|
7015
|
+
return {
|
|
7016
|
+
deviceRef: device.deviceRef,
|
|
7017
|
+
displayName: device.displayName,
|
|
7018
|
+
enabled: device.enabled,
|
|
7019
|
+
existingIdentityRef: identityRef(candidateRef, device.existingIdentityId),
|
|
7020
|
+
suggestedIdentityRef: identityRef(candidateRef, device.suggestedIdentityId),
|
|
7021
|
+
verifiedEvidenceKind: device.verifiedEvidenceKind,
|
|
7022
|
+
identityEvidence: device.identityEvidence,
|
|
7023
|
+
decision: device.decision,
|
|
7024
|
+
targetIdentityRef: identityRef(candidateRef, device.targetIdentityId),
|
|
7025
|
+
expectation: device.expectation.kind === "existing" ? {
|
|
7026
|
+
kind: "existing",
|
|
7027
|
+
assignmentVersion: device.expectation.assignmentVersion,
|
|
7028
|
+
identityRef: requiredIdentityRef(candidateRef, device.expectation.identityId)
|
|
7029
|
+
} : { kind: "absent" }
|
|
7030
|
+
};
|
|
7031
|
+
}
|
|
6807
7032
|
function viewerSafeDraft(store, draft) {
|
|
6808
7033
|
if (draft.devices.length > MAX_DEVICES || draft.projects.length > MAX_PROJECTS) throw new Error("legacy_team_setup_roster_too_large");
|
|
6809
7034
|
const completed = draft.state === "completed";
|
|
@@ -6813,21 +7038,7 @@ function viewerSafeDraft(store, draft) {
|
|
|
6813
7038
|
requireCompleteMappingChoices(mappableProjectCount, mappingChoices.length);
|
|
6814
7039
|
const mappingChoiceIdentities = mappingChoices.map((choice) => choice.projectIdentity);
|
|
6815
7040
|
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
|
-
})),
|
|
7041
|
+
devices: draft.devices.map((device) => viewerSafeDevice(draft.candidateRef, device)),
|
|
6831
7042
|
projects: draft.projects.map((project) => ({
|
|
6832
7043
|
projectRef: project.projectRef,
|
|
6833
7044
|
displayName: project.displayName,
|
|
@@ -9799,6 +10010,7 @@ function isPendingCaptureActorConflict(error) {
|
|
|
9799
10010
|
/** False leaves the conflict untouched; callers must check before claiming or merging identity. */
|
|
9800
10011
|
function ensureLocalActorRecord(store) {
|
|
9801
10012
|
return store.db.transaction(() => {
|
|
10013
|
+
store.assertLocalIdentityCurrent();
|
|
9802
10014
|
try {
|
|
9803
10015
|
store.assertPendingLocalCaptureAdoptionActors();
|
|
9804
10016
|
} catch (error) {
|
|
@@ -15358,6 +15570,7 @@ sqliteTable("legacy_team_setup_draft_devices", {
|
|
|
15358
15570
|
existing_identity_id: text("existing_identity_id"),
|
|
15359
15571
|
existing_assignment_version: integer("existing_assignment_version"),
|
|
15360
15572
|
verified_evidence_kind: text("verified_evidence_kind"),
|
|
15573
|
+
identity_evidence: text("identity_evidence"),
|
|
15361
15574
|
decision: text("decision").notNull().default("unresolved"),
|
|
15362
15575
|
target_identity_id: text("target_identity_id"),
|
|
15363
15576
|
expected_assignment_kind: text("expected_assignment_kind"),
|
|
@@ -15877,6 +16090,96 @@ function retainGeneration(profile, review, context) {
|
|
|
15877
16090
|
return retained.record;
|
|
15878
16091
|
}
|
|
15879
16092
|
//#endregion
|
|
16093
|
+
//#region src/account/owner-recovery-candidates.ts
|
|
16094
|
+
function historyRows(response) {
|
|
16095
|
+
const history = response?.legacy_history;
|
|
16096
|
+
return Array.isArray(history) ? history : [];
|
|
16097
|
+
}
|
|
16098
|
+
/** Return [identity, group] pairs for this device, or none when the record is unreadable. */
|
|
16099
|
+
function recordedOwners(row, deviceId) {
|
|
16100
|
+
const record = row;
|
|
16101
|
+
const groupId = record?.group_id;
|
|
16102
|
+
if (!recoveryId(groupId)) return [];
|
|
16103
|
+
let manifest;
|
|
16104
|
+
try {
|
|
16105
|
+
manifest = normalizeCoordinatorLegacyTeamCompletionManifest(record?.manifest);
|
|
16106
|
+
} catch {
|
|
16107
|
+
return [];
|
|
16108
|
+
}
|
|
16109
|
+
return manifest.device_decisions.flatMap((decision) => {
|
|
16110
|
+
const identityId = decision.identity_id;
|
|
16111
|
+
if (decision.device_id !== deviceId || !recoveryId(identityId)) return [];
|
|
16112
|
+
if (!legacyDecisionCountsAsOwner(decision)) return [];
|
|
16113
|
+
return [[identityId, groupId]];
|
|
16114
|
+
});
|
|
16115
|
+
}
|
|
16116
|
+
/** Map each Identity recorded for this device to the groups whose Team history records it. */
|
|
16117
|
+
function recordedOwnerGroups(responses, device) {
|
|
16118
|
+
const owners = /* @__PURE__ */ new Map();
|
|
16119
|
+
const pairs = responses.flatMap(historyRows).flatMap((row) => recordedOwners(row, device.deviceId));
|
|
16120
|
+
for (const [identityId, groupId] of pairs) owners.set(identityId, /* @__PURE__ */ new Set([...owners.get(identityId) ?? [], groupId]));
|
|
16121
|
+
return owners;
|
|
16122
|
+
}
|
|
16123
|
+
var MAX_LABEL_LENGTH = 256;
|
|
16124
|
+
var LISTED_GROUPS = 3;
|
|
16125
|
+
function candidateLabel(prefix, groups) {
|
|
16126
|
+
const sorted = [...groups].sort();
|
|
16127
|
+
const listed = sorted.slice(0, LISTED_GROUPS).join(", ");
|
|
16128
|
+
const more = sorted.length - LISTED_GROUPS;
|
|
16129
|
+
const label = `${prefix} recorded for this device in ${more > 0 ? `${listed} and ${more} more` : listed}`;
|
|
16130
|
+
if (label.length <= MAX_LABEL_LENGTH) return label;
|
|
16131
|
+
return `${prefix} recorded for this device in ${sorted.length === 1 ? "1 group" : `${sorted.length} groups`}`;
|
|
16132
|
+
}
|
|
16133
|
+
var MAX_CANDIDATES = 256;
|
|
16134
|
+
function candidateList(owners, currentIdentityId) {
|
|
16135
|
+
const rank = (id) => id === currentIdentityId ? 0 : 1;
|
|
16136
|
+
return [...owners.entries()].sort(([left], [right]) => rank(left) - rank(right) || left.localeCompare(right)).slice(0, MAX_CANDIDATES).map(([id, groups]) => {
|
|
16137
|
+
return {
|
|
16138
|
+
id,
|
|
16139
|
+
label: candidateLabel(id === currentIdentityId ? "This profile's Identity," : "Identity", [...groups])
|
|
16140
|
+
};
|
|
16141
|
+
});
|
|
16142
|
+
}
|
|
16143
|
+
/**
|
|
16144
|
+
* The coordinator returns this device's history across all groups once it finds an
|
|
16145
|
+
* enrollment. It hides the enrollment on an identity mismatch but still sends the
|
|
16146
|
+
* history, so non-empty history also counts as complete.
|
|
16147
|
+
*/
|
|
16148
|
+
function hasCompleteHistory(response) {
|
|
16149
|
+
const row = response;
|
|
16150
|
+
return row?.enrollment !== null && typeof row?.enrollment === "object" || historyRows(response).length > 0;
|
|
16151
|
+
}
|
|
16152
|
+
/**
|
|
16153
|
+
* Suggest owners from read-only coordinator previews of this device's current review.
|
|
16154
|
+
* Suggestions are display hints: the scoped owner preview still checks the chosen ID.
|
|
16155
|
+
*
|
|
16156
|
+
* Previews share the coordinator's admin review rate limit with the owner preview and
|
|
16157
|
+
* confirmation. History is filtered by device, not group, so this asks one group at a
|
|
16158
|
+
* time and stops at the first enrolled group instead of spending one request per group.
|
|
16159
|
+
*/
|
|
16160
|
+
async function readRecoveryCandidates(options) {
|
|
16161
|
+
const { device } = options;
|
|
16162
|
+
const responses = [];
|
|
16163
|
+
for (const groupId of options.groups) {
|
|
16164
|
+
let response;
|
|
16165
|
+
try {
|
|
16166
|
+
response = await options.previewDeviceOwner({
|
|
16167
|
+
remoteUrl: options.remoteUrl,
|
|
16168
|
+
adminSecret: options.adminSecret,
|
|
16169
|
+
groupId,
|
|
16170
|
+
deviceId: device.deviceId,
|
|
16171
|
+
fingerprint: device.fingerprint,
|
|
16172
|
+
identityId: device.identityId
|
|
16173
|
+
});
|
|
16174
|
+
} catch {
|
|
16175
|
+
continue;
|
|
16176
|
+
}
|
|
16177
|
+
responses.push(response);
|
|
16178
|
+
if (hasCompleteHistory(response)) break;
|
|
16179
|
+
}
|
|
16180
|
+
return candidateList(recordedOwnerGroups(responses, device), device.identityId);
|
|
16181
|
+
}
|
|
16182
|
+
//#endregion
|
|
15880
16183
|
//#region src/account/owner-recovery-context.ts
|
|
15881
16184
|
function recoverySha(value) {
|
|
15882
16185
|
return createHash("sha256").update(value).digest("hex");
|
|
@@ -15956,6 +16259,7 @@ function assertNoCompetingOwner(destinations, ownerIdentityId) {
|
|
|
15956
16259
|
}
|
|
15957
16260
|
//#endregion
|
|
15958
16261
|
//#region src/account/owner-recovery-service.ts
|
|
16262
|
+
var CANDIDATE_TTL_MS = 6e4;
|
|
15959
16263
|
function groupAction(state) {
|
|
15960
16264
|
if (state === "ready") return "new";
|
|
15961
16265
|
if (state === "restored") return "reuse";
|
|
@@ -15971,6 +16275,7 @@ function staleDenial(error) {
|
|
|
15971
16275
|
var AccountOwnerRecoveryService = class {
|
|
15972
16276
|
#options;
|
|
15973
16277
|
#handles = /* @__PURE__ */ new Map();
|
|
16278
|
+
#candidateCache;
|
|
15974
16279
|
#reservations = /* @__PURE__ */ new Set();
|
|
15975
16280
|
#controller = new AbortController();
|
|
15976
16281
|
#closed = false;
|
|
@@ -16010,14 +16315,38 @@ var AccountOwnerRecoveryService = class {
|
|
|
16010
16315
|
const capture = this.#capture();
|
|
16011
16316
|
const coordinatorId = await this.#discover(capture);
|
|
16012
16317
|
this.#assertCurrent(capture);
|
|
16013
|
-
const
|
|
16318
|
+
const adminSecret = this.#options.readAdminSecret();
|
|
16319
|
+
const candidates = adminSecret ? await this.#candidates(capture, adminSecret) : [];
|
|
16014
16320
|
this.#assertCurrent(capture);
|
|
16015
16321
|
return {
|
|
16016
16322
|
version: 1,
|
|
16017
16323
|
profile: recoveryProfile(capture, coordinatorId),
|
|
16018
|
-
reviewAccess,
|
|
16019
|
-
candidates
|
|
16324
|
+
reviewAccess: adminSecret ? "operator" : "request",
|
|
16325
|
+
candidates
|
|
16326
|
+
};
|
|
16327
|
+
}
|
|
16328
|
+
async #candidates(capture, adminSecret) {
|
|
16329
|
+
const cached = this.#candidateCache;
|
|
16330
|
+
if (cached?.semanticId === capture.semanticId && this.#now() - cached.at < CANDIDATE_TTL_MS) return structuredClone(cached.candidates);
|
|
16331
|
+
const c = capture.context;
|
|
16332
|
+
const candidates = await readRecoveryCandidates({
|
|
16333
|
+
previewDeviceOwner: this.#options.previewDeviceOwner ?? coordinatorAuthControllerReviewAction,
|
|
16334
|
+
remoteUrl: c.coordinatorUrl,
|
|
16335
|
+
adminSecret,
|
|
16336
|
+
groups: capture.groups,
|
|
16337
|
+
device: {
|
|
16338
|
+
deviceId: c.deviceId,
|
|
16339
|
+
fingerprint: c.fingerprint,
|
|
16340
|
+
identityId: c.identityId
|
|
16341
|
+
}
|
|
16342
|
+
});
|
|
16343
|
+
this.#assertCurrent(capture);
|
|
16344
|
+
this.#candidateCache = {
|
|
16345
|
+
semanticId: capture.semanticId,
|
|
16346
|
+
at: this.#now(),
|
|
16347
|
+
candidates
|
|
16020
16348
|
};
|
|
16349
|
+
return structuredClone(candidates);
|
|
16021
16350
|
}
|
|
16022
16351
|
#reviewInput(capture, selection, groupId) {
|
|
16023
16352
|
const adminSecret = this.#options.readAdminSecret();
|
|
@@ -16291,6 +16620,143 @@ function createAccountOwnerRecoveryService(options) {
|
|
|
16291
16620
|
return new AccountOwnerRecoveryService(options);
|
|
16292
16621
|
}
|
|
16293
16622
|
//#endregion
|
|
16623
|
+
//#region src/account/rebind.ts
|
|
16624
|
+
/**
|
|
16625
|
+
* Viewer side of "Sign in with Google" when the account belongs to another
|
|
16626
|
+
* Identity (ADR 0005 Section 8): run the coordinator rebind, then switch this
|
|
16627
|
+
* device's local Identity. An unclear outcome stays recorded and resumes.
|
|
16628
|
+
*/
|
|
16629
|
+
/** Pins the local facts the person reviewed; the coordinator stores and signs it. */
|
|
16630
|
+
function reviewDigest(store, context) {
|
|
16631
|
+
const binding = store.db.prepare("SELECT identity_id, assignment_version FROM identity_devices WHERE device_id = ?").get(context.deviceId);
|
|
16632
|
+
const localActors = store.db.prepare("SELECT actor_id FROM actors WHERE is_local = 1 ORDER BY actor_id").pluck().all();
|
|
16633
|
+
return createHash("sha256").update(JSON.stringify([
|
|
16634
|
+
"codemem-device-rebind-review-v1",
|
|
16635
|
+
context.deviceId,
|
|
16636
|
+
context.fingerprint,
|
|
16637
|
+
context.identityId,
|
|
16638
|
+
binding ?? null,
|
|
16639
|
+
localActors
|
|
16640
|
+
])).digest("hex");
|
|
16641
|
+
}
|
|
16642
|
+
/**
|
|
16643
|
+
* Finishes the local half from a coordinator receipt. Each step is
|
|
16644
|
+
* idempotent, so a later resume can repeat it.
|
|
16645
|
+
*/
|
|
16646
|
+
function finishLocalSwitch(store, paths, attemptId, receipt) {
|
|
16647
|
+
try {
|
|
16648
|
+
if (receipt) markLocalIdentitySwitchBound(store.db, receipt);
|
|
16649
|
+
const context = { configPath: paths.configPath };
|
|
16650
|
+
applyLocalIdentitySwitch(store.db, {
|
|
16651
|
+
attemptId,
|
|
16652
|
+
displayName: store.actorDisplayName,
|
|
16653
|
+
expectedPublicKey: loadPublicKey(paths.keysDir) ?? ""
|
|
16654
|
+
}, context);
|
|
16655
|
+
completeLocalIdentitySwitch(store.db, {
|
|
16656
|
+
attemptId,
|
|
16657
|
+
configPath: paths.configPath
|
|
16658
|
+
});
|
|
16659
|
+
const target = readTarget(store, attemptId);
|
|
16660
|
+
if (!target || !store.refreshPersistedLocalIdentity(target)) return { kind: "pending" };
|
|
16661
|
+
return {
|
|
16662
|
+
kind: "switched",
|
|
16663
|
+
targetIdentityId: target
|
|
16664
|
+
};
|
|
16665
|
+
} catch {
|
|
16666
|
+
return { kind: "pending" };
|
|
16667
|
+
}
|
|
16668
|
+
}
|
|
16669
|
+
function readTarget(store, attemptId) {
|
|
16670
|
+
const row = store.db.prepare("SELECT target_identity_id FROM local_identity_switches WHERE attempt_id = ? AND state = 'completed'").pluck().get(attemptId);
|
|
16671
|
+
return typeof row === "string" ? row : null;
|
|
16672
|
+
}
|
|
16673
|
+
function createAccountRebindRunner(store) {
|
|
16674
|
+
return async ({ context, signal, onBrowserStart, isCurrent = () => true }) => {
|
|
16675
|
+
try {
|
|
16676
|
+
assertNoLocalIdentityOverride();
|
|
16677
|
+
} catch {
|
|
16678
|
+
return { kind: "failed" };
|
|
16679
|
+
}
|
|
16680
|
+
const earlier = await resolveOpenSwitch(store, context);
|
|
16681
|
+
if (earlier) return earlier;
|
|
16682
|
+
const recorded = {};
|
|
16683
|
+
return settleOutcome(store, context, await runRebind(store, context, {
|
|
16684
|
+
signal,
|
|
16685
|
+
onBrowserStart,
|
|
16686
|
+
recorded,
|
|
16687
|
+
isCurrent
|
|
16688
|
+
}), recorded.attemptId);
|
|
16689
|
+
};
|
|
16690
|
+
}
|
|
16691
|
+
/** An earlier switch left open is resolved first; no new Google trip until it is. */
|
|
16692
|
+
async function resolveOpenSwitch(store, context) {
|
|
16693
|
+
if (!readOpenLocalIdentitySwitch(store.db)) return null;
|
|
16694
|
+
const resumed = await resumeAccountRebind(store, context);
|
|
16695
|
+
if (resumed?.kind === "switched") return resumed;
|
|
16696
|
+
return readOpenLocalIdentitySwitch(store.db) ? { kind: "pending" } : null;
|
|
16697
|
+
}
|
|
16698
|
+
/**
|
|
16699
|
+
* A refusal or failure means this run's finalize did not commit, and nothing
|
|
16700
|
+
* else sends it, so the attempt this run recorded can close at once.
|
|
16701
|
+
*/
|
|
16702
|
+
function settleOutcome(store, context, outcome, recordedAttemptId) {
|
|
16703
|
+
if ((outcome.kind === "refused" || outcome.kind === "failed") && recordedAttemptId) markLocalIdentitySwitchNotCommitted(store.db, recordedAttemptId);
|
|
16704
|
+
if (outcome.kind === "refused") return {
|
|
16705
|
+
kind: "refused",
|
|
16706
|
+
reason: outcome.reason,
|
|
16707
|
+
...outcome.beforeBrowser ? { beforeBrowser: true } : {}
|
|
16708
|
+
};
|
|
16709
|
+
if (outcome.kind === "failed") return { kind: "failed" };
|
|
16710
|
+
if (outcome.kind === "unknown") return isSelfBind(outcome.pending) ? { kind: "bind_unknown" } : { kind: "pending" };
|
|
16711
|
+
if (isSelfBind(outcome.receipt)) return { kind: "bound" };
|
|
16712
|
+
return finishLocalSwitch(store, context, outcome.receipt.attemptId, outcome.receipt);
|
|
16713
|
+
}
|
|
16714
|
+
/** A self-bind keeps this device's Identity, so it has no local switch. */
|
|
16715
|
+
function isSelfBind(attempt) {
|
|
16716
|
+
return attempt.targetIdentityId === attempt.previousIdentityId;
|
|
16717
|
+
}
|
|
16718
|
+
function runRebind(store, context, run) {
|
|
16719
|
+
return runDeviceRebind({
|
|
16720
|
+
dbPath: context.dbPath,
|
|
16721
|
+
keysDir: context.keysDir,
|
|
16722
|
+
coordinatorUrl: context.coordinatorUrl,
|
|
16723
|
+
groupId: context.groupId,
|
|
16724
|
+
currentIdentityId: context.identityId,
|
|
16725
|
+
reviewDigest: reviewDigest(store, context),
|
|
16726
|
+
openBrowser: run.onBrowserStart,
|
|
16727
|
+
recordPending: (pending) => {
|
|
16728
|
+
if (!run.isCurrent()) throw new Error("device_rebind_context_changed");
|
|
16729
|
+
if (reviewDigest(store, context) !== pending.reviewDigest) throw new Error("device_rebind_review_changed");
|
|
16730
|
+
if (isSelfBind(pending)) return;
|
|
16731
|
+
recordLocalIdentitySwitch(store.db, pending, { configPath: context.configPath });
|
|
16732
|
+
run.recorded.attemptId = pending.attemptId;
|
|
16733
|
+
},
|
|
16734
|
+
signal: run.signal
|
|
16735
|
+
});
|
|
16736
|
+
}
|
|
16737
|
+
/**
|
|
16738
|
+
* Picks up a switch an earlier run left open. A recorded attempt is resolved
|
|
16739
|
+
* by reading coordinator status, never by sending finalize again.
|
|
16740
|
+
*/
|
|
16741
|
+
async function resumeAccountRebind(store, paths) {
|
|
16742
|
+
const open = readOpenLocalIdentitySwitch(store.db);
|
|
16743
|
+
if (!open) return null;
|
|
16744
|
+
if (open.state !== "pending") return finishLocalSwitch(store, paths, open.attemptId);
|
|
16745
|
+
const resumed = await resumeDeviceRebind({
|
|
16746
|
+
dbPath: store.dbPath,
|
|
16747
|
+
keysDir: paths.keysDir,
|
|
16748
|
+
coordinatorUrl: open.coordinatorOrigin,
|
|
16749
|
+
groupId: open.groupId,
|
|
16750
|
+
pending: open
|
|
16751
|
+
});
|
|
16752
|
+
if (resumed.kind === "finalized") return finishLocalSwitch(store, paths, open.attemptId, resumed.receipt);
|
|
16753
|
+
if (resumed.kind === "not_committed") {
|
|
16754
|
+
markLocalIdentitySwitchNotCommitted(store.db, open.attemptId);
|
|
16755
|
+
return { kind: "failed" };
|
|
16756
|
+
}
|
|
16757
|
+
return { kind: "pending" };
|
|
16758
|
+
}
|
|
16759
|
+
//#endregion
|
|
16294
16760
|
//#region src/index.ts
|
|
16295
16761
|
/**
|
|
16296
16762
|
* @codemem/server — HTTP server (viewer, sync, API).
|
|
@@ -16443,6 +16909,6 @@ function createSyncApp(opts) {
|
|
|
16443
16909
|
return app;
|
|
16444
16910
|
}
|
|
16445
16911
|
//#endregion
|
|
16446
|
-
export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, closeStoreAsync, createAccountContextReader, createAccountLinkRegistry, createAccountOwnerRecoveryService, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory };
|
|
16912
|
+
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
16913
|
|
|
16448
16914
|
//# sourceMappingURL=index.js.map
|