@smthrs/control 0.0.0-stage → 1.0.0-rc.3
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/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +168 -2
- package/dist/cjs/ApprovalAuthority.d.ts +73 -0
- package/dist/cjs/ApprovalAuthority.d.ts.map +1 -0
- package/dist/cjs/ApprovalAuthority.js +62 -0
- package/dist/cjs/ApprovalAuthority.js.map +7 -0
- package/dist/cjs/Cancellation.d.ts +107 -0
- package/dist/cjs/Cancellation.d.ts.map +1 -0
- package/dist/cjs/Cancellation.js +72 -0
- package/dist/cjs/Cancellation.js.map +7 -0
- package/dist/cjs/Channels.d.ts +170 -0
- package/dist/cjs/Channels.d.ts.map +1 -0
- package/dist/cjs/Channels.js +278 -0
- package/dist/cjs/Channels.js.map +7 -0
- package/dist/cjs/Control.d.ts +202 -0
- package/dist/cjs/Control.d.ts.map +1 -0
- package/dist/cjs/Control.js +47 -0
- package/dist/cjs/Control.js.map +7 -0
- package/dist/cjs/ControlClient.d.ts +52 -0
- package/dist/cjs/ControlClient.d.ts.map +1 -0
- package/dist/cjs/ControlClient.js +191 -0
- package/dist/cjs/ControlClient.js.map +7 -0
- package/dist/cjs/ControlError.d.ts +318 -0
- package/dist/cjs/ControlError.d.ts.map +1 -0
- package/dist/cjs/ControlError.js +249 -0
- package/dist/cjs/ControlError.js.map +7 -0
- package/dist/cjs/ControlExecutor.d.ts +372 -0
- package/dist/cjs/ControlExecutor.d.ts.map +1 -0
- package/dist/cjs/ControlExecutor.js +123 -0
- package/dist/cjs/ControlExecutor.js.map +7 -0
- package/dist/cjs/ControlFacts.d.ts +454 -0
- package/dist/cjs/ControlFacts.d.ts.map +1 -0
- package/dist/cjs/ControlFacts.js +261 -0
- package/dist/cjs/ControlFacts.js.map +7 -0
- package/dist/cjs/ControlLive.d.ts +23 -0
- package/dist/cjs/ControlLive.d.ts.map +1 -0
- package/dist/cjs/ControlLive.js +1280 -0
- package/dist/cjs/ControlLive.js.map +7 -0
- package/dist/cjs/ControlRpcs.d.ts +1204 -0
- package/dist/cjs/ControlRpcs.d.ts.map +1 -0
- package/dist/cjs/ControlRpcs.js +247 -0
- package/dist/cjs/ControlRpcs.js.map +7 -0
- package/dist/cjs/ControlRuntime.d.ts +635 -0
- package/dist/cjs/ControlRuntime.d.ts.map +1 -0
- package/dist/cjs/ControlRuntime.js +740 -0
- package/dist/cjs/ControlRuntime.js.map +7 -0
- package/dist/cjs/ControlSchema.d.ts +2642 -0
- package/dist/cjs/ControlSchema.d.ts.map +1 -0
- package/dist/cjs/ControlSchema.js +634 -0
- package/dist/cjs/ControlSchema.js.map +7 -0
- package/dist/cjs/ControlServer.d.ts +51 -0
- package/dist/cjs/ControlServer.d.ts.map +1 -0
- package/dist/cjs/ControlServer.js +121 -0
- package/dist/cjs/ControlServer.js.map +7 -0
- package/dist/cjs/Credential.d.ts +136 -0
- package/dist/cjs/Credential.d.ts.map +1 -0
- package/dist/cjs/Credential.js +168 -0
- package/dist/cjs/Credential.js.map +7 -0
- package/dist/cjs/CredentialCipher.d.ts +90 -0
- package/dist/cjs/CredentialCipher.d.ts.map +1 -0
- package/dist/cjs/CredentialCipher.js +45 -0
- package/dist/cjs/CredentialCipher.js.map +7 -0
- package/dist/cjs/CredentialStore.d.ts +97 -0
- package/dist/cjs/CredentialStore.d.ts.map +1 -0
- package/dist/cjs/CredentialStore.js +81 -0
- package/dist/cjs/CredentialStore.js.map +7 -0
- package/dist/cjs/DispatchReader.d.ts +112 -0
- package/dist/cjs/DispatchReader.d.ts.map +1 -0
- package/dist/cjs/DispatchReader.js +45 -0
- package/dist/cjs/DispatchReader.js.map +7 -0
- package/dist/cjs/Health.d.ts +333 -0
- package/dist/cjs/Health.d.ts.map +1 -0
- package/dist/cjs/Health.js +311 -0
- package/dist/cjs/Health.js.map +7 -0
- package/dist/cjs/JevSessionChecker.d.ts +57 -0
- package/dist/cjs/JevSessionChecker.d.ts.map +1 -0
- package/dist/cjs/JevSessionChecker.js +113 -0
- package/dist/cjs/JevSessionChecker.js.map +7 -0
- package/dist/cjs/Lineage.d.ts +131 -0
- package/dist/cjs/Lineage.d.ts.map +1 -0
- package/dist/cjs/Lineage.js +81 -0
- package/dist/cjs/Lineage.js.map +7 -0
- package/dist/cjs/Migrations.d.ts +34 -0
- package/dist/cjs/Migrations.d.ts.map +1 -0
- package/dist/cjs/Migrations.js +60 -0
- package/dist/cjs/Migrations.js.map +7 -0
- package/dist/cjs/Monitor.d.ts +282 -0
- package/dist/cjs/Monitor.d.ts.map +1 -0
- package/dist/cjs/Monitor.js +283 -0
- package/dist/cjs/Monitor.js.map +7 -0
- package/dist/cjs/ScopedToken.d.ts +193 -0
- package/dist/cjs/ScopedToken.d.ts.map +1 -0
- package/dist/cjs/ScopedToken.js +135 -0
- package/dist/cjs/ScopedToken.js.map +7 -0
- package/dist/cjs/SqlControlRuntime.d.ts +161 -0
- package/dist/cjs/SqlControlRuntime.d.ts.map +1 -0
- package/dist/cjs/SqlControlRuntime.js +1521 -0
- package/dist/cjs/SqlControlRuntime.js.map +7 -0
- package/dist/cjs/SqlCredentialStore.d.ts +43 -0
- package/dist/cjs/SqlCredentialStore.d.ts.map +1 -0
- package/dist/cjs/SqlCredentialStore.js +113 -0
- package/dist/cjs/SqlCredentialStore.js.map +7 -0
- package/dist/cjs/Steering.d.ts +69 -0
- package/dist/cjs/Steering.d.ts.map +1 -0
- package/dist/cjs/Steering.js +49 -0
- package/dist/cjs/Steering.js.map +7 -0
- package/dist/cjs/SystemFlows.d.ts +223 -0
- package/dist/cjs/SystemFlows.d.ts.map +1 -0
- package/dist/cjs/SystemFlows.js +195 -0
- package/dist/cjs/SystemFlows.js.map +7 -0
- package/dist/cjs/WebCryptoCipher.d.ts +49 -0
- package/dist/cjs/WebCryptoCipher.d.ts.map +1 -0
- package/dist/cjs/WebCryptoCipher.js +129 -0
- package/dist/cjs/WebCryptoCipher.js.map +7 -0
- package/dist/cjs/WebhookChannel.d.ts +113 -0
- package/dist/cjs/WebhookChannel.d.ts.map +1 -0
- package/dist/cjs/WebhookChannel.js +98 -0
- package/dist/cjs/WebhookChannel.js.map +7 -0
- package/dist/cjs/index.d.ts +160 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +91 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/MutationBoundary.d.ts +27 -0
- package/dist/cjs/internal/MutationBoundary.d.ts.map +1 -0
- package/dist/cjs/internal/MutationBoundary.js +50 -0
- package/dist/cjs/internal/MutationBoundary.js.map +7 -0
- package/dist/cjs/internal/activeFibers.d.ts +12 -0
- package/dist/cjs/internal/activeFibers.d.ts.map +1 -0
- package/dist/cjs/internal/activeFibers.js +30 -0
- package/dist/cjs/internal/activeFibers.js.map +7 -0
- package/dist/cjs/internal/issues.d.ts +28 -0
- package/dist/cjs/internal/issues.d.ts.map +1 -0
- package/dist/cjs/internal/issues.js +34 -0
- package/dist/cjs/internal/issues.js.map +7 -0
- package/dist/cjs/internal/planning.d.ts +347 -0
- package/dist/cjs/internal/planning.d.ts.map +1 -0
- package/dist/cjs/internal/planning.js +137 -0
- package/dist/cjs/internal/planning.js.map +7 -0
- package/dist/cjs/internal/sqlSchemaErrors.d.ts +18 -0
- package/dist/cjs/internal/sqlSchemaErrors.d.ts.map +1 -0
- package/dist/cjs/internal/sqlSchemaErrors.js +40 -0
- package/dist/cjs/internal/sqlSchemaErrors.js.map +7 -0
- package/dist/cjs/migrations/0001_control_tables.d.ts +18 -0
- package/dist/cjs/migrations/0001_control_tables.d.ts.map +1 -0
- package/dist/cjs/migrations/0001_control_tables.js +115 -0
- package/dist/cjs/migrations/0001_control_tables.js.map +7 -0
- package/dist/cjs/migrations/0002_run_keys.d.ts +15 -0
- package/dist/cjs/migrations/0002_run_keys.d.ts.map +1 -0
- package/dist/cjs/migrations/0002_run_keys.js +44 -0
- package/dist/cjs/migrations/0002_run_keys.js.map +7 -0
- package/dist/cjs/migrations/0003_signal_commands.d.ts +13 -0
- package/dist/cjs/migrations/0003_signal_commands.d.ts.map +1 -0
- package/dist/cjs/migrations/0003_signal_commands.js +50 -0
- package/dist/cjs/migrations/0003_signal_commands.js.map +7 -0
- package/dist/cjs/migrations/0004_approval_decisions.d.ts +13 -0
- package/dist/cjs/migrations/0004_approval_decisions.d.ts.map +1 -0
- package/dist/cjs/migrations/0004_approval_decisions.js +45 -0
- package/dist/cjs/migrations/0004_approval_decisions.js.map +7 -0
- package/dist/cjs/migrations/0005_signal_principals.d.ts +18 -0
- package/dist/cjs/migrations/0005_signal_principals.d.ts.map +1 -0
- package/dist/cjs/migrations/0005_signal_principals.js +45 -0
- package/dist/cjs/migrations/0005_signal_principals.js.map +7 -0
- package/dist/cjs/migrations/0006_run_principals.d.ts +18 -0
- package/dist/cjs/migrations/0006_run_principals.d.ts.map +1 -0
- package/dist/cjs/migrations/0006_run_principals.js +49 -0
- package/dist/cjs/migrations/0006_run_principals.js.map +7 -0
- package/dist/cjs/migrations/0007_resume_consent.d.ts +18 -0
- package/dist/cjs/migrations/0007_resume_consent.d.ts.map +1 -0
- package/dist/cjs/migrations/0007_resume_consent.js +46 -0
- package/dist/cjs/migrations/0007_resume_consent.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestControl.d.ts +19 -0
- package/dist/cjs/test/TestControl.d.ts.map +1 -0
- package/dist/cjs/test/TestControl.js +62 -0
- package/dist/cjs/test/TestControl.js.map +7 -0
- package/dist/esm/ApprovalAuthority.d.ts +73 -0
- package/dist/esm/ApprovalAuthority.d.ts.map +1 -0
- package/dist/esm/ApprovalAuthority.js +72 -0
- package/dist/esm/ApprovalAuthority.js.map +1 -0
- package/dist/esm/Cancellation.d.ts +107 -0
- package/dist/esm/Cancellation.d.ts.map +1 -0
- package/dist/esm/Cancellation.js +116 -0
- package/dist/esm/Cancellation.js.map +1 -0
- package/dist/esm/Channels.d.ts +170 -0
- package/dist/esm/Channels.d.ts.map +1 -0
- package/dist/esm/Channels.js +312 -0
- package/dist/esm/Channels.js.map +1 -0
- package/dist/esm/Control.d.ts +202 -0
- package/dist/esm/Control.d.ts.map +1 -0
- package/dist/esm/Control.js +42 -0
- package/dist/esm/Control.js.map +1 -0
- package/dist/esm/ControlClient.d.ts +52 -0
- package/dist/esm/ControlClient.d.ts.map +1 -0
- package/dist/esm/ControlClient.js +217 -0
- package/dist/esm/ControlClient.js.map +1 -0
- package/dist/esm/ControlError.d.ts +318 -0
- package/dist/esm/ControlError.d.ts.map +1 -0
- package/dist/esm/ControlError.js +359 -0
- package/dist/esm/ControlError.js.map +1 -0
- package/dist/esm/ControlExecutor.d.ts +372 -0
- package/dist/esm/ControlExecutor.d.ts.map +1 -0
- package/dist/esm/ControlExecutor.js +212 -0
- package/dist/esm/ControlExecutor.js.map +1 -0
- package/dist/esm/ControlFacts.d.ts +454 -0
- package/dist/esm/ControlFacts.d.ts.map +1 -0
- package/dist/esm/ControlFacts.js +324 -0
- package/dist/esm/ControlFacts.js.map +1 -0
- package/dist/esm/ControlLive.d.ts +23 -0
- package/dist/esm/ControlLive.d.ts.map +1 -0
- package/dist/esm/ControlLive.js +1585 -0
- package/dist/esm/ControlLive.js.map +1 -0
- package/dist/esm/ControlRpcs.d.ts +1204 -0
- package/dist/esm/ControlRpcs.d.ts.map +1 -0
- package/dist/esm/ControlRpcs.js +299 -0
- package/dist/esm/ControlRpcs.js.map +1 -0
- package/dist/esm/ControlRuntime.d.ts +635 -0
- package/dist/esm/ControlRuntime.d.ts.map +1 -0
- package/dist/esm/ControlRuntime.js +807 -0
- package/dist/esm/ControlRuntime.js.map +1 -0
- package/dist/esm/ControlSchema.d.ts +2642 -0
- package/dist/esm/ControlSchema.d.ts.map +1 -0
- package/dist/esm/ControlSchema.js +1030 -0
- package/dist/esm/ControlSchema.js.map +1 -0
- package/dist/esm/ControlServer.d.ts +51 -0
- package/dist/esm/ControlServer.d.ts.map +1 -0
- package/dist/esm/ControlServer.js +145 -0
- package/dist/esm/ControlServer.js.map +1 -0
- package/dist/esm/Credential.d.ts +136 -0
- package/dist/esm/Credential.d.ts.map +1 -0
- package/dist/esm/Credential.js +190 -0
- package/dist/esm/Credential.js.map +1 -0
- package/dist/esm/CredentialCipher.d.ts +90 -0
- package/dist/esm/CredentialCipher.d.ts.map +1 -0
- package/dist/esm/CredentialCipher.js +56 -0
- package/dist/esm/CredentialCipher.js.map +1 -0
- package/dist/esm/CredentialStore.d.ts +97 -0
- package/dist/esm/CredentialStore.d.ts.map +1 -0
- package/dist/esm/CredentialStore.js +101 -0
- package/dist/esm/CredentialStore.js.map +1 -0
- package/dist/esm/DispatchReader.d.ts +112 -0
- package/dist/esm/DispatchReader.d.ts.map +1 -0
- package/dist/esm/DispatchReader.js +76 -0
- package/dist/esm/DispatchReader.js.map +1 -0
- package/dist/esm/Health.d.ts +333 -0
- package/dist/esm/Health.d.ts.map +1 -0
- package/dist/esm/Health.js +400 -0
- package/dist/esm/Health.js.map +1 -0
- package/dist/esm/JevSessionChecker.d.ts +57 -0
- package/dist/esm/JevSessionChecker.d.ts.map +1 -0
- package/dist/esm/JevSessionChecker.js +108 -0
- package/dist/esm/JevSessionChecker.js.map +1 -0
- package/dist/esm/Lineage.d.ts +131 -0
- package/dist/esm/Lineage.d.ts.map +1 -0
- package/dist/esm/Lineage.js +174 -0
- package/dist/esm/Lineage.js.map +1 -0
- package/dist/esm/Migrations.d.ts +34 -0
- package/dist/esm/Migrations.d.ts.map +1 -0
- package/dist/esm/Migrations.js +53 -0
- package/dist/esm/Migrations.js.map +1 -0
- package/dist/esm/Monitor.d.ts +282 -0
- package/dist/esm/Monitor.d.ts.map +1 -0
- package/dist/esm/Monitor.js +415 -0
- package/dist/esm/Monitor.js.map +1 -0
- package/dist/esm/ScopedToken.d.ts +193 -0
- package/dist/esm/ScopedToken.d.ts.map +1 -0
- package/dist/esm/ScopedToken.js +224 -0
- package/dist/esm/ScopedToken.js.map +1 -0
- package/dist/esm/SqlControlRuntime.d.ts +161 -0
- package/dist/esm/SqlControlRuntime.d.ts.map +1 -0
- package/dist/esm/SqlControlRuntime.js +1756 -0
- package/dist/esm/SqlControlRuntime.js.map +1 -0
- package/dist/esm/SqlCredentialStore.d.ts +43 -0
- package/dist/esm/SqlCredentialStore.d.ts.map +1 -0
- package/dist/esm/SqlCredentialStore.js +97 -0
- package/dist/esm/SqlCredentialStore.js.map +1 -0
- package/dist/esm/Steering.d.ts +69 -0
- package/dist/esm/Steering.d.ts.map +1 -0
- package/dist/esm/Steering.js +89 -0
- package/dist/esm/Steering.js.map +1 -0
- package/dist/esm/SystemFlows.d.ts +223 -0
- package/dist/esm/SystemFlows.d.ts.map +1 -0
- package/dist/esm/SystemFlows.js +198 -0
- package/dist/esm/SystemFlows.js.map +1 -0
- package/dist/esm/WebCryptoCipher.d.ts +49 -0
- package/dist/esm/WebCryptoCipher.d.ts.map +1 -0
- package/dist/esm/WebCryptoCipher.js +123 -0
- package/dist/esm/WebCryptoCipher.js.map +1 -0
- package/dist/esm/WebhookChannel.d.ts +113 -0
- package/dist/esm/WebhookChannel.d.ts.map +1 -0
- package/dist/esm/WebhookChannel.js +109 -0
- package/dist/esm/WebhookChannel.js.map +1 -0
- package/dist/esm/index.d.ts +160 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +160 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/MutationBoundary.d.ts +27 -0
- package/dist/esm/internal/MutationBoundary.d.ts.map +1 -0
- package/dist/esm/internal/MutationBoundary.js +40 -0
- package/dist/esm/internal/MutationBoundary.js.map +1 -0
- package/dist/esm/internal/activeFibers.d.ts +12 -0
- package/dist/esm/internal/activeFibers.d.ts.map +1 -0
- package/dist/esm/internal/activeFibers.js +17 -0
- package/dist/esm/internal/activeFibers.js.map +1 -0
- package/dist/esm/internal/issues.d.ts +28 -0
- package/dist/esm/internal/issues.d.ts.map +1 -0
- package/dist/esm/internal/issues.js +35 -0
- package/dist/esm/internal/issues.js.map +1 -0
- package/dist/esm/internal/planning.d.ts +347 -0
- package/dist/esm/internal/planning.d.ts.map +1 -0
- package/dist/esm/internal/planning.js +199 -0
- package/dist/esm/internal/planning.js.map +1 -0
- package/dist/esm/internal/sqlSchemaErrors.d.ts +18 -0
- package/dist/esm/internal/sqlSchemaErrors.d.ts.map +1 -0
- package/dist/esm/internal/sqlSchemaErrors.js +38 -0
- package/dist/esm/internal/sqlSchemaErrors.js.map +1 -0
- package/dist/esm/migrations/0001_control_tables.d.ts +18 -0
- package/dist/esm/migrations/0001_control_tables.d.ts.map +1 -0
- package/dist/esm/migrations/0001_control_tables.js +96 -0
- package/dist/esm/migrations/0001_control_tables.js.map +1 -0
- package/dist/esm/migrations/0002_run_keys.d.ts +15 -0
- package/dist/esm/migrations/0002_run_keys.d.ts.map +1 -0
- package/dist/esm/migrations/0002_run_keys.js +22 -0
- package/dist/esm/migrations/0002_run_keys.js.map +1 -0
- package/dist/esm/migrations/0003_signal_commands.d.ts +13 -0
- package/dist/esm/migrations/0003_signal_commands.d.ts.map +1 -0
- package/dist/esm/migrations/0003_signal_commands.js +26 -0
- package/dist/esm/migrations/0003_signal_commands.js.map +1 -0
- package/dist/esm/migrations/0004_approval_decisions.d.ts +13 -0
- package/dist/esm/migrations/0004_approval_decisions.d.ts.map +1 -0
- package/dist/esm/migrations/0004_approval_decisions.js +25 -0
- package/dist/esm/migrations/0004_approval_decisions.js.map +1 -0
- package/dist/esm/migrations/0005_signal_principals.d.ts +18 -0
- package/dist/esm/migrations/0005_signal_principals.d.ts.map +1 -0
- package/dist/esm/migrations/0005_signal_principals.js +27 -0
- package/dist/esm/migrations/0005_signal_principals.js.map +1 -0
- package/dist/esm/migrations/0006_run_principals.d.ts +18 -0
- package/dist/esm/migrations/0006_run_principals.d.ts.map +1 -0
- package/dist/esm/migrations/0006_run_principals.js +30 -0
- package/dist/esm/migrations/0006_run_principals.js.map +1 -0
- package/dist/esm/migrations/0007_resume_consent.d.ts +18 -0
- package/dist/esm/migrations/0007_resume_consent.d.ts.map +1 -0
- package/dist/esm/migrations/0007_resume_consent.js +27 -0
- package/dist/esm/migrations/0007_resume_consent.js.map +1 -0
- package/dist/esm/test/TestControl.d.ts +19 -0
- package/dist/esm/test/TestControl.d.ts.map +1 -0
- package/dist/esm/test/TestControl.js +30 -0
- package/dist/esm/test/TestControl.js.map +1 -0
- package/docs/README.md +189 -0
- package/docs/api.md +982 -0
- package/docs/concepts/authority.md +109 -0
- package/docs/concepts/cancellation.md +129 -0
- package/docs/concepts/lineage.md +132 -0
- package/docs/concepts/ownership.md +139 -0
- package/docs/concepts/projections.md +203 -0
- package/docs/concepts/receipts.md +128 -0
- package/docs/guides/approvals.md +284 -0
- package/docs/guides/cancel-and-resume.md +162 -0
- package/docs/guides/durable-storage.md +147 -0
- package/docs/guides/implement-an-executor.md +173 -0
- package/docs/guides/ingest-a-webhook.md +177 -0
- package/docs/guides/list-runs.md +160 -0
- package/docs/guides/monitor-runs.md +176 -0
- package/docs/guides/observe-health.md +147 -0
- package/docs/guides/postgres-tests.md +5 -0
- package/docs/guides/serve-over-rpc.md +220 -0
- package/docs/guides/signal-a-run.md +53 -0
- package/docs/guides/steer-a-run.md +138 -0
- package/docs/guides/store-credentials.md +164 -0
- package/docs/guides/testing.md +139 -0
- package/docs/guides/watch-a-run.md +154 -0
- package/docs/installation.md +106 -0
- package/docs/quickstart.md +163 -0
- package/docs/troubleshooting.md +208 -0
- package/package.json +405 -3
- package/src/ApprovalAuthority.ts +114 -0
- package/src/Cancellation.ts +172 -0
- package/src/Channels.ts +493 -0
- package/src/Control.ts +337 -0
- package/src/ControlClient.ts +319 -0
- package/src/ControlError.ts +378 -0
- package/src/ControlExecutor.ts +490 -0
- package/src/ControlFacts.ts +383 -0
- package/src/ControlLive.ts +2113 -0
- package/src/ControlRpcs.ts +443 -0
- package/src/ControlRuntime.ts +1597 -0
- package/src/ControlSchema.ts +1380 -0
- package/src/ControlServer.ts +182 -0
- package/src/Credential.ts +310 -0
- package/src/CredentialCipher.ts +110 -0
- package/src/CredentialStore.ts +152 -0
- package/src/DispatchReader.ts +122 -0
- package/src/Health.ts +591 -0
- package/src/JevSessionChecker.ts +127 -0
- package/src/Lineage.ts +203 -0
- package/src/Migrations.ts +56 -0
- package/src/Monitor.ts +600 -0
- package/src/ScopedToken.ts +306 -0
- package/src/SqlControlRuntime.ts +2476 -0
- package/src/SqlCredentialStore.ts +148 -0
- package/src/Steering.ts +96 -0
- package/src/SystemFlows.ts +225 -0
- package/src/WebCryptoCipher.ts +169 -0
- package/src/WebhookChannel.ts +166 -0
- package/src/index.ts +188 -0
- package/src/internal/MutationBoundary.ts +46 -0
- package/src/internal/activeFibers.ts +22 -0
- package/src/internal/issues.ts +40 -0
- package/src/internal/planning.ts +262 -0
- package/src/internal/sqlSchemaErrors.ts +37 -0
- package/src/migrations/0001_control_tables.ts +99 -0
- package/src/migrations/0002_run_keys.ts +23 -0
- package/src/migrations/0003_signal_commands.ts +27 -0
- package/src/migrations/0004_approval_decisions.ts +25 -0
- package/src/migrations/0005_signal_principals.ts +27 -0
- package/src/migrations/0006_run_principals.ts +31 -0
- package/src/migrations/0007_resume_consent.ts +28 -0
- package/src/test/TestControl.ts +47 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC server layers for the control service.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.1.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Cause, Effect, Layer, Stream } from "effect"
|
|
8
|
+
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
|
9
|
+
import { RpcServer } from "effect/unstable/rpc"
|
|
10
|
+
import { Control } from "./Control.ts"
|
|
11
|
+
import { Unauthorized } from "./ControlError.ts"
|
|
12
|
+
import { ControlPrincipal, ControlRpcs, RunVisibility } from "./ControlRpcs.ts"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Logs the raw defect of a failed RPC handler on the server, and nothing for
|
|
16
|
+
* a typed failure or an interruption. `ControlRpcs.ControlDefect` sends
|
|
17
|
+
* clients only the designed sentence, so this log is where the detail lives.
|
|
18
|
+
* Pass it to `Effect.tapCause` or `Stream.tapCause` around a handler.
|
|
19
|
+
*
|
|
20
|
+
* @category logging
|
|
21
|
+
* @since 1.0.0
|
|
22
|
+
*/
|
|
23
|
+
export const logDefect = (cause: Cause.Cause<unknown>): Effect.Effect<void> =>
|
|
24
|
+
Cause.hasDies(cause) ? Effect.logError("A control RPC handler died", cause) : Effect.void
|
|
25
|
+
|
|
26
|
+
const logged = <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.tapCause(effect, logDefect)
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Control RPC handlers delegating to the transport-independent service.
|
|
30
|
+
*
|
|
31
|
+
* Every mutation that records who asked reads `ControlPrincipal` and stamps
|
|
32
|
+
* it, rather than forwarding whatever the client sent. The identity the
|
|
33
|
+
* middleware authenticated is the only one the server can stand behind, and it
|
|
34
|
+
* is what reaches the journal, `RunSummary.cancellation`, and a steer's
|
|
35
|
+
* notification provenance.
|
|
36
|
+
*
|
|
37
|
+
* `List`, `Watch` and every run mutation read it too: a principal
|
|
38
|
+
* `ControlRpcs.RunVisibility` does not make an operator reads and mutates only
|
|
39
|
+
* the runs it launched; any other run answers `RunNotFound`.
|
|
40
|
+
*
|
|
41
|
+
* @category layers
|
|
42
|
+
* @since 0.1.0
|
|
43
|
+
*/
|
|
44
|
+
export const layer = ControlRpcs.toLayer(
|
|
45
|
+
Effect.gen(function*() {
|
|
46
|
+
const control = yield* Control
|
|
47
|
+
// A reader that is not an operator is restricted to the runs its own
|
|
48
|
+
// principal launched. `reader` is not on the wire, so only this stamp sets
|
|
49
|
+
// it. The authentication middleware provides the rule with the principal.
|
|
50
|
+
const reader = Effect.gen(function*() {
|
|
51
|
+
const principal = yield* ControlPrincipal
|
|
52
|
+
const visibility = yield* RunVisibility
|
|
53
|
+
return visibility.seesAllRuns(principal) ? {} : { reader: principal }
|
|
54
|
+
})
|
|
55
|
+
return ControlRpcs.of({
|
|
56
|
+
Plan: Effect.fn("Control.plan")((input) => control.plan(input), logged),
|
|
57
|
+
Run: Effect.fn("Control.run")((input) =>
|
|
58
|
+
Effect.gen(function*() {
|
|
59
|
+
const principal = yield* ControlPrincipal
|
|
60
|
+
return yield* control.run({ ...input, principal })
|
|
61
|
+
}), logged),
|
|
62
|
+
Approve: Effect.fn("Control.approve")((input) =>
|
|
63
|
+
Effect.gen(function*() {
|
|
64
|
+
const principal = yield* ControlPrincipal
|
|
65
|
+
if (principal.id === "loopback" && principal.kind === "anonymous") {
|
|
66
|
+
return yield* new Unauthorized({ message: "An operator credential is required" })
|
|
67
|
+
}
|
|
68
|
+
return yield* control.approve({ ...input, principal })
|
|
69
|
+
}), logged),
|
|
70
|
+
Deny: Effect.fn("Control.deny")((input) =>
|
|
71
|
+
Effect.gen(function*() {
|
|
72
|
+
const principal = yield* ControlPrincipal
|
|
73
|
+
if (principal.id === "loopback" && principal.kind === "anonymous") {
|
|
74
|
+
return yield* new Unauthorized({ message: "An operator credential is required" })
|
|
75
|
+
}
|
|
76
|
+
return yield* control.deny({ ...input, principal })
|
|
77
|
+
}), logged),
|
|
78
|
+
Steer: Effect.fn("Control.steer")((input) =>
|
|
79
|
+
Effect.gen(function*() {
|
|
80
|
+
const principal = yield* ControlPrincipal
|
|
81
|
+
// A steer carries a principal on the wire because an in-process
|
|
82
|
+
// caller names one that is not an operator: `agent/send` attributes
|
|
83
|
+
// a child's steer to the parent flow. A remote client may not, so
|
|
84
|
+
// the authenticated identity replaces whatever arrived. It reaches
|
|
85
|
+
// the notification's `sourceActor` and the run transcript, which is
|
|
86
|
+
// exactly where a spoofed name would be read as truth.
|
|
87
|
+
const { attribution: _attribution, ...message } = input.message
|
|
88
|
+
return yield* control.steer({ ...input, message: { ...message, principal }, ...yield* reader })
|
|
89
|
+
}), logged),
|
|
90
|
+
Signal: Effect.fn("Control.signal")((input) =>
|
|
91
|
+
Effect.gen(function*() {
|
|
92
|
+
const principal = yield* ControlPrincipal
|
|
93
|
+
return yield* control.signal({ ...input, principal, ...yield* reader })
|
|
94
|
+
}), logged),
|
|
95
|
+
Cancel: Effect.fn("Control.cancel")((input) =>
|
|
96
|
+
Effect.gen(function*() {
|
|
97
|
+
const principal = yield* ControlPrincipal
|
|
98
|
+
return yield* control.cancel({ ...input, principal, ...yield* reader })
|
|
99
|
+
}), logged),
|
|
100
|
+
Resume: Effect.fn("Control.resume")((input) =>
|
|
101
|
+
Effect.gen(function*() {
|
|
102
|
+
const principal = yield* ControlPrincipal
|
|
103
|
+
return yield* control.resume({ ...input, principal, ...yield* reader })
|
|
104
|
+
}), logged),
|
|
105
|
+
List: Effect.fn("Control.list")(
|
|
106
|
+
(input) => Effect.flatMap(reader, (restriction) => control.list({ ...input, ...restriction })),
|
|
107
|
+
logged
|
|
108
|
+
),
|
|
109
|
+
Watch: (input) =>
|
|
110
|
+
Stream.tapCause(
|
|
111
|
+
Stream.unwrap(Effect.map(reader, (restriction) => control.watch({ ...input, ...restriction }))),
|
|
112
|
+
logDefect
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether a request's `Origin` may reach the control mounts.
|
|
120
|
+
*
|
|
121
|
+
* Non-browser clients send no `Origin`. A browser always sends one, on POST
|
|
122
|
+
* and on the WebSocket upgrade, and a cross-site page cannot forge it. Refusing
|
|
123
|
+
* any `Origin` whose authority differs from `Host` stops a page on another site
|
|
124
|
+
* from riding a credential that a proxy or cookie attaches on the victim's
|
|
125
|
+
* behalf (cross-site WebSocket hijacking of the `Watch` stream).
|
|
126
|
+
*/
|
|
127
|
+
const sameOrigin = (headers: Readonly<Record<string, string>>): boolean => {
|
|
128
|
+
const origin = headers.origin
|
|
129
|
+
if (origin === undefined) return true
|
|
130
|
+
const host = headers.host
|
|
131
|
+
const parsed = URL.parse(origin)
|
|
132
|
+
return host !== undefined && parsed !== null &&
|
|
133
|
+
(parsed.protocol === "http:" || parsed.protocol === "https:") &&
|
|
134
|
+
parsed.origin === origin && parsed.host === host.toLowerCase()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Route middleware on the two control mounts only, so the router's own path
|
|
139
|
+
* matching decides what it guards, and a host's stricter global policy (the
|
|
140
|
+
* gateway's ingress) still answers first with its own typed refusal.
|
|
141
|
+
*/
|
|
142
|
+
const originGuard = HttpRouter.middleware((httpEffect) =>
|
|
143
|
+
Effect.gen(function*() {
|
|
144
|
+
const request = yield* HttpServerRequest.HttpServerRequest
|
|
145
|
+
if (sameOrigin(request.headers)) return yield* httpEffect
|
|
146
|
+
return HttpServerResponse.text("The browser Origin must match the control server Host", { status: 403 })
|
|
147
|
+
})
|
|
148
|
+
).layer
|
|
149
|
+
|
|
150
|
+
const server = RpcServer.layer(ControlRpcs, {
|
|
151
|
+
disableFatalDefects: true
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const http = server.pipe(
|
|
155
|
+
Layer.provide(layer),
|
|
156
|
+
Layer.provideMerge(RpcServer.layerProtocolHttp({ path: "/rpc" }).pipe(Layer.provide(originGuard))),
|
|
157
|
+
Layer.fresh
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const websocket = server.pipe(
|
|
161
|
+
Layer.provide(layer),
|
|
162
|
+
Layer.provideMerge(RpcServer.layerProtocolWebsocket({ path: "/rpc/ws" }).pipe(Layer.provide(originGuard))),
|
|
163
|
+
Layer.fresh
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Mounts control RPC on the ambient `HttpRouter`: unary procedures over POST
|
|
168
|
+
* `/rpc` and the `watch` stream over WebSocket `/rpc/ws`. Both protocols are
|
|
169
|
+
* mounted together because `ControlClient` projects the same `Control` vtable
|
|
170
|
+
* across the two transports.
|
|
171
|
+
*
|
|
172
|
+
* A request carrying a browser `Origin` that does not match its `Host` is
|
|
173
|
+
* refused with 403 before authentication, so a page on another site cannot
|
|
174
|
+
* open the socket or post to `/rpc` with a credential a proxy attaches.
|
|
175
|
+
*
|
|
176
|
+
* @category layers
|
|
177
|
+
* @since 0.1.0
|
|
178
|
+
*/
|
|
179
|
+
export const layerHttp = Layer.mergeAll(
|
|
180
|
+
http,
|
|
181
|
+
websocket
|
|
182
|
+
)
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential storage boundary.
|
|
3
|
+
*
|
|
4
|
+
* Credentials are capabilities. They must never enter flow input, plan
|
|
5
|
+
* digests, journal payloads, or model context, so only a {@link CredentialRef}
|
|
6
|
+
* — an id and a human-readable connection name — crosses the browser-safe
|
|
7
|
+
* contract. Plaintext exists in exactly two places: inside a `Redacted` handed
|
|
8
|
+
* to `create`/`rotate`, and inside the `Redacted` returned by `resolve`.
|
|
9
|
+
*
|
|
10
|
+
* The implementation composes two ports:
|
|
11
|
+
*
|
|
12
|
+
* - {@link CredentialStore} persists an opaque sealed record and enforces
|
|
13
|
+
* compare-and-set on writes, so concurrent rotations cannot interleave.
|
|
14
|
+
* `rotate` with `expected` extends that compare to the secret itself: the
|
|
15
|
+
* secret it checks and the version it writes over come from one read, so a
|
|
16
|
+
* writer that commits in between, in any process, is refused.
|
|
17
|
+
* - {@link CredentialCipher} seals and opens the secret under host-managed
|
|
18
|
+
* keys. `WebCryptoCipher` is the browser and Node adapter; a host with no
|
|
19
|
+
* secure key material fails with the typed `Unavailable`.
|
|
20
|
+
*
|
|
21
|
+
* Authorization is a host decision, injected as {@link Options.authorize}. A
|
|
22
|
+
* reference is also authenticated on every operation: caller-owned fields are
|
|
23
|
+
* snapshotted before policy effects run, and a caller cannot present a forged
|
|
24
|
+
* or stale `CredentialRef` because the snapshotted name must still match the
|
|
25
|
+
* stored record.
|
|
26
|
+
*
|
|
27
|
+
* @since 0.1.0
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { Context, Effect, Layer, Option, Redacted } from "effect"
|
|
31
|
+
import { CredentialConflict, InvalidInput, type PersistenceError, Unauthorized, Unavailable } from "./ControlError.ts"
|
|
32
|
+
import * as CredentialCipher from "./CredentialCipher.ts"
|
|
33
|
+
import * as CredentialStore from "./CredentialStore.ts"
|
|
34
|
+
|
|
35
|
+
// String.prototype.isWellFormed without the ES2024 lib, which every package
|
|
36
|
+
// compiling this source would also need. A loop, not a regex: a match would
|
|
37
|
+
// leave the rejected secret in the legacy RegExp.input static.
|
|
38
|
+
const wellFormed = (value: string): boolean => {
|
|
39
|
+
for (let index = 0; index < value.length; index++) {
|
|
40
|
+
const unit = value.charCodeAt(index)
|
|
41
|
+
if (unit >= 0xdc00 && unit <= 0xdfff) return false
|
|
42
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
43
|
+
const low = value.charCodeAt(++index)
|
|
44
|
+
if (!(low >= 0xdc00 && low <= 0xdfff)) return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A journal-safe name for a stored connection credential.
|
|
52
|
+
*
|
|
53
|
+
* @category models
|
|
54
|
+
* @since 0.1.0
|
|
55
|
+
*/
|
|
56
|
+
export interface CredentialRef {
|
|
57
|
+
readonly id: string
|
|
58
|
+
readonly name: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* One credential operation, as seen by a host authorization policy.
|
|
63
|
+
*
|
|
64
|
+
* @category models
|
|
65
|
+
* @since 0.1.0
|
|
66
|
+
*/
|
|
67
|
+
export type Operation = "list" | "get" | "create" | "resolve" | "rotate" | "revoke"
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* How {@link Credential.rotate} compares before it writes.
|
|
71
|
+
*
|
|
72
|
+
* @category models
|
|
73
|
+
* @since 1.0.0
|
|
74
|
+
*/
|
|
75
|
+
export interface RotateOptions {
|
|
76
|
+
/** The secret the caller last saw; the rotation commits only over it. */
|
|
77
|
+
readonly expected?: Redacted.Redacted<string> | undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Adapter-bound credential resolution. References are safe to list and
|
|
82
|
+
* persist; only `resolve` crosses into a secret-bearing adapter boundary.
|
|
83
|
+
*
|
|
84
|
+
* @category services
|
|
85
|
+
* @since 0.1.0
|
|
86
|
+
*/
|
|
87
|
+
export interface Credential {
|
|
88
|
+
readonly list: () => Effect.Effect<ReadonlyArray<CredentialRef>, Unavailable | Unauthorized>
|
|
89
|
+
readonly get: (id: string) => Effect.Effect<CredentialRef, Unavailable | Unauthorized>
|
|
90
|
+
readonly create: (
|
|
91
|
+
options: { readonly id: string; readonly name: string; readonly secret: Redacted.Redacted<string> }
|
|
92
|
+
) => Effect.Effect<CredentialRef, Unavailable | Unauthorized | CredentialConflict | InvalidInput>
|
|
93
|
+
readonly resolve: (
|
|
94
|
+
reference: CredentialRef
|
|
95
|
+
) => Effect.Effect<Redacted.Redacted<string>, Unavailable | Unauthorized | PersistenceError>
|
|
96
|
+
/**
|
|
97
|
+
* Replaces the secret. With `expected`, replaces it only while the stored
|
|
98
|
+
* secret still equals `expected`, and otherwise fails `CredentialConflict`
|
|
99
|
+
* at the stored version without writing.
|
|
100
|
+
*/
|
|
101
|
+
readonly rotate: (
|
|
102
|
+
reference: CredentialRef,
|
|
103
|
+
secret: Redacted.Redacted<string>,
|
|
104
|
+
options?: RotateOptions | undefined
|
|
105
|
+
) => Effect.Effect<
|
|
106
|
+
CredentialRef,
|
|
107
|
+
Unavailable | Unauthorized | CredentialConflict | InvalidInput | PersistenceError
|
|
108
|
+
>
|
|
109
|
+
readonly revoke: (reference: CredentialRef) => Effect.Effect<void, Unavailable | Unauthorized>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Service tag for credential resolution.
|
|
114
|
+
*
|
|
115
|
+
* @category services
|
|
116
|
+
* @since 0.1.0
|
|
117
|
+
*/
|
|
118
|
+
export const Credential: Context.Service<Credential, Credential> = Context.Service("/control/Credential")
|
|
119
|
+
|
|
120
|
+
const unavailable = (): Unavailable =>
|
|
121
|
+
new Unavailable({
|
|
122
|
+
feature: "credential storage",
|
|
123
|
+
ticket: "control-credential-storage"
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A credential implementation that explicitly reports unavailable storage.
|
|
128
|
+
*
|
|
129
|
+
* @category constructors
|
|
130
|
+
* @since 0.1.0
|
|
131
|
+
*/
|
|
132
|
+
export const makeNoop = (): Credential =>
|
|
133
|
+
Credential.of({
|
|
134
|
+
list: Effect.fn("Credential.list")(() => Effect.fail(unavailable())),
|
|
135
|
+
get: Effect.fn("Credential.get")(() => Effect.fail(unavailable())),
|
|
136
|
+
create: Effect.fn("Credential.create")(() => Effect.fail(unavailable())),
|
|
137
|
+
resolve: Effect.fn("Credential.resolve")(() => Effect.fail(unavailable())),
|
|
138
|
+
rotate: Effect.fn("Credential.rotate")(() => Effect.fail(unavailable())),
|
|
139
|
+
revoke: Effect.fn("Credential.revoke")(() => Effect.fail(unavailable()))
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Layer for the unavailable credential-storage boundary.
|
|
144
|
+
*
|
|
145
|
+
* @category layers
|
|
146
|
+
* @since 0.1.0
|
|
147
|
+
*/
|
|
148
|
+
export const layerNoop = Layer.succeed(Credential, makeNoop())
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The collaborators a working credential boundary needs.
|
|
152
|
+
*
|
|
153
|
+
* `authorize` is the host's policy hook. It defaults to allowing every
|
|
154
|
+
* operation, which is correct for a single-principal local process; a server
|
|
155
|
+
* composition supplies its own and gets typed `Unauthorized` failures for free.
|
|
156
|
+
*
|
|
157
|
+
* @category models
|
|
158
|
+
* @since 0.1.0
|
|
159
|
+
*/
|
|
160
|
+
export interface Options {
|
|
161
|
+
readonly store: CredentialStore.Service
|
|
162
|
+
readonly cipher: CredentialCipher.Service
|
|
163
|
+
readonly authorize?:
|
|
164
|
+
| ((operation: Operation, reference: Option.Option<CredentialRef>) => Effect.Effect<void, Unauthorized>)
|
|
165
|
+
| undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const referenceOf = (value: { readonly id: string; readonly name: string }): CredentialRef =>
|
|
169
|
+
Object.freeze({
|
|
170
|
+
id: value.id,
|
|
171
|
+
name: value.name
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const cipherContextOf = (
|
|
175
|
+
value: { readonly id: string; readonly name: string; readonly version: number }
|
|
176
|
+
): CredentialCipher.Context =>
|
|
177
|
+
Object.freeze({
|
|
178
|
+
id: value.id,
|
|
179
|
+
name: value.name,
|
|
180
|
+
version: value.version
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const missing = (id: string): Unauthorized =>
|
|
184
|
+
// Deliberately indistinguishable from a denied reference: telling an
|
|
185
|
+
// unauthorized caller which ids exist is itself a leak.
|
|
186
|
+
new Unauthorized({ message: `Credential ${id} is not available to this caller` })
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Constructs a working credential boundary over a store and a cipher.
|
|
190
|
+
*
|
|
191
|
+
* @category constructors
|
|
192
|
+
* @since 0.1.0
|
|
193
|
+
*/
|
|
194
|
+
export const make = (options: Options): Credential => {
|
|
195
|
+
const authorize = options.authorize ?? (() => Effect.void)
|
|
196
|
+
const store = options.store
|
|
197
|
+
const cipher = options.cipher
|
|
198
|
+
|
|
199
|
+
/** Reads the record `reference` names, refusing a forged or stale one. */
|
|
200
|
+
const authenticate = (
|
|
201
|
+
operation: Operation,
|
|
202
|
+
reference: CredentialRef
|
|
203
|
+
): Effect.Effect<CredentialStore.SealedRecord, Unavailable | Unauthorized> =>
|
|
204
|
+
Effect.gen(function*() {
|
|
205
|
+
const snapshot = referenceOf(reference)
|
|
206
|
+
yield* authorize(operation, Option.some(snapshot))
|
|
207
|
+
const record = yield* store.read(snapshot.id)
|
|
208
|
+
if (Option.isNone(record)) return yield* Effect.fail(missing(snapshot.id))
|
|
209
|
+
if (record.value.name !== snapshot.name) return yield* Effect.fail(missing(snapshot.id))
|
|
210
|
+
return record.value
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const put = (
|
|
214
|
+
record: CredentialStore.SealedRecord,
|
|
215
|
+
secret: Redacted.Redacted<string>
|
|
216
|
+
): Effect.Effect<CredentialRef, Unavailable | CredentialConflict | InvalidInput> =>
|
|
217
|
+
Effect.gen(function*() {
|
|
218
|
+
if (!wellFormed(Redacted.value(secret))) {
|
|
219
|
+
return yield* Effect.fail(new InvalidInput({ issue: "Credential secret must be well-formed Unicode" }))
|
|
220
|
+
}
|
|
221
|
+
// The metadata written beside this blob is also its authenticated data;
|
|
222
|
+
// moving the blob to another id, name, or version must make it unreadable.
|
|
223
|
+
const context = cipherContextOf(record)
|
|
224
|
+
const sealed = yield* cipher.seal(secret, context)
|
|
225
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis)
|
|
226
|
+
yield* store.write({ ...record, ...sealed, updatedAtMs: now })
|
|
227
|
+
return referenceOf(record)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
return Credential.of({
|
|
231
|
+
list: Effect.fn("Credential.list")(function*() {
|
|
232
|
+
yield* authorize("list", Option.none())
|
|
233
|
+
const records = yield* store.list()
|
|
234
|
+
return records.map(referenceOf)
|
|
235
|
+
}),
|
|
236
|
+
get: Effect.fn("Credential.get")(function*(id) {
|
|
237
|
+
const record = yield* store.read(id)
|
|
238
|
+
if (Option.isNone(record)) return yield* Effect.fail(missing(id))
|
|
239
|
+
const reference = referenceOf(record.value)
|
|
240
|
+
yield* authorize("get", Option.some(reference))
|
|
241
|
+
return reference
|
|
242
|
+
}),
|
|
243
|
+
create: Effect.fn("Credential.create")(function*(input) {
|
|
244
|
+
const snapshot = Object.freeze({
|
|
245
|
+
reference: referenceOf(input),
|
|
246
|
+
secret: input.secret
|
|
247
|
+
})
|
|
248
|
+
yield* authorize("create", Option.some(snapshot.reference))
|
|
249
|
+
return yield* put(
|
|
250
|
+
{
|
|
251
|
+
id: snapshot.reference.id,
|
|
252
|
+
name: snapshot.reference.name,
|
|
253
|
+
ciphertext: "",
|
|
254
|
+
nonce: "",
|
|
255
|
+
version: 1,
|
|
256
|
+
updatedAtMs: 0
|
|
257
|
+
},
|
|
258
|
+
snapshot.secret
|
|
259
|
+
)
|
|
260
|
+
}),
|
|
261
|
+
resolve: Effect.fn("Credential.resolve")(function*(reference) {
|
|
262
|
+
const record = yield* authenticate("resolve", reference)
|
|
263
|
+
return yield* cipher.open(
|
|
264
|
+
{ ciphertext: record.ciphertext, nonce: record.nonce },
|
|
265
|
+
cipherContextOf(record)
|
|
266
|
+
)
|
|
267
|
+
}),
|
|
268
|
+
rotate: Effect.fn("Credential.rotate")(function*(reference, secret, options) {
|
|
269
|
+
const expected = options?.expected
|
|
270
|
+
const record = yield* authenticate("rotate", reference)
|
|
271
|
+
if (expected !== undefined) {
|
|
272
|
+
const stored = yield* cipher.open(
|
|
273
|
+
{ ciphertext: record.ciphertext, nonce: record.nonce },
|
|
274
|
+
cipherContextOf(record)
|
|
275
|
+
)
|
|
276
|
+
if (Redacted.value(stored) !== Redacted.value(expected)) {
|
|
277
|
+
return yield* Effect.fail(
|
|
278
|
+
new CredentialConflict({ id: record.id, expectedVersion: record.version, actualVersion: record.version })
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// `put` writes over exactly the version read above, so the secret just
|
|
283
|
+
// compared is still the stored one when the write commits.
|
|
284
|
+
return yield* put({ ...record, version: record.version + 1 }, secret)
|
|
285
|
+
}),
|
|
286
|
+
revoke: Effect.fn("Credential.revoke")(function*(reference) {
|
|
287
|
+
const record = yield* authenticate("revoke", reference)
|
|
288
|
+
yield* store.remove(record.id)
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Provides a working credential boundary over the ambient store and cipher.
|
|
295
|
+
*
|
|
296
|
+
* @category layers
|
|
297
|
+
* @since 0.1.0
|
|
298
|
+
*/
|
|
299
|
+
export const layer = (
|
|
300
|
+
options: { readonly authorize?: Options["authorize"] } = {}
|
|
301
|
+
): Layer.Layer<
|
|
302
|
+
Credential,
|
|
303
|
+
never,
|
|
304
|
+
CredentialStore.CredentialStore | CredentialCipher.CredentialCipher
|
|
305
|
+
> =>
|
|
306
|
+
Layer.effect(Credential)(Effect.gen(function*() {
|
|
307
|
+
const store = yield* CredentialStore.CredentialStore
|
|
308
|
+
const cipher = yield* CredentialCipher.CredentialCipher
|
|
309
|
+
return make({ store, cipher, ...(options.authorize === undefined ? {} : { authorize: options.authorize }) })
|
|
310
|
+
}))
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe encryption port for credential material.
|
|
3
|
+
*
|
|
4
|
+
* The cipher is the only place plaintext exists outside a `Redacted`. Keys are
|
|
5
|
+
* host-managed: an adapter receives a key from its host and never persists one
|
|
6
|
+
* through {@link CredentialStore}, so a stolen store is ciphertext and nothing
|
|
7
|
+
* else.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Context as EffectContext, Effect, Layer, type Redacted } from "effect"
|
|
13
|
+
import { type PersistenceError, Unavailable } from "./ControlError.ts"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One encrypted secret: base64 ciphertext and the nonce it was sealed under.
|
|
17
|
+
*
|
|
18
|
+
* @category models
|
|
19
|
+
* @since 0.1.0
|
|
20
|
+
*/
|
|
21
|
+
export interface Sealed {
|
|
22
|
+
readonly ciphertext: string
|
|
23
|
+
readonly nonce: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Credential metadata authenticated with one sealed secret.
|
|
28
|
+
*
|
|
29
|
+
* Every field is immutable because the same values must be written beside the
|
|
30
|
+
* ciphertext and supplied again when it is opened.
|
|
31
|
+
*
|
|
32
|
+
* @category models
|
|
33
|
+
* @since 0.1.0
|
|
34
|
+
*/
|
|
35
|
+
export interface Context {
|
|
36
|
+
readonly id: string
|
|
37
|
+
readonly name: string
|
|
38
|
+
readonly version: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Authenticated encryption over credential plaintext.
|
|
43
|
+
*
|
|
44
|
+
* @category services
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
export interface Service {
|
|
48
|
+
readonly seal: (plaintext: Redacted.Redacted<string>, context: Context) => Effect.Effect<Sealed, Unavailable>
|
|
49
|
+
/**
|
|
50
|
+
* Fails with `PersistenceError` on operation `credential.open` when the
|
|
51
|
+
* record does not open: a malformed nonce, or ciphertext that fails
|
|
52
|
+
* authentication under this key and context.
|
|
53
|
+
*/
|
|
54
|
+
readonly open: (
|
|
55
|
+
sealed: Sealed,
|
|
56
|
+
context: Context
|
|
57
|
+
) => Effect.Effect<Redacted.Redacted<string>, Unavailable | PersistenceError>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Service key for credential encryption.
|
|
62
|
+
*
|
|
63
|
+
* @category services
|
|
64
|
+
* @since 0.1.0
|
|
65
|
+
*/
|
|
66
|
+
export class CredentialCipher extends EffectContext.Service<CredentialCipher, Service>()(
|
|
67
|
+
"/control/CredentialCipher"
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Constructs a cipher from an implementation record.
|
|
72
|
+
*
|
|
73
|
+
* @category constructors
|
|
74
|
+
* @since 0.1.0
|
|
75
|
+
*/
|
|
76
|
+
export const make = (implementation: Service): Service => CredentialCipher.of(implementation)
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The typed failure a host reports when no secure key material is reachable.
|
|
80
|
+
*
|
|
81
|
+
* @category constructors
|
|
82
|
+
* @since 0.1.0
|
|
83
|
+
*/
|
|
84
|
+
export const unavailable = (): Unavailable =>
|
|
85
|
+
new Unavailable({
|
|
86
|
+
feature: "credential encryption",
|
|
87
|
+
ticket: "control-credential-storage"
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A cipher that reports unavailable key material for every operation.
|
|
92
|
+
*
|
|
93
|
+
* @category constructors
|
|
94
|
+
* @since 0.1.0
|
|
95
|
+
*/
|
|
96
|
+
export const makeNoop = (overrides: Partial<Service> = {}): Service =>
|
|
97
|
+
make({
|
|
98
|
+
seal: Effect.fn("CredentialCipher.seal")(() => Effect.fail(unavailable())),
|
|
99
|
+
open: Effect.fn("CredentialCipher.open")(() => Effect.fail(unavailable())),
|
|
100
|
+
...overrides
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Provides a cipher that reports unavailable key material.
|
|
105
|
+
*
|
|
106
|
+
* @category layers
|
|
107
|
+
* @since 0.1.0
|
|
108
|
+
*/
|
|
109
|
+
export const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<CredentialCipher> =>
|
|
110
|
+
Layer.succeed(CredentialCipher)(makeNoop(overrides))
|