@codemem/server 0.47.0-beta.3 → 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/context.d.ts +2 -0
- package/dist/account/context.d.ts.map +1 -1
- package/dist/account/link-registry.d.ts +20 -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-context.d.ts +20 -0
- package/dist/account/owner-recovery-context.d.ts.map +1 -0
- package/dist/account/owner-recovery-evidence.d.ts +24 -0
- package/dist/account/owner-recovery-evidence.d.ts.map +1 -0
- package/dist/account/owner-recovery-service.d.ts +41 -0
- package/dist/account/owner-recovery-service.d.ts.map +1 -0
- package/dist/account/owner-recovery-types.d.ts +65 -0
- package/dist/account/owner-recovery-types.d.ts.map +1 -0
- package/dist/account/owner-status.d.ts +8 -0
- package/dist/account/owner-status.d.ts.map +1 -0
- package/dist/account/rebind.d.ts +41 -0
- package/dist/account/rebind.d.ts.map +1 -0
- package/dist/account/status.d.ts +40 -7
- package/dist/account/status.d.ts.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3138 -74
- 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 +7 -0
- package/dist/routes/account-owner-recovery.d.ts.map +1 -0
- package/dist/routes/memory-tools.d.ts +1 -2
- package/dist/routes/memory-tools.d.ts.map +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/pack.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 +3 -3
- package/static/app.js +43 -43
- package/static/app.js.br +0 -0
- package/static/app.js.gz +0 -0
- package/static/index.html +24 -2
package/dist/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { 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, changeCoordinatorAccount, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, classifyRecordedSyncFailure, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, 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, 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, readCodememConfigFile, readCoordinatorAccountChangeCapabilities, readCoordinatorAccountLinkStatus, 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, 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";
|
|
1
|
+
import { closeSync, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
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, 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";
|
|
7
7
|
import { createMiddleware } from "hono/factory";
|
|
8
8
|
import { RateLimiter } from "limiter";
|
|
9
|
-
import { and, count, desc, eq, inArray, max, ne } from "drizzle-orm";
|
|
9
|
+
import { and, count, desc, eq, inArray, max, ne, sql } from "drizzle-orm";
|
|
10
10
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
11
11
|
import { homedir, hostname, networkInterfaces } from "node:os";
|
|
12
12
|
import * as z from "zod";
|
|
13
13
|
import { chmod, link, mkdir, open, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
14
|
+
import "better-sqlite3";
|
|
15
|
+
import "sqlite-vec";
|
|
16
|
+
import { index, integer, primaryKey, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
14
17
|
//#region src/routes/account.ts
|
|
15
18
|
/** Mounted only alongside the account request middleware. */
|
|
16
19
|
function accountRoutes(getSession) {
|
|
@@ -313,10 +316,31 @@ function safeFailure(error) {
|
|
|
313
316
|
default: return "request_failed";
|
|
314
317
|
}
|
|
315
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
|
+
}
|
|
316
340
|
function validBrowserUrl(value, origin, intent) {
|
|
317
341
|
try {
|
|
318
342
|
const url = new URL(value);
|
|
319
|
-
return url.origin === origin && url.pathname === (intent
|
|
343
|
+
return url.origin === origin && url.pathname === browserPath(intent) && !url.username && !url.password && !url.hash;
|
|
320
344
|
} catch {
|
|
321
345
|
return false;
|
|
322
346
|
}
|
|
@@ -329,6 +353,7 @@ var InProcessAccountLinkRegistry = class {
|
|
|
329
353
|
#shuttingDown = false;
|
|
330
354
|
#closing = null;
|
|
331
355
|
#uncertainOwner = null;
|
|
356
|
+
#recoveryOwner = null;
|
|
332
357
|
constructor(options) {
|
|
333
358
|
this.#options = options;
|
|
334
359
|
this.#now = options.now ?? Date.now;
|
|
@@ -409,6 +434,7 @@ var InProcessAccountLinkRegistry = class {
|
|
|
409
434
|
onBrowserStart: (url) => this.#browserStart(job, url)
|
|
410
435
|
};
|
|
411
436
|
const intent = job.operation.intent ?? "connect";
|
|
437
|
+
if (intent === "sign_in") return await this.#runSignIn(job, options.onBrowserStart);
|
|
412
438
|
if (intent === "connect") await (this.#options.link ?? linkCoordinatorAccount)({
|
|
413
439
|
...options,
|
|
414
440
|
expectedAuthority
|
|
@@ -426,6 +452,25 @@ var InProcessAccountLinkRegistry = class {
|
|
|
426
452
|
this.#fail(job, error);
|
|
427
453
|
}
|
|
428
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
|
+
}
|
|
429
474
|
#fail(job, error) {
|
|
430
475
|
let failure = safeFailure(error);
|
|
431
476
|
if (job.abortFailure && (failure === "link_stopped" || failure === "request_failed")) failure = job.abortFailure;
|
|
@@ -480,10 +525,35 @@ var InProcessAccountLinkRegistry = class {
|
|
|
480
525
|
});
|
|
481
526
|
return { operation: this.#snapshot(job) };
|
|
482
527
|
}
|
|
528
|
+
supportsSignIn() {
|
|
529
|
+
if (!this.#options.rebind) return false;
|
|
530
|
+
try {
|
|
531
|
+
assertNoLocalIdentityOverride();
|
|
532
|
+
return true;
|
|
533
|
+
} catch {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
483
537
|
lifecycleToken() {
|
|
484
538
|
busySlot.lifecycle ??= Symbol();
|
|
485
539
|
return busySlot.lifecycle;
|
|
486
540
|
}
|
|
541
|
+
tryReserveRecovery() {
|
|
542
|
+
if (this.#shuttingDown || this.#inProgress()) return null;
|
|
543
|
+
if (this.#shuttingDown || busySlot.owner) return null;
|
|
544
|
+
const owner = Symbol();
|
|
545
|
+
busySlot.owner = owner;
|
|
546
|
+
busySlot.lifecycle = Symbol();
|
|
547
|
+
this.#recoveryOwner = owner;
|
|
548
|
+
return {
|
|
549
|
+
isCurrent: () => !this.#shuttingDown && this.#recoveryOwner === owner && busySlot.owner === owner,
|
|
550
|
+
release: () => {
|
|
551
|
+
if (this.#recoveryOwner !== owner) return;
|
|
552
|
+
this.#recoveryOwner = null;
|
|
553
|
+
releaseBusyOwner(owner);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
487
557
|
get(operationId) {
|
|
488
558
|
this.#cleanup();
|
|
489
559
|
const job = this.#jobs.get(operationId);
|
|
@@ -529,6 +599,10 @@ var InProcessAccountLinkRegistry = class {
|
|
|
529
599
|
close() {
|
|
530
600
|
if (this.#closing) return this.#closing;
|
|
531
601
|
this.#shuttingDown = true;
|
|
602
|
+
if (this.#recoveryOwner) {
|
|
603
|
+
releaseBusyOwner(this.#recoveryOwner);
|
|
604
|
+
this.#recoveryOwner = null;
|
|
605
|
+
}
|
|
532
606
|
for (const job of this.#jobs.values()) {
|
|
533
607
|
this.#clearUrl(job);
|
|
534
608
|
if (!terminal(job)) {
|
|
@@ -554,17 +628,200 @@ function createAccountLinkRegistry(options) {
|
|
|
554
628
|
return new InProcessAccountLinkRegistry(options);
|
|
555
629
|
}
|
|
556
630
|
//#endregion
|
|
631
|
+
//#region src/account/owner-status.ts
|
|
632
|
+
/** The coordinator ID comes from the first signed actor-status read, never the origin. */
|
|
633
|
+
function readOwnerAssociation(context, coordinatorId) {
|
|
634
|
+
if (context.publicKeyHash === void 0) return null;
|
|
635
|
+
const before = statSync(context.dbPath, { bigint: true });
|
|
636
|
+
const device = readAccountLinkDevice(context.dbPath);
|
|
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
|
+
};
|
|
648
|
+
};
|
|
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
|
+
});
|
|
661
|
+
const after = statSync(context.dbPath, { bigint: true });
|
|
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");
|
|
663
|
+
return association;
|
|
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
|
+
}
|
|
700
|
+
function ownerUnavailable(view, code) {
|
|
701
|
+
return {
|
|
702
|
+
...withoutOwnerCapabilities(view),
|
|
703
|
+
remote: {
|
|
704
|
+
kind: "unavailable",
|
|
705
|
+
code
|
|
706
|
+
},
|
|
707
|
+
canLink: false,
|
|
708
|
+
blockedReason: code === "context_changed" ? "status_unavailable" : "review_required"
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function withoutOwnerCapabilities(view) {
|
|
712
|
+
const { managementActions, unlinkTarget, accountOwner, ownerProjection, ...local } = view;
|
|
713
|
+
return local;
|
|
714
|
+
}
|
|
715
|
+
function ownerProfile(value) {
|
|
716
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
717
|
+
const source = value;
|
|
718
|
+
const profile = {};
|
|
719
|
+
for (const [field, limit] of [
|
|
720
|
+
["displayName", 256],
|
|
721
|
+
["email", 320],
|
|
722
|
+
["pictureUrl", 2048]
|
|
723
|
+
]) {
|
|
724
|
+
const text = source[field];
|
|
725
|
+
if (text === void 0) continue;
|
|
726
|
+
if (typeof text !== "string" || !text.trim() || text.length > limit || /[\p{Cc}\p{Cf}\p{Cs}]/u.test(text)) return null;
|
|
727
|
+
if (field === "pictureUrl" && !safeOwnerPicture(text)) return null;
|
|
728
|
+
profile[field] = text;
|
|
729
|
+
}
|
|
730
|
+
return profile;
|
|
731
|
+
}
|
|
732
|
+
function safeOwnerPicture(text) {
|
|
733
|
+
try {
|
|
734
|
+
const url = new URL(text);
|
|
735
|
+
return url.protocol === "https:" && url.hostname === "lh3.googleusercontent.com" && !url.username && !url.password && !url.port;
|
|
736
|
+
} catch {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function associatedOwnerView(view, context, association, remote) {
|
|
741
|
+
const contextId = createHash("sha256").update(JSON.stringify({
|
|
742
|
+
version: "account-owner-view-v1",
|
|
743
|
+
context,
|
|
744
|
+
association
|
|
745
|
+
})).digest("hex");
|
|
746
|
+
const base = {
|
|
747
|
+
...withoutOwnerCapabilities(view),
|
|
748
|
+
contextId
|
|
749
|
+
};
|
|
750
|
+
if (remote?.kind !== "owner_status" || !remote.status || remote.status.version !== 1 || remote.status.coordinatorId !== association.coordinatorId || remote.status.groupId !== context.groupId || remote.status.deviceId !== context.deviceId || remote.status.fingerprint !== context.fingerprint || remote.status.ownerIdentity !== association.ownerIdentityId || remote.status.state !== "owner_confirmed") return ownerUnavailable(base, "account_owner_association_invalid");
|
|
751
|
+
const status = remote.status;
|
|
752
|
+
const profile = status.profileState === "linked" ? ownerProfile(status.profile) : null;
|
|
753
|
+
if (status.profileState === "linked" && profile === null || status.profileState !== "linked" && (status.profileState !== "account_unlinked" && status.profileState !== "unavailable" || status.profile !== null)) return ownerUnavailable(base, "account_owner_association_invalid");
|
|
754
|
+
const subject = {
|
|
755
|
+
version: 1,
|
|
756
|
+
coordinatorId: status.coordinatorId,
|
|
757
|
+
groupId: status.groupId,
|
|
758
|
+
deviceId: status.deviceId,
|
|
759
|
+
fingerprint: status.fingerprint,
|
|
760
|
+
state: "owner_confirmed",
|
|
761
|
+
ownerIdentity: status.ownerIdentity
|
|
762
|
+
};
|
|
763
|
+
let confirmed;
|
|
764
|
+
if (status.profileState === "linked" && profile) confirmed = {
|
|
765
|
+
...subject,
|
|
766
|
+
profileState: "linked",
|
|
767
|
+
profile
|
|
768
|
+
};
|
|
769
|
+
else if (status.profileState === "account_unlinked" || status.profileState === "unavailable") confirmed = {
|
|
770
|
+
...subject,
|
|
771
|
+
profileState: status.profileState,
|
|
772
|
+
profile: null
|
|
773
|
+
};
|
|
774
|
+
else return ownerUnavailable(base, "account_owner_association_invalid");
|
|
775
|
+
return {
|
|
776
|
+
...base,
|
|
777
|
+
accountOwner: {
|
|
778
|
+
identityId: association.ownerIdentityId,
|
|
779
|
+
memoryActorId: context.identityId
|
|
780
|
+
},
|
|
781
|
+
remote: { kind: "not_checked" },
|
|
782
|
+
ownerProjection: {
|
|
783
|
+
kind: "resolved_account_owner",
|
|
784
|
+
status: confirmed
|
|
785
|
+
},
|
|
786
|
+
canLink: false,
|
|
787
|
+
blockedReason: profile ? "already_linked" : "status_unavailable"
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
//#endregion
|
|
557
791
|
//#region src/account/status.ts
|
|
558
|
-
|
|
559
|
-
|
|
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;
|
|
812
|
+
const operation = operationContextId ? registry?.forContext(operationContextId) ?? null : null;
|
|
813
|
+
const operationContext = operation && operationContextId && operationContextId !== view.contextId ? { operationContextId } : {};
|
|
560
814
|
if (registry?.hasActive()) return {
|
|
561
815
|
...view,
|
|
816
|
+
...operationContext,
|
|
562
817
|
operation,
|
|
563
818
|
canLink: false,
|
|
819
|
+
..."canSignIn" in view ? { canSignIn: false } : {},
|
|
564
820
|
blockedReason: "operation_active"
|
|
565
821
|
};
|
|
566
822
|
return {
|
|
567
823
|
...view,
|
|
824
|
+
...operationContext,
|
|
568
825
|
operation
|
|
569
826
|
};
|
|
570
827
|
}
|
|
@@ -586,6 +843,7 @@ function accountLocalView(result) {
|
|
|
586
843
|
} : null,
|
|
587
844
|
remote: { kind: "not_checked" },
|
|
588
845
|
canLink: false,
|
|
846
|
+
canSignIn: false,
|
|
589
847
|
blockedReason: "setup_incomplete",
|
|
590
848
|
operation: null,
|
|
591
849
|
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -619,12 +877,84 @@ function applyRemote(view, remote) {
|
|
|
619
877
|
account: identityMatches && status.state === "linked" ? status.account : null
|
|
620
878
|
},
|
|
621
879
|
canLink: status.state === "unlinked" && identityMatches,
|
|
880
|
+
canSignIn: identityMatches && signInState(status),
|
|
622
881
|
blockedReason
|
|
623
882
|
};
|
|
624
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
|
+
}
|
|
625
888
|
async function readAccountStatus(options, groupId, signal, readOptions = {}) {
|
|
626
889
|
const initial = options.readContext(groupId);
|
|
627
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 = {}) {
|
|
628
958
|
const view = accountLocalView(initial);
|
|
629
959
|
if (initial.setup !== "ready" || !initial.context) return attachOperation(view, options.registry);
|
|
630
960
|
const snapshot = JSON.stringify(initial);
|
|
@@ -668,10 +998,66 @@ async function readAccountStatus(options, groupId, signal, readOptions = {}) {
|
|
|
668
998
|
},
|
|
669
999
|
blockedReason: "status_unavailable"
|
|
670
1000
|
}, options.registry);
|
|
1001
|
+
return finishAccountStatus(options, initial.context, fresh, remote, {
|
|
1002
|
+
signal,
|
|
1003
|
+
ownIdentityOnly: readOptions.ownIdentityOnly === true
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
async function finishAccountStatus(options, context, fresh, remote, read) {
|
|
1007
|
+
const { signal } = read;
|
|
671
1008
|
const currentView = applyRemote(accountLocalView(fresh), remote);
|
|
672
|
-
|
|
1009
|
+
if (remote.kind === "status") {
|
|
1010
|
+
const associated = await readAssociatedOwner(options, context, currentView, remote, signal);
|
|
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
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
await attachManagementActions(options, context, currentView, signal);
|
|
1018
|
+
if (remote.kind === "status") try {
|
|
1019
|
+
if (readOwnerAssociation(context, remote.status.coordinatorId)) return attachOperation(ownerUnavailable(accountLocalView(fresh), "context_changed"), options.registry);
|
|
1020
|
+
} catch {
|
|
1021
|
+
return attachOperation(ownerUnavailable(accountLocalView(fresh), "account_owner_association_invalid"), options.registry);
|
|
1022
|
+
}
|
|
673
1023
|
return attachOperation(currentView, options.registry);
|
|
674
1024
|
}
|
|
1025
|
+
async function readAssociatedOwner(options, context, view, actorRemote, signal) {
|
|
1026
|
+
const local = {
|
|
1027
|
+
...view,
|
|
1028
|
+
remote: { kind: "not_checked" },
|
|
1029
|
+
canLink: false,
|
|
1030
|
+
canSignIn: false
|
|
1031
|
+
};
|
|
1032
|
+
try {
|
|
1033
|
+
const association = readOwnerAssociation(context, actorRemote.status.coordinatorId);
|
|
1034
|
+
if (!association) return null;
|
|
1035
|
+
const before = options.readContext(context.groupId);
|
|
1036
|
+
if ("error" in before || !before.context || !sameAuthority(context, before.context)) return ownerUnavailable(local, "context_changed");
|
|
1037
|
+
const { dbPath, keysDir, groupId, coordinatorUrl } = context;
|
|
1038
|
+
let remote;
|
|
1039
|
+
try {
|
|
1040
|
+
remote = await (options.readRemoteOwnerStatus ?? readCoordinatorResolvedAccountOwnerStatus)({
|
|
1041
|
+
dbPath,
|
|
1042
|
+
keysDir,
|
|
1043
|
+
groupId,
|
|
1044
|
+
coordinatorUrl,
|
|
1045
|
+
ownerIdentityId: association.ownerIdentityId,
|
|
1046
|
+
signal
|
|
1047
|
+
});
|
|
1048
|
+
} catch {
|
|
1049
|
+
remote = {
|
|
1050
|
+
kind: "unavailable",
|
|
1051
|
+
code: "coordinator_unreachable"
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const after = options.readContext(context.groupId);
|
|
1055
|
+
if (signal.aborted || JSON.stringify(before) !== JSON.stringify(after) || JSON.stringify(association) !== JSON.stringify(readOwnerAssociation(context, association.coordinatorId))) return ownerUnavailable(local, "context_changed");
|
|
1056
|
+
return associatedOwnerView(local, context, association, remote);
|
|
1057
|
+
} catch {
|
|
1058
|
+
return ownerUnavailable(local, "account_owner_association_invalid");
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
675
1061
|
async function attachManagementActions(options, context, view, signal) {
|
|
676
1062
|
if (view.remote.kind !== "status" || view.remote.state !== "linked" || !view.remote.identityMatches) return;
|
|
677
1063
|
const read = options.readRemoteCapabilities ?? (options.readRemoteStatus ? null : readCoordinatorAccountChangeCapabilities);
|
|
@@ -711,7 +1097,7 @@ async function attachManagementActions(options, context, view, signal) {
|
|
|
711
1097
|
}
|
|
712
1098
|
//#endregion
|
|
713
1099
|
//#region src/routes/account-operations.ts
|
|
714
|
-
var ROOT = "/api/account/link-operations";
|
|
1100
|
+
var ROOT$1 = "/api/account/link-operations";
|
|
715
1101
|
function validString(value) {
|
|
716
1102
|
return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
|
|
717
1103
|
}
|
|
@@ -720,7 +1106,7 @@ function noBody(request) {
|
|
|
720
1106
|
return request.body === null && (length === null || length === "0") && !request.headers.has("Transfer-Encoding");
|
|
721
1107
|
}
|
|
722
1108
|
/** Cap both bytes and elapsed time; cancellation must not wait on an uncooperative stream. */
|
|
723
|
-
async function readBody(request, keys, optional = []) {
|
|
1109
|
+
async function readBody$1(request, keys, optional = []) {
|
|
724
1110
|
if (!/^application\/json(?:\s*;\s*charset\s*=\s*(?:utf-8|"utf-8"))?\s*$/i.test(request.headers.get("Content-Type") ?? "")) return null;
|
|
725
1111
|
const reader = request.body?.getReader();
|
|
726
1112
|
if (!reader) return null;
|
|
@@ -761,13 +1147,13 @@ function isCurrent(options, operation) {
|
|
|
761
1147
|
return !("error" in fresh) && fresh.setup === "ready" && fresh.context?.groupId === operation.groupId && fresh.context.contextId === operation.contextId;
|
|
762
1148
|
}
|
|
763
1149
|
function validStartBody(body) {
|
|
764
|
-
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");
|
|
765
1151
|
}
|
|
766
1152
|
function unlinkTargetChanged(body, view) {
|
|
767
1153
|
return body.intent === "disconnect" && body.expectedLinkId !== view.unlinkTarget;
|
|
768
1154
|
}
|
|
769
1155
|
async function startOperation(c, options) {
|
|
770
|
-
const body = await readBody(c.req.raw, [
|
|
1156
|
+
const body = await readBody$1(c.req.raw, [
|
|
771
1157
|
"groupId",
|
|
772
1158
|
"contextId",
|
|
773
1159
|
"authority",
|
|
@@ -779,7 +1165,10 @@ async function startOperation(c, options) {
|
|
|
779
1165
|
if ("error" in initial || initial.setup !== "ready" || !initial.context || initial.context?.contextId !== body.contextId) return c.json({ error: "account_context_changed" }, 409);
|
|
780
1166
|
const checkedContext = structuredClone(initial.context);
|
|
781
1167
|
const expectedLifecycleToken = options.registry.lifecycleToken();
|
|
782
|
-
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
|
+
});
|
|
783
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);
|
|
784
1173
|
if (view.operation && view.operation.finishedAt === null) return c.json({
|
|
785
1174
|
error: "account_link_in_progress",
|
|
@@ -799,6 +1188,7 @@ async function startOperation(c, options) {
|
|
|
799
1188
|
}
|
|
800
1189
|
function actionAllowed(view, intent = "connect") {
|
|
801
1190
|
if (intent === "connect") return view.canLink;
|
|
1191
|
+
if (intent === "sign_in") return view.canSignIn === true;
|
|
802
1192
|
if (intent === "refresh" || intent === "disconnect") return view.managementActions?.[intent] === true;
|
|
803
1193
|
return false;
|
|
804
1194
|
}
|
|
@@ -823,7 +1213,7 @@ function beginCurrentOperation(c, options, body, view, expectedCoordinatorId, ch
|
|
|
823
1213
|
}
|
|
824
1214
|
async function performOperation(c, options) {
|
|
825
1215
|
const { id, action } = c.req.param();
|
|
826
|
-
if (!validString(id) || action !== "browser-url" && action !== "cancel" || !await readBody(c.req.raw, [])) return c.json({ error: "account_invalid_request" }, 400);
|
|
1216
|
+
if (!validString(id) || action !== "browser-url" && action !== "cancel" || !await readBody$1(c.req.raw, [])) return c.json({ error: "account_invalid_request" }, 400);
|
|
827
1217
|
const operation = options.registry.get(id);
|
|
828
1218
|
if (!operation) return c.json({ error: "account_operation_not_found" }, 404);
|
|
829
1219
|
if (action === "cancel") {
|
|
@@ -849,10 +1239,10 @@ function accountOperationRoutes(options) {
|
|
|
849
1239
|
if ([...new URL(c.req.url).searchParams].length) return c.json({ error: "account_invalid_request" }, 400);
|
|
850
1240
|
await next();
|
|
851
1241
|
};
|
|
852
|
-
app.use(ROOT, rejectQuery);
|
|
853
|
-
app.use(`${ROOT}/*`, rejectQuery);
|
|
854
|
-
app.post(ROOT, (c) => startOperation(c, options));
|
|
855
|
-
app.get(`${ROOT}/:id`, (c) => {
|
|
1242
|
+
app.use(ROOT$1, rejectQuery);
|
|
1243
|
+
app.use(`${ROOT$1}/*`, rejectQuery);
|
|
1244
|
+
app.post(ROOT$1, (c) => startOperation(c, options));
|
|
1245
|
+
app.get(`${ROOT$1}/:id`, (c) => {
|
|
856
1246
|
if (!noBody(c.req.raw) || !validString(c.req.param("id"))) return c.json({ error: "account_invalid_request" }, 400);
|
|
857
1247
|
const operation = options.registry.get(c.req.param("id"));
|
|
858
1248
|
if (!operation) return c.json({ error: "account_operation_not_found" }, 404);
|
|
@@ -862,7 +1252,338 @@ function accountOperationRoutes(options) {
|
|
|
862
1252
|
current: isCurrent(options, operation)
|
|
863
1253
|
});
|
|
864
1254
|
});
|
|
865
|
-
app.post(`${ROOT}/:id/:action`, (c) => performOperation(c, options));
|
|
1255
|
+
app.post(`${ROOT$1}/:id/:action`, (c) => performOperation(c, options));
|
|
1256
|
+
return app;
|
|
1257
|
+
}
|
|
1258
|
+
//#endregion
|
|
1259
|
+
//#region src/account/owner-recovery-evidence.ts
|
|
1260
|
+
var RECOVERY_SCOPE = "historical_included_owner_labels_only";
|
|
1261
|
+
var RECOVERY_REASON = /^[a-z][a-z0-9_]{0,119}(?:: [\x20-\x7e]{1,240})?$/;
|
|
1262
|
+
var AccountOwnerRecoveryError = class extends Error {
|
|
1263
|
+
code;
|
|
1264
|
+
constructor(code = "invalid") {
|
|
1265
|
+
super("account_owner_recovery_invalid");
|
|
1266
|
+
this.code = code;
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
function requireRecovery(condition, code = "invalid") {
|
|
1270
|
+
if (!condition) throw new AccountOwnerRecoveryError(code);
|
|
1271
|
+
}
|
|
1272
|
+
function recoveryId(value) {
|
|
1273
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
|
|
1274
|
+
}
|
|
1275
|
+
function recoveryHash(value) {
|
|
1276
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
1277
|
+
}
|
|
1278
|
+
function recoveryRecord(value) {
|
|
1279
|
+
requireRecovery(value && typeof value === "object" && !Array.isArray(value));
|
|
1280
|
+
return value;
|
|
1281
|
+
}
|
|
1282
|
+
function exactRecoveryFields(row, fields) {
|
|
1283
|
+
requireRecovery(Object.keys(row).length === fields.length && fields.every((key) => Object.hasOwn(row, key)));
|
|
1284
|
+
}
|
|
1285
|
+
function timestamp(value) {
|
|
1286
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
1287
|
+
}
|
|
1288
|
+
function readWeakLabel(value) {
|
|
1289
|
+
const row = recoveryRecord(value);
|
|
1290
|
+
exactRecoveryFields(row, [
|
|
1291
|
+
"label",
|
|
1292
|
+
"kind",
|
|
1293
|
+
"group_id",
|
|
1294
|
+
"device_id",
|
|
1295
|
+
"evidence_ref",
|
|
1296
|
+
"effect"
|
|
1297
|
+
]);
|
|
1298
|
+
requireRecovery(recoveryId(row.label) && recoveryId(row.group_id) && recoveryId(row.device_id) && recoveryId(row.evidence_ref));
|
|
1299
|
+
requireRecovery(row.kind === "legacy_included_owner_label" || row.kind === "consumed_project_actor_label");
|
|
1300
|
+
requireRecovery(row.effect === "account_owner_display_only");
|
|
1301
|
+
return {
|
|
1302
|
+
label: row.label,
|
|
1303
|
+
kind: row.kind,
|
|
1304
|
+
group_id: row.group_id,
|
|
1305
|
+
device_id: row.device_id,
|
|
1306
|
+
evidence_ref: row.evidence_ref,
|
|
1307
|
+
effect: row.effect
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function historyRows$1(value, selection, labels) {
|
|
1311
|
+
const row = recoveryRecord(value);
|
|
1312
|
+
requireRecovery(recoveryId(row.group_id) && recoveryId(row.candidate_ref) && timestamp(row.created_at));
|
|
1313
|
+
const manifest = normalizeCoordinatorLegacyTeamCompletionManifest(row.manifest);
|
|
1314
|
+
requireRecovery(manifest.coordinator_id === selection.profile.coordinatorId && manifest.candidate_ref === row.candidate_ref && manifest.version === row.manifest_version && manifest.completed_at === row.completed_at && legacyTeamCandidateId(manifest.coordinator_id, row.group_id) === manifest.candidate_ref && canonicalCoordinatorLegacyTeamCompletionManifestJson(manifest) === row.manifest_json);
|
|
1315
|
+
const groupId = row.group_id;
|
|
1316
|
+
return manifest.device_decisions.map((decision) => {
|
|
1317
|
+
let relation = "other";
|
|
1318
|
+
if (decision.device_id === selection.profile.deviceId) relation = "exact_subject";
|
|
1319
|
+
else if (labels.some((label) => label.kind === "legacy_included_owner_label" && label.group_id === groupId && label.device_id === decision.device_id && label.label === decision.identity_id && label.evidence_ref === manifest.source_digest)) relation = "alias";
|
|
1320
|
+
return {
|
|
1321
|
+
id: `${manifest.candidate_ref}:${decision.device_id}`,
|
|
1322
|
+
groupId,
|
|
1323
|
+
deviceId: decision.device_id,
|
|
1324
|
+
fingerprint: decision.key_fingerprint,
|
|
1325
|
+
identityId: decision.identity_id ?? "",
|
|
1326
|
+
assignment: decision.decision,
|
|
1327
|
+
relation,
|
|
1328
|
+
reason: legacyDecisionCountsAsOwner(decision) ? "Recorded in Team history." : "Guessed by another device in Team history."
|
|
1329
|
+
};
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
function receiptPart(value, idField) {
|
|
1333
|
+
const row = recoveryRecord(value);
|
|
1334
|
+
exactRecoveryFields(row, [
|
|
1335
|
+
"disposition",
|
|
1336
|
+
idField,
|
|
1337
|
+
"evidence_digest",
|
|
1338
|
+
"created_at"
|
|
1339
|
+
]);
|
|
1340
|
+
const id = row[idField];
|
|
1341
|
+
requireRecovery(recoveryId(id) && recoveryHash(row.evidence_digest) && timestamp(row.created_at) && (row.disposition === "created" || row.disposition === "reused"));
|
|
1342
|
+
return {
|
|
1343
|
+
id,
|
|
1344
|
+
digest: row.evidence_digest
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
function reviewReceipt(row) {
|
|
1348
|
+
requireRecovery(recoveryId(row.attestation_id));
|
|
1349
|
+
const source = receiptPart(row.recovery_receipt, "review_receipt_id");
|
|
1350
|
+
const resolution = receiptPart(row.owner_resolution, "resolution_id");
|
|
1351
|
+
return {
|
|
1352
|
+
attestationId: row.attestation_id,
|
|
1353
|
+
reviewReceiptId: source.id,
|
|
1354
|
+
reviewEvidenceDigest: source.digest,
|
|
1355
|
+
resolutionId: resolution.id,
|
|
1356
|
+
resolutionEvidenceDigest: resolution.digest
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
function validateRecoveryReview(value, selection, groupId) {
|
|
1360
|
+
const row = recoveryRecord(value);
|
|
1361
|
+
const profile = selection.profile;
|
|
1362
|
+
requireRecovery(row.purpose === "existing_account_owner" && row.resolution_scope === "historical_included_owner_labels_only" && row.coordinator_id === profile.coordinatorId && row.group_id === groupId && row.device_id === profile.deviceId && row.fingerprint === profile.fingerprint && row.identity_id === selection.existingOwnerId);
|
|
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"));
|
|
1364
|
+
const labels = row.reviewed_weak_labels.map(readWeakLabel);
|
|
1365
|
+
requireRecovery(new Set(labels.map((label) => JSON.stringify(label))).size === labels.length);
|
|
1366
|
+
const rows = row.legacy_history.flatMap((history) => historyRows$1(history, selection, labels));
|
|
1367
|
+
requireRecovery(row.reasons.length <= 8 && row.reasons.every((reason) => typeof reason === "string" && RECOVERY_REASON.test(reason)));
|
|
1368
|
+
const reasons = row.reasons;
|
|
1369
|
+
if (row.state === "needs_review") {
|
|
1370
|
+
requireRecovery(row.reasons.length > 0 && !Object.hasOwn(row, "owner_resolution_digest"));
|
|
1371
|
+
return {
|
|
1372
|
+
state: row.state,
|
|
1373
|
+
rows,
|
|
1374
|
+
labels,
|
|
1375
|
+
reasons
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
requireRecovery((row.state === "ready" || row.state === "restored") && recoveryHash(row.owner_resolution_digest) && row.reasons.length === 0);
|
|
1379
|
+
const target = recoveryRecord(row.account_owner_target);
|
|
1380
|
+
requireRecovery(target.identity_id === selection.existingOwnerId && recoveryId(target.link_id) && recoveryId(target.auth_config_revision) && typeof target.created_at_ms === "number" && Number.isSafeInteger(target.created_at_ms));
|
|
1381
|
+
let receipt;
|
|
1382
|
+
if (row.state === "restored" || Object.hasOwn(row, "owner_resolution")) receipt = reviewReceipt(row);
|
|
1383
|
+
return {
|
|
1384
|
+
state: row.state,
|
|
1385
|
+
digest: row.owner_resolution_digest,
|
|
1386
|
+
receipt,
|
|
1387
|
+
rows,
|
|
1388
|
+
labels,
|
|
1389
|
+
reasons
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function replyGroups(row, selection) {
|
|
1393
|
+
exactRecoveryFields(row, [
|
|
1394
|
+
"version",
|
|
1395
|
+
"purpose",
|
|
1396
|
+
"scope",
|
|
1397
|
+
"coordinatorOrigin",
|
|
1398
|
+
"coordinatorId",
|
|
1399
|
+
"deviceId",
|
|
1400
|
+
"fingerprint",
|
|
1401
|
+
"ownerIdentityId",
|
|
1402
|
+
"groups"
|
|
1403
|
+
]);
|
|
1404
|
+
const p = selection.profile;
|
|
1405
|
+
requireRecovery(row.version === 1 && row.purpose === "existing_account_owner" && row.scope === "historical_included_owner_labels_only" && row.coordinatorOrigin === p.coordinatorOrigin && row.coordinatorId === p.coordinatorId && row.deviceId === p.deviceId && row.fingerprint === p.fingerprint && row.ownerIdentityId === selection.existingOwnerId && Array.isArray(row.groups) && row.groups.length === selection.groups.length);
|
|
1406
|
+
return row.groups;
|
|
1407
|
+
}
|
|
1408
|
+
function parseRecoveryReply(text, selection) {
|
|
1409
|
+
requireRecovery(typeof text === "string" && Buffer.byteLength(text) <= 32768);
|
|
1410
|
+
const groups = replyGroups(recoveryRecord(JSON.parse(text)), selection);
|
|
1411
|
+
const receipts = /* @__PURE__ */ new Map();
|
|
1412
|
+
for (const input of groups) {
|
|
1413
|
+
const group = recoveryRecord(input);
|
|
1414
|
+
exactRecoveryFields(group, [
|
|
1415
|
+
"groupId",
|
|
1416
|
+
"attestationId",
|
|
1417
|
+
"reviewReceiptId",
|
|
1418
|
+
"reviewEvidenceDigest",
|
|
1419
|
+
"resolutionId",
|
|
1420
|
+
"resolutionEvidenceDigest"
|
|
1421
|
+
]);
|
|
1422
|
+
requireRecovery(typeof group.groupId === "string" && selection.groups.includes(group.groupId) && !receipts.has(group.groupId));
|
|
1423
|
+
requireRecovery(recoveryId(group.attestationId) && recoveryId(group.reviewReceiptId) && recoveryId(group.resolutionId) && recoveryHash(group.reviewEvidenceDigest) && recoveryHash(group.resolutionEvidenceDigest));
|
|
1424
|
+
receipts.set(group.groupId, {
|
|
1425
|
+
attestationId: group.attestationId,
|
|
1426
|
+
reviewReceiptId: group.reviewReceiptId,
|
|
1427
|
+
reviewEvidenceDigest: group.reviewEvidenceDigest,
|
|
1428
|
+
resolutionId: group.resolutionId,
|
|
1429
|
+
resolutionEvidenceDigest: group.resolutionEvidenceDigest
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
return receipts;
|
|
1433
|
+
}
|
|
1434
|
+
//#endregion
|
|
1435
|
+
//#region src/routes/account-owner-recovery.ts
|
|
1436
|
+
var ROOT = "/api/account/owner-recovery";
|
|
1437
|
+
var LIMITS = {
|
|
1438
|
+
preview: 65536,
|
|
1439
|
+
confirm: 1048576,
|
|
1440
|
+
import: 65536
|
|
1441
|
+
};
|
|
1442
|
+
function bodyLength(request) {
|
|
1443
|
+
requireRecovery(!request.headers.has("Transfer-Encoding"));
|
|
1444
|
+
const length = request.headers.get("Content-Length");
|
|
1445
|
+
if (length === null) return null;
|
|
1446
|
+
requireRecovery(/^(0|[1-9][0-9]*)$/.test(length));
|
|
1447
|
+
const parsed = Number(length);
|
|
1448
|
+
requireRecovery(Number.isSafeInteger(parsed));
|
|
1449
|
+
return parsed;
|
|
1450
|
+
}
|
|
1451
|
+
/** Limit actual UTF-8 bytes and elapsed time, regardless of transport metadata. */
|
|
1452
|
+
async function readBody(request, cap) {
|
|
1453
|
+
const declared = bodyLength(request);
|
|
1454
|
+
requireRecovery(declared === null || declared <= cap);
|
|
1455
|
+
requireRecovery(/^application\/json(?:\s*;\s*charset\s*=\s*(?:utf-8|"utf-8"))?\s*$/i.test(request.headers.get("Content-Type") ?? ""));
|
|
1456
|
+
const reader = request.body?.getReader();
|
|
1457
|
+
requireRecovery(reader);
|
|
1458
|
+
let timer;
|
|
1459
|
+
let stop = () => {};
|
|
1460
|
+
try {
|
|
1461
|
+
const expired = new Promise((_, reject) => {
|
|
1462
|
+
stop = () => reject(new AccountOwnerRecoveryError());
|
|
1463
|
+
timer = setTimeout(stop, 1e4);
|
|
1464
|
+
request.signal.addEventListener("abort", stop, { once: true });
|
|
1465
|
+
if (request.signal.aborted) stop();
|
|
1466
|
+
});
|
|
1467
|
+
const bytes = new Uint8Array(cap);
|
|
1468
|
+
let length = 0;
|
|
1469
|
+
while (true) {
|
|
1470
|
+
const chunk = await Promise.race([reader.read(), expired]);
|
|
1471
|
+
if (chunk.done) break;
|
|
1472
|
+
requireRecovery(length + chunk.value.byteLength <= cap);
|
|
1473
|
+
bytes.set(chunk.value, length);
|
|
1474
|
+
length += chunk.value.byteLength;
|
|
1475
|
+
}
|
|
1476
|
+
requireRecovery(!request.signal.aborted && (declared === null || declared === length));
|
|
1477
|
+
return recoveryRecord(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length))));
|
|
1478
|
+
} catch {
|
|
1479
|
+
throw new AccountOwnerRecoveryError();
|
|
1480
|
+
} finally {
|
|
1481
|
+
clearTimeout(timer);
|
|
1482
|
+
request.signal.removeEventListener("abort", stop);
|
|
1483
|
+
reader.cancel().catch(() => {});
|
|
1484
|
+
reader.releaseLock();
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function stringList(value) {
|
|
1488
|
+
requireRecovery(Array.isArray(value) && value.every(recoveryId));
|
|
1489
|
+
return value;
|
|
1490
|
+
}
|
|
1491
|
+
function profile(value) {
|
|
1492
|
+
const row = recoveryRecord(value);
|
|
1493
|
+
exactRecoveryFields(row, [
|
|
1494
|
+
"contextId",
|
|
1495
|
+
"memoryActorId",
|
|
1496
|
+
"deviceId",
|
|
1497
|
+
"fingerprint",
|
|
1498
|
+
"coordinatorOrigin",
|
|
1499
|
+
"coordinatorId",
|
|
1500
|
+
"configuredGroups"
|
|
1501
|
+
]);
|
|
1502
|
+
const { contextId, memoryActorId, deviceId, fingerprint, coordinatorOrigin, coordinatorId } = row;
|
|
1503
|
+
requireRecovery(recoveryId(contextId) && recoveryId(memoryActorId) && recoveryId(deviceId) && recoveryId(fingerprint) && recoveryId(coordinatorOrigin) && recoveryId(coordinatorId));
|
|
1504
|
+
return {
|
|
1505
|
+
contextId,
|
|
1506
|
+
memoryActorId,
|
|
1507
|
+
deviceId,
|
|
1508
|
+
fingerprint,
|
|
1509
|
+
coordinatorOrigin,
|
|
1510
|
+
coordinatorId,
|
|
1511
|
+
configuredGroups: stringList(row.configuredGroups)
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
function selection(row) {
|
|
1515
|
+
requireRecovery(recoveryId(row.existingOwnerId));
|
|
1516
|
+
return {
|
|
1517
|
+
profile: profile(row.profile),
|
|
1518
|
+
existingOwnerId: row.existingOwnerId,
|
|
1519
|
+
groups: stringList(row.groups)
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
async function dispatch(c, service) {
|
|
1523
|
+
const request = c.req.raw;
|
|
1524
|
+
requireRecovery(!new URL(request.url).search && !request.signal.aborted);
|
|
1525
|
+
if (c.req.path === ROOT && request.method === "GET") {
|
|
1526
|
+
const length = bodyLength(request);
|
|
1527
|
+
requireRecovery(request.body === null && (length === null || length === 0));
|
|
1528
|
+
return c.json(await service.getProfile());
|
|
1529
|
+
}
|
|
1530
|
+
if (request.method !== "POST") return c.json({ error: "account_owner_recovery_invalid" }, 405);
|
|
1531
|
+
if (c.req.path === `${ROOT}/preview`) {
|
|
1532
|
+
const body = await readBody(request, LIMITS.preview);
|
|
1533
|
+
exactRecoveryFields(body, [
|
|
1534
|
+
"profile",
|
|
1535
|
+
"existingOwnerId",
|
|
1536
|
+
"groups",
|
|
1537
|
+
"requestId",
|
|
1538
|
+
"purpose"
|
|
1539
|
+
]);
|
|
1540
|
+
requireRecovery(recoveryId(body.requestId) && (body.purpose === "review" || body.purpose === "check_status"));
|
|
1541
|
+
return c.json(await service.preview({
|
|
1542
|
+
...selection(body),
|
|
1543
|
+
requestId: body.requestId,
|
|
1544
|
+
purpose: body.purpose
|
|
1545
|
+
}));
|
|
1546
|
+
}
|
|
1547
|
+
if (c.req.path === `${ROOT}/confirm`) {
|
|
1548
|
+
const body = await readBody(request, LIMITS.confirm);
|
|
1549
|
+
exactRecoveryFields(body, ["preview", "approvalDigest"]);
|
|
1550
|
+
requireRecovery(recoveryId(body.approvalDigest));
|
|
1551
|
+
return c.json(await service.confirm({
|
|
1552
|
+
preview: body.preview,
|
|
1553
|
+
approvalDigest: body.approvalDigest
|
|
1554
|
+
}));
|
|
1555
|
+
}
|
|
1556
|
+
if (c.req.path === `${ROOT}/import`) {
|
|
1557
|
+
const body = await readBody(request, LIMITS.import);
|
|
1558
|
+
exactRecoveryFields(body, ["selection", "replyText"]);
|
|
1559
|
+
const selected = recoveryRecord(body.selection);
|
|
1560
|
+
exactRecoveryFields(selected, [
|
|
1561
|
+
"profile",
|
|
1562
|
+
"existingOwnerId",
|
|
1563
|
+
"groups"
|
|
1564
|
+
]);
|
|
1565
|
+
requireRecovery(typeof body.replyText === "string");
|
|
1566
|
+
return c.json(await service.importReply({
|
|
1567
|
+
selection: selection(selected),
|
|
1568
|
+
replyText: body.replyText
|
|
1569
|
+
}));
|
|
1570
|
+
}
|
|
1571
|
+
return c.json({ error: "account_owner_recovery_invalid" }, 404);
|
|
1572
|
+
}
|
|
1573
|
+
/** Mount only after createAccountGuard: this router grants no independent authority. */
|
|
1574
|
+
function accountOwnerRecoveryRoutes(service) {
|
|
1575
|
+
const app = new Hono();
|
|
1576
|
+
app.onError((error, c) => {
|
|
1577
|
+
if (!(error instanceof AccountOwnerRecoveryError)) return c.json({ error: "account_owner_recovery_unavailable" }, 503);
|
|
1578
|
+
return c.json({ error: `account_owner_recovery_${error.code}` }, {
|
|
1579
|
+
invalid: 400,
|
|
1580
|
+
busy: 409,
|
|
1581
|
+
context_changed: 409,
|
|
1582
|
+
unavailable: 503
|
|
1583
|
+
}[error.code]);
|
|
1584
|
+
});
|
|
1585
|
+
app.all(ROOT, (c) => dispatch(c, service));
|
|
1586
|
+
app.all(`${ROOT}/*`, (c) => dispatch(c, service));
|
|
866
1587
|
return app;
|
|
867
1588
|
}
|
|
868
1589
|
//#endregion
|
|
@@ -2306,6 +3027,23 @@ function queryMemoryPage(store, options) {
|
|
|
2306
3027
|
const ownedBySelf = store.buildOwnershipPredicate();
|
|
2307
3028
|
return rows.map((row) => serializeMemoryRow(ownedBySelf, row));
|
|
2308
3029
|
}
|
|
3030
|
+
async function handleLegacyPackRequest(c, getStore) {
|
|
3031
|
+
const store = getStore();
|
|
3032
|
+
const context = c.req.query("context") || "";
|
|
3033
|
+
if (!context) return c.json({ error: "context required" }, 400);
|
|
3034
|
+
const limit = queryInt(c.req.query("limit"), 10);
|
|
3035
|
+
const tokenBudgetStr = c.req.query("token_budget");
|
|
3036
|
+
let tokenBudget;
|
|
3037
|
+
if (tokenBudgetStr) {
|
|
3038
|
+
tokenBudget = parseStrictInteger(tokenBudgetStr) ?? void 0;
|
|
3039
|
+
if (tokenBudget === void 0) return c.json({ error: "token_budget must be int" }, 400);
|
|
3040
|
+
}
|
|
3041
|
+
const project = c.req.query("project") || void 0;
|
|
3042
|
+
const filters = {};
|
|
3043
|
+
if (project) filters.project = project;
|
|
3044
|
+
const pack = await store.buildMemoryPackAsync(context, limit, tokenBudget ?? null, filters, void 0, void 0, { signal: c.req.raw.signal });
|
|
3045
|
+
return c.json(pack);
|
|
3046
|
+
}
|
|
2309
3047
|
function memoryRoutes(getStore) {
|
|
2310
3048
|
const app = new Hono();
|
|
2311
3049
|
app.get("/api/sessions", (c) => {
|
|
@@ -2431,25 +3169,7 @@ function memoryRoutes(getStore) {
|
|
|
2431
3169
|
});
|
|
2432
3170
|
}
|
|
2433
3171
|
});
|
|
2434
|
-
app.get("/api/pack",
|
|
2435
|
-
const store = getStore();
|
|
2436
|
-
{
|
|
2437
|
-
const context = c.req.query("context") || "";
|
|
2438
|
-
if (!context) return c.json({ error: "context required" }, 400);
|
|
2439
|
-
const limit = queryInt(c.req.query("limit"), 10);
|
|
2440
|
-
const tokenBudgetStr = c.req.query("token_budget");
|
|
2441
|
-
let tokenBudget;
|
|
2442
|
-
if (tokenBudgetStr) {
|
|
2443
|
-
tokenBudget = parseStrictInteger(tokenBudgetStr) ?? void 0;
|
|
2444
|
-
if (tokenBudget === void 0) return c.json({ error: "token_budget must be int" }, 400);
|
|
2445
|
-
}
|
|
2446
|
-
const project = c.req.query("project") || void 0;
|
|
2447
|
-
const filters = {};
|
|
2448
|
-
if (project) filters.project = project;
|
|
2449
|
-
const pack = await store.buildMemoryPackAsync(context, limit, tokenBudget ?? null, filters);
|
|
2450
|
-
return c.json(pack);
|
|
2451
|
-
}
|
|
2452
|
-
});
|
|
3172
|
+
app.get("/api/pack", (c) => handleLegacyPackRequest(c, getStore));
|
|
2453
3173
|
app.post("/api/pack/trace", async (c) => {
|
|
2454
3174
|
const store = getStore();
|
|
2455
3175
|
const parsed = await c.req.json().catch(() => Symbol.for("invalid-json"));
|
|
@@ -2471,7 +3191,11 @@ function memoryRoutes(getStore) {
|
|
|
2471
3191
|
const filters = {};
|
|
2472
3192
|
if (project) filters.project = project;
|
|
2473
3193
|
if (workingSetFiles.length > 0) filters.working_set_paths = workingSetFiles;
|
|
2474
|
-
const trace = await store.buildMemoryPackTraceAsync(context, limit, tokenBudget, filters
|
|
3194
|
+
const trace = await store.buildMemoryPackTraceAsync(context, limit, tokenBudget, filters, ...[
|
|
3195
|
+
void 0,
|
|
3196
|
+
void 0,
|
|
3197
|
+
{ signal: c.req.raw.signal }
|
|
3198
|
+
]);
|
|
2475
3199
|
return c.json(trace);
|
|
2476
3200
|
});
|
|
2477
3201
|
app.get("/api/memory", (c) => {
|
|
@@ -2893,6 +3617,22 @@ function expandMemories(store, args) {
|
|
|
2893
3617
|
}
|
|
2894
3618
|
};
|
|
2895
3619
|
}
|
|
3620
|
+
function piSessionSearchRoutes(getStore) {
|
|
3621
|
+
const searchApp = new Hono();
|
|
3622
|
+
searchApp.get("/api/pi/sessions/search", (c) => {
|
|
3623
|
+
const store = getStore();
|
|
3624
|
+
const query = c.req.query("query") ?? "";
|
|
3625
|
+
if (!query.trim()) return c.json({ error: "query required" }, 400);
|
|
3626
|
+
const result = searchPiSessions(store.db, query, {
|
|
3627
|
+
project: c.req.query("project"),
|
|
3628
|
+
session_id: c.req.query("session_id"),
|
|
3629
|
+
limit: queryInt(c.req.query("limit"), 10),
|
|
3630
|
+
snippet_chars: queryInt(c.req.query("snippet_chars"), 1200)
|
|
3631
|
+
});
|
|
3632
|
+
return c.json(result);
|
|
3633
|
+
});
|
|
3634
|
+
return searchApp;
|
|
3635
|
+
}
|
|
2896
3636
|
function memoryToolRoutes(getStore) {
|
|
2897
3637
|
const app = new Hono();
|
|
2898
3638
|
app.post("/api/memories/remember", async (c) => {
|
|
@@ -3091,7 +3831,7 @@ function memoryToolRoutes(getStore) {
|
|
|
3091
3831
|
return c.json({ error: msg }, 400);
|
|
3092
3832
|
}
|
|
3093
3833
|
});
|
|
3094
|
-
return app;
|
|
3834
|
+
return app.route("/", piSessionSearchRoutes(getStore));
|
|
3095
3835
|
}
|
|
3096
3836
|
//#endregion
|
|
3097
3837
|
//#region src/routes/observer-status.ts
|
|
@@ -3333,14 +4073,15 @@ function packRenderOptions(value) {
|
|
|
3333
4073
|
...value.compact_detail_count != null ? { compactDetailCount: value.compact_detail_count } : {}
|
|
3334
4074
|
};
|
|
3335
4075
|
}
|
|
3336
|
-
function packRequestArgs(request) {
|
|
4076
|
+
function packRequestArgs(request, signal) {
|
|
3337
4077
|
return [
|
|
3338
4078
|
request.context,
|
|
3339
4079
|
request.limit,
|
|
3340
4080
|
request.tokenBudget,
|
|
3341
4081
|
request.filters,
|
|
3342
4082
|
request.renderOptions,
|
|
3343
|
-
request.automaticContext
|
|
4083
|
+
request.automaticContext,
|
|
4084
|
+
{ signal }
|
|
3344
4085
|
];
|
|
3345
4086
|
}
|
|
3346
4087
|
function packSchemaError(error) {
|
|
@@ -3528,11 +4269,9 @@ function packTransportRoutes(getStore) {
|
|
|
3528
4269
|
const store = getStore();
|
|
3529
4270
|
const target = validateViewerTarget(store, parsed, { requireCurrentIdentity: true });
|
|
3530
4271
|
if (!target.ok) return c.json(target.body, target.status);
|
|
3531
|
-
if (!request.attempt)
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
}
|
|
3535
|
-
const artifacts = await store.buildMemoryPackWithTraceAsync(...packRequestArgs(request));
|
|
4272
|
+
if (!request.attempt) return await handleUntracedPackRequest(c, store, request);
|
|
4273
|
+
const artifacts = await store.buildMemoryPackWithTraceAsync(...packRequestArgs(request, c.req.raw.signal));
|
|
4274
|
+
c.req.raw.signal.throwIfAborted();
|
|
3536
4275
|
let ledgerArtifactFingerprint;
|
|
3537
4276
|
try {
|
|
3538
4277
|
ledgerArtifactFingerprint = promptPackArtifactFingerprint(store.db, request.context, request.filters, artifacts);
|
|
@@ -3556,6 +4295,11 @@ function packTransportRoutes(getStore) {
|
|
|
3556
4295
|
app.post("/api/prompt-pack-ledger", (c) => handleLedgerRequest(c, getStore));
|
|
3557
4296
|
return app;
|
|
3558
4297
|
}
|
|
4298
|
+
async function handleUntracedPackRequest(c, store, request) {
|
|
4299
|
+
const pack = await store.buildMemoryPackAsync(...packRequestArgs(request, c.req.raw.signal));
|
|
4300
|
+
c.req.raw.signal.throwIfAborted();
|
|
4301
|
+
return c.json(pack);
|
|
4302
|
+
}
|
|
3559
4303
|
//#endregion
|
|
3560
4304
|
//#region src/raw-event-processing.ts
|
|
3561
4305
|
function nudgeRawEventSessions(sweeper, sessions) {
|
|
@@ -3643,7 +4387,7 @@ async function writeDurableFile(path, contents) {
|
|
|
3643
4387
|
await handle.close();
|
|
3644
4388
|
}
|
|
3645
4389
|
}
|
|
3646
|
-
async function syncDirectory(path) {
|
|
4390
|
+
async function syncDirectory$1(path) {
|
|
3647
4391
|
let handle;
|
|
3648
4392
|
try {
|
|
3649
4393
|
handle = await open(path, "r");
|
|
@@ -3671,7 +4415,7 @@ async function ensurePrivateDirectory(path) {
|
|
|
3671
4415
|
const boundary = dirname(firstCreated);
|
|
3672
4416
|
let current = path;
|
|
3673
4417
|
while (true) {
|
|
3674
|
-
await syncDirectory(current);
|
|
4418
|
+
await syncDirectory$1(current);
|
|
3675
4419
|
if (current === boundary) return;
|
|
3676
4420
|
current = dirname(current);
|
|
3677
4421
|
}
|
|
@@ -3751,7 +4495,7 @@ var FileRawEventInbox = class {
|
|
|
3751
4495
|
const existingName = parseInboxFileName(existingEntry.name);
|
|
3752
4496
|
if (entryContentId(parsedExisting.entry) !== id || existingName?.enqueueOrder !== parsedExisting.enqueueOrder) throw new Error("raw-event inbox entry conflict");
|
|
3753
4497
|
await chmod(join(this.directory, existingEntry.name), 384);
|
|
3754
|
-
await syncDirectory(this.directory);
|
|
4498
|
+
await syncDirectory$1(this.directory);
|
|
3755
4499
|
this.scheduleDrain();
|
|
3756
4500
|
return;
|
|
3757
4501
|
}
|
|
@@ -3777,7 +4521,7 @@ var FileRawEventInbox = class {
|
|
|
3777
4521
|
} finally {
|
|
3778
4522
|
await rm(temporary, { force: true }).catch(() => {});
|
|
3779
4523
|
}
|
|
3780
|
-
await syncDirectory(this.directory);
|
|
4524
|
+
await syncDirectory$1(this.directory);
|
|
3781
4525
|
this.scheduleDrain();
|
|
3782
4526
|
}
|
|
3783
4527
|
async allocateEnqueueOrder(entries) {
|
|
@@ -6267,6 +7011,24 @@ function projectMappingChoices(store) {
|
|
|
6267
7011
|
sourceDisplayName: candidate.display_project
|
|
6268
7012
|
}));
|
|
6269
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
|
+
}
|
|
6270
7032
|
function viewerSafeDraft(store, draft) {
|
|
6271
7033
|
if (draft.devices.length > MAX_DEVICES || draft.projects.length > MAX_PROJECTS) throw new Error("legacy_team_setup_roster_too_large");
|
|
6272
7034
|
const completed = draft.state === "completed";
|
|
@@ -6276,21 +7038,7 @@ function viewerSafeDraft(store, draft) {
|
|
|
6276
7038
|
requireCompleteMappingChoices(mappableProjectCount, mappingChoices.length);
|
|
6277
7039
|
const mappingChoiceIdentities = mappingChoices.map((choice) => choice.projectIdentity);
|
|
6278
7040
|
return {
|
|
6279
|
-
devices: draft.devices.map((device) => (
|
|
6280
|
-
deviceRef: device.deviceRef,
|
|
6281
|
-
displayName: device.displayName,
|
|
6282
|
-
enabled: device.enabled,
|
|
6283
|
-
existingIdentityRef: identityRef(draft.candidateRef, device.existingIdentityId),
|
|
6284
|
-
suggestedIdentityRef: identityRef(draft.candidateRef, device.suggestedIdentityId),
|
|
6285
|
-
verifiedEvidenceKind: device.verifiedEvidenceKind,
|
|
6286
|
-
decision: device.decision,
|
|
6287
|
-
targetIdentityRef: identityRef(draft.candidateRef, device.targetIdentityId),
|
|
6288
|
-
expectation: device.expectation.kind === "existing" ? {
|
|
6289
|
-
kind: "existing",
|
|
6290
|
-
assignmentVersion: device.expectation.assignmentVersion,
|
|
6291
|
-
identityRef: requiredIdentityRef(draft.candidateRef, device.expectation.identityId)
|
|
6292
|
-
} : { kind: "absent" }
|
|
6293
|
-
})),
|
|
7041
|
+
devices: draft.devices.map((device) => viewerSafeDevice(draft.candidateRef, device)),
|
|
6294
7042
|
projects: draft.projects.map((project) => ({
|
|
6295
7043
|
projectRef: project.projectRef,
|
|
6296
7044
|
displayName: project.displayName,
|
|
@@ -9262,6 +10010,7 @@ function isPendingCaptureActorConflict(error) {
|
|
|
9262
10010
|
/** False leaves the conflict untouched; callers must check before claiming or merging identity. */
|
|
9263
10011
|
function ensureLocalActorRecord(store) {
|
|
9264
10012
|
return store.db.transaction(() => {
|
|
10013
|
+
store.assertLocalIdentityCurrent();
|
|
9265
10014
|
try {
|
|
9266
10015
|
store.assertPendingLocalCaptureAdoptionActors();
|
|
9267
10016
|
} catch (error) {
|
|
@@ -13628,6 +14377,7 @@ function readyContext(options, groupId, coordinatorUrl, raw, groups) {
|
|
|
13628
14377
|
groupId,
|
|
13629
14378
|
deviceId: device.deviceId,
|
|
13630
14379
|
fingerprint: device.fingerprint,
|
|
14380
|
+
publicKeyHash: createHash("sha256").update(device.publicKey).digest("hex"),
|
|
13631
14381
|
identityId: actor,
|
|
13632
14382
|
identityLabel: label(profile.identityLabel),
|
|
13633
14383
|
deviceLabel: label(profile.deviceLabel),
|
|
@@ -13707,28 +14457,2340 @@ function createAccountContextReader(options) {
|
|
|
13707
14457
|
};
|
|
13708
14458
|
}
|
|
13709
14459
|
//#endregion
|
|
13710
|
-
//#region src/
|
|
14460
|
+
//#region ../core/src/coordinator-ed25519-key-id.ts
|
|
14461
|
+
function decodeWire(encoded) {
|
|
14462
|
+
try {
|
|
14463
|
+
const binary = atob(encoded);
|
|
14464
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
14465
|
+
} catch {
|
|
14466
|
+
return null;
|
|
14467
|
+
}
|
|
14468
|
+
}
|
|
14469
|
+
function readRawKey(wire) {
|
|
14470
|
+
if (wire.length < 4) return null;
|
|
14471
|
+
const view = new DataView(wire.buffer, wire.byteOffset, wire.byteLength);
|
|
14472
|
+
const typeEnd = 4 + view.getUint32(0);
|
|
14473
|
+
if (wire.length < typeEnd + 4) return null;
|
|
14474
|
+
const keyLength = view.getUint32(typeEnd);
|
|
14475
|
+
const keyStart = typeEnd + 4;
|
|
14476
|
+
if (keyLength !== 32 || wire.length < keyStart + keyLength) return null;
|
|
14477
|
+
return wire.subarray(keyStart, keyStart + keyLength);
|
|
14478
|
+
}
|
|
14479
|
+
function canonicalBlob(rawKey) {
|
|
14480
|
+
const blob = /* @__PURE__ */ new Uint8Array(51);
|
|
14481
|
+
const view = new DataView(blob.buffer);
|
|
14482
|
+
view.setUint32(0, 11);
|
|
14483
|
+
blob.set(new TextEncoder().encode("ssh-ed25519"), 4);
|
|
14484
|
+
view.setUint32(15, 32);
|
|
14485
|
+
blob.set(rawKey, 19);
|
|
14486
|
+
return blob;
|
|
14487
|
+
}
|
|
13711
14488
|
/**
|
|
13712
|
-
*
|
|
13713
|
-
*
|
|
13714
|
-
*
|
|
13715
|
-
|
|
13716
|
-
|
|
14489
|
+
* Internal string contract, not an authentication acceptance check. Like the
|
|
14490
|
+
* Worker verifier, skip the inner type bytes and ignore trailing wire bytes.
|
|
14491
|
+
* atob rejects some encodings accepted by Node's Buffer base64 decoder.
|
|
14492
|
+
*/
|
|
14493
|
+
function parseSshEd25519PublicKey(publicKey) {
|
|
14494
|
+
return parseSshEd25519PublicKeyWithDecoder(publicKey, decodeWire);
|
|
14495
|
+
}
|
|
14496
|
+
/** Internal trusted decoder seam; callers must not use it to decide authentication acceptance. */
|
|
14497
|
+
function parseSshEd25519PublicKeyWithDecoder(publicKey, decode) {
|
|
14498
|
+
if (typeof publicKey !== "string") return { kind: "other" };
|
|
14499
|
+
const [keyType, keyData] = publicKey.trim().split(/\s+/);
|
|
14500
|
+
if (keyType !== "ssh-ed25519") return { kind: "other" };
|
|
14501
|
+
if (!keyData) return { kind: "malformed_ed25519" };
|
|
14502
|
+
let wire;
|
|
14503
|
+
try {
|
|
14504
|
+
wire = decode(keyData);
|
|
14505
|
+
} catch {
|
|
14506
|
+
return { kind: "malformed_ed25519" };
|
|
14507
|
+
}
|
|
14508
|
+
if (!wire) return { kind: "malformed_ed25519" };
|
|
14509
|
+
const rawKey = readRawKey(wire);
|
|
14510
|
+
if (!rawKey) return { kind: "malformed_ed25519" };
|
|
14511
|
+
return {
|
|
14512
|
+
kind: "ed25519",
|
|
14513
|
+
blob: canonicalBlob(rawKey)
|
|
14514
|
+
};
|
|
14515
|
+
}
|
|
14516
|
+
//#endregion
|
|
14517
|
+
//#region ../core/src/coordinator-auth-controller-history.ts
|
|
14518
|
+
var MAX_BYTES = 75e4;
|
|
14519
|
+
var CONTROLLER_HISTORY_CTE = `history_input AS MATERIALIZED (
|
|
14520
|
+
SELECT json_extract(value, '$.legacyHistory') AS value FROM snapshot
|
|
14521
|
+
), history_key_sources AS NOT MATERIALIZED (
|
|
14522
|
+
SELECT public_key, fingerprint FROM enrolled_devices
|
|
14523
|
+
UNION
|
|
14524
|
+
SELECT a.public_key, a.fingerprint FROM coordinator_auth_controller_attestations a
|
|
14525
|
+
CROSS JOIN history_input h
|
|
14526
|
+
WHERE a.coordinator_id = json_extract(h.value, '$.coordinatorId')
|
|
14527
|
+
), history_rows AS NOT MATERIALIZED (
|
|
14528
|
+
SELECT c.*, CASE WHEN json_valid(c.manifest_json) THEN c.manifest_json ELSE '{}' END AS parsed
|
|
14529
|
+
FROM coordinator_legacy_team_completions c
|
|
14530
|
+
), live_history AS MATERIALIZED (
|
|
14531
|
+
SELECT c.group_id, c.candidate_ref, c.manifest_version, c.completed_at, c.created_at,
|
|
14532
|
+
CASE WHEN length(CAST(c.manifest_json AS BLOB)) <= ${MAX_BYTES} THEN c.manifest_json ELSE NULL END AS manifest_json
|
|
14533
|
+
FROM history_rows c CROSS JOIN history_input h
|
|
14534
|
+
WHERE h.value IS NOT NULL
|
|
14535
|
+
AND (json_extract(c.parsed, '$.coordinator_id') = json_extract(h.value, '$.coordinatorId')
|
|
14536
|
+
OR json_extract(c.parsed, '$.coordinator_id') IS NULL)
|
|
14537
|
+
AND (EXISTS (
|
|
14538
|
+
SELECT 1 FROM json_each(c.parsed, '$.device_decisions') d
|
|
14539
|
+
WHERE json_extract(CASE WHEN d.type = 'object' THEN d.value ELSE '{}' END, '$.device_id') = json_extract(h.value, '$.deviceId')
|
|
14540
|
+
OR json_extract(CASE WHEN d.type = 'object' THEN d.value ELSE '{}' END, '$.key_fingerprint')
|
|
14541
|
+
IN (SELECT value FROM json_each(h.value, '$.fingerprints'))
|
|
14542
|
+
OR ((d.type <> 'object'
|
|
14543
|
+
OR json_type(CASE WHEN d.type = 'object' THEN d.value ELSE '{}' END, '$.device_id') IS NOT 'text'
|
|
14544
|
+
OR json_type(CASE WHEN d.type = 'object' THEN d.value ELSE '{}' END, '$.key_fingerprint') IS NOT 'text')
|
|
14545
|
+
AND (instr(CASE WHEN d.type = 'text' THEN json_quote(d.value) ELSE d.value END,
|
|
14546
|
+
json_quote(json_extract(h.value, '$.deviceId'))) > 0
|
|
14547
|
+
OR EXISTS (SELECT 1 FROM json_each(h.value, '$.fingerprints') f
|
|
14548
|
+
WHERE instr(d.value, f.value) > 0)))
|
|
14549
|
+
) OR ((json_valid(c.manifest_json) = 0 OR json_type(c.parsed, '$.device_decisions') IS NOT 'array') AND (
|
|
14550
|
+
instr(c.manifest_json, json_quote(json_extract(h.value, '$.deviceId'))) > 0
|
|
14551
|
+
OR EXISTS (SELECT 1 FROM json_each(h.value, '$.fingerprints') f
|
|
14552
|
+
WHERE instr(c.manifest_json, f.value) > 0)
|
|
14553
|
+
))) LIMIT 501
|
|
14554
|
+
), snapshot_history AS MATERIALIZED (
|
|
14555
|
+
SELECT json_extract(j.value, '$.group_id') AS group_id,
|
|
14556
|
+
json_extract(j.value, '$.candidate_ref') AS candidate_ref,
|
|
14557
|
+
json_extract(j.value, '$.manifest_version') AS manifest_version,
|
|
14558
|
+
json_extract(j.value, '$.completed_at') AS completed_at,
|
|
14559
|
+
json_extract(j.value, '$.created_at') AS created_at,
|
|
14560
|
+
json_extract(j.value, '$.manifest_json') AS manifest_json
|
|
14561
|
+
FROM history_input h, json_each(h.value, '$.records') j
|
|
14562
|
+
), snapshot_history_keys AS MATERIALIZED (
|
|
14563
|
+
SELECT json_extract(j.value, '$.public_key') AS public_key,
|
|
14564
|
+
json_extract(j.value, '$.fingerprint') AS fingerprint
|
|
14565
|
+
FROM history_input h, json_each(h.value, '$.keys') j
|
|
14566
|
+
), snapshot_discovery_keys AS MATERIALIZED (
|
|
14567
|
+
SELECT j.value AS public_key FROM history_input h, json_each(h.value, '$.discoveryKeys') j
|
|
14568
|
+
), discovery_lines AS NOT MATERIALIZED (
|
|
14569
|
+
SELECT public_key,
|
|
14570
|
+
ltrim(replace(replace(replace(replace(replace(public_key,
|
|
14571
|
+
char(9), ' '), char(10), ' '), char(11), ' '), char(12), ' '), char(13), ' '), ' ') AS line
|
|
14572
|
+
FROM history_key_sources
|
|
14573
|
+
), discovery_tails AS NOT MATERIALIZED (
|
|
14574
|
+
SELECT public_key, CASE WHEN substr(line, 1, 12) = 'ssh-ed25519 '
|
|
14575
|
+
THEN ltrim(substr(line, 13), ' ') ELSE NULL END AS tail FROM discovery_lines
|
|
14576
|
+
), discovery_tokens AS NOT MATERIALIZED (
|
|
14577
|
+
SELECT public_key, CASE WHEN tail IS NOT NULL
|
|
14578
|
+
THEN substr(tail, 1, instr(tail || ' ', ' ') - 1) ELSE NULL END AS token
|
|
14579
|
+
FROM discovery_tails
|
|
14580
|
+
), live_discovery_keys AS MATERIALIZED (
|
|
14581
|
+
SELECT DISTINCT e.public_key FROM discovery_tokens e CROSS JOIN history_input h
|
|
14582
|
+
WHERE e.public_key = json_extract(h.value, '$.publicKey')
|
|
14583
|
+
OR instr(e.public_key, char(0)) > 0
|
|
14584
|
+
OR e.token IS NULL
|
|
14585
|
+
OR EXISTS (SELECT 1 FROM json_each(h.value, '$.discoveryFragments') f
|
|
14586
|
+
WHERE instr(replace(replace(e.token, '-', '+'), '_', '/'), f.value) > 0)
|
|
14587
|
+
OR e.token GLOB '*[^A-Za-z0-9+/=_-]*'
|
|
14588
|
+
LIMIT 8193
|
|
14589
|
+
), live_history_keys AS MATERIALIZED (
|
|
14590
|
+
SELECT DISTINCT e.public_key, e.fingerprint FROM history_key_sources e
|
|
14591
|
+
WHERE e.public_key IN (SELECT public_key FROM snapshot_history_keys)
|
|
14592
|
+
LIMIT 4097
|
|
14593
|
+
)`;
|
|
14594
|
+
`${CONTROLLER_HISTORY_CTE}`;
|
|
14595
|
+
`${CONTROLLER_HISTORY_CTE}`;
|
|
14596
|
+
`${CONTROLLER_HISTORY_CTE}${MAX_BYTES}`;
|
|
14597
|
+
var CONTROLLER_HISTORY_GUARD = `AND (
|
|
14598
|
+
(SELECT value FROM history_input) IS NULL OR (
|
|
14599
|
+
json_extract((SELECT value FROM history_input), '$.deviceId') = e.device_id
|
|
14600
|
+
AND json_extract((SELECT value FROM history_input), '$.publicKey') = e.public_key
|
|
14601
|
+
AND json_extract((SELECT value FROM history_input), '$.fingerprint') = e.fingerprint
|
|
14602
|
+
AND (SELECT count(*) FROM live_history) = (SELECT count(*) FROM snapshot_history)
|
|
14603
|
+
AND NOT EXISTS (SELECT * FROM live_history EXCEPT SELECT * FROM snapshot_history)
|
|
14604
|
+
AND NOT EXISTS (SELECT * FROM live_history_keys EXCEPT SELECT * FROM snapshot_history_keys)
|
|
14605
|
+
AND NOT EXISTS (SELECT * FROM snapshot_history_keys EXCEPT SELECT * FROM live_history_keys)
|
|
14606
|
+
AND NOT EXISTS (SELECT * FROM live_discovery_keys EXCEPT SELECT * FROM snapshot_discovery_keys)
|
|
14607
|
+
AND NOT EXISTS (SELECT * FROM snapshot_discovery_keys EXCEPT SELECT * FROM live_discovery_keys)
|
|
14608
|
+
))`;
|
|
14609
|
+
//#endregion
|
|
14610
|
+
//#region ../core/src/coordinator-account-connection-storage.ts
|
|
14611
|
+
/**
|
|
14612
|
+
* Storage constraints, not an unlink/relink authorization API.
|
|
14613
|
+
* The explicit account-schema migration installs this DDL. Management writers
|
|
14614
|
+
* compose the winning proof, archive move and successor CAS in one backend.batch;
|
|
14615
|
+
* an Identity identifier alone never authorizes these effects.
|
|
14616
|
+
*/
|
|
14617
|
+
var CONNECTION_BIRTH_COLUMNS = [
|
|
14618
|
+
"coordinator_id",
|
|
14619
|
+
"link_id",
|
|
14620
|
+
"issuer",
|
|
14621
|
+
"subject",
|
|
14622
|
+
"identity_id",
|
|
14623
|
+
"attempt_id",
|
|
14624
|
+
"controller_attestation_id",
|
|
14625
|
+
"auth_config_revision",
|
|
14626
|
+
"created_at_ms",
|
|
14627
|
+
"revoked_at_ms",
|
|
14628
|
+
"authority_kind",
|
|
14629
|
+
"device_claim_id"
|
|
14630
|
+
];
|
|
14631
|
+
var current = "coordinator_auth_account_links";
|
|
14632
|
+
var history = "coordinator_auth_account_connection_history";
|
|
14633
|
+
var orders = "coordinator_auth_account_connection_orders";
|
|
14634
|
+
var birthEqual = CONNECTION_BIRTH_COLUMNS.map((c) => `c.${c} IS NEW.${c}`).join(" AND ");
|
|
14635
|
+
var immutableBirth = CONNECTION_BIRTH_COLUMNS.filter((c) => c !== "revoked_at_ms").map((c) => `NEW.${c} IS NOT OLD.${c}`).join(" OR ");
|
|
14636
|
+
`${CONNECTION_BIRTH_COLUMNS.map((column) => `h.${column} IS c.${column}`).join(" AND ")}`;
|
|
14637
|
+
`${current}${history}`;
|
|
14638
|
+
/** Alias l must be the pinned current target, never a caller-selected Identity. */
|
|
14639
|
+
var CONNECTION_NOT_REVOKED_SQL = `l.revoked_at_ms IS NULL AND NOT EXISTS (
|
|
14640
|
+
SELECT 1 FROM ${history} h WHERE h.coordinator_id = l.coordinator_id
|
|
14641
|
+
AND h.identity_id = l.identity_id AND h.revoked_at_ms IS NOT NULL)`;
|
|
14642
|
+
`${history}${current}${history}`;
|
|
14643
|
+
`${orders}`;
|
|
14644
|
+
`${orders}`;
|
|
14645
|
+
`${history}`, `${history}${history}${history}${history}${current}${birthEqual}`, `${history}`, `${history}${immutableBirth}${history}${current}${history}`, `${history}`, `${current}${history}`, `${history}`, `${orders}`, `${orders}${orders}`, `${orders}`, `${orders}`;
|
|
14646
|
+
`${CONNECTION_BIRTH_COLUMNS.join(",")}${CONNECTION_BIRTH_COLUMNS.join(",")}`;
|
|
14647
|
+
function rootBirths(coordinator, link) {
|
|
14648
|
+
return `SELECT ${CONNECTION_BIRTH_COLUMNS.join(",")} FROM coordinator_auth_account_links WHERE coordinator_id = ${coordinator} AND link_id = ${link}
|
|
14649
|
+
UNION ALL SELECT ${CONNECTION_BIRTH_COLUMNS.join(",")} FROM coordinator_auth_account_connection_history WHERE coordinator_id = ${coordinator} AND link_id = ${link}`;
|
|
14650
|
+
}
|
|
14651
|
+
/** Trusted SQL aliases only. Historical ownership is independent of current enrollment.
|
|
14652
|
+
* The immutable proof's key ID was derived at commit. Signed-device receipts also
|
|
14653
|
+
* pass a freshly derived canonical key ID; account sessions need no current key.
|
|
14654
|
+
* Link revocation is deliberately checked by the caller, including post-revoke audit.
|
|
14655
|
+
*/
|
|
14656
|
+
function authDeviceClaimProvenanceSql(options) {
|
|
14657
|
+
if (options.connections) return connectionClaimProvenance(options);
|
|
14658
|
+
return genesisClaimProvenance(options.link, "coordinator_auth_account_links", options.canonicalKeyId);
|
|
14659
|
+
}
|
|
14660
|
+
function genesisClaimProvenance(link, births, canonicalKeyId) {
|
|
14661
|
+
const l = "pl";
|
|
14662
|
+
return `EXISTS (WITH committed_claim AS MATERIALIZED (
|
|
14663
|
+
SELECT ct.coordinator_id, ct.link_id, cd.key_id FROM coordinator_auth_link_attempts ct
|
|
14664
|
+
JOIN ${births} pl ON pl.coordinator_id = ct.coordinator_id AND pl.link_id = ct.link_id
|
|
14665
|
+
JOIN coordinator_auth_device_claims cd ON cd.coordinator_id = ct.coordinator_id AND cd.claim_id = ${l}.device_claim_id
|
|
14666
|
+
JOIN coordinator_auth_link_audit_log ca ON ca.coordinator_id = ct.coordinator_id AND ca.link_id = ct.link_id AND ca.action = 'link_created'
|
|
14667
|
+
WHERE ct.coordinator_id = ${link}.coordinator_id AND ct.link_id = ${link}.link_id
|
|
14668
|
+
AND ct.attempt_id = ${l}.attempt_id
|
|
14669
|
+
AND ct.authority_kind = 'device_claim' AND ct.controller_attestation_id IS NULL
|
|
14670
|
+
AND ct.controller_review_receipt_id IS NULL AND ct.controller_revision IS NULL AND ct.state IN ('finalized','session_redeemed')
|
|
14671
|
+
AND ${l}.authority_kind = 'device_claim' AND ${l}.controller_attestation_id IS NULL
|
|
14672
|
+
AND ct.identity_id = ${l}.identity_id AND ct.issuer = ${l}.issuer AND ct.account_subject = ${l}.subject
|
|
14673
|
+
AND ct.auth_config_revision = ${l}.auth_config_revision AND ct.finalized_at_ms = ${l}.created_at_ms
|
|
14674
|
+
AND ct.browser_transaction_hash IS NOT NULL AND ct.browser_start_hash IS NOT NULL
|
|
14675
|
+
AND ct.runtime_verifier_hash IS NOT NULL AND ct.completion_secret_hash IS NOT NULL
|
|
14676
|
+
AND ct.created_at_ms <= ct.claimed_at_ms AND ct.claimed_at_ms <= ct.oidc_verified_at_ms
|
|
14677
|
+
AND ct.oidc_verified_at_ms <= ct.confirmed_at_ms AND ct.confirmed_at_ms <= ct.finalized_at_ms AND ct.finalized_at_ms < ct.expires_at_ms
|
|
14678
|
+
AND cd.attempt_id = ct.attempt_id AND cd.identity_id = ct.identity_id AND cd.group_id = ct.group_id AND cd.device_id = ct.device_id
|
|
14679
|
+
AND cd.public_key = ct.public_key AND cd.fingerprint = ct.fingerprint
|
|
14680
|
+
AND cd.issuer = ct.issuer AND cd.auth_config_revision = ct.auth_config_revision AND cd.created_at_ms = ct.finalized_at_ms
|
|
14681
|
+
AND ca.authority_kind = 'device_claim' AND ca.controller_attestation_id IS NULL AND ca.device_claim_id IS ${l}.device_claim_id
|
|
14682
|
+
AND ca.attempt_id = ct.attempt_id AND ca.identity_id = ct.identity_id AND ca.group_id = ct.group_id AND ca.device_id = ct.device_id
|
|
14683
|
+
AND ca.fingerprint = ct.fingerprint AND ca.auth_config_revision = ct.auth_config_revision AND ca.created_at_ms = ct.finalized_at_ms)
|
|
14684
|
+
SELECT 1 FROM committed_claim cp WHERE cp.coordinator_id = ${link}.coordinator_id AND cp.link_id = ${link}.link_id
|
|
14685
|
+
${canonicalKeyId ? `AND cp.key_id = ${canonicalKeyId}` : ""})`;
|
|
14686
|
+
}
|
|
14687
|
+
function connectionClaimProvenance(options) {
|
|
14688
|
+
const l = options.link;
|
|
14689
|
+
return `EXISTS (WITH connection_births AS MATERIALIZED (${rootBirths(`${l}.coordinator_id`, `${l}.device_claim_id`)}), continuation AS MATERIALIZED (
|
|
14690
|
+
SELECT t.coordinator_id,t.link_id,d.key_id FROM coordinator_auth_link_attempts t
|
|
14691
|
+
JOIN coordinator_auth_link_audit_log a ON a.coordinator_id = t.coordinator_id AND a.link_id = t.link_id AND a.action = 'link_created'
|
|
14692
|
+
JOIN coordinator_auth_device_claims d ON d.coordinator_id = t.coordinator_id AND d.claim_id = ${l}.device_claim_id
|
|
14693
|
+
WHERE t.coordinator_id = ${l}.coordinator_id AND t.link_id = ${l}.link_id AND t.attempt_id = ${l}.attempt_id
|
|
14694
|
+
AND (${l}.authority_kind = 'device_claim' AND ${l}.controller_attestation_id IS NULL AND t.authority_kind = 'device_claim' AND t.controller_attestation_id IS NULL AND t.controller_review_receipt_id IS NULL AND t.controller_revision IS NULL)
|
|
14695
|
+
AND (t.state IN ('finalized','session_redeemed') AND t.identity_id = ${l}.identity_id AND t.issuer = ${l}.issuer AND t.account_subject = ${l}.subject AND t.auth_config_revision = ${l}.auth_config_revision AND t.finalized_at_ms = ${l}.created_at_ms)
|
|
14696
|
+
AND (t.browser_start_hash IS NOT NULL AND t.browser_transaction_hash IS NOT NULL AND t.runtime_verifier_hash IS NOT NULL AND t.completion_secret_hash IS NOT NULL AND t.created_at_ms <= t.claimed_at_ms AND t.claimed_at_ms <= t.oidc_verified_at_ms AND t.oidc_verified_at_ms <= t.confirmed_at_ms AND t.confirmed_at_ms <= t.finalized_at_ms AND t.finalized_at_ms < t.expires_at_ms)
|
|
14697
|
+
AND (d.identity_id = t.identity_id AND d.issuer = t.issuer AND d.group_id = t.group_id AND d.device_id = t.device_id AND d.public_key = t.public_key AND d.fingerprint = t.fingerprint)
|
|
14698
|
+
AND (a.attempt_id = t.attempt_id AND a.identity_id = t.identity_id AND a.group_id = t.group_id AND a.device_id = t.device_id AND a.fingerprint = t.fingerprint AND a.auth_config_revision = t.auth_config_revision AND a.created_at_ms = t.finalized_at_ms AND a.authority_kind = 'device_claim' AND a.controller_attestation_id IS NULL AND a.device_claim_id = d.claim_id)
|
|
14699
|
+
AND ((t.prior_link_id IS NULL AND t.link_id = d.claim_id) OR EXISTS (SELECT 1 FROM coordinator_auth_account_connection_history h WHERE h.coordinator_id = t.coordinator_id AND h.link_id = t.prior_link_id AND h.identity_id = t.identity_id AND h.successor_link_id = t.link_id))
|
|
14700
|
+
AND EXISTS (SELECT 1 FROM connection_births gl WHERE gl.coordinator_id = d.coordinator_id AND gl.link_id = d.claim_id AND ${genesisClaimProvenance("gl", "connection_births")}))
|
|
14701
|
+
SELECT 1 FROM continuation cp ${options.canonicalKeyId ? "WHERE cp.key_id = ?" : ""})`;
|
|
14702
|
+
}
|
|
14703
|
+
/** Account management only; privileged enrollment/grant proofs remain attested-only. */
|
|
14704
|
+
function controllerBirthProvenance(link) {
|
|
14705
|
+
return `EXISTS (WITH controller_receipt AS MATERIALIZED (SELECT t.coordinator_id,t.link_id FROM coordinator_auth_link_attempts t
|
|
14706
|
+
JOIN coordinator_auth_controller_attestations origin ON origin.coordinator_id=t.coordinator_id AND origin.attestation_id=t.controller_attestation_id
|
|
14707
|
+
JOIN coordinator_auth_link_audit_log a ON a.coordinator_id=t.coordinator_id AND a.link_id=t.link_id AND a.action='link_created'
|
|
14708
|
+
WHERE (t.coordinator_id=${link}.coordinator_id AND t.link_id=${link}.link_id AND t.attempt_id=${link}.attempt_id AND t.authority_kind='controller_attestation' AND t.controller_attestation_id=${link}.controller_attestation_id)
|
|
14709
|
+
AND (t.identity_id=${link}.identity_id AND t.issuer=${link}.issuer AND t.account_subject=${link}.subject AND t.auth_config_revision=${link}.auth_config_revision AND t.finalized_at_ms=${link}.created_at_ms AND t.state IN ('finalized','session_redeemed'))
|
|
14710
|
+
AND (t.browser_transaction_hash IS NOT NULL AND t.runtime_verifier_hash IS NOT NULL AND t.completion_secret_hash IS NOT NULL AND t.created_at_ms<=t.claimed_at_ms AND t.claimed_at_ms<=t.oidc_verified_at_ms AND t.oidc_verified_at_ms<=t.confirmed_at_ms AND t.confirmed_at_ms<=t.finalized_at_ms AND t.finalized_at_ms<t.expires_at_ms)
|
|
14711
|
+
AND (origin.identity_id=t.identity_id AND origin.group_id=t.group_id AND origin.device_id=t.device_id AND origin.public_key=t.public_key AND origin.fingerprint=t.fingerprint AND origin.review_receipt_id=t.controller_review_receipt_id AND origin.revision=t.controller_revision)
|
|
14712
|
+
AND (t.prior_link_id IS NULL OR EXISTS (SELECT 1 FROM coordinator_auth_account_connection_history h WHERE h.coordinator_id=t.coordinator_id AND h.identity_id=t.identity_id AND h.link_id=t.prior_link_id AND h.successor_link_id=t.link_id))
|
|
14713
|
+
AND (a.attempt_id=t.attempt_id AND a.identity_id=t.identity_id AND a.group_id=t.group_id AND a.device_id=t.device_id AND a.fingerprint=t.fingerprint AND a.controller_attestation_id=t.controller_attestation_id AND a.authority_kind=t.authority_kind AND a.device_claim_id IS NULL AND a.auth_config_revision=t.auth_config_revision AND a.created_at_ms=t.finalized_at_ms)) SELECT 1 FROM controller_receipt)`;
|
|
14714
|
+
}
|
|
14715
|
+
function authAccountLinkProvenanceSql(link, options = {}) {
|
|
14716
|
+
return `((${link}.authority_kind = 'controller_attestation' AND ${link}.controller_attestation_id IS NOT NULL AND ${link}.device_claim_id IS NULL${options.connections ? ` AND ${controllerBirthProvenance(link)}` : ""})
|
|
14717
|
+
OR (${authDeviceClaimProvenanceSql({
|
|
14718
|
+
link,
|
|
14719
|
+
connections: options.connections
|
|
14720
|
+
})}))`;
|
|
14721
|
+
}
|
|
14722
|
+
`${CONNECTION_NOT_REVOKED_SQL}${authAccountLinkProvenanceSql("l", { connections: true })}`;
|
|
14723
|
+
//#endregion
|
|
14724
|
+
//#region ../core/src/coordinator-auth-controller.ts
|
|
14725
|
+
function isAuthControllerId(value) {
|
|
14726
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
|
|
14727
|
+
}
|
|
14728
|
+
var AUTH_CONTROLLER_SNAPSHOT_CTE = `
|
|
14729
|
+
WITH snapshot(value) AS (SELECT ?), snapshot_invites AS MATERIALIZED (
|
|
14730
|
+
SELECT json_extract(j.value, '$.inviteId') AS invite_id,
|
|
14731
|
+
json_extract(j.value, '$.kind') AS invite_kind,
|
|
14732
|
+
json_extract(j.value, '$.actorId') AS recipient_actor_id,
|
|
14733
|
+
json_extract(j.value, '$.assignedIdentityId') AS assigned_identity_id,
|
|
14734
|
+
json_extract(j.value, '$.targetIdentityId') AS target_identity_id,
|
|
14735
|
+
json_extract(j.value, '$.digest') AS reviewed_preview_digest
|
|
14736
|
+
FROM json_each((SELECT value FROM snapshot), '$.invites') j
|
|
14737
|
+
), live_invites AS MATERIALIZED (
|
|
14738
|
+
SELECT i.invite_id, i.invite_kind, i.recipient_actor_id, i.assigned_identity_id,
|
|
14739
|
+
i.target_identity_id, i.reviewed_preview_digest FROM coordinator_invites i
|
|
14740
|
+
WHERE i.group_id = ? AND i.bound_device_id = ? AND i.bound_public_key = ?
|
|
14741
|
+
AND i.bound_fingerprint = ? AND i.revoked_at IS NULL
|
|
14742
|
+
AND i.consumed_at IS NOT NULL AND i.consumed_at != ''
|
|
14743
|
+
AND i.invite_kind IN ('team_member', 'add_device')
|
|
14744
|
+
), ${CONTROLLER_HISTORY_CTE}`;
|
|
14745
|
+
var AUTH_CONTROLLER_SNAPSHOT_GUARD = `
|
|
14746
|
+
AND ((SELECT value FROM snapshot) IS NULL OR (
|
|
14747
|
+
e.identity_id IS json_extract((SELECT value FROM snapshot), '$.enrollmentIdentityId')
|
|
14748
|
+
AND (SELECT count(*) FROM live_invites) =
|
|
14749
|
+
(SELECT count(*) FROM snapshot_invites)
|
|
14750
|
+
AND NOT EXISTS (
|
|
14751
|
+
SELECT * FROM live_invites
|
|
14752
|
+
EXCEPT
|
|
14753
|
+
SELECT * FROM snapshot_invites
|
|
14754
|
+
)
|
|
14755
|
+
)) ${CONTROLLER_HISTORY_GUARD}`;
|
|
14756
|
+
var AUTH_CONTROLLER_AUTHORITY_GUARD = `
|
|
14757
|
+
AND e.group_id = ? AND e.device_id = ? AND e.public_key = ?
|
|
14758
|
+
AND e.fingerprint = ? AND e.identity_id IS ?
|
|
14759
|
+
AND NOT EXISTS (SELECT 1 FROM coordinator_device_revocations WHERE
|
|
14760
|
+
(subject_kind = 'device_id' AND subject_value = ?) OR
|
|
14761
|
+
(subject_kind = 'ed25519_key' AND subject_value = ?))`;
|
|
14762
|
+
`${AUTH_CONTROLLER_SNAPSHOT_CTE}${AUTH_CONTROLLER_SNAPSHOT_GUARD}${AUTH_CONTROLLER_AUTHORITY_GUARD}`;
|
|
14763
|
+
`${AUTH_CONTROLLER_SNAPSHOT_CTE}${AUTH_CONTROLLER_SNAPSHOT_GUARD}${AUTH_CONTROLLER_AUTHORITY_GUARD}`;
|
|
14764
|
+
var AUTH_CONTROLLER_CURRENT_SQL = `
|
|
14765
|
+
SELECT a.* FROM coordinator_auth_controller_attestations a
|
|
14766
|
+
JOIN enrolled_devices e ON e.group_id = a.group_id AND e.device_id = a.device_id
|
|
14767
|
+
JOIN groups g ON g.group_id = a.group_id
|
|
14768
|
+
WHERE a.coordinator_id = ? AND a.attestation_id = ? AND a.revoked_at IS NULL
|
|
14769
|
+
AND e.enabled = 1 AND g.archived_at IS NULL AND e.public_key = a.public_key
|
|
14770
|
+
AND e.fingerprint = a.fingerprint
|
|
14771
|
+
AND (e.identity_id IS NULL OR e.identity_id = a.identity_id)`;
|
|
14772
|
+
AUTH_CONTROLLER_CURRENT_SQL.replace("SELECT a.*", "SELECT e.*");
|
|
14773
|
+
`${AUTH_CONTROLLER_SNAPSHOT_CTE}`, `${AUTH_CONTROLLER_CURRENT_SQL}${AUTH_CONTROLLER_AUTHORITY_GUARD}`, `${AUTH_CONTROLLER_SNAPSHOT_GUARD}`;
|
|
14774
|
+
function ownerProfileConflict() {
|
|
14775
|
+
throw new Error("Owner enrollment stopped for review. Keep the profile and saved request unchanged; no identity or access repair is automatic.");
|
|
14776
|
+
}
|
|
14777
|
+
[{
|
|
14778
|
+
name: "trg_identity_devices_assignment_version",
|
|
14779
|
+
create: `CREATE TRIGGER trg_identity_devices_assignment_version
|
|
14780
|
+
AFTER UPDATE OF identity_id ON identity_devices
|
|
14781
|
+
WHEN NEW.identity_id <> OLD.identity_id
|
|
14782
|
+
BEGIN
|
|
14783
|
+
UPDATE identity_devices
|
|
14784
|
+
SET assignment_version = OLD.assignment_version + 1
|
|
14785
|
+
WHERE device_id = NEW.device_id;
|
|
14786
|
+
END`
|
|
14787
|
+
}, {
|
|
14788
|
+
name: "trg_identity_devices_purge_decisions",
|
|
14789
|
+
create: `CREATE TRIGGER trg_identity_devices_purge_decisions
|
|
14790
|
+
AFTER DELETE ON identity_devices
|
|
14791
|
+
BEGIN
|
|
14792
|
+
DELETE FROM policy_team_device_decisions WHERE device_id = OLD.device_id;
|
|
14793
|
+
END`
|
|
14794
|
+
}].map(({ name, create }) => `DROP TRIGGER IF EXISTS ${name};\n${create};`).join("\n");
|
|
14795
|
+
[
|
|
14796
|
+
[
|
|
14797
|
+
"insert",
|
|
14798
|
+
"AFTER INSERT",
|
|
14799
|
+
""
|
|
14800
|
+
],
|
|
14801
|
+
[
|
|
14802
|
+
"delete",
|
|
14803
|
+
"AFTER DELETE",
|
|
14804
|
+
""
|
|
14805
|
+
],
|
|
14806
|
+
[
|
|
14807
|
+
"update",
|
|
14808
|
+
"AFTER UPDATE",
|
|
14809
|
+
"WHEN OLD.id IS NOT NEW.id OR OLD.title IS NOT NEW.title OR OLD.body_text IS NOT NEW.body_text OR OLD.active IS NOT NEW.active"
|
|
14810
|
+
]
|
|
14811
|
+
].map(([suffix, event, condition]) => ({
|
|
14812
|
+
name: `vector_corpus_epoch_${suffix}`,
|
|
14813
|
+
sql: `CREATE TRIGGER vector_corpus_epoch_${suffix} ${event} ON memory_items ${condition}
|
|
14814
|
+
BEGIN UPDATE vector_corpus_epoch SET epoch = epoch + 1 WHERE id = 1; END`
|
|
14815
|
+
}));
|
|
14816
|
+
join(homedir(), ".codemem", "mem.sqlite");
|
|
14817
|
+
join(homedir(), ".codemem.sqlite"), join(homedir(), ".opencode-mem.sqlite");
|
|
14818
|
+
//#endregion
|
|
14819
|
+
//#region ../core/src/schema.ts
|
|
14820
|
+
/**
|
|
14821
|
+
* Drizzle ORM schema for the codemem SQLite database.
|
|
14822
|
+
*/
|
|
14823
|
+
var sessions = sqliteTable("sessions", {
|
|
14824
|
+
id: integer("id").primaryKey(),
|
|
14825
|
+
started_at: text("started_at").notNull(),
|
|
14826
|
+
ended_at: text("ended_at"),
|
|
14827
|
+
cwd: text("cwd"),
|
|
14828
|
+
project: text("project"),
|
|
14829
|
+
git_remote: text("git_remote"),
|
|
14830
|
+
git_branch: text("git_branch"),
|
|
14831
|
+
user: text("user"),
|
|
14832
|
+
tool_version: text("tool_version"),
|
|
14833
|
+
metadata_json: text("metadata_json"),
|
|
14834
|
+
import_key: text("import_key")
|
|
14835
|
+
}, (table) => [index("idx_sessions_started_id").on(table.started_at, table.id)]);
|
|
14836
|
+
sqliteTable("replication_scopes", {
|
|
14837
|
+
scope_id: text("scope_id").primaryKey(),
|
|
14838
|
+
label: text("label").notNull(),
|
|
14839
|
+
kind: text("kind").notNull().default("user"),
|
|
14840
|
+
authority_type: text("authority_type").notNull().default("local"),
|
|
14841
|
+
coordinator_id: text("coordinator_id"),
|
|
14842
|
+
group_id: text("group_id"),
|
|
14843
|
+
manifest_issuer_device_id: text("manifest_issuer_device_id"),
|
|
14844
|
+
membership_epoch: integer("membership_epoch").notNull().default(0),
|
|
14845
|
+
manifest_hash: text("manifest_hash"),
|
|
14846
|
+
status: text("status").notNull().default("active"),
|
|
14847
|
+
created_at: text("created_at").notNull(),
|
|
14848
|
+
updated_at: text("updated_at").notNull()
|
|
14849
|
+
}, (table) => [index("idx_replication_scopes_status").on(table.status), index("idx_replication_scopes_authority_group").on(table.coordinator_id, table.group_id)]);
|
|
14850
|
+
sqliteTable("project_scope_mappings", {
|
|
14851
|
+
id: integer("id").primaryKey(),
|
|
14852
|
+
workspace_identity: text("workspace_identity"),
|
|
14853
|
+
project_pattern: text("project_pattern").notNull(),
|
|
14854
|
+
scope_id: text("scope_id").notNull(),
|
|
14855
|
+
priority: integer("priority").notNull().default(0),
|
|
14856
|
+
source: text("source").notNull().default("user"),
|
|
14857
|
+
created_at: text("created_at").notNull(),
|
|
14858
|
+
updated_at: text("updated_at").notNull()
|
|
14859
|
+
}, (table) => [
|
|
14860
|
+
index("idx_project_scope_mappings_workspace_priority").on(table.workspace_identity, table.priority),
|
|
14861
|
+
index("idx_project_scope_mappings_pattern_priority").on(table.project_pattern, table.priority),
|
|
14862
|
+
index("idx_project_scope_mappings_scope").on(table.scope_id)
|
|
14863
|
+
]);
|
|
14864
|
+
sqliteTable("scope_memberships", {
|
|
14865
|
+
scope_id: text("scope_id").notNull(),
|
|
14866
|
+
device_id: text("device_id").notNull(),
|
|
14867
|
+
role: text("role").notNull().default("member"),
|
|
14868
|
+
status: text("status").notNull().default("active"),
|
|
14869
|
+
membership_epoch: integer("membership_epoch").notNull().default(0),
|
|
14870
|
+
coordinator_id: text("coordinator_id"),
|
|
14871
|
+
group_id: text("group_id"),
|
|
14872
|
+
manifest_issuer_device_id: text("manifest_issuer_device_id"),
|
|
14873
|
+
manifest_hash: text("manifest_hash"),
|
|
14874
|
+
signed_manifest_json: text("signed_manifest_json"),
|
|
14875
|
+
updated_at: text("updated_at").notNull()
|
|
14876
|
+
}, (table) => [
|
|
14877
|
+
primaryKey({ columns: [table.scope_id, table.device_id] }),
|
|
14878
|
+
index("idx_scope_memberships_device_status").on(table.device_id, table.status),
|
|
14879
|
+
index("idx_scope_memberships_scope_status").on(table.scope_id, table.status),
|
|
14880
|
+
index("idx_scope_memberships_authority_group").on(table.coordinator_id, table.group_id)
|
|
14881
|
+
]);
|
|
14882
|
+
sqliteTable("artifacts", {
|
|
14883
|
+
id: integer("id").primaryKey(),
|
|
14884
|
+
session_id: integer("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
14885
|
+
kind: text("kind").notNull(),
|
|
14886
|
+
path: text("path"),
|
|
14887
|
+
content_text: text("content_text"),
|
|
14888
|
+
content_hash: text("content_hash"),
|
|
14889
|
+
created_at: text("created_at").notNull(),
|
|
14890
|
+
metadata_json: text("metadata_json")
|
|
14891
|
+
}, (table) => [index("idx_artifacts_session_kind").on(table.session_id, table.kind)]);
|
|
14892
|
+
var memoryItems = sqliteTable("memory_items", {
|
|
14893
|
+
id: integer("id").primaryKey(),
|
|
14894
|
+
session_id: integer("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
14895
|
+
kind: text("kind").notNull(),
|
|
14896
|
+
title: text("title").notNull(),
|
|
14897
|
+
subtitle: text("subtitle"),
|
|
14898
|
+
body_text: text("body_text").notNull(),
|
|
14899
|
+
confidence: real("confidence").default(.5),
|
|
14900
|
+
tags_text: text("tags_text").default(""),
|
|
14901
|
+
active: integer("active").default(1),
|
|
14902
|
+
created_at: text("created_at").notNull(),
|
|
14903
|
+
updated_at: text("updated_at").notNull(),
|
|
14904
|
+
metadata_json: text("metadata_json"),
|
|
14905
|
+
actor_id: text("actor_id"),
|
|
14906
|
+
actor_display_name: text("actor_display_name"),
|
|
14907
|
+
visibility: text("visibility"),
|
|
14908
|
+
workspace_id: text("workspace_id"),
|
|
14909
|
+
workspace_kind: text("workspace_kind"),
|
|
14910
|
+
origin_device_id: text("origin_device_id"),
|
|
14911
|
+
origin_source: text("origin_source"),
|
|
14912
|
+
trust_state: text("trust_state"),
|
|
14913
|
+
facts: text("facts"),
|
|
14914
|
+
narrative: text("narrative"),
|
|
14915
|
+
concepts: text("concepts"),
|
|
14916
|
+
files_read: text("files_read"),
|
|
14917
|
+
files_modified: text("files_modified"),
|
|
14918
|
+
user_prompt_id: integer("user_prompt_id"),
|
|
14919
|
+
prompt_number: integer("prompt_number"),
|
|
14920
|
+
deleted_at: text("deleted_at"),
|
|
14921
|
+
rev: integer("rev").default(0),
|
|
14922
|
+
dedup_key: text("dedup_key"),
|
|
14923
|
+
import_key: text("import_key"),
|
|
14924
|
+
scope_id: text("scope_id"),
|
|
14925
|
+
project: text("project")
|
|
14926
|
+
}, (table) => [
|
|
14927
|
+
index("idx_memory_items_active_created").on(table.active, table.created_at),
|
|
14928
|
+
index("idx_memory_items_created_id").on(table.created_at, table.id),
|
|
14929
|
+
index("idx_memory_items_origin_device_active").on(table.origin_device_id, table.active),
|
|
14930
|
+
index("idx_memory_items_session").on(table.session_id),
|
|
14931
|
+
index("idx_memory_items_project").on(table.project),
|
|
14932
|
+
index("idx_memory_items_scope_visibility_created").on(table.scope_id, table.visibility, table.created_at),
|
|
14933
|
+
index("idx_memory_items_scope_backfill_pending").on(table.id).where(sql`scope_id IS NULL OR scope_id = ''`),
|
|
14934
|
+
index("idx_memory_items_dedup_key_active_created").on(table.dedup_key, table.active, table.created_at),
|
|
14935
|
+
uniqueIndex("idx_memory_items_same_session_dedup_unique").on(table.session_id, table.kind, table.visibility, table.workspace_id, table.dedup_key).where(sql`active = 1 AND dedup_key IS NOT NULL`)
|
|
14936
|
+
]);
|
|
14937
|
+
sqliteTable("memory_file_refs", {
|
|
14938
|
+
memory_id: integer("memory_id").notNull().references(() => memoryItems.id, { onDelete: "cascade" }),
|
|
14939
|
+
file_path: text("file_path").notNull(),
|
|
14940
|
+
relation: text("relation").notNull()
|
|
14941
|
+
}, (table) => [primaryKey({ columns: [
|
|
14942
|
+
table.memory_id,
|
|
14943
|
+
table.file_path,
|
|
14944
|
+
table.relation
|
|
14945
|
+
] }), index("idx_memory_file_refs_path").on(table.file_path)]);
|
|
14946
|
+
sqliteTable("memory_concept_refs", {
|
|
14947
|
+
memory_id: integer("memory_id").notNull().references(() => memoryItems.id, { onDelete: "cascade" }),
|
|
14948
|
+
concept: text("concept").notNull()
|
|
14949
|
+
}, (table) => [primaryKey({ columns: [table.memory_id, table.concept] }), index("idx_memory_concept_refs_concept").on(table.concept)]);
|
|
14950
|
+
sqliteTable("usage_events", {
|
|
14951
|
+
id: integer("id").primaryKey(),
|
|
14952
|
+
session_id: integer("session_id").references(() => sessions.id, { onDelete: "set null" }),
|
|
14953
|
+
event: text("event").notNull(),
|
|
14954
|
+
tokens_read: integer("tokens_read").default(0),
|
|
14955
|
+
tokens_written: integer("tokens_written").default(0),
|
|
14956
|
+
tokens_saved: integer("tokens_saved").default(0),
|
|
14957
|
+
created_at: text("created_at").notNull(),
|
|
14958
|
+
metadata_json: text("metadata_json")
|
|
14959
|
+
}, (table) => [index("idx_usage_events_event_created").on(table.event, table.created_at), index("idx_usage_events_session").on(table.session_id)]);
|
|
14960
|
+
var retrievalAttempts = sqliteTable("retrieval_attempts", {
|
|
14961
|
+
attempt_id: text("attempt_id").primaryKey(),
|
|
14962
|
+
contract_version: integer("contract_version").notNull(),
|
|
14963
|
+
surface: text("surface").notNull(),
|
|
14964
|
+
trigger: text("trigger").notNull(),
|
|
14965
|
+
started_at: text("started_at").notNull(),
|
|
14966
|
+
completed_at: text("completed_at"),
|
|
14967
|
+
retrieval_status: text("retrieval_status").notNull(),
|
|
14968
|
+
delivery_status: text("delivery_status").notNull(),
|
|
14969
|
+
candidate_count: integer("candidate_count").notNull(),
|
|
14970
|
+
selected_count: integer("selected_count").notNull(),
|
|
14971
|
+
persisted_candidate_count: integer("persisted_candidate_count").notNull(),
|
|
14972
|
+
recorder_version: text("recorder_version").notNull(),
|
|
14973
|
+
session_id: integer("session_id").references(() => sessions.id, { onDelete: "cascade" }),
|
|
14974
|
+
source: text("source"),
|
|
14975
|
+
stream_id: text("stream_id"),
|
|
14976
|
+
source_session_id: text("source_session_id"),
|
|
14977
|
+
prompt_number: integer("prompt_number"),
|
|
14978
|
+
request_id: text("request_id"),
|
|
14979
|
+
raw_event_start_seq: integer("raw_event_start_seq"),
|
|
14980
|
+
raw_event_end_seq: integer("raw_event_end_seq"),
|
|
14981
|
+
experiment_id: text("experiment_id"),
|
|
14982
|
+
experiment_cell_id: text("experiment_cell_id"),
|
|
14983
|
+
evaluation_checkout_id: text("evaluation_checkout_id"),
|
|
14984
|
+
evaluation_fixture_id: text("evaluation_fixture_id"),
|
|
14985
|
+
evaluation_seed: integer("evaluation_seed"),
|
|
14986
|
+
latency_ms: integer("latency_ms"),
|
|
14987
|
+
project: text("project"),
|
|
14988
|
+
scope_id: text("scope_id"),
|
|
14989
|
+
mode: text("mode"),
|
|
14990
|
+
limit_requested: integer("limit_requested"),
|
|
14991
|
+
token_budget: integer("token_budget"),
|
|
14992
|
+
output_tokens: integer("output_tokens"),
|
|
14993
|
+
working_set_file_count: integer("working_set_file_count"),
|
|
14994
|
+
working_set_files_json: text("working_set_files_json"),
|
|
14995
|
+
query_hash_sha256: text("query_hash_sha256"),
|
|
14996
|
+
query_char_count: integer("query_char_count"),
|
|
14997
|
+
query_token_estimate: integer("query_token_estimate"),
|
|
14998
|
+
filter_summary_json: text("filter_summary_json"),
|
|
14999
|
+
failure_code: text("failure_code"),
|
|
15000
|
+
failure_stage: text("failure_stage"),
|
|
15001
|
+
trace_version: integer("trace_version"),
|
|
15002
|
+
automatic_recall_json: text("automatic_recall_json"),
|
|
15003
|
+
automatic_recall_key: text("automatic_recall_key"),
|
|
15004
|
+
retention_until: text("retention_until"),
|
|
15005
|
+
retention_pinned: integer("retention_pinned").notNull().default(0),
|
|
15006
|
+
retention_finalized_at: text("retention_finalized_at")
|
|
15007
|
+
}, (table) => [
|
|
15008
|
+
index("idx_retrieval_attempts_session_started").on(table.session_id, table.started_at),
|
|
15009
|
+
uniqueIndex("idx_retrieval_attempts_automatic_recall_key").on(table.automatic_recall_key),
|
|
15010
|
+
index("idx_retrieval_attempts_source_stream_started").on(table.source, table.stream_id, table.started_at),
|
|
15011
|
+
index("idx_retrieval_attempts_retention").on(table.retention_pinned, table.retention_until),
|
|
15012
|
+
index("idx_retrieval_attempts_started").on(table.started_at, table.attempt_id),
|
|
15013
|
+
index("idx_retrieval_attempts_surface_started").on(table.surface, table.started_at),
|
|
15014
|
+
uniqueIndex("idx_retrieval_attempts_request_identity").on(table.source, table.surface, table.request_id).where(sql`request_id IS NOT NULL`),
|
|
15015
|
+
index("idx_retrieval_attempts_experiment_cell").on(table.experiment_id, table.experiment_cell_id)
|
|
15016
|
+
]);
|
|
15017
|
+
var retrievalExposures = sqliteTable("retrieval_exposures", {
|
|
15018
|
+
exposure_id: integer("exposure_id").primaryKey({ autoIncrement: true }),
|
|
15019
|
+
attempt_id: text("attempt_id").notNull().references(() => retrievalAttempts.attempt_id, { onDelete: "cascade" }),
|
|
15020
|
+
memory_id: integer("memory_id").references(() => memoryItems.id, { onDelete: "set null" }),
|
|
15021
|
+
memory_import_key: text("memory_import_key"),
|
|
15022
|
+
origin_device_id: text("origin_device_id"),
|
|
15023
|
+
rank: integer("rank").notNull(),
|
|
15024
|
+
disposition: text("disposition").notNull(),
|
|
15025
|
+
section: text("section"),
|
|
15026
|
+
handoff_status: text("handoff_status").notNull(),
|
|
15027
|
+
memory_rev: integer("memory_rev"),
|
|
15028
|
+
memory_updated_at: text("memory_updated_at"),
|
|
15029
|
+
memory_scope_id: text("memory_scope_id"),
|
|
15030
|
+
memory_kind: text("memory_kind"),
|
|
15031
|
+
memory_active: integer("memory_active"),
|
|
15032
|
+
memory_deleted_at: text("memory_deleted_at"),
|
|
15033
|
+
score_summary_json: text("score_summary_json"),
|
|
15034
|
+
reason_codes_json: text("reason_codes_json")
|
|
15035
|
+
}, (table) => [uniqueIndex("idx_retrieval_exposures_attempt_rank").on(table.attempt_id, table.rank), index("idx_retrieval_exposures_memory").on(table.memory_id)]);
|
|
15036
|
+
var outcomeEvidence = sqliteTable("outcome_evidence", {
|
|
15037
|
+
evidence_id: text("evidence_id").primaryKey(),
|
|
15038
|
+
contract_version: integer("contract_version").notNull(),
|
|
15039
|
+
dimension: text("dimension").notNull(),
|
|
15040
|
+
evidence_type: text("evidence_type").notNull(),
|
|
15041
|
+
source_class: text("source_class").notNull(),
|
|
15042
|
+
observed_at: text("observed_at").notNull(),
|
|
15043
|
+
producer: text("producer").notNull(),
|
|
15044
|
+
producer_version: text("producer_version").notNull(),
|
|
15045
|
+
status: text("status").notNull(),
|
|
15046
|
+
value_type: text("value_type"),
|
|
15047
|
+
value_integer: integer("value_integer"),
|
|
15048
|
+
value_real: real("value_real"),
|
|
15049
|
+
value_unit: text("value_unit"),
|
|
15050
|
+
session_id: integer("session_id").references(() => sessions.id, { onDelete: "cascade" }),
|
|
15051
|
+
source: text("source"),
|
|
15052
|
+
stream_id: text("stream_id"),
|
|
15053
|
+
source_session_id: text("source_session_id"),
|
|
15054
|
+
prompt_number: integer("prompt_number"),
|
|
15055
|
+
raw_event_start_seq: integer("raw_event_start_seq"),
|
|
15056
|
+
raw_event_end_seq: integer("raw_event_end_seq"),
|
|
15057
|
+
experiment_id: text("experiment_id"),
|
|
15058
|
+
experiment_cell_id: text("experiment_cell_id"),
|
|
15059
|
+
window_start_at: text("window_start_at"),
|
|
15060
|
+
window_end_at: text("window_end_at"),
|
|
15061
|
+
references_json: text("references_json"),
|
|
15062
|
+
retention_until: text("retention_until"),
|
|
15063
|
+
retention_pinned: integer("retention_pinned").notNull().default(0),
|
|
15064
|
+
retention_finalized_at: text("retention_finalized_at")
|
|
15065
|
+
}, (table) => [
|
|
15066
|
+
index("idx_outcome_evidence_observed_id").on(table.observed_at, table.evidence_id),
|
|
15067
|
+
index("idx_outcome_evidence_session_observed").on(table.session_id, table.observed_at),
|
|
15068
|
+
index("idx_outcome_evidence_source_stream_observed").on(table.source, table.stream_id, table.observed_at),
|
|
15069
|
+
index("idx_outcome_evidence_type_observed").on(table.evidence_type, table.observed_at),
|
|
15070
|
+
index("idx_outcome_evidence_retention").on(table.retention_pinned, table.retention_until)
|
|
15071
|
+
]);
|
|
15072
|
+
var attributionAssessments = sqliteTable("attribution_assessments", {
|
|
15073
|
+
assessment_id: text("assessment_id").primaryKey(),
|
|
15074
|
+
contract_version: integer("contract_version").notNull(),
|
|
15075
|
+
subject_type: text("subject_type").notNull(),
|
|
15076
|
+
attempt_id: text("attempt_id").notNull().references(() => retrievalAttempts.attempt_id, { onDelete: "cascade" }),
|
|
15077
|
+
exposure_id: integer("exposure_id").references(() => retrievalExposures.exposure_id, { onDelete: "cascade" }),
|
|
15078
|
+
dimension: text("dimension").notNull(),
|
|
15079
|
+
impact_label: text("impact_label").notNull(),
|
|
15080
|
+
basis: text("basis").notNull(),
|
|
15081
|
+
confidence_level: text("confidence_level").notNull(),
|
|
15082
|
+
method: text("method").notNull(),
|
|
15083
|
+
method_version: text("method_version").notNull(),
|
|
15084
|
+
created_at: text("created_at").notNull(),
|
|
15085
|
+
claim_type: text("claim_type").notNull().default("observational")
|
|
15086
|
+
}, (table) => [
|
|
15087
|
+
index("idx_attribution_assessments_attempt_created").on(table.attempt_id, table.created_at),
|
|
15088
|
+
index("idx_attribution_assessments_label_created").on(table.impact_label, table.created_at),
|
|
15089
|
+
index("idx_attribution_assessments_exposure").on(table.exposure_id)
|
|
15090
|
+
]);
|
|
15091
|
+
sqliteTable("attribution_assessment_evidence", {
|
|
15092
|
+
assessment_id: text("assessment_id").notNull().references(() => attributionAssessments.assessment_id, { onDelete: "cascade" }),
|
|
15093
|
+
evidence_id: text("evidence_id").notNull().references(() => outcomeEvidence.evidence_id, { onDelete: "cascade" })
|
|
15094
|
+
}, (table) => [primaryKey({ columns: [table.assessment_id, table.evidence_id] }), index("idx_attribution_assessment_evidence_evidence").on(table.evidence_id, table.assessment_id)]);
|
|
15095
|
+
sqliteTable("maintenance_jobs", {
|
|
15096
|
+
kind: text("kind").primaryKey(),
|
|
15097
|
+
title: text("title").notNull(),
|
|
15098
|
+
status: text("status").notNull(),
|
|
15099
|
+
message: text("message"),
|
|
15100
|
+
progress_current: integer("progress_current").notNull().default(0),
|
|
15101
|
+
progress_total: integer("progress_total"),
|
|
15102
|
+
progress_unit: text("progress_unit").notNull().default("items"),
|
|
15103
|
+
metadata_json: text("metadata_json"),
|
|
15104
|
+
started_at: text("started_at"),
|
|
15105
|
+
updated_at: text("updated_at").notNull(),
|
|
15106
|
+
finished_at: text("finished_at"),
|
|
15107
|
+
error: text("error")
|
|
15108
|
+
}, (table) => [index("idx_maintenance_jobs_status_updated").on(table.status, table.updated_at)]);
|
|
15109
|
+
sqliteTable("raw_events", {
|
|
15110
|
+
id: integer("id").primaryKey(),
|
|
15111
|
+
source: text("source").notNull().default("opencode"),
|
|
15112
|
+
stream_id: text("stream_id").notNull().default(""),
|
|
15113
|
+
opencode_session_id: text("opencode_session_id").notNull(),
|
|
15114
|
+
event_id: text("event_id"),
|
|
15115
|
+
event_seq: integer("event_seq").notNull(),
|
|
15116
|
+
event_type: text("event_type").notNull(),
|
|
15117
|
+
ts_wall_ms: integer("ts_wall_ms"),
|
|
15118
|
+
ts_mono_ms: real("ts_mono_ms"),
|
|
15119
|
+
payload_json: text("payload_json").notNull(),
|
|
15120
|
+
capture_context_json: text("capture_context_json"),
|
|
15121
|
+
created_at: text("created_at").notNull()
|
|
15122
|
+
}, (table) => [
|
|
15123
|
+
uniqueIndex("idx_raw_events_source_stream_seq").on(table.source, table.stream_id, table.event_seq),
|
|
15124
|
+
uniqueIndex("idx_raw_events_source_stream_event_id").on(table.source, table.stream_id, table.event_id),
|
|
15125
|
+
index("idx_raw_events_session_seq").on(table.opencode_session_id, table.event_seq),
|
|
15126
|
+
index("idx_raw_events_created").on(table.created_at),
|
|
15127
|
+
index("idx_raw_events_capture_context").on(table.source, table.stream_id, table.event_seq).where(sql`${table.capture_context_json} IS NOT NULL`)
|
|
15128
|
+
]);
|
|
15129
|
+
sqliteTable("raw_event_sessions", {
|
|
15130
|
+
source: text("source").notNull().default("opencode"),
|
|
15131
|
+
stream_id: text("stream_id").notNull().default(""),
|
|
15132
|
+
opencode_session_id: text("opencode_session_id").notNull(),
|
|
15133
|
+
cwd: text("cwd"),
|
|
15134
|
+
project: text("project"),
|
|
15135
|
+
started_at: text("started_at"),
|
|
15136
|
+
last_seen_ts_wall_ms: integer("last_seen_ts_wall_ms"),
|
|
15137
|
+
last_received_event_seq: integer("last_received_event_seq").notNull().default(-1),
|
|
15138
|
+
last_flushed_event_seq: integer("last_flushed_event_seq").notNull().default(-1),
|
|
15139
|
+
updated_at: text("updated_at").notNull()
|
|
15140
|
+
}, (table) => [primaryKey({ columns: [table.source, table.stream_id] }), index("idx_raw_event_sessions_pending_updated").on(table.updated_at).where(sql`${table.last_received_event_seq} > ${table.last_flushed_event_seq}`)]);
|
|
15141
|
+
sqliteTable("pi_import_state", {
|
|
15142
|
+
file_path: text("file_path").primaryKey(),
|
|
15143
|
+
size_bytes: integer("size_bytes").notNull(),
|
|
15144
|
+
mtime_ms: real("mtime_ms").notNull(),
|
|
15145
|
+
updated_at: text("updated_at").notNull()
|
|
15146
|
+
});
|
|
15147
|
+
sqliteTable("opencode_sessions", {
|
|
15148
|
+
source: text("source").notNull().default("opencode"),
|
|
15149
|
+
stream_id: text("stream_id").notNull().default(""),
|
|
15150
|
+
opencode_session_id: text("opencode_session_id").notNull(),
|
|
15151
|
+
session_id: integer("session_id").references(() => sessions.id, { onDelete: "cascade" }),
|
|
15152
|
+
created_at: text("created_at").notNull()
|
|
15153
|
+
}, (table) => [primaryKey({ columns: [table.source, table.stream_id] }), index("idx_opencode_sessions_session").on(table.session_id)]);
|
|
15154
|
+
sqliteTable("raw_event_flush_batches", {
|
|
15155
|
+
id: integer("id").primaryKey(),
|
|
15156
|
+
source: text("source").notNull().default("opencode"),
|
|
15157
|
+
stream_id: text("stream_id").notNull().default(""),
|
|
15158
|
+
opencode_session_id: text("opencode_session_id").notNull(),
|
|
15159
|
+
start_event_seq: integer("start_event_seq").notNull(),
|
|
15160
|
+
end_event_seq: integer("end_event_seq").notNull(),
|
|
15161
|
+
extractor_version: text("extractor_version").notNull(),
|
|
15162
|
+
status: text("status").notNull(),
|
|
15163
|
+
error_message: text("error_message"),
|
|
15164
|
+
error_type: text("error_type"),
|
|
15165
|
+
observer_provider: text("observer_provider"),
|
|
15166
|
+
observer_model: text("observer_model"),
|
|
15167
|
+
observer_runtime: text("observer_runtime"),
|
|
15168
|
+
observer_auth_source: text("observer_auth_source"),
|
|
15169
|
+
observer_auth_type: text("observer_auth_type"),
|
|
15170
|
+
observer_error_code: text("observer_error_code"),
|
|
15171
|
+
observer_error_message: text("observer_error_message"),
|
|
15172
|
+
attempt_count: integer("attempt_count").notNull().default(0),
|
|
15173
|
+
created_at: text("created_at").notNull(),
|
|
15174
|
+
updated_at: text("updated_at").notNull()
|
|
15175
|
+
}, (table) => [
|
|
15176
|
+
uniqueIndex("idx_flush_batches_source_stream_seq_ver").on(table.source, table.stream_id, table.start_event_seq, table.end_event_seq, table.extractor_version),
|
|
15177
|
+
index("idx_flush_batches_session_created").on(table.opencode_session_id, table.created_at),
|
|
15178
|
+
index("idx_flush_batches_status_updated").on(table.status, table.updated_at),
|
|
15179
|
+
index("idx_flush_batches_observer_status_updated").on(table.status, table.updated_at).where(sql`${table.observer_provider} IS NOT NULL`),
|
|
15180
|
+
index("idx_flush_batches_capture_status_updated").on(table.status, table.updated_at).where(sql`${table.observer_provider} IS NULL`)
|
|
15181
|
+
]);
|
|
15182
|
+
sqliteTable("user_prompts", {
|
|
15183
|
+
id: integer("id").primaryKey(),
|
|
15184
|
+
session_id: integer("session_id").references(() => sessions.id, { onDelete: "cascade" }),
|
|
15185
|
+
project: text("project"),
|
|
15186
|
+
prompt_text: text("prompt_text").notNull(),
|
|
15187
|
+
prompt_number: integer("prompt_number"),
|
|
15188
|
+
created_at: text("created_at").notNull(),
|
|
15189
|
+
created_at_epoch: integer("created_at_epoch").notNull(),
|
|
15190
|
+
metadata_json: text("metadata_json"),
|
|
15191
|
+
import_key: text("import_key")
|
|
15192
|
+
}, (table) => [
|
|
15193
|
+
index("idx_user_prompts_session").on(table.session_id),
|
|
15194
|
+
index("idx_user_prompts_project").on(table.project),
|
|
15195
|
+
index("idx_user_prompts_epoch").on(table.created_at_epoch)
|
|
15196
|
+
]);
|
|
15197
|
+
sqliteTable("session_summaries", {
|
|
15198
|
+
id: integer("id").primaryKey(),
|
|
15199
|
+
session_id: integer("session_id").references(() => sessions.id, { onDelete: "cascade" }),
|
|
15200
|
+
project: text("project"),
|
|
15201
|
+
request: text("request"),
|
|
15202
|
+
investigated: text("investigated"),
|
|
15203
|
+
learned: text("learned"),
|
|
15204
|
+
completed: text("completed"),
|
|
15205
|
+
next_steps: text("next_steps"),
|
|
15206
|
+
notes: text("notes"),
|
|
15207
|
+
files_read: text("files_read"),
|
|
15208
|
+
files_edited: text("files_edited"),
|
|
15209
|
+
prompt_number: integer("prompt_number"),
|
|
15210
|
+
created_at: text("created_at").notNull(),
|
|
15211
|
+
created_at_epoch: integer("created_at_epoch").notNull(),
|
|
15212
|
+
metadata_json: text("metadata_json"),
|
|
15213
|
+
import_key: text("import_key")
|
|
15214
|
+
}, (table) => [
|
|
15215
|
+
index("idx_session_summaries_session").on(table.session_id),
|
|
15216
|
+
index("idx_session_summaries_project").on(table.project),
|
|
15217
|
+
index("idx_session_summaries_epoch").on(table.created_at_epoch)
|
|
15218
|
+
]);
|
|
15219
|
+
sqliteTable("replication_ops", {
|
|
15220
|
+
op_id: text("op_id").primaryKey(),
|
|
15221
|
+
entity_type: text("entity_type").notNull(),
|
|
15222
|
+
entity_id: text("entity_id").notNull(),
|
|
15223
|
+
op_type: text("op_type").notNull(),
|
|
15224
|
+
payload_json: text("payload_json"),
|
|
15225
|
+
clock_rev: integer("clock_rev").notNull(),
|
|
15226
|
+
clock_updated_at: text("clock_updated_at").notNull(),
|
|
15227
|
+
clock_device_id: text("clock_device_id").notNull(),
|
|
15228
|
+
device_id: text("device_id").notNull(),
|
|
15229
|
+
created_at: text("created_at").notNull(),
|
|
15230
|
+
scope_id: text("scope_id")
|
|
15231
|
+
}, (table) => [
|
|
15232
|
+
index("idx_replication_ops_created").on(table.created_at, table.op_id),
|
|
15233
|
+
index("idx_replication_ops_entity").on(table.entity_type, table.entity_id),
|
|
15234
|
+
index("idx_replication_ops_scope_created").on(table.scope_id, table.created_at, table.op_id)
|
|
15235
|
+
]);
|
|
15236
|
+
sqliteTable("replication_cursors", {
|
|
15237
|
+
peer_device_id: text("peer_device_id").primaryKey(),
|
|
15238
|
+
last_applied_cursor: text("last_applied_cursor"),
|
|
15239
|
+
last_acked_cursor: text("last_acked_cursor"),
|
|
15240
|
+
updated_at: text("updated_at").notNull()
|
|
15241
|
+
});
|
|
15242
|
+
sqliteTable("replication_cursors_v2", {
|
|
15243
|
+
peer_device_id: text("peer_device_id").notNull(),
|
|
15244
|
+
scope_id: text("scope_id").notNull(),
|
|
15245
|
+
last_applied_cursor: text("last_applied_cursor"),
|
|
15246
|
+
last_acked_cursor: text("last_acked_cursor"),
|
|
15247
|
+
updated_at: text("updated_at").notNull()
|
|
15248
|
+
}, (table) => [primaryKey({ columns: [table.peer_device_id, table.scope_id] }), index("idx_replication_cursors_v2_scope").on(table.scope_id)]);
|
|
15249
|
+
sqliteTable("sync_peers", {
|
|
15250
|
+
peer_device_id: text("peer_device_id").primaryKey(),
|
|
15251
|
+
name: text("name"),
|
|
15252
|
+
pinned_fingerprint: text("pinned_fingerprint"),
|
|
15253
|
+
public_key: text("public_key"),
|
|
15254
|
+
addresses_json: text("addresses_json"),
|
|
15255
|
+
manual_addresses_json: text("manual_addresses_json"),
|
|
15256
|
+
last_success_address: text("last_success_address"),
|
|
15257
|
+
claimed_local_actor: integer("claimed_local_actor").notNull().default(0),
|
|
15258
|
+
actor_id: text("actor_id"),
|
|
15259
|
+
projects_include_json: text("projects_include_json"),
|
|
15260
|
+
projects_exclude_json: text("projects_exclude_json"),
|
|
15261
|
+
created_at: text("created_at").notNull(),
|
|
15262
|
+
last_seen_at: text("last_seen_at"),
|
|
15263
|
+
last_sync_at: text("last_sync_at"),
|
|
15264
|
+
last_error: text("last_error"),
|
|
15265
|
+
runtime_version: text("runtime_version"),
|
|
15266
|
+
runtime_version_observed_at: text("runtime_version_observed_at"),
|
|
15267
|
+
highest_observed_direct_signature_version: integer("highest_observed_direct_signature_version"),
|
|
15268
|
+
discovered_via_coordinator_id: text("discovered_via_coordinator_id"),
|
|
15269
|
+
discovered_via_group_id: text("discovered_via_group_id"),
|
|
15270
|
+
trust_provenance: text("trust_provenance"),
|
|
15271
|
+
pending_bootstrap_grant_id: text("pending_bootstrap_grant_id")
|
|
15272
|
+
});
|
|
15273
|
+
sqliteTable("sync_peer_signature_state", {
|
|
15274
|
+
peer_device_id: text("peer_device_id").primaryKey(),
|
|
15275
|
+
highest_observed_direct_signature_version: integer("highest_observed_direct_signature_version").notNull()
|
|
15276
|
+
});
|
|
15277
|
+
sqliteTable("sync_nonces", {
|
|
15278
|
+
nonce: text("nonce").primaryKey(),
|
|
15279
|
+
device_id: text("device_id").notNull(),
|
|
15280
|
+
created_at: text("created_at").notNull()
|
|
15281
|
+
});
|
|
15282
|
+
sqliteTable("sync_device", {
|
|
15283
|
+
device_id: text("device_id").primaryKey(),
|
|
15284
|
+
public_key: text("public_key").notNull(),
|
|
15285
|
+
fingerprint: text("fingerprint").notNull(),
|
|
15286
|
+
created_at: text("created_at").notNull()
|
|
15287
|
+
});
|
|
15288
|
+
sqliteTable("sync_attempts", {
|
|
15289
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
15290
|
+
peer_device_id: text("peer_device_id").notNull(),
|
|
15291
|
+
started_at: text("started_at").notNull(),
|
|
15292
|
+
finished_at: text("finished_at"),
|
|
15293
|
+
ok: integer("ok").notNull(),
|
|
15294
|
+
ops_in: integer("ops_in").notNull(),
|
|
15295
|
+
ops_out: integer("ops_out").notNull(),
|
|
15296
|
+
error: text("error"),
|
|
15297
|
+
local_sync_capability: text("local_sync_capability"),
|
|
15298
|
+
peer_sync_capability: text("peer_sync_capability"),
|
|
15299
|
+
negotiated_sync_capability: text("negotiated_sync_capability"),
|
|
15300
|
+
failure_category: text("failure_category")
|
|
15301
|
+
}, (table) => [
|
|
15302
|
+
index("idx_sync_attempts_peer_started").on(table.peer_device_id, table.started_at),
|
|
15303
|
+
index("idx_sync_attempts_occurred").on(sql`CASE WHEN ${table.finished_at} IS NULL THEN ${table.started_at} ELSE ${table.finished_at} END`, table.id),
|
|
15304
|
+
index("idx_sync_attempts_error_occurred").on(sql`CASE WHEN ${table.finished_at} IS NULL THEN ${table.started_at} ELSE ${table.finished_at} END`, table.id).where(sql`${table.ok} = 0`),
|
|
15305
|
+
index("idx_sync_attempts_success_occurred").on(sql`CASE WHEN ${table.finished_at} IS NULL THEN ${table.started_at} ELSE ${table.finished_at} END`, table.id).where(sql`${table.ok} <> 0`)
|
|
15306
|
+
]);
|
|
15307
|
+
sqliteTable("sync_scope_rejections", {
|
|
15308
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
15309
|
+
peer_device_id: text("peer_device_id"),
|
|
15310
|
+
op_id: text("op_id").notNull(),
|
|
15311
|
+
entity_type: text("entity_type").notNull(),
|
|
15312
|
+
entity_id: text("entity_id").notNull(),
|
|
15313
|
+
scope_id: text("scope_id"),
|
|
15314
|
+
reason: text("reason").notNull(),
|
|
15315
|
+
created_at: text("created_at").notNull()
|
|
15316
|
+
}, (table) => [index("idx_sync_scope_rejections_peer_created").on(table.peer_device_id, table.created_at), index("idx_sync_scope_rejections_scope_created").on(table.scope_id, table.created_at)]);
|
|
15317
|
+
sqliteTable("sync_daemon_state", {
|
|
15318
|
+
id: integer("id").primaryKey(),
|
|
15319
|
+
last_error: text("last_error"),
|
|
15320
|
+
last_traceback: text("last_traceback"),
|
|
15321
|
+
last_error_at: text("last_error_at"),
|
|
15322
|
+
last_ok_at: text("last_ok_at"),
|
|
15323
|
+
phase: text("phase")
|
|
15324
|
+
});
|
|
15325
|
+
sqliteTable("sync_reset_state", {
|
|
15326
|
+
id: integer("id").primaryKey(),
|
|
15327
|
+
generation: integer("generation").notNull(),
|
|
15328
|
+
snapshot_id: text("snapshot_id").notNull(),
|
|
15329
|
+
baseline_cursor: text("baseline_cursor"),
|
|
15330
|
+
retained_floor_cursor: text("retained_floor_cursor"),
|
|
15331
|
+
updated_at: text("updated_at").notNull()
|
|
15332
|
+
});
|
|
15333
|
+
sqliteTable("sync_reset_state_v2", {
|
|
15334
|
+
scope_id: text("scope_id").primaryKey(),
|
|
15335
|
+
generation: integer("generation").notNull(),
|
|
15336
|
+
snapshot_id: text("snapshot_id").notNull(),
|
|
15337
|
+
baseline_cursor: text("baseline_cursor"),
|
|
15338
|
+
retained_floor_cursor: text("retained_floor_cursor"),
|
|
15339
|
+
updated_at: text("updated_at").notNull()
|
|
15340
|
+
});
|
|
15341
|
+
sqliteTable("sync_retention_state", {
|
|
15342
|
+
id: integer("id").primaryKey(),
|
|
15343
|
+
last_run_at: text("last_run_at"),
|
|
15344
|
+
last_duration_ms: integer("last_duration_ms"),
|
|
15345
|
+
last_deleted_ops: integer("last_deleted_ops").notNull().default(0),
|
|
15346
|
+
last_estimated_bytes_before: integer("last_estimated_bytes_before"),
|
|
15347
|
+
last_estimated_bytes_after: integer("last_estimated_bytes_after"),
|
|
15348
|
+
retained_floor_cursor: text("retained_floor_cursor"),
|
|
15349
|
+
last_error: text("last_error"),
|
|
15350
|
+
last_error_at: text("last_error_at")
|
|
15351
|
+
});
|
|
15352
|
+
sqliteTable("sync_retention_state_v2", {
|
|
15353
|
+
scope_id: text("scope_id").primaryKey(),
|
|
15354
|
+
last_run_at: text("last_run_at"),
|
|
15355
|
+
last_duration_ms: integer("last_duration_ms"),
|
|
15356
|
+
last_deleted_ops: integer("last_deleted_ops").notNull().default(0),
|
|
15357
|
+
last_estimated_bytes_before: integer("last_estimated_bytes_before"),
|
|
15358
|
+
last_estimated_bytes_after: integer("last_estimated_bytes_after"),
|
|
15359
|
+
retained_floor_cursor: text("retained_floor_cursor"),
|
|
15360
|
+
last_error: text("last_error"),
|
|
15361
|
+
last_error_at: text("last_error_at")
|
|
15362
|
+
});
|
|
15363
|
+
sqliteTable("raw_event_ingest_samples", {
|
|
15364
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
15365
|
+
created_at: text("created_at").notNull(),
|
|
15366
|
+
inserted_events: integer("inserted_events").notNull().default(0),
|
|
15367
|
+
skipped_invalid: integer("skipped_invalid").notNull().default(0),
|
|
15368
|
+
skipped_duplicate: integer("skipped_duplicate").notNull().default(0),
|
|
15369
|
+
skipped_conflict: integer("skipped_conflict").notNull().default(0)
|
|
15370
|
+
});
|
|
15371
|
+
sqliteTable("raw_event_ingest_stats", {
|
|
15372
|
+
id: integer("id").primaryKey(),
|
|
15373
|
+
inserted_events: integer("inserted_events").notNull().default(0),
|
|
15374
|
+
skipped_events: integer("skipped_events").notNull().default(0),
|
|
15375
|
+
skipped_invalid: integer("skipped_invalid").notNull().default(0),
|
|
15376
|
+
skipped_duplicate: integer("skipped_duplicate").notNull().default(0),
|
|
15377
|
+
skipped_conflict: integer("skipped_conflict").notNull().default(0),
|
|
15378
|
+
updated_at: text("updated_at").notNull()
|
|
15379
|
+
});
|
|
15380
|
+
sqliteTable("coordinator_group_preferences", {
|
|
15381
|
+
coordinator_id: text("coordinator_id").notNull(),
|
|
15382
|
+
group_id: text("group_id").notNull(),
|
|
15383
|
+
projects_include_json: text("projects_include_json"),
|
|
15384
|
+
projects_exclude_json: text("projects_exclude_json"),
|
|
15385
|
+
auto_seed_scope: integer("auto_seed_scope").notNull().default(1),
|
|
15386
|
+
default_space_scope_id: text("default_space_scope_id"),
|
|
15387
|
+
auto_grant_default_space_on_join: integer("auto_grant_default_space_on_join").notNull().default(0),
|
|
15388
|
+
updated_at: text("updated_at").notNull()
|
|
15389
|
+
}, (table) => ({ pk: primaryKey({ columns: [table.coordinator_id, table.group_id] }) }));
|
|
15390
|
+
sqliteTable("actors", {
|
|
15391
|
+
actor_id: text("actor_id").primaryKey(),
|
|
15392
|
+
display_name: text("display_name").notNull(),
|
|
15393
|
+
is_local: integer("is_local").notNull().default(0),
|
|
15394
|
+
status: text("status").notNull().default("active"),
|
|
15395
|
+
merged_into_actor_id: text("merged_into_actor_id"),
|
|
15396
|
+
created_at: text("created_at").notNull(),
|
|
15397
|
+
updated_at: text("updated_at").notNull()
|
|
15398
|
+
}, (table) => ({
|
|
15399
|
+
isLocalIdx: index("idx_actors_is_local").on(table.is_local),
|
|
15400
|
+
statusIdx: index("idx_actors_status").on(table.status)
|
|
15401
|
+
}));
|
|
15402
|
+
sqliteTable("recipient_policy_review_resolutions", {
|
|
15403
|
+
review_item_id: text("review_item_id").notNull(),
|
|
15404
|
+
source_fingerprint: text("source_fingerprint").notNull(),
|
|
15405
|
+
decision: text("decision").notNull(),
|
|
15406
|
+
decision_input_json: text("decision_input_json").notNull(),
|
|
15407
|
+
preview_json: text("preview_json").notNull(),
|
|
15408
|
+
decided_by_identity_id: text("decided_by_identity_id").notNull(),
|
|
15409
|
+
decided_by_device_id: text("decided_by_device_id").notNull(),
|
|
15410
|
+
resolved_at: text("resolved_at").notNull()
|
|
15411
|
+
}, (table) => [primaryKey({ columns: [table.review_item_id, table.source_fingerprint] })]);
|
|
15412
|
+
sqliteTable("coordinator_enrollment_reconciliation_issues", {
|
|
15413
|
+
coordinator_id: text("coordinator_id").notNull(),
|
|
15414
|
+
group_id: text("group_id").notNull(),
|
|
15415
|
+
kind: text("kind").notNull(),
|
|
15416
|
+
reference_id: text("reference_id").notNull(),
|
|
15417
|
+
code: text("code").notNull(),
|
|
15418
|
+
status: text("status").notNull().default("open"),
|
|
15419
|
+
first_seen_at: text("first_seen_at").notNull(),
|
|
15420
|
+
last_seen_at: text("last_seen_at").notNull(),
|
|
15421
|
+
resolved_at: text("resolved_at"),
|
|
15422
|
+
occurrence_count: integer("occurrence_count").notNull().default(1),
|
|
15423
|
+
updated_at: text("updated_at").notNull()
|
|
15424
|
+
}, (table) => [
|
|
15425
|
+
primaryKey({ columns: [
|
|
15426
|
+
table.coordinator_id,
|
|
15427
|
+
table.group_id,
|
|
15428
|
+
table.kind,
|
|
15429
|
+
table.reference_id,
|
|
15430
|
+
table.code
|
|
15431
|
+
] }),
|
|
15432
|
+
index("idx_coordinator_enrollment_issues_boundary_status").on(table.coordinator_id, table.group_id, table.status),
|
|
15433
|
+
index("idx_coordinator_enrollment_issues_status_recent").on(table.status, table.last_seen_at, table.resolved_at)
|
|
15434
|
+
]);
|
|
15435
|
+
var policyTeams = sqliteTable("policy_teams", {
|
|
15436
|
+
team_id: text("team_id").primaryKey(),
|
|
15437
|
+
display_name: text("display_name").notNull(),
|
|
15438
|
+
status: text("status").notNull(),
|
|
15439
|
+
device_eligibility_mode: text("device_eligibility_mode").notNull().default("person_all_devices"),
|
|
15440
|
+
provenance: text("provenance").notNull(),
|
|
15441
|
+
revision: text("revision").notNull(),
|
|
15442
|
+
migration_state: text("migration_state").notNull(),
|
|
15443
|
+
source_fingerprint: text("source_fingerprint"),
|
|
15444
|
+
idempotency_key: text("idempotency_key").notNull().unique(),
|
|
15445
|
+
created_at: text("created_at").notNull(),
|
|
15446
|
+
updated_at: text("updated_at").notNull()
|
|
15447
|
+
});
|
|
15448
|
+
sqliteTable("policy_team_memberships", {
|
|
15449
|
+
team_id: text("team_id").notNull(),
|
|
15450
|
+
identity_id: text("identity_id").notNull(),
|
|
15451
|
+
role: text("role").notNull(),
|
|
15452
|
+
status: text("status").notNull(),
|
|
15453
|
+
provenance: text("provenance").notNull(),
|
|
15454
|
+
revision: text("revision").notNull(),
|
|
15455
|
+
migration_state: text("migration_state").notNull(),
|
|
15456
|
+
source_fingerprint: text("source_fingerprint"),
|
|
15457
|
+
idempotency_key: text("idempotency_key").notNull().unique(),
|
|
15458
|
+
created_at: text("created_at").notNull(),
|
|
15459
|
+
updated_at: text("updated_at").notNull()
|
|
15460
|
+
}, (table) => ({
|
|
15461
|
+
pk: primaryKey({ columns: [table.team_id, table.identity_id] }),
|
|
15462
|
+
identityStatusIdx: index("idx_policy_team_memberships_identity_status").on(table.identity_id, table.status)
|
|
15463
|
+
}));
|
|
15464
|
+
sqliteTable("policy_team_device_decisions", {
|
|
15465
|
+
team_id: text("team_id").notNull().references(() => policyTeams.team_id, { onDelete: "cascade" }),
|
|
15466
|
+
device_id: text("device_id").notNull(),
|
|
15467
|
+
decision: text("decision").notNull(),
|
|
15468
|
+
assignment_version: integer("assignment_version").notNull().default(0),
|
|
15469
|
+
provenance: text("provenance").notNull(),
|
|
15470
|
+
revision: text("revision").notNull(),
|
|
15471
|
+
created_at: text("created_at").notNull(),
|
|
15472
|
+
updated_at: text("updated_at").notNull()
|
|
15473
|
+
}, (table) => [primaryKey({ columns: [table.team_id, table.device_id] }), index("idx_policy_team_device_decisions_device").on(table.device_id)]);
|
|
15474
|
+
sqliteTable("identity_devices", {
|
|
15475
|
+
device_id: text("device_id").primaryKey(),
|
|
15476
|
+
identity_id: text("identity_id").notNull(),
|
|
15477
|
+
display_name: text("display_name").notNull(),
|
|
15478
|
+
status: text("status").notNull(),
|
|
15479
|
+
provenance: text("provenance").notNull(),
|
|
15480
|
+
revision: text("revision").notNull(),
|
|
15481
|
+
migration_state: text("migration_state").notNull(),
|
|
15482
|
+
assignment_version: integer("assignment_version").notNull().default(0),
|
|
15483
|
+
source_fingerprint: text("source_fingerprint"),
|
|
15484
|
+
idempotency_key: text("idempotency_key").notNull().unique(),
|
|
15485
|
+
created_at: text("created_at").notNull(),
|
|
15486
|
+
updated_at: text("updated_at").notNull()
|
|
15487
|
+
}, (table) => ({ identityStatusIdx: index("idx_identity_devices_identity_status").on(table.identity_id, table.status) }));
|
|
15488
|
+
var deviceIdentityBindingCommits = sqliteTable("device_identity_binding_commits", {
|
|
15489
|
+
commit_digest: text("commit_digest").primaryKey(),
|
|
15490
|
+
reviewed_inventory_digest: text("reviewed_inventory_digest").notNull(),
|
|
15491
|
+
request_json: text("request_json").notNull(),
|
|
15492
|
+
outcomes_json: text("outcomes_json").notNull(),
|
|
15493
|
+
write_count: integer("write_count").notNull(),
|
|
15494
|
+
decided_by_identity_id: text("decided_by_identity_id").notNull(),
|
|
15495
|
+
decided_by_device_id: text("decided_by_device_id").notNull(),
|
|
15496
|
+
created_at: text("created_at").notNull()
|
|
15497
|
+
});
|
|
15498
|
+
sqliteTable("device_identity_binding_audit", {
|
|
15499
|
+
event_id: text("event_id").primaryKey(),
|
|
15500
|
+
commit_digest: text("commit_digest").notNull().references(() => deviceIdentityBindingCommits.commit_digest),
|
|
15501
|
+
device_id: text("device_id").notNull(),
|
|
15502
|
+
previous_identity_id: text("previous_identity_id"),
|
|
15503
|
+
target_identity_id: text("target_identity_id").notNull(),
|
|
15504
|
+
action: text("action").notNull(),
|
|
15505
|
+
previous_assignment_version: integer("previous_assignment_version"),
|
|
15506
|
+
resulting_assignment_version: integer("resulting_assignment_version").notNull(),
|
|
15507
|
+
decided_by_identity_id: text("decided_by_identity_id").notNull(),
|
|
15508
|
+
decided_by_device_id: text("decided_by_device_id").notNull(),
|
|
15509
|
+
created_at: text("created_at").notNull()
|
|
15510
|
+
}, (table) => [uniqueIndex("idx_device_identity_binding_audit_commit_device").on(table.commit_digest, table.device_id), index("idx_device_identity_binding_audit_device_created").on(table.device_id, table.created_at, table.event_id)]);
|
|
15511
|
+
sqliteTable("project_recipients", {
|
|
15512
|
+
canonical_project_identity: text("canonical_project_identity").notNull(),
|
|
15513
|
+
recipient_kind: text("recipient_kind").notNull(),
|
|
15514
|
+
recipient_id: text("recipient_id").notNull(),
|
|
15515
|
+
status: text("status").notNull(),
|
|
15516
|
+
provenance: text("provenance").notNull(),
|
|
15517
|
+
policy_revision: text("policy_revision").notNull(),
|
|
15518
|
+
migration_state: text("migration_state").notNull(),
|
|
15519
|
+
source_fingerprint: text("source_fingerprint"),
|
|
15520
|
+
idempotency_key: text("idempotency_key").notNull().unique(),
|
|
15521
|
+
created_at: text("created_at").notNull(),
|
|
15522
|
+
updated_at: text("updated_at").notNull()
|
|
15523
|
+
}, (table) => ({
|
|
15524
|
+
pk: primaryKey({ columns: [
|
|
15525
|
+
table.canonical_project_identity,
|
|
15526
|
+
table.recipient_kind,
|
|
15527
|
+
table.recipient_id
|
|
15528
|
+
] }),
|
|
15529
|
+
projectStatusIdx: index("idx_project_recipients_project_status").on(table.canonical_project_identity, table.status),
|
|
15530
|
+
recipientStatusIdx: index("idx_project_recipients_recipient_status").on(table.recipient_kind, table.recipient_id, table.status)
|
|
15531
|
+
}));
|
|
15532
|
+
var legacyTeamSetupDrafts = sqliteTable("legacy_team_setup_drafts", {
|
|
15533
|
+
attempt_id: text("attempt_id").primaryKey(),
|
|
15534
|
+
candidate_id: text("candidate_id").notNull(),
|
|
15535
|
+
coordinator_id: text("coordinator_id").notNull(),
|
|
15536
|
+
group_id: text("group_id").notNull(),
|
|
15537
|
+
state: text("state").notNull().default("needs_setup"),
|
|
15538
|
+
display_name: text("display_name").notNull(),
|
|
15539
|
+
roster_fingerprint: text("roster_fingerprint").notNull(),
|
|
15540
|
+
projection_fingerprint: text("projection_fingerprint").notNull(),
|
|
15541
|
+
finish_digest: text("finish_digest"),
|
|
15542
|
+
safe_error_code: text("safe_error_code"),
|
|
15543
|
+
completed_team_id: text("completed_team_id"),
|
|
15544
|
+
created_at: text("created_at").notNull(),
|
|
15545
|
+
updated_at: text("updated_at").notNull(),
|
|
15546
|
+
completed_at: text("completed_at"),
|
|
15547
|
+
superseded_at: text("superseded_at")
|
|
15548
|
+
}, (table) => [
|
|
15549
|
+
index("idx_legacy_team_setup_drafts_candidate_state").on(table.candidate_id, table.state, table.created_at),
|
|
15550
|
+
index("idx_legacy_team_setup_drafts_state_updated").on(table.state, table.updated_at),
|
|
15551
|
+
index("idx_legacy_team_setup_drafts_finish_digest").on(table.finish_digest)
|
|
15552
|
+
]);
|
|
15553
|
+
sqliteTable("legacy_team_setup_completions", {
|
|
15554
|
+
attempt_id: text("attempt_id").notNull(),
|
|
15555
|
+
finish_digest: text("finish_digest").notNull(),
|
|
15556
|
+
candidate_ref: text("candidate_ref").notNull(),
|
|
15557
|
+
confirmed_access_delta_digest: text("confirmed_access_delta_digest").notNull(),
|
|
15558
|
+
completed_team_id: text("completed_team_id").notNull(),
|
|
15559
|
+
response_json: text("response_json").notNull(),
|
|
15560
|
+
completed_at: text("completed_at").notNull(),
|
|
15561
|
+
created_at: text("created_at").notNull()
|
|
15562
|
+
}, (table) => [uniqueIndex("idx_legacy_team_setup_completions_attempt_finish").on(table.attempt_id, table.finish_digest), index("idx_legacy_team_setup_completions_exact_replay").on(table.candidate_ref, table.attempt_id, table.finish_digest, table.confirmed_access_delta_digest)]);
|
|
15563
|
+
sqliteTable("legacy_team_setup_draft_devices", {
|
|
15564
|
+
attempt_id: text("attempt_id").notNull().references(() => legacyTeamSetupDrafts.attempt_id, { onDelete: "cascade" }),
|
|
15565
|
+
device_id: text("device_id").notNull(),
|
|
15566
|
+
device_ref: text("device_ref").notNull(),
|
|
15567
|
+
key_fingerprint: text("key_fingerprint").notNull(),
|
|
15568
|
+
display_name: text("display_name").notNull(),
|
|
15569
|
+
enabled: integer("enabled", { mode: "boolean" }).notNull(),
|
|
15570
|
+
existing_identity_id: text("existing_identity_id"),
|
|
15571
|
+
existing_assignment_version: integer("existing_assignment_version"),
|
|
15572
|
+
verified_evidence_kind: text("verified_evidence_kind"),
|
|
15573
|
+
identity_evidence: text("identity_evidence"),
|
|
15574
|
+
decision: text("decision").notNull().default("unresolved"),
|
|
15575
|
+
target_identity_id: text("target_identity_id"),
|
|
15576
|
+
expected_assignment_kind: text("expected_assignment_kind"),
|
|
15577
|
+
expected_assignment_version: integer("expected_assignment_version"),
|
|
15578
|
+
updated_at: text("updated_at").notNull()
|
|
15579
|
+
}, (table) => [
|
|
15580
|
+
primaryKey({ columns: [table.attempt_id, table.device_id] }),
|
|
15581
|
+
uniqueIndex("idx_legacy_team_setup_devices_attempt_ref").on(table.attempt_id, table.device_ref),
|
|
15582
|
+
index("idx_legacy_team_setup_devices_attempt_decision").on(table.attempt_id, table.decision)
|
|
15583
|
+
]);
|
|
15584
|
+
sqliteTable("legacy_team_setup_draft_projects", {
|
|
15585
|
+
attempt_id: text("attempt_id").notNull().references(() => legacyTeamSetupDrafts.attempt_id, { onDelete: "cascade" }),
|
|
15586
|
+
project_ref: text("project_ref").notNull(),
|
|
15587
|
+
source_project_identity: text("source_project_identity").notNull(),
|
|
15588
|
+
display_name: text("display_name").notNull(),
|
|
15589
|
+
source_fingerprint: text("source_fingerprint").notNull(),
|
|
15590
|
+
resolution_kind: text("resolution_kind").notNull().default("unresolved"),
|
|
15591
|
+
resolved_project_identity: text("resolved_project_identity"),
|
|
15592
|
+
target_scope_id: text("target_scope_id"),
|
|
15593
|
+
updated_at: text("updated_at").notNull()
|
|
15594
|
+
}, (table) => [primaryKey({ columns: [table.attempt_id, table.project_ref] }), index("idx_legacy_team_setup_projects_attempt_resolution").on(table.attempt_id, table.resolution_kind)]);
|
|
15595
|
+
sqliteTable("recipient_managed_project_projections", {
|
|
15596
|
+
canonical_project_identity: text("canonical_project_identity").notNull(),
|
|
15597
|
+
display_name: text("display_name").notNull(),
|
|
15598
|
+
managed_scope_id: text("managed_scope_id").notNull(),
|
|
15599
|
+
coordinator_id: text("coordinator_id").notNull(),
|
|
15600
|
+
group_id: text("group_id").notNull(),
|
|
15601
|
+
recipient_identity_id: text("recipient_identity_id").notNull(),
|
|
15602
|
+
accepting_device_id: text("accepting_device_id").notNull(),
|
|
15603
|
+
source_operation_id: text("source_operation_id").notNull(),
|
|
15604
|
+
reviewed_project_set_digest: text("reviewed_project_set_digest").notNull(),
|
|
15605
|
+
status: text("status").notNull().default("active"),
|
|
15606
|
+
accepted_at: text("accepted_at").notNull(),
|
|
15607
|
+
revoked_at: text("revoked_at"),
|
|
15608
|
+
created_at: text("created_at").notNull(),
|
|
15609
|
+
updated_at: text("updated_at").notNull()
|
|
15610
|
+
}, (table) => [
|
|
15611
|
+
primaryKey({ columns: [table.source_operation_id, table.canonical_project_identity] }),
|
|
15612
|
+
index("idx_recipient_managed_projects_identity_status").on(table.recipient_identity_id, table.status),
|
|
15613
|
+
index("idx_recipient_managed_projects_scope_authority").on(table.managed_scope_id, table.coordinator_id, table.group_id, table.status)
|
|
15614
|
+
]);
|
|
15615
|
+
sqliteTable("recipient_policy_authority_states", {
|
|
15616
|
+
canonical_project_identity: text("canonical_project_identity").primaryKey(),
|
|
15617
|
+
authority_state: text("authority_state").notNull().default("legacy"),
|
|
15618
|
+
generation: integer("generation").notNull().default(0),
|
|
15619
|
+
desired_devices_digest: text("desired_devices_digest"),
|
|
15620
|
+
current_devices_digest: text("current_devices_digest"),
|
|
15621
|
+
stable_parity_evidence_digest: text("stable_parity_evidence_digest"),
|
|
15622
|
+
stable_parity_passed_at: text("stable_parity_passed_at"),
|
|
15623
|
+
fresh_snapshot_fingerprint: text("fresh_snapshot_fingerprint"),
|
|
15624
|
+
fresh_snapshot_observed_at: text("fresh_snapshot_observed_at"),
|
|
15625
|
+
safe_error_code: text("safe_error_code"),
|
|
15626
|
+
state_changed_at: text("state_changed_at").notNull(),
|
|
15627
|
+
last_error_at: text("last_error_at"),
|
|
15628
|
+
attempt_count: integer("attempt_count").notNull().default(0),
|
|
15629
|
+
last_attempt_at: text("last_attempt_at"),
|
|
15630
|
+
wake_epoch: integer("wake_epoch").notNull().default(0),
|
|
15631
|
+
last_completed_at: text("last_completed_at"),
|
|
15632
|
+
lease_owner: text("lease_owner"),
|
|
15633
|
+
lease_acquired_at: text("lease_acquired_at"),
|
|
15634
|
+
lease_expires_at: text("lease_expires_at"),
|
|
15635
|
+
created_at: text("created_at").notNull(),
|
|
15636
|
+
updated_at: text("updated_at").notNull()
|
|
15637
|
+
});
|
|
15638
|
+
sqliteTable("recipient_policy_reconciliation_steps", {
|
|
15639
|
+
canonical_project_identity: text("canonical_project_identity").notNull(),
|
|
15640
|
+
generation: integer("generation").notNull(),
|
|
15641
|
+
step_key: text("step_key").notNull(),
|
|
15642
|
+
effect_id: text("effect_id").notNull(),
|
|
15643
|
+
payload_digest: text("payload_digest").notNull(),
|
|
15644
|
+
status: text("status").notNull().default("pending"),
|
|
15645
|
+
attempt_count: integer("attempt_count").notNull().default(0),
|
|
15646
|
+
started_at: text("started_at"),
|
|
15647
|
+
completed_at: text("completed_at"),
|
|
15648
|
+
last_attempt_at: text("last_attempt_at"),
|
|
15649
|
+
safe_error_code: text("safe_error_code"),
|
|
15650
|
+
error_at: text("error_at"),
|
|
15651
|
+
lease_owner: text("lease_owner"),
|
|
15652
|
+
lease_acquired_at: text("lease_acquired_at"),
|
|
15653
|
+
lease_expires_at: text("lease_expires_at"),
|
|
15654
|
+
created_at: text("created_at").notNull(),
|
|
15655
|
+
updated_at: text("updated_at").notNull()
|
|
15656
|
+
}, (table) => [
|
|
15657
|
+
primaryKey({ columns: [
|
|
15658
|
+
table.canonical_project_identity,
|
|
15659
|
+
table.generation,
|
|
15660
|
+
table.step_key
|
|
15661
|
+
] }),
|
|
15662
|
+
uniqueIndex("idx_recipient_policy_reconciliation_steps_effect").on(table.effect_id),
|
|
15663
|
+
index("idx_recipient_policy_reconciliation_steps_status").on(table.canonical_project_identity, table.status),
|
|
15664
|
+
index("idx_recipient_policy_reconciliation_steps_pending_refresh").on(table.canonical_project_identity, table.generation, table.step_key).where(sql`${table.status} IN ('pending', 'running', 'failed') AND ${table.step_key} GLOB 'refresh-after-revocations-v2:*'`)
|
|
15665
|
+
]);
|
|
15666
|
+
sqliteTable("recipient_policy_deny_overlays", {
|
|
15667
|
+
canonical_project_identity: text("canonical_project_identity").notNull(),
|
|
15668
|
+
scope_id: text("scope_id").notNull(),
|
|
15669
|
+
device_id: text("device_id").notNull(),
|
|
15670
|
+
generation: integer("generation").notNull(),
|
|
15671
|
+
reason_code: text("reason_code").notNull(),
|
|
15672
|
+
created_at: text("created_at").notNull(),
|
|
15673
|
+
updated_at: text("updated_at").notNull()
|
|
15674
|
+
}, (table) => [primaryKey({ columns: [
|
|
15675
|
+
table.canonical_project_identity,
|
|
15676
|
+
table.scope_id,
|
|
15677
|
+
table.device_id
|
|
15678
|
+
] }), index("idx_recipient_policy_deny_overlays_scope_device").on(table.scope_id, table.device_id)]);
|
|
15679
|
+
//#endregion
|
|
15680
|
+
//#region ../core/src/coordinator-owner-profile-files.ts
|
|
15681
|
+
function ownerDatabaseFile(path) {
|
|
15682
|
+
const node = lstatSync(path, { bigint: true });
|
|
15683
|
+
if (!node.isFile() || node.isSymbolicLink()) return ownerProfileConflict();
|
|
15684
|
+
return `${node.dev}:${node.ino}`;
|
|
15685
|
+
}
|
|
15686
|
+
function ownerObject(value) {
|
|
15687
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return ownerProfileConflict();
|
|
15688
|
+
return value;
|
|
15689
|
+
}
|
|
15690
|
+
function assertFile(path, options = {}) {
|
|
15691
|
+
const stat = lstatSync(path);
|
|
15692
|
+
if (!stat.isFile() || stat.isSymbolicLink() || options.private && (stat.mode & 511) !== 384 || stat.size > 1048576) ownerProfileConflict();
|
|
15693
|
+
}
|
|
15694
|
+
function ownerFileSnapshot(path) {
|
|
15695
|
+
assertFile(path);
|
|
15696
|
+
const node = lstatSync(path, { bigint: true });
|
|
15697
|
+
return {
|
|
15698
|
+
text: readFileSync(path, "utf8"),
|
|
15699
|
+
dev: String(node.dev),
|
|
15700
|
+
ino: String(node.ino),
|
|
15701
|
+
ctime: String(node.ctimeNs)
|
|
15702
|
+
};
|
|
15703
|
+
}
|
|
15704
|
+
function readOwnerState(path) {
|
|
15705
|
+
assertFile(path, { private: true });
|
|
15706
|
+
return ownerObject(JSON.parse(readFileSync(path, "utf8")));
|
|
15707
|
+
}
|
|
15708
|
+
function syncDirectory(path) {
|
|
15709
|
+
const fd = openSync(path, "r");
|
|
15710
|
+
try {
|
|
15711
|
+
fsyncSync(fd);
|
|
15712
|
+
} finally {
|
|
15713
|
+
closeSync(fd);
|
|
15714
|
+
}
|
|
15715
|
+
}
|
|
15716
|
+
/** Sync an existing POSIX directory without changing its permissions or contents. */
|
|
15717
|
+
function syncOwnerProfileDirectory(path) {
|
|
15718
|
+
syncDirectory(path);
|
|
15719
|
+
}
|
|
15720
|
+
/** Durable, create-only publication; never overwrite a retained final request. */
|
|
15721
|
+
function retainOwnerState(path, value) {
|
|
15722
|
+
const text = JSON.stringify(value);
|
|
15723
|
+
if (text === void 0) ownerProfileConflict();
|
|
15724
|
+
const staged = stageOwnerState(path, text);
|
|
15725
|
+
linkSync(staged.path, path);
|
|
15726
|
+
syncDirectory(dirname(path));
|
|
15727
|
+
assertPublishedConfigLock(path, staged.snapshot);
|
|
15728
|
+
assertPublishedConfigLock(staged.path, staged.snapshot);
|
|
15729
|
+
unlinkSync(staged.path);
|
|
15730
|
+
syncDirectory(dirname(path));
|
|
15731
|
+
}
|
|
15732
|
+
function stageOwnerState(path, text) {
|
|
15733
|
+
const temporary = `${path}.tmp-${randomUUID()}`;
|
|
15734
|
+
const fd = openSync(temporary, "wx", 384);
|
|
15735
|
+
let identity;
|
|
15736
|
+
let closeAttempted = false;
|
|
15737
|
+
try {
|
|
15738
|
+
const node = fstatSync(fd, { bigint: true });
|
|
15739
|
+
identity = {
|
|
15740
|
+
dev: String(node.dev),
|
|
15741
|
+
ino: String(node.ino)
|
|
15742
|
+
};
|
|
15743
|
+
writeFileSync(fd, text);
|
|
15744
|
+
fsyncSync(fd);
|
|
15745
|
+
const snapshot = ownerFileSnapshot(temporary);
|
|
15746
|
+
if (snapshot.dev !== identity.dev || snapshot.ino !== identity.ino || snapshot.text !== text) ownerProfileConflict();
|
|
15747
|
+
syncDirectory(dirname(path));
|
|
15748
|
+
closeAttempted = true;
|
|
15749
|
+
closeSync(fd);
|
|
15750
|
+
return {
|
|
15751
|
+
path: temporary,
|
|
15752
|
+
snapshot
|
|
15753
|
+
};
|
|
15754
|
+
} catch (error) {
|
|
15755
|
+
if (identity) cleanupUnpublishedConfigLock(temporary, identity);
|
|
15756
|
+
if (!closeAttempted) closeSync(fd);
|
|
15757
|
+
throw error;
|
|
15758
|
+
}
|
|
15759
|
+
}
|
|
15760
|
+
function cleanupUnpublishedConfigLock(path, identity) {
|
|
15761
|
+
try {
|
|
15762
|
+
const node = lstatSync(path, { bigint: true });
|
|
15763
|
+
if (!node.isFile() || node.isSymbolicLink() || node.nlink !== 1n || String(node.dev) !== identity.dev || String(node.ino) !== identity.ino) return;
|
|
15764
|
+
unlinkSync(path);
|
|
15765
|
+
} catch {}
|
|
15766
|
+
}
|
|
15767
|
+
function assertPublishedConfigLock(path, staged) {
|
|
15768
|
+
const current = ownerFileSnapshot(path);
|
|
15769
|
+
if (current.dev !== staged.dev || current.ino !== staged.ino || current.text !== staged.text) ownerProfileConflict();
|
|
15770
|
+
return current;
|
|
15771
|
+
}
|
|
15772
|
+
//#endregion
|
|
15773
|
+
//#region ../core/src/account-owner-association-directory.ts
|
|
15774
|
+
function accountOwnerAssociationDirectory() {
|
|
15775
|
+
return join(homedir(), ".codemem", "account-owner-associations");
|
|
15776
|
+
}
|
|
15777
|
+
function checkDirectory(path, options = {}) {
|
|
15778
|
+
try {
|
|
15779
|
+
const stat = lstatSync(path);
|
|
15780
|
+
const unsafeMode = options.private ? (stat.mode & 511) !== 448 : (stat.mode & 18) !== 0;
|
|
15781
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || unsafeMode || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("account_owner_association_invalid");
|
|
15782
|
+
return true;
|
|
15783
|
+
} catch (error) {
|
|
15784
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
|
|
15785
|
+
throw error;
|
|
15786
|
+
}
|
|
15787
|
+
}
|
|
15788
|
+
/** Missing directories stay missing during reads. Never follow a state-directory symlink. */
|
|
15789
|
+
function readAccountOwnerAssociationDirectory(path) {
|
|
15790
|
+
return checkDirectory(dirname(path)) && checkDirectory(path, { private: true });
|
|
15791
|
+
}
|
|
15792
|
+
function createDirectory(path) {
|
|
15793
|
+
try {
|
|
15794
|
+
mkdirSync(path, { mode: 448 });
|
|
15795
|
+
} catch (error) {
|
|
15796
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") throw error;
|
|
15797
|
+
}
|
|
15798
|
+
}
|
|
15799
|
+
/** Sync ancestors on every retry: an earlier mkdir may have become visible without its fsync. */
|
|
15800
|
+
function retainAccountOwnerAssociationDirectory(path) {
|
|
15801
|
+
const parent = dirname(path);
|
|
15802
|
+
createDirectory(parent);
|
|
15803
|
+
if (!checkDirectory(parent)) throw new Error("account_owner_association_invalid");
|
|
15804
|
+
syncOwnerProfileDirectory(dirname(parent));
|
|
15805
|
+
createDirectory(path);
|
|
15806
|
+
if (!checkDirectory(path, { private: true })) throw new Error("account_owner_association_invalid");
|
|
15807
|
+
syncOwnerProfileDirectory(parent);
|
|
15808
|
+
}
|
|
15809
|
+
//#endregion
|
|
15810
|
+
//#region ../core/src/coordinator-auth-browser-style.ts
|
|
15811
|
+
/** Standalone auth pages mirror the Viewer's tokens without loading remote assets.
|
|
15812
|
+
* Keep these palette values aligned with ui/static/{tokens,themes}.css.
|
|
15813
|
+
* Separate origins cannot read the Viewer's saved theme; use the OS preference.
|
|
15814
|
+
*/
|
|
15815
|
+
var COORDINATOR_AUTH_STYLE = `
|
|
15816
|
+
:root{color-scheme:dark;--surface-0:#0b1016;--surface-1:#0f1720;--surface-2:#111c27;--text-primary:#e7f0f7;--text-secondary:#b7c5d3;--accent:#7ef0c2;--accent-hover:#9eff87;--border:rgba(126,240,194,.12);--border-strong:rgba(126,240,194,.28);--font-sans:"Geist",system-ui,-apple-system,sans-serif;--font-mono:"JetBrains Mono",ui-monospace,monospace;font-family:var(--font-sans);color:var(--text-primary);background:var(--surface-0)}
|
|
15817
|
+
*{box-sizing:border-box}body{margin:0;padding:clamp(1rem,5vw,4rem) 1rem;line-height:1.6}main{max-width:34rem;margin:0 auto;padding:clamp(1.25rem,4vw,2rem);border:1px solid var(--border-strong);border-radius:20px;background:var(--surface-1)}
|
|
15818
|
+
.brand{display:flex;align-items:center;gap:.6rem;margin:0 0 1.5rem;color:var(--accent);font-size:.875rem;font-weight:700;letter-spacing:.02em}.brand::before{content:"";width:.65rem;height:.65rem;border-radius:3px;background:var(--accent)}
|
|
15819
|
+
h1,h2{font-family:inherit;font-weight:600;line-height:1.25;letter-spacing:-.025em}h1{font-size:1.6rem;margin:0 0 1rem}h2{font-size:1.05rem;margin:0 0 .5rem}p{margin:.5rem 0}section{padding:1.25rem 0;border-bottom:1px solid var(--border)}.eyebrow{font-size:.75rem;letter-spacing:.08em;text-transform:uppercase;color:var(--text-secondary)}
|
|
15820
|
+
.profile{display:flex;align-items:center;gap:1rem}.profile-text{min-width:0;overflow-wrap:anywhere}.avatar{position:relative;display:inline-flex;align-items:center;justify-content:center;flex:0 0 3rem;width:3rem;height:3rem;border-radius:50%;background:var(--surface-2);color:var(--accent);overflow:hidden}.initials{font-weight:700}.avatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.note{font-size:.875rem;color:var(--text-secondary)}
|
|
15821
|
+
dl{margin:0}dt{font-size:.8rem;font-weight:600;color:var(--text-secondary);margin-top:1rem}dd{margin:.25rem 0 0}code{font-family:var(--font-mono);font-size:.8rem;overflow-wrap:anywhere;white-space:pre-wrap}
|
|
15822
|
+
.actions{display:flex;flex-wrap:wrap;gap:.75rem;margin:1.5rem 0 0}form{margin:0}button,.link{font:inherit;font-size:.875rem;font-weight:600;display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:.65rem 1rem;border:1px solid var(--border-strong);border-radius:10px;background:var(--accent);color:var(--surface-0);cursor:pointer;text-decoration:none}button.secondary{background:transparent;color:var(--accent)}a{color:var(--accent)}button:hover,.link:hover{background:var(--accent-hover);color:var(--surface-0)}:focus-visible{outline:3px solid var(--accent);outline-offset:4px}
|
|
15823
|
+
@media(prefers-color-scheme:light){:root{color-scheme:light;--surface-0:#f7faf8;--surface-1:#fff;--surface-2:#eef5f1;--text-primary:#0b1016;--text-secondary:#3f4d5c;--accent:#0f8a68;--accent-hover:#0a6f53;--border:rgba(15,138,104,.16);--border-strong:rgba(15,138,104,.3)}button,.link,button:hover,.link:hover{color:#fff}button.secondary{color:var(--accent)}button.secondary:hover{color:#fff}}
|
|
15824
|
+
@media(max-width:30rem){body{padding:1rem}.actions{flex-direction:column}.actions form,.actions button,.actions .link{width:100%}h1{font-size:1.4rem}}
|
|
15825
|
+
`;
|
|
15826
|
+
//#endregion
|
|
15827
|
+
//#region ../core/src/coordinator-account-link-receiver.ts
|
|
15828
|
+
function normalizeCoordinatorAccountLinkOrigin$1(value) {
|
|
15829
|
+
if (typeof value !== "string") return null;
|
|
15830
|
+
try {
|
|
15831
|
+
const url = new URL(value);
|
|
15832
|
+
if (!(url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "[::1]")) && url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash || value.includes("\\") || /[\s\p{Cc}]/u.test(value) || value.includes("?") || value.includes("#") || value !== url.origin && value !== `${url.origin}/`) return null;
|
|
15833
|
+
return url.origin;
|
|
15834
|
+
} catch {
|
|
15835
|
+
return null;
|
|
15836
|
+
}
|
|
15837
|
+
}
|
|
15838
|
+
`${createHash("sha256").update(COORDINATOR_AUTH_STYLE).digest("base64")}`;
|
|
15839
|
+
//#endregion
|
|
15840
|
+
//#region ../core/src/account-owner-association.ts
|
|
15841
|
+
var PIN_FIELDS = [
|
|
15842
|
+
"databaseFile",
|
|
15843
|
+
"deviceId",
|
|
15844
|
+
"keyId",
|
|
15845
|
+
"fingerprint",
|
|
15846
|
+
"actorId",
|
|
15847
|
+
"coordinatorUrl",
|
|
15848
|
+
"coordinatorId",
|
|
15849
|
+
"groupId"
|
|
15850
|
+
];
|
|
15851
|
+
var REVIEW_FIELDS = [
|
|
15852
|
+
"ownerIdentityId",
|
|
15853
|
+
"attestationId",
|
|
15854
|
+
"reviewDigest"
|
|
15855
|
+
];
|
|
15856
|
+
function invalidAssociation() {
|
|
15857
|
+
throw new Error("account_owner_association_invalid");
|
|
15858
|
+
}
|
|
15859
|
+
function hash(value) {
|
|
15860
|
+
return createHash("sha256").update(value).digest("hex");
|
|
15861
|
+
}
|
|
15862
|
+
/** Local context only, not authority. Does not inspect the running actor or its config. */
|
|
15863
|
+
function captureAccountOwnerProfilePins$1(profile) {
|
|
15864
|
+
const origin = normalizeCoordinatorAccountLinkOrigin$1(profile.coordinatorUrl);
|
|
15865
|
+
const key = parseSshEd25519PublicKey(profile.publicKey);
|
|
15866
|
+
if (!origin || key.kind !== "ed25519" || !isAuthControllerId(profile.actorId) || !isAuthControllerId(profile.deviceId) || !isAuthControllerId(profile.groupId) || !isAuthControllerId(profile.coordinatorId) || typeof profile.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(profile.fingerprint)) return invalidAssociation();
|
|
15867
|
+
return {
|
|
15868
|
+
databaseFile: ownerDatabaseFile(profile.dbPath),
|
|
15869
|
+
deviceId: profile.deviceId,
|
|
15870
|
+
keyId: hash(key.blob),
|
|
15871
|
+
fingerprint: profile.fingerprint,
|
|
15872
|
+
actorId: profile.actorId,
|
|
15873
|
+
coordinatorUrl: origin,
|
|
15874
|
+
coordinatorId: profile.coordinatorId,
|
|
15875
|
+
groupId: profile.groupId
|
|
15876
|
+
};
|
|
15877
|
+
}
|
|
15878
|
+
var profilePins = captureAccountOwnerProfilePins$1;
|
|
15879
|
+
function ownData(value, fields) {
|
|
15880
|
+
if (!value || typeof value !== "object") return invalidAssociation();
|
|
15881
|
+
const prototype = Object.getPrototypeOf(value);
|
|
15882
|
+
if (prototype !== Object.prototype && prototype !== null) return invalidAssociation();
|
|
15883
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
15884
|
+
if (Reflect.ownKeys(descriptors).length !== fields.length) return invalidAssociation();
|
|
15885
|
+
return Object.fromEntries(fields.map((field) => {
|
|
15886
|
+
const descriptor = descriptors[field];
|
|
15887
|
+
if (!Object.hasOwn(descriptors, field) || !descriptor || !("value" in descriptor)) return invalidAssociation();
|
|
15888
|
+
return [field, descriptor.value];
|
|
15889
|
+
}));
|
|
15890
|
+
}
|
|
15891
|
+
function captureStoredPins(value) {
|
|
15892
|
+
if (typeof value.databaseFile !== "string" || !/^(0|[1-9][0-9]*):(0|[1-9][0-9]*)$/.test(value.databaseFile) || !isAuthControllerId(value.deviceId) || !isAuthControllerId(value.actorId) || !isAuthControllerId(value.coordinatorId) || !isAuthControllerId(value.groupId) || typeof value.keyId !== "string" || !/^[a-f0-9]{64}$/.test(value.keyId) || typeof value.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(value.fingerprint) || typeof value.coordinatorUrl !== "string" || normalizeCoordinatorAccountLinkOrigin$1(value.coordinatorUrl) !== value.coordinatorUrl) return invalidAssociation();
|
|
15893
|
+
return {
|
|
15894
|
+
databaseFile: value.databaseFile,
|
|
15895
|
+
deviceId: value.deviceId,
|
|
15896
|
+
keyId: value.keyId,
|
|
15897
|
+
fingerprint: value.fingerprint,
|
|
15898
|
+
actorId: value.actorId,
|
|
15899
|
+
coordinatorUrl: value.coordinatorUrl,
|
|
15900
|
+
coordinatorId: value.coordinatorId,
|
|
15901
|
+
groupId: value.groupId
|
|
15902
|
+
};
|
|
15903
|
+
}
|
|
15904
|
+
function pinsMatch(value, expected) {
|
|
15905
|
+
return PIN_FIELDS.every((field) => value[field] === expected[field]);
|
|
15906
|
+
}
|
|
15907
|
+
function captureStoredAssociation(input) {
|
|
15908
|
+
const value = ownData(input, [
|
|
15909
|
+
"version",
|
|
15910
|
+
...PIN_FIELDS,
|
|
15911
|
+
...REVIEW_FIELDS
|
|
15912
|
+
]);
|
|
15913
|
+
const pins = captureStoredPins(value);
|
|
15914
|
+
if (value.version !== 1 || !isAuthControllerId(value.ownerIdentityId) || !isAuthControllerId(value.attestationId) || typeof value.reviewDigest !== "string" || !/^[a-f0-9]{64}$/.test(value.reviewDigest)) return invalidAssociation();
|
|
15915
|
+
return {
|
|
15916
|
+
version: 1,
|
|
15917
|
+
...pins,
|
|
15918
|
+
ownerIdentityId: value.ownerIdentityId,
|
|
15919
|
+
attestationId: value.attestationId,
|
|
15920
|
+
reviewDigest: value.reviewDigest
|
|
15921
|
+
};
|
|
15922
|
+
}
|
|
15923
|
+
function associationPath(profile) {
|
|
15924
|
+
const origin = normalizeCoordinatorAccountLinkOrigin$1(profile.coordinatorUrl);
|
|
15925
|
+
if (!origin || !isAuthControllerId(profile.groupId)) return invalidAssociation();
|
|
15926
|
+
const scope = hash(JSON.stringify([
|
|
15927
|
+
origin,
|
|
15928
|
+
profile.coordinatorId,
|
|
15929
|
+
profile.groupId
|
|
15930
|
+
])).slice(0, 32);
|
|
15931
|
+
return `${resolve(profile.dbPath)}.account-owner-${scope}.json`;
|
|
15932
|
+
}
|
|
15933
|
+
function captureAssociation(value, profile) {
|
|
15934
|
+
const record = captureStoredAssociation(value);
|
|
15935
|
+
if (!pinsMatch(record, profilePins(profile))) return invalidAssociation();
|
|
15936
|
+
return record;
|
|
15937
|
+
}
|
|
15938
|
+
function captureGeneration(profile, expected) {
|
|
15939
|
+
const expectedPins = captureStoredPins(ownData(expected, PIN_FIELDS));
|
|
15940
|
+
const dbPath = resolve(profile.dbPath);
|
|
15941
|
+
const pins = profilePins({
|
|
15942
|
+
...profile,
|
|
15943
|
+
dbPath
|
|
15944
|
+
});
|
|
15945
|
+
if (!pinsMatch(pins, expectedPins)) return invalidAssociation();
|
|
15946
|
+
const scope = hash(JSON.stringify([
|
|
15947
|
+
pins.coordinatorUrl,
|
|
15948
|
+
pins.coordinatorId,
|
|
15949
|
+
pins.groupId
|
|
15950
|
+
])).slice(0, 32);
|
|
15951
|
+
const base = `${dbPath}.account-owner-${scope}`;
|
|
15952
|
+
const generation = hash(JSON.stringify(pins));
|
|
15953
|
+
const basenameBase = join(dirname(dbPath), `${hash(basename(dbPath))}.account-owner-${scope}`);
|
|
15954
|
+
return {
|
|
15955
|
+
dbPath,
|
|
15956
|
+
pins,
|
|
15957
|
+
legacyPath: `${base}.json`,
|
|
15958
|
+
previousPaths: [`${base}-${generation}.json`, `${basenameBase}-${generation}.json`],
|
|
15959
|
+
path: join(accountOwnerAssociationDirectory(), `.account-owner-${generation}.json`)
|
|
15960
|
+
};
|
|
15961
|
+
}
|
|
15962
|
+
function assertGenerationCurrent(profile, context) {
|
|
15963
|
+
try {
|
|
15964
|
+
if (resolve(profile.dbPath) === context.dbPath && pinsMatch(profilePins(profile), context.pins)) return;
|
|
15965
|
+
} catch {}
|
|
15966
|
+
throw new Error("account_owner_association_context_changed_outcome_unknown");
|
|
15967
|
+
}
|
|
15968
|
+
function readStoredAssociation(path, options = {}) {
|
|
15969
|
+
try {
|
|
15970
|
+
lstatSync(path);
|
|
15971
|
+
} catch (error) {
|
|
15972
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
15973
|
+
if (error.code === "ENOENT") return null;
|
|
15974
|
+
if (options.historicalPath && error.code === "ENAMETOOLONG") return null;
|
|
15975
|
+
}
|
|
15976
|
+
return invalidAssociation();
|
|
15977
|
+
}
|
|
15978
|
+
try {
|
|
15979
|
+
const stat = lstatSync(path);
|
|
15980
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) return invalidAssociation();
|
|
15981
|
+
return captureStoredAssociation(readOwnerState(path));
|
|
15982
|
+
} catch {
|
|
15983
|
+
return invalidAssociation();
|
|
15984
|
+
}
|
|
15985
|
+
}
|
|
15986
|
+
function readGeneration(context) {
|
|
15987
|
+
const legacy = readStoredAssociation(context.legacyPath, { historicalPath: true });
|
|
15988
|
+
let found = null;
|
|
15989
|
+
const paths = [...context.previousPaths];
|
|
15990
|
+
if (readAccountOwnerAssociationDirectory(dirname(context.path))) paths.push(context.path);
|
|
15991
|
+
for (const path of paths) {
|
|
15992
|
+
const record = readStoredAssociation(path, { historicalPath: path !== context.path });
|
|
15993
|
+
if (!record) continue;
|
|
15994
|
+
if (!pinsMatch(record, context.pins)) return invalidAssociation();
|
|
15995
|
+
if (found && JSON.stringify(found.record) !== JSON.stringify(record)) return invalidAssociation();
|
|
15996
|
+
found = {
|
|
15997
|
+
record,
|
|
15998
|
+
path
|
|
15999
|
+
};
|
|
16000
|
+
}
|
|
16001
|
+
if (found) return found;
|
|
16002
|
+
if (legacy && pinsMatch(legacy, context.pins)) return {
|
|
16003
|
+
record: legacy,
|
|
16004
|
+
path: context.legacyPath
|
|
16005
|
+
};
|
|
16006
|
+
return null;
|
|
16007
|
+
}
|
|
16008
|
+
/**
|
|
16009
|
+
* Read without initializing the database, changing the actor, or granting access.
|
|
16010
|
+
* Expected pins opt trusted local callers into exact generations; omitted pins keep legacy behavior.
|
|
16011
|
+
*/
|
|
16012
|
+
function readAccountOwnerAssociation$1(profile, options) {
|
|
16013
|
+
if (options?.expectedProfilePins !== void 0) {
|
|
16014
|
+
const context = captureGeneration(profile, options.expectedProfilePins);
|
|
16015
|
+
const record = readGeneration(context);
|
|
16016
|
+
assertGenerationCurrent(profile, context);
|
|
16017
|
+
return record?.record ?? null;
|
|
16018
|
+
}
|
|
16019
|
+
const path = associationPath(profile);
|
|
16020
|
+
try {
|
|
16021
|
+
lstatSync(path);
|
|
16022
|
+
} catch (error) {
|
|
16023
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
|
|
16024
|
+
return invalidAssociation();
|
|
16025
|
+
}
|
|
16026
|
+
try {
|
|
16027
|
+
const stat = lstatSync(path);
|
|
16028
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) return invalidAssociation();
|
|
16029
|
+
return captureAssociation(readOwnerState(path), profile);
|
|
16030
|
+
} catch {
|
|
16031
|
+
return invalidAssociation();
|
|
16032
|
+
}
|
|
16033
|
+
}
|
|
16034
|
+
/**
|
|
16035
|
+
* Create-only publication after review; an exact retry never replaces prior owner evidence.
|
|
16036
|
+
* Generation checks cover this profile object and the DB file, not runtime actor/config state.
|
|
16037
|
+
* A future recovery service must check that state before and after calling this helper.
|
|
16038
|
+
* Pathname replacement and restoration between checks is not detected atomically.
|
|
16039
|
+
*/
|
|
16040
|
+
function retainAccountOwnerAssociation(profile, input, options) {
|
|
16041
|
+
const review = ownData(input, REVIEW_FIELDS);
|
|
16042
|
+
if (options?.expectedProfilePins !== void 0) return retainGeneration(profile, review, captureGeneration(profile, options.expectedProfilePins));
|
|
16043
|
+
const record = captureAssociation({
|
|
16044
|
+
version: 1,
|
|
16045
|
+
...profilePins(profile),
|
|
16046
|
+
...review
|
|
16047
|
+
}, profile);
|
|
16048
|
+
const existing = readAccountOwnerAssociation$1(profile);
|
|
16049
|
+
if (existing) {
|
|
16050
|
+
if (JSON.stringify(existing) !== JSON.stringify(record)) return invalidAssociation();
|
|
16051
|
+
syncOwnerProfileDirectory(dirname(associationPath(profile)));
|
|
16052
|
+
return existing;
|
|
16053
|
+
}
|
|
16054
|
+
try {
|
|
16055
|
+
retainOwnerState(associationPath(profile), record);
|
|
16056
|
+
} catch (error) {
|
|
16057
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") return invalidAssociation();
|
|
16058
|
+
}
|
|
16059
|
+
const retained = readAccountOwnerAssociation$1(profile);
|
|
16060
|
+
if (!retained || JSON.stringify(retained) !== JSON.stringify(record)) return invalidAssociation();
|
|
16061
|
+
syncOwnerProfileDirectory(dirname(associationPath(profile)));
|
|
16062
|
+
return retained;
|
|
16063
|
+
}
|
|
16064
|
+
function retainGeneration(profile, review, context) {
|
|
16065
|
+
const record = captureStoredAssociation({
|
|
16066
|
+
version: 1,
|
|
16067
|
+
...context.pins,
|
|
16068
|
+
...review
|
|
16069
|
+
});
|
|
16070
|
+
const existing = readGeneration(context);
|
|
16071
|
+
assertGenerationCurrent(profile, context);
|
|
16072
|
+
if (existing && JSON.stringify(existing.record) !== JSON.stringify(record)) return invalidAssociation();
|
|
16073
|
+
retainAccountOwnerAssociationDirectory(dirname(context.path));
|
|
16074
|
+
assertGenerationCurrent(profile, context);
|
|
16075
|
+
if (existing?.path === context.path) {
|
|
16076
|
+
syncOwnerProfileDirectory(dirname(context.path));
|
|
16077
|
+
assertGenerationCurrent(profile, context);
|
|
16078
|
+
return existing.record;
|
|
16079
|
+
}
|
|
16080
|
+
try {
|
|
16081
|
+
retainOwnerState(context.path, record);
|
|
16082
|
+
} catch (error) {
|
|
16083
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") return invalidAssociation();
|
|
16084
|
+
}
|
|
16085
|
+
assertGenerationCurrent(profile, context);
|
|
16086
|
+
const retained = readGeneration(context);
|
|
16087
|
+
if (!retained || JSON.stringify(retained.record) !== JSON.stringify(record)) return invalidAssociation();
|
|
16088
|
+
syncOwnerProfileDirectory(dirname(context.path));
|
|
16089
|
+
assertGenerationCurrent(profile, context);
|
|
16090
|
+
return retained.record;
|
|
16091
|
+
}
|
|
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
|
|
16183
|
+
//#region src/account/owner-recovery-context.ts
|
|
16184
|
+
function recoverySha(value) {
|
|
16185
|
+
return createHash("sha256").update(value).digest("hex");
|
|
16186
|
+
}
|
|
16187
|
+
function captureDevice(context) {
|
|
16188
|
+
const before = statSync(context.dbPath, { bigint: true });
|
|
16189
|
+
const device = readAccountLinkDevice(context.dbPath);
|
|
16190
|
+
const publicKey = loadPublicKey(context.keysDir);
|
|
16191
|
+
const after = statSync(context.dbPath, { bigint: true });
|
|
16192
|
+
requireRecovery(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 && device.fingerprint === context.fingerprint && recoverySha(device.publicKey) === context.publicKeyHash && publicKey === device.publicKey);
|
|
16193
|
+
return device.publicKey;
|
|
16194
|
+
}
|
|
16195
|
+
function captureRecoveryContext(readContext) {
|
|
16196
|
+
let result = readContext();
|
|
16197
|
+
requireRecovery(!("error" in result) && result.groups.length > 0 && result.groups.length <= 25);
|
|
16198
|
+
if (!result.context) result = readContext(result.groups[0]);
|
|
16199
|
+
requireRecovery(!("error" in result) && result.setup === "ready" && result.context);
|
|
16200
|
+
const context = structuredClone(result.context);
|
|
16201
|
+
const groups = [...result.groups].sort();
|
|
16202
|
+
requireRecovery(groups.every(recoveryId) && new Set(groups).size === groups.length && recoveryId(context.identityId));
|
|
16203
|
+
return {
|
|
16204
|
+
context,
|
|
16205
|
+
groups,
|
|
16206
|
+
publicKey: captureDevice(context),
|
|
16207
|
+
semanticId: recoverySha(JSON.stringify([
|
|
16208
|
+
resolve(context.dbPath),
|
|
16209
|
+
context.keysDir === void 0 ? null : resolve(context.keysDir),
|
|
16210
|
+
context.configPath === void 0 ? null : resolve(context.configPath),
|
|
16211
|
+
context.fileIdentity,
|
|
16212
|
+
context.identityId,
|
|
16213
|
+
context.deviceId,
|
|
16214
|
+
context.publicKeyHash,
|
|
16215
|
+
context.fingerprint,
|
|
16216
|
+
context.coordinatorUrl,
|
|
16217
|
+
groups
|
|
16218
|
+
]))
|
|
16219
|
+
};
|
|
16220
|
+
}
|
|
16221
|
+
function recoveryProfile(capture, coordinatorId) {
|
|
16222
|
+
const context = capture.context;
|
|
16223
|
+
return {
|
|
16224
|
+
contextId: recoverySha(JSON.stringify([capture.semanticId, coordinatorId])),
|
|
16225
|
+
memoryActorId: context.identityId,
|
|
16226
|
+
deviceId: context.deviceId,
|
|
16227
|
+
fingerprint: context.fingerprint,
|
|
16228
|
+
coordinatorOrigin: context.coordinatorUrl,
|
|
16229
|
+
coordinatorId,
|
|
16230
|
+
configuredGroups: [...capture.groups]
|
|
16231
|
+
};
|
|
16232
|
+
}
|
|
16233
|
+
function assertRecoverySelection(selection, expected) {
|
|
16234
|
+
requireRecovery(JSON.stringify(selection.profile) === JSON.stringify(expected) && recoveryId(selection.existingOwnerId) && Array.isArray(selection.groups) && selection.groups.length > 0 && selection.groups.length <= 25 && new Set(selection.groups).size === selection.groups.length && selection.groups.every((group) => expected.configuredGroups.includes(group)));
|
|
16235
|
+
}
|
|
16236
|
+
function recoveryDestinations(capture, coordinatorId) {
|
|
16237
|
+
return new Map(capture.groups.map((groupId) => {
|
|
16238
|
+
const profile = {
|
|
16239
|
+
dbPath: capture.context.dbPath,
|
|
16240
|
+
deviceId: capture.context.deviceId,
|
|
16241
|
+
publicKey: capture.publicKey,
|
|
16242
|
+
fingerprint: capture.context.fingerprint,
|
|
16243
|
+
actorId: capture.context.identityId,
|
|
16244
|
+
coordinatorUrl: capture.context.coordinatorUrl,
|
|
16245
|
+
coordinatorId,
|
|
16246
|
+
groupId
|
|
16247
|
+
};
|
|
16248
|
+
return [groupId, {
|
|
16249
|
+
profile,
|
|
16250
|
+
pins: captureAccountOwnerProfilePins(profile)
|
|
16251
|
+
}];
|
|
16252
|
+
}));
|
|
16253
|
+
}
|
|
16254
|
+
function assertNoCompetingOwner(destinations, ownerIdentityId) {
|
|
16255
|
+
for (const { profile, pins } of destinations.values()) {
|
|
16256
|
+
const existing = readAccountOwnerAssociation(profile, { expectedProfilePins: pins });
|
|
16257
|
+
requireRecovery(!existing || existing.ownerIdentityId === ownerIdentityId);
|
|
16258
|
+
}
|
|
16259
|
+
}
|
|
16260
|
+
//#endregion
|
|
16261
|
+
//#region src/account/owner-recovery-service.ts
|
|
16262
|
+
var CANDIDATE_TTL_MS = 6e4;
|
|
16263
|
+
function groupAction(state) {
|
|
16264
|
+
if (state === "ready") return "new";
|
|
16265
|
+
if (state === "restored") return "reuse";
|
|
16266
|
+
return "blocked";
|
|
16267
|
+
}
|
|
16268
|
+
function uniqueRows(rows) {
|
|
16269
|
+
return [...new Map(rows.map((row) => [JSON.stringify(row), row])).values()];
|
|
16270
|
+
}
|
|
16271
|
+
function staleDenial(error) {
|
|
16272
|
+
return error instanceof Error && "status" in error && error.status === 409 && "code" in error && error.code === "review_stale";
|
|
16273
|
+
}
|
|
16274
|
+
/** No routes or configuration composition: these callbacks are private server capabilities. */
|
|
16275
|
+
var AccountOwnerRecoveryService = class {
|
|
16276
|
+
#options;
|
|
16277
|
+
#handles = /* @__PURE__ */ new Map();
|
|
16278
|
+
#candidateCache;
|
|
16279
|
+
#reservations = /* @__PURE__ */ new Set();
|
|
16280
|
+
#controller = new AbortController();
|
|
16281
|
+
#closed = false;
|
|
16282
|
+
constructor(options) {
|
|
16283
|
+
this.#options = options;
|
|
16284
|
+
}
|
|
16285
|
+
#now() {
|
|
16286
|
+
return (this.#options.now ?? Date.now)();
|
|
16287
|
+
}
|
|
16288
|
+
#assertCurrent(capture, reservation) {
|
|
16289
|
+
requireRecovery(!this.#closed, "unavailable");
|
|
16290
|
+
requireRecovery(!reservation || reservation.isCurrent(), "context_changed");
|
|
16291
|
+
requireRecovery(captureRecoveryContext(this.#options.readContext).semanticId === capture.semanticId && !this.#closed && (!reservation || reservation.isCurrent()), "context_changed");
|
|
16292
|
+
}
|
|
16293
|
+
#capture() {
|
|
16294
|
+
requireRecovery(!this.#closed, "unavailable");
|
|
16295
|
+
const capture = captureRecoveryContext(this.#options.readContext);
|
|
16296
|
+
this.#assertCurrent(capture);
|
|
16297
|
+
return capture;
|
|
16298
|
+
}
|
|
16299
|
+
async #discover(capture) {
|
|
16300
|
+
this.#assertCurrent(capture);
|
|
16301
|
+
const c = capture.context;
|
|
16302
|
+
const response = await (this.#options.readRemoteStatus ?? readCoordinatorAccountLinkStatus)({
|
|
16303
|
+
dbPath: c.dbPath,
|
|
16304
|
+
keysDir: c.keysDir,
|
|
16305
|
+
coordinatorUrl: c.coordinatorUrl,
|
|
16306
|
+
groupId: c.groupId,
|
|
16307
|
+
identityId: c.identityId,
|
|
16308
|
+
signal: this.#controller.signal
|
|
16309
|
+
});
|
|
16310
|
+
this.#assertCurrent(capture);
|
|
16311
|
+
requireRecovery(response.kind === "status" && recoveryId(response.status.coordinatorId) && response.status.deviceId === c.deviceId && response.status.fingerprint === c.fingerprint && response.status.groupId === c.groupId, "unavailable");
|
|
16312
|
+
return response.status.coordinatorId;
|
|
16313
|
+
}
|
|
16314
|
+
async getProfile() {
|
|
16315
|
+
const capture = this.#capture();
|
|
16316
|
+
const coordinatorId = await this.#discover(capture);
|
|
16317
|
+
this.#assertCurrent(capture);
|
|
16318
|
+
const adminSecret = this.#options.readAdminSecret();
|
|
16319
|
+
const candidates = adminSecret ? await this.#candidates(capture, adminSecret) : [];
|
|
16320
|
+
this.#assertCurrent(capture);
|
|
16321
|
+
return {
|
|
16322
|
+
version: 1,
|
|
16323
|
+
profile: recoveryProfile(capture, coordinatorId),
|
|
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
|
|
16348
|
+
};
|
|
16349
|
+
return structuredClone(candidates);
|
|
16350
|
+
}
|
|
16351
|
+
#reviewInput(capture, selection, groupId) {
|
|
16352
|
+
const adminSecret = this.#options.readAdminSecret();
|
|
16353
|
+
requireRecovery(typeof adminSecret === "string" && adminSecret.length > 0);
|
|
16354
|
+
this.#assertCurrent(capture);
|
|
16355
|
+
return {
|
|
16356
|
+
remoteUrl: capture.context.coordinatorUrl,
|
|
16357
|
+
adminSecret,
|
|
16358
|
+
purpose: "existing_account_owner",
|
|
16359
|
+
resolutionScope: RECOVERY_SCOPE,
|
|
16360
|
+
groupId,
|
|
16361
|
+
deviceId: capture.context.deviceId,
|
|
16362
|
+
fingerprint: capture.context.fingerprint,
|
|
16363
|
+
identityId: selection.existingOwnerId
|
|
16364
|
+
};
|
|
16365
|
+
}
|
|
16366
|
+
async #previewReview(entry, groupId) {
|
|
16367
|
+
const input = this.#reviewInput(entry.capture, entry.preview, groupId);
|
|
16368
|
+
this.#assertCurrent(entry.capture);
|
|
16369
|
+
const raw = await (this.#options.reviewOwner ?? coordinatorAuthControllerReviewAction)(input);
|
|
16370
|
+
this.#assertCurrent(entry.capture);
|
|
16371
|
+
const evidence = structuredClone(raw);
|
|
16372
|
+
entry.reviews.set(groupId, validateRecoveryReview(evidence, entry.preview, groupId));
|
|
16373
|
+
entry.evidence.set(groupId, evidence);
|
|
16374
|
+
}
|
|
16375
|
+
#newReview(request, capture) {
|
|
16376
|
+
return {
|
|
16377
|
+
capture,
|
|
16378
|
+
destinations: recoveryDestinations(capture, request.profile.coordinatorId),
|
|
16379
|
+
purpose: request.purpose,
|
|
16380
|
+
createdAt: this.#now(),
|
|
16381
|
+
reviews: /* @__PURE__ */ new Map(),
|
|
16382
|
+
evidence: /* @__PURE__ */ new Map(),
|
|
16383
|
+
preview: {
|
|
16384
|
+
...structuredClone(request),
|
|
16385
|
+
scope: RECOVERY_SCOPE,
|
|
16386
|
+
approvalDigest: randomBytes(32).toString("hex"),
|
|
16387
|
+
groupPreviews: [],
|
|
16388
|
+
history: {
|
|
16389
|
+
complete: false,
|
|
16390
|
+
rows: [],
|
|
16391
|
+
operatorHelp: "Ask the coordinator operator to review the existing account owner and provide receipt references."
|
|
16392
|
+
},
|
|
16393
|
+
reviewedWeakLabels: []
|
|
16394
|
+
}
|
|
16395
|
+
};
|
|
16396
|
+
}
|
|
16397
|
+
#projectReviews(entry) {
|
|
16398
|
+
const reviews = [...entry.reviews.values()];
|
|
16399
|
+
entry.preview.history = {
|
|
16400
|
+
complete: true,
|
|
16401
|
+
rows: uniqueRows(reviews.flatMap((review) => review.rows))
|
|
16402
|
+
};
|
|
16403
|
+
entry.preview.reviewedWeakLabels = uniqueRows(reviews.flatMap((review) => review.labels));
|
|
16404
|
+
entry.preview.groupPreviews = entry.preview.groups.map((groupId) => {
|
|
16405
|
+
const review = entry.reviews.get(groupId);
|
|
16406
|
+
requireRecovery(review);
|
|
16407
|
+
return {
|
|
16408
|
+
groupId,
|
|
16409
|
+
action: groupAction(review.state),
|
|
16410
|
+
reason: review.state === "needs_review" ? review.reasons.join("; ") : "Fresh coordinator owner review."
|
|
16411
|
+
};
|
|
16412
|
+
});
|
|
16413
|
+
}
|
|
16414
|
+
#retainHandle(entry) {
|
|
16415
|
+
for (const [key, value] of this.#handles) if (this.#now() - value.createdAt >= 3e5) this.#handles.delete(key);
|
|
16416
|
+
while (this.#handles.size >= 16) {
|
|
16417
|
+
const oldest = this.#handles.keys().next().value;
|
|
16418
|
+
requireRecovery(oldest);
|
|
16419
|
+
this.#handles.delete(oldest);
|
|
16420
|
+
}
|
|
16421
|
+
this.#handles.set(entry.preview.approvalDigest, entry);
|
|
16422
|
+
return structuredClone(entry.preview);
|
|
16423
|
+
}
|
|
16424
|
+
async preview(request) {
|
|
16425
|
+
const capture = this.#capture();
|
|
16426
|
+
const coordinatorId = await this.#discover(capture);
|
|
16427
|
+
this.#assertCurrent(capture);
|
|
16428
|
+
assertRecoverySelection(request, recoveryProfile(capture, coordinatorId));
|
|
16429
|
+
requireRecovery(recoveryId(request.requestId) && (request.purpose === "review" || request.purpose === "check_status"));
|
|
16430
|
+
const entry = this.#newReview(request, capture);
|
|
16431
|
+
const admin = this.#options.readAdminSecret();
|
|
16432
|
+
this.#assertCurrent(capture);
|
|
16433
|
+
if (admin) {
|
|
16434
|
+
for (const groupId of entry.preview.groups) await this.#previewReview(entry, groupId);
|
|
16435
|
+
this.#projectReviews(entry);
|
|
16436
|
+
} else await this.#requestAssistance(entry);
|
|
16437
|
+
this.#assertCurrent(capture);
|
|
16438
|
+
return this.#retainHandle(entry);
|
|
16439
|
+
}
|
|
16440
|
+
async #requestAssistance(entry) {
|
|
16441
|
+
for (const groupId of entry.preview.groups) {
|
|
16442
|
+
this.#assertCurrent(entry.capture);
|
|
16443
|
+
await (this.#options.readRemoteOwnerStatus ?? readCoordinatorResolvedAccountOwnerStatus)({
|
|
16444
|
+
...entry.capture.context,
|
|
16445
|
+
groupId,
|
|
16446
|
+
ownerIdentityId: entry.preview.existingOwnerId,
|
|
16447
|
+
signal: this.#controller.signal
|
|
16448
|
+
});
|
|
16449
|
+
this.#assertCurrent(entry.capture);
|
|
16450
|
+
}
|
|
16451
|
+
entry.preview.groupPreviews = entry.preview.groups.map((groupId) => ({
|
|
16452
|
+
groupId,
|
|
16453
|
+
action: "blocked",
|
|
16454
|
+
reason: "An operator must review the full retained history."
|
|
16455
|
+
}));
|
|
16456
|
+
}
|
|
16457
|
+
#reserve() {
|
|
16458
|
+
requireRecovery(!this.#closed, "unavailable");
|
|
16459
|
+
const reservation = this.#options.registry.tryReserveRecovery?.();
|
|
16460
|
+
requireRecovery(reservation, "busy");
|
|
16461
|
+
requireRecovery(reservation.isCurrent(), "context_changed");
|
|
16462
|
+
this.#reservations.add(reservation);
|
|
16463
|
+
return reservation;
|
|
16464
|
+
}
|
|
16465
|
+
#release(reservation) {
|
|
16466
|
+
reservation.release();
|
|
16467
|
+
this.#reservations.delete(reservation);
|
|
16468
|
+
}
|
|
16469
|
+
async confirm(input) {
|
|
16470
|
+
requireRecovery(!this.#closed, "unavailable");
|
|
16471
|
+
const entry = this.#handles.get(input.approvalDigest);
|
|
16472
|
+
requireRecovery(entry && JSON.stringify(entry.preview) === JSON.stringify(input.preview));
|
|
16473
|
+
if (entry.outcome) return structuredClone(await entry.outcome);
|
|
16474
|
+
requireRecovery(this.#now() - entry.createdAt < 3e5 && entry.purpose === "review" && entry.preview.history.complete && entry.preview.groupPreviews.every((group) => group.action !== "blocked"));
|
|
16475
|
+
const reservation = this.#reserve();
|
|
16476
|
+
entry.outcome = this.#applyReview(entry, reservation).finally(() => this.#release(reservation));
|
|
16477
|
+
return structuredClone(await entry.outcome);
|
|
16478
|
+
}
|
|
16479
|
+
async #applyReview(entry, reservation) {
|
|
16480
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16481
|
+
assertNoCompetingOwner(entry.destinations, entry.preview.existingOwnerId);
|
|
16482
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16483
|
+
const groups = [];
|
|
16484
|
+
let verifiedReceipt;
|
|
16485
|
+
for (const groupId of entry.preview.groups) {
|
|
16486
|
+
const applied = await this.#applyGroup(entry, groupId, reservation, { verifiedReceipt });
|
|
16487
|
+
groups.push(applied.outcome);
|
|
16488
|
+
verifiedReceipt ??= applied.verifiedReceipt;
|
|
16489
|
+
}
|
|
16490
|
+
return {
|
|
16491
|
+
preview: structuredClone(entry.preview),
|
|
16492
|
+
groups
|
|
16493
|
+
};
|
|
16494
|
+
}
|
|
16495
|
+
async #confirmNew(entry, groupId, reservation) {
|
|
16496
|
+
const review = entry.reviews.get(groupId);
|
|
16497
|
+
requireRecovery(review?.digest);
|
|
16498
|
+
const input = this.#reviewInput(entry.capture, entry.preview, groupId);
|
|
16499
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16500
|
+
const raw = await (this.#options.reviewOwner ?? coordinatorAuthControllerReviewAction)({
|
|
16501
|
+
...input,
|
|
16502
|
+
confirmOwnerResolutionDigest: review.digest,
|
|
16503
|
+
assertBeforeOwnerResolutionConfirm: () => this.#assertCurrent(entry.capture, reservation)
|
|
16504
|
+
});
|
|
16505
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16506
|
+
const confirmed = validateRecoveryReview(structuredClone(raw), entry.preview, groupId);
|
|
16507
|
+
requireRecovery(confirmed.state === "ready" && confirmed.digest === review.digest && confirmed.receipt);
|
|
16508
|
+
return confirmed.receipt;
|
|
16509
|
+
}
|
|
16510
|
+
async #applyGroup(entry, groupId, reservation, options) {
|
|
16511
|
+
try {
|
|
16512
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16513
|
+
const review = entry.reviews.get(groupId);
|
|
16514
|
+
requireRecovery(review && review.state !== "needs_review");
|
|
16515
|
+
let receipt = options.verifiedReceipt ?? review.receipt;
|
|
16516
|
+
if (!options.verifiedReceipt && review.state === "ready") receipt = await this.#confirmNew(entry, groupId, reservation);
|
|
16517
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16518
|
+
requireRecovery(receipt);
|
|
16519
|
+
const outcome = await this.#verifyAndRetain(entry, groupId, receipt, reservation, { afterRemoteConfirmation: Boolean(options.verifiedReceipt) || review.state === "ready" });
|
|
16520
|
+
if (outcome.status !== "confirmed") return { outcome };
|
|
16521
|
+
return {
|
|
16522
|
+
outcome,
|
|
16523
|
+
verifiedReceipt: structuredClone(receipt)
|
|
16524
|
+
};
|
|
16525
|
+
} catch (error) {
|
|
16526
|
+
return { outcome: {
|
|
16527
|
+
groupId,
|
|
16528
|
+
status: staleDenial(error) ? "failed" : "unknown",
|
|
16529
|
+
message: "Check coordinator status or import original receipts before trying again."
|
|
16530
|
+
} };
|
|
16531
|
+
}
|
|
16532
|
+
}
|
|
16533
|
+
async #verifyAndRetain(entry, groupId, receipt, reservation, options = {}) {
|
|
16534
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16535
|
+
const response = await (this.#options.readRemoteReceiptStatus ?? readCoordinatorResolvedAccountOwnerReceiptStatus)({
|
|
16536
|
+
dbPath: entry.capture.context.dbPath,
|
|
16537
|
+
keysDir: entry.capture.context.keysDir,
|
|
16538
|
+
coordinatorUrl: entry.capture.context.coordinatorUrl,
|
|
16539
|
+
groupId,
|
|
16540
|
+
ownerIdentityId: entry.preview.existingOwnerId,
|
|
16541
|
+
receipt: structuredClone(receipt),
|
|
16542
|
+
signal: this.#controller.signal
|
|
16543
|
+
});
|
|
16544
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16545
|
+
if (!this.#validOwnerProof(response, entry, groupId)) return {
|
|
16546
|
+
groupId,
|
|
16547
|
+
status: options.afterRemoteConfirmation ? "unknown" : "failed",
|
|
16548
|
+
message: "The original owner receipt could not be verified with a linked account."
|
|
16549
|
+
};
|
|
16550
|
+
this.#publish(entry, groupId, receipt, reservation);
|
|
16551
|
+
return {
|
|
16552
|
+
groupId,
|
|
16553
|
+
status: "confirmed",
|
|
16554
|
+
message: "Verified existing owner context retained; memory authorship is unchanged."
|
|
16555
|
+
};
|
|
16556
|
+
}
|
|
16557
|
+
#validOwnerProof(response, entry, groupId) {
|
|
16558
|
+
if (response.kind !== "owner_status") return false;
|
|
16559
|
+
const status = response.status, p = entry.preview.profile;
|
|
16560
|
+
return status.version === 1 && status.coordinatorId === p.coordinatorId && status.groupId === groupId && status.deviceId === p.deviceId && status.fingerprint === p.fingerprint && status.state === "owner_confirmed" && status.ownerIdentity === entry.preview.existingOwnerId && status.profileState === "linked" && status.profile !== null && typeof status.profile === "object" && !Array.isArray(status.profile);
|
|
16561
|
+
}
|
|
16562
|
+
#publish(entry, groupId, receipt, reservation) {
|
|
16563
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16564
|
+
assertNoCompetingOwner(entry.destinations, entry.preview.existingOwnerId);
|
|
16565
|
+
const destination = entry.destinations.get(groupId);
|
|
16566
|
+
requireRecovery(destination);
|
|
16567
|
+
const { profile, pins } = destination;
|
|
16568
|
+
const existing = readAccountOwnerAssociation(profile, { expectedProfilePins: pins });
|
|
16569
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16570
|
+
if (!existing) retainAccountOwnerAssociation(profile, {
|
|
16571
|
+
ownerIdentityId: entry.preview.existingOwnerId,
|
|
16572
|
+
attestationId: receipt.attestationId,
|
|
16573
|
+
reviewDigest: receipt.reviewEvidenceDigest
|
|
16574
|
+
}, { expectedProfilePins: pins });
|
|
16575
|
+
this.#assertCurrent(entry.capture, reservation);
|
|
16576
|
+
}
|
|
16577
|
+
async importReply(input) {
|
|
16578
|
+
const reservation = this.#reserve();
|
|
16579
|
+
try {
|
|
16580
|
+
const capture = this.#capture();
|
|
16581
|
+
const coordinatorId = await this.#discover(capture);
|
|
16582
|
+
this.#assertCurrent(capture, reservation);
|
|
16583
|
+
assertRecoverySelection(input.selection, recoveryProfile(capture, coordinatorId));
|
|
16584
|
+
const receipts = parseRecoveryReply(input.replyText, input.selection);
|
|
16585
|
+
const entry = this.#newReview({
|
|
16586
|
+
...input.selection,
|
|
16587
|
+
requestId: randomBytes(16).toString("hex"),
|
|
16588
|
+
purpose: "review"
|
|
16589
|
+
}, capture);
|
|
16590
|
+
assertNoCompetingOwner(entry.destinations, entry.preview.existingOwnerId);
|
|
16591
|
+
const groups = [];
|
|
16592
|
+
for (const [groupId, receipt] of receipts) groups.push(await this.#importGroup(entry, groupId, receipt, reservation));
|
|
16593
|
+
return {
|
|
16594
|
+
preview: entry.preview,
|
|
16595
|
+
groups
|
|
16596
|
+
};
|
|
16597
|
+
} finally {
|
|
16598
|
+
this.#release(reservation);
|
|
16599
|
+
}
|
|
16600
|
+
}
|
|
16601
|
+
async #importGroup(entry, groupId, receipt, reservation) {
|
|
16602
|
+
try {
|
|
16603
|
+
return await this.#verifyAndRetain(entry, groupId, receipt, reservation);
|
|
16604
|
+
} catch {
|
|
16605
|
+
return {
|
|
16606
|
+
groupId,
|
|
16607
|
+
status: "unknown",
|
|
16608
|
+
message: "Context or proof outcome changed; check status before importing again."
|
|
16609
|
+
};
|
|
16610
|
+
}
|
|
16611
|
+
}
|
|
16612
|
+
async close() {
|
|
16613
|
+
this.#closed = true;
|
|
16614
|
+
this.#controller.abort();
|
|
16615
|
+
this.#handles.clear();
|
|
16616
|
+
for (const reservation of this.#reservations) this.#release(reservation);
|
|
16617
|
+
}
|
|
16618
|
+
};
|
|
16619
|
+
function createAccountOwnerRecoveryService(options) {
|
|
16620
|
+
return new AccountOwnerRecoveryService(options);
|
|
16621
|
+
}
|
|
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
|
|
16760
|
+
//#region src/index.ts
|
|
16761
|
+
/**
|
|
16762
|
+
* @codemem/server — HTTP server (viewer, sync, API).
|
|
16763
|
+
*
|
|
16764
|
+
* Single HTTP server handling viewer routes and sync daemon.
|
|
16765
|
+
* Shares one better-sqlite3 connection between viewer and sync.
|
|
16766
|
+
* Embedding inference runs in a worker_thread (lazy-started).
|
|
13717
16767
|
*
|
|
13718
16768
|
* Entry: `codemem serve`
|
|
13719
16769
|
*/
|
|
13720
16770
|
/** Shared store instance — SQLite WAL mode handles concurrent reads safely. */
|
|
13721
16771
|
var sharedStore = null;
|
|
16772
|
+
var sharedStoreClosePromise = null;
|
|
13722
16773
|
/** Get (or create) the shared store instance. Exported so the sweeper can share it. */
|
|
13723
16774
|
function getStore() {
|
|
13724
16775
|
if (!sharedStore) sharedStore = new MemoryStore(resolveDbPath());
|
|
13725
16776
|
return sharedStore;
|
|
13726
16777
|
}
|
|
13727
|
-
/** Close
|
|
16778
|
+
/** Close a synchronous-only shared store. Async pack owners use closeStoreAsync(). */
|
|
13728
16779
|
function closeStore() {
|
|
13729
16780
|
sharedStore?.close();
|
|
13730
16781
|
sharedStore = null;
|
|
13731
16782
|
}
|
|
16783
|
+
/** Drain semantic work before closing SQLite and releasing the shared store. */
|
|
16784
|
+
function closeStoreAsync() {
|
|
16785
|
+
if (sharedStoreClosePromise) return sharedStoreClosePromise;
|
|
16786
|
+
if (!sharedStore) return Promise.resolve();
|
|
16787
|
+
const store = sharedStore;
|
|
16788
|
+
sharedStoreClosePromise = store.closeAsync().then(() => {
|
|
16789
|
+
if (sharedStore === store) sharedStore = null;
|
|
16790
|
+
sharedStoreClosePromise = null;
|
|
16791
|
+
});
|
|
16792
|
+
return sharedStoreClosePromise;
|
|
16793
|
+
}
|
|
13732
16794
|
function registerStaticRoutes(app) {
|
|
13733
16795
|
const staticRoot = process.env.CODEMEM_VIEWER_STATIC_DIR ?? join(import.meta.dirname ?? ".", "../static");
|
|
13734
16796
|
app.use("/assets/*", async (c, next) => {
|
|
@@ -13761,6 +16823,7 @@ function registerAccountRoutes(app, options) {
|
|
|
13761
16823
|
const account = createAccountGuard(options);
|
|
13762
16824
|
app.use("*", account.middleware);
|
|
13763
16825
|
app.route("/", account.routes);
|
|
16826
|
+
if (options.ownerRecovery) app.route("/", accountOwnerRecoveryRoutes(options.ownerRecovery));
|
|
13764
16827
|
if (options.readContext) app.route("/", accountStatusRoutes({
|
|
13765
16828
|
...options,
|
|
13766
16829
|
readContext: options.readContext
|
|
@@ -13784,6 +16847,7 @@ function createApp(opts) {
|
|
|
13784
16847
|
const viewerTargetGuard = viewerTargetQueryGuard(storeFactory);
|
|
13785
16848
|
app.use("/api/memories/*", viewerTargetGuard);
|
|
13786
16849
|
app.use("/api/memory", viewerTargetGuard);
|
|
16850
|
+
app.use("/api/pi/sessions/search", viewerTargetGuard);
|
|
13787
16851
|
app.route("/", healthRoutes(storeFactory));
|
|
13788
16852
|
app.route("/", diagnosticsRoutes(storeFactory));
|
|
13789
16853
|
app.route("/", statsRoutes(storeFactory));
|
|
@@ -13845,6 +16909,6 @@ function createSyncApp(opts) {
|
|
|
13845
16909
|
return app;
|
|
13846
16910
|
}
|
|
13847
16911
|
//#endregion
|
|
13848
|
-
export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, createAccountContextReader, createAccountLinkRegistry, 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 };
|
|
13849
16913
|
|
|
13850
16914
|
//# sourceMappingURL=index.js.map
|