@cuna_labs/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +169 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/api/capability-evidence.d.ts +19 -0
- package/dist/api/capability-evidence.d.ts.map +1 -0
- package/dist/api/capability-evidence.js +38 -0
- package/dist/api/capability-evidence.js.map +1 -0
- package/dist/api/client.d.ts +167 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +937 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/contracts.d.ts +345 -0
- package/dist/api/contracts.d.ts.map +1 -0
- package/dist/api/contracts.js +976 -0
- package/dist/api/contracts.js.map +1 -0
- package/dist/api/execution-workspaces.d.ts +17 -0
- package/dist/api/execution-workspaces.d.ts.map +1 -0
- package/dist/api/execution-workspaces.js +46 -0
- package/dist/api/execution-workspaces.js.map +1 -0
- package/dist/api/http.d.ts +76 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +838 -0
- package/dist/api/http.js.map +1 -0
- package/dist/api/managed-executions.d.ts +37 -0
- package/dist/api/managed-executions.d.ts.map +1 -0
- package/dist/api/managed-executions.js +80 -0
- package/dist/api/managed-executions.js.map +1 -0
- package/dist/api/observer-v2-schema.d.ts +206 -0
- package/dist/api/observer-v2-schema.d.ts.map +1 -0
- package/dist/api/observer-v2-schema.js +264 -0
- package/dist/api/observer-v2-schema.js.map +1 -0
- package/dist/api/observer-v2.d.ts +33 -0
- package/dist/api/observer-v2.d.ts.map +1 -0
- package/dist/api/observer-v2.js +60 -0
- package/dist/api/observer-v2.js.map +1 -0
- package/dist/api/owner-observe-grants-v2-schema.d.ts +678 -0
- package/dist/api/owner-observe-grants-v2-schema.d.ts.map +1 -0
- package/dist/api/owner-observe-grants-v2-schema.js +887 -0
- package/dist/api/owner-observe-grants-v2-schema.js.map +1 -0
- package/dist/api/owner-observe-grants-v2.d.ts +268 -0
- package/dist/api/owner-observe-grants-v2.d.ts.map +1 -0
- package/dist/api/owner-observe-grants-v2.js +604 -0
- package/dist/api/owner-observe-grants-v2.js.map +1 -0
- package/dist/api/provider-v2-schema.d.ts +293 -0
- package/dist/api/provider-v2-schema.d.ts.map +1 -0
- package/dist/api/provider-v2-schema.js +381 -0
- package/dist/api/provider-v2-schema.js.map +1 -0
- package/dist/api/provider-v2.d.ts +55 -0
- package/dist/api/provider-v2.d.ts.map +1 -0
- package/dist/api/provider-v2.js +54 -0
- package/dist/api/provider-v2.js.map +1 -0
- package/dist/api/remote-workspace.d.ts +29 -0
- package/dist/api/remote-workspace.d.ts.map +1 -0
- package/dist/api/remote-workspace.js +62 -0
- package/dist/api/remote-workspace.js.map +1 -0
- package/dist/api/supervisor-live-update.d.ts +248 -0
- package/dist/api/supervisor-live-update.d.ts.map +1 -0
- package/dist/api/supervisor-live-update.js +434 -0
- package/dist/api/supervisor-live-update.js.map +1 -0
- package/dist/api/terminal-reason.d.ts +5 -0
- package/dist/api/terminal-reason.d.ts.map +1 -0
- package/dist/api/terminal-reason.js +12 -0
- package/dist/api/terminal-reason.js.map +1 -0
- package/dist/auth/browser-handoff.d.ts +59 -0
- package/dist/auth/browser-handoff.d.ts.map +1 -0
- package/dist/auth/browser-handoff.js +33 -0
- package/dist/auth/browser-handoff.js.map +1 -0
- package/dist/auth/browser.d.ts +11 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/browser.js +47 -0
- package/dist/auth/browser.js.map +1 -0
- package/dist/auth/human-client.d.ts +39 -0
- package/dist/auth/human-client.d.ts.map +1 -0
- package/dist/auth/human-client.js +66 -0
- package/dist/auth/human-client.js.map +1 -0
- package/dist/auth/human-contracts.d.ts +52 -0
- package/dist/auth/human-contracts.d.ts.map +1 -0
- package/dist/auth/human-contracts.js +229 -0
- package/dist/auth/human-contracts.js.map +1 -0
- package/dist/auth/human-session.d.ts +48 -0
- package/dist/auth/human-session.d.ts.map +1 -0
- package/dist/auth/human-session.js +902 -0
- package/dist/auth/human-session.js.map +1 -0
- package/dist/auth/onboarding-state.d.ts +31 -0
- package/dist/auth/onboarding-state.d.ts.map +1 -0
- package/dist/auth/onboarding-state.js +52 -0
- package/dist/auth/onboarding-state.js.map +1 -0
- package/dist/auth/pkce.d.ts +15 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +19 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/bin/cuna.d.ts +3 -0
- package/dist/bin/cuna.d.ts.map +1 -0
- package/dist/bin/cuna.js +4 -0
- package/dist/bin/cuna.js.map +1 -0
- package/dist/build-identity.d.ts +20 -0
- package/dist/build-identity.d.ts.map +1 -0
- package/dist/build-identity.js +77 -0
- package/dist/build-identity.js.map +1 -0
- package/dist/cli/command-help.d.ts +9 -0
- package/dist/cli/command-help.d.ts.map +1 -0
- package/dist/cli/command-help.js +324 -0
- package/dist/cli/command-help.js.map +1 -0
- package/dist/cli/help.d.ts +40 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +256 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/output.d.ts +28 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +95 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/parser.d.ts +65 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +294 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/process-entrypoint.d.ts +18 -0
- package/dist/cli/process-entrypoint.d.ts.map +1 -0
- package/dist/cli/process-entrypoint.js +26 -0
- package/dist/cli/process-entrypoint.js.map +1 -0
- package/dist/cli/run.d.ts +114 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +1969 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/commands/commands.d.ts +74 -0
- package/dist/commands/commands.d.ts.map +1 -0
- package/dist/commands/commands.js +2814 -0
- package/dist/commands/commands.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +323 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/infra-contract-witness.d.ts +29 -0
- package/dist/config/infra-contract-witness.d.ts.map +1 -0
- package/dist/config/infra-contract-witness.js +30 -0
- package/dist/config/infra-contract-witness.js.map +1 -0
- package/dist/core/browser-url.d.ts +2 -0
- package/dist/core/browser-url.d.ts.map +1 -0
- package/dist/core/browser-url.js +20 -0
- package/dist/core/browser-url.js.map +1 -0
- package/dist/core/deployed-wire-compatibility.d.ts +15 -0
- package/dist/core/deployed-wire-compatibility.d.ts.map +1 -0
- package/dist/core/deployed-wire-compatibility.js +15 -0
- package/dist/core/deployed-wire-compatibility.js.map +1 -0
- package/dist/core/errors.d.ts +59 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +82 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/exit-codes.d.ts +68 -0
- package/dist/core/exit-codes.d.ts.map +1 -0
- package/dist/core/exit-codes.js +84 -0
- package/dist/core/exit-codes.js.map +1 -0
- package/dist/core/instant.d.ts +60 -0
- package/dist/core/instant.d.ts.map +1 -0
- package/dist/core/instant.js +90 -0
- package/dist/core/instant.js.map +1 -0
- package/dist/core/namespace.d.ts +171 -0
- package/dist/core/namespace.d.ts.map +1 -0
- package/dist/core/namespace.js +227 -0
- package/dist/core/namespace.js.map +1 -0
- package/dist/core/observation-budget.d.ts +185 -0
- package/dist/core/observation-budget.d.ts.map +1 -0
- package/dist/core/observation-budget.js +209 -0
- package/dist/core/observation-budget.js.map +1 -0
- package/dist/core/product-web.d.ts +81 -0
- package/dist/core/product-web.d.ts.map +1 -0
- package/dist/core/product-web.js +87 -0
- package/dist/core/product-web.js.map +1 -0
- package/dist/core/validation.d.ts +90 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +199 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/credentials/contracts.d.ts +82 -0
- package/dist/credentials/contracts.d.ts.map +1 -0
- package/dist/credentials/contracts.js +2 -0
- package/dist/credentials/contracts.js.map +1 -0
- package/dist/credentials/errors.d.ts +19 -0
- package/dist/credentials/errors.d.ts.map +1 -0
- package/dist/credentials/errors.js +16 -0
- package/dist/credentials/errors.js.map +1 -0
- package/dist/credentials/index.d.ts +6 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +6 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/local-session.d.ts +101 -0
- package/dist/credentials/local-session.d.ts.map +1 -0
- package/dist/credentials/local-session.js +839 -0
- package/dist/credentials/local-session.js.map +1 -0
- package/dist/credentials/secret-material.d.ts +17 -0
- package/dist/credentials/secret-material.d.ts.map +1 -0
- package/dist/credentials/secret-material.js +49 -0
- package/dist/credentials/secret-material.js.map +1 -0
- package/dist/credentials/vault.d.ts +41 -0
- package/dist/credentials/vault.d.ts.map +1 -0
- package/dist/credentials/vault.js +719 -0
- package/dist/credentials/vault.js.map +1 -0
- package/dist/daemon/endpoint.d.ts +29 -0
- package/dist/daemon/endpoint.d.ts.map +1 -0
- package/dist/daemon/endpoint.js +66 -0
- package/dist/daemon/endpoint.js.map +1 -0
- package/dist/daemon/journal.d.ts +62 -0
- package/dist/daemon/journal.d.ts.map +1 -0
- package/dist/daemon/journal.js +299 -0
- package/dist/daemon/journal.js.map +1 -0
- package/dist/daemon/lease.d.ts +25 -0
- package/dist/daemon/lease.d.ts.map +1 -0
- package/dist/daemon/lease.js +94 -0
- package/dist/daemon/lease.js.map +1 -0
- package/dist/daemon/lifecycle.d.ts +14 -0
- package/dist/daemon/lifecycle.d.ts.map +1 -0
- package/dist/daemon/lifecycle.js +29 -0
- package/dist/daemon/lifecycle.js.map +1 -0
- package/dist/daemon/protocol.d.ts +65 -0
- package/dist/daemon/protocol.d.ts.map +1 -0
- package/dist/daemon/protocol.js +194 -0
- package/dist/daemon/protocol.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +29 -0
- package/dist/daemon/supervisor.d.ts.map +1 -0
- package/dist/daemon/supervisor.js +87 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/daemon/views.d.ts +31 -0
- package/dist/daemon/views.d.ts.map +1 -0
- package/dist/daemon/views.js +86 -0
- package/dist/daemon/views.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/journey/api-effects.d.ts +59 -0
- package/dist/journey/api-effects.d.ts.map +1 -0
- package/dist/journey/api-effects.js +386 -0
- package/dist/journey/api-effects.js.map +1 -0
- package/dist/journey/derived-identity.d.ts +91 -0
- package/dist/journey/derived-identity.d.ts.map +1 -0
- package/dist/journey/derived-identity.js +123 -0
- package/dist/journey/derived-identity.js.map +1 -0
- package/dist/journey/index.d.ts +6 -0
- package/dist/journey/index.d.ts.map +1 -0
- package/dist/journey/index.js +6 -0
- package/dist/journey/index.js.map +1 -0
- package/dist/journey/intent.d.ts +36 -0
- package/dist/journey/intent.d.ts.map +1 -0
- package/dist/journey/intent.js +191 -0
- package/dist/journey/intent.js.map +1 -0
- package/dist/journey/orchestrator.d.ts +140 -0
- package/dist/journey/orchestrator.d.ts.map +1 -0
- package/dist/journey/orchestrator.js +415 -0
- package/dist/journey/orchestrator.js.map +1 -0
- package/dist/journey/provider-launch-intent.d.ts +12 -0
- package/dist/journey/provider-launch-intent.d.ts.map +1 -0
- package/dist/journey/provider-launch-intent.js +40 -0
- package/dist/journey/provider-launch-intent.js.map +1 -0
- package/dist/journey/remote-workspace.d.ts +34 -0
- package/dist/journey/remote-workspace.d.ts.map +1 -0
- package/dist/journey/remote-workspace.js +91 -0
- package/dist/journey/remote-workspace.js.map +1 -0
- package/dist/journey/root-entry.d.ts +56 -0
- package/dist/journey/root-entry.d.ts.map +1 -0
- package/dist/journey/root-entry.js +19 -0
- package/dist/journey/root-entry.js.map +1 -0
- package/dist/journey/selection.d.ts +165 -0
- package/dist/journey/selection.d.ts.map +1 -0
- package/dist/journey/selection.js +565 -0
- package/dist/journey/selection.js.map +1 -0
- package/dist/journey/session-failure.d.ts +4 -0
- package/dist/journey/session-failure.d.ts.map +1 -0
- package/dist/journey/session-failure.js +28 -0
- package/dist/journey/session-failure.js.map +1 -0
- package/dist/journey/workspace-effects.d.ts +30 -0
- package/dist/journey/workspace-effects.d.ts.map +1 -0
- package/dist/journey/workspace-effects.js +348 -0
- package/dist/journey/workspace-effects.js.map +1 -0
- package/dist/local-actions/broker.d.ts +32 -0
- package/dist/local-actions/broker.d.ts.map +1 -0
- package/dist/local-actions/broker.js +400 -0
- package/dist/local-actions/broker.js.map +1 -0
- package/dist/local-actions/browser-action.d.ts +78 -0
- package/dist/local-actions/browser-action.d.ts.map +1 -0
- package/dist/local-actions/browser-action.js +307 -0
- package/dist/local-actions/browser-action.js.map +1 -0
- package/dist/local-actions/callback-relay.d.ts +45 -0
- package/dist/local-actions/callback-relay.d.ts.map +1 -0
- package/dist/local-actions/callback-relay.js +265 -0
- package/dist/local-actions/callback-relay.js.map +1 -0
- package/dist/local-actions/clipboard.d.ts +3 -0
- package/dist/local-actions/clipboard.d.ts.map +1 -0
- package/dist/local-actions/clipboard.js +18 -0
- package/dist/local-actions/clipboard.js.map +1 -0
- package/dist/local-actions/contracts.d.ts +63 -0
- package/dist/local-actions/contracts.d.ts.map +1 -0
- package/dist/local-actions/contracts.js +18 -0
- package/dist/local-actions/contracts.js.map +1 -0
- package/dist/local-actions/desktop-presentation.d.ts +141 -0
- package/dist/local-actions/desktop-presentation.d.ts.map +1 -0
- package/dist/local-actions/desktop-presentation.js +253 -0
- package/dist/local-actions/desktop-presentation.js.map +1 -0
- package/dist/local-actions/device-selection.d.ts +43 -0
- package/dist/local-actions/device-selection.d.ts.map +1 -0
- package/dist/local-actions/device-selection.js +154 -0
- package/dist/local-actions/device-selection.js.map +1 -0
- package/dist/local-actions/executor.d.ts +49 -0
- package/dist/local-actions/executor.d.ts.map +1 -0
- package/dist/local-actions/executor.js +329 -0
- package/dist/local-actions/executor.js.map +1 -0
- package/dist/local-actions/file-transfers.d.ts +141 -0
- package/dist/local-actions/file-transfers.d.ts.map +1 -0
- package/dist/local-actions/file-transfers.js +446 -0
- package/dist/local-actions/file-transfers.js.map +1 -0
- package/dist/local-actions/foreground-notifications.d.ts +45 -0
- package/dist/local-actions/foreground-notifications.d.ts.map +1 -0
- package/dist/local-actions/foreground-notifications.js +163 -0
- package/dist/local-actions/foreground-notifications.js.map +1 -0
- package/dist/local-actions/git-signing.d.ts +39 -0
- package/dist/local-actions/git-signing.d.ts.map +1 -0
- package/dist/local-actions/git-signing.js +135 -0
- package/dist/local-actions/git-signing.js.map +1 -0
- package/dist/local-actions/index.d.ts +19 -0
- package/dist/local-actions/index.d.ts.map +1 -0
- package/dist/local-actions/index.js +19 -0
- package/dist/local-actions/index.js.map +1 -0
- package/dist/local-actions/local-service-actions.d.ts +87 -0
- package/dist/local-actions/local-service-actions.d.ts.map +1 -0
- package/dist/local-actions/local-service-actions.js +347 -0
- package/dist/local-actions/local-service-actions.js.map +1 -0
- package/dist/local-actions/loopback.d.ts +5 -0
- package/dist/local-actions/loopback.d.ts.map +1 -0
- package/dist/local-actions/loopback.js +15 -0
- package/dist/local-actions/loopback.js.map +1 -0
- package/dist/local-actions/policy-store.d.ts +7 -0
- package/dist/local-actions/policy-store.d.ts.map +1 -0
- package/dist/local-actions/policy-store.js +70 -0
- package/dist/local-actions/policy-store.js.map +1 -0
- package/dist/local-actions/policy.d.ts +21 -0
- package/dist/local-actions/policy.d.ts.map +1 -0
- package/dist/local-actions/policy.js +43 -0
- package/dist/local-actions/policy.js.map +1 -0
- package/dist/local-actions/private-port-forward.d.ts +36 -0
- package/dist/local-actions/private-port-forward.d.ts.map +1 -0
- package/dist/local-actions/private-port-forward.js +154 -0
- package/dist/local-actions/private-port-forward.js.map +1 -0
- package/dist/local-actions/provider-mcp-config.d.ts +101 -0
- package/dist/local-actions/provider-mcp-config.d.ts.map +1 -0
- package/dist/local-actions/provider-mcp-config.js +166 -0
- package/dist/local-actions/provider-mcp-config.js.map +1 -0
- package/dist/local-actions/providers.d.ts +12 -0
- package/dist/local-actions/providers.d.ts.map +1 -0
- package/dist/local-actions/providers.js +42 -0
- package/dist/local-actions/providers.js.map +1 -0
- package/dist/local-actions/schemas.d.ts +4 -0
- package/dist/local-actions/schemas.d.ts.map +1 -0
- package/dist/local-actions/schemas.js +314 -0
- package/dist/local-actions/schemas.js.map +1 -0
- package/dist/local-actions/sensitive-consent.d.ts +32 -0
- package/dist/local-actions/sensitive-consent.d.ts.map +1 -0
- package/dist/local-actions/sensitive-consent.js +47 -0
- package/dist/local-actions/sensitive-consent.js.map +1 -0
- package/dist/machines/command-launch.d.ts +17 -0
- package/dist/machines/command-launch.d.ts.map +1 -0
- package/dist/machines/command-launch.js +52 -0
- package/dist/machines/command-launch.js.map +1 -0
- package/dist/machines/execution-receipt.d.ts +18 -0
- package/dist/machines/execution-receipt.d.ts.map +1 -0
- package/dist/machines/execution-receipt.js +87 -0
- package/dist/machines/execution-receipt.js.map +1 -0
- package/dist/machines/executions-screen.d.ts +6 -0
- package/dist/machines/executions-screen.d.ts.map +1 -0
- package/dist/machines/executions-screen.js +410 -0
- package/dist/machines/executions-screen.js.map +1 -0
- package/dist/machines/explorer.d.ts +75 -0
- package/dist/machines/explorer.d.ts.map +1 -0
- package/dist/machines/explorer.js +1726 -0
- package/dist/machines/explorer.js.map +1 -0
- package/dist/machines/live-supervisor-update.d.ts +325 -0
- package/dist/machines/live-supervisor-update.d.ts.map +1 -0
- package/dist/machines/live-supervisor-update.js +513 -0
- package/dist/machines/live-supervisor-update.js.map +1 -0
- package/dist/machines/machine-first.d.ts +123 -0
- package/dist/machines/machine-first.d.ts.map +1 -0
- package/dist/machines/machine-first.js +119 -0
- package/dist/machines/machine-first.js.map +1 -0
- package/dist/machines/opencode-supervisor.d.ts +51 -0
- package/dist/machines/opencode-supervisor.d.ts.map +1 -0
- package/dist/machines/opencode-supervisor.js +124 -0
- package/dist/machines/opencode-supervisor.js.map +1 -0
- package/dist/machines/pagination.d.ts +4 -0
- package/dist/machines/pagination.d.ts.map +1 -0
- package/dist/machines/pagination.js +7 -0
- package/dist/machines/pagination.js.map +1 -0
- package/dist/machines/provider-availability.d.ts +42 -0
- package/dist/machines/provider-availability.d.ts.map +1 -0
- package/dist/machines/provider-availability.js +82 -0
- package/dist/machines/provider-availability.js.map +1 -0
- package/dist/machines/provider-screen.d.ts +12 -0
- package/dist/machines/provider-screen.d.ts.map +1 -0
- package/dist/machines/provider-screen.js +129 -0
- package/dist/machines/provider-screen.js.map +1 -0
- package/dist/machines/session-actionability.d.ts +86 -0
- package/dist/machines/session-actionability.d.ts.map +1 -0
- package/dist/machines/session-actionability.js +125 -0
- package/dist/machines/session-actionability.js.map +1 -0
- package/dist/machines/session-visibility.d.ts +139 -0
- package/dist/machines/session-visibility.d.ts.map +1 -0
- package/dist/machines/session-visibility.js +109 -0
- package/dist/machines/session-visibility.js.map +1 -0
- package/dist/platform/adapter.d.ts +62 -0
- package/dist/platform/adapter.d.ts.map +1 -0
- package/dist/platform/adapter.js +240 -0
- package/dist/platform/adapter.js.map +1 -0
- package/dist/platform/support.d.ts +12 -0
- package/dist/platform/support.d.ts.map +1 -0
- package/dist/platform/support.js +25 -0
- package/dist/platform/support.js.map +1 -0
- package/dist/pty/contracts.d.ts +45 -0
- package/dist/pty/contracts.d.ts.map +1 -0
- package/dist/pty/contracts.js +2 -0
- package/dist/pty/contracts.js.map +1 -0
- package/dist/pty/evidence-gate.d.ts +14 -0
- package/dist/pty/evidence-gate.d.ts.map +1 -0
- package/dist/pty/evidence-gate.js +35 -0
- package/dist/pty/evidence-gate.js.map +1 -0
- package/dist/pty/index.d.ts +5 -0
- package/dist/pty/index.d.ts.map +1 -0
- package/dist/pty/index.js +5 -0
- package/dist/pty/index.js.map +1 -0
- package/dist/pty/node-host-terminal.d.ts +14 -0
- package/dist/pty/node-host-terminal.d.ts.map +1 -0
- package/dist/pty/node-host-terminal.js +173 -0
- package/dist/pty/node-host-terminal.js.map +1 -0
- package/dist/pty/node-process.d.ts +26 -0
- package/dist/pty/node-process.d.ts.map +1 -0
- package/dist/pty/node-process.js +102 -0
- package/dist/pty/node-process.js.map +1 -0
- package/dist/runtime/api-terminal-control-plane.d.ts +7 -0
- package/dist/runtime/api-terminal-control-plane.d.ts.map +1 -0
- package/dist/runtime/api-terminal-control-plane.js +95 -0
- package/dist/runtime/api-terminal-control-plane.js.map +1 -0
- package/dist/runtime/backoff.d.ts +17 -0
- package/dist/runtime/backoff.d.ts.map +1 -0
- package/dist/runtime/backoff.js +61 -0
- package/dist/runtime/backoff.js.map +1 -0
- package/dist/runtime/boundary.d.ts +171 -0
- package/dist/runtime/boundary.d.ts.map +1 -0
- package/dist/runtime/boundary.js +1858 -0
- package/dist/runtime/boundary.js.map +1 -0
- package/dist/runtime/capability-gate.d.ts +25 -0
- package/dist/runtime/capability-gate.d.ts.map +1 -0
- package/dist/runtime/capability-gate.js +77 -0
- package/dist/runtime/capability-gate.js.map +1 -0
- package/dist/runtime/contracts.d.ts +25 -0
- package/dist/runtime/contracts.d.ts.map +1 -0
- package/dist/runtime/contracts.js +66 -0
- package/dist/runtime/contracts.js.map +1 -0
- package/dist/runtime/errors.d.ts +20 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +22 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/node-foreground-session.d.ts +50 -0
- package/dist/runtime/node-foreground-session.d.ts.map +1 -0
- package/dist/runtime/node-foreground-session.js +431 -0
- package/dist/runtime/node-foreground-session.js.map +1 -0
- package/dist/runtime/node-websocket-connector.d.ts +6 -0
- package/dist/runtime/node-websocket-connector.d.ts.map +1 -0
- package/dist/runtime/node-websocket-connector.js +344 -0
- package/dist/runtime/node-websocket-connector.js.map +1 -0
- package/dist/runtime/observer-screen.d.ts +6 -0
- package/dist/runtime/observer-screen.d.ts.map +1 -0
- package/dist/runtime/observer-screen.js +144 -0
- package/dist/runtime/observer-screen.js.map +1 -0
- package/dist/runtime/owner-grant-operations.d.ts +136 -0
- package/dist/runtime/owner-grant-operations.d.ts.map +1 -0
- package/dist/runtime/owner-grant-operations.js +216 -0
- package/dist/runtime/owner-grant-operations.js.map +1 -0
- package/dist/runtime/owner-grants-screen.d.ts +40 -0
- package/dist/runtime/owner-grants-screen.d.ts.map +1 -0
- package/dist/runtime/owner-grants-screen.js +987 -0
- package/dist/runtime/owner-grants-screen.js.map +1 -0
- package/dist/runtime/terminal-transport.d.ts +85 -0
- package/dist/runtime/terminal-transport.d.ts.map +1 -0
- package/dist/runtime/terminal-transport.js +152 -0
- package/dist/runtime/terminal-transport.js.map +1 -0
- package/dist/sync/continuous-sync-supervisor.d.ts +107 -0
- package/dist/sync/continuous-sync-supervisor.d.ts.map +1 -0
- package/dist/sync/continuous-sync-supervisor.js +1117 -0
- package/dist/sync/continuous-sync-supervisor.js.map +1 -0
- package/dist/sync/index.d.ts +9 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +9 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/journal.d.ts +57 -0
- package/dist/sync/journal.d.ts.map +1 -0
- package/dist/sync/journal.js +685 -0
- package/dist/sync/journal.js.map +1 -0
- package/dist/sync/revisions.d.ts +118 -0
- package/dist/sync/revisions.d.ts.map +1 -0
- package/dist/sync/revisions.js +364 -0
- package/dist/sync/revisions.js.map +1 -0
- package/dist/sync/supervisor.d.ts +107 -0
- package/dist/sync/supervisor.d.ts.map +1 -0
- package/dist/sync/supervisor.js +246 -0
- package/dist/sync/supervisor.js.map +1 -0
- package/dist/sync/workspace-sync-client.d.ts +18 -0
- package/dist/sync/workspace-sync-client.d.ts.map +1 -0
- package/dist/sync/workspace-sync-client.js +150 -0
- package/dist/sync/workspace-sync-client.js.map +1 -0
- package/dist/sync/workspace-sync-coordinator.d.ts +76 -0
- package/dist/sync/workspace-sync-coordinator.d.ts.map +1 -0
- package/dist/sync/workspace-sync-coordinator.js +621 -0
- package/dist/sync/workspace-sync-coordinator.js.map +1 -0
- package/dist/sync/workspace-sync-product-service.d.ts +79 -0
- package/dist/sync/workspace-sync-product-service.d.ts.map +1 -0
- package/dist/sync/workspace-sync-product-service.js +398 -0
- package/dist/sync/workspace-sync-product-service.js.map +1 -0
- package/dist/sync/workspace-sync-protocol.d.ts +145 -0
- package/dist/sync/workspace-sync-protocol.d.ts.map +1 -0
- package/dist/sync/workspace-sync-protocol.js +347 -0
- package/dist/sync/workspace-sync-protocol.js.map +1 -0
- package/dist/terminal/appbar.d.ts +40 -0
- package/dist/terminal/appbar.d.ts.map +1 -0
- package/dist/terminal/appbar.js +55 -0
- package/dist/terminal/appbar.js.map +1 -0
- package/dist/terminal/cell-width.d.ts +3 -0
- package/dist/terminal/cell-width.d.ts.map +1 -0
- package/dist/terminal/cell-width.js +53 -0
- package/dist/terminal/cell-width.js.map +1 -0
- package/dist/terminal/codec.d.ts +107 -0
- package/dist/terminal/codec.d.ts.map +1 -0
- package/dist/terminal/codec.js +538 -0
- package/dist/terminal/codec.js.map +1 -0
- package/dist/terminal/foreground.d.ts +135 -0
- package/dist/terminal/foreground.d.ts.map +1 -0
- package/dist/terminal/foreground.js +2097 -0
- package/dist/terminal/foreground.js.map +1 -0
- package/dist/terminal/generated/terminal-wire-v1.d.ts +26 -0
- package/dist/terminal/generated/terminal-wire-v1.d.ts.map +1 -0
- package/dist/terminal/generated/terminal-wire-v1.js +27 -0
- package/dist/terminal/generated/terminal-wire-v1.js.map +1 -0
- package/dist/terminal/mode.d.ts +33 -0
- package/dist/terminal/mode.d.ts.map +1 -0
- package/dist/terminal/mode.js +99 -0
- package/dist/terminal/mode.js.map +1 -0
- package/dist/terminal/passthrough.d.ts +47 -0
- package/dist/terminal/passthrough.d.ts.map +1 -0
- package/dist/terminal/passthrough.js +666 -0
- package/dist/terminal/passthrough.js.map +1 -0
- package/dist/terminal/resume.d.ts +58 -0
- package/dist/terminal/resume.d.ts.map +1 -0
- package/dist/terminal/resume.js +185 -0
- package/dist/terminal/resume.js.map +1 -0
- package/dist/terminal/viewport.d.ts +93 -0
- package/dist/terminal/viewport.d.ts.map +1 -0
- package/dist/terminal/viewport.js +266 -0
- package/dist/terminal/viewport.js.map +1 -0
- package/dist/terminal/workbench.d.ts +32 -0
- package/dist/terminal/workbench.d.ts.map +1 -0
- package/dist/terminal/workbench.js +316 -0
- package/dist/terminal/workbench.js.map +1 -0
- package/dist/terminal/xterm-vte.d.ts +49 -0
- package/dist/terminal/xterm-vte.d.ts.map +1 -0
- package/dist/terminal/xterm-vte.js +602 -0
- package/dist/terminal/xterm-vte.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace/binding-path.d.ts +17 -0
- package/dist/workspace/binding-path.d.ts.map +1 -0
- package/dist/workspace/binding-path.js +91 -0
- package/dist/workspace/binding-path.js.map +1 -0
- package/dist/workspace/binding-store.d.ts +107 -0
- package/dist/workspace/binding-store.d.ts.map +1 -0
- package/dist/workspace/binding-store.js +783 -0
- package/dist/workspace/binding-store.js.map +1 -0
- package/dist/workspace/binding.d.ts +35 -0
- package/dist/workspace/binding.d.ts.map +1 -0
- package/dist/workspace/binding.js +70 -0
- package/dist/workspace/binding.js.map +1 -0
- package/dist/workspace/errors.d.ts +4 -0
- package/dist/workspace/errors.d.ts.map +1 -0
- package/dist/workspace/errors.js +19 -0
- package/dist/workspace/errors.js.map +1 -0
- package/dist/workspace/exclusion.d.ts +21 -0
- package/dist/workspace/exclusion.d.ts.map +1 -0
- package/dist/workspace/exclusion.js +243 -0
- package/dist/workspace/exclusion.js.map +1 -0
- package/dist/workspace/index.d.ts +8 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +8 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/lifecycle.d.ts +56 -0
- package/dist/workspace/lifecycle.d.ts.map +1 -0
- package/dist/workspace/lifecycle.js +94 -0
- package/dist/workspace/lifecycle.js.map +1 -0
- package/dist/workspace/manifest.d.ts +47 -0
- package/dist/workspace/manifest.d.ts.map +1 -0
- package/dist/workspace/manifest.js +243 -0
- package/dist/workspace/manifest.js.map +1 -0
- package/dist/workspace/paths.d.ts +14 -0
- package/dist/workspace/paths.d.ts.map +1 -0
- package/dist/workspace/paths.js +67 -0
- package/dist/workspace/paths.js.map +1 -0
- package/dist/workspace/schema.d.ts +15 -0
- package/dist/workspace/schema.d.ts.map +1 -0
- package/dist/workspace/schema.js +33 -0
- package/dist/workspace/schema.js.map +1 -0
- package/dist/workspace/selection-screen.d.ts +4 -0
- package/dist/workspace/selection-screen.d.ts.map +1 -0
- package/dist/workspace/selection-screen.js +216 -0
- package/dist/workspace/selection-screen.js.map +1 -0
- package/dist/workspace/selection-service.d.ts +16 -0
- package/dist/workspace/selection-service.d.ts.map +1 -0
- package/dist/workspace/selection-service.js +65 -0
- package/dist/workspace/selection-service.js.map +1 -0
- package/node_modules/@xterm/headless/README.md +31 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js +2 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js.map +1 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs.map +7 -0
- package/node_modules/@xterm/headless/package.json +29 -0
- package/node_modules/@xterm/headless/typings/xterm-headless.d.ts +1385 -0
- package/package.json +75 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { chmod, lstat, mkdir, open, readFile, realpath, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { basename, dirname, resolve } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { createServer } from "node:net";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
import { performance } from "node:perf_hooks";
|
|
10
|
+
import { CREDENTIAL_BACKEND_PROTOCOL } from "./contracts.js";
|
|
11
|
+
import { CredentialBoundaryError, credentialFailure } from "./errors.js";
|
|
12
|
+
const KEY_BYTES = 32;
|
|
13
|
+
const NONCE_BYTES = 12;
|
|
14
|
+
const TAG_BYTES = 16;
|
|
15
|
+
const MAX_SESSION_BYTES = 96 * 1024;
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
const WINDOWS_SYSTEM32 = "C:\\Windows\\System32";
|
|
18
|
+
const WINDOWS_ACL_PATH_ENVIRONMENT = "CUNA_SESSION_ACL_PATH_V1";
|
|
19
|
+
const WINDOWS_ACL_PATHS_ENVIRONMENT = "CUNA_SESSION_ACL_PATHS_V1";
|
|
20
|
+
const WINDOWS_ACL_CHILD_ENVIRONMENT_NAMES = new Set([
|
|
21
|
+
WINDOWS_ACL_PATH_ENVIRONMENT,
|
|
22
|
+
WINDOWS_ACL_PATHS_ENVIRONMENT,
|
|
23
|
+
// This name is intentionally stripped even though no current command uses
|
|
24
|
+
// it: a stale inherited value must not travel into the constrained child.
|
|
25
|
+
"CUNA_SESSION_ACL_SID_V1",
|
|
26
|
+
]);
|
|
27
|
+
// The batched inspection reads a newline-separated path list. Win32 paths
|
|
28
|
+
// cannot legally contain control characters; refuse rather than misparse.
|
|
29
|
+
const WINDOWS_ACL_BATCH_PATH_SEPARATOR = "\n";
|
|
30
|
+
const WINDOWS_ACL_BATCH_MAX_PATHS = 8;
|
|
31
|
+
// Windows ACL verification may invoke several bounded OS helpers while an
|
|
32
|
+
// operation owns the physical lock. Keep a finite deadline, but make it longer
|
|
33
|
+
// than that legitimate critical section so a second Cuna process can observe a
|
|
34
|
+
// committed revision instead of failing spuriously while the first is healthy.
|
|
35
|
+
const LOCK_TIMEOUT_MS = 90_000;
|
|
36
|
+
const LOCK_RETRY_MS = 20;
|
|
37
|
+
/**
|
|
38
|
+
* These programs are deliberately static. Dynamic values are supplied through
|
|
39
|
+
* a child-only environment instead of being interpolated into `-Command`.
|
|
40
|
+
* This keeps the Restricted-policy-compatible invocation while avoiding
|
|
41
|
+
* encoded/dynamically generated PowerShell source.
|
|
42
|
+
*/
|
|
43
|
+
export const WINDOWS_ACL_COMMAND_PROGRAMS = Object.freeze({
|
|
44
|
+
inspect: [
|
|
45
|
+
"$ErrorActionPreference = 'Stop'",
|
|
46
|
+
"Import-Module 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules\\Microsoft.PowerShell.Security\\Microsoft.PowerShell.Security.psd1' -ErrorAction Stop",
|
|
47
|
+
"$path = $env:CUNA_SESSION_ACL_PATH_V1",
|
|
48
|
+
"if ([string]::IsNullOrWhiteSpace($path)) { throw 'Cuna session ACL path is unavailable.' }",
|
|
49
|
+
"$current = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
50
|
+
"$sddl = (Get-Acl -LiteralPath $path).Sddl",
|
|
51
|
+
"[Console]::WriteLine('CUNA_CURRENT_SID=' + $current)",
|
|
52
|
+
"[Console]::WriteLine('CUNA_SDDL=' + $sddl)",
|
|
53
|
+
].join("; "),
|
|
54
|
+
// One spawn inspects several paths. A path that cannot be inspected (most
|
|
55
|
+
// often: it does not exist yet) emits no line; the caller treats a missing
|
|
56
|
+
// index as "not inspected", never as "safe", and falls back to `inspect`.
|
|
57
|
+
inspectMany: [
|
|
58
|
+
"$ErrorActionPreference = 'Stop'",
|
|
59
|
+
"Import-Module 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules\\Microsoft.PowerShell.Security\\Microsoft.PowerShell.Security.psd1' -ErrorAction Stop",
|
|
60
|
+
"$list = $env:CUNA_SESSION_ACL_PATHS_V1",
|
|
61
|
+
"if ([string]::IsNullOrWhiteSpace($list)) { throw 'Cuna session ACL paths are unavailable.' }",
|
|
62
|
+
"$paths = $list.Split([char]10)",
|
|
63
|
+
"$current = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
64
|
+
"[Console]::WriteLine('CUNA_CURRENT_SID=' + $current)",
|
|
65
|
+
"for ($i = 0; $i -lt $paths.Length; $i++) {",
|
|
66
|
+
" $path = $paths[$i]",
|
|
67
|
+
" if ([string]::IsNullOrWhiteSpace($path)) { continue }",
|
|
68
|
+
" try { $sddl = (Get-Acl -LiteralPath $path).Sddl } catch { continue }",
|
|
69
|
+
" [Console]::WriteLine('CUNA_SDDL:' + $i + '=' + $sddl)",
|
|
70
|
+
"}",
|
|
71
|
+
// A caught per-path error leaves `$?` false, which `-Command` would report
|
|
72
|
+
// as exit code 1 and the parent would read as a failed batch.
|
|
73
|
+
"exit 0",
|
|
74
|
+
].join("; "),
|
|
75
|
+
reconcileFile: [
|
|
76
|
+
"$ErrorActionPreference = 'Stop'",
|
|
77
|
+
"Import-Module 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules\\Microsoft.PowerShell.Security\\Microsoft.PowerShell.Security.psd1' -ErrorAction Stop",
|
|
78
|
+
"$path = $env:CUNA_SESSION_ACL_PATH_V1",
|
|
79
|
+
"if ([string]::IsNullOrWhiteSpace($path)) { throw 'Cuna session ACL path is unavailable.' }",
|
|
80
|
+
"$beforeCurrent = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
81
|
+
"$beforeSddl = (Get-Acl -LiteralPath $path).Sddl",
|
|
82
|
+
"$beforeOwner = [regex]::Match($beforeSddl, '^O:(S-1-[0-9-]+)(?:G:|D:|S:)').Groups[1].Value",
|
|
83
|
+
"if ([string]::IsNullOrWhiteSpace($beforeOwner) -or $beforeOwner -ne $beforeCurrent) { throw 'Cuna session ACL owner is not the current user.' }",
|
|
84
|
+
"$daclStart = $beforeSddl.IndexOf('D:')",
|
|
85
|
+
"$saclStart = if ($daclStart -lt 0) { -1 } else { $beforeSddl.IndexOf('S:', $daclStart + 2) }",
|
|
86
|
+
"$beforeDacl = if ($daclStart -lt 0) { '' } elseif ($saclStart -lt 0) { $beforeSddl.Substring($daclStart) } else { $beforeSddl.Substring($daclStart, $saclStart - $daclStart) }",
|
|
87
|
+
"$expectedAce = '(A;;FA;;;' + $beforeCurrent + ')'",
|
|
88
|
+
"$exact = $beforeDacl -ceq ('D:PAI' + $expectedAce) -or $beforeDacl -ceq ('D:P' + $expectedAce)",
|
|
89
|
+
"$repaired = 0",
|
|
90
|
+
"if (-not $exact) {",
|
|
91
|
+
" $currentBeforeWrite = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
92
|
+
" if ($currentBeforeWrite -ne $beforeCurrent) { throw 'Cuna session ACL SID changed.' }",
|
|
93
|
+
" $sid = [System.Security.Principal.SecurityIdentifier]::new($beforeCurrent)",
|
|
94
|
+
" $acl = [System.Security.AccessControl.FileSecurity]::new()",
|
|
95
|
+
" $acl.SetOwner($sid)",
|
|
96
|
+
" $acl.SetAccessRuleProtection($true, $false)",
|
|
97
|
+
" $rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, [System.Security.AccessControl.FileSystemRights]::FullControl, [System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow)",
|
|
98
|
+
" $acl.AddAccessRule($rule)",
|
|
99
|
+
" Set-Acl -LiteralPath $path -AclObject $acl",
|
|
100
|
+
" $repaired = 1",
|
|
101
|
+
"}",
|
|
102
|
+
"$afterCurrent = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
103
|
+
"$afterSddl = (Get-Acl -LiteralPath $path).Sddl",
|
|
104
|
+
"[Console]::WriteLine('CUNA_BEFORE_CURRENT_SID=' + $beforeCurrent)",
|
|
105
|
+
"[Console]::WriteLine('CUNA_BEFORE_SDDL=' + $beforeSddl)",
|
|
106
|
+
"[Console]::WriteLine('CUNA_AFTER_CURRENT_SID=' + $afterCurrent)",
|
|
107
|
+
"[Console]::WriteLine('CUNA_AFTER_SDDL=' + $afterSddl)",
|
|
108
|
+
"[Console]::WriteLine('CUNA_REPAIRED=' + $repaired)",
|
|
109
|
+
].join("; "),
|
|
110
|
+
reconcileDirectory: [
|
|
111
|
+
"$ErrorActionPreference = 'Stop'",
|
|
112
|
+
"Import-Module 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules\\Microsoft.PowerShell.Security\\Microsoft.PowerShell.Security.psd1' -ErrorAction Stop",
|
|
113
|
+
"$path = $env:CUNA_SESSION_ACL_PATH_V1",
|
|
114
|
+
"if ([string]::IsNullOrWhiteSpace($path)) { throw 'Cuna session ACL path is unavailable.' }",
|
|
115
|
+
"$beforeCurrent = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
116
|
+
"$beforeSddl = (Get-Acl -LiteralPath $path).Sddl",
|
|
117
|
+
"$beforeOwner = [regex]::Match($beforeSddl, '^O:(S-1-[0-9-]+)(?:G:|D:|S:)').Groups[1].Value",
|
|
118
|
+
"if ([string]::IsNullOrWhiteSpace($beforeOwner) -or $beforeOwner -ne $beforeCurrent) { throw 'Cuna session ACL owner is not the current user.' }",
|
|
119
|
+
"$daclStart = $beforeSddl.IndexOf('D:')",
|
|
120
|
+
"$saclStart = if ($daclStart -lt 0) { -1 } else { $beforeSddl.IndexOf('S:', $daclStart + 2) }",
|
|
121
|
+
"$beforeDacl = if ($daclStart -lt 0) { '' } elseif ($saclStart -lt 0) { $beforeSddl.Substring($daclStart) } else { $beforeSddl.Substring($daclStart, $saclStart - $daclStart) }",
|
|
122
|
+
"$expectedAce = '(A;OICI;FA;;;' + $beforeCurrent + ')'",
|
|
123
|
+
"$exact = $beforeDacl -ceq ('D:PAI' + $expectedAce) -or $beforeDacl -ceq ('D:P' + $expectedAce)",
|
|
124
|
+
"$repaired = 0",
|
|
125
|
+
"if (-not $exact) {",
|
|
126
|
+
" $currentBeforeWrite = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
127
|
+
" if ($currentBeforeWrite -ne $beforeCurrent) { throw 'Cuna session ACL SID changed.' }",
|
|
128
|
+
" $sid = [System.Security.Principal.SecurityIdentifier]::new($beforeCurrent)",
|
|
129
|
+
" $acl = [System.Security.AccessControl.DirectorySecurity]::new()",
|
|
130
|
+
" $acl.SetOwner($sid)",
|
|
131
|
+
" $acl.SetAccessRuleProtection($true, $false)",
|
|
132
|
+
" $rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, [System.Security.AccessControl.FileSystemRights]::FullControl, [System.Security.AccessControl.InheritanceFlags]::ObjectInherit -bor [System.Security.AccessControl.InheritanceFlags]::ContainerInherit, [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow)",
|
|
133
|
+
" $acl.AddAccessRule($rule)",
|
|
134
|
+
" Set-Acl -LiteralPath $path -AclObject $acl",
|
|
135
|
+
" $repaired = 1",
|
|
136
|
+
"}",
|
|
137
|
+
"$afterCurrent = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
138
|
+
"$afterSddl = (Get-Acl -LiteralPath $path).Sddl",
|
|
139
|
+
"[Console]::WriteLine('CUNA_BEFORE_CURRENT_SID=' + $beforeCurrent)",
|
|
140
|
+
"[Console]::WriteLine('CUNA_BEFORE_SDDL=' + $beforeSddl)",
|
|
141
|
+
"[Console]::WriteLine('CUNA_AFTER_CURRENT_SID=' + $afterCurrent)",
|
|
142
|
+
"[Console]::WriteLine('CUNA_AFTER_SDDL=' + $afterSddl)",
|
|
143
|
+
"[Console]::WriteLine('CUNA_REPAIRED=' + $repaired)",
|
|
144
|
+
].join("; "),
|
|
145
|
+
});
|
|
146
|
+
function windowsAclScopeKey(path, directory) {
|
|
147
|
+
return `${directory ? "d" : "f"}:${path}`;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Pure-JavaScript session persistence for the durable browser login code.
|
|
151
|
+
* The random AES key and ciphertext are separate current-user-only files.
|
|
152
|
+
* Their separation is not a backup boundary: a copied profile containing both
|
|
153
|
+
* files, or compromise of the same OS account, can decrypt the session.
|
|
154
|
+
*/
|
|
155
|
+
export class LocalEncryptedSessionBackend {
|
|
156
|
+
backendId = "cuna-local-aes256gcm-v1";
|
|
157
|
+
platform;
|
|
158
|
+
#sessionFile;
|
|
159
|
+
#keyFile;
|
|
160
|
+
#clock;
|
|
161
|
+
#lockTimeoutMs;
|
|
162
|
+
#windowsAcl;
|
|
163
|
+
#aclScope;
|
|
164
|
+
constructor(input) {
|
|
165
|
+
this.#sessionFile = exactJsonPath(input.sessionFile, "session");
|
|
166
|
+
this.#keyFile = exactKeyPath(input.keyFile);
|
|
167
|
+
if (this.#sessionFile === this.#keyFile || dirname(this.#sessionFile) !== dirname(this.#keyFile)) {
|
|
168
|
+
throw credentialFailure("credential_backend_unverified", "The encrypted session paths are invalid.");
|
|
169
|
+
}
|
|
170
|
+
this.platform = input.platform;
|
|
171
|
+
this.#clock = input.clock ?? Date.now;
|
|
172
|
+
this.#lockTimeoutMs = input.lockTimeoutMs ?? LOCK_TIMEOUT_MS;
|
|
173
|
+
this.#windowsAcl = input.windowsAcl ?? WINDOWS_ACL_AUTHORITY;
|
|
174
|
+
if (!Number.isSafeInteger(this.#lockTimeoutMs) || this.#lockTimeoutMs < 10 || this.#lockTimeoutMs > 120_000) {
|
|
175
|
+
throw credentialFailure("credential_backend_unverified", "The encrypted session lock timeout is invalid.");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async probe() {
|
|
179
|
+
const now = this.#clock();
|
|
180
|
+
if (!Number.isSafeInteger(now) || now < 0)
|
|
181
|
+
throw credentialFailure("credential_backend_unverified", "The session clock is not trustworthy.");
|
|
182
|
+
try {
|
|
183
|
+
await this.#withProcessLock(async () => {
|
|
184
|
+
for (const [file, maximum] of [[this.#keyFile, KEY_BYTES], [this.#sessionFile, MAX_SESSION_BYTES]]) {
|
|
185
|
+
try {
|
|
186
|
+
await this.#assertSafeIfPresent(file, maximum);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (error.code !== "ENOENT")
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return evidence(this, now, "verified");
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return evidence(this, now, "unavailable", "encrypted_session_permissions_unverified");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async read(_target) {
|
|
201
|
+
return await this.#withProcessLock(async () => this.#readUnlocked());
|
|
202
|
+
}
|
|
203
|
+
async #readUnlocked() {
|
|
204
|
+
let encoded;
|
|
205
|
+
try {
|
|
206
|
+
await this.#assertSafeIfPresent(this.#sessionFile, MAX_SESSION_BYTES);
|
|
207
|
+
encoded = await readFile(this.#sessionFile);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error.code === "ENOENT") {
|
|
211
|
+
// A prior delete may have stopped after removing the ciphertext. Retry
|
|
212
|
+
// the other half so a new process converges to zero local residue.
|
|
213
|
+
await this.#deleteFiles([this.#keyFile]);
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
throw localSessionReadFailure("ciphertext", error);
|
|
217
|
+
}
|
|
218
|
+
let key;
|
|
219
|
+
try {
|
|
220
|
+
await this.#assertSafeIfPresent(this.#keyFile, KEY_BYTES);
|
|
221
|
+
key = await readFile(this.#keyFile);
|
|
222
|
+
if (key.byteLength !== KEY_BYTES) {
|
|
223
|
+
key.fill(0);
|
|
224
|
+
throw credentialFailure("credential_corrupt", "The encrypted local session key file is invalid.");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
encoded.fill(0);
|
|
229
|
+
// Only an observed absence proves the key can never decrypt this
|
|
230
|
+
// ciphertext. An ACL, metadata, or transient OS failure is not evidence
|
|
231
|
+
// that either file is corrupt; deleting a valid durable login code in
|
|
232
|
+
// that case would turn a fail-closed read into destructive data loss.
|
|
233
|
+
if (error.code === "ENOENT") {
|
|
234
|
+
await this.#deleteFiles([this.#sessionFile, this.#keyFile]);
|
|
235
|
+
throw credentialFailure("credential_corrupt", "The encrypted local session key is unavailable.", { cause: error });
|
|
236
|
+
}
|
|
237
|
+
throw localSessionReadFailure("key", error);
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return decrypt(encoded, key);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
key.fill(0);
|
|
244
|
+
encoded.fill(0);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async replace(_target, protectedValue) {
|
|
248
|
+
if (protectedValue.byteLength < 1 || protectedValue.byteLength > 32 * 1024) {
|
|
249
|
+
throw credentialFailure("credential_corrupt", "The encrypted session payload is invalid.");
|
|
250
|
+
}
|
|
251
|
+
await this.#withProcessLock(async () => this.#replaceUnlocked(protectedValue));
|
|
252
|
+
}
|
|
253
|
+
async #replaceUnlocked(protectedValue) {
|
|
254
|
+
const key = await this.#loadOrCreateKey();
|
|
255
|
+
const record = encrypt(protectedValue, key);
|
|
256
|
+
key.fill(0);
|
|
257
|
+
await this.#writeRestricted(this.#sessionFile, Buffer.from(JSON.stringify(record), "utf8"));
|
|
258
|
+
}
|
|
259
|
+
async compareAndSwap(_target, expectedSha256, protectedValue) {
|
|
260
|
+
validateExpectedDigest(expectedSha256);
|
|
261
|
+
if (protectedValue.byteLength < 1 || protectedValue.byteLength > 32 * 1024) {
|
|
262
|
+
throw credentialFailure("credential_corrupt", "The encrypted session payload is invalid.");
|
|
263
|
+
}
|
|
264
|
+
return await this.#withProcessLock(async () => {
|
|
265
|
+
const observed = await this.#protectedValueDigest(_target);
|
|
266
|
+
if (observed !== expectedSha256)
|
|
267
|
+
return "conflict";
|
|
268
|
+
await this.#replaceUnlocked(protectedValue);
|
|
269
|
+
return "replaced";
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async compareAndDelete(_target, expectedSha256) {
|
|
273
|
+
validateExpectedDigest(expectedSha256);
|
|
274
|
+
return await this.#withProcessLock(async () => {
|
|
275
|
+
const observed = await this.#protectedValueDigest(_target);
|
|
276
|
+
if (observed === null)
|
|
277
|
+
return "absent";
|
|
278
|
+
if (observed !== expectedSha256)
|
|
279
|
+
return "conflict";
|
|
280
|
+
return await this.#deleteFiles([this.#sessionFile, this.#keyFile]);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async delete(_target) {
|
|
284
|
+
return await this.#withProcessLock(async () => this.#deleteFiles([this.#sessionFile, this.#keyFile]));
|
|
285
|
+
}
|
|
286
|
+
async withRefreshLock(_target, operation) {
|
|
287
|
+
return await this.#withNamedProcessLock("refresh", operation);
|
|
288
|
+
}
|
|
289
|
+
async #deleteFiles(files) {
|
|
290
|
+
let deleted = false;
|
|
291
|
+
let failure;
|
|
292
|
+
for (const file of files) {
|
|
293
|
+
try {
|
|
294
|
+
await unlink(file);
|
|
295
|
+
deleted = true;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (error.code !== "ENOENT" && failure === undefined)
|
|
299
|
+
failure = error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (failure !== undefined) {
|
|
303
|
+
throw credentialFailure("credential_backend_failure", "The encrypted local session could not be removed.", { retryable: true, cause: failure });
|
|
304
|
+
}
|
|
305
|
+
return deleted ? "deleted" : "absent";
|
|
306
|
+
}
|
|
307
|
+
async #protectedValueDigest(target) {
|
|
308
|
+
void target;
|
|
309
|
+
const bytes = await this.#readUnlocked();
|
|
310
|
+
if (bytes === undefined)
|
|
311
|
+
return null;
|
|
312
|
+
try {
|
|
313
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
bytes.fill(0);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async #withProcessLock(operation) {
|
|
320
|
+
return await this.#withNamedProcessLock("storage", operation);
|
|
321
|
+
}
|
|
322
|
+
async #withNamedProcessLock(namespace, operation) {
|
|
323
|
+
// Canonicalize only after the directory exists and its no-reparse/ACL
|
|
324
|
+
// boundary has been checked. Lexical aliases (junctions, symlinks, 8.3
|
|
325
|
+
// names) must never create a second lock namespace for the same bytes.
|
|
326
|
+
await this.#ensureDirectory({ verifyExistingWindowsAcl: false });
|
|
327
|
+
const authority = await processLockAuthority(this.#sessionFile, this.platform, namespace);
|
|
328
|
+
const deadline = performance.now() + this.#lockTimeoutMs;
|
|
329
|
+
let server;
|
|
330
|
+
for (;;) {
|
|
331
|
+
try {
|
|
332
|
+
server = await listenForLock(authority);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (error.code !== "EADDRINUSE" || performance.now() >= deadline) {
|
|
337
|
+
throw credentialFailure("credential_backend_failure", "The encrypted local session is busy and could not be locked safely.", { retryable: true, safeDetails: { reason: "process_lock_unavailable" } });
|
|
338
|
+
}
|
|
339
|
+
await delay(LOCK_RETRY_MS);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Nested locks (refresh around storage) each get their own scope; the
|
|
343
|
+
// innermost operation is the unit of ACL freshness.
|
|
344
|
+
const previousScope = this.#aclScope;
|
|
345
|
+
this.#aclScope = { batched: false, failure: undefined, entries: new Map() };
|
|
346
|
+
try {
|
|
347
|
+
// The initial check is required before resolving the physical lock
|
|
348
|
+
// authority. Re-check once after acquiring it: this keeps the prior
|
|
349
|
+
// boundary against a directory replacement between those steps. Key
|
|
350
|
+
// loading follows immediately, while atomic replacement retains its
|
|
351
|
+
// own final boundary check before changing ciphertext.
|
|
352
|
+
await this.#ensureDirectory();
|
|
353
|
+
if (!sameProcessLockAuthority(authority, await processLockAuthority(this.#sessionFile, this.platform, namespace))) {
|
|
354
|
+
throw credentialFailure("credential_backend_failure", "The encrypted local session path changed while acquiring its lock.", { retryable: true, safeDetails: { reason: "process_lock_authority_changed" } });
|
|
355
|
+
}
|
|
356
|
+
return await operation();
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
this.#aclScope = previousScope;
|
|
360
|
+
await closeServer(server);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* One ACL observation per path per store operation. The first request in an
|
|
365
|
+
* operation inspects the directory, the key and the ciphertext in a single
|
|
366
|
+
* OS invocation when the authority supports it; every later request in the
|
|
367
|
+
* same operation is served from that observation. A path the batch could
|
|
368
|
+
* not inspect is inspected on its own, and a batch failure is raised at the
|
|
369
|
+
* first enforcement site rather than silently retried.
|
|
370
|
+
*/
|
|
371
|
+
async #inspectWindowsAcl(path, directory) {
|
|
372
|
+
const scope = this.#aclScope;
|
|
373
|
+
if (scope === undefined)
|
|
374
|
+
return await this.#windowsAcl.inspectOwnerOnly(path, directory);
|
|
375
|
+
const key = windowsAclScopeKey(path, directory);
|
|
376
|
+
const cached = scope.entries.get(key);
|
|
377
|
+
if (cached !== undefined)
|
|
378
|
+
return cached;
|
|
379
|
+
const inspectMany = this.#windowsAcl.inspectManyOwnerOnly;
|
|
380
|
+
if (!scope.batched && inspectMany !== undefined) {
|
|
381
|
+
scope.batched = true;
|
|
382
|
+
const requests = [
|
|
383
|
+
{ path: dirname(this.#sessionFile), directory: true },
|
|
384
|
+
{ path: this.#keyFile, directory: false },
|
|
385
|
+
{ path: this.#sessionFile, directory: false },
|
|
386
|
+
];
|
|
387
|
+
try {
|
|
388
|
+
const results = await inspectMany.call(this.#windowsAcl, requests);
|
|
389
|
+
if (results.length !== requests.length)
|
|
390
|
+
throw new Error("Windows ACL batch inspection is misaligned");
|
|
391
|
+
requests.forEach((request, index) => {
|
|
392
|
+
const result = results[index];
|
|
393
|
+
if (result !== undefined)
|
|
394
|
+
scope.entries.set(windowsAclScopeKey(request.path, request.directory), result);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
scope.failure = error;
|
|
399
|
+
}
|
|
400
|
+
const batched = scope.entries.get(key);
|
|
401
|
+
if (batched !== undefined)
|
|
402
|
+
return batched;
|
|
403
|
+
}
|
|
404
|
+
if (scope.failure !== undefined)
|
|
405
|
+
throw scope.failure;
|
|
406
|
+
const inspection = await this.#windowsAcl.inspectOwnerOnly(path, directory);
|
|
407
|
+
scope.entries.set(key, inspection);
|
|
408
|
+
return inspection;
|
|
409
|
+
}
|
|
410
|
+
async #assertWindowsOwnerOnlyAcl(path, directory) {
|
|
411
|
+
assertWindowsOwnerOnlyInspection(await this.#inspectWindowsAcl(path, directory));
|
|
412
|
+
}
|
|
413
|
+
async #ensureNewWindowsOwnerOnlyAcl(path, directory, assertSafeMetadata) {
|
|
414
|
+
// A reconciliation rewrites the ACL; any observation of this path taken
|
|
415
|
+
// earlier in the same operation is stale from here on.
|
|
416
|
+
this.#aclScope?.entries.delete(windowsAclScopeKey(path, directory));
|
|
417
|
+
await ensureNewWindowsOwnerOnlyAcl(this.#windowsAcl, path, directory, assertSafeMetadata);
|
|
418
|
+
}
|
|
419
|
+
async #ensureDirectory(input = {}) {
|
|
420
|
+
const directory = dirname(this.#sessionFile);
|
|
421
|
+
const created = await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
422
|
+
await this.#assertSafeDirectoryMetadata(directory);
|
|
423
|
+
if (this.platform === "win32") {
|
|
424
|
+
if (created === undefined) {
|
|
425
|
+
// An already-present directory can contain an attacker-controlled
|
|
426
|
+
// junction or material. It is a verification boundary, never a repair
|
|
427
|
+
// target. Before acquiring the physical lock we only need the metadata
|
|
428
|
+
// boundary to derive its authority; immediately after acquisition the
|
|
429
|
+
// full owner/DACL inspection below is mandatory before any operation.
|
|
430
|
+
if (input.verifyExistingWindowsAcl ?? true) {
|
|
431
|
+
// The directory check is the first enforcement site of every
|
|
432
|
+
// operation. A bounded helper timeout here used to escape as a raw
|
|
433
|
+
// child-process error and render as an internal defect (exit 70);
|
|
434
|
+
// it is the same retryable, fail-closed condition as at the key
|
|
435
|
+
// and ciphertext sites.
|
|
436
|
+
try {
|
|
437
|
+
await this.#assertWindowsOwnerOnlyAcl(directory, true);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
throw localSessionReadFailure("directory", error);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
await this.#ensureNewWindowsOwnerOnlyAcl(directory, true, async () => await this.#assertSafeDirectoryMetadata(directory));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
await chmod(directory, 0o700);
|
|
450
|
+
}
|
|
451
|
+
// Re-check after chmod or any Windows ACL mutation. This does not claim
|
|
452
|
+
// to close arbitrary filesystem races; it rejects a reparse observed at
|
|
453
|
+
// either checked boundary.
|
|
454
|
+
await this.#assertSafeDirectoryMetadata(directory);
|
|
455
|
+
}
|
|
456
|
+
async #assertSafeDirectoryMetadata(directory) {
|
|
457
|
+
const metadata = await lstat(directory);
|
|
458
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (this.platform !== "win32" && (metadata.mode & 0o077) !== 0)) {
|
|
459
|
+
throw new Error("encrypted session directory is unsafe");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async #loadOrCreateKey() {
|
|
463
|
+
let created = false;
|
|
464
|
+
try {
|
|
465
|
+
const handle = await open(this.#keyFile, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o600);
|
|
466
|
+
created = true;
|
|
467
|
+
const generated = randomBytes(KEY_BYTES);
|
|
468
|
+
try {
|
|
469
|
+
await handle.writeFile(generated);
|
|
470
|
+
await handle.sync();
|
|
471
|
+
}
|
|
472
|
+
finally {
|
|
473
|
+
generated.fill(0);
|
|
474
|
+
await handle.close();
|
|
475
|
+
}
|
|
476
|
+
await chmod(this.#keyFile, 0o600);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (error.code !== "EEXIST")
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
if (created) {
|
|
483
|
+
await this.#assertSafeFileMetadata(this.#keyFile, KEY_BYTES);
|
|
484
|
+
if (this.platform === "win32") {
|
|
485
|
+
await this.#ensureNewWindowsOwnerOnlyAcl(this.#keyFile, false, async () => await this.#assertSafeFileMetadata(this.#keyFile, KEY_BYTES));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
await this.#assertSafeIfPresent(this.#keyFile, KEY_BYTES);
|
|
490
|
+
}
|
|
491
|
+
const key = await readFile(this.#keyFile);
|
|
492
|
+
if (key.byteLength !== KEY_BYTES) {
|
|
493
|
+
key.fill(0);
|
|
494
|
+
throw credentialFailure("credential_corrupt", "The encrypted session key file is invalid.");
|
|
495
|
+
}
|
|
496
|
+
return key;
|
|
497
|
+
}
|
|
498
|
+
async #assertSafeIfPresent(file, maximum) {
|
|
499
|
+
await this.#assertSafeFileMetadata(file, maximum);
|
|
500
|
+
if (this.platform === "win32")
|
|
501
|
+
await this.#assertWindowsOwnerOnlyAcl(file, false);
|
|
502
|
+
}
|
|
503
|
+
async #assertSafeFileMetadata(file, maximum) {
|
|
504
|
+
const metadata = await lstat(file);
|
|
505
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > maximum ||
|
|
506
|
+
(this.platform !== "win32" && (metadata.mode & 0o077) !== 0)) {
|
|
507
|
+
throw new Error("encrypted session file is unsafe");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async #writeRestricted(file, bytes) {
|
|
511
|
+
// The lock wrapper has already completed a full owner/DACL inspection
|
|
512
|
+
// after acquiring the physical authority. Keep a final reparse/metadata
|
|
513
|
+
// boundary immediately before the atomic replacement without repeating a
|
|
514
|
+
// second expensive ACL subprocess in the same locked operation.
|
|
515
|
+
await this.#assertSafeDirectoryMetadata(dirname(file));
|
|
516
|
+
const temporary = `${file}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
517
|
+
let handle;
|
|
518
|
+
try {
|
|
519
|
+
handle = await open(temporary, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o600);
|
|
520
|
+
await handle.writeFile(bytes);
|
|
521
|
+
await handle.sync();
|
|
522
|
+
await handle.close();
|
|
523
|
+
handle = undefined;
|
|
524
|
+
await chmod(temporary, 0o600);
|
|
525
|
+
await rename(temporary, file);
|
|
526
|
+
await chmod(file, 0o600);
|
|
527
|
+
if (this.platform === "win32") {
|
|
528
|
+
await this.#assertSafeFileMetadata(file, MAX_SESSION_BYTES);
|
|
529
|
+
await this.#ensureNewWindowsOwnerOnlyAcl(file, false, async () => await this.#assertSafeFileMetadata(file, MAX_SESSION_BYTES));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
try {
|
|
534
|
+
await handle?.close();
|
|
535
|
+
}
|
|
536
|
+
catch { }
|
|
537
|
+
try {
|
|
538
|
+
await unlink(temporary);
|
|
539
|
+
}
|
|
540
|
+
catch { }
|
|
541
|
+
throw credentialFailure("credential_backend_failure", "The encrypted local session could not be replaced.", { retryable: true, cause: error });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function validateExpectedDigest(value) {
|
|
546
|
+
if (value !== null && !/^[a-f0-9]{64}$/u.test(value)) {
|
|
547
|
+
throw credentialFailure("credential_backend_unverified", "The encrypted session comparison digest is invalid.");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* An unreadable file and an invalid encrypted value are different claims. In
|
|
552
|
+
* particular, a bounded Windows ACL helper timeout proves neither that the
|
|
553
|
+
* key changed nor that its AEAD tag is invalid. Keep that condition
|
|
554
|
+
* retryable/fail-closed and leave the durable pair untouched; only a parsed
|
|
555
|
+
* invalid key or ciphertext is reported as credential corruption.
|
|
556
|
+
*/
|
|
557
|
+
function localSessionReadFailure(subject, error) {
|
|
558
|
+
if (error instanceof CredentialBoundaryError)
|
|
559
|
+
return error;
|
|
560
|
+
if (isBoundedWindowsAclTimeout(error)) {
|
|
561
|
+
return credentialFailure("credential_backend_failure", `The encrypted local session ${subject} could not be verified before the bounded ACL check expired.`, { retryable: true, safeDetails: { reason: "windows_acl_inspection_timeout" }, cause: error });
|
|
562
|
+
}
|
|
563
|
+
if (isTransientFilesystemFailure(error)) {
|
|
564
|
+
return credentialFailure("credential_backend_failure", `The encrypted local session ${subject} could not be read from the local store.`, { retryable: true, safeDetails: { reason: `encrypted_session_${subject}_read_failed` }, cause: error });
|
|
565
|
+
}
|
|
566
|
+
return credentialFailure("credential_backend_unverified", `The encrypted local session ${subject} could not be verified safely.`, { safeDetails: { reason: `encrypted_session_${subject}_security_unverified` }, cause: error });
|
|
567
|
+
}
|
|
568
|
+
function isBoundedWindowsAclTimeout(error) {
|
|
569
|
+
const record = error;
|
|
570
|
+
return record?.code === "ETIMEDOUT" || (record?.killed === true && typeof record.signal === "string" && record.signal.length > 0);
|
|
571
|
+
}
|
|
572
|
+
function isTransientFilesystemFailure(error) {
|
|
573
|
+
const code = error?.code;
|
|
574
|
+
return code === "EAGAIN" || code === "EBUSY" || code === "EINTR" || code === "EIO" ||
|
|
575
|
+
code === "EMFILE" || code === "ENFILE" || code === "ENODEV" || code === "ENXIO" || code === "ESTALE";
|
|
576
|
+
}
|
|
577
|
+
async function processLockAuthority(sessionFile, platform, namespace = "storage") {
|
|
578
|
+
const physicalDirectory = await realpath(dirname(sessionFile));
|
|
579
|
+
const physicalSession = resolve(physicalDirectory, basename(sessionFile));
|
|
580
|
+
const canonical = platform === "win32" ? physicalSession.toLocaleLowerCase("en-US") : physicalSession;
|
|
581
|
+
const digest = createHash("sha256").update(`${namespace}\0${canonical}`, "utf8").digest();
|
|
582
|
+
if (platform === "win32")
|
|
583
|
+
return `\\\\.\\pipe\\cuna-session-${namespace}-${digest.toString("hex").slice(0, 40)}`;
|
|
584
|
+
// A loopback listener is kernel-owned and disappears on process death. A
|
|
585
|
+
// hash collision or unrelated listener only causes a bounded fail-closed
|
|
586
|
+
// refusal; it can never permit two writers.
|
|
587
|
+
const namespaceBase = namespace === "storage" ? 49_152 : 57_344;
|
|
588
|
+
return { host: "127.0.0.1", port: namespaceBase + digest.readUInt16BE(0) % 8_192 };
|
|
589
|
+
}
|
|
590
|
+
function sameProcessLockAuthority(left, right) {
|
|
591
|
+
return typeof left === "string" || typeof right === "string"
|
|
592
|
+
? left === right
|
|
593
|
+
: left.host === right.host && left.port === right.port;
|
|
594
|
+
}
|
|
595
|
+
async function listenForLock(authority) {
|
|
596
|
+
const server = createServer((socket) => socket.destroy());
|
|
597
|
+
return await new Promise((resolveListen, rejectListen) => {
|
|
598
|
+
const onError = (error) => { server.close(); rejectListen(error); };
|
|
599
|
+
server.once("error", onError);
|
|
600
|
+
server.listen(authority, () => {
|
|
601
|
+
server.off("error", onError);
|
|
602
|
+
server.unref();
|
|
603
|
+
resolveListen(server);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
async function closeServer(server) {
|
|
608
|
+
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
609
|
+
}
|
|
610
|
+
export function localEncryptedSessionPaths(configDirectory, profile = "default") {
|
|
611
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(profile))
|
|
612
|
+
throw credentialFailure("credential_backend_unverified", "The session profile is invalid.");
|
|
613
|
+
const digest = createHash("sha256").update(profile, "utf8").digest("hex").slice(0, 32);
|
|
614
|
+
return Object.freeze({
|
|
615
|
+
// `sessions` existed before this AES-GCM format. It may have inherited an
|
|
616
|
+
// unsafe ACL, which Cuna must never repair or trust. A new format root is
|
|
617
|
+
// created and verified independently; older bytes remain untouched.
|
|
618
|
+
sessionFile: resolve(configDirectory, "sessions-v1", `session-${digest}.json`),
|
|
619
|
+
keyFile: resolve(configDirectory, "sessions-v1", `session-${digest}.key`),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function encrypt(plain, key) {
|
|
623
|
+
const nonce = randomBytes(NONCE_BYTES);
|
|
624
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
625
|
+
const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
626
|
+
const tag = cipher.getAuthTag();
|
|
627
|
+
return { version: 1, algorithm: "aes-256-gcm", nonce: nonce.toString("base64url"), tag: tag.toString("base64url"), ciphertext: ciphertext.toString("base64url") };
|
|
628
|
+
}
|
|
629
|
+
function decrypt(bytes, key) {
|
|
630
|
+
let value;
|
|
631
|
+
try {
|
|
632
|
+
value = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
throw credentialFailure("credential_corrupt", "The encrypted local session is malformed.", { cause: error });
|
|
636
|
+
}
|
|
637
|
+
if (!isRecord(value) || Object.keys(value).sort().join(",") !== "algorithm,ciphertext,nonce,tag,version" || value.version !== 1 || value.algorithm !== "aes-256-gcm" ||
|
|
638
|
+
typeof value.nonce !== "string" || typeof value.tag !== "string" || typeof value.ciphertext !== "string") {
|
|
639
|
+
throw credentialFailure("credential_corrupt", "The encrypted local session shape is invalid.");
|
|
640
|
+
}
|
|
641
|
+
const nonce = Buffer.from(value.nonce, "base64url");
|
|
642
|
+
const tag = Buffer.from(value.tag, "base64url");
|
|
643
|
+
const ciphertext = Buffer.from(value.ciphertext, "base64url");
|
|
644
|
+
if (nonce.byteLength !== NONCE_BYTES || tag.byteLength !== TAG_BYTES || ciphertext.byteLength < 1 || ciphertext.byteLength > 40 * 1024)
|
|
645
|
+
throw credentialFailure("credential_corrupt", "The encrypted local session bytes are invalid.");
|
|
646
|
+
try {
|
|
647
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
648
|
+
decipher.setAuthTag(tag);
|
|
649
|
+
return new Uint8Array(Buffer.concat([decipher.update(ciphertext), decipher.final()]));
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
throw credentialFailure("credential_corrupt", "The encrypted local session authentication tag is invalid.", { cause: error });
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
nonce.fill(0);
|
|
656
|
+
tag.fill(0);
|
|
657
|
+
ciphertext.fill(0);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function evidence(backend, now, status, reason) {
|
|
661
|
+
return { protocol: CREDENTIAL_BACKEND_PROTOCOL, backendId: backend.backendId, platform: backend.platform, status, observedAt: now, expiresAt: now + 60_000, source: status === "verified" ? "encrypted_local_file" : "probe_failed", ...(reason === undefined ? {} : { reason }) };
|
|
662
|
+
}
|
|
663
|
+
function exactJsonPath(value, label) { const path = resolve(value); if (!path.endsWith(".json"))
|
|
664
|
+
throw credentialFailure("credential_backend_unverified", `The encrypted ${label} path is invalid.`); return path; }
|
|
665
|
+
function exactKeyPath(value) { const path = resolve(value); if (!path.endsWith(".key"))
|
|
666
|
+
throw credentialFailure("credential_backend_unverified", "The encrypted session key path is invalid."); return path; }
|
|
667
|
+
function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
668
|
+
/**
|
|
669
|
+
* Isolates the two ACL inputs from inherited Windows environment variables.
|
|
670
|
+
* Windows environment-variable names are case-insensitive, so simply adding
|
|
671
|
+
* canonical uppercase keys would leave an attacker-controlled differently
|
|
672
|
+
* cased duplicate ambiguous to the child process.
|
|
673
|
+
*/
|
|
674
|
+
export function isolateWindowsAclChildEnvironment(inherited, path) {
|
|
675
|
+
const environment = stripWindowsAclChildEnvironment(inherited);
|
|
676
|
+
environment[WINDOWS_ACL_PATH_ENVIRONMENT] = path;
|
|
677
|
+
return environment;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* The batched variant carries a newline-separated list under its own name.
|
|
681
|
+
* Both isolated environments strip every ACL input name, so a single-path
|
|
682
|
+
* child never sees a list and a batched child never sees a single path.
|
|
683
|
+
*/
|
|
684
|
+
export function isolateWindowsAclBatchChildEnvironment(inherited, paths) {
|
|
685
|
+
if (paths.length < 1 || paths.length > WINDOWS_ACL_BATCH_MAX_PATHS)
|
|
686
|
+
throw new Error("Windows ACL batch size is invalid");
|
|
687
|
+
for (const path of paths) {
|
|
688
|
+
if (path.length < 1 || [...path].some((character) => character.codePointAt(0) < 0x20))
|
|
689
|
+
throw new Error("Windows ACL batch path is invalid");
|
|
690
|
+
}
|
|
691
|
+
const environment = stripWindowsAclChildEnvironment(inherited);
|
|
692
|
+
environment[WINDOWS_ACL_PATHS_ENVIRONMENT] = paths.join(WINDOWS_ACL_BATCH_PATH_SEPARATOR);
|
|
693
|
+
return environment;
|
|
694
|
+
}
|
|
695
|
+
function stripWindowsAclChildEnvironment(inherited) {
|
|
696
|
+
const environment = Object.create(null);
|
|
697
|
+
for (const [name, value] of Object.entries(inherited)) {
|
|
698
|
+
if (WINDOWS_ACL_CHILD_ENVIRONMENT_NAMES.has(name.toLocaleUpperCase("en-US")))
|
|
699
|
+
continue;
|
|
700
|
+
if (value !== undefined)
|
|
701
|
+
environment[name] = value;
|
|
702
|
+
}
|
|
703
|
+
return environment;
|
|
704
|
+
}
|
|
705
|
+
const WINDOWS_ACL_AUTHORITY = Object.freeze({
|
|
706
|
+
inspectOwnerOnly: async (path, directory) => await inspectWindowsAcl(path, directory),
|
|
707
|
+
inspectManyOwnerOnly: async (requests) => await inspectWindowsAclMany(requests),
|
|
708
|
+
reconcileNewOwnerOnly: async (path, directory) => await reconcileNewWindowsAcl(path, directory),
|
|
709
|
+
});
|
|
710
|
+
function assertWindowsOwnerOnlyInspection(inspection) {
|
|
711
|
+
assertWindowsCurrentUserOwner(inspection);
|
|
712
|
+
if (!inspection.daclOwnerOnly) {
|
|
713
|
+
throw new Error("encrypted session ACL is not current-user-only");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function assertWindowsCurrentUserOwner(inspection) {
|
|
717
|
+
if (inspection.ownerSid !== inspection.currentSid) {
|
|
718
|
+
throw new Error("encrypted session owner is not the current user");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function ensureNewWindowsOwnerOnlyAcl(authority, path, directory, assertSafeMetadata) {
|
|
722
|
+
// The ACL authority returns a before/after pair from a single static OS
|
|
723
|
+
// invocation. That avoids a slow and racy inspect -> separate-process
|
|
724
|
+
// repair -> inspect sequence while retaining the boundary checks on both
|
|
725
|
+
// sides of a possible ACL mutation.
|
|
726
|
+
await assertSafeMetadata();
|
|
727
|
+
const reconciliation = await authority.reconcileNewOwnerOnly(path, directory);
|
|
728
|
+
await assertSafeMetadata();
|
|
729
|
+
assertWindowsCurrentUserOwner(reconciliation.before);
|
|
730
|
+
if (reconciliation.before.currentSid !== reconciliation.after.currentSid) {
|
|
731
|
+
throw new Error("encrypted session current user changed during ACL reconciliation");
|
|
732
|
+
}
|
|
733
|
+
if (reconciliation.repaired !== !reconciliation.before.daclOwnerOnly) {
|
|
734
|
+
throw new Error("encrypted session ACL reconciliation result is inconsistent");
|
|
735
|
+
}
|
|
736
|
+
assertWindowsOwnerOnlyInspection(reconciliation.after);
|
|
737
|
+
}
|
|
738
|
+
async function inspectWindowsAcl(path, directory) {
|
|
739
|
+
// The program is a constant and `path` is data in a child-only environment;
|
|
740
|
+
// never interpolate paths into PowerShell source or encode them as commands.
|
|
741
|
+
const { stdout } = await execFileAsync(resolve(WINDOWS_SYSTEM32, "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_ACL_COMMAND_PROGRAMS.inspect], {
|
|
742
|
+
windowsHide: true,
|
|
743
|
+
// PowerShell 5.1 start-up alone is 1–3 s on a loaded host and single
|
|
744
|
+
// spawns of 4.9–5.2 s were measured under a CPU hog; the bound must
|
|
745
|
+
// outlast that without becoming an effective hang.
|
|
746
|
+
timeout: 15_000,
|
|
747
|
+
maxBuffer: 16 * 1024,
|
|
748
|
+
encoding: "utf8",
|
|
749
|
+
env: isolateWindowsAclChildEnvironment(process.env, path),
|
|
750
|
+
});
|
|
751
|
+
return parseWindowsAclInspection(stdout, "CUNA_CURRENT_SID", "CUNA_SDDL", directory);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Batched form of `inspectWindowsAcl`: one `powershell.exe` spawn for every
|
|
755
|
+
* requested path. Exported so the real program and its parser can be
|
|
756
|
+
* exercised against real files on Windows.
|
|
757
|
+
*/
|
|
758
|
+
export async function inspectWindowsAclMany(requests) {
|
|
759
|
+
const { stdout } = await execFileAsync(resolve(WINDOWS_SYSTEM32, "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_ACL_COMMAND_PROGRAMS.inspectMany], {
|
|
760
|
+
windowsHide: true,
|
|
761
|
+
// PowerShell 5.1 start-up alone is 1–3 s on a loaded host and single
|
|
762
|
+
// spawns of 4.9–5.2 s were measured under a CPU hog; the bound must
|
|
763
|
+
// outlast that without becoming an effective hang.
|
|
764
|
+
timeout: 15_000,
|
|
765
|
+
maxBuffer: 64 * 1024,
|
|
766
|
+
encoding: "utf8",
|
|
767
|
+
env: isolateWindowsAclBatchChildEnvironment(process.env, requests.map((request) => request.path)),
|
|
768
|
+
});
|
|
769
|
+
return parseWindowsAclBatchInspection(stdout, requests.map((request) => request.directory));
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Parses `inspectMany` output. Exactly one `CUNA_CURRENT_SID=` line is
|
|
773
|
+
* required; each `CUNA_SDDL:<index>=` line yields an inspection at that index
|
|
774
|
+
* and an index without a line stays `undefined`. A duplicated or out-of-range
|
|
775
|
+
* index is a malformed observation and rejects the whole batch.
|
|
776
|
+
*/
|
|
777
|
+
export function parseWindowsAclBatchInspection(stdout, directories) {
|
|
778
|
+
const sidLines = stdout.match(/^CUNA_CURRENT_SID=.*$/gmu) ?? [];
|
|
779
|
+
const currentSid = sidLines.length === 1 ? /^CUNA_CURRENT_SID=(S-1-[0-9-]+)$/u.exec(sidLines[0])?.[1] : undefined;
|
|
780
|
+
if (currentSid === undefined)
|
|
781
|
+
throw new Error("Windows ACL inspection is unavailable");
|
|
782
|
+
const results = directories.map(() => undefined);
|
|
783
|
+
for (const line of stdout.match(/^CUNA_SDDL:.*$/gmu) ?? []) {
|
|
784
|
+
const match = /^CUNA_SDDL:(0|[1-9][0-9]*)=(.+)$/u.exec(line);
|
|
785
|
+
const index = match === null ? Number.NaN : Number(match[1]);
|
|
786
|
+
if (match === null || !(index < directories.length) || results[index] !== undefined) {
|
|
787
|
+
throw new Error("Windows ACL batch inspection is malformed");
|
|
788
|
+
}
|
|
789
|
+
results[index] = windowsAclInspectionFromSddl(currentSid, match[2], directories[index]);
|
|
790
|
+
}
|
|
791
|
+
return results;
|
|
792
|
+
}
|
|
793
|
+
async function reconcileNewWindowsAcl(path, directory) {
|
|
794
|
+
const program = directory
|
|
795
|
+
? WINDOWS_ACL_COMMAND_PROGRAMS.reconcileDirectory
|
|
796
|
+
: WINDOWS_ACL_COMMAND_PROGRAMS.reconcileFile;
|
|
797
|
+
const { stdout } = await execFileAsync(resolve(WINDOWS_SYSTEM32, "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoProfile", "-NonInteractive", "-Command", program], {
|
|
798
|
+
windowsHide: true,
|
|
799
|
+
// PowerShell 5.1 start-up alone is 1–3 s on a loaded host and single
|
|
800
|
+
// spawns of 4.9–5.2 s were measured under a CPU hog; the bound must
|
|
801
|
+
// outlast that without becoming an effective hang.
|
|
802
|
+
timeout: 15_000,
|
|
803
|
+
maxBuffer: 32 * 1024,
|
|
804
|
+
encoding: "utf8",
|
|
805
|
+
env: isolateWindowsAclChildEnvironment(process.env, path),
|
|
806
|
+
});
|
|
807
|
+
const repaired = /^CUNA_REPAIRED=([01])$/mu.exec(stdout)?.[1];
|
|
808
|
+
if (repaired === undefined)
|
|
809
|
+
throw new Error("Windows ACL reconciliation is unavailable");
|
|
810
|
+
return Object.freeze({
|
|
811
|
+
before: parseWindowsAclInspection(stdout, "CUNA_BEFORE_CURRENT_SID", "CUNA_BEFORE_SDDL", directory),
|
|
812
|
+
after: parseWindowsAclInspection(stdout, "CUNA_AFTER_CURRENT_SID", "CUNA_AFTER_SDDL", directory),
|
|
813
|
+
repaired: repaired === "1",
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function parseWindowsAclInspection(stdout, currentSidLabel, sddlLabel, directory) {
|
|
817
|
+
const currentSid = new RegExp(`^${currentSidLabel}=(S-1-[0-9-]+)$`, "mu").exec(stdout)?.[1];
|
|
818
|
+
const sddl = new RegExp(`^${sddlLabel}=(.+)$`, "mu").exec(stdout)?.[1];
|
|
819
|
+
if (currentSid === undefined || sddl === undefined) {
|
|
820
|
+
throw new Error("Windows ACL inspection is unavailable");
|
|
821
|
+
}
|
|
822
|
+
return windowsAclInspectionFromSddl(currentSid, sddl, directory);
|
|
823
|
+
}
|
|
824
|
+
function windowsAclInspectionFromSddl(currentSid, sddl, directory) {
|
|
825
|
+
const ownerSid = /^O:(S-1-[0-9-]+)(?:G:|D:|S:)/u.exec(sddl)?.[1];
|
|
826
|
+
if (ownerSid === undefined) {
|
|
827
|
+
throw new Error("Windows ACL inspection is unavailable");
|
|
828
|
+
}
|
|
829
|
+
const daclStart = sddl.indexOf("D:");
|
|
830
|
+
const saclStart = daclStart < 0 ? -1 : sddl.indexOf("S:", daclStart + 2);
|
|
831
|
+
const dacl = daclStart < 0 ? undefined : sddl.slice(daclStart, saclStart < 0 ? undefined : saclStart);
|
|
832
|
+
const expectedAce = directory ? `(A;OICI;FA;;;${currentSid})` : `(A;;FA;;;${currentSid})`;
|
|
833
|
+
return {
|
|
834
|
+
currentSid,
|
|
835
|
+
ownerSid,
|
|
836
|
+
daclOwnerOnly: dacl === `D:PAI${expectedAce}` || dacl === `D:P${expectedAce}`,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
//# sourceMappingURL=local-session.js.map
|