@smthrs/control 0.0.0-stage → 1.0.0-rc.4
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 +1293 -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 +744 -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 +1522 -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 +1589 -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 +808 -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 +7 -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 +2127 -0
- package/src/ControlRpcs.ts +443 -0
- package/src/ControlRuntime.ts +1601 -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 +2478 -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,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable credential persistence over `@smthrs/database`.
|
|
3
|
+
*
|
|
4
|
+
* This is the host adapter behind {@link CredentialStore} on any runtime with
|
|
5
|
+
* a SQL database — the Node composition uses `@smthrs/database/node`, and
|
|
6
|
+
* tests use its in-memory SQLite driver. The module itself imports no `node:*`
|
|
7
|
+
* API: it speaks the driver-neutral SQL contract, so it stays bundleable and a
|
|
8
|
+
* browser host that ships a SQL driver can use it unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Only ciphertext is stored. The row carries no key material, and the
|
|
11
|
+
* compare-and-set on `version` is what makes two concurrent rotations
|
|
12
|
+
* serializable: the loser is refused with `CredentialConflict` instead of
|
|
13
|
+
* clobbering the winner.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { DurableWriter } from "@smthrs/database/DurableWriter"
|
|
19
|
+
import { Effect, Layer, Option } from "effect"
|
|
20
|
+
import * as SqlClient from "effect/unstable/sql/SqlClient"
|
|
21
|
+
import { CredentialConflict, Unavailable } from "./ControlError.ts"
|
|
22
|
+
import * as CredentialStore from "./CredentialStore.ts"
|
|
23
|
+
import { initial } from "./migrations/0001_control_tables.ts"
|
|
24
|
+
|
|
25
|
+
interface Row {
|
|
26
|
+
readonly id: string
|
|
27
|
+
readonly name: string
|
|
28
|
+
readonly ciphertext: string
|
|
29
|
+
readonly nonce: string
|
|
30
|
+
readonly version: number
|
|
31
|
+
readonly updated_at_ms: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const toRecord = (row: Row): CredentialStore.SealedRecord => ({
|
|
35
|
+
id: row.id,
|
|
36
|
+
name: row.name,
|
|
37
|
+
ciphertext: row.ciphertext,
|
|
38
|
+
nonce: row.nonce,
|
|
39
|
+
version: Number(row.version),
|
|
40
|
+
updatedAtMs: Number(row.updated_at_ms)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const unavailable = (operation: string) => (): Unavailable =>
|
|
44
|
+
new Unavailable({
|
|
45
|
+
feature: `credential storage (${operation})`,
|
|
46
|
+
ticket: "control-credential-storage"
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates the credential table if it does not exist.
|
|
51
|
+
*
|
|
52
|
+
* @category migrations
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
*/
|
|
55
|
+
export const migrate: Effect.Effect<void, Unavailable, SqlClient.SqlClient> = initial.pipe(
|
|
56
|
+
Effect.mapError(unavailable("migrate"))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Constructs a durable credential store over the ambient database.
|
|
61
|
+
*
|
|
62
|
+
* @category constructors
|
|
63
|
+
* @since 0.1.0
|
|
64
|
+
*/
|
|
65
|
+
export const make: Effect.Effect<
|
|
66
|
+
CredentialStore.Service,
|
|
67
|
+
Unavailable,
|
|
68
|
+
DurableWriter | SqlClient.SqlClient
|
|
69
|
+
> = Effect.gen(function*() {
|
|
70
|
+
const sql = yield* Effect.service(SqlClient.SqlClient)
|
|
71
|
+
const writer = yield* DurableWriter
|
|
72
|
+
yield* migrate
|
|
73
|
+
|
|
74
|
+
const list = (): Effect.Effect<ReadonlyArray<CredentialStore.SealedRecord>, Unavailable> =>
|
|
75
|
+
sql<Row>`SELECT * FROM control_credentials ORDER BY name`.pipe(
|
|
76
|
+
Effect.map((rows) => rows.map(toRecord)),
|
|
77
|
+
Effect.mapError(unavailable("list"))
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const read = (id: string): Effect.Effect<Option.Option<CredentialStore.SealedRecord>, Unavailable> =>
|
|
81
|
+
sql<Row>`SELECT * FROM control_credentials WHERE id = ${id}`.pipe(
|
|
82
|
+
Effect.map((rows) => Option.map(Option.fromNullishOr(rows[0]), toRecord)),
|
|
83
|
+
Effect.mapError(unavailable("read"))
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// One write transaction, so the version this writer read and the row it
|
|
87
|
+
// guards cannot interleave with a concurrent rotation.
|
|
88
|
+
const write = (
|
|
89
|
+
record: CredentialStore.SealedRecord
|
|
90
|
+
): Effect.Effect<void, Unavailable | CredentialConflict> =>
|
|
91
|
+
writer.write(Effect.gen(function*() {
|
|
92
|
+
const rows = yield* sql<{ readonly version: number }>`
|
|
93
|
+
SELECT version FROM control_credentials WHERE id = ${record.id}
|
|
94
|
+
`
|
|
95
|
+
const stored = rows[0]
|
|
96
|
+
const actual = stored === undefined ? 0 : Number(stored.version)
|
|
97
|
+
if (actual !== record.version - 1) {
|
|
98
|
+
return yield* Effect.fail(
|
|
99
|
+
new CredentialConflict({
|
|
100
|
+
id: record.id,
|
|
101
|
+
expectedVersion: record.version - 1,
|
|
102
|
+
actualVersion: actual
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
yield* sql`
|
|
107
|
+
INSERT INTO control_credentials (id, name, ciphertext, nonce, version, updated_at_ms)
|
|
108
|
+
VALUES (
|
|
109
|
+
${record.id},
|
|
110
|
+
${record.name},
|
|
111
|
+
${record.ciphertext},
|
|
112
|
+
${record.nonce},
|
|
113
|
+
${record.version},
|
|
114
|
+
${record.updatedAtMs}
|
|
115
|
+
)
|
|
116
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
117
|
+
name = excluded.name,
|
|
118
|
+
ciphertext = excluded.ciphertext,
|
|
119
|
+
nonce = excluded.nonce,
|
|
120
|
+
version = excluded.version,
|
|
121
|
+
updated_at_ms = excluded.updated_at_ms
|
|
122
|
+
`
|
|
123
|
+
})).pipe(
|
|
124
|
+
Effect.mapError((cause): Unavailable | CredentialConflict =>
|
|
125
|
+
cause instanceof CredentialConflict ? cause : unavailable("write")()
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const remove = (id: string): Effect.Effect<void, Unavailable> =>
|
|
130
|
+
sql`DELETE FROM control_credentials WHERE id = ${id}`.pipe(
|
|
131
|
+
Effect.asVoid,
|
|
132
|
+
Effect.mapError(unavailable("remove"))
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return CredentialStore.make({ list, read, write, remove })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Provides durable credential persistence over the ambient database.
|
|
140
|
+
*
|
|
141
|
+
* @category layers
|
|
142
|
+
* @since 0.1.0
|
|
143
|
+
*/
|
|
144
|
+
export const layer: Layer.Layer<
|
|
145
|
+
CredentialStore.CredentialStore,
|
|
146
|
+
Unavailable,
|
|
147
|
+
DurableWriter | SqlClient.SqlClient
|
|
148
|
+
> = Layer.effect(CredentialStore.CredentialStore)(make)
|
package/src/Steering.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The steer lifecycle as the control plane reads it back.
|
|
3
|
+
*
|
|
4
|
+
* A steer has two durable moments and two different writers. The control plane
|
|
5
|
+
* records the first — `control.steer.enqueued`, written beside the admission
|
|
6
|
+
* it caused. The second belongs to `@smthrs/notifications`: a turn boundary
|
|
7
|
+
* promotes the notification and the queue journals the promotion. This module
|
|
8
|
+
* projects that promotion into the control vocabulary, so a watcher folds one
|
|
9
|
+
* stream instead of two, and so nothing writes "delivered" twice.
|
|
10
|
+
*
|
|
11
|
+
* Deriving rather than re-recording is what keeps the two halves honest. A
|
|
12
|
+
* control plane that emitted its own delivery record would be asserting a fact
|
|
13
|
+
* it did not observe: the boundary that delivered the steer runs in the agent
|
|
14
|
+
* process, not this one.
|
|
15
|
+
*
|
|
16
|
+
* @since 0.1.0
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ControlEvent } from "./ControlSchema.ts"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The journal event type the notification queue records a promotion under.
|
|
23
|
+
*
|
|
24
|
+
* Named as a string rather than imported so this projection reads a journal
|
|
25
|
+
* written by any queue implementation, exactly as `Lineage` reads engine
|
|
26
|
+
* decisions.
|
|
27
|
+
*
|
|
28
|
+
* @category constants
|
|
29
|
+
* @since 0.1.0
|
|
30
|
+
*/
|
|
31
|
+
export const promotedEventType = "flows/notifications/Promoted"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The kind `watch` reports one delivered steer under.
|
|
35
|
+
*
|
|
36
|
+
* @category constants
|
|
37
|
+
* @since 0.1.0
|
|
38
|
+
*/
|
|
39
|
+
export const deliveredEventType = "control.steer.delivered"
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The kind `Control.steer` records an accepted steer under.
|
|
43
|
+
*
|
|
44
|
+
* @category constants
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
export const enqueuedEventType = "control.steer.enqueued"
|
|
48
|
+
|
|
49
|
+
const record = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
50
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
51
|
+
? value as Readonly<Record<string, unknown>>
|
|
52
|
+
: undefined
|
|
53
|
+
|
|
54
|
+
const strings = (value: unknown): ReadonlyArray<string> =>
|
|
55
|
+
Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : []
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derives one delivery delta per message a promotion entry named.
|
|
59
|
+
*
|
|
60
|
+
* A boundary promotes a batch, so one entry can disclose several deliveries.
|
|
61
|
+
* Each delta carries the source sequence and the boundary that took it.
|
|
62
|
+
* `ControlLive.watch` adds an expansion cursor so a consumer can resume
|
|
63
|
+
* between deliveries in the same batch.
|
|
64
|
+
*
|
|
65
|
+
* A promotion that named nothing derives nothing: an idle boundary drains an
|
|
66
|
+
* empty queue on every turn, and reporting that as a delivery would bury the
|
|
67
|
+
* real ones.
|
|
68
|
+
*
|
|
69
|
+
* @param event the projected journal entry
|
|
70
|
+
* @category projections
|
|
71
|
+
* @since 0.1.0
|
|
72
|
+
*/
|
|
73
|
+
export const derive = (event: ControlEvent): ReadonlyArray<ControlEvent> => {
|
|
74
|
+
const runId = event.runId
|
|
75
|
+
if (event.kind !== promotedEventType || runId === undefined) return []
|
|
76
|
+
const payload = record(event.payload)
|
|
77
|
+
if (payload === undefined) return []
|
|
78
|
+
const boundary = typeof payload["boundary"] === "string" ? payload["boundary"] : undefined
|
|
79
|
+
if (boundary === undefined) return []
|
|
80
|
+
return strings(payload["ids"]).map((messageId): ControlEvent => ({
|
|
81
|
+
sequence: event.sequence,
|
|
82
|
+
kind: deliveredEventType,
|
|
83
|
+
runId,
|
|
84
|
+
occurredAt: event.occurredAt,
|
|
85
|
+
payload: { runId, messageId, boundary }
|
|
86
|
+
}))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Expands one projected entry into itself plus any deliveries it discloses.
|
|
91
|
+
*
|
|
92
|
+
* @param event the projected journal entry
|
|
93
|
+
* @category projections
|
|
94
|
+
* @since 0.1.0
|
|
95
|
+
*/
|
|
96
|
+
export const expand = (event: ControlEvent): ReadonlyArray<ControlEvent> => [event, ...derive(event)]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved system-flow identifiers projected by the command-line interface.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.1.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The projection metadata for one reserved system flow.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.1.0
|
|
11
|
+
* @category models
|
|
12
|
+
*/
|
|
13
|
+
export interface SystemFlowEntry {
|
|
14
|
+
readonly verb: string
|
|
15
|
+
readonly flowId: `system/${string}`
|
|
16
|
+
readonly projection: "procedure" | "systemFlow"
|
|
17
|
+
readonly deployClass: boolean
|
|
18
|
+
readonly planBearing: boolean
|
|
19
|
+
/**
|
|
20
|
+
* Whether a control plane may plan and run this reserved id.
|
|
21
|
+
*
|
|
22
|
+
* `false` means the row is command-line metadata and nothing else: the verb
|
|
23
|
+
* is named here so the binary can refuse it by name, and no runtime may
|
|
24
|
+
* offer it as a flow. `replay` was removed by the frozen rc.0 contract yet
|
|
25
|
+
* every runtime turned the whole catalog into plannable flows, so
|
|
26
|
+
* `plan({flowId: "system/replay"})` returned a real approval card and only a
|
|
27
|
+
* later `run` failed. A removed feature that half works is exactly what the
|
|
28
|
+
* contract forbids.
|
|
29
|
+
*/
|
|
30
|
+
readonly plannable: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The authoritative command-line verb to reserved-flow map.
|
|
35
|
+
*
|
|
36
|
+
* `gc` is the retention verb: it resolves `@smthrs/engine-store`'s `retain`
|
|
37
|
+
* operation, which deletes aged terminal runs with their attempts, clock
|
|
38
|
+
* deadlines, deferred completions, journal entries, and time-travel archive
|
|
39
|
+
* rows. It is a procedure rather than a deploy-class system flow because it
|
|
40
|
+
* takes no plan and bears no approval envelope; the deletion is gated by the
|
|
41
|
+
* operator invoking it, and automatic retention stays opt-in.
|
|
42
|
+
*
|
|
43
|
+
* @since 0.1.0
|
|
44
|
+
* @category models
|
|
45
|
+
*/
|
|
46
|
+
export const catalog = [
|
|
47
|
+
{
|
|
48
|
+
verb: "plan",
|
|
49
|
+
flowId: "system/plan",
|
|
50
|
+
projection: "procedure",
|
|
51
|
+
deployClass: false,
|
|
52
|
+
planBearing: false,
|
|
53
|
+
plannable: true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
verb: "run",
|
|
57
|
+
flowId: "system/run",
|
|
58
|
+
projection: "procedure",
|
|
59
|
+
deployClass: false,
|
|
60
|
+
planBearing: false,
|
|
61
|
+
plannable: true
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
verb: "release",
|
|
65
|
+
flowId: "system/release",
|
|
66
|
+
projection: "systemFlow",
|
|
67
|
+
deployClass: true,
|
|
68
|
+
planBearing: true,
|
|
69
|
+
plannable: true
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
verb: "serve",
|
|
73
|
+
flowId: "system/serve",
|
|
74
|
+
projection: "systemFlow",
|
|
75
|
+
deployClass: true,
|
|
76
|
+
planBearing: true,
|
|
77
|
+
plannable: true
|
|
78
|
+
},
|
|
79
|
+
{ verb: "up", flowId: "system/up", projection: "systemFlow", deployClass: true, planBearing: true, plannable: true },
|
|
80
|
+
{ verb: "ls", flowId: "system/ls", projection: "procedure", deployClass: false, planBearing: false, plannable: true },
|
|
81
|
+
{ verb: "ps", flowId: "system/ps", projection: "procedure", deployClass: false, planBearing: false, plannable: true },
|
|
82
|
+
{
|
|
83
|
+
verb: "logs",
|
|
84
|
+
flowId: "system/logs",
|
|
85
|
+
projection: "procedure",
|
|
86
|
+
deployClass: false,
|
|
87
|
+
planBearing: false,
|
|
88
|
+
plannable: true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
verb: "status",
|
|
92
|
+
flowId: "system/status",
|
|
93
|
+
projection: "procedure",
|
|
94
|
+
deployClass: false,
|
|
95
|
+
planBearing: false,
|
|
96
|
+
plannable: true
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
verb: "cancel",
|
|
100
|
+
flowId: "system/cancel",
|
|
101
|
+
projection: "procedure",
|
|
102
|
+
deployClass: false,
|
|
103
|
+
planBearing: false,
|
|
104
|
+
plannable: true
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
verb: "approve",
|
|
108
|
+
flowId: "system/approve",
|
|
109
|
+
projection: "procedure",
|
|
110
|
+
deployClass: false,
|
|
111
|
+
planBearing: false,
|
|
112
|
+
plannable: true
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
verb: "deny",
|
|
116
|
+
flowId: "system/deny",
|
|
117
|
+
projection: "procedure",
|
|
118
|
+
deployClass: false,
|
|
119
|
+
planBearing: false,
|
|
120
|
+
plannable: true
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
verb: "signal",
|
|
124
|
+
flowId: "system/signal",
|
|
125
|
+
projection: "procedure",
|
|
126
|
+
deployClass: false,
|
|
127
|
+
planBearing: false,
|
|
128
|
+
plannable: true
|
|
129
|
+
},
|
|
130
|
+
{ verb: "gc", flowId: "system/gc", projection: "procedure", deployClass: false, planBearing: false, plannable: true },
|
|
131
|
+
{
|
|
132
|
+
verb: "replay",
|
|
133
|
+
flowId: "system/replay",
|
|
134
|
+
projection: "systemFlow",
|
|
135
|
+
deployClass: false,
|
|
136
|
+
planBearing: false,
|
|
137
|
+
plannable: false
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
verb: "add",
|
|
141
|
+
flowId: "system/add",
|
|
142
|
+
projection: "systemFlow",
|
|
143
|
+
deployClass: false,
|
|
144
|
+
planBearing: true,
|
|
145
|
+
plannable: true
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
verb: "remove",
|
|
149
|
+
flowId: "system/remove",
|
|
150
|
+
projection: "systemFlow",
|
|
151
|
+
deployClass: false,
|
|
152
|
+
planBearing: true,
|
|
153
|
+
plannable: true
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
verb: "eject",
|
|
157
|
+
flowId: "system/eject",
|
|
158
|
+
projection: "systemFlow",
|
|
159
|
+
deployClass: false,
|
|
160
|
+
planBearing: true,
|
|
161
|
+
plannable: true
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
verb: "test",
|
|
165
|
+
flowId: "system/test",
|
|
166
|
+
projection: "systemFlow",
|
|
167
|
+
deployClass: false,
|
|
168
|
+
planBearing: false,
|
|
169
|
+
plannable: true
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
verb: "init",
|
|
173
|
+
flowId: "system/init",
|
|
174
|
+
projection: "systemFlow",
|
|
175
|
+
deployClass: false,
|
|
176
|
+
planBearing: true,
|
|
177
|
+
plannable: true
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
verb: "doctor",
|
|
181
|
+
flowId: "system/doctor",
|
|
182
|
+
projection: "systemFlow",
|
|
183
|
+
deployClass: false,
|
|
184
|
+
planBearing: false,
|
|
185
|
+
plannable: true
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
verb: "migrate",
|
|
189
|
+
flowId: "system/migrate",
|
|
190
|
+
projection: "systemFlow",
|
|
191
|
+
deployClass: false,
|
|
192
|
+
planBearing: true,
|
|
193
|
+
plannable: true
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
// A build target declaring `approval: "required"`: its plan input is the
|
|
197
|
+
// target's label and revision digest, and the build planner runs it only
|
|
198
|
+
// once that plan is approved. The build executes it, never a control run.
|
|
199
|
+
verb: "target",
|
|
200
|
+
flowId: "system/target",
|
|
201
|
+
projection: "systemFlow",
|
|
202
|
+
deployClass: true,
|
|
203
|
+
planBearing: true,
|
|
204
|
+
plannable: true
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
verb: "docs",
|
|
208
|
+
flowId: "system/docs",
|
|
209
|
+
projection: "systemFlow",
|
|
210
|
+
deployClass: false,
|
|
211
|
+
planBearing: false,
|
|
212
|
+
plannable: true
|
|
213
|
+
}
|
|
214
|
+
] as const satisfies ReadonlyArray<SystemFlowEntry>
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The catalog entries a control runtime may offer as flows.
|
|
218
|
+
*
|
|
219
|
+
* One list, so a composition that builds its own flow map (the CLI does) and
|
|
220
|
+
* the runtimes' own defaults cannot disagree about which reserved ids exist.
|
|
221
|
+
*
|
|
222
|
+
* @since 0.1.0
|
|
223
|
+
* @category models
|
|
224
|
+
*/
|
|
225
|
+
export const plannable: ReadonlyArray<SystemFlowEntry> = catalog.filter((entry) => entry.plannable)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM credential encryption over the Web Crypto API.
|
|
3
|
+
*
|
|
4
|
+
* One adapter serves both hosts the project supports: `globalThis.crypto.subtle`
|
|
5
|
+
* is the browser's own API and has been Node's since v19, so this module
|
|
6
|
+
* imports nothing from `node:*` and still runs unmodified on the server. A host
|
|
7
|
+
* without Web Crypto, such as an old runtime or a locked-down worker, fails
|
|
8
|
+
* with the typed `Unavailable`, never a defect. A key that is not 32
|
|
9
|
+
* base64-encoded bytes fails with `InvalidInput`. A record that does not open
|
|
10
|
+
* fails with `PersistenceError` on operation `credential.open`, which names
|
|
11
|
+
* whether the stored nonce was malformed or the ciphertext failed
|
|
12
|
+
* authentication (a different key, changed metadata, or tampered bytes).
|
|
13
|
+
*
|
|
14
|
+
* The key is host-managed and supplied at layer construction. It is held as a
|
|
15
|
+
* non-extractable `CryptoKey`, so it cannot be read back out of the cipher, and
|
|
16
|
+
* it never reaches `CredentialStore`.
|
|
17
|
+
*
|
|
18
|
+
* @since 0.1.0
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { canonicalize } from "@smthrs/canonical"
|
|
22
|
+
import { Effect, Layer, Redacted } from "effect"
|
|
23
|
+
import { InvalidInput, PersistenceError, type Unavailable } from "./ControlError.ts"
|
|
24
|
+
import * as CredentialCipher from "./CredentialCipher.ts"
|
|
25
|
+
|
|
26
|
+
const algorithm = "AES-GCM"
|
|
27
|
+
const nonceBytes = 12
|
|
28
|
+
const contextFormatVersion = 1
|
|
29
|
+
|
|
30
|
+
const toBase64 = (bytes: Uint8Array): string => {
|
|
31
|
+
let binary = ""
|
|
32
|
+
for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
33
|
+
return btoa(binary)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fromBase64 = (value: string): Uint8Array<ArrayBuffer> => {
|
|
37
|
+
const binary = atob(value)
|
|
38
|
+
const bytes = new Uint8Array(binary.length)
|
|
39
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index)
|
|
40
|
+
return bytes
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const subtle = (): Effect.Effect<SubtleCrypto, Unavailable> =>
|
|
44
|
+
Effect.suspend(() => {
|
|
45
|
+
const available = globalThis.crypto as Crypto | undefined
|
|
46
|
+
return available?.subtle === undefined
|
|
47
|
+
? Effect.fail(CredentialCipher.unavailable())
|
|
48
|
+
: Effect.succeed(available.subtle)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const randomNonce = (): Effect.Effect<Uint8Array<ArrayBuffer>, Unavailable> =>
|
|
52
|
+
Effect.suspend(() => {
|
|
53
|
+
const available = globalThis.crypto as Crypto | undefined
|
|
54
|
+
return available?.getRandomValues === undefined
|
|
55
|
+
? Effect.fail(CredentialCipher.unavailable())
|
|
56
|
+
: Effect.succeed(available.getRandomValues(new Uint8Array(nonceBytes)))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const encodeContext = (context: CredentialCipher.Context): Effect.Effect<Uint8Array<ArrayBuffer>, Unavailable> =>
|
|
60
|
+
Effect.try({
|
|
61
|
+
try: () => {
|
|
62
|
+
// Canonical JSON names and escapes each field, so embedded delimiters
|
|
63
|
+
// cannot make two metadata tuples authenticate as the same bytes. The
|
|
64
|
+
// explicit format version makes a future byte contract distinguishable.
|
|
65
|
+
const document = canonicalize({
|
|
66
|
+
formatVersion: contextFormatVersion,
|
|
67
|
+
id: context.id,
|
|
68
|
+
name: context.name,
|
|
69
|
+
version: context.version
|
|
70
|
+
})
|
|
71
|
+
return new TextEncoder().encode(document)
|
|
72
|
+
},
|
|
73
|
+
catch: CredentialCipher.unavailable
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const invalidKey = (): InvalidInput => new InvalidInput({ issue: "Credential key must be 32 base64-encoded bytes" })
|
|
77
|
+
|
|
78
|
+
const unopenable = (reason: "malformed_nonce" | "authentication_failed", cause: unknown): PersistenceError =>
|
|
79
|
+
new PersistenceError({
|
|
80
|
+
operation: "credential.open",
|
|
81
|
+
message: reason === "malformed_nonce"
|
|
82
|
+
? "Stored credential nonce is malformed"
|
|
83
|
+
: "Credential failed authentication: a different key, changed metadata, or tampered ciphertext",
|
|
84
|
+
cause
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Host-managed key material for the cipher.
|
|
89
|
+
*
|
|
90
|
+
* `key` is 32 raw bytes, base64-encoded, held redacted so it cannot be printed
|
|
91
|
+
* or serialized by accident.
|
|
92
|
+
*
|
|
93
|
+
* @category models
|
|
94
|
+
* @since 0.1.0
|
|
95
|
+
*/
|
|
96
|
+
export interface Options {
|
|
97
|
+
readonly key: Redacted.Redacted<string>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Imports the host key and constructs the cipher.
|
|
102
|
+
*
|
|
103
|
+
* @category constructors
|
|
104
|
+
* @since 0.1.0
|
|
105
|
+
*/
|
|
106
|
+
export const make = (options: Options): Effect.Effect<CredentialCipher.Service, Unavailable | InvalidInput> =>
|
|
107
|
+
Effect.gen(function*() {
|
|
108
|
+
const crypto = yield* subtle()
|
|
109
|
+
const raw = yield* Effect.try({
|
|
110
|
+
try: () => fromBase64(Redacted.value(options.key)),
|
|
111
|
+
catch: invalidKey
|
|
112
|
+
})
|
|
113
|
+
if (raw.length !== 32) return yield* Effect.fail(invalidKey())
|
|
114
|
+
const key = yield* Effect.tryPromise({
|
|
115
|
+
try: () => crypto.importKey("raw", raw, algorithm, false, ["encrypt", "decrypt"]),
|
|
116
|
+
catch: CredentialCipher.unavailable
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return CredentialCipher.make({
|
|
120
|
+
seal: Effect.fn("WebCryptoCipher.seal")(function*(plaintext, context) {
|
|
121
|
+
const additionalData = yield* encodeContext(context)
|
|
122
|
+
const nonce = yield* randomNonce()
|
|
123
|
+
const ciphertext = yield* Effect.tryPromise({
|
|
124
|
+
try: () =>
|
|
125
|
+
crypto.encrypt(
|
|
126
|
+
{ name: algorithm, iv: nonce, additionalData: additionalData },
|
|
127
|
+
key,
|
|
128
|
+
new TextEncoder().encode(Redacted.value(plaintext))
|
|
129
|
+
),
|
|
130
|
+
catch: CredentialCipher.unavailable
|
|
131
|
+
})
|
|
132
|
+
return { ciphertext: toBase64(new Uint8Array(ciphertext)), nonce: toBase64(nonce) }
|
|
133
|
+
}),
|
|
134
|
+
open: Effect.fn("WebCryptoCipher.open")(function*(sealed, context) {
|
|
135
|
+
const additionalData = yield* encodeContext(context)
|
|
136
|
+
const plaintext = yield* Effect.try({
|
|
137
|
+
try: () => fromBase64(sealed.nonce),
|
|
138
|
+
catch: (cause) => unopenable("malformed_nonce", cause)
|
|
139
|
+
}).pipe(
|
|
140
|
+
Effect.flatMap((nonce) =>
|
|
141
|
+
Effect.tryPromise({
|
|
142
|
+
try: () =>
|
|
143
|
+
crypto.decrypt(
|
|
144
|
+
{ name: algorithm, iv: nonce, additionalData: additionalData },
|
|
145
|
+
key,
|
|
146
|
+
fromBase64(sealed.ciphertext)
|
|
147
|
+
),
|
|
148
|
+
catch: (cause) => unopenable("authentication_failed", cause)
|
|
149
|
+
})
|
|
150
|
+
),
|
|
151
|
+
Effect.tapError((error) =>
|
|
152
|
+
Effect.logWarning({ message: error.message, operation: error.operation, credentialId: context.id })
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
return Redacted.make(new TextDecoder().decode(plaintext))
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Provides AES-256-GCM credential encryption under a host-managed key.
|
|
162
|
+
*
|
|
163
|
+
* @category layers
|
|
164
|
+
* @since 0.1.0
|
|
165
|
+
*/
|
|
166
|
+
export const layer = (
|
|
167
|
+
options: Options
|
|
168
|
+
): Layer.Layer<CredentialCipher.CredentialCipher, Unavailable | InvalidInput> =>
|
|
169
|
+
Layer.effect(CredentialCipher.CredentialCipher)(make(options))
|