@harperfast/harper 5.2.13 → 5.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/mcpTools.ts +1 -1
- package/agent/session.ts +25 -14
- package/bin/cliOperations.ts +58 -9
- package/bin/copyDb.ts +282 -59
- package/bin/deploySetup.ts +16 -5
- package/bin/harper.ts +1 -1
- package/bin/help.ts +4 -1
- package/bin/lite.ts +4 -1
- package/bin/restart.ts +120 -4
- package/bin/run.ts +6 -11
- package/bin/upgrade.js +7 -3
- package/bin/workloadIdentity.ts +119 -0
- package/components/Application.ts +3436 -242
- package/components/ApplicationScope.ts +8 -0
- package/components/EntryHandler.ts +59 -39
- package/components/OptionsWatcher.ts +440 -98
- package/components/RuntimeModuleTracker.ts +38 -7
- package/components/Scope.ts +56 -15
- package/components/awaitRestart.ts +84 -0
- package/components/componentLoader.ts +381 -32
- package/components/componentPreparationLock.ts +16 -5
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/mcp/adapters/harperHttp.ts +4 -0
- package/components/mcp/listChanged.ts +4 -0
- package/components/mcp/toolRegistry.ts +2 -0
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +2 -2
- package/components/operations.js +537 -113
- package/components/operationsValidation.js +98 -3
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/components/status/ComponentStatusRegistry.ts +59 -0
- package/config/RootConfigWatcher.ts +240 -40
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +357 -48
- package/config/harperConfigEnvVars.ts +170 -27
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +33 -0
- package/dataLayer/blobBackup.ts +160 -50
- package/dataLayer/delete.ts +6 -1
- package/dataLayer/harperBridge/ResourceBridge.ts +80 -10
- package/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dataLayer/hdbInfoController.ts +34 -1
- package/dataLayer/insert.ts +44 -1
- package/dataLayer/rocksdbBackup.ts +53 -10
- package/dataLayer/schema.ts +11 -1
- package/dataLayer/schemaDescribe.ts +8 -1
- package/dist/agent/mcpTools.js +1 -1
- package/dist/agent/mcpTools.js.map +1 -1
- package/dist/agent/session.d.ts +22 -0
- package/dist/agent/session.js +26 -15
- package/dist/agent/session.js.map +1 -1
- package/dist/bin/cliOperations.js +61 -9
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.d.ts +12 -1
- package/dist/bin/copyDb.js +248 -60
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deploySetup.d.ts +2 -0
- package/dist/bin/deploySetup.js +11 -3
- package/dist/bin/deploySetup.js.map +1 -1
- package/dist/bin/harper.js +1 -1
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.js +4 -1
- package/dist/bin/help.js.map +1 -1
- package/dist/bin/lite.js +4 -1
- package/dist/bin/lite.js.map +1 -1
- package/dist/bin/restart.js +91 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/bin/run.js +4 -10
- package/dist/bin/run.js.map +1 -1
- package/dist/bin/upgrade.js +4 -3
- package/dist/bin/upgrade.js.map +1 -1
- package/dist/bin/workloadIdentity.d.ts +18 -0
- package/dist/bin/workloadIdentity.js +100 -0
- package/dist/bin/workloadIdentity.js.map +1 -0
- package/dist/components/Application.d.ts +235 -17
- package/dist/components/Application.js +3038 -238
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +8 -0
- package/dist/components/ApplicationScope.js +7 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.js +26 -10
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -0
- package/dist/components/OptionsWatcher.js +440 -99
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.js +40 -6
- package/dist/components/RuntimeModuleTracker.js.map +1 -1
- package/dist/components/Scope.js +52 -13
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/awaitRestart.d.ts +33 -0
- package/dist/components/awaitRestart.js +61 -0
- package/dist/components/awaitRestart.js.map +1 -0
- package/dist/components/componentLoader.d.ts +38 -1
- package/dist/components/componentLoader.js +313 -24
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/componentPreparationLock.d.ts +5 -0
- package/dist/components/componentPreparationLock.js +14 -6
- package/dist/components/componentPreparationLock.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/adapters/harperHttp.js +4 -0
- package/dist/components/mcp/adapters/harperHttp.js.map +1 -1
- package/dist/components/mcp/listChanged.js +5 -0
- package/dist/components/mcp/listChanged.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +1 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/operations.d.ts +5 -0
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -2
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +476 -113
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +97 -3
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/components/status/ComponentStatusRegistry.d.ts +0 -4
- package/dist/components/status/ComponentStatusRegistry.js +63 -0
- package/dist/components/status/ComponentStatusRegistry.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +9 -1
- package/dist/config/RootConfigWatcher.js +228 -37
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +11 -2
- package/dist/config/configUtils.js +304 -47
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.d.ts +16 -0
- package/dist/config/harperConfigEnvVars.js +162 -25
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +49 -20
- package/dist/dataLayer/blobBackup.js +139 -50
- package/dist/dataLayer/blobBackup.js.map +1 -1
- package/dist/dataLayer/delete.js +1 -1
- package/dist/dataLayer/delete.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +14 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +72 -12
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.d.ts +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js +3 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbMethods/DeleteAuditLogsBeforeResults.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.d.ts +10 -0
- package/dist/dataLayer/hdbInfoController.js +30 -1
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/insert.d.ts +9 -1
- package/dist/dataLayer/insert.js +30 -0
- package/dist/dataLayer/insert.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.d.ts +2 -2
- package/dist/dataLayer/rocksdbBackup.js +45 -8
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/dataLayer/schema.js +8 -0
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +8 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +55 -0
- package/dist/resources/DatabaseTransaction.d.ts +77 -0
- package/dist/resources/DatabaseTransaction.js +586 -54
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +43 -6
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.d.ts +1 -0
- package/dist/resources/PrimaryRocksDatabase.js +49 -5
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +20 -0
- package/dist/resources/RecordEncoder.js +110 -9
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/RequestTarget.d.ts +2 -0
- package/dist/resources/RequestTarget.js.map +1 -1
- package/dist/resources/Resource.js +117 -24
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +28 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +6 -1
- package/dist/resources/RocksIndexStore.js +24 -9
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +24 -1
- package/dist/resources/RocksTransactionLogStore.js +160 -49
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +165 -10
- package/dist/resources/Table.js +1805 -303
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +181 -2
- package/dist/resources/auditStore.js +640 -29
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +129 -9
- package/dist/resources/blob.js +992 -114
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.d.ts +48 -0
- package/dist/resources/branchDatabase.js +892 -0
- package/dist/resources/branchDatabase.js.map +1 -0
- package/dist/resources/crdt.js +50 -12
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +157 -14
- package/dist/resources/databases.js +1462 -325
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/defineTable.d.ts +10 -2
- package/dist/resources/defineTable.js +9 -1
- package/dist/resources/defineTable.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.d.ts +1 -1
- package/dist/resources/graphql.js +52 -16
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +103 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +854 -41
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/longLivedTransactions.d.ts +71 -0
- package/dist/resources/longLivedTransactions.js +358 -0
- package/dist/resources/longLivedTransactions.js.map +1 -0
- package/dist/resources/models/backendRegistry.d.ts +26 -0
- package/dist/resources/models/backendRegistry.js +60 -2
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +33 -1
- package/dist/resources/models/bootstrap.js +416 -31
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +123 -0
- package/dist/resources/recordLock.js +315 -0
- package/dist/resources/recordLock.js.map +1 -0
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.d.ts +14 -1
- package/dist/resources/replayLogs.js +157 -24
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replayLogsGuards.d.ts +94 -7
- package/dist/resources/replayLogsGuards.js +111 -7
- package/dist/resources/replayLogsGuards.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +283 -37
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +1 -1
- package/dist/resources/transactionBroadcast.js +2 -2
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +61 -30
- package/dist/security/auth.js.map +1 -1
- package/dist/security/authn/oidc/claims.d.ts +22 -0
- package/dist/security/authn/oidc/claims.js +71 -0
- package/dist/security/authn/oidc/claims.js.map +1 -0
- package/dist/security/authn/oidc/identityToken.d.ts +27 -0
- package/dist/security/authn/oidc/identityToken.js +111 -0
- package/dist/security/authn/oidc/identityToken.js.map +1 -0
- package/dist/security/authn/oidc/jwks.d.ts +25 -0
- package/dist/security/authn/oidc/jwks.js +261 -0
- package/dist/security/authn/oidc/jwks.js.map +1 -0
- package/dist/security/authn/oidc/providers/generic.d.ts +13 -0
- package/dist/security/authn/oidc/providers/generic.js +34 -0
- package/dist/security/authn/oidc/providers/generic.js.map +1 -0
- package/dist/security/authn/oidc/providers/githubActions.d.ts +11 -0
- package/dist/security/authn/oidc/providers/githubActions.js +129 -0
- package/dist/security/authn/oidc/providers/githubActions.js.map +1 -0
- package/dist/security/authn/oidc/providers/index.d.ts +37 -0
- package/dist/security/authn/oidc/providers/index.js +24 -0
- package/dist/security/authn/oidc/providers/index.js.map +1 -0
- package/dist/security/authn/oidc/tokenExchange.d.ts +12 -0
- package/dist/security/authn/oidc/tokenExchange.js +306 -0
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -0
- package/dist/security/authn/oidc/trustPolicyOperations.d.ts +49 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js +358 -0
- package/dist/security/authn/oidc/trustPolicyOperations.js.map +1 -0
- package/dist/security/authn/oidc/types.d.ts +38 -0
- package/dist/security/authn/oidc/types.js +6 -0
- package/dist/security/authn/oidc/types.js.map +1 -0
- package/dist/security/certificateVerification/index.js +40 -11
- package/dist/security/certificateVerification/index.js.map +1 -1
- package/dist/security/certificateVerification/trustedIssuers.d.ts +24 -0
- package/dist/security/certificateVerification/trustedIssuers.js +79 -0
- package/dist/security/certificateVerification/trustedIssuers.js.map +1 -0
- package/dist/security/certificateVerification/types.d.ts +1 -0
- package/dist/security/credentialProvenance.d.ts +35 -0
- package/dist/security/credentialProvenance.js +51 -0
- package/dist/security/credentialProvenance.js.map +1 -0
- package/dist/security/credentialRejection.d.ts +4 -0
- package/dist/security/credentialRejection.js +24 -0
- package/dist/security/credentialRejection.js.map +1 -0
- package/dist/security/deferredAuthentication.d.ts +36 -0
- package/dist/security/deferredAuthentication.js +70 -0
- package/dist/security/deferredAuthentication.js.map +1 -0
- package/dist/security/impersonation.d.ts +21 -0
- package/dist/security/impersonation.js +108 -9
- package/dist/security/impersonation.js.map +1 -1
- package/dist/security/jsLoader.d.ts +6 -0
- package/dist/security/jsLoader.js +77 -15
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/keys.js +301 -71
- package/dist/security/keys.js.map +1 -1
- package/dist/security/operationScope.d.ts +21 -0
- package/dist/security/operationScope.js +36 -0
- package/dist/security/operationScope.js.map +1 -0
- package/dist/security/permissionsTranslator.js +21 -0
- package/dist/security/permissionsTranslator.js.map +1 -1
- package/dist/security/tokenAuthentication.d.ts +19 -1
- package/dist/security/tokenAuthentication.js +191 -10
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.js +4 -3
- package/dist/security/user.js.map +1 -1
- package/dist/server/DurableSubscriptionsSession.d.ts +2 -2
- package/dist/server/DurableSubscriptionsSession.js +67 -10
- package/dist/server/DurableSubscriptionsSession.js.map +1 -1
- package/dist/server/REST.js +106 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/graphqlQuerying.js +4 -0
- package/dist/server/graphqlQuerying.js.map +1 -1
- package/dist/server/http.d.ts +16 -1
- package/dist/server/http.js +122 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +8 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +6 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +4 -1
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/liveSubscriptionAuth.d.ts +26 -4
- package/dist/server/liveSubscriptionAuth.js +105 -39
- package/dist/server/liveSubscriptionAuth.js.map +1 -1
- package/dist/server/loadRootComponents.js +49 -10
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/mqtt.d.ts +2 -0
- package/dist/server/mqtt.js +165 -30
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/nodeName.d.ts +2 -0
- package/dist/server/nodeName.js +107 -23
- package/dist/server/nodeName.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +27 -0
- package/dist/server/serverHelpers/Headers.js +145 -1
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +11 -0
- package/dist/server/serverHelpers/contentTypes.js +221 -40
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/deployValidationState.d.ts +3 -0
- package/dist/server/serverHelpers/deployValidationState.js +9 -19
- package/dist/server/serverHelpers/deployValidationState.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +12 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +24 -2
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -1
- package/dist/server/serverHelpers/registeredOperations.d.ts +5 -4
- package/dist/server/serverHelpers/registeredOperations.js +74 -21
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/requestSanitization.d.ts +11 -0
- package/dist/server/serverHelpers/requestSanitization.js +20 -0
- package/dist/server/serverHelpers/requestSanitization.js.map +1 -0
- package/dist/server/serverHelpers/serverHandlers.js +6 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +18 -0
- package/dist/server/serverHelpers/serverUtilities.js +178 -29
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/sharedMessageEncoding.d.ts +67 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js +280 -0
- package/dist/server/serverHelpers/sharedMessageEncoding.js.map +1 -0
- package/dist/server/serverHelpers/uwsServer.js +19 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/static.js +24 -28
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +5 -0
- package/dist/server/storageReclamation.js +17 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/itc.d.ts +7 -2
- package/dist/server/threads/itc.js +5 -1
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +83 -3
- package/dist/server/threads/manageThreads.js +769 -75
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +277 -31
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadHeapMemory.d.ts +2 -0
- package/dist/server/threads/threadHeapMemory.js +31 -0
- package/dist/server/threads/threadHeapMemory.js.map +1 -0
- package/dist/server/threads/threadServer.js +76 -22
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/config.d.ts +1 -3
- package/dist/sqlEngine/config.js +19 -16
- package/dist/sqlEngine/config.js.map +1 -1
- package/dist/sqlTranslator/index.d.ts +1 -1
- package/dist/sqlTranslator/index.js +30 -7
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.d.ts +7 -0
- package/dist/upgrade/directives/5-3-0.js +148 -0
- package/dist/upgrade/directives/5-3-0.js.map +1 -0
- package/dist/upgrade/directives/directivesController.js +2 -1
- package/dist/upgrade/directives/directivesController.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +2 -1
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/common_utils.d.ts +16 -0
- package/dist/utility/common_utils.js +32 -6
- package/dist/utility/common_utils.js.map +1 -1
- package/dist/utility/componentNames.d.ts +8 -0
- package/dist/utility/componentNames.js +12 -1
- package/dist/utility/componentNames.js.map +1 -1
- package/dist/utility/environment/environmentManager.js +3 -6
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/environment/systemInformation.d.ts +1 -0
- package/dist/utility/environment/systemInformation.js +1 -0
- package/dist/utility/environment/systemInformation.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +2 -0
- package/dist/utility/errors/commonErrors.js +2 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +33 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +18 -0
- package/dist/utility/hdbTerms.js +20 -2
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +2 -0
- package/dist/utility/logging/harper_logger.js +286 -30
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +192 -85
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/nodeIdentity.d.ts +9 -0
- package/dist/utility/nodeIdentity.js +58 -0
- package/dist/utility/nodeIdentity.js.map +1 -0
- package/dist/utility/npmUtilities.js +11 -7
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/operationPermissions.d.ts +3 -1
- package/dist/utility/operationPermissions.js +16 -1
- package/dist/utility/operationPermissions.js.map +1 -1
- package/dist/utility/operation_authorization.d.ts +10 -7
- package/dist/utility/operation_authorization.js +212 -41
- package/dist/utility/operation_authorization.js.map +1 -1
- package/dist/utility/watchPath.d.ts +29 -0
- package/dist/utility/watchPath.js +68 -0
- package/dist/utility/watchPath.js.map +1 -0
- package/dist/utility/watcherFallback.d.ts +41 -0
- package/dist/utility/watcherFallback.js +153 -0
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.d.ts +12 -0
- package/dist/validation/configValidator.js +205 -75
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/installValidator.js +12 -0
- package/dist/validation/installValidator.js.map +1 -1
- package/dist/validation/validationWrapper.d.ts +11 -0
- package/dist/validation/validationWrapper.js +16 -3
- package/dist/validation/validationWrapper.js.map +1 -1
- package/index.ts +8 -0
- package/json/systemSchema.json +55 -0
- package/npm-shrinkwrap.json +286 -194
- package/package.json +15 -7
- package/resources/DESIGN.md +219 -52
- package/resources/DatabaseTransaction.ts +657 -52
- package/resources/LMDBTransaction.ts +46 -6
- package/resources/PrimaryRocksDatabase.ts +46 -7
- package/resources/RecordEncoder.ts +124 -9
- package/resources/RequestTarget.ts +2 -0
- package/resources/Resource.ts +114 -22
- package/resources/ResourceInterface.ts +31 -0
- package/resources/RocksIndexStore.ts +30 -9
- package/resources/RocksTransactionLogStore.ts +188 -51
- package/resources/Table.ts +1960 -301
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +647 -32
- package/resources/blob.ts +1029 -110
- package/resources/branchDatabase.ts +941 -0
- package/resources/crdt.ts +76 -12
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +1584 -320
- package/resources/defineTable.ts +18 -2
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +72 -17
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +911 -48
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/longLivedTransactions.ts +360 -0
- package/resources/models/backendRegistry.ts +84 -2
- package/resources/models/bootstrap.ts +473 -28
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +419 -0
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +152 -26
- package/resources/replayLogsGuards.ts +171 -8
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +288 -46
- package/resources/tracked.ts +73 -22
- package/resources/transactionBroadcast.ts +3 -3
- package/security/auth.ts +68 -29
- package/security/authn/oidc/claims.ts +72 -0
- package/security/authn/oidc/identityToken.ts +129 -0
- package/security/authn/oidc/jwks.ts +260 -0
- package/security/authn/oidc/providers/generic.ts +40 -0
- package/security/authn/oidc/providers/githubActions.ts +137 -0
- package/security/authn/oidc/providers/index.ts +52 -0
- package/security/authn/oidc/tokenExchange.ts +300 -0
- package/security/authn/oidc/trustPolicyOperations.ts +343 -0
- package/security/authn/oidc/types.ts +41 -0
- package/security/certificateVerification/index.ts +54 -13
- package/security/certificateVerification/trustedIssuers.ts +76 -0
- package/security/certificateVerification/types.ts +1 -0
- package/security/credentialProvenance.ts +47 -0
- package/security/credentialRejection.ts +22 -0
- package/security/deferredAuthentication.ts +71 -0
- package/security/impersonation.ts +117 -12
- package/security/jsLoader.ts +83 -18
- package/security/keys.ts +298 -72
- package/security/operationScope.ts +33 -0
- package/security/permissionsTranslator.js +23 -0
- package/security/tokenAuthentication.ts +233 -12
- package/security/user.ts +4 -3
- package/server/DESIGN.md +194 -16
- package/server/DurableSubscriptionsSession.ts +71 -11
- package/server/REST.ts +115 -4
- package/server/graphqlQuerying.ts +4 -0
- package/server/http.ts +133 -20
- package/server/itc/serverHandlers.js +8 -1
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +4 -1
- package/server/liveSubscriptionAuth.ts +129 -46
- package/server/loadRootComponents.js +50 -8
- package/server/mqtt.ts +179 -38
- package/server/nodeName.ts +103 -21
- package/server/serverHelpers/Headers.ts +135 -0
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +217 -36
- package/server/serverHelpers/deployValidationState.ts +24 -13
- package/server/serverHelpers/operationAuthorizationState.ts +34 -3
- package/server/serverHelpers/registeredOperations.ts +79 -22
- package/server/serverHelpers/requestSanitization.ts +15 -0
- package/server/serverHelpers/serverHandlers.js +6 -3
- package/server/serverHelpers/serverUtilities.ts +232 -40
- package/server/serverHelpers/sharedMessageEncoding.ts +307 -0
- package/server/serverHelpers/uwsServer.ts +17 -2
- package/server/static.ts +23 -29
- package/server/storageReclamation.ts +15 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/itc.js +11 -1
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +771 -66
- package/server/threads/socketRouter.ts +291 -30
- package/server/threads/threadHeapMemory.ts +26 -0
- package/server/threads/threadServer.js +73 -22
- package/sqlTranslator/index.ts +31 -8
- package/studio/web/assets/{Chat-4RrB5134.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-omlNMDcJ.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bke3wvfZ.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DvFDYJgK.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-C1vRa7zh.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-3XRWEDWX.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-CTY5tdMr.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CG1vSD9t.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-Dfpeofdu.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-B1VOIv-t.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-CJeJzGnI.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-CYPBwM82.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-IKH4GUHl.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BC8_gxKG.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-BfKBpYcq.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-DFbArTfC.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-Q4-T6c-S.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-CtkV0xje.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-1ZZuMsiA.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-ZWLcWv5X.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-DWprJyJy.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status--aNm8isn.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-Cq_lt3XD.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-pjgytARx.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-Cdg0mqUh.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-RZhaY8Rn.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-BgoXIqQe.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-BwSeDMiP.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-DotCUG5l.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +132 -0
- package/upgrade/directives/directivesController.ts +2 -1
- package/utility/OperationFunctionCaller.ts +2 -1
- package/utility/common_utils.ts +30 -5
- package/utility/componentNames.ts +12 -0
- package/utility/environment/environmentManager.ts +3 -7
- package/utility/environment/systemInformation.ts +7 -0
- package/utility/errors/commonErrors.ts +4 -0
- package/utility/errors/hdbError.ts +57 -0
- package/utility/hdbTerms.ts +19 -0
- package/utility/logging/harper_logger.ts +278 -26
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +213 -81
- package/utility/nodeIdentity.ts +45 -0
- package/utility/npmUtilities.ts +12 -8
- package/utility/operationPermissions.ts +18 -1
- package/utility/operation_authorization.ts +231 -42
- package/utility/watchPath.ts +63 -0
- package/utility/watcherFallback.ts +148 -0
- package/validation/configValidator.ts +215 -75
- package/validation/installValidator.ts +15 -0
- package/validation/validationWrapper.ts +18 -4
package/resources/auditStore.ts
CHANGED
|
@@ -2,14 +2,15 @@ import { readKey, writeKey } from 'ordered-binary';
|
|
|
2
2
|
import { initSync, get as envGet } from '../utility/environment/environmentManager.ts';
|
|
3
3
|
import { AUDIT_STORE_NAME } from '../utility/lmdb/terms.ts';
|
|
4
4
|
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
|
|
5
|
-
import { getWorkerIndex,
|
|
5
|
+
import { getWorkerIndex, ownsStoreMaintenance } from '../server/threads/manageThreads.js';
|
|
6
6
|
import { convertToMS } from '../utility/common_utils.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { LAST_TIMESTAMP_PLACEHOLDER, HAS_STRUCTURE_UPDATE, PENDING_LOCAL_TIME } from './RecordEncoder.ts';
|
|
8
8
|
import * as harperLogger from '../utility/logging/harper_logger.ts';
|
|
9
9
|
import { getRecordAtTime } from './crdt.ts';
|
|
10
10
|
import { decodeFromDatabase } from './blob.ts';
|
|
11
11
|
import { onStorageReclamation } from '../server/storageReclamation.ts';
|
|
12
12
|
import { RocksDatabase } from '@harperfast/rocksdb-js';
|
|
13
|
+
import { asBinary } from 'lmdb';
|
|
13
14
|
import { RocksTransactionLogStore } from './RocksTransactionLogStore.ts';
|
|
14
15
|
import { isReadOnlyMode } from './databases.ts';
|
|
15
16
|
|
|
@@ -33,9 +34,10 @@ import { isReadOnlyMode } from './databases.ts';
|
|
|
33
34
|
initSync();
|
|
34
35
|
|
|
35
36
|
export type AuditRecord = {
|
|
36
|
-
version: number;
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
version: number; // the record's own version: LWW ordering, @updatedTime, ETag
|
|
38
|
+
txnLogKey: number; // position in the origin's transaction log
|
|
39
|
+
/** Physical transaction log that yielded this entry, populated only when requested by the reader. */
|
|
40
|
+
logName?: string;
|
|
39
41
|
type: string;
|
|
40
42
|
encodedRecord?: Buffer;
|
|
41
43
|
extendedType?: number;
|
|
@@ -116,8 +118,40 @@ const MAX_CLEANUP_DELAY = 2 ** 31 - 1;
|
|
|
116
118
|
// mint latch keyed per (table, type) so one entry type can't silence another's producer stack
|
|
117
119
|
const warnedBodylessMints = new Set<string>();
|
|
118
120
|
const warnedBodylessTables = new Set<number>();
|
|
121
|
+
// legacy-format latches, mirroring the bodyless ones above: a range scan over millions of pre-#2247
|
|
122
|
+
// entries must not turn a recovered read into a log flood. Recovery latches per table (the table is
|
|
123
|
+
// known by then); an undecodable header has no table, so it latches once per process.
|
|
124
|
+
const warnedRecoveredTables = new Set<number>();
|
|
125
|
+
let warnedUndecodableHeader = false;
|
|
126
|
+
// keyed per (table, type) like warnedBodylessMints, so one entry type cannot silence another's stack
|
|
127
|
+
const warnedPendingPreviousVersion = new Set<string>();
|
|
119
128
|
const FLOAT_TARGET = new Float64Array(1);
|
|
120
129
|
const FLOAT_BUFFER = new Uint8Array(FLOAT_TARGET.buffer);
|
|
130
|
+
/**
|
|
131
|
+
* Key of this database's audit retention floor — the staleness horizon `getAuditFloor` reports.
|
|
132
|
+
* Its *presence* is what marks the floor trustworthy, which is why it is not the `last-removed`
|
|
133
|
+
* marker above: that one is written after its removals and by only one of the five prune paths, so a
|
|
134
|
+
* value found there cannot be told apart from one carrying the write-ahead and monotonicity
|
|
135
|
+
* guarantees `raiseAuditFloor` provides. The two coexist deliberately and answer different
|
|
136
|
+
* questions.
|
|
137
|
+
*/
|
|
138
|
+
const AUDIT_FLOOR_KEY = Symbol.for('audit-floor');
|
|
139
|
+
// The epoch `establishAuditFloor` stamped, never raised or removed. Its PRESENCE marks the floor as
|
|
140
|
+
// unverified provenance — a guess bounded by what survived, blind to history a legacy prune removed
|
|
141
|
+
// before tracking began — until a database generation (#2451) retires the mark. No comparison does:
|
|
142
|
+
// a later prune certifies only what it removed, so `floor > bootstrap` says nothing about the older
|
|
143
|
+
// gap. The value records how far the guess reached, for that repair. See `establishAuditFloor`.
|
|
144
|
+
const AUDIT_FLOOR_BOOTSTRAP_KEY = Symbol.for('audit-floor-bootstrap');
|
|
145
|
+
/**
|
|
146
|
+
* The floor's own eight bytes, deliberately NOT the FLOAT_TARGET/FLOAT_BUFFER pair the `last-removed`
|
|
147
|
+
* marker uses: decoding a floor writes into its buffer on every read, including the pre-check on each
|
|
148
|
+
* prune, while `updateLastRemoved` hands the shared buffer to an async `put` it has not yet consumed.
|
|
149
|
+
* Sharing them would let a floor read rewrite a marker still in flight.
|
|
150
|
+
*/
|
|
151
|
+
const FLOOR_TARGET = new Float64Array(1);
|
|
152
|
+
const FLOOR_BUFFER = new Uint8Array(FLOOR_TARGET.buffer);
|
|
153
|
+
/** No trustworthy floor: the highest possible floor, so every cursor compares as stale. */
|
|
154
|
+
const AUDIT_FLOOR_UNKNOWN = Infinity;
|
|
121
155
|
|
|
122
156
|
/** Last resort on a detached path: a failing log sink must not itself become an unhandled rejection. */
|
|
123
157
|
function warnContained(message: string, error: unknown) {
|
|
@@ -149,13 +183,14 @@ export function openAuditStore(rootStore) {
|
|
|
149
183
|
auditStore.getRange = function (options) {
|
|
150
184
|
if (options.values === false) return superGetRange(options); // getKeys shouldn't be modified
|
|
151
185
|
return superGetRange(options).map(({ key, value }) => {
|
|
152
|
-
value.key = value.
|
|
186
|
+
value.key = value.txnLogKey = key;
|
|
153
187
|
return value;
|
|
154
188
|
});
|
|
155
189
|
};
|
|
156
190
|
}
|
|
157
191
|
rootStore.auditStore = auditStore;
|
|
158
192
|
auditStore.rootStore = rootStore;
|
|
193
|
+
establishAuditFloor(auditStore);
|
|
159
194
|
auditStore.tableStores = [];
|
|
160
195
|
const deleteCallbacks = [];
|
|
161
196
|
auditStore.addDeleteRemovalCallback = function (tableId, table, callback) {
|
|
@@ -165,6 +200,7 @@ export function openAuditStore(rootStore) {
|
|
|
165
200
|
return {
|
|
166
201
|
remove() {
|
|
167
202
|
delete deleteCallbacks[tableId];
|
|
203
|
+
if (auditStore.tableStores[tableId] === table) delete auditStore.tableStores[tableId];
|
|
168
204
|
},
|
|
169
205
|
};
|
|
170
206
|
};
|
|
@@ -228,25 +264,39 @@ export function openAuditStore(rootStore) {
|
|
|
228
264
|
let lastKey: any;
|
|
229
265
|
try {
|
|
230
266
|
if (isRocksAuditStore) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
267
|
+
const before = retentionCutoff(1 + passCleanupPriority * passCleanupPriority);
|
|
268
|
+
raiseAuditFloor(auditStore, before);
|
|
269
|
+
auditStore.rootStore.purgeLogs({ before });
|
|
234
270
|
} else {
|
|
235
271
|
// Driven explicitly rather than with for-of: this loop suspends on the awaits below, and a
|
|
236
272
|
// close landing mid-pass closes the env under it. for-of calls next() before the body, so a
|
|
237
273
|
// check inside the body advances the cursor first — the guard has to precede every next().
|
|
274
|
+
// remove up until the audit retention time, reducing audit retention time if cleanup is higher priority
|
|
275
|
+
const end = retentionCutoff(1 + passCleanupPriority * passCleanupPriority);
|
|
276
|
+
// Probe before raising, so an idle database does not write a floor transaction on every
|
|
277
|
+
// pass forever. `end` is fixed and audit keys only move forward, so an empty probe means
|
|
278
|
+
// the loop below finds nothing either.
|
|
238
279
|
const entries = auditStore
|
|
239
280
|
.getRange({
|
|
240
281
|
start: 1, // must not be zero or it will be interpreted as null and overlap with symbols in search
|
|
241
282
|
snapshot: false,
|
|
242
|
-
end
|
|
283
|
+
end,
|
|
243
284
|
})
|
|
244
285
|
[Symbol.iterator]();
|
|
245
286
|
try {
|
|
287
|
+
// Raised off the first eligible entry rather than a separate probe range: one cursor,
|
|
288
|
+
// so a pass over an idle database writes no floor at all, and nothing else observing
|
|
289
|
+
// this range sees an extra advance. Still strictly before any removal — see
|
|
290
|
+
// raiseAuditFloor.
|
|
291
|
+
let floorRaised = false;
|
|
246
292
|
while (!cleanupStopped && !storeClosing()) {
|
|
247
293
|
const entry = entries.next();
|
|
248
294
|
if (entry.done) break;
|
|
249
295
|
const auditRecord = entry.value;
|
|
296
|
+
if (!floorRaised) {
|
|
297
|
+
raiseAuditFloor(auditStore, end);
|
|
298
|
+
floorRaised = true;
|
|
299
|
+
}
|
|
250
300
|
try {
|
|
251
301
|
// awaited so a rejection (not just a synchronous throw) is caught here instead of
|
|
252
302
|
// escaping as an unhandled rejection once a later iteration's promise replaces this one
|
|
@@ -321,7 +371,7 @@ export function openAuditStore(rootStore) {
|
|
|
321
371
|
if (
|
|
322
372
|
!cleanupStopped &&
|
|
323
373
|
!storeClosing() &&
|
|
324
|
-
(!isRocksAuditStore || (
|
|
374
|
+
(!isRocksAuditStore || (ownsStoreMaintenance(rootStore.path) && !pendingCleanupResolve))
|
|
325
375
|
) {
|
|
326
376
|
scheduleAuditCleanup();
|
|
327
377
|
}
|
|
@@ -357,7 +407,7 @@ export function openAuditStore(rootStore) {
|
|
|
357
407
|
pendingCleanupResolve = null;
|
|
358
408
|
return lastCleanupResolution ?? Promise.resolve();
|
|
359
409
|
};
|
|
360
|
-
if (
|
|
410
|
+
if (ownsStoreMaintenance(rootStore.path)) {
|
|
361
411
|
scheduleAuditCleanup();
|
|
362
412
|
}
|
|
363
413
|
if (getWorkerIndex() === 0 && !timestampErrored) {
|
|
@@ -374,16 +424,46 @@ export function openAuditStore(rootStore) {
|
|
|
374
424
|
return auditStore;
|
|
375
425
|
}
|
|
376
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Whether `entry` is the very write `auditRecord` describes. Write identity is (origin node, log key),
|
|
429
|
+
* never the record version: a version is legitimately non-unique, so a version match can name a
|
|
430
|
+
* different write and authorize destroying live state (a tombstone, a still-referenced blob).
|
|
431
|
+
*
|
|
432
|
+
* On LMDB this is exact — the audit-store key IS the record's `txnLogKey`. On RocksDB a record stores
|
|
433
|
+
* its version in the compatibility word and keeps a divergent log key in `additionalAuditRefs`.
|
|
434
|
+
* Absent identity answers false. The direction is deliberate: uncertainty retains.
|
|
435
|
+
*/
|
|
436
|
+
export function isAuditEntryWrite(entry: any, auditRecord: AuditRecord): boolean {
|
|
437
|
+
if (entry == null || auditRecord.txnLogKey == null) return false;
|
|
438
|
+
const auditNodeId = auditRecord.nodeId ?? 0;
|
|
439
|
+
return (
|
|
440
|
+
(entry.localTime === auditRecord.txnLogKey && (entry.nodeId ?? 0) === auditNodeId) ||
|
|
441
|
+
entry.additionalAuditRefs?.some(
|
|
442
|
+
(ref) => ref.version === auditRecord.txnLogKey && (ref.nodeId ?? 0) === auditNodeId
|
|
443
|
+
) === true
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
377
447
|
export function removeAuditEntry(auditStore: any, auditRecord: AuditRecord): Promise<void> {
|
|
448
|
+
let tombstoneRemoval: Promise<void> | undefined;
|
|
378
449
|
if (auditRecord.type === 'delete') {
|
|
379
450
|
// if this is a delete, we remove the delete entry from the primary table
|
|
380
|
-
// at the same time so the audit table the primary table are in sync, assuming the entry
|
|
451
|
+
// at the same time so the audit table the primary table are in sync, assuming the entry is still
|
|
452
|
+
// the record state this audit record wrote
|
|
381
453
|
const tableId = auditRecord.tableId;
|
|
382
454
|
const primaryStore = auditStore.tableStores[auditRecord.tableId];
|
|
383
|
-
|
|
384
|
-
|
|
455
|
+
const tombstone = primaryStore?.getEntry(auditRecord.recordId);
|
|
456
|
+
if (isAuditEntryWrite(tombstone, auditRecord))
|
|
457
|
+
// a failed tombstone removal doesn't mean the audit entry removal failed — only
|
|
458
|
+
// auditStore.remove() below decides this function's outcome
|
|
459
|
+
tombstoneRemoval = new Promise<void>((resolve) => {
|
|
460
|
+
resolve(auditStore.deleteCallbacks?.[tableId]?.(auditRecord.recordId, tombstone.version));
|
|
461
|
+
}).catch((error) => {
|
|
462
|
+
harperLogger.warn('Error removing deleted record while removing its audit entry', error);
|
|
463
|
+
});
|
|
385
464
|
}
|
|
386
|
-
|
|
465
|
+
const auditRemoval = auditStore.remove(auditRecord.key);
|
|
466
|
+
return tombstoneRemoval ? Promise.all([tombstoneRemoval, auditRemoval]).then(() => undefined) : auditRemoval;
|
|
387
467
|
}
|
|
388
468
|
|
|
389
469
|
function updateLastRemoved(auditStore, lastKey) {
|
|
@@ -398,11 +478,377 @@ export function getLastRemoved(auditStore) {
|
|
|
398
478
|
return FLOAT_TARGET[0];
|
|
399
479
|
}
|
|
400
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Read the recorded floor, normalizing anything we cannot trust to AUDIT_FLOOR_UNKNOWN. Reads bytes
|
|
483
|
+
* rather than going through the store's value decoder, which would read these eight raw float bytes
|
|
484
|
+
* as an audit entry (and as msgpack on RocksDB). Callers get a number in every case: a NaN or
|
|
485
|
+
* negative floor read as a number would make one of `cursor >= floor` / `cursor < floor` report
|
|
486
|
+
* safety, and which of the two a consumer writes must not decide whether corrupt metadata fails
|
|
487
|
+
* closed.
|
|
488
|
+
*/
|
|
489
|
+
function decodeAuditFloor(stored: any): number {
|
|
490
|
+
// RocksDB's getBinarySync is typed to also return a length; anything that is not exactly the
|
|
491
|
+
// eight bytes we write is metadata written by something else.
|
|
492
|
+
if (stored?.byteLength !== 8) return AUDIT_FLOOR_UNKNOWN;
|
|
493
|
+
FLOOR_BUFFER.set(stored);
|
|
494
|
+
const floor = FLOOR_TARGET[0];
|
|
495
|
+
// `Object.is` for -0, which passes `>= 0` and would then read as a permissive zero — every cursor
|
|
496
|
+
// safe — from bytes with the sign bit set that nothing here writes. raiseAuditFloor rejects the
|
|
497
|
+
// same value as a cutoff; the read side has to agree or corrupt metadata fails open.
|
|
498
|
+
if (!Number.isFinite(floor) || floor < 0 || Object.is(floor, -0)) return AUDIT_FLOOR_UNKNOWN;
|
|
499
|
+
return floor;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Did the floor write land? A record has to be PRESENT, not merely decode to the value we wrote:
|
|
504
|
+
* `decodeAuditFloor(undefined)` is the unknown sentinel too, so on a floorless store — where the
|
|
505
|
+
* resolver writes exactly that sentinel — comparing decoded values alone reported a commit for a
|
|
506
|
+
* write that never happened, and the caller pruned with nothing persisted.
|
|
507
|
+
*/
|
|
508
|
+
function floorWriteLanded(stored: any, floor: number): boolean {
|
|
509
|
+
return stored !== undefined && decodeAuditFloor(stored) === floor;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Own eight bytes per write: the store must never be handed a live view of the reused module buffer. */
|
|
513
|
+
function encodeAuditFloor(floor: number): Uint8Array {
|
|
514
|
+
FLOOR_TARGET[0] = floor;
|
|
515
|
+
return FLOOR_BUFFER.slice();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Read-modify-write the floor under one store transaction. `resolve` receives the recorded floor
|
|
520
|
+
* (AUDIT_FLOOR_UNKNOWN when there is none) and returns the value to store, or undefined to leave it
|
|
521
|
+
* alone. `key` selects the record: the floor itself, or the bootstrap-provenance record beside it,
|
|
522
|
+
* which wants the same verified commit rather than a second write path.
|
|
523
|
+
*
|
|
524
|
+
* The transaction is the point: several paths advance the floor from different workers, and two
|
|
525
|
+
* unsynchronized read-then-writes can interleave so the lower cutoff lands last — a floor below
|
|
526
|
+
* history the higher one already removed. Read-only mode writes nothing, and prunes nothing.
|
|
527
|
+
*/
|
|
528
|
+
function updateAuditFloor(
|
|
529
|
+
auditStore: any,
|
|
530
|
+
resolve: (current: number, recorded: boolean) => number | undefined,
|
|
531
|
+
key: symbol = AUDIT_FLOOR_KEY
|
|
532
|
+
): void {
|
|
533
|
+
// A legacy `auditPath` layout is opened as its own standalone LMDB root (databases.ts) and has no
|
|
534
|
+
// `.rootStore`, so it owns the transaction itself.
|
|
535
|
+
const transactionOwner = auditStore?.rootStore ?? auditStore;
|
|
536
|
+
if (!transactionOwner?.transactionSync)
|
|
537
|
+
throw new Error('Cannot record the audit retention floor: this database has no audit store');
|
|
538
|
+
// Both branches read their own write back and report `false` on mismatch, and the caller demands an
|
|
539
|
+
// explicit `true`. Both halves are load-bearing: a RocksDB transactionSync returns undefined for a
|
|
540
|
+
// swallowed abort rather than throwing (see RecordEncoder.saveStructures), and a write that fails
|
|
541
|
+
// without throwing is otherwise indistinguishable from one that landed — a caller pruning against a
|
|
542
|
+
// floor never recorded. Reads inside a write transaction see their own writes on both engines, so
|
|
543
|
+
// the read-back observes what commit will make durable.
|
|
544
|
+
const committed =
|
|
545
|
+
auditStore instanceof RocksTransactionLogStore
|
|
546
|
+
? transactionOwner.transactionSync(
|
|
547
|
+
(txn) => {
|
|
548
|
+
const stored = txn.getBinarySync(key);
|
|
549
|
+
const floor = resolve(decodeAuditFloor(stored), stored !== undefined);
|
|
550
|
+
if (floor !== undefined) {
|
|
551
|
+
txn.putSync(key, asBinary(encodeAuditFloor(floor)));
|
|
552
|
+
if (!floorWriteLanded(txn.getBinarySync(key), floor)) return false;
|
|
553
|
+
}
|
|
554
|
+
return true;
|
|
555
|
+
},
|
|
556
|
+
{ retryOnBusy: true }
|
|
557
|
+
)
|
|
558
|
+
: transactionOwner.transactionSync(() => {
|
|
559
|
+
const stored = auditStore.getBinary(key);
|
|
560
|
+
const floor = resolve(decodeAuditFloor(stored), stored !== undefined);
|
|
561
|
+
// `put` rather than `putSync`, and inside the transaction: lmdb's putSync is
|
|
562
|
+
// `put(...) === SYNC_PROMISE_SUCCESS`, so it drops whatever put returns, and a rejected put
|
|
563
|
+
// would leak with no owner. Within a write transaction put writes synchronously and returns
|
|
564
|
+
// an already-resolved sentinel, so the value is visible immediately either way and this
|
|
565
|
+
// only takes ownership of the failure case.
|
|
566
|
+
// asBinary: a legacy standalone audit root's encoder has no Uint8Array passthrough, so raw
|
|
567
|
+
// bytes would reach createAuditEntry and throw. This bypasses both encoders.
|
|
568
|
+
if (floor !== undefined) {
|
|
569
|
+
auditStore
|
|
570
|
+
.put(key, asBinary(encodeAuditFloor(floor)))
|
|
571
|
+
?.catch?.((error) => warnContained('Error writing the audit retention floor', error));
|
|
572
|
+
if (!floorWriteLanded(auditStore.getBinary(key), floor)) return false;
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
});
|
|
576
|
+
if (committed !== true) throw new Error('The audit retention floor transaction did not commit');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The bound a prune should use — and record — in place of an unbounded cutoff.
|
|
581
|
+
*
|
|
582
|
+
* `Infinity` is a legitimate thing for a caller to *mean* ("remove all of it") and a ruinous thing to
|
|
583
|
+
* store: it is the unknown sentinel, and the sentinel is absorbing. `raiseAuditFloor`'s lock-free
|
|
584
|
+
* pre-check skips any present record no cutoff exceeds, and `establishAuditFloor` skips any store that
|
|
585
|
+
* has one, so a floor at `Infinity` never comes back down — for the whole database, including sibling
|
|
586
|
+
* tables whose own history was never touched. One `deleteHistory(Infinity)` would otherwise retire the
|
|
587
|
+
* accessor for that database permanently (#2458).
|
|
588
|
+
*
|
|
589
|
+
* So bound it by what exists: strictly above the newest key currently in the log, and never below the
|
|
590
|
+
* clock. Nothing already written can escape that bound, and a caller that passes it as the prune's
|
|
591
|
+
* range end as well as its floor cannot remove anything the floor does not cover — which is what makes
|
|
592
|
+
* the write-ahead ordering hold without an infinite bound. An entry written *after* this returns is
|
|
593
|
+
* simply not history the call asked to remove.
|
|
594
|
+
*
|
|
595
|
+
* `Infinity` is only the extreme case. Any cutoff above this bound is the same defect by degree: a
|
|
596
|
+
* finite year-2286 bound (`Date.now() * 1000`, or a bare `'9999999999999'`) is equally unreachable
|
|
597
|
+
* and equally permanent, and entries written after the prune then land *below* the recorded floor,
|
|
598
|
+
* so the floor's promise — nothing after it was pruned — is false about history that is still there.
|
|
599
|
+
* Every cutoff is therefore clamped, not just the unbounded one.
|
|
600
|
+
*
|
|
601
|
+
* Non-numbers, NaN, negatives and `-0` fall through unchanged to `raiseAuditFloor`, which rejects
|
|
602
|
+
* them: the numeric ones are ordered keys the prune range would honor, not bounds anyone meant, and a
|
|
603
|
+
* non-number is guarded explicitly because `>` would otherwise coerce it into a bound the floor
|
|
604
|
+
* accepts. `cutoff > bound` rather than `Math.min` keeps NaN falling through too.
|
|
605
|
+
*
|
|
606
|
+
* On RocksDB `getKeys` is unimplemented and returns `[]`, so the bound reduces to `Date.now()`. A
|
|
607
|
+
* key above the clock therefore survives a purge that asked for it on that engine. That is the safe
|
|
608
|
+
* direction: the entry is kept and the floor stays honest, where the alternative removes history the
|
|
609
|
+
* floor does not cover.
|
|
610
|
+
*/
|
|
611
|
+
export function boundedAuditPruneEnd(auditStore: any, cutoff: number): number {
|
|
612
|
+
// Non-numbers pass through untouched for `raiseAuditFloor` to reject. `>` coerces, so without this
|
|
613
|
+
// a numeric STRING ('9999999999999', 'Infinity') or a future Date compares true against the bound
|
|
614
|
+
// and comes back AS the bound — a number raiseAuditFloor accepts — turning a type error into a
|
|
615
|
+
// whole-log prune.
|
|
616
|
+
if (typeof cutoff !== 'number') return cutoff;
|
|
617
|
+
let bound = Date.now();
|
|
618
|
+
for (const newest of auditStore.getKeys({ reverse: true, limit: 1 })) {
|
|
619
|
+
if (typeof newest === 'number' && newest >= bound) bound = newest + 1;
|
|
620
|
+
}
|
|
621
|
+
return cutoff > bound ? bound : cutoff;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Raise the floor to `cutoff`, the exclusive lower bound of the history a prune is about to make
|
|
626
|
+
* unreachable.
|
|
627
|
+
*
|
|
628
|
+
* **Call this before removing anything.** A floor written after the removal is lost if the process
|
|
629
|
+
* dies in between, and the surviving lower floor then certifies a cursor whose history is gone.
|
|
630
|
+
* Ordering it first also covers a prune that removes less than `cutoff` spans (a RocksDB purge with
|
|
631
|
+
* no whole droppable file, a retention pass stopping at MAX_DELETES_PER_CLEANUP): over-reporting costs
|
|
632
|
+
* one unnecessary resync, under-reporting loses data silently. The retention paths bound that
|
|
633
|
+
* over-report at the configured horizon (`Date.now() - auditRetention`, never below 0); the two
|
|
634
|
+
* operator-supplied bounds have no ceiling of their own and go through `boundedAuditPruneEnd` first,
|
|
635
|
+
* since a bound above everything reachable would be recorded verbatim and never come down.
|
|
636
|
+
*
|
|
637
|
+
* Throws if the floor cannot be persisted, which is why it is called first — the throw is what stops
|
|
638
|
+
* the prune from proceeding unrecorded. Never lowers the floor, so a narrower prune cannot undo a
|
|
639
|
+
* wider one, and a store whose floor is unknown stays unknown rather than being talked down to a
|
|
640
|
+
* cutoff that says nothing about the history it has already lost.
|
|
641
|
+
*/
|
|
642
|
+
export function raiseAuditFloor(auditStore: any, cutoff: number): void {
|
|
643
|
+
// Throw rather than no-op on a bound we will not store: audit keys are raw float64, so NaN and
|
|
644
|
+
// negatives (sign bit set) sort ABOVE every real timestamp and a range ending there spans the whole
|
|
645
|
+
// log — declining the floor silently would leave the prune deleting everything. `-0` and a non-number
|
|
646
|
+
// slip past a naive `< 0` check but are still ordered keys the range honors, so they are rejected too.
|
|
647
|
+
// Infinity is accepted and stored, decoding back to "unknown": every production caller clamps before
|
|
648
|
+
// reaching here (`boundedAuditPruneEnd`, the bridge's 400), so it stays reachable only for a caller
|
|
649
|
+
// that genuinely cannot bound its prune.
|
|
650
|
+
if (typeof cutoff !== 'number' || Number.isNaN(cutoff) || cutoff < 0 || Object.is(cutoff, -0))
|
|
651
|
+
throw new Error(`Invalid audit prune bound: ${String(cutoff)}`);
|
|
652
|
+
// Read-only mode does not exempt a prune from recording its floor; it means the prune must not
|
|
653
|
+
// happen. Only scheduleAuditCleanup and purgeAgedLogs check read-only themselves, so for
|
|
654
|
+
// deleteHistory and the whole-database purge this throw is the guard.
|
|
655
|
+
if (isReadOnlyMode()) throw new Error('Cannot record the audit retention floor: the database is read-only');
|
|
656
|
+
// Lock-free pre-check, getBinary-guarded so this optimization never decides the error a store with
|
|
657
|
+
// no audit store reports. Most calls cannot move the floor (a RocksDB reclamation pass on an idle
|
|
658
|
+
// database, a cutoff below one a wider prune already set), and taking the env write lock to
|
|
659
|
+
// discover that serializes every worker's boot and reclamation on it. The in-transaction guards
|
|
660
|
+
// below stay authoritative.
|
|
661
|
+
// Skips only the case it can prove is a no-op: a record that exists and already sits at or above
|
|
662
|
+
// the cutoff. An absent record is NOT decided here — the presence question is settled inside the
|
|
663
|
+
// transaction below, because another worker's establishAuditFloor can land between this read and
|
|
664
|
+
// that write.
|
|
665
|
+
if (auditStore?.getBinary) {
|
|
666
|
+
const stored = auditStore.getBinary(AUDIT_FLOOR_KEY);
|
|
667
|
+
if (stored !== undefined && !(cutoff > decodeAuditFloor(stored))) return;
|
|
668
|
+
}
|
|
669
|
+
updateAuditFloor(auditStore, (current, recorded) => {
|
|
670
|
+
// Still no record, and we are about to prune: persist the unknown sentinel. Leaving no marker
|
|
671
|
+
// lets the next open stamp a FINITE epoch, and a prune bound above that epoch (a future
|
|
672
|
+
// `endTime`, or a rolled-back clock) then certifies cursors whose history this prune deleted.
|
|
673
|
+
// Unknown is the honest value, because a store with no record may have been pruned before this
|
|
674
|
+
// run too.
|
|
675
|
+
if (!recorded) return AUDIT_FLOOR_UNKNOWN;
|
|
676
|
+
// A record appeared while we were getting here, so this is an ordinary monotonic raise: pruning
|
|
677
|
+
// to `cutoff` against a floor left below it is exactly the silent gap this function prevents.
|
|
678
|
+
return cutoff > current ? cutoff : undefined;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Give a database a trustworthy floor if it has none: the current time, as a one-time resync epoch.
|
|
684
|
+
*
|
|
685
|
+
* The floor record's *presence* is the trust marker, so a store without one is a store whose
|
|
686
|
+
* retention history we cannot account for. It may have been pruned by a version that recorded no
|
|
687
|
+
* floor; it may be the empty audit store an LMDB→RocksDB migration deliberately leaves behind
|
|
688
|
+
* (`bin/copyDb.ts` does not migrate it, so the records and their resumable cursors outlive their
|
|
689
|
+
* history); or it may be a database restored from a table-scoped backup taken without
|
|
690
|
+
* `include_audit`, which carries records but no audit DBI. Cursors from before this moment are
|
|
691
|
+
* therefore reported stale — not because we know they are, but because we do not know they are not.
|
|
692
|
+
*
|
|
693
|
+
* There is no "brand new store, use a permissive baseline" case: creating the audit DBI proves only
|
|
694
|
+
* that the DBI was absent, which the audit-less backup above also produces. Being conservative on a
|
|
695
|
+
* genuinely new database costs nothing, since its entries are all written after this instant.
|
|
696
|
+
*
|
|
697
|
+
* In read-only mode nothing is written and the floor stays unknown — the fail-closed answer for a
|
|
698
|
+
* process that cannot record what it does not know.
|
|
699
|
+
*
|
|
700
|
+
* **The epoch is also recorded under its own key, so this bootstrap is repairable.** The epoch is a
|
|
701
|
+
* guess bounded by surviving state, and surviving state cannot see history a selective prune already
|
|
702
|
+
* removed (see the clock note below). The record marks the store as one that carried a guess, and
|
|
703
|
+
* preserves the value guessed — the two facts a later release needs to raise such a floor to
|
|
704
|
+
* something it can stand behind. See "Audit retention floor" in DESIGN.md for the full reading.
|
|
705
|
+
*
|
|
706
|
+
* **The mark is the signal, not a comparison against the floor.** A store carrying this record has an
|
|
707
|
+
* unverified pre-tracking window for as long as it exists, however far the floor has since moved: a
|
|
708
|
+
* prune raising the floor above the epoch certifies only what that prune removed, and says nothing
|
|
709
|
+
* about history removed before tracking began — which may sit above the epoch, since that is exactly
|
|
710
|
+
* the case the guess cannot see. Retiring the mark takes a database generation (harper#2451), not a
|
|
711
|
+
* floor that has climbed past it.
|
|
712
|
+
*
|
|
713
|
+
* Ordering. The provenance record is written **first**, so a crash between the two writes leaves a
|
|
714
|
+
* record with no floor — which the next open retries, since the early return above tests the floor.
|
|
715
|
+
* The epoch actually stamped is always read back from the record rather than taken from this call's
|
|
716
|
+
* own `Date.now()`, so a worker whose record lost the race adopts the winner's value and the two
|
|
717
|
+
* always agree. Re-adopting an older record is sound: nothing was pruned in the meantime, or a prune
|
|
718
|
+
* would have written the floor this function returns early on.
|
|
719
|
+
*/
|
|
720
|
+
export function establishAuditFloor(auditStore: any): void {
|
|
721
|
+
if (isReadOnlyMode()) return;
|
|
722
|
+
// Every read and write in here is inside the try: the contract is that a database open never fails
|
|
723
|
+
// over this metadata, and a throwing getBinary/getKeys would escape to initStores just as a
|
|
724
|
+
// throwing write would.
|
|
725
|
+
try {
|
|
726
|
+
// Absence of the record, not `getAuditFloor() === AUDIT_FLOOR_UNKNOWN`: a record that exists but
|
|
727
|
+
// decodes to unknown — corrupt bytes, or the Infinity a prune-everything stored — is already the
|
|
728
|
+
// fail-closed answer, and stamping over it would LOWER a floor that is never supposed to lower.
|
|
729
|
+
// The check is repeated inside the transaction (the `recorded` argument), because between this
|
|
730
|
+
// read and that write another worker's prune can store exactly such a value; this read only keeps
|
|
731
|
+
// the common case — a floor already established, every worker, every database, every boot — off
|
|
732
|
+
// the env write lock.
|
|
733
|
+
if (auditStore.getBinary(AUDIT_FLOOR_KEY) !== undefined) return;
|
|
734
|
+
// Not bare Date.now(): a clock that has rolled back would bootstrap a floor BELOW history this
|
|
735
|
+
// database may already have pruned, certifying a stale cursor. The newest retained entry is a
|
|
736
|
+
// lower bound the clock cannot argue with — everything at or above it is demonstrably still here —
|
|
737
|
+
// so take whichever is later.
|
|
738
|
+
//
|
|
739
|
+
// It narrows that hole; it does not close it, because the bound covers what SURVIVES rather than
|
|
740
|
+
// what existed. A legacy `deleteHistory` removes one table's entries below its endTime out of the
|
|
741
|
+
// shared log, so a table whose entries were the newest and all fell below that bound leaves the
|
|
742
|
+
// log's newest survivor being a sibling's OLDER entry — removed history above every surviving key.
|
|
743
|
+
// A clock rolled back to between the two then stamps an epoch below entries that are gone, and a
|
|
744
|
+
// cursor in that window resumes over the gap (#2458). Also unclosed:
|
|
745
|
+
// RocksTransactionLogStore.getKeys() is unimplemented, so on that engine this reduces to
|
|
746
|
+
// Date.now() outright.
|
|
747
|
+
//
|
|
748
|
+
// Neither is a reason to refuse to stamp — the unknown sentinel is absorbing, so that would make
|
|
749
|
+
// every upgraded deployment fail closed forever. They are the reason
|
|
750
|
+
// the guess is RECORDED as a guess: written first, so it cannot be lost behind a floor that
|
|
751
|
+
// outlives it, and left in place afterwards so the repair reading stays available.
|
|
752
|
+
let fresh = Date.now();
|
|
753
|
+
for (const newest of auditStore.getKeys({ reverse: true, limit: 1 })) {
|
|
754
|
+
if (typeof newest === 'number' && newest > fresh) fresh = newest;
|
|
755
|
+
}
|
|
756
|
+
// A READABLE record is kept, so the epoch read back below is whatever landed first and the floor
|
|
757
|
+
// always matches it. An unreadable one is replaced: unlike the floor, where a present record may
|
|
758
|
+
// be a deliberate AUDIT_FLOOR_UNKNOWN and overwriting it would lower a floor, this record is only
|
|
759
|
+
// a comparison basis, and undecodable bytes carry nothing worth keeping. Declining to replace
|
|
760
|
+
// them pinned the store's floor to unknown forever — the resolver would skip the write on every
|
|
761
|
+
// later open, the read back would fail identically, and no retry could ever succeed, which is the
|
|
762
|
+
// fail-closed-forever state this bootstrap exists to avoid.
|
|
763
|
+
updateAuditFloor(
|
|
764
|
+
auditStore,
|
|
765
|
+
(current, recorded) => (recorded && Number.isFinite(current) ? undefined : fresh),
|
|
766
|
+
AUDIT_FLOOR_BOOTSTRAP_KEY
|
|
767
|
+
);
|
|
768
|
+
const epoch = decodeAuditFloor(auditStore.getBinary(AUDIT_FLOOR_BOOTSTRAP_KEY));
|
|
769
|
+
// Never stamp a floor whose provenance cannot be read: that is the one state a later repair cannot
|
|
770
|
+
// act on. Unreachable in principle, since `updateAuditFloor` throws if its write did not land.
|
|
771
|
+
if (!Number.isFinite(epoch)) return;
|
|
772
|
+
updateAuditFloor(auditStore, (_current, recorded) => (recorded ? undefined : epoch));
|
|
773
|
+
} catch (error) {
|
|
774
|
+
// An unrecorded floor already reads as unknown, which is the fail-closed answer; aborting startup
|
|
775
|
+
// instead would turn a metadata failure into an outage. The next open retries.
|
|
776
|
+
warnContained('Error initializing the audit retention floor', error);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* The floor of this database's retained audit history: every audit entry at or after the returned
|
|
782
|
+
* time is still retained, so a consumer whose last-processed audit-log cursor is `>=` it can resume
|
|
783
|
+
* incrementally, and one below it must resync — it may have lost nothing, but the floor cannot
|
|
784
|
+
* certify it either way. Returns `Infinity` when
|
|
785
|
+
* the floor is unknown, which fails closed — no cursor compares as safe.
|
|
786
|
+
*
|
|
787
|
+
* **One exception, and it is the only one: history removed before the floor existed.** Every prune
|
|
788
|
+
* that runs with a floor recorded is covered — it raises the floor first, so it cannot remove an entry
|
|
789
|
+
* the floor does not cover. But the first open stamps a floor from what *survives*
|
|
790
|
+
* (`establishAuditFloor`), and a legacy `Table.deleteHistory` that removed a table's newest entries,
|
|
791
|
+
* followed by a clock rollback, leaves that stamp below history that is gone; a cursor in the window
|
|
792
|
+
* is then certified over the gap. So the guarantee is one-directional for tracked prunes and silent
|
|
793
|
+
* about untracked ones. Only a database generation can close that (harper#2451).
|
|
794
|
+
*
|
|
795
|
+
* The time domain is the audit-log key: what `subscribe`'s events carry as `localTime` and what MQTT
|
|
796
|
+
* durable sessions persist as `startTime`, so those compare against the floor directly.
|
|
797
|
+
* **`getHistory` is not in that domain** — it reports each entry's origin `version` under the name
|
|
798
|
+
* `localTime`, which a backdated or replicated write makes differ from the audit-log key. A cursor
|
|
799
|
+
* saved from `getHistory` is not comparable to this floor.
|
|
800
|
+
*
|
|
801
|
+
* **Database-scoped**, and deliberately conservative: the audit store is per-database and its
|
|
802
|
+
* entries carry a `tableId`, so a per-table floor would need a scan for the first entry matching
|
|
803
|
+
* that table. For a valid cursor, `cursor >= floor` therefore means no entry of *any* table in the
|
|
804
|
+
* database was removed *after* the cursor. What it never promises is anything below the FLOOR — that
|
|
805
|
+
* history is exactly what a prune takes. Below the *cursor* is not the same set: for a cursor strictly
|
|
806
|
+
* above the floor, `[floor, cursor)` sits below the cursor and is still covered by the guarantee. `Table.deleteHistory`
|
|
807
|
+
* prunes one table out of that shared log and raises the whole database's floor, which can overstate
|
|
808
|
+
* the floor for its siblings.
|
|
809
|
+
*
|
|
810
|
+
* **A moment-in-time observation.** Retention can advance between this call and whatever the caller
|
|
811
|
+
* does with the answer, so a check-then-resume sequence has a window where the floor moves under it.
|
|
812
|
+
* Closing that requires validating the cursor inside the resume itself (harper#2448); until then a
|
|
813
|
+
* lost race degrades to the truncation that happens today, never to anything worse.
|
|
814
|
+
*
|
|
815
|
+
* **On RocksDB the floor tracks the configured horizon, not retained reality.** That branch purges at
|
|
816
|
+
* whole-log-file granularity, so it cannot know before the fact which entries a purge will drop, and
|
|
817
|
+
* the floor has to be written first — so every retention pass advances it to
|
|
818
|
+
* `Date.now() - auditRetention / (1 + priority²)` whether or not a file was dropped. Entries below
|
|
819
|
+
* that horizon are routinely still on disk, and a consumer holding a cursor among them is told to
|
|
820
|
+
* resync. Conservative in the one safe direction, and the reason the LMDB branch (which can see a
|
|
821
|
+
* single eligible entry) instead raises off the first one it finds.
|
|
822
|
+
*
|
|
823
|
+
* **Not covered: copying a database's state without its history.** `restore_backup` replaces a
|
|
824
|
+
* database with the backup's, floor and all, and a RocksDB checkpoint (a branch database) copies the
|
|
825
|
+
* floor record but no transaction logs — so in both cases a cursor from after the copy point sits
|
|
826
|
+
* above a floor that is present, and therefore trusted, for history that is not there. Nothing here
|
|
827
|
+
* can detect that on its own, and it is not the audit log's problem alone: the same copy rolls back
|
|
828
|
+
* record versions and per-node replication sequence state, so making this one field honest while
|
|
829
|
+
* those stay stale would not give a consumer a coherent answer. It needs a database-level epoch —
|
|
830
|
+
* harper#2451.
|
|
831
|
+
*/
|
|
832
|
+
export function getAuditFloor(auditStore: any): number {
|
|
833
|
+
return decodeAuditFloor(auditStore.getBinary(AUDIT_FLOOR_KEY));
|
|
834
|
+
}
|
|
401
835
|
export function setAuditRetention(retentionTime, defaultDelay = DEFAULT_AUDIT_CLEANUP_DELAY) {
|
|
402
836
|
auditRetention = retentionTime;
|
|
403
837
|
DEFAULT_AUDIT_CLEANUP_DELAY = defaultDelay;
|
|
404
838
|
}
|
|
405
839
|
|
|
840
|
+
/**
|
|
841
|
+
* The retention cutoff a prune uses: `Date.now() - auditRetention`, scaled down by `divisor` for a
|
|
842
|
+
* higher-priority pass, and never below 0. A retention above ~55.7 years (or `Infinity`, to keep
|
|
843
|
+
* logs indefinitely) would otherwise go negative, and a negative bound is not "nothing eligible" —
|
|
844
|
+
* `raiseAuditFloor` rejects it, so every boot purge and retention pass would warn and the floor
|
|
845
|
+
* would never be raised on that install. At 0 the pass is a harmless no-op: nothing sits before
|
|
846
|
+
* the epoch, and a floor that already exists is never lowered to it.
|
|
847
|
+
*/
|
|
848
|
+
function retentionCutoff(divisor = 1): number {
|
|
849
|
+
return Math.max(0, Date.now() - auditRetention / divisor);
|
|
850
|
+
}
|
|
851
|
+
|
|
406
852
|
/**
|
|
407
853
|
* One-shot purge of transaction-log files already older than the audit retention window,
|
|
408
854
|
* intended to run during startup/recovery before transaction-log replay. The steady-state
|
|
@@ -415,7 +861,10 @@ export function setAuditRetention(retentionTime, defaultDelay = DEFAULT_AUDIT_CL
|
|
|
415
861
|
export function purgeAgedLogs(rootStore: RocksDatabase): string[] {
|
|
416
862
|
// Mirror the read-only guard in scheduleAuditCleanup: never delete log files in read-only mode.
|
|
417
863
|
if (isReadOnlyMode()) return [];
|
|
418
|
-
|
|
864
|
+
const before = retentionCutoff();
|
|
865
|
+
// The audit store is reachable this early because initStores opens it before replayLogs runs this.
|
|
866
|
+
raiseAuditFloor((rootStore as any).auditStore, before);
|
|
867
|
+
return rootStore.purgeLogs({ before });
|
|
419
868
|
}
|
|
420
869
|
|
|
421
870
|
const HAS_RECORD = 16;
|
|
@@ -430,13 +879,31 @@ const STRUCTURES = 7;
|
|
|
430
879
|
// Whole-table "reload" marker: a control entry (no record) signalling that a table was bulk-reloaded
|
|
431
880
|
// and subscribers should re-read it. Used after a copyApply base copy, whose per-row snapshot writes
|
|
432
881
|
// carry no audit entry (harper-pro#489). The entry type lives in the low nibble of the action byte
|
|
433
|
-
// (decoded via `action & 0xf`); 1–7 are the record actions above, 8 is reload, leaving
|
|
434
|
-
// future actions.
|
|
882
|
+
// (decoded via `action & 0xf`); 1–7 are the record actions above, 8 is reload, 9 is eviction, leaving 10–15 free for
|
|
883
|
+
// future actions. Reload markers are always written LOCAL_ONLY so an unknown type never reaches a
|
|
884
|
+
// peer; the lock control entries below deliberately are not, and rely on the capability gate instead.
|
|
435
885
|
const RELOAD = 8;
|
|
886
|
+
const EVICT = 9;
|
|
436
887
|
export const ACTION_32_BIT = 14;
|
|
437
888
|
export const ACTION_64_BIT = 15;
|
|
438
889
|
/** Used to indicate we have received a remote local time update */
|
|
439
890
|
export const REMOTE_SEQUENCE_UPDATE = 11;
|
|
891
|
+
/**
|
|
892
|
+
* Cluster record-lock coordination (harper#483 Phase 1). This replicates — unlike the reload marker
|
|
893
|
+
* it is NOT `LOCAL_ONLY` — and carries a control payload rather than a record, so it is written with
|
|
894
|
+
* `recordId: null`: an entry sharing a real record's `(version, tableId, recordId, nodeId)` would be
|
|
895
|
+
* returned by `RocksTransactionLogStore.getSync` ahead of that record's own audit entry and make
|
|
896
|
+
* `_writeUpdate`'s keyed dedup drop the holder's write.
|
|
897
|
+
*
|
|
898
|
+
* Two entries exist: the release, and the barrier — a replicated no-op a member commits on request
|
|
899
|
+
* so its own log position can serve as the §7.2 recovery fence (harper#2625). Nibbles 9 and 10
|
|
900
|
+
* briefly held `lockRequest`/`lockGrant` for the Ricart–Agrawala arbitration rule that
|
|
901
|
+
* `docs/record-lock-ownership.md` replaces; that rule never shipped enabled, so they were retired
|
|
902
|
+
* rather than migrated — and 9 has since been taken by eviction. 10 is spare; 14/15 are the width
|
|
903
|
+
* flags.
|
|
904
|
+
*/
|
|
905
|
+
export const LOCK_RELEASE = 12;
|
|
906
|
+
export const LOCK_BARRIER = 13;
|
|
440
907
|
export const HAS_CURRENT_RESIDENCY_ID = 512;
|
|
441
908
|
export const HAS_PREVIOUS_RESIDENCY_ID = 1024;
|
|
442
909
|
export const HAS_ORIGINATING_OPERATION = 2048;
|
|
@@ -470,16 +937,83 @@ const EVENT_TYPES = {
|
|
|
470
937
|
[STRUCTURES]: 'structures',
|
|
471
938
|
reload: RELOAD,
|
|
472
939
|
[RELOAD]: 'reload',
|
|
940
|
+
evict: EVICT,
|
|
941
|
+
[EVICT]: 'evict',
|
|
473
942
|
remoteSequenceUpdate: REMOTE_SEQUENCE_UPDATE,
|
|
474
943
|
[REMOTE_SEQUENCE_UPDATE]: 'remoteSequenceUpdate',
|
|
944
|
+
lockRelease: LOCK_RELEASE | HAS_RECORD,
|
|
945
|
+
[LOCK_RELEASE]: 'lockRelease',
|
|
946
|
+
lockBarrier: LOCK_BARRIER | HAS_RECORD,
|
|
947
|
+
[LOCK_BARRIER]: 'lockBarrier',
|
|
475
948
|
};
|
|
949
|
+
/**
|
|
950
|
+
* The LMDB audit entry states the presence of its leading 8-byte previousVersion field with that
|
|
951
|
+
* field's own first byte. The same test is what harperdb 4.x's reader uses and what both versions'
|
|
952
|
+
* replication senders use to strip the field before framing an entry for the wire, so it is a
|
|
953
|
+
* cross-version contract, not a local convention: a field written with any other leading byte is
|
|
954
|
+
* skipped by every reader and shifts action/nodeId/tableId/recordId/version by 8. See harper#2247.
|
|
955
|
+
*/
|
|
956
|
+
const PREVIOUS_VERSION_FIRST_BYTE = 66;
|
|
957
|
+
/**
|
|
958
|
+
* Which first bytes can begin an action, and so cannot be a previousVersion field. Derived from what
|
|
959
|
+
* the encoding can express, not from the types defined today: nibbles 9-15 are reserved for future
|
|
960
|
+
* entry types, and a peer one version ahead must keep decoding rather than look corrupt here.
|
|
961
|
+
*/
|
|
962
|
+
const ACTION_FIRST_BYTE = new Uint8Array(256);
|
|
963
|
+
// single-byte form; 0 is not an entry type, so a zero low nibble cannot start one
|
|
964
|
+
for (let firstByte = 1; firstByte <= (HAS_RECORD | HAS_PARTIAL_RECORD | 0xf); firstByte++) {
|
|
965
|
+
if (firstByte & 0xf) ACTION_FIRST_BYTE[firstByte] = 1;
|
|
966
|
+
}
|
|
967
|
+
// extended form, written as `action | extendedType | 0xc0000000`; 0xff is the five-byte readInt form
|
|
968
|
+
for (let firstByte = 0xc0; firstByte < 0xff; firstByte++) ACTION_FIRST_BYTE[firstByte] = 1;
|
|
969
|
+
let knownActionFlags: number | undefined;
|
|
970
|
+
/**
|
|
971
|
+
* Whether an action word decodes wholly into what this version understands. Consulted only to accept
|
|
972
|
+
* or reject a legacy prefix recovery — never to validate a normally-framed entry, where an unknown
|
|
973
|
+
* future flag must stay forwards-compatible rather than corrupt. The mask is built on first use
|
|
974
|
+
* because HAS_STRUCTURE_UPDATE crosses the RecordEncoder import cycle: read at module scope it
|
|
975
|
+
* resolves to undefined whenever RecordEncoder is the cycle's entry, silently dropping that bit.
|
|
976
|
+
*/
|
|
977
|
+
function isDecodableAction(action: number) {
|
|
978
|
+
knownActionFlags ??=
|
|
979
|
+
0xf |
|
|
980
|
+
HAS_RECORD |
|
|
981
|
+
HAS_PARTIAL_RECORD |
|
|
982
|
+
HAS_STRUCTURE_UPDATE |
|
|
983
|
+
HAS_CURRENT_RESIDENCY_ID |
|
|
984
|
+
HAS_PREVIOUS_RESIDENCY_ID |
|
|
985
|
+
HAS_ORIGINATING_OPERATION |
|
|
986
|
+
HAS_EXPIRATION_EXTENDED_TYPE |
|
|
987
|
+
HAS_BLOBS |
|
|
988
|
+
HAS_ADDITIONAL_AUDIT_REFS |
|
|
989
|
+
LOCAL_ONLY;
|
|
990
|
+
return (action & 0xf) !== 0 && !(action & ~knownActionFlags);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Cluster lock coordination entries. They ride the replicated audit stream but describe no record,
|
|
995
|
+
* so every consumer that surfaces audit entries as record activity — subscriber fan-out, the
|
|
996
|
+
* `startTime` replay, the `previousCount` backfill, the replicated-event sink — must exclude them.
|
|
997
|
+
* An equality chain rather than a Set: this runs once per audit entry on the replay and fan-out
|
|
998
|
+
* paths, where the common answer is false on the first comparison.
|
|
999
|
+
*/
|
|
1000
|
+
export function isLockControlType(type: unknown): boolean {
|
|
1001
|
+
return type === 'lockRelease' || type === 'lockBarrier';
|
|
1002
|
+
}
|
|
476
1003
|
const ORIGINATING_OPERATIONS = {
|
|
477
1004
|
insert: 1,
|
|
478
1005
|
update: 2,
|
|
479
1006
|
upsert: 3,
|
|
1007
|
+
// `put` must be persisted, not inferred: the physical write type is also `put` for an `upsert`
|
|
1008
|
+
// that happened to create a record, so the history readers cannot tell the two apart from the
|
|
1009
|
+
// type alone. Without an id here the originating operation decoded as undefined, the readers fell
|
|
1010
|
+
// back to the physical type, and replication catch-up replayed a `put` as an `upsert` — patching
|
|
1011
|
+
// the replica and RETAINING attributes the source had removed.
|
|
1012
|
+
put: 4,
|
|
480
1013
|
1: 'insert',
|
|
481
1014
|
2: 'update',
|
|
482
1015
|
3: 'upsert',
|
|
1016
|
+
4: 'put',
|
|
483
1017
|
};
|
|
484
1018
|
|
|
485
1019
|
/**
|
|
@@ -531,12 +1065,43 @@ export function createAuditEntry(auditRecord: AuditRecord, start = 0) {
|
|
|
531
1065
|
}
|
|
532
1066
|
action &= ~HAS_RECORD;
|
|
533
1067
|
}
|
|
534
|
-
let
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1068
|
+
let hasPreviousVersion: boolean;
|
|
1069
|
+
if (start > 0) {
|
|
1070
|
+
// RocksTransactionLogStore states presence with its own prelude flag, derived from this same
|
|
1071
|
+
// truthiness, so this container's field stays unconditional and its value is unconstrained.
|
|
1072
|
+
hasPreviousVersion = !!previousVersion;
|
|
1073
|
+
if (hasPreviousVersion) ENTRY_DATAVIEW.setFloat64(start, previousVersion);
|
|
1074
|
+
} else if (previousVersion == null || previousVersion === 0) {
|
|
1075
|
+
// absence is stated, not inferred from truthiness: NaN is falsy and would otherwise be
|
|
1076
|
+
// silently dropped rather than rejected below
|
|
1077
|
+
hasPreviousVersion = false;
|
|
1078
|
+
} else if (previousVersion === PENDING_LOCAL_TIME) {
|
|
1079
|
+
// The previous entry has no log position yet, and a format whose presence signal is the value's
|
|
1080
|
+
// own first byte cannot express "to be filled in at commit". The superseded code deferred to
|
|
1081
|
+
// lmdb-js's instructed-write substitution, which resolves to 2.0 whenever no previous time was
|
|
1082
|
+
// recorded — the unreadable entry behind harper-pro#737. It resolved correctly when one was, so
|
|
1083
|
+
// this trades a lost link on that path for never minting the unreadable form. Not a throw: a
|
|
1084
|
+
// pending previous is a producer state, unlike a value the format cannot hold.
|
|
1085
|
+
hasPreviousVersion = false;
|
|
1086
|
+
if (!warnedPendingPreviousVersion.has(`${tableId}:${type}`)) {
|
|
1087
|
+
warnedPendingPreviousVersion.add(`${tableId}:${type}`);
|
|
1088
|
+
warnContained(
|
|
1089
|
+
`Audit entry (${type}) for record ${recordId} in table ${tableId} has a pending previous version; recording it without a previous-version link`,
|
|
1090
|
+
new Error('pending audit previousVersion')
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
ENTRY_DATAVIEW.setFloat64(start, previousVersion);
|
|
1095
|
+
if (ENTRY_HEADER[start] !== PREVIOUS_VERSION_FIRST_BYTE) {
|
|
1096
|
+
throw new Error(
|
|
1097
|
+
`Audit entry previousVersion ${previousVersion} for record ${recordId} in table ${tableId} is not representable. ` +
|
|
1098
|
+
'The LMDB audit format signals this field with its own leading 0x42 byte, so only values in [2**33, 2**49) can be written; ' +
|
|
1099
|
+
'writing any other value produces an entry every reader parses 8 bytes off.'
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
hasPreviousVersion = true;
|
|
539
1103
|
}
|
|
1104
|
+
let position = start + (hasPreviousVersion ? 9 : 1);
|
|
540
1105
|
if (extendedType) {
|
|
541
1106
|
if (extendedType & 0xff) {
|
|
542
1107
|
throw new Error('Illegal extended type');
|
|
@@ -575,8 +1140,9 @@ export function createAuditEntry(auditRecord: AuditRecord, start = 0) {
|
|
|
575
1140
|
|
|
576
1141
|
if (user) writeValue(user);
|
|
577
1142
|
else ENTRY_HEADER[position++] = 0;
|
|
578
|
-
|
|
579
|
-
|
|
1143
|
+
const actionPosition = start + (hasPreviousVersion ? 8 : 0);
|
|
1144
|
+
if (extendedType) ENTRY_DATAVIEW.setUint32(actionPosition, action | extendedType | 0xc0000000);
|
|
1145
|
+
else ENTRY_HEADER[actionPosition] = action;
|
|
580
1146
|
const header = ENTRY_HEADER.subarray(0, position);
|
|
581
1147
|
if (encodedRecord) {
|
|
582
1148
|
return Buffer.concat([header, encodedRecord]);
|
|
@@ -634,13 +1200,40 @@ export function readAuditEntry(buffer: Uint8Array, start = 0, end = undefined):
|
|
|
634
1200
|
((buffer as any).decoder = new Decoder(buffer.buffer, buffer.byteOffset, buffer.byteLength));
|
|
635
1201
|
decoder.position = start;
|
|
636
1202
|
let previousVersion;
|
|
637
|
-
|
|
638
|
-
|
|
1203
|
+
let entryStart = start;
|
|
1204
|
+
const firstByte = buffer[start];
|
|
1205
|
+
if (firstByte === PREVIOUS_VERSION_FIRST_BYTE) {
|
|
639
1206
|
previousVersion = decoder.readFloat64();
|
|
1207
|
+
} else if (ACTION_FIRST_BYTE[firstByte] !== 1) {
|
|
1208
|
+
// Written before the writer enforced the leading-0x42 contract (harper#2247): the field is
|
|
1209
|
+
// physically here but does not announce itself, so the action sits 8 bytes further on.
|
|
1210
|
+
// Recover only when the bytes cannot be anything else, and never for the RocksDB container,
|
|
1211
|
+
// whose prelude flag already consumed its previousVersion before this offset.
|
|
1212
|
+
if (start !== 0 || start + 9 > buffer.byteLength || ACTION_FIRST_BYTE[buffer[start + 8]] !== 1) {
|
|
1213
|
+
return corruptEntry(buffer, start, end, firstByte);
|
|
1214
|
+
}
|
|
1215
|
+
// > 1 is exactly what the superseded writer's own guard could emit, the tightest available
|
|
1216
|
+
// bound on "these bytes really are an old previousVersion"
|
|
1217
|
+
if (!(decoder.getFloat64(start) > 1)) return corruptEntry(buffer, start, end, firstByte);
|
|
1218
|
+
entryStart = decoder.position = start + 8;
|
|
1219
|
+
// The value itself is dropped, not reported. It cannot lead with 0x42 (that is the branch
|
|
1220
|
+
// above), and audit keys carry the same constraint, so it could never have addressed a
|
|
1221
|
+
// retrievable entry; reporting it would only feed an unrepresentable value back to
|
|
1222
|
+
// createAuditEntry through the resolveRecord re-mint in RecordEncoder, which now rejects it.
|
|
640
1223
|
}
|
|
641
1224
|
const action = decoder.readInt();
|
|
1225
|
+
if (entryStart !== start && !isDecodableAction(action)) {
|
|
1226
|
+
return corruptEntry(buffer, start, end, firstByte);
|
|
1227
|
+
}
|
|
642
1228
|
const nodeId = decoder.readInt();
|
|
643
1229
|
const tableId = decoder.readInt();
|
|
1230
|
+
if (entryStart !== start && !warnedRecoveredTables.has(tableId)) {
|
|
1231
|
+
warnedRecoveredTables.add(tableId);
|
|
1232
|
+
warnContained('Audit entry carries an unannounced previousVersion field; recovering its field offsets', {
|
|
1233
|
+
tableId,
|
|
1234
|
+
firstByte,
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
644
1237
|
let length = decoder.readInt();
|
|
645
1238
|
// A corrupt length field (e.g., a 0xff-prefixed uint32) would otherwise push
|
|
646
1239
|
// decoder.position hundreds of megabytes past the buffer; the next readFloat64
|
|
@@ -690,10 +1283,11 @@ export function readAuditEntry(buffer: Uint8Array, start = 0, end = undefined):
|
|
|
690
1283
|
const usernameEnd = (decoder.position += length);
|
|
691
1284
|
let value: any;
|
|
692
1285
|
return {
|
|
693
|
-
// The entry type is the low nibble of the action byte (1–7 record actions, 8 reload, 9–15
|
|
1286
|
+
// The entry type is the low nibble of the action byte (1–7 record actions, 8 reload, 9 eviction, 10–15
|
|
694
1287
|
// reserved); the flag bits (HAS_RECORD, HAS_PARTIAL_RECORD, …) sit above it. `& 0xf` is
|
|
695
1288
|
// identical to the historical `& 7` for every pre-reload entry (bit 3 was always clear).
|
|
696
1289
|
type: EVENT_TYPES[action & 0xf],
|
|
1290
|
+
logName: undefined,
|
|
697
1291
|
tableId,
|
|
698
1292
|
nodeId,
|
|
699
1293
|
get recordId() {
|
|
@@ -713,7 +1307,6 @@ export function readAuditEntry(buffer: Uint8Array, start = 0, end = undefined):
|
|
|
713
1307
|
return buffer.subarray(recordIdStart, recordIdEnd);
|
|
714
1308
|
},
|
|
715
1309
|
version,
|
|
716
|
-
recordVersion: version,
|
|
717
1310
|
previousVersion,
|
|
718
1311
|
get user() {
|
|
719
1312
|
try {
|
|
@@ -726,10 +1319,14 @@ export function readAuditEntry(buffer: Uint8Array, start = 0, end = undefined):
|
|
|
726
1319
|
}
|
|
727
1320
|
},
|
|
728
1321
|
get encoded() {
|
|
729
|
-
|
|
1322
|
+
// On a recovered entry this drops the unannounced prefix, so a replication sender —
|
|
1323
|
+
// which strips by the same leading-0x42 test and frames by encoded.length — forwards a
|
|
1324
|
+
// clean entry instead of passing the same misparse to the next hop.
|
|
1325
|
+
return entryStart ? buffer.subarray(entryStart, end) : buffer;
|
|
730
1326
|
},
|
|
731
1327
|
get size() {
|
|
732
|
-
|
|
1328
|
+
// only the recovered prefix is discounted; every other case keeps its existing basis
|
|
1329
|
+
return (end !== undefined ? end - start : buffer.byteLength) - (entryStart - start);
|
|
733
1330
|
},
|
|
734
1331
|
getValue(store, fullRecord?, auditTime?) {
|
|
735
1332
|
if (action & HAS_RECORD || (action & HAS_PARTIAL_RECORD && !fullRecord)) {
|
|
@@ -778,6 +1375,23 @@ export function readAuditEntry(buffer: Uint8Array, start = 0, end = undefined):
|
|
|
778
1375
|
}
|
|
779
1376
|
}
|
|
780
1377
|
|
|
1378
|
+
/**
|
|
1379
|
+
* Reject a header whose first byte can be neither an action nor a previousVersion field. Returns the
|
|
1380
|
+
* sentinel directly rather than throwing into readAuditEntry's catch, whose error log is not
|
|
1381
|
+
* contained: a throwing log sink there would escape the decoder and stall the iteration it runs in.
|
|
1382
|
+
*/
|
|
1383
|
+
function corruptEntry(buffer: Uint8Array, start: number, end: number | undefined, firstByte: number): AuditRecord {
|
|
1384
|
+
if (!warnedUndecodableHeader) {
|
|
1385
|
+
warnedUndecodableHeader = true;
|
|
1386
|
+
warnContained('Audit entry header begins with neither an action nor a previousVersion; treating as corrupt', {
|
|
1387
|
+
firstByte,
|
|
1388
|
+
// the 8 prefix bytes plus the action byte: the classifying bytes, stopping before the recordId
|
|
1389
|
+
header: Buffer.from(buffer.subarray(start, Math.min(start + 9, buffer.byteLength))).toString('hex'),
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
return createCorruptAuditSentinel(buffer, start, end);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
781
1395
|
/**
|
|
782
1396
|
* Build a structurally complete audit record for an entry that failed to decode. The fields
|
|
783
1397
|
* mirror the happy-path shape so downstream consumers that access (e.g.) `getValue` or the
|
|
@@ -790,6 +1404,7 @@ export function readAuditEntry(buffer: Uint8Array, start = 0, end = undefined):
|
|
|
790
1404
|
function createCorruptAuditSentinel(buffer: Uint8Array, start: number, end: number | undefined): AuditRecord {
|
|
791
1405
|
return {
|
|
792
1406
|
type: undefined,
|
|
1407
|
+
logName: undefined,
|
|
793
1408
|
tableId: undefined,
|
|
794
1409
|
nodeId: undefined,
|
|
795
1410
|
recordId: undefined,
|