@bothat-io/molenkopf 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/SECURITY.md +36 -0
- package/bin/launcher.js +76 -0
- package/bin/molenkopf.js +4 -0
- package/docs/DEPLOYMENT.md +104 -0
- package/docs/MOLENKOPF_PLUGIN_API.md +113 -0
- package/docs/MOLENKOPF_PROVIDER_ENV.md +123 -0
- package/docs/MOLENKOPF_USAGE.md +195 -0
- package/docs/PRODUCT_INTENT.md +36 -0
- package/docs/THREAT_MODEL.md +94 -0
- package/molenkopf.config.example.json +68 -0
- package/package.json +98 -0
- package/packages/core/src/auth/password.ts +47 -0
- package/packages/core/src/auth/session.ts +64 -0
- package/packages/core/src/ci/ci-mode.ts +71 -0
- package/packages/core/src/compression/content-classifier.ts +25 -0
- package/packages/core/src/compression/context-compressor.ts +48 -0
- package/packages/core/src/compression/json-compressor.ts +54 -0
- package/packages/core/src/compression/log-compressor.ts +32 -0
- package/packages/core/src/compression/operational-block-compressor.ts +43 -0
- package/packages/core/src/compression/stacktrace-compressor.ts +23 -0
- package/packages/core/src/config/config-policies.ts +146 -0
- package/packages/core/src/config/molenkopf-config.ts +137 -0
- package/packages/core/src/config/provider-config.ts +139 -0
- package/packages/core/src/events/event-bus.ts +88 -0
- package/packages/core/src/identity/api-keys.ts +149 -0
- package/packages/core/src/identity/budget.ts +51 -0
- package/packages/core/src/identity/db.ts +68 -0
- package/packages/core/src/identity/identity-store.ts +175 -0
- package/packages/core/src/identity/identity-validation.ts +102 -0
- package/packages/core/src/identity/key-permissions.ts +18 -0
- package/packages/core/src/identity/pricing.ts +11 -0
- package/packages/core/src/identity/types.ts +87 -0
- package/packages/core/src/identity/usage-snapshot.ts +116 -0
- package/packages/core/src/manifest/audit-activity.ts +74 -0
- package/packages/core/src/manifest/audit-metrics.ts +7 -0
- package/packages/core/src/manifest/audit-safety.ts +113 -0
- package/packages/core/src/manifest/audit-store.ts +189 -0
- package/packages/core/src/manifest/audit-summary.ts +184 -0
- package/packages/core/src/manifest/usage-meter.ts +105 -0
- package/packages/core/src/memory/memory-extractor.ts +57 -0
- package/packages/core/src/memory/memory-graph.ts +55 -0
- package/packages/core/src/pipeline/json-string-spans.ts +143 -0
- package/packages/core/src/pipeline/openai-request-rewriter.ts +66 -0
- package/packages/core/src/plugins/builtin-plugin-descriptors.ts +10 -0
- package/packages/core/src/plugins/builtin-plugin-modules.ts +9 -0
- package/packages/core/src/plugins/plugin-api.ts +96 -0
- package/packages/core/src/plugins/plugin-catalog.ts +42 -0
- package/packages/core/src/plugins/plugin-descriptor.ts +51 -0
- package/packages/core/src/plugins/plugin-sdk.ts +47 -0
- package/packages/core/src/plugins/static-pipeline.ts +5 -0
- package/packages/core/src/profiles/profile-router.ts +45 -0
- package/packages/core/src/providers/provider-catalog.ts +186 -0
- package/packages/core/src/routing/distribution.ts +31 -0
- package/packages/core/src/security/secret-redactor.ts +139 -0
- package/packages/core/src/security/target-policy.ts +61 -0
- package/packages/core/src/storage/local-paths.ts +6 -0
- package/packages/core/src/storage/private-state.ts +30 -0
- package/packages/core/src/storage/purge-dir.ts +10 -0
- package/packages/core/src/store/retrieval-store.ts +114 -0
- package/packages/core/src/utils/hash.ts +9 -0
- package/packages/core/src/utils/text.ts +18 -0
- package/packages/core/src/utils/tokens.ts +3 -0
- package/packages/dashboard/dist/assets/index-B_aSPgHx.js +11 -0
- package/packages/dashboard/dist/assets/index-D6z2TEL2.css +1 -0
- package/packages/dashboard/dist/favicon.png +0 -0
- package/packages/dashboard/dist/index.html +15 -0
- package/packages/dashboard/dist/molenkopf-logo.png +0 -0
- package/packages/dashboard/public/favicon.png +0 -0
- package/packages/dashboard/public/molenkopf-logo.png +0 -0
- package/packages/plugins/context-compressor-plugin/descriptor.ts +19 -0
- package/packages/plugins/context-compressor-plugin/page.html +191 -0
- package/packages/plugins/context-compressor-plugin/plugin.ts +40 -0
- package/packages/plugins/obsidian-graph-plugin/descriptor.ts +19 -0
- package/packages/plugins/obsidian-graph-plugin/page.html +68 -0
- package/packages/plugins/obsidian-graph-plugin/plugin.ts +27 -0
- package/packages/plugins/shared/audit-projects.ts +32 -0
- package/packages/proxy/src/cli/args.ts +34 -0
- package/packages/proxy/src/cli/config-loader.ts +43 -0
- package/packages/proxy/src/cli/env-file.ts +43 -0
- package/packages/proxy/src/cli/main.ts +132 -0
- package/packages/proxy/src/cli/profile-server.ts +176 -0
- package/packages/proxy/src/cli/target.ts +7 -0
- package/packages/proxy/src/http/agent-drafts.ts +103 -0
- package/packages/proxy/src/http/agent-router.ts +69 -0
- package/packages/proxy/src/http/audit-view.ts +15 -0
- package/packages/proxy/src/http/auth-state.ts +44 -0
- package/packages/proxy/src/http/budget-gate.ts +45 -0
- package/packages/proxy/src/http/budget-warnings.ts +7 -0
- package/packages/proxy/src/http/cli-stream-response.ts +51 -0
- package/packages/proxy/src/http/client-identity.ts +51 -0
- package/packages/proxy/src/http/communication-graph.ts +139 -0
- package/packages/proxy/src/http/control-plane-guard.ts +56 -0
- package/packages/proxy/src/http/dashboard-assets.ts +115 -0
- package/packages/proxy/src/http/encoded-usage-meter.ts +32 -0
- package/packages/proxy/src/http/header-utils.ts +65 -0
- package/packages/proxy/src/http/identity-id.ts +11 -0
- package/packages/proxy/src/http/local-api-agent-actions.ts +17 -0
- package/packages/proxy/src/http/local-api-auth.ts +120 -0
- package/packages/proxy/src/http/local-api-consumer-actions.ts +20 -0
- package/packages/proxy/src/http/local-api-identity.ts +194 -0
- package/packages/proxy/src/http/local-api-io.ts +82 -0
- package/packages/proxy/src/http/local-api-keys.ts +126 -0
- package/packages/proxy/src/http/local-api-pipeline.ts +41 -0
- package/packages/proxy/src/http/local-api-plugin-actions.ts +31 -0
- package/packages/proxy/src/http/local-api-provider-actions.ts +181 -0
- package/packages/proxy/src/http/local-api-retention.ts +28 -0
- package/packages/proxy/src/http/local-api-runtime-auth.ts +119 -0
- package/packages/proxy/src/http/local-api-scope.ts +47 -0
- package/packages/proxy/src/http/local-api-state.ts +180 -0
- package/packages/proxy/src/http/local-api.ts +166 -0
- package/packages/proxy/src/http/password-policy.ts +5 -0
- package/packages/proxy/src/http/plugin-data.ts +38 -0
- package/packages/proxy/src/http/plugin-host.ts +87 -0
- package/packages/proxy/src/http/plugin-modules.ts +1 -0
- package/packages/proxy/src/http/plugin-page-loader.ts +24 -0
- package/packages/proxy/src/http/plugin-pipeline.ts +125 -0
- package/packages/proxy/src/http/provider-access.ts +33 -0
- package/packages/proxy/src/http/provider-http-test.ts +133 -0
- package/packages/proxy/src/http/provider-input.ts +39 -0
- package/packages/proxy/src/http/provider-routing-snapshot.ts +28 -0
- package/packages/proxy/src/http/provider-test.ts +149 -0
- package/packages/proxy/src/http/proxy-identity.ts +78 -0
- package/packages/proxy/src/http/public-bind.ts +8 -0
- package/packages/proxy/src/http/request-finish.ts +62 -0
- package/packages/proxy/src/http/request-path.ts +8 -0
- package/packages/proxy/src/http/request-policy.ts +46 -0
- package/packages/proxy/src/http/runtime-auth-proof.ts +55 -0
- package/packages/proxy/src/http/runtime-auth-registry.ts +105 -0
- package/packages/proxy/src/http/runtime-settings.ts +199 -0
- package/packages/proxy/src/http/runtime-state.ts +198 -0
- package/packages/proxy/src/http/server-io.ts +80 -0
- package/packages/proxy/src/http/server-types.ts +17 -0
- package/packages/proxy/src/http/server.ts +190 -0
- package/packages/proxy/src/http/session-secret.ts +19 -0
- package/packages/proxy/src/http/streaming-proxy.ts +88 -0
- package/packages/proxy/src/http/usage-accounting.ts +100 -0
- package/packages/proxy/src/http/usage-restore.ts +15 -0
- package/packages/proxy/src/runtime/cli-diagnostics.ts +64 -0
- package/packages/proxy/src/runtime/cli-env.ts +22 -0
- package/packages/proxy/src/runtime/cli-executor.ts +134 -0
- package/packages/proxy/src/runtime/cli-provider.ts +162 -0
- package/packages/proxy/src/runtime/cli-request.ts +79 -0
- package/packages/proxy/src/runtime/codex-runtime-config.ts +37 -0
- package/packages/proxy/src/runtime/runtime-profile.ts +170 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Molenkopf Provider ENV Setup
|
|
2
|
+
|
|
3
|
+
This documents environment-based provider setup. The JSON config path for
|
|
4
|
+
providers, agent bindings, plugin policies, and team distribution is
|
|
5
|
+
[MOLENKOPF_JSON_CONFIG_PLAN.md](MOLENKOPF_JSON_CONFIG_PLAN.md).
|
|
6
|
+
|
|
7
|
+
Use this when you want several upstream provider profiles in one local
|
|
8
|
+
Molenkopf instance. Credentials stay in environment variables. Molenkopf only
|
|
9
|
+
stores and displays the variable names.
|
|
10
|
+
|
|
11
|
+
## Do Not Use
|
|
12
|
+
|
|
13
|
+
- Do not paste Claude, ChatGPT, Codex, or browser login/session tokens here.
|
|
14
|
+
- Do not commit a file with real keys.
|
|
15
|
+
- Do not give employees upstream provider keys. Later they get Molenkopf tokens.
|
|
16
|
+
|
|
17
|
+
Use provider API keys for API routing:
|
|
18
|
+
|
|
19
|
+
- OpenAI API key: `OPENAI_API_KEY` or your own named env key.
|
|
20
|
+
- Anthropic/Claude API key: `ANTHROPIC_API_KEY` or your own named env key.
|
|
21
|
+
- Codex CLI / Claude CLI login sessions are supported through JSON config and
|
|
22
|
+
runtime-auth import flows. They are not configured through env provider blocks.
|
|
23
|
+
|
|
24
|
+
## Single Built-In Profiles
|
|
25
|
+
|
|
26
|
+
PowerShell:
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
Copy-Item .env.example .env
|
|
30
|
+
# Edit .env and set MOLENKOPF_SESSION_SECRET.
|
|
31
|
+
$env:OPENAI_API_KEY = "replace-with-openai-api-key"
|
|
32
|
+
$env:ANTHROPIC_API_KEY = "replace-with-anthropic-api-key"
|
|
33
|
+
npm run dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then open:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
http://127.0.0.1:8787/__molenkopf/dashboard
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Built-in provider IDs:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
openai-env
|
|
46
|
+
anthropic-env
|
|
47
|
+
ollama-local
|
|
48
|
+
lmstudio-local
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Multiple Provider Profiles
|
|
52
|
+
|
|
53
|
+
Use `MOLENKOPF_PROVIDER_IDS` plus one block per ID. The ID is converted to an
|
|
54
|
+
env suffix by uppercasing and replacing `-` with `_`.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
$env:MOLENKOPF_PROVIDER_IDS = "openai-main,openai-backup,claude-main,claude-work"
|
|
60
|
+
|
|
61
|
+
$env:MOLENKOPF_PROVIDER_OPENAI_MAIN_NAME = "OpenAI Main"
|
|
62
|
+
$env:MOLENKOPF_PROVIDER_OPENAI_MAIN_TARGET = "https://api.openai.com/v1"
|
|
63
|
+
$env:MOLENKOPF_PROVIDER_OPENAI_MAIN_CREDENTIAL_ENV = "OPENAI_MAIN_API_KEY"
|
|
64
|
+
$env:OPENAI_MAIN_API_KEY = "replace-with-openai-main-api-key"
|
|
65
|
+
|
|
66
|
+
$env:MOLENKOPF_PROVIDER_OPENAI_BACKUP_NAME = "OpenAI Backup"
|
|
67
|
+
$env:MOLENKOPF_PROVIDER_OPENAI_BACKUP_TARGET = "https://api.openai.com/v1"
|
|
68
|
+
$env:MOLENKOPF_PROVIDER_OPENAI_BACKUP_CREDENTIAL_ENV = "OPENAI_BACKUP_API_KEY"
|
|
69
|
+
$env:OPENAI_BACKUP_API_KEY = "replace-with-openai-backup-api-key"
|
|
70
|
+
|
|
71
|
+
$env:MOLENKOPF_PROVIDER_CLAUDE_MAIN_NAME = "Claude Main"
|
|
72
|
+
$env:MOLENKOPF_PROVIDER_CLAUDE_MAIN_TARGET = "https://api.anthropic.com/v1"
|
|
73
|
+
$env:MOLENKOPF_PROVIDER_CLAUDE_MAIN_CREDENTIAL_ENV = "ANTHROPIC_MAIN_API_KEY"
|
|
74
|
+
$env:MOLENKOPF_PROVIDER_CLAUDE_MAIN_AUTH = "x-api-key"
|
|
75
|
+
$env:ANTHROPIC_MAIN_API_KEY = "replace-with-anthropic-main-api-key"
|
|
76
|
+
|
|
77
|
+
$env:MOLENKOPF_PROVIDER_CLAUDE_WORK_NAME = "Claude Work"
|
|
78
|
+
$env:MOLENKOPF_PROVIDER_CLAUDE_WORK_TARGET = "https://api.anthropic.com/v1"
|
|
79
|
+
$env:MOLENKOPF_PROVIDER_CLAUDE_WORK_CREDENTIAL_ENV = "ANTHROPIC_WORK_API_KEY"
|
|
80
|
+
$env:MOLENKOPF_PROVIDER_CLAUDE_WORK_AUTH = "x-api-key"
|
|
81
|
+
$env:ANTHROPIC_WORK_API_KEY = "replace-with-anthropic-work-api-key"
|
|
82
|
+
|
|
83
|
+
npm run dev
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Private Env File
|
|
87
|
+
|
|
88
|
+
`.env` is ignored by git and loaded automatically by source runs. It may contain
|
|
89
|
+
the required session secret and provider environment variables. Replace the
|
|
90
|
+
placeholder session secret before starting Molenkopf:
|
|
91
|
+
|
|
92
|
+
```env
|
|
93
|
+
MOLENKOPF_SESSION_SECRET=replace-with-at-least-32-random-characters
|
|
94
|
+
MOLENKOPF_PROVIDER_IDS=openai-main,claude-main
|
|
95
|
+
MOLENKOPF_PROVIDER_OPENAI_MAIN_NAME=OpenAI Main
|
|
96
|
+
MOLENKOPF_PROVIDER_OPENAI_MAIN_TARGET=https://api.openai.com/v1
|
|
97
|
+
MOLENKOPF_PROVIDER_OPENAI_MAIN_CREDENTIAL_ENV=OPENAI_MAIN_API_KEY
|
|
98
|
+
OPENAI_MAIN_API_KEY=replace-with-openai-main-api-key
|
|
99
|
+
MOLENKOPF_PROVIDER_CLAUDE_MAIN_NAME=Claude Main
|
|
100
|
+
MOLENKOPF_PROVIDER_CLAUDE_MAIN_TARGET=https://api.anthropic.com/v1
|
|
101
|
+
MOLENKOPF_PROVIDER_CLAUDE_MAIN_CREDENTIAL_ENV=ANTHROPIC_MAIN_API_KEY
|
|
102
|
+
MOLENKOPF_PROVIDER_CLAUDE_MAIN_AUTH=x-api-key
|
|
103
|
+
ANTHROPIC_MAIN_API_KEY=replace-with-anthropic-main-api-key
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`--env-file FILE` remains available when you intentionally want a different
|
|
107
|
+
private file. Docker does not automatically read host `.env` files; pass the
|
|
108
|
+
file explicitly with `docker run --env-file .env ...`.
|
|
109
|
+
|
|
110
|
+
## Fields
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
MOLENKOPF_PROVIDER_<ID>_NAME
|
|
114
|
+
MOLENKOPF_PROVIDER_<ID>_TARGET
|
|
115
|
+
MOLENKOPF_PROVIDER_<ID>_CREDENTIAL_ENV
|
|
116
|
+
MOLENKOPF_PROVIDER_<ID>_AUTH=bearer | x-api-key | none
|
|
117
|
+
MOLENKOPF_PROVIDER_<ID>_KIND=api | local
|
|
118
|
+
MOLENKOPF_PROVIDER_<ID>_ENABLED=false
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
When a configured provider is selected, Molenkopf strips incoming client auth
|
|
122
|
+
and injects the configured server-side env credential at the forwarding
|
|
123
|
+
boundary.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Molenkopf Usage
|
|
2
|
+
|
|
3
|
+
This is the practical local test flow for Molenkopf.
|
|
4
|
+
|
|
5
|
+
## Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp .env.example .env
|
|
9
|
+
# Edit .env and set MOLENKOPF_SESSION_SECRET.
|
|
10
|
+
npm run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Source runs load `./.env` automatically without overriding values already
|
|
14
|
+
exported in your shell.
|
|
15
|
+
|
|
16
|
+
Open `http://127.0.0.1:8787/__molenkopf/dashboard`. Use
|
|
17
|
+
`http://127.0.0.1:8787/v1` as the OpenAI-compatible base URL in local clients.
|
|
18
|
+
|
|
19
|
+
## Authenticate Proxy Traffic
|
|
20
|
+
|
|
21
|
+
Create a Molenkopf API key in the dashboard after first-run setup. Every
|
|
22
|
+
`/v1/...` proxy request needs that key. Use `Authorization: Bearer mk_...` when
|
|
23
|
+
Molenkopf supplies provider credentials. If your client also needs to forward an
|
|
24
|
+
upstream `Authorization` header, send the Molenkopf key as `x-molenkopf-token`.
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
Authorization: Bearer mk_...
|
|
28
|
+
x-molenkopf-token: mk_...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`x-molenkopf-agent` is optional local request metadata. It can select only an
|
|
32
|
+
agent/provider binding already allowed by the authenticated key, team, and
|
|
33
|
+
profile policy. Molenkopf strips local Molenkopf auth and routing headers before
|
|
34
|
+
forwarding upstream.
|
|
35
|
+
|
|
36
|
+
## Test Request
|
|
37
|
+
|
|
38
|
+
Send a normal OpenAI-compatible request through the proxy. Then refresh the dashboard.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
MOLENKOPF_API_KEY="mk_..."
|
|
42
|
+
curl http://127.0.0.1:8787/v1/responses \
|
|
43
|
+
-H "x-molenkopf-token: ${MOLENKOPF_API_KEY}" \
|
|
44
|
+
-H "authorization: Bearer ${OPENAI_API_KEY}" \
|
|
45
|
+
-H "content-type: application/json" \
|
|
46
|
+
-d '{"model":"gpt-4.1-mini","input":"short local test"}'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
PowerShell:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
$env:MOLENKOPF_API_KEY = "mk_..."
|
|
53
|
+
curl.exe http://127.0.0.1:8787/v1/responses `
|
|
54
|
+
-H "x-molenkopf-token: $env:MOLENKOPF_API_KEY" `
|
|
55
|
+
-H "authorization: Bearer $env:OPENAI_API_KEY" `
|
|
56
|
+
-H "content-type: application/json" `
|
|
57
|
+
-d '{ "model": "gpt-4.1-mini", "input": "short local test" }'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Expected result:
|
|
61
|
+
|
|
62
|
+
- `/__molenkopf/requests/latest` returns a redacted audit manifest for an
|
|
63
|
+
authenticated admin session.
|
|
64
|
+
- `Dashboard -> Overview` updates after refresh or polling.
|
|
65
|
+
- `Context compression flow` increments request and token counters.
|
|
66
|
+
- `Memory graph workspace` shows text-derived safe graph data after transferred text is observed.
|
|
67
|
+
|
|
68
|
+
## Provider Setup
|
|
69
|
+
|
|
70
|
+
`molenkopf.config.json` is the provider startup source when present or passed
|
|
71
|
+
with `--config`. Provider API keys must be referenced from environment
|
|
72
|
+
variables with `auth.credentialRef`, for example `env:OPENAI_MAIN_API_KEY`.
|
|
73
|
+
Inline credentials are rejected in file config. The setup for ENV-defined
|
|
74
|
+
providers remains in
|
|
75
|
+
[MOLENKOPF_PROVIDER_ENV.md](MOLENKOPF_PROVIDER_ENV.md).
|
|
76
|
+
|
|
77
|
+
Start from the example:
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
Copy-Item molenkopf.config.example.json molenkopf.config.json
|
|
81
|
+
$env:OPENAI_MAIN_API_KEY = "<your OpenAI API key>"
|
|
82
|
+
$env:ANTHROPIC_MAIN_API_KEY = "<your Anthropic API key>"
|
|
83
|
+
molenkopf proxy --config molenkopf.config.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then inspect:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
http://127.0.0.1:8787/__molenkopf/providers
|
|
90
|
+
http://127.0.0.1:8787/__molenkopf/config
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Expected:
|
|
94
|
+
|
|
95
|
+
- only the JSON providers are listed
|
|
96
|
+
- `credentialRef: env:OPENAI_MAIN_API_KEY` and `credentialConfigured: true`
|
|
97
|
+
are visible after the environment variable is set
|
|
98
|
+
- real API key values are loaded from the local environment but not returned by
|
|
99
|
+
Local API
|
|
100
|
+
- no ENV provider blocks are mixed into JSON startup
|
|
101
|
+
|
|
102
|
+
### Local CLI And Local Models
|
|
103
|
+
|
|
104
|
+
Use `kind: "cli-claude"` or `kind: "cli-codex"` for local CLI accounts, and
|
|
105
|
+
`kind: "ollama"` or `kind: "lmstudio"` for local OpenAI-compatible model
|
|
106
|
+
servers. Runtime-auth imports run CLI providers with isolated local auth/profile
|
|
107
|
+
directories. Provider switching is visible in Admin; explicit agent bindings can
|
|
108
|
+
route `x-molenkopf-agent` values to configured providers without bypassing
|
|
109
|
+
team/provider allowlists or API-key scopes.
|
|
110
|
+
|
|
111
|
+
Providers added through the Admin form are runtime state unless represented by
|
|
112
|
+
JSON config, env configuration, or imported runtime-auth metadata.
|
|
113
|
+
|
|
114
|
+
## Users, Teams, Projects, And Keys
|
|
115
|
+
|
|
116
|
+
Molenkopf separates project attribution from team policy:
|
|
117
|
+
|
|
118
|
+
- `Project` is required on every API key. It describes the workload that uses
|
|
119
|
+
the key, for example `project-a`, `billing-batch`, or `local-test`.
|
|
120
|
+
- `Team` is the organizational policy bucket. Teams own provider allowlists,
|
|
121
|
+
budget accounting, and scoped visibility. First-run setup creates the default
|
|
122
|
+
`everyone` team for the first admin.
|
|
123
|
+
- `User` is the login and key owner. Users can belong to one or more teams.
|
|
124
|
+
|
|
125
|
+
Dashboard support: Admin manages users, teams, providers, and system controls;
|
|
126
|
+
Overview lets the signed-in user create and revoke permitted project keys. Raw
|
|
127
|
+
key secrets are shown once only.
|
|
128
|
+
|
|
129
|
+
Local API support:
|
|
130
|
+
|
|
131
|
+
| Action | Endpoint |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| List users and teams | `GET /__molenkopf/identity` |
|
|
134
|
+
| Create/update user | `POST /__molenkopf/identity/users` |
|
|
135
|
+
| Remove user | `POST /__molenkopf/identity/users/remove` |
|
|
136
|
+
| Create/update team | `POST /__molenkopf/identity/teams` |
|
|
137
|
+
| Remove team | `POST /__molenkopf/identity/teams/remove` |
|
|
138
|
+
| Create/list/revoke keys | `/__molenkopf/keys` and `/__molenkopf/keys/revoke` |
|
|
139
|
+
|
|
140
|
+
If a user belongs to multiple teams, key creation must choose the team. If a
|
|
141
|
+
user belongs to one team, Molenkopf can use that team automatically.
|
|
142
|
+
|
|
143
|
+
## Troubleshooting
|
|
144
|
+
|
|
145
|
+
- Dashboard stuck on `connecting`: restart the proxy so the current dashboard bundle is served.
|
|
146
|
+
- `EADDRINUSE`: another proxy is already listening on the port; stop that process or start on another port.
|
|
147
|
+
- `invalid_api_key`: create a Molenkopf project key in the dashboard and send it
|
|
148
|
+
as `Authorization: Bearer mk_...` or `x-molenkopf-token: mk_...`.
|
|
149
|
+
- `OPENAI_API_KEY missing`: set the provider environment variable or forward an
|
|
150
|
+
upstream `Authorization` header with `x-molenkopf-token` for Molenkopf auth.
|
|
151
|
+
- No graph nodes: send API traffic through `/v1/...`; internal dashboard requests are intentionally ignored.
|
|
152
|
+
- Saved tokens stay `0`: the payload may be small or the compressor is disabled.
|
|
153
|
+
- Imported Claude auth works but write/edit prompts still appear: that is the outer Claude harness permission profile, not the imported runtime auth. Track and fix this separately from provider auth.
|
|
154
|
+
- Claude/Codex no-response: use the provider card `Test` action first. Molenkopf reports spawned CLI lifecycle, timeout, malformed output, and permission-prompt classes for the child process it owns.
|
|
155
|
+
|
|
156
|
+
## Current Dashboard Views
|
|
157
|
+
|
|
158
|
+
- `Overview`: connection status, signed-in user usage, teams, members, project
|
|
159
|
+
keys, and client connection snippets.
|
|
160
|
+
- `Admin`: users, teams, provider accounts, routing, plugin controls, imported runtime auth, and workspace links.
|
|
161
|
+
|
|
162
|
+
Dedicated Providers, Plugins, Requests, Audit, Agents, and Settings views are
|
|
163
|
+
planned in `ROADMAP.md`. Until then, use the local APIs and plugin pages below
|
|
164
|
+
for request/audit/plugin details.
|
|
165
|
+
|
|
166
|
+
## Plugin Pages And Data
|
|
167
|
+
|
|
168
|
+
Open plugin pages from the Admin plugin section. Context compression owns token
|
|
169
|
+
pressure and savings views. The graph page is fed from redacted transferred text
|
|
170
|
+
and safe request metadata; it does not read an Obsidian vault yet and does not
|
|
171
|
+
render raw prompt or response content.
|
|
172
|
+
|
|
173
|
+
## Current Boundaries
|
|
174
|
+
|
|
175
|
+
- No full prompts or full responses are displayed.
|
|
176
|
+
- File config rejects inline raw API credentials; use `auth.credentialRef`.
|
|
177
|
+
Provider credentials entered in the dashboard and imported runtime auth are
|
|
178
|
+
intentional local operator state, and Local API responses do not display
|
|
179
|
+
credential values.
|
|
180
|
+
- Token hash drafts store hash metadata only; list responses show only that a hash exists plus a short fingerprint.
|
|
181
|
+
- Provider routing is explicit: teams carry provider allowlists, API keys carry
|
|
182
|
+
project attribution, and provider selection can be manual or distributed by
|
|
183
|
+
configured profile.
|
|
184
|
+
- Plugin toggle and order state persists in local runtime settings. Locked
|
|
185
|
+
plugin settings are normalized back to safe defaults on startup.
|
|
186
|
+
|
|
187
|
+
## Verification
|
|
188
|
+
|
|
189
|
+
Run:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npm test
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The relevant checks cover dashboard rendering, script parsing, provider/plugin controls, agent draft safety, audit summaries, plugin pages, and proxy behavior.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Molenkopf Product Intent
|
|
2
|
+
|
|
3
|
+
Molenkopf is a local agent gateway for API and CLI based coding agents. The
|
|
4
|
+
product must connect OpenAI/Codex and Anthropic/Claude traffic into one local
|
|
5
|
+
endpoint and one local memory layer.
|
|
6
|
+
|
|
7
|
+
## Target
|
|
8
|
+
|
|
9
|
+
- Accept agent traffic from OpenAI-compatible APIs, Anthropic/Claude-compatible APIs, Codex CLI, and Claude CLI.
|
|
10
|
+
- Let CLI agents run locally while their prompt/context stream is still observable through the gateway.
|
|
11
|
+
- Reduce the real text/context that is sent upstream or into a CLI runtime.
|
|
12
|
+
- Show live token savings only from real transferred payloads, not placeholder counters.
|
|
13
|
+
- Store safe derived memory from transferred text so later turns can reuse it.
|
|
14
|
+
- Build an Obsidian-style graph from the transferred text and derived memory, not from generic HTTP metadata.
|
|
15
|
+
|
|
16
|
+
## Plugin Meaning
|
|
17
|
+
|
|
18
|
+
Plugins are local capabilities in the agent path, not decorative dashboard pages.
|
|
19
|
+
Plugins are middleware, but not all middleware can mutate traffic. Mutation
|
|
20
|
+
rights are explicit plugin descriptor fields and are enforced by the proxy.
|
|
21
|
+
|
|
22
|
+
- Context compression plugin: owns token reduction, before/after accounting, saved-token totals, skip reasons, and retrieval IDs for real context chunks.
|
|
23
|
+
- Memory/Obsidian plugin: owns the derived text memory graph, semantic nodes, links, and local store updates from real agent text.
|
|
24
|
+
- Provider/runtime plugins: adapt OpenAI API, Anthropic API, Codex CLI, Claude CLI, and later MCP into the same run/event contract.
|
|
25
|
+
- Core safety pipeline: redacts secrets and prevents raw credentials, full prompts, and full responses from leaking into UI/logs. This is not optional plugin behavior.
|
|
26
|
+
|
|
27
|
+
## UI Rules
|
|
28
|
+
|
|
29
|
+
- Empty state must say no payloads observed yet. It must not show fake token pressure.
|
|
30
|
+
- No unauthenticated proxy traffic graph as a product target. If attribution is
|
|
31
|
+
unknown, show it only as a routing/accounting warning.
|
|
32
|
+
- Compression views must answer: what text entered, what was removed or represented by ID, what was sent, and how many tokens were saved.
|
|
33
|
+
- Graph views must answer: what concepts/entities/threads were learned from transferred text and how they relate.
|
|
34
|
+
- HTTP metadata graphs are diagnostics only, not the Obsidian workspace goal.
|
|
35
|
+
|
|
36
|
+
Molenkopf should adapt that pattern at the gateway/runtime boundary instead of pretending all agents are API-key-only HTTP clients.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Molenkopf Threat Model
|
|
2
|
+
|
|
3
|
+
Date: 2026-06-23
|
|
4
|
+
|
|
5
|
+
This model describes the public Molenkopf product target and calls out
|
|
6
|
+
unfinished hardening explicitly.
|
|
7
|
+
|
|
8
|
+
## Assets
|
|
9
|
+
|
|
10
|
+
- Upstream provider credentials.
|
|
11
|
+
- Molenkopf employee and agent tokens.
|
|
12
|
+
- Raw prompts and raw responses.
|
|
13
|
+
- Retrieval originals and derived summaries.
|
|
14
|
+
- Audit manifests and token accounting.
|
|
15
|
+
- Provider selection, plugin toggles, and memory export controls.
|
|
16
|
+
|
|
17
|
+
## Attackers
|
|
18
|
+
|
|
19
|
+
- Local untrusted process on the same machine.
|
|
20
|
+
- Malicious local webpage that can reach loopback endpoints.
|
|
21
|
+
- Another employee on the LAN if public bind is enabled.
|
|
22
|
+
- Compromised plugin or dashboard payload.
|
|
23
|
+
- Over-privileged internal agent token.
|
|
24
|
+
- Accidental operator mistake, such as sending secrets in query strings.
|
|
25
|
+
|
|
26
|
+
## Boundaries
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
employee client
|
|
30
|
+
-> Molenkopf auth boundary
|
|
31
|
+
-> policy and plugin boundary
|
|
32
|
+
-> provider credential boundary
|
|
33
|
+
-> upstream provider
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
safe transferred text
|
|
38
|
+
-> redaction
|
|
39
|
+
-> compression
|
|
40
|
+
-> retrieval or memory store
|
|
41
|
+
-> audit/dashboard/exports
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Hardened Release Invariants
|
|
45
|
+
|
|
46
|
+
These are target invariants for the team gateway. Remaining hardening work is
|
|
47
|
+
tracked through the roadmap, GitHub issues, and release PRs.
|
|
48
|
+
|
|
49
|
+
- No provider credential values in audit, dashboard, events, docs, or plugin data.
|
|
50
|
+
- No full prompt or full response in audit, dashboard, events, or plugin pages.
|
|
51
|
+
- Query strings are stripped or redacted before audit persistence.
|
|
52
|
+
- First-run open mode exposes only health, current-session status, and
|
|
53
|
+
first-run admin creation.
|
|
54
|
+
- `__molenkopf/*` control APIs require auth after setup; provider, plugin,
|
|
55
|
+
routing, agent, stats, event, config metadata, and retention endpoints are
|
|
56
|
+
admin-only.
|
|
57
|
+
- `/v1/*` proxy traffic requires a valid Molenkopf API key.
|
|
58
|
+
- Non-loopback source binds require an explicit `--allow-public-bind` opt-in.
|
|
59
|
+
- Molenkopf tokens are stored as hashes and shown once.
|
|
60
|
+
- Retrieval writes happen only after a real compression artifact is committed.
|
|
61
|
+
- Obsidian writes require dry-run and path guards before apply.
|
|
62
|
+
- Multi-account routing uses explicit provider profiles only.
|
|
63
|
+
|
|
64
|
+
## Implemented Plugin And Core Safety Invariants
|
|
65
|
+
|
|
66
|
+
- Plugins are optional extensions; core safety, audit, event, storage, and
|
|
67
|
+
routing code is not exposed as a plugin and is not user-toggleable.
|
|
68
|
+
- Plugin descriptors declare type, read scopes, mutation scopes, toggle policy,
|
|
69
|
+
and workspace data scopes.
|
|
70
|
+
- The request pipeline restores unauthorized body, route, and block mutations
|
|
71
|
+
and fails closed with `plugin_capability_violation`.
|
|
72
|
+
- Remote plugin loading is disabled.
|
|
73
|
+
- Core redaction runs before optional plugin middleware.
|
|
74
|
+
|
|
75
|
+
## Storage Policy
|
|
76
|
+
|
|
77
|
+
- Audit manifests are metadata-only. Manual purge exists; retention policy,
|
|
78
|
+
quotas, pagination, and project scope are planned.
|
|
79
|
+
- Retrieval originals are local sensitive artifacts. Manual purge exists;
|
|
80
|
+
retention policy, quotas, and project scope are planned.
|
|
81
|
+
- Memory currently stores a bounded in-memory derived graph. Persistent memory,
|
|
82
|
+
source refs, and retention policy are planned.
|
|
83
|
+
- Dashboard state must not persist raw tokens.
|
|
84
|
+
|
|
85
|
+
## Required Tests
|
|
86
|
+
|
|
87
|
+
- Query secret does not appear in `/requests`, latest audit, summaries, SSE, dashboard, or plugin pages.
|
|
88
|
+
- Nested JSON secrets are redacted before compression and audit.
|
|
89
|
+
- `x-molenkopf-token` and local routing headers never reach upstream.
|
|
90
|
+
- Unauthenticated control APIs return `401`; insufficient scopes return `403`.
|
|
91
|
+
- Non-loopback bind without explicit opt-in fails at startup.
|
|
92
|
+
- Revoked/expired tokens cannot call `/v1/*` or control APIs.
|
|
93
|
+
- Retrieval no-op compression leaves no stored original.
|
|
94
|
+
- Obsidian apply cannot write outside the selected vault root.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"server": {
|
|
4
|
+
"bindHost": "127.0.0.1",
|
|
5
|
+
"port": 8787,
|
|
6
|
+
"dataDir": ".molenkopf"
|
|
7
|
+
},
|
|
8
|
+
"providers": [
|
|
9
|
+
{
|
|
10
|
+
"id": "openai-main",
|
|
11
|
+
"name": "OpenAI Main",
|
|
12
|
+
"kind": "openai-compatible",
|
|
13
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
14
|
+
"auth": {
|
|
15
|
+
"scheme": "bearer",
|
|
16
|
+
"credentialRef": "env:OPENAI_MAIN_API_KEY"
|
|
17
|
+
},
|
|
18
|
+
"enabled": true
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "claude-main",
|
|
22
|
+
"name": "Claude Main",
|
|
23
|
+
"kind": "anthropic",
|
|
24
|
+
"baseUrl": "https://api.anthropic.com/v1",
|
|
25
|
+
"auth": {
|
|
26
|
+
"scheme": "x-api-key",
|
|
27
|
+
"credentialRef": "env:ANTHROPIC_MAIN_API_KEY"
|
|
28
|
+
},
|
|
29
|
+
"enabled": true
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "claude-local",
|
|
33
|
+
"name": "Claude Local CLI",
|
|
34
|
+
"kind": "cli-claude",
|
|
35
|
+
"command": "claude",
|
|
36
|
+
"args": ["--print"],
|
|
37
|
+
"inputMode": "stdin",
|
|
38
|
+
"timeoutMs": 120000,
|
|
39
|
+
"enabled": false
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"profiles": [
|
|
43
|
+
{
|
|
44
|
+
"id": "default-local",
|
|
45
|
+
"providerId": "openai-main",
|
|
46
|
+
"allowedModels": ["gpt-4.1-mini"],
|
|
47
|
+
"defaultModel": "gpt-4.1-mini"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"pluginPolicies": [
|
|
51
|
+
{
|
|
52
|
+
"id": "standard-policy",
|
|
53
|
+
"enabledPluginIds": ["context-compressor-plugin"],
|
|
54
|
+
"remotePlugins": false
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"agents": [
|
|
58
|
+
{
|
|
59
|
+
"id": "local-codex",
|
|
60
|
+
"ownerId": "local",
|
|
61
|
+
"kind": "local-agent",
|
|
62
|
+
"profileId": "default-local",
|
|
63
|
+
"pluginPolicyId": "standard-policy",
|
|
64
|
+
"scopes": ["proxy:use"],
|
|
65
|
+
"enabled": true
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bothat-io/molenkopf",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Local gateway for API and CLI based coding agents with routing, redaction, compression, memory, audit, and dashboard controls.",
|
|
5
|
+
"author": "Molenkopf contributors",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"molenkopf": "bin/molenkopf.js"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/bothat-io/molenkopf.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/bothat-io/molenkopf/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/bothat-io/molenkopf#readme",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"molenkopf",
|
|
25
|
+
"local-ai-gateway",
|
|
26
|
+
"coding-agents",
|
|
27
|
+
"proxy",
|
|
28
|
+
"redaction",
|
|
29
|
+
"audit"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
".env.example",
|
|
36
|
+
"bin/",
|
|
37
|
+
"packages/core/src/",
|
|
38
|
+
"packages/proxy/src/",
|
|
39
|
+
"packages/plugins/context-compressor-plugin/descriptor.ts",
|
|
40
|
+
"packages/plugins/context-compressor-plugin/plugin.ts",
|
|
41
|
+
"packages/plugins/context-compressor-plugin/page.html",
|
|
42
|
+
"packages/plugins/obsidian-graph-plugin/descriptor.ts",
|
|
43
|
+
"packages/plugins/obsidian-graph-plugin/plugin.ts",
|
|
44
|
+
"packages/plugins/obsidian-graph-plugin/page.html",
|
|
45
|
+
"packages/plugins/shared/audit-projects.ts",
|
|
46
|
+
"packages/dashboard/dist/",
|
|
47
|
+
"packages/dashboard/public/molenkopf-logo.png",
|
|
48
|
+
"packages/dashboard/public/favicon.png",
|
|
49
|
+
"docs/DEPLOYMENT.md",
|
|
50
|
+
"docs/MOLENKOPF_PLUGIN_API.md",
|
|
51
|
+
"docs/MOLENKOPF_PROVIDER_ENV.md",
|
|
52
|
+
"docs/MOLENKOPF_USAGE.md",
|
|
53
|
+
"docs/PRODUCT_INTENT.md",
|
|
54
|
+
"docs/THREAT_MODEL.md",
|
|
55
|
+
"molenkopf.config.example.json",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"SECURITY.md"
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"bootstrap": "npm ci && npm --prefix packages/dashboard ci",
|
|
62
|
+
"typecheck": "npm run typecheck:node && npm --prefix packages/dashboard run typecheck",
|
|
63
|
+
"typecheck:node": "tsc -p tsconfig.node.json",
|
|
64
|
+
"test": "npm run test:unit",
|
|
65
|
+
"test:unit": "npm run check:docs && npm run check:dashboard-install && npm run check:reachability && npm run check:source-completeness && npm run check:container-contract && npm run check:line-limits && npm run typecheck && npm --prefix packages/dashboard run build && npm run check:package && npm run test:scripts && npm run test:launcher && npm run vitest && npm run test:node",
|
|
66
|
+
"check:docs": "node scripts/check-doc-paths.js",
|
|
67
|
+
"check:dashboard-install": "node scripts/check-dashboard-install.js",
|
|
68
|
+
"check:reachability": "node scripts/check-reachability.js",
|
|
69
|
+
"check:source-completeness": "node scripts/check-source-completeness.js",
|
|
70
|
+
"check:container-contract": "node scripts/check-container-contract.js",
|
|
71
|
+
"check:package": "node scripts/check-package-contents.js",
|
|
72
|
+
"check:line-limits": "node scripts/check-line-limits.js",
|
|
73
|
+
"test:scripts": "node --test scripts/*.test.js",
|
|
74
|
+
"test:launcher": "node --test test/*.test.js",
|
|
75
|
+
"test:node": "node --experimental-strip-types --experimental-sqlite --disable-warning=ExperimentalWarning --import ./packages/proxy/test/setup.ts --test --test-concurrency=1 packages/core/test/*.test.ts packages/proxy/test/*.test.ts packages/dashboard/test/*.test.ts",
|
|
76
|
+
"test:all": "npm run test:unit && npm run e2e",
|
|
77
|
+
"vitest": "npm --prefix packages/dashboard run test",
|
|
78
|
+
"e2e": "npm --prefix packages/dashboard run e2e",
|
|
79
|
+
"prepack": "npm --prefix packages/dashboard run build && npm run check:package",
|
|
80
|
+
"pack:dry": "npm pack --dry-run",
|
|
81
|
+
"smoke:installed": "node scripts/smoke-installed-package.js",
|
|
82
|
+
"smoke:docker": "node scripts/smoke-docker.js",
|
|
83
|
+
"release:npm:check": "node scripts/check-npm-release.js",
|
|
84
|
+
"release:verify": "npm run test:unit && npm run e2e && npm run smoke:installed && npm run smoke:docker",
|
|
85
|
+
"serve:dev": "node --experimental-strip-types --experimental-sqlite --disable-warning=ExperimentalWarning packages/proxy/src/cli/profile-server.ts dev",
|
|
86
|
+
"serve:test": "node --experimental-strip-types --experimental-sqlite --disable-warning=ExperimentalWarning packages/proxy/src/cli/profile-server.ts test",
|
|
87
|
+
"serve:prod": "npm --prefix packages/dashboard run build && node --experimental-strip-types --experimental-sqlite --disable-warning=ExperimentalWarning packages/proxy/src/cli/profile-server.ts prod",
|
|
88
|
+
"dev": "npm run serve:dev",
|
|
89
|
+
"prod": "npm run serve:prod"
|
|
90
|
+
},
|
|
91
|
+
"directories": {
|
|
92
|
+
"doc": "docs"
|
|
93
|
+
},
|
|
94
|
+
"devDependencies": {
|
|
95
|
+
"@types/node": "^24.0.0",
|
|
96
|
+
"typescript": "^5.9.0"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomBytes, scrypt, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
// Password hashing with scrypt (Node built-in). We store salt + hash only, never
|
|
5
|
+
// the raw password. Used for admin/user login.
|
|
6
|
+
|
|
7
|
+
const scryptAsync = promisify(scrypt);
|
|
8
|
+
export const MAX_PASSWORD_BYTES = 4096;
|
|
9
|
+
export type PasswordHash = { salt: string; hash: string; version?: 1; keyLength?: number };
|
|
10
|
+
|
|
11
|
+
export function hashPassword(password: string): PasswordHash {
|
|
12
|
+
assertPasswordSize(password);
|
|
13
|
+
const salt = randomBytes(16).toString("hex");
|
|
14
|
+
const hash = scryptSync(password, salt, 64).toString("hex");
|
|
15
|
+
return { salt, hash, version: 1, keyLength: 64 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function hashPasswordAsync(password: string): Promise<PasswordHash> {
|
|
19
|
+
assertPasswordSize(password);
|
|
20
|
+
const salt = randomBytes(16).toString("hex");
|
|
21
|
+
const keyLength = 64;
|
|
22
|
+
const hash = (await scryptAsync(password, salt, keyLength) as Buffer).toString("hex");
|
|
23
|
+
return { salt, hash, version: 1, keyLength };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function verifyPassword(password: string, stored: PasswordHash | undefined): boolean {
|
|
27
|
+
if (!stored?.salt || !stored.hash) return false;
|
|
28
|
+
if (passwordTooLong(password)) return false;
|
|
29
|
+
const candidate = scryptSync(password, stored.salt, stored.keyLength ?? 64);
|
|
30
|
+
const expected = Buffer.from(stored.hash, "hex");
|
|
31
|
+
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function verifyPasswordAsync(password: string, stored: PasswordHash | undefined): Promise<boolean> {
|
|
35
|
+
if (!stored?.salt || !stored.hash || passwordTooLong(password)) return false;
|
|
36
|
+
const candidate = await scryptAsync(password, stored.salt, stored.keyLength ?? 64) as Buffer;
|
|
37
|
+
const expected = Buffer.from(stored.hash, "hex");
|
|
38
|
+
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function passwordTooLong(password: string): boolean {
|
|
42
|
+
return Buffer.byteLength(password, "utf8") > MAX_PASSWORD_BYTES;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertPasswordSize(password: string): void {
|
|
46
|
+
if (passwordTooLong(password)) throw new Error("password_too_long");
|
|
47
|
+
}
|