@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,190 @@
|
|
|
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
|
+
import { Context, Effect, Layer, Option, Redacted } from "effect";
|
|
30
|
+
import { CredentialConflict, InvalidInput, Unauthorized, Unavailable } from "./ControlError.js";
|
|
31
|
+
import * as CredentialCipher from "./CredentialCipher.js";
|
|
32
|
+
import * as CredentialStore from "./CredentialStore.js";
|
|
33
|
+
// String.prototype.isWellFormed without the ES2024 lib, which every package
|
|
34
|
+
// compiling this source would also need. A loop, not a regex: a match would
|
|
35
|
+
// leave the rejected secret in the legacy RegExp.input static.
|
|
36
|
+
const wellFormed = (value) => {
|
|
37
|
+
for (let index = 0; index < value.length; index++) {
|
|
38
|
+
const unit = value.charCodeAt(index);
|
|
39
|
+
if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
40
|
+
return false;
|
|
41
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
42
|
+
const low = value.charCodeAt(++index);
|
|
43
|
+
if (!(low >= 0xdc00 && low <= 0xdfff))
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Service tag for credential resolution.
|
|
51
|
+
*
|
|
52
|
+
* @category services
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
*/
|
|
55
|
+
export const Credential = Context.Service("/control/Credential");
|
|
56
|
+
const unavailable = () => new Unavailable({
|
|
57
|
+
feature: "credential storage",
|
|
58
|
+
ticket: "control-credential-storage"
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* A credential implementation that explicitly reports unavailable storage.
|
|
62
|
+
*
|
|
63
|
+
* @category constructors
|
|
64
|
+
* @since 0.1.0
|
|
65
|
+
*/
|
|
66
|
+
export const makeNoop = () => Credential.of({
|
|
67
|
+
list: Effect.fn("Credential.list")(() => Effect.fail(unavailable())),
|
|
68
|
+
get: Effect.fn("Credential.get")(() => Effect.fail(unavailable())),
|
|
69
|
+
create: Effect.fn("Credential.create")(() => Effect.fail(unavailable())),
|
|
70
|
+
resolve: Effect.fn("Credential.resolve")(() => Effect.fail(unavailable())),
|
|
71
|
+
rotate: Effect.fn("Credential.rotate")(() => Effect.fail(unavailable())),
|
|
72
|
+
revoke: Effect.fn("Credential.revoke")(() => Effect.fail(unavailable()))
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Layer for the unavailable credential-storage boundary.
|
|
76
|
+
*
|
|
77
|
+
* @category layers
|
|
78
|
+
* @since 0.1.0
|
|
79
|
+
*/
|
|
80
|
+
export const layerNoop = Layer.succeed(Credential, makeNoop());
|
|
81
|
+
const referenceOf = (value) => Object.freeze({
|
|
82
|
+
id: value.id,
|
|
83
|
+
name: value.name
|
|
84
|
+
});
|
|
85
|
+
const cipherContextOf = (value) => Object.freeze({
|
|
86
|
+
id: value.id,
|
|
87
|
+
name: value.name,
|
|
88
|
+
version: value.version
|
|
89
|
+
});
|
|
90
|
+
const missing = (id) =>
|
|
91
|
+
// Deliberately indistinguishable from a denied reference: telling an
|
|
92
|
+
// unauthorized caller which ids exist is itself a leak.
|
|
93
|
+
new Unauthorized({ message: `Credential ${id} is not available to this caller` });
|
|
94
|
+
/**
|
|
95
|
+
* Constructs a working credential boundary over a store and a cipher.
|
|
96
|
+
*
|
|
97
|
+
* @category constructors
|
|
98
|
+
* @since 0.1.0
|
|
99
|
+
*/
|
|
100
|
+
export const make = (options) => {
|
|
101
|
+
const authorize = options.authorize ?? (() => Effect.void);
|
|
102
|
+
const store = options.store;
|
|
103
|
+
const cipher = options.cipher;
|
|
104
|
+
/** Reads the record `reference` names, refusing a forged or stale one. */
|
|
105
|
+
const authenticate = (operation, reference) => Effect.gen(function* () {
|
|
106
|
+
const snapshot = referenceOf(reference);
|
|
107
|
+
yield* authorize(operation, Option.some(snapshot));
|
|
108
|
+
const record = yield* store.read(snapshot.id);
|
|
109
|
+
if (Option.isNone(record))
|
|
110
|
+
return yield* Effect.fail(missing(snapshot.id));
|
|
111
|
+
if (record.value.name !== snapshot.name)
|
|
112
|
+
return yield* Effect.fail(missing(snapshot.id));
|
|
113
|
+
return record.value;
|
|
114
|
+
});
|
|
115
|
+
const put = (record, secret) => Effect.gen(function* () {
|
|
116
|
+
if (!wellFormed(Redacted.value(secret))) {
|
|
117
|
+
return yield* Effect.fail(new InvalidInput({ issue: "Credential secret must be well-formed Unicode" }));
|
|
118
|
+
}
|
|
119
|
+
// The metadata written beside this blob is also its authenticated data;
|
|
120
|
+
// moving the blob to another id, name, or version must make it unreadable.
|
|
121
|
+
const context = cipherContextOf(record);
|
|
122
|
+
const sealed = yield* cipher.seal(secret, context);
|
|
123
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
124
|
+
yield* store.write({ ...record, ...sealed, updatedAtMs: now });
|
|
125
|
+
return referenceOf(record);
|
|
126
|
+
});
|
|
127
|
+
return Credential.of({
|
|
128
|
+
list: Effect.fn("Credential.list")(function* () {
|
|
129
|
+
yield* authorize("list", Option.none());
|
|
130
|
+
const records = yield* store.list();
|
|
131
|
+
return records.map(referenceOf);
|
|
132
|
+
}),
|
|
133
|
+
get: Effect.fn("Credential.get")(function* (id) {
|
|
134
|
+
const record = yield* store.read(id);
|
|
135
|
+
if (Option.isNone(record))
|
|
136
|
+
return yield* Effect.fail(missing(id));
|
|
137
|
+
const reference = referenceOf(record.value);
|
|
138
|
+
yield* authorize("get", Option.some(reference));
|
|
139
|
+
return reference;
|
|
140
|
+
}),
|
|
141
|
+
create: Effect.fn("Credential.create")(function* (input) {
|
|
142
|
+
const snapshot = Object.freeze({
|
|
143
|
+
reference: referenceOf(input),
|
|
144
|
+
secret: input.secret
|
|
145
|
+
});
|
|
146
|
+
yield* authorize("create", Option.some(snapshot.reference));
|
|
147
|
+
return yield* put({
|
|
148
|
+
id: snapshot.reference.id,
|
|
149
|
+
name: snapshot.reference.name,
|
|
150
|
+
ciphertext: "",
|
|
151
|
+
nonce: "",
|
|
152
|
+
version: 1,
|
|
153
|
+
updatedAtMs: 0
|
|
154
|
+
}, snapshot.secret);
|
|
155
|
+
}),
|
|
156
|
+
resolve: Effect.fn("Credential.resolve")(function* (reference) {
|
|
157
|
+
const record = yield* authenticate("resolve", reference);
|
|
158
|
+
return yield* cipher.open({ ciphertext: record.ciphertext, nonce: record.nonce }, cipherContextOf(record));
|
|
159
|
+
}),
|
|
160
|
+
rotate: Effect.fn("Credential.rotate")(function* (reference, secret, options) {
|
|
161
|
+
const expected = options?.expected;
|
|
162
|
+
const record = yield* authenticate("rotate", reference);
|
|
163
|
+
if (expected !== undefined) {
|
|
164
|
+
const stored = yield* cipher.open({ ciphertext: record.ciphertext, nonce: record.nonce }, cipherContextOf(record));
|
|
165
|
+
if (Redacted.value(stored) !== Redacted.value(expected)) {
|
|
166
|
+
return yield* Effect.fail(new CredentialConflict({ id: record.id, expectedVersion: record.version, actualVersion: record.version }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// `put` writes over exactly the version read above, so the secret just
|
|
170
|
+
// compared is still the stored one when the write commits.
|
|
171
|
+
return yield* put({ ...record, version: record.version + 1 }, secret);
|
|
172
|
+
}),
|
|
173
|
+
revoke: Effect.fn("Credential.revoke")(function* (reference) {
|
|
174
|
+
const record = yield* authenticate("revoke", reference);
|
|
175
|
+
yield* store.remove(record.id);
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Provides a working credential boundary over the ambient store and cipher.
|
|
181
|
+
*
|
|
182
|
+
* @category layers
|
|
183
|
+
* @since 0.1.0
|
|
184
|
+
*/
|
|
185
|
+
export const layer = (options = {}) => Layer.effect(Credential)(Effect.gen(function* () {
|
|
186
|
+
const store = yield* CredentialStore.CredentialStore;
|
|
187
|
+
const cipher = yield* CredentialCipher.CredentialCipher;
|
|
188
|
+
return make({ store, cipher, ...(options.authorize === undefined ? {} : { authorize: options.authorize }) });
|
|
189
|
+
}));
|
|
190
|
+
//# sourceMappingURL=Credential.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Credential.js","sourceRoot":"","sources":["../../src/Credential.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential storage boundary.\n *\n * Credentials are capabilities. They must never enter flow input, plan\n * digests, journal payloads, or model context, so only a {@link CredentialRef}\n * — an id and a human-readable connection name — crosses the browser-safe\n * contract. Plaintext exists in exactly two places: inside a `Redacted` handed\n * to `create`/`rotate`, and inside the `Redacted` returned by `resolve`.\n *\n * The implementation composes two ports:\n *\n * - {@link CredentialStore} persists an opaque sealed record and enforces\n * compare-and-set on writes, so concurrent rotations cannot interleave.\n * `rotate` with `expected` extends that compare to the secret itself: the\n * secret it checks and the version it writes over come from one read, so a\n * writer that commits in between, in any process, is refused.\n * - {@link CredentialCipher} seals and opens the secret under host-managed\n * keys. `WebCryptoCipher` is the browser and Node adapter; a host with no\n * secure key material fails with the typed `Unavailable`.\n *\n * Authorization is a host decision, injected as {@link Options.authorize}. A\n * reference is also authenticated on every operation: caller-owned fields are\n * snapshotted before policy effects run, and a caller cannot present a forged\n * or stale `CredentialRef` because the snapshotted name must still match the\n * stored record.\n *\n * @since 0.1.0\n */\n\nimport { Context, Effect, Layer, Option, Redacted } from \"effect\"\nimport { CredentialConflict, InvalidInput, type PersistenceError, Unauthorized, Unavailable } from \"./ControlError.ts\"\nimport * as CredentialCipher from \"./CredentialCipher.ts\"\nimport * as CredentialStore from \"./CredentialStore.ts\"\n\n// String.prototype.isWellFormed without the ES2024 lib, which every package\n// compiling this source would also need. A loop, not a regex: a match would\n// leave the rejected secret in the legacy RegExp.input static.\nconst wellFormed = (value: string): boolean => {\n for (let index = 0; index < value.length; index++) {\n const unit = value.charCodeAt(index)\n if (unit >= 0xdc00 && unit <= 0xdfff) return false\n if (unit >= 0xd800 && unit <= 0xdbff) {\n const low = value.charCodeAt(++index)\n if (!(low >= 0xdc00 && low <= 0xdfff)) return false\n }\n }\n return true\n}\n\n/**\n * A journal-safe name for a stored connection credential.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface CredentialRef {\n readonly id: string\n readonly name: string\n}\n\n/**\n * One credential operation, as seen by a host authorization policy.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Operation = \"list\" | \"get\" | \"create\" | \"resolve\" | \"rotate\" | \"revoke\"\n\n/**\n * How {@link Credential.rotate} compares before it writes.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface RotateOptions {\n /** The secret the caller last saw; the rotation commits only over it. */\n readonly expected?: Redacted.Redacted<string> | undefined\n}\n\n/**\n * Adapter-bound credential resolution. References are safe to list and\n * persist; only `resolve` crosses into a secret-bearing adapter boundary.\n *\n * @category services\n * @since 0.1.0\n */\nexport interface Credential {\n readonly list: () => Effect.Effect<ReadonlyArray<CredentialRef>, Unavailable | Unauthorized>\n readonly get: (id: string) => Effect.Effect<CredentialRef, Unavailable | Unauthorized>\n readonly create: (\n options: { readonly id: string; readonly name: string; readonly secret: Redacted.Redacted<string> }\n ) => Effect.Effect<CredentialRef, Unavailable | Unauthorized | CredentialConflict | InvalidInput>\n readonly resolve: (\n reference: CredentialRef\n ) => Effect.Effect<Redacted.Redacted<string>, Unavailable | Unauthorized | PersistenceError>\n /**\n * Replaces the secret. With `expected`, replaces it only while the stored\n * secret still equals `expected`, and otherwise fails `CredentialConflict`\n * at the stored version without writing.\n */\n readonly rotate: (\n reference: CredentialRef,\n secret: Redacted.Redacted<string>,\n options?: RotateOptions | undefined\n ) => Effect.Effect<\n CredentialRef,\n Unavailable | Unauthorized | CredentialConflict | InvalidInput | PersistenceError\n >\n readonly revoke: (reference: CredentialRef) => Effect.Effect<void, Unavailable | Unauthorized>\n}\n\n/**\n * Service tag for credential resolution.\n *\n * @category services\n * @since 0.1.0\n */\nexport const Credential: Context.Service<Credential, Credential> = Context.Service(\"/control/Credential\")\n\nconst unavailable = (): Unavailable =>\n new Unavailable({\n feature: \"credential storage\",\n ticket: \"control-credential-storage\"\n })\n\n/**\n * A credential implementation that explicitly reports unavailable storage.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeNoop = (): Credential =>\n Credential.of({\n list: Effect.fn(\"Credential.list\")(() => Effect.fail(unavailable())),\n get: Effect.fn(\"Credential.get\")(() => Effect.fail(unavailable())),\n create: Effect.fn(\"Credential.create\")(() => Effect.fail(unavailable())),\n resolve: Effect.fn(\"Credential.resolve\")(() => Effect.fail(unavailable())),\n rotate: Effect.fn(\"Credential.rotate\")(() => Effect.fail(unavailable())),\n revoke: Effect.fn(\"Credential.revoke\")(() => Effect.fail(unavailable()))\n })\n\n/**\n * Layer for the unavailable credential-storage boundary.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop = Layer.succeed(Credential, makeNoop())\n\n/**\n * The collaborators a working credential boundary needs.\n *\n * `authorize` is the host's policy hook. It defaults to allowing every\n * operation, which is correct for a single-principal local process; a server\n * composition supplies its own and gets typed `Unauthorized` failures for free.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Options {\n readonly store: CredentialStore.Service\n readonly cipher: CredentialCipher.Service\n readonly authorize?:\n | ((operation: Operation, reference: Option.Option<CredentialRef>) => Effect.Effect<void, Unauthorized>)\n | undefined\n}\n\nconst referenceOf = (value: { readonly id: string; readonly name: string }): CredentialRef =>\n Object.freeze({\n id: value.id,\n name: value.name\n })\n\nconst cipherContextOf = (\n value: { readonly id: string; readonly name: string; readonly version: number }\n): CredentialCipher.Context =>\n Object.freeze({\n id: value.id,\n name: value.name,\n version: value.version\n })\n\nconst missing = (id: string): Unauthorized =>\n // Deliberately indistinguishable from a denied reference: telling an\n // unauthorized caller which ids exist is itself a leak.\n new Unauthorized({ message: `Credential ${id} is not available to this caller` })\n\n/**\n * Constructs a working credential boundary over a store and a cipher.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (options: Options): Credential => {\n const authorize = options.authorize ?? (() => Effect.void)\n const store = options.store\n const cipher = options.cipher\n\n /** Reads the record `reference` names, refusing a forged or stale one. */\n const authenticate = (\n operation: Operation,\n reference: CredentialRef\n ): Effect.Effect<CredentialStore.SealedRecord, Unavailable | Unauthorized> =>\n Effect.gen(function*() {\n const snapshot = referenceOf(reference)\n yield* authorize(operation, Option.some(snapshot))\n const record = yield* store.read(snapshot.id)\n if (Option.isNone(record)) return yield* Effect.fail(missing(snapshot.id))\n if (record.value.name !== snapshot.name) return yield* Effect.fail(missing(snapshot.id))\n return record.value\n })\n\n const put = (\n record: CredentialStore.SealedRecord,\n secret: Redacted.Redacted<string>\n ): Effect.Effect<CredentialRef, Unavailable | CredentialConflict | InvalidInput> =>\n Effect.gen(function*() {\n if (!wellFormed(Redacted.value(secret))) {\n return yield* Effect.fail(new InvalidInput({ issue: \"Credential secret must be well-formed Unicode\" }))\n }\n // The metadata written beside this blob is also its authenticated data;\n // moving the blob to another id, name, or version must make it unreadable.\n const context = cipherContextOf(record)\n const sealed = yield* cipher.seal(secret, context)\n const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis)\n yield* store.write({ ...record, ...sealed, updatedAtMs: now })\n return referenceOf(record)\n })\n\n return Credential.of({\n list: Effect.fn(\"Credential.list\")(function*() {\n yield* authorize(\"list\", Option.none())\n const records = yield* store.list()\n return records.map(referenceOf)\n }),\n get: Effect.fn(\"Credential.get\")(function*(id) {\n const record = yield* store.read(id)\n if (Option.isNone(record)) return yield* Effect.fail(missing(id))\n const reference = referenceOf(record.value)\n yield* authorize(\"get\", Option.some(reference))\n return reference\n }),\n create: Effect.fn(\"Credential.create\")(function*(input) {\n const snapshot = Object.freeze({\n reference: referenceOf(input),\n secret: input.secret\n })\n yield* authorize(\"create\", Option.some(snapshot.reference))\n return yield* put(\n {\n id: snapshot.reference.id,\n name: snapshot.reference.name,\n ciphertext: \"\",\n nonce: \"\",\n version: 1,\n updatedAtMs: 0\n },\n snapshot.secret\n )\n }),\n resolve: Effect.fn(\"Credential.resolve\")(function*(reference) {\n const record = yield* authenticate(\"resolve\", reference)\n return yield* cipher.open(\n { ciphertext: record.ciphertext, nonce: record.nonce },\n cipherContextOf(record)\n )\n }),\n rotate: Effect.fn(\"Credential.rotate\")(function*(reference, secret, options) {\n const expected = options?.expected\n const record = yield* authenticate(\"rotate\", reference)\n if (expected !== undefined) {\n const stored = yield* cipher.open(\n { ciphertext: record.ciphertext, nonce: record.nonce },\n cipherContextOf(record)\n )\n if (Redacted.value(stored) !== Redacted.value(expected)) {\n return yield* Effect.fail(\n new CredentialConflict({ id: record.id, expectedVersion: record.version, actualVersion: record.version })\n )\n }\n }\n // `put` writes over exactly the version read above, so the secret just\n // compared is still the stored one when the write commits.\n return yield* put({ ...record, version: record.version + 1 }, secret)\n }),\n revoke: Effect.fn(\"Credential.revoke\")(function*(reference) {\n const record = yield* authenticate(\"revoke\", reference)\n yield* store.remove(record.id)\n })\n })\n}\n\n/**\n * Provides a working credential boundary over the ambient store and cipher.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer = (\n options: { readonly authorize?: Options[\"authorize\"] } = {}\n): Layer.Layer<\n Credential,\n never,\n CredentialStore.CredentialStore | CredentialCipher.CredentialCipher\n> =>\n Layer.effect(Credential)(Effect.gen(function*() {\n const store = yield* CredentialStore.CredentialStore\n const cipher = yield* CredentialCipher.CredentialCipher\n return make({ store, cipher, ...(options.authorize === undefined ? {} : { authorize: options.authorize }) })\n }))\n"]}
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
import { Context as EffectContext, Effect, Layer, type Redacted } from "effect";
|
|
12
|
+
import { type PersistenceError, Unavailable } from "./ControlError.ts";
|
|
13
|
+
/**
|
|
14
|
+
* One encrypted secret: base64 ciphertext and the nonce it was sealed under.
|
|
15
|
+
*
|
|
16
|
+
* @category models
|
|
17
|
+
* @since 0.1.0
|
|
18
|
+
*/
|
|
19
|
+
export interface Sealed {
|
|
20
|
+
readonly ciphertext: string;
|
|
21
|
+
readonly nonce: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Credential metadata authenticated with one sealed secret.
|
|
25
|
+
*
|
|
26
|
+
* Every field is immutable because the same values must be written beside the
|
|
27
|
+
* ciphertext and supplied again when it is opened.
|
|
28
|
+
*
|
|
29
|
+
* @category models
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
*/
|
|
32
|
+
export interface Context {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly version: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Authenticated encryption over credential plaintext.
|
|
39
|
+
*
|
|
40
|
+
* @category services
|
|
41
|
+
* @since 0.1.0
|
|
42
|
+
*/
|
|
43
|
+
export interface Service {
|
|
44
|
+
readonly seal: (plaintext: Redacted.Redacted<string>, context: Context) => Effect.Effect<Sealed, Unavailable>;
|
|
45
|
+
/**
|
|
46
|
+
* Fails with `PersistenceError` on operation `credential.open` when the
|
|
47
|
+
* record does not open: a malformed nonce, or ciphertext that fails
|
|
48
|
+
* authentication under this key and context.
|
|
49
|
+
*/
|
|
50
|
+
readonly open: (sealed: Sealed, context: Context) => Effect.Effect<Redacted.Redacted<string>, Unavailable | PersistenceError>;
|
|
51
|
+
}
|
|
52
|
+
declare const CredentialCipher_base: EffectContext.ServiceClass<CredentialCipher, "/control/CredentialCipher", Service>;
|
|
53
|
+
/**
|
|
54
|
+
* Service key for credential encryption.
|
|
55
|
+
*
|
|
56
|
+
* @category services
|
|
57
|
+
* @since 0.1.0
|
|
58
|
+
*/
|
|
59
|
+
export declare class CredentialCipher extends CredentialCipher_base {
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Constructs a cipher from an implementation record.
|
|
63
|
+
*
|
|
64
|
+
* @category constructors
|
|
65
|
+
* @since 0.1.0
|
|
66
|
+
*/
|
|
67
|
+
export declare const make: (implementation: Service) => Service;
|
|
68
|
+
/**
|
|
69
|
+
* The typed failure a host reports when no secure key material is reachable.
|
|
70
|
+
*
|
|
71
|
+
* @category constructors
|
|
72
|
+
* @since 0.1.0
|
|
73
|
+
*/
|
|
74
|
+
export declare const unavailable: () => Unavailable;
|
|
75
|
+
/**
|
|
76
|
+
* A cipher that reports unavailable key material for every operation.
|
|
77
|
+
*
|
|
78
|
+
* @category constructors
|
|
79
|
+
* @since 0.1.0
|
|
80
|
+
*/
|
|
81
|
+
export declare const makeNoop: (overrides?: Partial<Service>) => Service;
|
|
82
|
+
/**
|
|
83
|
+
* Provides a cipher that reports unavailable key material.
|
|
84
|
+
*
|
|
85
|
+
* @category layers
|
|
86
|
+
* @since 0.1.0
|
|
87
|
+
*/
|
|
88
|
+
export declare const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<CredentialCipher>;
|
|
89
|
+
export {};
|
|
90
|
+
//# sourceMappingURL=CredentialCipher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialCipher.d.ts","sourceRoot":"","sources":["../../src/CredentialCipher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAC/E,OAAO,EAAE,KAAK,gBAAgB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEtE;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IAC7G;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,CACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,KACb,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,gBAAgB,CAAC,CAAA;CAC9E;;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,qBAErC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,gBAAgB,OAAO,KAAG,OAA8C,CAAA;AAE7F;;;;;GAKG;AACH,eAAO,MAAM,WAAW,QAAO,WAI3B,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,YAAW,OAAO,CAAC,OAAO,CAAM,KAAG,OAKxD,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,YAAW,OAAO,CAAC,OAAO,CAAM,KAAG,KAAK,CAAC,KAAK,CAAC,gBAAgB,CACnC,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
import { Context as EffectContext, Effect, Layer } from "effect";
|
|
12
|
+
import { Unavailable } from "./ControlError.js";
|
|
13
|
+
/**
|
|
14
|
+
* Service key for credential encryption.
|
|
15
|
+
*
|
|
16
|
+
* @category services
|
|
17
|
+
* @since 0.1.0
|
|
18
|
+
*/
|
|
19
|
+
export class CredentialCipher extends EffectContext.Service()("/control/CredentialCipher") {
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Constructs a cipher from an implementation record.
|
|
23
|
+
*
|
|
24
|
+
* @category constructors
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
*/
|
|
27
|
+
export const make = (implementation) => CredentialCipher.of(implementation);
|
|
28
|
+
/**
|
|
29
|
+
* The typed failure a host reports when no secure key material is reachable.
|
|
30
|
+
*
|
|
31
|
+
* @category constructors
|
|
32
|
+
* @since 0.1.0
|
|
33
|
+
*/
|
|
34
|
+
export const unavailable = () => new Unavailable({
|
|
35
|
+
feature: "credential encryption",
|
|
36
|
+
ticket: "control-credential-storage"
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* A cipher that reports unavailable key material for every operation.
|
|
40
|
+
*
|
|
41
|
+
* @category constructors
|
|
42
|
+
* @since 0.1.0
|
|
43
|
+
*/
|
|
44
|
+
export const makeNoop = (overrides = {}) => make({
|
|
45
|
+
seal: Effect.fn("CredentialCipher.seal")(() => Effect.fail(unavailable())),
|
|
46
|
+
open: Effect.fn("CredentialCipher.open")(() => Effect.fail(unavailable())),
|
|
47
|
+
...overrides
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Provides a cipher that reports unavailable key material.
|
|
51
|
+
*
|
|
52
|
+
* @category layers
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
*/
|
|
55
|
+
export const layerNoop = (overrides = {}) => Layer.succeed(CredentialCipher)(makeNoop(overrides));
|
|
56
|
+
//# sourceMappingURL=CredentialCipher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialCipher.js","sourceRoot":"","sources":["../../src/CredentialCipher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,EAAE,KAAK,EAAiB,MAAM,QAAQ,CAAA;AAC/E,OAAO,EAAyB,WAAW,EAAE,MAAM,mBAAmB,CAAA;AA+CtE;;;;;GAKG;AACH,MAAM,OAAO,gBAAiB,SAAQ,aAAa,CAAC,OAAO,EAA6B,CACtF,2BAA2B,CAC5B;CAAG;AAEJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,cAAuB,EAAW,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC,cAAc,CAAC,CAAA;AAE7F;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,GAAgB,EAAE,CAC3C,IAAI,WAAW,CAAC;IACd,OAAO,EAAE,uBAAuB;IAChC,MAAM,EAAE,4BAA4B;CACrC,CAAC,CAAA;AAEJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,YAA8B,EAAE,EAAW,EAAE,CACpE,IAAI,CAAC;IACH,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,uBAAuB,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1E,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,uBAAuB,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1E,GAAG,SAAS;CACb,CAAC,CAAA;AAEJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,YAA8B,EAAE,EAAiC,EAAE,CAC3F,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAA","sourcesContent":["/**\n * Browser-safe encryption port for credential material.\n *\n * The cipher is the only place plaintext exists outside a `Redacted`. Keys are\n * host-managed: an adapter receives a key from its host and never persists one\n * through {@link CredentialStore}, so a stolen store is ciphertext and nothing\n * else.\n *\n * @since 0.1.0\n */\n\nimport { Context as EffectContext, Effect, Layer, type Redacted } from \"effect\"\nimport { type PersistenceError, Unavailable } from \"./ControlError.ts\"\n\n/**\n * One encrypted secret: base64 ciphertext and the nonce it was sealed under.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Sealed {\n readonly ciphertext: string\n readonly nonce: string\n}\n\n/**\n * Credential metadata authenticated with one sealed secret.\n *\n * Every field is immutable because the same values must be written beside the\n * ciphertext and supplied again when it is opened.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Context {\n readonly id: string\n readonly name: string\n readonly version: number\n}\n\n/**\n * Authenticated encryption over credential plaintext.\n *\n * @category services\n * @since 0.1.0\n */\nexport interface Service {\n readonly seal: (plaintext: Redacted.Redacted<string>, context: Context) => Effect.Effect<Sealed, Unavailable>\n /**\n * Fails with `PersistenceError` on operation `credential.open` when the\n * record does not open: a malformed nonce, or ciphertext that fails\n * authentication under this key and context.\n */\n readonly open: (\n sealed: Sealed,\n context: Context\n ) => Effect.Effect<Redacted.Redacted<string>, Unavailable | PersistenceError>\n}\n\n/**\n * Service key for credential encryption.\n *\n * @category services\n * @since 0.1.0\n */\nexport class CredentialCipher extends EffectContext.Service<CredentialCipher, Service>()(\n \"/control/CredentialCipher\"\n) {}\n\n/**\n * Constructs a cipher from an implementation record.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (implementation: Service): Service => CredentialCipher.of(implementation)\n\n/**\n * The typed failure a host reports when no secure key material is reachable.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const unavailable = (): Unavailable =>\n new Unavailable({\n feature: \"credential encryption\",\n ticket: \"control-credential-storage\"\n })\n\n/**\n * A cipher that reports unavailable key material for every operation.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service =>\n make({\n seal: Effect.fn(\"CredentialCipher.seal\")(() => Effect.fail(unavailable())),\n open: Effect.fn(\"CredentialCipher.open\")(() => Effect.fail(unavailable())),\n ...overrides\n })\n\n/**\n * Provides a cipher that reports unavailable key material.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<CredentialCipher> =>\n Layer.succeed(CredentialCipher)(makeNoop(overrides))\n"]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe persistence port for encrypted credential material.
|
|
3
|
+
*
|
|
4
|
+
* The store only ever sees a {@link SealedRecord}: an opaque connection name
|
|
5
|
+
* plus ciphertext. Plaintext never reaches this boundary, so an adapter can be
|
|
6
|
+
* backed by anything — SQL, Web Storage, a remote vault — without becoming a
|
|
7
|
+
* place a secret can leak from.
|
|
8
|
+
*
|
|
9
|
+
* Writes are compare-and-set on `version`. A writer that read version *n*
|
|
10
|
+
* commits version *n + 1*; a concurrent writer that read the same *n* is
|
|
11
|
+
* refused with {@link ControlError.CredentialConflict} rather than silently
|
|
12
|
+
* overwriting the winner.
|
|
13
|
+
*
|
|
14
|
+
* @since 0.1.0
|
|
15
|
+
*/
|
|
16
|
+
import { Context, Effect, Layer, Option } from "effect";
|
|
17
|
+
import { CredentialConflict, Unavailable } from "./ControlError.ts";
|
|
18
|
+
/**
|
|
19
|
+
* One credential at rest: opaque metadata plus the sealed secret.
|
|
20
|
+
*
|
|
21
|
+
* @category models
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
export interface SealedRecord {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/** Base64 ciphertext produced by a `CredentialCipher`. */
|
|
28
|
+
readonly ciphertext: string;
|
|
29
|
+
/** Base64 per-record nonce. Never reused across versions. */
|
|
30
|
+
readonly nonce: string;
|
|
31
|
+
/** Monotonic write counter; 1 for a freshly created credential. */
|
|
32
|
+
readonly version: number;
|
|
33
|
+
readonly updatedAtMs: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The persistence operations `Credential` needs.
|
|
37
|
+
*
|
|
38
|
+
* @category services
|
|
39
|
+
* @since 0.1.0
|
|
40
|
+
*/
|
|
41
|
+
export interface Service {
|
|
42
|
+
readonly list: () => Effect.Effect<ReadonlyArray<SealedRecord>, Unavailable>;
|
|
43
|
+
readonly read: (id: string) => Effect.Effect<Option.Option<SealedRecord>, Unavailable>;
|
|
44
|
+
/** Commits `record` only if the stored version is `record.version - 1`. */
|
|
45
|
+
readonly write: (record: SealedRecord) => Effect.Effect<void, Unavailable | CredentialConflict>;
|
|
46
|
+
readonly remove: (id: string) => Effect.Effect<void, Unavailable>;
|
|
47
|
+
}
|
|
48
|
+
declare const CredentialStore_base: Context.ServiceClass<CredentialStore, "/control/CredentialStore", Service>;
|
|
49
|
+
/**
|
|
50
|
+
* Service key for encrypted credential persistence.
|
|
51
|
+
*
|
|
52
|
+
* @category services
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
*/
|
|
55
|
+
export declare class CredentialStore extends CredentialStore_base {
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Constructs a store from an implementation record.
|
|
59
|
+
*
|
|
60
|
+
* @category constructors
|
|
61
|
+
* @since 0.1.0
|
|
62
|
+
*/
|
|
63
|
+
export declare const make: (implementation: Service) => Service;
|
|
64
|
+
/**
|
|
65
|
+
* A store that reports unavailable persistence for every operation.
|
|
66
|
+
*
|
|
67
|
+
* @category constructors
|
|
68
|
+
* @since 0.1.0
|
|
69
|
+
*/
|
|
70
|
+
export declare const makeNoop: (overrides?: Partial<Service>) => Service;
|
|
71
|
+
/**
|
|
72
|
+
* Provides a store that reports unavailable persistence.
|
|
73
|
+
*
|
|
74
|
+
* @category layers
|
|
75
|
+
* @since 0.1.0
|
|
76
|
+
*/
|
|
77
|
+
export declare const layerNoop: (overrides?: Partial<Service>) => Layer.Layer<CredentialStore>;
|
|
78
|
+
/**
|
|
79
|
+
* Constructs a process-local store.
|
|
80
|
+
*
|
|
81
|
+
* Browser-safe and durable for the lifetime of the process only; it exists so
|
|
82
|
+
* the credential contract can be exercised without a host adapter, and as the
|
|
83
|
+
* fallback a short-lived worker can use.
|
|
84
|
+
*
|
|
85
|
+
* @category constructors
|
|
86
|
+
* @since 0.1.0
|
|
87
|
+
*/
|
|
88
|
+
export declare const makeMemory: () => Service;
|
|
89
|
+
/**
|
|
90
|
+
* Provides a process-local store.
|
|
91
|
+
*
|
|
92
|
+
* @category layers
|
|
93
|
+
* @since 0.1.0
|
|
94
|
+
*/
|
|
95
|
+
export declare const layerMemory: Layer.Layer<CredentialStore>;
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=CredentialStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialStore.d.ts","sourceRoot":"","sources":["../../src/CredentialStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEnE;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,WAAW,CAAC,CAAA;IAC5E,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,WAAW,CAAC,CAAA;IACtF,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,GAAG,kBAAkB,CAAC,CAAA;IAC/F,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAA;CAClE;;AAED;;;;;GAKG;AACH,qBAAa,eAAgB,SAAQ,oBAEpC;CAAG;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,gBAAgB,OAAO,KAAG,OAA6C,CAAA;AAQ5F;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,YAAW,OAAO,CAAC,OAAO,CAAM,KAAG,OAOxD,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,YAAW,OAAO,CAAC,OAAO,CAAM,KAAG,KAAK,CAAC,KAAK,CAAC,eAAe,CACnC,CAAA;AAErD;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,QAAO,OAkC7B,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,eAAe,CAA2C,CAAA"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe persistence port for encrypted credential material.
|
|
3
|
+
*
|
|
4
|
+
* The store only ever sees a {@link SealedRecord}: an opaque connection name
|
|
5
|
+
* plus ciphertext. Plaintext never reaches this boundary, so an adapter can be
|
|
6
|
+
* backed by anything — SQL, Web Storage, a remote vault — without becoming a
|
|
7
|
+
* place a secret can leak from.
|
|
8
|
+
*
|
|
9
|
+
* Writes are compare-and-set on `version`. A writer that read version *n*
|
|
10
|
+
* commits version *n + 1*; a concurrent writer that read the same *n* is
|
|
11
|
+
* refused with {@link ControlError.CredentialConflict} rather than silently
|
|
12
|
+
* overwriting the winner.
|
|
13
|
+
*
|
|
14
|
+
* @since 0.1.0
|
|
15
|
+
*/
|
|
16
|
+
import { Context, Effect, Layer, Option } from "effect";
|
|
17
|
+
import { CredentialConflict, Unavailable } from "./ControlError.js";
|
|
18
|
+
/**
|
|
19
|
+
* Service key for encrypted credential persistence.
|
|
20
|
+
*
|
|
21
|
+
* @category services
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
export class CredentialStore extends Context.Service()("/control/CredentialStore") {
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Constructs a store from an implementation record.
|
|
28
|
+
*
|
|
29
|
+
* @category constructors
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
*/
|
|
32
|
+
export const make = (implementation) => CredentialStore.of(implementation);
|
|
33
|
+
const unavailable = () => new Unavailable({
|
|
34
|
+
feature: "credential storage",
|
|
35
|
+
ticket: "control-credential-storage"
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* A store that reports unavailable persistence for every operation.
|
|
39
|
+
*
|
|
40
|
+
* @category constructors
|
|
41
|
+
* @since 0.1.0
|
|
42
|
+
*/
|
|
43
|
+
export const makeNoop = (overrides = {}) => make({
|
|
44
|
+
list: Effect.fn("CredentialStore.list")(() => Effect.fail(unavailable())),
|
|
45
|
+
read: Effect.fn("CredentialStore.read")(() => Effect.fail(unavailable())),
|
|
46
|
+
write: Effect.fn("CredentialStore.write")(() => Effect.fail(unavailable())),
|
|
47
|
+
remove: Effect.fn("CredentialStore.remove")(() => Effect.fail(unavailable())),
|
|
48
|
+
...overrides
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* Provides a store that reports unavailable persistence.
|
|
52
|
+
*
|
|
53
|
+
* @category layers
|
|
54
|
+
* @since 0.1.0
|
|
55
|
+
*/
|
|
56
|
+
export const layerNoop = (overrides = {}) => Layer.succeed(CredentialStore)(makeNoop(overrides));
|
|
57
|
+
/**
|
|
58
|
+
* Constructs a process-local store.
|
|
59
|
+
*
|
|
60
|
+
* Browser-safe and durable for the lifetime of the process only; it exists so
|
|
61
|
+
* the credential contract can be exercised without a host adapter, and as the
|
|
62
|
+
* fallback a short-lived worker can use.
|
|
63
|
+
*
|
|
64
|
+
* @category constructors
|
|
65
|
+
* @since 0.1.0
|
|
66
|
+
*/
|
|
67
|
+
export const makeMemory = () => {
|
|
68
|
+
const records = new Map();
|
|
69
|
+
// SQL adapters serialize each row on both sides of storage. Copying here
|
|
70
|
+
// keeps the in-memory double from granting callers mutable access to state a
|
|
71
|
+
// durable store would have isolated.
|
|
72
|
+
const copy = (record) => structuredClone(record);
|
|
73
|
+
return make({
|
|
74
|
+
list: Effect.fn("CredentialStore.list")(() => Effect.sync(() => Array.from(records.values(), copy))),
|
|
75
|
+
read: Effect.fn("CredentialStore.read")((id) => Effect.sync(() => Option.map(Option.fromNullishOr(records.get(id)), copy))),
|
|
76
|
+
write: Effect.fn("CredentialStore.write")((record) => Effect.suspend(() => {
|
|
77
|
+
const current = records.get(record.id);
|
|
78
|
+
const actual = current?.version ?? 0;
|
|
79
|
+
if (actual !== record.version - 1) {
|
|
80
|
+
return Effect.fail(new CredentialConflict({
|
|
81
|
+
id: record.id,
|
|
82
|
+
expectedVersion: record.version - 1,
|
|
83
|
+
actualVersion: actual
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
records.set(record.id, copy(record));
|
|
87
|
+
return Effect.void;
|
|
88
|
+
})),
|
|
89
|
+
remove: Effect.fn("CredentialStore.remove")((id) => Effect.sync(() => {
|
|
90
|
+
records.delete(id);
|
|
91
|
+
}))
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Provides a process-local store.
|
|
96
|
+
*
|
|
97
|
+
* @category layers
|
|
98
|
+
* @since 0.1.0
|
|
99
|
+
*/
|
|
100
|
+
export const layerMemory = Layer.sync(CredentialStore)(makeMemory);
|
|
101
|
+
//# sourceMappingURL=CredentialStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CredentialStore.js","sourceRoot":"","sources":["../../src/CredentialStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Browser-safe persistence port for encrypted credential material.\n *\n * The store only ever sees a {@link SealedRecord}: an opaque connection name\n * plus ciphertext. Plaintext never reaches this boundary, so an adapter can be\n * backed by anything — SQL, Web Storage, a remote vault — without becoming a\n * place a secret can leak from.\n *\n * Writes are compare-and-set on `version`. A writer that read version *n*\n * commits version *n + 1*; a concurrent writer that read the same *n* is\n * refused with {@link ControlError.CredentialConflict} rather than silently\n * overwriting the winner.\n *\n * @since 0.1.0\n */\n\nimport { Context, Effect, Layer, Option } from \"effect\"\nimport { CredentialConflict, Unavailable } from \"./ControlError.ts\"\n\n/**\n * One credential at rest: opaque metadata plus the sealed secret.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SealedRecord {\n readonly id: string\n readonly name: string\n /** Base64 ciphertext produced by a `CredentialCipher`. */\n readonly ciphertext: string\n /** Base64 per-record nonce. Never reused across versions. */\n readonly nonce: string\n /** Monotonic write counter; 1 for a freshly created credential. */\n readonly version: number\n readonly updatedAtMs: number\n}\n\n/**\n * The persistence operations `Credential` needs.\n *\n * @category services\n * @since 0.1.0\n */\nexport interface Service {\n readonly list: () => Effect.Effect<ReadonlyArray<SealedRecord>, Unavailable>\n readonly read: (id: string) => Effect.Effect<Option.Option<SealedRecord>, Unavailable>\n /** Commits `record` only if the stored version is `record.version - 1`. */\n readonly write: (record: SealedRecord) => Effect.Effect<void, Unavailable | CredentialConflict>\n readonly remove: (id: string) => Effect.Effect<void, Unavailable>\n}\n\n/**\n * Service key for encrypted credential persistence.\n *\n * @category services\n * @since 0.1.0\n */\nexport class CredentialStore extends Context.Service<CredentialStore, Service>()(\n \"/control/CredentialStore\"\n) {}\n\n/**\n * Constructs a store from an implementation record.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (implementation: Service): Service => CredentialStore.of(implementation)\n\nconst unavailable = (): Unavailable =>\n new Unavailable({\n feature: \"credential storage\",\n ticket: \"control-credential-storage\"\n })\n\n/**\n * A store that reports unavailable persistence for every operation.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service =>\n make({\n list: Effect.fn(\"CredentialStore.list\")(() => Effect.fail(unavailable())),\n read: Effect.fn(\"CredentialStore.read\")(() => Effect.fail(unavailable())),\n write: Effect.fn(\"CredentialStore.write\")(() => Effect.fail(unavailable())),\n remove: Effect.fn(\"CredentialStore.remove\")(() => Effect.fail(unavailable())),\n ...overrides\n })\n\n/**\n * Provides a store that reports unavailable persistence.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<CredentialStore> =>\n Layer.succeed(CredentialStore)(makeNoop(overrides))\n\n/**\n * Constructs a process-local store.\n *\n * Browser-safe and durable for the lifetime of the process only; it exists so\n * the credential contract can be exercised without a host adapter, and as the\n * fallback a short-lived worker can use.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeMemory = (): Service => {\n const records = new Map<string, SealedRecord>()\n // SQL adapters serialize each row on both sides of storage. Copying here\n // keeps the in-memory double from granting callers mutable access to state a\n // durable store would have isolated.\n const copy = (record: SealedRecord): SealedRecord => structuredClone(record)\n return make({\n list: Effect.fn(\"CredentialStore.list\")(() => Effect.sync(() => Array.from(records.values(), copy))),\n read: Effect.fn(\"CredentialStore.read\")((id) =>\n Effect.sync(() => Option.map(Option.fromNullishOr(records.get(id)), copy))\n ),\n write: Effect.fn(\"CredentialStore.write\")((record) =>\n Effect.suspend(() => {\n const current = records.get(record.id)\n const actual = current?.version ?? 0\n if (actual !== record.version - 1) {\n return Effect.fail(\n new CredentialConflict({\n id: record.id,\n expectedVersion: record.version - 1,\n actualVersion: actual\n })\n )\n }\n records.set(record.id, copy(record))\n return Effect.void\n })\n ),\n remove: Effect.fn(\"CredentialStore.remove\")((id) =>\n Effect.sync(() => {\n records.delete(id)\n })\n )\n })\n}\n\n/**\n * Provides a process-local store.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerMemory: Layer.Layer<CredentialStore> = Layer.sync(CredentialStore)(makeMemory)\n"]}
|