@nklisch/pi-enhanced 0.1.0 → 0.1.2
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/node_modules/@nklisch/pi-conveniences/package.json +5 -2
- package/node_modules/@nklisch/pi-fff-compat/package.json +5 -2
- package/node_modules/@nklisch/pi-plugins/README.md +16 -13
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-trust-continuity.d.ts +34 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-trust-continuity.js +310 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-trust-continuity.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-update-coordinator.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-update-coordinator.js +30 -17
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-update-coordinator.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-update-eligibility.d.ts +0 -3
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-update-eligibility.js +0 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/automatic-update-eligibility.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/hook-visibility-contract.d.ts +40 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/hook-visibility-contract.js +28 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/hook-visibility-contract.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/hook-visibility-service.d.ts +24 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/hook-visibility-service.js +67 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/hook-visibility-service.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/installed-trust-grant-service.d.ts +110 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/installed-trust-grant-service.js +186 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/installed-trust-grant-service.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/lifecycle-transition-reconciler.d.ts +3 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/lifecycle-transition-reconciler.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/marketplace-plugin-probe.d.ts +0 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/marketplace-plugin-probe.js +38 -11
- package/node_modules/@nklisch/pi-plugins/dist/application/marketplace-plugin-probe.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/marketplace-refresh-service.d.ts +2 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/marketplace-refresh-service.js +22 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/marketplace-refresh-service.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-automatic-run-presenter.d.ts +11 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-automatic-run-presenter.js +58 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-automatic-run-presenter.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-contract.d.ts +2 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-dispatcher.js +2 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-dispatcher.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-help.d.ts +5 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-mutation-dispatch.js +47 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-mutation-dispatch.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-operation.d.ts +2505 -2439
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-parser.d.ts +2 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-parser.js +3 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-parser.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-progress.d.ts +4 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-registry.d.ts +46 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-registry.js +7 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/native-control-registry.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-diagnostic-registry.d.ts +12 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-diagnostic-registry.js +2 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-diagnostic-registry.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-failure-presenter.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-failure-presenter.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-inspection-contract.d.ts +5 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-installed-inspection.js +7 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/native-installed-inspection.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-lifecycle-operation-contract.d.ts +464 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-lifecycle-operation-contract.js +3 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/native-lifecycle-operation-contract.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-lifecycle-operation-session.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-lifecycle-operation-session.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-lifecycle-operation.d.ts +77 -11
- package/node_modules/@nklisch/pi-plugins/dist/application/native-lifecycle-result.js +3 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/native-lifecycle-result.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-update-contract.d.ts +11 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-update-contract.js +9 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/native-update-contract.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/plugin-lifecycle-service.d.ts +22 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/plugin-lifecycle-service.js +21 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/plugin-lifecycle-service.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/ports/automatic-update-lifecycle.d.ts +4 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/ports/mcp-runtime.d.ts +0 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/ports/mcp-runtime.js +2 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/ports/mcp-runtime.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/ports/native-control-applications.d.ts +4 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/state-contract.d.ts +28 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/trusted-install-contract.d.ts +15 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/trusted-install-contract.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/trusted-install-contract.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/application/trusted-install-service.js +4 -0
- package/node_modules/@nklisch/pi-plugins/dist/application/trusted-install-service.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/automatic-update-lifecycle-adapter.js +88 -80
- package/node_modules/@nklisch/pi-plugins/dist/composition/automatic-update-lifecycle-adapter.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/complete-plugin-reload.js +32 -4
- package/node_modules/@nklisch/pi-plugins/dist/composition/complete-plugin-reload.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/create-native-update-management-service.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/composition/create-native-update-management-service.js +9 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/create-native-update-management-service.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/create-packaged-plugin-host.js +49 -10
- package/node_modules/@nklisch/pi-plugins/dist/composition/create-packaged-plugin-host.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/create-skill-hook-runtime.d.ts +2 -0
- package/node_modules/@nklisch/pi-plugins/dist/composition/create-skill-hook-runtime.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/create-skill-hook-runtime.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/packaged-host-startup.js +4 -3
- package/node_modules/@nklisch/pi-plugins/dist/composition/packaged-host-startup.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/composition/packaged-plugin-host-contract.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/composition/runtime-desired-state.js +21 -7
- package/node_modules/@nklisch/pi-plugins/dist/composition/runtime-desired-state.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/domain/hook-output-contract.d.ts +7 -1
- package/node_modules/@nklisch/pi-plugins/dist/domain/hook-output-contract.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/domain/hook-visibility.d.ts +16 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/hook-visibility.js +12 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/hook-visibility.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/mcp-session-environment.d.ts +23 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/mcp-session-environment.js +30 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/mcp-session-environment.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/state/codec.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/domain/state/codec.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/domain/state/config-state.d.ts +15 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/state/config-state.js +7 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/state/config-state.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/domain/state/registry.d.ts +7 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/update-policy.js +2 -0
- package/node_modules/@nklisch/pi-plugins/dist/domain/update-policy.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/index.d.ts +2 -1
- package/node_modules/@nklisch/pi-plugins/dist/index.js +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/index.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/project/node-project-root-resolver.js +8 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/project/node-project-root-resolver.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/local-recovery-filesystem.d.ts +0 -2
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/local-recovery-filesystem.js +3 -52
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/local-recovery-filesystem.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/process-revision-leases.js +6 -8
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/process-revision-leases.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/sqlite-revision-retention.js +6 -6
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/sqlite-revision-retention.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/sqlite-transition-journal.js +4 -49
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/recovery/sqlite-transition-journal.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/sqlite-lifecycle-state-store.js +9 -18
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/sqlite-lifecycle-state-store.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/sqlite-open.d.ts +23 -0
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/sqlite-open.js +80 -0
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/sqlite-open.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/sqlite-scope-lock.d.ts +1 -11
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/sqlite-scope-lock.js +59 -593
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/sqlite-scope-lock.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/extension.js +2 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/extension.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/hooks/pi-hook-context-renderer.d.ts +18 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/hooks/pi-hook-context-renderer.js +47 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/hooks/pi-hook-context-renderer.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/hooks/pi-hook-decision-adapter.d.ts +3 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/hooks/pi-hook-decision-adapter.js +28 -11
- package/node_modules/@nklisch/pi-plugins/dist/pi/hooks/pi-hook-decision-adapter.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-install-component.js +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-install-component.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-actions.d.ts +7 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-actions.js +26 -2
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-actions.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-commands.d.ts +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-commands.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-commands.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-component.d.ts +4 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-component.js +25 -3
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-component.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-controller.js +9 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-controller.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-model.d.ts +4 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-model.js +5 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-model.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-render.d.ts +3 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-render.js +20 -12
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-render.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-session.js +43 -14
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-manager-session.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-operation-view.js +13 -8
- package/node_modules/@nklisch/pi-plugins/dist/pi/manager/plugin-operation-view.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/native-control-human.js +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/pi/native-control-human.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/pi-reload-broker.d.ts +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/pi/pi-trust-review.d.ts +8 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/pi-trust-review.js +93 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/pi-trust-review.js.map +1 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-command.js +3 -3
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-command.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-lifecycle.d.ts +2 -0
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-lifecycle.js +8 -2
- package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager-lifecycle.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/hooks/hook-decision-aggregator.js +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/hooks/hook-decision-aggregator.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/launch-value-provider.js +24 -0
- package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/launch-value-provider.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/pi-mcp-adapter-package.js +1 -7
- package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/pi-mcp-adapter-package.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/pi-mcp-adapter-runtime.js +1 -2
- package/node_modules/@nklisch/pi-plugins/dist/runtime/mcp/pi-mcp-adapter-runtime.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/published-package-receipt.d.ts +10 -13
- package/node_modules/@nklisch/pi-plugins/dist/runtime/published-package-receipt.js +7 -92
- package/node_modules/@nklisch/pi-plugins/dist/runtime/published-package-receipt.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/subagents/pi-subagents-lifecycle.js +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/runtime/subagents/pi-subagents-package.js +1 -7
- package/node_modules/@nklisch/pi-plugins/dist/runtime/subagents/pi-subagents-package.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/dist/runtime/subagents/subagent-hook-coordinator.js +2 -2
- package/node_modules/@nklisch/pi-plugins/dist/runtime/subagents/subagent-hook-coordinator.js.map +1 -1
- package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +5 -2
- package/node_modules/@nklisch/pi-plugins/package.json +3 -3
- package/package.json +7 -7
- package/node_modules/@nklisch/pi-conveniences/extensions/agents-context.test.ts +0 -140
- package/node_modules/@nklisch/pi-conveniences/extensions/context-window-footer.test.ts +0 -30
- package/node_modules/@nklisch/pi-conveniences/extensions/exit.test.ts +0 -44
- package/node_modules/@nklisch/pi-conveniences/extensions/model-list.test.ts +0 -34
- package/node_modules/@nklisch/pi-fff-compat/extensions/fff-compat-search.test.ts +0 -76
- package/node_modules/@nklisch/pi-plugins/dist/application/ports/update-activation-context.d.ts +0 -4
- package/node_modules/@nklisch/pi-plugins/dist/application/ports/update-activation-context.js +0 -2
- package/node_modules/@nklisch/pi-plugins/dist/application/ports/update-activation-context.js.map +0 -1
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/identity-bound-sqlite.d.ts +0 -22
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/identity-bound-sqlite.js +0 -256
- package/node_modules/@nklisch/pi-plugins/dist/infrastructure/state/identity-bound-sqlite.js.map +0 -1
|
@@ -1,37 +1,14 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { DatabaseSync } from "node:sqlite";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { basename, join } from "node:path";
|
|
2
|
+
import { chmodSync, lstatSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
6
4
|
import { BoundaryError } from "../../domain/errors.js";
|
|
7
5
|
import { ScopeReferenceSchema, } from "../../domain/state/scope.js";
|
|
8
6
|
import { ensurePrivateLockRoot, LOCAL_LOCK_DATABASE_MODE, verifyLocalFilesystemCapability, } from "./local-lock-filesystem.js";
|
|
9
|
-
import { classifyProcessIdentity, readLinuxProcessStartToken } from "../process/process-identity.js";
|
|
10
7
|
const PROTOCOL = "pi-plugin-host-scope-lock";
|
|
11
8
|
const PROTOCOL_VERSION = 1;
|
|
12
9
|
const PROTOCOL_TABLE = "scope_lock_protocol";
|
|
13
|
-
const ROOT_MARKER_NAME = ".scope-lock-root.identity";
|
|
14
|
-
const DATABASE_MARKER_SUFFIX = ".identity";
|
|
15
|
-
const DATABASE_INITIALIZATION_CLAIM_SUFFIX = ".initializing";
|
|
16
|
-
const DATABASE_HANDLE_ALIAS_SUFFIX = ".handle";
|
|
17
|
-
const ROOT_MARKER_PROTOCOL = "pi-plugin-host-scope-lock-root";
|
|
18
|
-
const DATABASE_MARKER_PROTOCOL = "pi-plugin-host-scope-lock-database";
|
|
19
10
|
const DEFAULT_RETRY_DELAY = Object.freeze({ minimum: 2, maximum: 25 });
|
|
20
11
|
const SQLITE_BUSY = 5;
|
|
21
|
-
class DatabaseInitializationInProgress extends Error {
|
|
22
|
-
owner;
|
|
23
|
-
constructor(owner) {
|
|
24
|
-
super("scope lock database initialization is in progress");
|
|
25
|
-
this.owner = owner;
|
|
26
|
-
this.name = "DatabaseInitializationInProgress";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
class DatabaseInitializationSnapshotChanged extends Error {
|
|
30
|
-
constructor() {
|
|
31
|
-
super("scope lock database initialization snapshot changed");
|
|
32
|
-
this.name = "DatabaseInitializationSnapshotChanged";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
12
|
function assertSignal(signal) {
|
|
36
13
|
if (signal === null ||
|
|
37
14
|
typeof signal !== "object" ||
|
|
@@ -87,249 +64,9 @@ function scopeDatabaseName(scope) {
|
|
|
87
64
|
function databasePath(root, scope) {
|
|
88
65
|
return join(root, scopeDatabaseName(scope));
|
|
89
66
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
function regularFileIdentity(path) {
|
|
94
|
-
const stats = lstatSync(path);
|
|
95
|
-
if (stats.isSymbolicLink() || !stats.isFile())
|
|
96
|
-
throw new Error("scope lock identity path is not a regular file");
|
|
97
|
-
return { device: String(stats.dev), inode: String(stats.ino) };
|
|
98
|
-
}
|
|
99
|
-
function currentInitializationOwner() {
|
|
100
|
-
const startTime = readLinuxProcessStartToken(process.pid);
|
|
101
|
-
if (startTime === undefined)
|
|
102
|
-
throw new Error("scope lock cannot establish process identity");
|
|
103
|
-
return { pid: process.pid, startTime };
|
|
104
|
-
}
|
|
105
|
-
function ownerStatus(owner) {
|
|
106
|
-
return classifyProcessIdentity({ pid: owner.pid, startToken: owner.startTime });
|
|
107
|
-
}
|
|
108
|
-
function parseInitializationOwner(input) {
|
|
109
|
-
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
110
|
-
throw new Error("scope lock initialization owner is invalid");
|
|
111
|
-
}
|
|
112
|
-
const value = input;
|
|
113
|
-
const pid = value.pid;
|
|
114
|
-
const startTime = value.startTime;
|
|
115
|
-
if (Object.keys(value).length !== 2 ||
|
|
116
|
-
typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0 ||
|
|
117
|
-
typeof startTime !== "string" || !/^\d+$/.test(startTime)) {
|
|
118
|
-
throw new Error("scope lock initialization owner is invalid");
|
|
119
|
-
}
|
|
120
|
-
return { pid, startTime };
|
|
121
|
-
}
|
|
122
|
-
function parseRootIdentityMarker(input) {
|
|
123
|
-
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
124
|
-
throw new Error("scope lock root identity marker is invalid");
|
|
125
|
-
const value = input;
|
|
126
|
-
if (Object.keys(value).length !== 3 ||
|
|
127
|
-
value.protocol !== ROOT_MARKER_PROTOCOL ||
|
|
128
|
-
value.version !== 1 ||
|
|
129
|
-
typeof value.identity !== "string" ||
|
|
130
|
-
value.identity.length === 0)
|
|
131
|
-
throw new Error("scope lock root identity marker is invalid");
|
|
132
|
-
return value;
|
|
133
|
-
}
|
|
134
|
-
function rootIdentityMarkerPath(root) {
|
|
135
|
-
return join(root, ROOT_MARKER_NAME);
|
|
136
|
-
}
|
|
137
|
-
function ensureRootIdentityMarker(root) {
|
|
138
|
-
const path = rootIdentityMarkerPath(root);
|
|
139
|
-
let markerExists = false;
|
|
140
|
-
try {
|
|
141
|
-
regularFileIdentity(path);
|
|
142
|
-
markerExists = true;
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
if (error.code !== "ENOENT")
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
if (!markerExists) {
|
|
149
|
-
const marker = {
|
|
150
|
-
protocol: ROOT_MARKER_PROTOCOL,
|
|
151
|
-
version: 1,
|
|
152
|
-
identity: randomUUID(),
|
|
153
|
-
};
|
|
154
|
-
// Exclusive creation exposes an empty file before writeFileSync has
|
|
155
|
-
// populated it. Publish a complete marker through a hard-link so another
|
|
156
|
-
// first-use process can never parse a partially-written identity.
|
|
157
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
158
|
-
writeFileSync(temporary, `${JSON.stringify(marker)}\n`, {
|
|
159
|
-
flag: "wx",
|
|
160
|
-
mode: LOCAL_LOCK_DATABASE_MODE,
|
|
161
|
-
});
|
|
162
|
-
try {
|
|
163
|
-
linkSync(temporary, path);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
if (error.code !== "EEXIST")
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
finally {
|
|
170
|
-
try {
|
|
171
|
-
unlinkSync(temporary);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
if (error.code !== "ENOENT")
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
chmodSync(path, LOCAL_LOCK_DATABASE_MODE);
|
|
180
|
-
return parseRootIdentityMarker(JSON.parse(readFileSync(path, "utf8"))).identity;
|
|
181
|
-
}
|
|
182
|
-
function databaseIdentityMarkerPath(path) {
|
|
183
|
-
return `${path}${DATABASE_MARKER_SUFFIX}`;
|
|
184
|
-
}
|
|
185
|
-
function databaseInitializationClaimPath(path) {
|
|
186
|
-
return `${path}${DATABASE_INITIALIZATION_CLAIM_SUFFIX}`;
|
|
187
|
-
}
|
|
188
|
-
function parseDatabaseIdentityMarker(input) {
|
|
189
|
-
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
190
|
-
throw new Error("scope lock database identity marker is invalid");
|
|
191
|
-
const value = input;
|
|
192
|
-
if ((value.state !== "initializing" && value.state !== "ready") ||
|
|
193
|
-
Object.keys(value).length !== (value.state === "ready" ? 6 : 6) ||
|
|
194
|
-
value.protocol !== DATABASE_MARKER_PROTOCOL ||
|
|
195
|
-
value.version !== 1 ||
|
|
196
|
-
typeof value.rootIdentity !== "string" ||
|
|
197
|
-
value.rootIdentity.length === 0 ||
|
|
198
|
-
typeof value.database !== "string" ||
|
|
199
|
-
value.database.length === 0)
|
|
200
|
-
throw new Error("scope lock database identity marker is invalid");
|
|
201
|
-
if (value.state === "initializing")
|
|
202
|
-
return {
|
|
203
|
-
protocol: DATABASE_MARKER_PROTOCOL,
|
|
204
|
-
version: 1,
|
|
205
|
-
rootIdentity: value.rootIdentity,
|
|
206
|
-
database: value.database,
|
|
207
|
-
state: "initializing",
|
|
208
|
-
owner: parseInitializationOwner(value.owner),
|
|
209
|
-
};
|
|
210
|
-
const identity = value.identity;
|
|
211
|
-
if (identity === null || typeof identity !== "object" || Array.isArray(identity)) {
|
|
212
|
-
throw new Error("scope lock database identity marker is invalid");
|
|
213
|
-
}
|
|
214
|
-
const fileIdentity = identity;
|
|
215
|
-
if (Object.keys(fileIdentity).length !== 2 ||
|
|
216
|
-
typeof fileIdentity.device !== "string" ||
|
|
217
|
-
typeof fileIdentity.inode !== "string" ||
|
|
218
|
-
fileIdentity.device.length === 0 ||
|
|
219
|
-
fileIdentity.inode.length === 0)
|
|
220
|
-
throw new Error("scope lock database identity marker is invalid");
|
|
221
|
-
return {
|
|
222
|
-
protocol: DATABASE_MARKER_PROTOCOL,
|
|
223
|
-
version: 1,
|
|
224
|
-
rootIdentity: value.rootIdentity,
|
|
225
|
-
database: value.database,
|
|
226
|
-
state: "ready",
|
|
227
|
-
identity: { device: fileIdentity.device, inode: fileIdentity.inode },
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
function readDatabaseIdentityMarker(path) {
|
|
231
|
-
const markerPath = databaseIdentityMarkerPath(path);
|
|
232
|
-
try {
|
|
233
|
-
regularFileIdentity(markerPath);
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
if (error.code !== "ENOENT")
|
|
237
|
-
throw error;
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
chmodSync(markerPath, LOCAL_LOCK_DATABASE_MODE);
|
|
241
|
-
return parseDatabaseIdentityMarker(JSON.parse(readFileSync(markerPath, "utf8")));
|
|
242
|
-
}
|
|
243
|
-
function readInitializationClaim(path) {
|
|
244
|
-
const claimPath = databaseInitializationClaimPath(path);
|
|
245
|
-
try {
|
|
246
|
-
regularFileIdentity(claimPath);
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
if (error.code === "ENOENT")
|
|
250
|
-
return undefined;
|
|
251
|
-
throw error;
|
|
252
|
-
}
|
|
253
|
-
const claim = parseDatabaseIdentityMarker(JSON.parse(readFileSync(claimPath, "utf8")));
|
|
254
|
-
if (claim.state !== "initializing")
|
|
255
|
-
throw new Error("scope lock initialization claim is invalid");
|
|
256
|
-
return claim;
|
|
257
|
-
}
|
|
258
|
-
function sameDatabaseMarker(left, right) {
|
|
259
|
-
if (left.protocol !== right.protocol || left.version !== right.version ||
|
|
260
|
-
left.rootIdentity !== right.rootIdentity || left.database !== right.database ||
|
|
261
|
-
left.state !== right.state)
|
|
262
|
-
return false;
|
|
263
|
-
if (left.state === "initializing" && right.state === "initializing") {
|
|
264
|
-
return left.owner.pid === right.owner.pid && left.owner.startTime === right.owner.startTime;
|
|
265
|
-
}
|
|
266
|
-
return left.state === "ready" && right.state === "ready" &&
|
|
267
|
-
left.identity.device === right.identity.device && left.identity.inode === right.identity.inode;
|
|
268
|
-
}
|
|
269
|
-
function markerMatchesDatabase(marker, rootIdentity, databaseName) {
|
|
270
|
-
return marker.rootIdentity === rootIdentity && marker.database === databaseName;
|
|
271
|
-
}
|
|
272
|
-
function removeIfRegular(path) {
|
|
273
|
-
try {
|
|
274
|
-
regularFileIdentity(path);
|
|
275
|
-
unlinkSync(path);
|
|
276
|
-
}
|
|
277
|
-
catch (error) {
|
|
278
|
-
if (error.code !== "ENOENT")
|
|
279
|
-
throw error;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
function reclaimDeadInitialization(path, marker, rootIdentity, databaseName) {
|
|
283
|
-
if (marker.state !== "initializing")
|
|
284
|
-
throw new Error("scope lock initialization marker is not reclaimable");
|
|
285
|
-
if (!markerMatchesDatabase(marker, rootIdentity, databaseName)) {
|
|
286
|
-
throw new Error("scope lock initialization marker does not match its path");
|
|
287
|
-
}
|
|
288
|
-
const status = ownerStatus(marker.owner);
|
|
289
|
-
if (status !== "dead") {
|
|
290
|
-
throw new DatabaseInitializationInProgress(marker.owner);
|
|
291
|
-
}
|
|
292
|
-
// Only a proven-dead owner permits removing the marker, claim, and any
|
|
293
|
-
// partially-created database. Unknown and live owners remain cancellable.
|
|
294
|
-
removeIfRegular(databaseIdentityMarkerPath(path));
|
|
295
|
-
removeIfRegular(databaseInitializationClaimPath(path));
|
|
296
|
-
removeIfRegular(path);
|
|
297
|
-
}
|
|
298
|
-
function createExclusiveClaim(path, marker) {
|
|
299
|
-
const claimPath = databaseInitializationClaimPath(path);
|
|
300
|
-
const temporary = `${claimPath}.${randomUUID()}.tmp`;
|
|
301
|
-
writeFileSync(temporary, `${JSON.stringify(marker)}\n`, {
|
|
302
|
-
flag: "wx",
|
|
303
|
-
mode: LOCAL_LOCK_DATABASE_MODE,
|
|
304
|
-
});
|
|
305
|
-
try {
|
|
306
|
-
linkSync(temporary, claimPath);
|
|
307
|
-
}
|
|
308
|
-
finally {
|
|
309
|
-
try {
|
|
310
|
-
unlinkSync(temporary);
|
|
311
|
-
}
|
|
312
|
-
catch (error) {
|
|
313
|
-
if (error.code !== "ENOENT")
|
|
314
|
-
throw error;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
function validateDatabaseIdentity(root, rootIdentity, path, databaseName, marker) {
|
|
319
|
-
if (marker.state === "initializing")
|
|
320
|
-
throw new DatabaseInitializationInProgress(marker.owner);
|
|
321
|
-
if (marker.rootIdentity !== rootIdentity ||
|
|
322
|
-
marker.database !== databaseName ||
|
|
323
|
-
basename(path) !== databaseName)
|
|
324
|
-
throw new Error("scope lock database identity marker does not match its path");
|
|
325
|
-
regularFileIdentity(rootIdentityMarkerPath(root));
|
|
326
|
-
if (parseRootIdentityMarker(JSON.parse(readFileSync(rootIdentityMarkerPath(root), "utf8"))).identity !== rootIdentity) {
|
|
327
|
-
throw new Error("scope lock root identity marker changed");
|
|
328
|
-
}
|
|
329
|
-
const identity = regularFileIdentity(path);
|
|
330
|
-
if (marker.identity === undefined || !sameFileIdentity(identity, marker.identity))
|
|
331
|
-
throw new Error("scope lock database path was replaced");
|
|
332
|
-
return identity;
|
|
67
|
+
function createProtocolSchema(database) {
|
|
68
|
+
database.exec(`CREATE TABLE IF NOT EXISTS ${PROTOCOL_TABLE} (protocol TEXT PRIMARY KEY NOT NULL CHECK (protocol = '${PROTOCOL}'), version INTEGER NOT NULL CHECK (version = ${PROTOCOL_VERSION})) STRICT;`);
|
|
69
|
+
database.prepare(`INSERT OR IGNORE INTO ${PROTOCOL_TABLE} (protocol, version) VALUES ('${PROTOCOL}', ${PROTOCOL_VERSION})`).run();
|
|
333
70
|
}
|
|
334
71
|
function validateProtocolSchema(database) {
|
|
335
72
|
const objects = database
|
|
@@ -369,227 +106,47 @@ function validateProtocolSchema(database) {
|
|
|
369
106
|
throw new Error("scope lock protocol version is invalid");
|
|
370
107
|
}
|
|
371
108
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
try {
|
|
390
|
-
regularFileIdentity(path);
|
|
391
|
-
}
|
|
392
|
-
catch (error) {
|
|
393
|
-
if (error.code !== "ENOENT")
|
|
394
|
-
throw error;
|
|
395
|
-
existing = false;
|
|
396
|
-
}
|
|
397
|
-
if (marker?.state === "initializing") {
|
|
398
|
-
reclaimDeadInitialization(path, marker, rootIdentity, databaseName);
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
if (marker === undefined && existing) {
|
|
402
|
-
// The marker and database path are independent files. A first-use loser
|
|
403
|
-
// can read the old marker state, yield to the winner, then observe the
|
|
404
|
-
// newly-created database through its later path check. That combination
|
|
405
|
-
// is stale evidence, not proof of an orphan. Retry once through the
|
|
406
|
-
// caller's cancellable acquisition loop; if the same coherent state is
|
|
407
|
-
// still present, fail closed rather than adopting the database.
|
|
408
|
-
const claim = readInitializationClaim(path);
|
|
409
|
-
if (claim !== undefined) {
|
|
410
|
-
reclaimDeadInitialization(path, claim, rootIdentity, databaseName);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
if (retryingMissingMarkerDatabase)
|
|
414
|
-
throw new Error("scope lock database identity marker is missing");
|
|
415
|
-
throw new DatabaseInitializationSnapshotChanged();
|
|
416
|
-
}
|
|
417
|
-
if (marker === undefined) {
|
|
418
|
-
if (existing)
|
|
419
|
-
throw new Error("scope lock database identity marker is missing");
|
|
420
|
-
const initializing = {
|
|
421
|
-
protocol: DATABASE_MARKER_PROTOCOL,
|
|
422
|
-
version: 1,
|
|
423
|
-
rootIdentity,
|
|
424
|
-
database: databaseName,
|
|
425
|
-
state: "initializing",
|
|
426
|
-
owner,
|
|
427
|
-
};
|
|
428
|
-
try {
|
|
429
|
-
// The claim is a complete atomically-linked file. The marker is then
|
|
430
|
-
// published by rename, so a crash at either boundary leaves an owner
|
|
431
|
-
// identity that a later process can evaluate without guessing.
|
|
432
|
-
createExclusiveClaim(path, initializing);
|
|
433
|
-
const temporary = `${databaseIdentityMarkerPath(path)}.${randomUUID()}.tmp`;
|
|
434
|
-
writeFileSync(temporary, `${JSON.stringify(initializing)}\n`, {
|
|
435
|
-
flag: "wx",
|
|
436
|
-
mode: LOCAL_LOCK_DATABASE_MODE,
|
|
437
|
-
});
|
|
438
|
-
try {
|
|
439
|
-
renameSync(temporary, databaseIdentityMarkerPath(path));
|
|
440
|
-
}
|
|
441
|
-
finally {
|
|
442
|
-
try {
|
|
443
|
-
unlinkSync(temporary);
|
|
444
|
-
}
|
|
445
|
-
catch (error) {
|
|
446
|
-
if (error.code !== "ENOENT")
|
|
447
|
-
throw error;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
marker = initializing;
|
|
451
|
-
}
|
|
452
|
-
catch (error) {
|
|
453
|
-
if (error.code !== "EEXIST")
|
|
454
|
-
throw error;
|
|
455
|
-
const observed = readDatabaseIdentityMarker(path) ?? readInitializationClaim(path);
|
|
456
|
-
if (observed === undefined)
|
|
457
|
-
throw new Error("scope lock initialization claim disappeared");
|
|
458
|
-
if (observed.state === "initializing")
|
|
459
|
-
reclaimDeadInitialization(path, observed, rootIdentity, databaseName);
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
if (marker === undefined)
|
|
464
|
-
throw new Error("scope lock database identity marker disappeared");
|
|
465
|
-
if (marker.state === "ready" && !existing) {
|
|
466
|
-
throw new Error("scope lock database is missing after initialization");
|
|
467
|
-
}
|
|
468
|
-
let created = false;
|
|
469
|
-
if (!existing) {
|
|
470
|
-
let descriptor;
|
|
471
|
-
try {
|
|
472
|
-
descriptor = openSync(path, "wx", LOCAL_LOCK_DATABASE_MODE);
|
|
473
|
-
}
|
|
474
|
-
catch (error) {
|
|
475
|
-
if (error.code === "EEXIST") {
|
|
476
|
-
const observed = readDatabaseIdentityMarker(path) ?? readInitializationClaim(path);
|
|
477
|
-
if (observed?.state === "initializing")
|
|
478
|
-
throw new DatabaseInitializationInProgress(observed.owner);
|
|
479
|
-
throw new DatabaseInitializationInProgress();
|
|
480
|
-
}
|
|
481
|
-
throw error;
|
|
482
|
-
}
|
|
483
|
-
closeSync(descriptor);
|
|
484
|
-
const identity = regularFileIdentity(path);
|
|
485
|
-
marker = {
|
|
486
|
-
protocol: DATABASE_MARKER_PROTOCOL,
|
|
487
|
-
version: 1,
|
|
488
|
-
rootIdentity,
|
|
489
|
-
database: databaseName,
|
|
490
|
-
state: "ready",
|
|
491
|
-
identity,
|
|
492
|
-
};
|
|
493
|
-
created = true;
|
|
494
|
-
}
|
|
495
|
-
validateDatabaseIdentity(root, rootIdentity, path, databaseName, marker);
|
|
496
|
-
const aliasPath = `${path}${DATABASE_HANDLE_ALIAS_SUFFIX}-${randomUUID()}`;
|
|
497
|
-
let database;
|
|
498
|
-
let aliasLinked = false;
|
|
499
|
-
try {
|
|
500
|
-
// DatabaseSync accepts a path rather than an fd. A private hard-link
|
|
501
|
-
// alias pins the exact inode the native handle opens; the durable path
|
|
502
|
-
// and marker are checked separately before BEGIN and on every assert.
|
|
503
|
-
linkSync(path, aliasPath);
|
|
504
|
-
aliasLinked = true;
|
|
505
|
-
database = new DatabaseSync(aliasPath, {
|
|
506
|
-
allowExtension: false,
|
|
507
|
-
defensive: true,
|
|
508
|
-
enableDoubleQuotedStringLiterals: false,
|
|
509
|
-
enableForeignKeyConstraints: true,
|
|
510
|
-
readOnly: false,
|
|
511
|
-
timeout: 0,
|
|
512
|
-
});
|
|
513
|
-
const prepared = { database, aliasPath };
|
|
514
|
-
validateOpenedDatabaseIdentity(prepared, root, rootIdentity, path, databaseName, marker);
|
|
515
|
-
database.enableLoadExtension(false);
|
|
516
|
-
database.enableDefensive(true);
|
|
517
|
-
chmodSync(path, LOCAL_LOCK_DATABASE_MODE);
|
|
518
|
-
if (process.platform !== "win32" && (lstatSync(path).mode & 0o777) !== LOCAL_LOCK_DATABASE_MODE) {
|
|
519
|
-
throw new Error("scope lock database is not private");
|
|
520
|
-
}
|
|
521
|
-
database.exec("PRAGMA journal_mode = DELETE; PRAGMA locking_mode = NORMAL; PRAGMA foreign_keys = ON; PRAGMA trusted_schema = OFF;");
|
|
522
|
-
if (created) {
|
|
523
|
-
database.exec(`CREATE TABLE ${PROTOCOL_TABLE} (protocol TEXT PRIMARY KEY NOT NULL CHECK (protocol = '${PROTOCOL}'), version INTEGER NOT NULL CHECK (version = ${PROTOCOL_VERSION})) STRICT;`);
|
|
524
|
-
database.exec(`INSERT INTO ${PROTOCOL_TABLE} (protocol, version) VALUES ('${PROTOCOL}', ${PROTOCOL_VERSION});`);
|
|
525
|
-
}
|
|
526
|
-
validateProtocolSchema(database);
|
|
527
|
-
validateOpenedDatabaseIdentity(prepared, root, rootIdentity, path, databaseName, marker);
|
|
528
|
-
const journal = database.prepare("PRAGMA journal_mode").get();
|
|
529
|
-
if (journal?.journal_mode !== "delete")
|
|
530
|
-
throw new Error("scope lock database is not using rollback journal mode");
|
|
531
|
-
if (created) {
|
|
532
|
-
const temporary = `${databaseIdentityMarkerPath(path)}.${randomUUID()}.tmp`;
|
|
533
|
-
try {
|
|
534
|
-
writeFileSync(temporary, `${JSON.stringify(marker)}\n`, { flag: "wx", mode: LOCAL_LOCK_DATABASE_MODE });
|
|
535
|
-
renameSync(temporary, databaseIdentityMarkerPath(path));
|
|
536
|
-
unlinkSync(databaseInitializationClaimPath(path));
|
|
537
|
-
}
|
|
538
|
-
catch (error) {
|
|
539
|
-
throw new Error("scope lock database identity marker could not be finalized", { cause: error });
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
const durableMarker = readDatabaseIdentityMarker(path);
|
|
543
|
-
if (durableMarker === undefined || !sameDatabaseMarker(durableMarker, marker)) {
|
|
544
|
-
throw new Error("scope lock database identity marker changed during setup");
|
|
545
|
-
}
|
|
546
|
-
validateOpenedDatabaseIdentity(prepared, root, rootIdentity, path, databaseName, durableMarker);
|
|
547
|
-
return prepared;
|
|
548
|
-
}
|
|
549
|
-
catch (error) {
|
|
550
|
-
let closeError;
|
|
551
|
-
try {
|
|
552
|
-
if (database?.isOpen)
|
|
553
|
-
database.close();
|
|
554
|
-
}
|
|
555
|
-
catch (cause) {
|
|
556
|
-
closeError = cause;
|
|
557
|
-
}
|
|
558
|
-
if (aliasLinked) {
|
|
559
|
-
try {
|
|
560
|
-
unlinkSync(aliasPath);
|
|
561
|
-
}
|
|
562
|
-
catch (cause) {
|
|
563
|
-
closeError = closeError === undefined ? cause : { close: closeError, unlink: cause };
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (closeError !== undefined)
|
|
567
|
-
throw new Error("scope lock database setup and cleanup failed", { cause: { error, closeError } });
|
|
568
|
-
throw error;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
async function closeDatabase(prepared) {
|
|
573
|
-
let closeError;
|
|
109
|
+
/**
|
|
110
|
+
* Open the scope database and take the write lock. The lock itself is the
|
|
111
|
+
* held BEGIN IMMEDIATE transaction; first-use schema setup runs under the
|
|
112
|
+
* same transaction so concurrent creators serialize on the database. No
|
|
113
|
+
* sidecar marker, claim, or alias files exist to drift across mounts or
|
|
114
|
+
* strand a later process — a killed holder's transaction is released by the
|
|
115
|
+
* OS and the next acquirer proceeds after the busy window.
|
|
116
|
+
*/
|
|
117
|
+
function openLockedDatabase(path) {
|
|
118
|
+
const database = new DatabaseSync(path, {
|
|
119
|
+
allowExtension: false,
|
|
120
|
+
defensive: true,
|
|
121
|
+
enableDoubleQuotedStringLiterals: false,
|
|
122
|
+
enableForeignKeyConstraints: true,
|
|
123
|
+
readOnly: false,
|
|
124
|
+
timeout: 0,
|
|
125
|
+
});
|
|
574
126
|
try {
|
|
575
|
-
|
|
576
|
-
|
|
127
|
+
const stats = lstatSync(path);
|
|
128
|
+
if (!stats.isFile() || stats.isSymbolicLink())
|
|
129
|
+
throw new Error("scope lock database is not a regular file");
|
|
130
|
+
chmodSync(path, LOCAL_LOCK_DATABASE_MODE);
|
|
131
|
+
if (process.platform !== "win32" && (lstatSync(path).mode & 0o777) !== LOCAL_LOCK_DATABASE_MODE) {
|
|
132
|
+
throw new Error("scope lock database is not private");
|
|
133
|
+
}
|
|
134
|
+
database.exec("PRAGMA journal_mode = DELETE; PRAGMA locking_mode = NORMAL; PRAGMA foreign_keys = ON; PRAGMA trusted_schema = OFF;");
|
|
135
|
+
database.exec("BEGIN IMMEDIATE");
|
|
136
|
+
createProtocolSchema(database);
|
|
137
|
+
validateProtocolSchema(database);
|
|
138
|
+
const journal = database.prepare("PRAGMA journal_mode").get();
|
|
139
|
+
if (journal?.journal_mode !== "delete")
|
|
140
|
+
throw new Error("scope lock database is not using rollback journal mode");
|
|
141
|
+
return database;
|
|
577
142
|
}
|
|
578
143
|
catch (error) {
|
|
579
|
-
closeError = error;
|
|
580
|
-
}
|
|
581
|
-
finally {
|
|
582
144
|
try {
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
catch (error) {
|
|
586
|
-
if (error.code !== "ENOENT") {
|
|
587
|
-
closeError = closeError === undefined ? error : { close: closeError, unlink: error };
|
|
588
|
-
}
|
|
145
|
+
database.close();
|
|
589
146
|
}
|
|
147
|
+
catch { /* preserve primary failure */ }
|
|
148
|
+
throw error;
|
|
590
149
|
}
|
|
591
|
-
if (closeError !== undefined)
|
|
592
|
-
throw closeError;
|
|
593
150
|
}
|
|
594
151
|
async function waitForRetry(delayMs, signal) {
|
|
595
152
|
throwIfAborted(signal);
|
|
@@ -612,39 +169,19 @@ async function waitForRetry(delayMs, signal) {
|
|
|
612
169
|
});
|
|
613
170
|
}
|
|
614
171
|
class SqliteScopeLockLease {
|
|
615
|
-
|
|
616
|
-
root;
|
|
617
|
-
rootIdentity;
|
|
618
|
-
path;
|
|
619
|
-
databaseName;
|
|
620
|
-
marker;
|
|
172
|
+
database;
|
|
621
173
|
scope;
|
|
622
174
|
status = "held";
|
|
623
|
-
constructor(scope,
|
|
624
|
-
this.
|
|
625
|
-
this.root = root;
|
|
626
|
-
this.rootIdentity = rootIdentity;
|
|
627
|
-
this.path = path;
|
|
628
|
-
this.databaseName = databaseName;
|
|
629
|
-
this.marker = marker;
|
|
175
|
+
constructor(scope, database) {
|
|
176
|
+
this.database = database;
|
|
630
177
|
this.scope = scope;
|
|
631
178
|
}
|
|
632
179
|
async assertOwned(signal) {
|
|
633
180
|
assertSignal(signal);
|
|
634
181
|
throwIfAborted(signal);
|
|
635
|
-
if (this.status !== "held" || !this.
|
|
182
|
+
if (this.status !== "held" || !this.database.isOpen || !this.database.isTransaction) {
|
|
636
183
|
throw adapterFailure("scope-lock.assert-owned", new Error("scope lock is no longer held"));
|
|
637
184
|
}
|
|
638
|
-
try {
|
|
639
|
-
const marker = readDatabaseIdentityMarker(this.path);
|
|
640
|
-
if (marker === undefined || !sameDatabaseMarker(marker, this.marker)) {
|
|
641
|
-
throw new Error("scope lock database identity marker changed during ownership");
|
|
642
|
-
}
|
|
643
|
-
validateOpenedDatabaseIdentity(this.prepared, this.root, this.rootIdentity, this.path, this.databaseName, marker);
|
|
644
|
-
}
|
|
645
|
-
catch (error) {
|
|
646
|
-
throw adapterFailure("scope-lock.assert-owned", error);
|
|
647
|
-
}
|
|
648
185
|
}
|
|
649
186
|
async release() {
|
|
650
187
|
if (this.status === "released")
|
|
@@ -652,21 +189,22 @@ class SqliteScopeLockLease {
|
|
|
652
189
|
let rollbackError;
|
|
653
190
|
let closeError;
|
|
654
191
|
try {
|
|
655
|
-
if (this.
|
|
656
|
-
this.
|
|
192
|
+
if (this.database.isOpen && this.database.isTransaction)
|
|
193
|
+
this.database.exec("ROLLBACK");
|
|
657
194
|
}
|
|
658
195
|
catch (error) {
|
|
659
196
|
rollbackError = error;
|
|
660
197
|
}
|
|
661
198
|
finally {
|
|
662
199
|
try {
|
|
663
|
-
|
|
200
|
+
if (this.database.isOpen)
|
|
201
|
+
this.database.close();
|
|
664
202
|
}
|
|
665
203
|
catch (error) {
|
|
666
204
|
closeError = error;
|
|
667
205
|
}
|
|
668
206
|
}
|
|
669
|
-
if (!this.
|
|
207
|
+
if (!this.database.isOpen && closeError === undefined)
|
|
670
208
|
this.status = "released";
|
|
671
209
|
else
|
|
672
210
|
this.status = "uncertain";
|
|
@@ -677,43 +215,31 @@ class SqliteScopeLockLease {
|
|
|
677
215
|
}
|
|
678
216
|
class SqliteScopeLockManager {
|
|
679
217
|
root;
|
|
680
|
-
rootIdentity;
|
|
681
218
|
retryDelayMs;
|
|
682
219
|
random;
|
|
683
|
-
|
|
684
|
-
constructor(root, rootIdentity, retryDelayMs, random, initializationMarkerRead) {
|
|
220
|
+
constructor(root, retryDelayMs, random) {
|
|
685
221
|
this.root = root;
|
|
686
|
-
this.rootIdentity = rootIdentity;
|
|
687
222
|
this.retryDelayMs = retryDelayMs;
|
|
688
223
|
this.random = random;
|
|
689
|
-
this.initializationMarkerRead = initializationMarkerRead;
|
|
690
224
|
}
|
|
691
225
|
async acquire(input, signal) {
|
|
692
226
|
assertSignal(signal);
|
|
693
227
|
const scope = ScopeReferenceSchema.parse(input);
|
|
694
228
|
const path = databasePath(this.root, scope);
|
|
695
|
-
let retryingMissingMarkerDatabase = false;
|
|
696
229
|
for (;;) {
|
|
697
230
|
throwIfAborted(signal);
|
|
698
|
-
let
|
|
231
|
+
let database;
|
|
699
232
|
try {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
prepared.database.exec("BEGIN IMMEDIATE");
|
|
703
|
-
if (signal.aborted) {
|
|
704
|
-
await closeDatabase(prepared);
|
|
233
|
+
database = openLockedDatabase(path);
|
|
234
|
+
if (signal.aborted)
|
|
705
235
|
throw signal.reason;
|
|
706
|
-
|
|
707
|
-
const marker = readDatabaseIdentityMarker(path);
|
|
708
|
-
if (marker === undefined)
|
|
709
|
-
throw new Error("scope lock database identity marker disappeared");
|
|
710
|
-
validateOpenedDatabaseIdentity(prepared, this.root, this.rootIdentity, path, databaseName, marker);
|
|
711
|
-
return new SqliteScopeLockLease(scope, prepared, this.root, this.rootIdentity, path, databaseName, marker);
|
|
236
|
+
return new SqliteScopeLockLease(scope, database);
|
|
712
237
|
}
|
|
713
238
|
catch (error) {
|
|
714
|
-
if (
|
|
239
|
+
if (database !== undefined) {
|
|
715
240
|
try {
|
|
716
|
-
|
|
241
|
+
if (database.isOpen)
|
|
242
|
+
database.close();
|
|
717
243
|
}
|
|
718
244
|
catch (closeError) {
|
|
719
245
|
throw adapterFailure("scope-lock.acquire", cleanupCause(error, closeError));
|
|
@@ -721,17 +247,6 @@ class SqliteScopeLockManager {
|
|
|
721
247
|
}
|
|
722
248
|
if (signal.aborted)
|
|
723
249
|
throw signal.reason;
|
|
724
|
-
if (error instanceof DatabaseInitializationInProgress) {
|
|
725
|
-
const delay = await this.retryDelay();
|
|
726
|
-
await waitForRetry(delay, signal);
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
if (error instanceof DatabaseInitializationSnapshotChanged) {
|
|
730
|
-
retryingMissingMarkerDatabase = true;
|
|
731
|
-
const delay = await this.retryDelay();
|
|
732
|
-
await waitForRetry(delay, signal);
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
250
|
if (!isBusy(error))
|
|
736
251
|
throw adapterFailure("scope-lock.acquire", error);
|
|
737
252
|
const delay = await this.retryDelay();
|
|
@@ -753,54 +268,7 @@ class SqliteScopeLockManager {
|
|
|
753
268
|
return Math.floor(this.retryDelayMs.minimum + value * (this.retryDelayMs.maximum - this.retryDelayMs.minimum));
|
|
754
269
|
}
|
|
755
270
|
}
|
|
756
|
-
|
|
757
|
-
const probeDirectory = await mkdtemp(join(root, ".scope-lock-probe-"));
|
|
758
|
-
const path = join(probeDirectory, "probe.sqlite");
|
|
759
|
-
let holder;
|
|
760
|
-
let contender;
|
|
761
|
-
let released;
|
|
762
|
-
try {
|
|
763
|
-
holder = await prepareDatabase(root, rootIdentity, path, "probe.sqlite");
|
|
764
|
-
holder.database.exec("BEGIN IMMEDIATE");
|
|
765
|
-
try {
|
|
766
|
-
contender = await prepareDatabase(root, rootIdentity, path, "probe.sqlite");
|
|
767
|
-
contender.database.exec("BEGIN IMMEDIATE");
|
|
768
|
-
throw new Error("SQLite did not exclude a second writer");
|
|
769
|
-
}
|
|
770
|
-
catch (error) {
|
|
771
|
-
if (!isBusy(error))
|
|
772
|
-
throw error;
|
|
773
|
-
}
|
|
774
|
-
if (contender !== undefined)
|
|
775
|
-
await closeDatabase(contender);
|
|
776
|
-
contender = undefined;
|
|
777
|
-
holder.database.exec("ROLLBACK");
|
|
778
|
-
await closeDatabase(holder);
|
|
779
|
-
holder = undefined;
|
|
780
|
-
released = await prepareDatabase(root, rootIdentity, path, "probe.sqlite");
|
|
781
|
-
released.database.exec("BEGIN IMMEDIATE");
|
|
782
|
-
released.database.exec("ROLLBACK");
|
|
783
|
-
await closeDatabase(released);
|
|
784
|
-
released = undefined;
|
|
785
|
-
}
|
|
786
|
-
finally {
|
|
787
|
-
for (const prepared of [contender, holder, released]) {
|
|
788
|
-
if (prepared !== undefined) {
|
|
789
|
-
try {
|
|
790
|
-
await closeDatabase(prepared);
|
|
791
|
-
}
|
|
792
|
-
catch {
|
|
793
|
-
// The probe is failing anyway; cleanup must not expose a native error.
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
await rm(probeDirectory, { recursive: true, force: true });
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Create a scope lock manager after proving the configured root has the
|
|
802
|
-
* capabilities this safety boundary needs. No process-local fallback exists.
|
|
803
|
-
*/
|
|
271
|
+
/** Create a scope lock manager rooted at a private, local-filesystem directory. */
|
|
804
272
|
export async function createSqliteScopeLockManager(options) {
|
|
805
273
|
if (options === null || typeof options !== "object")
|
|
806
274
|
throw new TypeError("scope lock options are required");
|
|
@@ -811,10 +279,8 @@ export async function createSqliteScopeLockManager(options) {
|
|
|
811
279
|
throw adapterFailure("scope-lock.initialize", error);
|
|
812
280
|
});
|
|
813
281
|
try {
|
|
814
|
-
const rootIdentity = ensureRootIdentityMarker(root);
|
|
815
282
|
await (options.verifyLocalFilesystem ?? verifyLocalFilesystemCapability)(root);
|
|
816
|
-
|
|
817
|
-
return new SqliteScopeLockManager(root, rootIdentity, retryDelayMs, random, options.initializationMarkerRead);
|
|
283
|
+
return new SqliteScopeLockManager(root, retryDelayMs, random);
|
|
818
284
|
}
|
|
819
285
|
catch (error) {
|
|
820
286
|
if (error instanceof BoundaryError)
|