@blacksandscyber/mcp-server-bursar 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +230 -0
  2. package/build/config.d.ts +45 -0
  3. package/build/config.js +177 -0
  4. package/build/http-transport.d.ts +16 -0
  5. package/build/http-transport.js +191 -0
  6. package/build/index.d.ts +16 -0
  7. package/build/index.js +31 -0
  8. package/build/server.d.ts +41 -0
  9. package/build/server.js +902 -0
  10. package/build/shared/errors.d.ts +50 -0
  11. package/build/shared/errors.js +69 -0
  12. package/build/shared/linkBuilder.d.ts +93 -0
  13. package/build/shared/linkBuilder.js +148 -0
  14. package/build/shared/logger.d.ts +10 -0
  15. package/build/shared/logger.js +28 -0
  16. package/build/shield/bootRole.d.ts +60 -0
  17. package/build/shield/bootRole.js +145 -0
  18. package/build/shield/client.d.ts +265 -0
  19. package/build/shield/client.js +656 -0
  20. package/build/shield/deploy/index.d.ts +69 -0
  21. package/build/shield/deploy/index.js +569 -0
  22. package/build/shield/discovery/dataStoreDetector.d.ts +3 -0
  23. package/build/shield/discovery/dataStoreDetector.js +125 -0
  24. package/build/shield/discovery/dockerScanner.d.ts +34 -0
  25. package/build/shield/discovery/dockerScanner.js +543 -0
  26. package/build/shield/discovery/endpointScanner.d.ts +3 -0
  27. package/build/shield/discovery/endpointScanner.js +306 -0
  28. package/build/shield/discovery/environmentScanner.d.ts +86 -0
  29. package/build/shield/discovery/environmentScanner.js +545 -0
  30. package/build/shield/discovery/externalServiceDetector.d.ts +3 -0
  31. package/build/shield/discovery/externalServiceDetector.js +98 -0
  32. package/build/shield/discovery/frameworkDetector.d.ts +3 -0
  33. package/build/shield/discovery/frameworkDetector.js +114 -0
  34. package/build/shield/discovery/manifestGenerator.d.ts +12 -0
  35. package/build/shield/discovery/manifestGenerator.js +124 -0
  36. package/build/shield/discovery/piiDetector.d.ts +5 -0
  37. package/build/shield/discovery/piiDetector.js +203 -0
  38. package/build/shield/discovery/severity.d.ts +47 -0
  39. package/build/shield/discovery/severity.js +138 -0
  40. package/build/shield/discovery/topologyNormalizer.d.ts +109 -0
  41. package/build/shield/discovery/topologyNormalizer.js +416 -0
  42. package/build/shield/identity.d.ts +53 -0
  43. package/build/shield/identity.js +70 -0
  44. package/build/shield/install/configMerge.d.ts +91 -0
  45. package/build/shield/install/configMerge.js +324 -0
  46. package/build/shield/install/keystore.d.ts +25 -0
  47. package/build/shield/install/keystore.js +156 -0
  48. package/build/shield/install/orchestrator.d.ts +33 -0
  49. package/build/shield/install/orchestrator.js +404 -0
  50. package/build/shield/install/transports/awsSsm.d.ts +43 -0
  51. package/build/shield/install/transports/awsSsm.js +378 -0
  52. package/build/shield/install/transports/bootstrapToken.d.ts +39 -0
  53. package/build/shield/install/transports/bootstrapToken.js +117 -0
  54. package/build/shield/install/transports/ssh.d.ts +50 -0
  55. package/build/shield/install/transports/ssh.js +569 -0
  56. package/build/shield/install/types.d.ts +139 -0
  57. package/build/shield/install/types.js +10 -0
  58. package/build/shield/protocol-walkthrough.d.ts +65 -0
  59. package/build/shield/protocol-walkthrough.js +392 -0
  60. package/build/shield/provision/appProvisioner.d.ts +15 -0
  61. package/build/shield/provision/appProvisioner.js +25 -0
  62. package/build/shield/types.d.ts +261 -0
  63. package/build/shield/types.js +4 -0
  64. package/build/shield/verify/postureReporter.d.ts +4 -0
  65. package/build/shield/verify/postureReporter.js +31 -0
  66. package/dxt/blacksands-ca.crt +67 -0
  67. package/dxt/scripts/setup.js +520 -0
  68. package/package.json +76 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Deployment guides for non-coder "bring-your-own-host" protection flow.
3
+ *
4
+ * Shipped as part of the [FREE] tool surface — no Shield account needed to
5
+ * read these. Claude can walk a novice user through setting up a single
6
+ * DigitalOcean Droplet running both their app and a hardened Blacksands
7
+ * Receiver that proxies traffic through Shield's zero-trust controls.
8
+ *
9
+ * New deploy targets (Railway, Render, Fly, EC2) can be added by:
10
+ * 1. Extending the DeployTarget union
11
+ * 2. Implementing a guide function in this file
12
+ * 3. Dispatching from getDeploymentGuide()
13
+ */
14
+ export type DeployTarget = "digitalocean-droplet" | "local-docker-development";
15
+ /** Architecture-overview shape — returned when no target is specified. */
16
+ export interface DeploymentArchitectureOverview {
17
+ kind: "architecture-overview";
18
+ authoritativeNote: string;
19
+ blacksandsArchitecture: {
20
+ receiverIsPublicIpEdgeProxy: string;
21
+ inbound: string;
22
+ outbound: string;
23
+ controlPlaneChannels: string;
24
+ falseModelsToReject: string[];
25
+ };
26
+ pickATarget: Array<{
27
+ target: DeployTarget;
28
+ description: string;
29
+ recommended: string;
30
+ }>;
31
+ }
32
+ export interface DeploymentGuide {
33
+ kind: "guide";
34
+ target: DeployTarget;
35
+ summary: string;
36
+ estimatedCost: string;
37
+ estimatedTimeMinutes: number;
38
+ prerequisites: string[];
39
+ walkthroughMarkdown: string;
40
+ dockerCompose: string;
41
+ verificationSteps: string[];
42
+ securityControls: Array<{
43
+ control: string;
44
+ effect: string;
45
+ }>;
46
+ honestTamperingNote: string;
47
+ }
48
+ export interface DeploymentGuideOpts {
49
+ /** Docker image tag for the user's app (e.g. "myapp:latest"). Defaults to a placeholder. */
50
+ appImage?: string;
51
+ /** Port the user's app listens on inside its container. Default: 8080. */
52
+ appPort?: number;
53
+ /** Short slug for the app — used in container/host names. Default: "my-app". */
54
+ appName?: string;
55
+ /** Receiver setup token from receiver_initialize. Can be omitted and pasted later. */
56
+ receiverSetupToken?: string;
57
+ /** DigitalOcean region slug (e.g. "nyc3", "sfo3"). Default: "nyc3". */
58
+ region?: string;
59
+ /** DigitalOcean droplet size slug (e.g. "s-1vcpu-2gb"). Default: "s-1vcpu-2gb". */
60
+ dropletSize?: string;
61
+ }
62
+ /**
63
+ * The single entry point. Pass a target to get a target-specific walkthrough.
64
+ * Pass undefined / no target to get an architecture overview that any caller
65
+ * MUST read before producing their own walkthrough — this prevents LLMs from
66
+ * confidently improvising wrong deployment models from training data.
67
+ */
68
+ export declare function getDeploymentGuide(target: DeployTarget | undefined | null, opts?: DeploymentGuideOpts): DeploymentGuide | DeploymentArchitectureOverview;
69
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,569 @@
1
+ "use strict";
2
+ /**
3
+ * Deployment guides for non-coder "bring-your-own-host" protection flow.
4
+ *
5
+ * Shipped as part of the [FREE] tool surface — no Shield account needed to
6
+ * read these. Claude can walk a novice user through setting up a single
7
+ * DigitalOcean Droplet running both their app and a hardened Blacksands
8
+ * Receiver that proxies traffic through Shield's zero-trust controls.
9
+ *
10
+ * New deploy targets (Railway, Render, Fly, EC2) can be added by:
11
+ * 1. Extending the DeployTarget union
12
+ * 2. Implementing a guide function in this file
13
+ * 3. Dispatching from getDeploymentGuide()
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.getDeploymentGuide = getDeploymentGuide;
17
+ /**
18
+ * The single entry point. Pass a target to get a target-specific walkthrough.
19
+ * Pass undefined / no target to get an architecture overview that any caller
20
+ * MUST read before producing their own walkthrough — this prevents LLMs from
21
+ * confidently improvising wrong deployment models from training data.
22
+ */
23
+ function getDeploymentGuide(target, opts = {}) {
24
+ if (!target) {
25
+ return buildArchitectureOverview();
26
+ }
27
+ switch (target) {
28
+ case "digitalocean-droplet":
29
+ return buildDigitalOceanDropletGuide(opts);
30
+ case "local-docker-development":
31
+ return buildLocalDockerDevelopmentGuide(opts);
32
+ default:
33
+ throw new Error(`Unsupported deployment target: ${target}`);
34
+ }
35
+ }
36
+ // ──────────────────────────────────────────────────────────────────────────
37
+ // Architecture overview — returned when target is unspecified.
38
+ // ──────────────────────────────────────────────────────────────────────────
39
+ function buildArchitectureOverview() {
40
+ return {
41
+ kind: "architecture-overview",
42
+ authoritativeNote: "This is the AUTHORITATIVE description of how a Blacksands Bursar deployment is shaped. " +
43
+ "Do NOT answer Blacksands deployment questions from general zero-trust knowledge — " +
44
+ "Blacksands does not match the Cloudflare-Tunnel / Tailscale-Funnel / Twingate model. " +
45
+ "Read this overview, then call bursar_guide_deployment again with a specific target " +
46
+ "for copy-paste steps.",
47
+ blacksandsArchitecture: {
48
+ receiverIsPublicIpEdgeProxy: "The Blacksands Receiver is a PUBLIC-IP EDGE PROXY. It is NOT an outbound " +
49
+ "tunneling client. It runs on a host with a stable public IP and accepts " +
50
+ "incoming TLS connections directly from the internet on TCP 443.",
51
+ inbound: "TCP 443 ONLY, mTLS mandatory. The Receiver terminates TLS and independently " +
52
+ "verifies the client certificate chain against the org CA on every new connection. " +
53
+ "There is no plaintext HTTP path; port 80 is not exposed.",
54
+ outbound: "TLS to the Blacksands Kafka cluster. The Receiver consumes service_authorization " +
55
+ "messages from the Manager via Kafka — that is its only control-plane channel.",
56
+ controlPlaneChannels: "Receiver ↔ Manager via Kafka (TLS). The Receiver does NOT speak HTTP to the " +
57
+ "Authorizer; the Authorizer is the MCP-client side of the system. MCP clients " +
58
+ "(Claude Desktop) talk to the Authorizer to discover the Receiver's URL.",
59
+ falseModelsToReject: [
60
+ "WRONG: 'The Receiver dials out to the Blacksands control plane' — no, it is a public-IP proxy.",
61
+ "WRONG: 'No inbound ports needed' — inbound TCP 443 is required.",
62
+ "WRONG: 'Like Cloudflare Tunnel / Tailscale Funnel / ngrok' — different model entirely.",
63
+ "WRONG: 'Receiver authenticates to the Authorizer' — Receiver only consumes Kafka from the Manager.",
64
+ "WRONG: 'No public IP needed' — Receivers REQUIRE a stable public IP for SNI routing.",
65
+ "WRONG: 'Plaintext HTTP supported' — TLS+mTLS only.",
66
+ ],
67
+ },
68
+ pickATarget: [
69
+ {
70
+ target: "digitalocean-droplet",
71
+ description: "Single-VPS production-ready bundle. App + hardened Receiver in one docker-compose " +
72
+ "on a DigitalOcean Droplet (~$6/mo). ~20 minutes to a protected endpoint. " +
73
+ "Recommended for non-coders and for the first end-to-end test of Blacksands.",
74
+ recommended: "Start here unless you have a specific reason to do something else.",
75
+ },
76
+ {
77
+ target: "local-docker-development",
78
+ description: "DEV ONLY: a docker-compose for running your app locally on a laptop, WITHOUT a real " +
79
+ "Receiver. Use ngrok or cloudflared as a temporary public-IP shim if you want to " +
80
+ "test against a remote Receiver. Not suitable for production — laptops do not have " +
81
+ "the stable public IP a Blacksands Receiver requires.",
82
+ recommended: "Only for local iteration on the app itself. Move to digitalocean-droplet (or another " +
83
+ "VPS target) before any real traffic.",
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ // ──────────────────────────────────────────────────────────────────────────
89
+ // DigitalOcean single-VPS bundle
90
+ // ──────────────────────────────────────────────────────────────────────────
91
+ function buildDigitalOceanDropletGuide(opts) {
92
+ const appImage = opts.appImage || "YOUR_APP_IMAGE:tag";
93
+ const appPort = opts.appPort || 8080;
94
+ const appName = opts.appName || "my-app";
95
+ const token = opts.receiverSetupToken || "<paste-receiver-setup-token-here>";
96
+ const region = opts.region || "nyc3";
97
+ const size = opts.dropletSize || "s-1vcpu-2gb";
98
+ const dockerCompose = buildDropletDockerCompose({ appImage, appPort, appName });
99
+ const walkthrough = buildDropletWalkthrough({ appImage, appPort, appName, token, region, size, dockerCompose });
100
+ return {
101
+ kind: "guide",
102
+ target: "digitalocean-droplet",
103
+ summary: "Deploy your app + a hardened Blacksands Receiver on one DigitalOcean Droplet. Single VPS, ~$6/month, about 20 minutes to a protected endpoint.",
104
+ estimatedCost: `~$6/month (${size} droplet) + DigitalOcean bandwidth. New accounts typically get $200 free credit.`,
105
+ estimatedTimeMinutes: 20,
106
+ prerequisites: [
107
+ "DigitalOcean account (free to sign up at digitalocean.com)",
108
+ "Your app packaged as a Docker image (ask Claude to help containerize if needed)",
109
+ "A Blacksands Bursar account at shield.blacksandscyber.online",
110
+ "SSH key pair on your computer (run `ssh-keygen -t ed25519` if you don't have one)",
111
+ ],
112
+ walkthroughMarkdown: walkthrough,
113
+ dockerCompose,
114
+ verificationSteps: [
115
+ "`docker compose logs receiver` shows 'Receiver: bootstrap complete' and 'authenticated with Authorizer'",
116
+ "The `receiver_health` MCP tool returns status=active for your Receiver",
117
+ "The `receiver_list_services` MCP tool lists your app",
118
+ "Your app is reachable at the Receiver's assigned DNS name (use `receiver_get_dns`)",
119
+ ],
120
+ securityControls: [
121
+ { control: "mTLS-only on TCP 443", effect: "Every inbound connection must present a valid client certificate; plaintext HTTP is not exposed (no port 80 binding)" },
122
+ { control: "Receiver re-verifies cert chain on every new connection", effect: "The Receiver independently validates each client's certificate chain against the org CA on inbound TLS handshake — it does not trust forwarded cert headers from any upstream component" },
123
+ { control: "read_only: true", effect: "Receiver container filesystem is immutable at runtime" },
124
+ { control: "tmpfs mount for /certs", effect: "mTLS keys live in memory only — never hit disk, wiped on restart" },
125
+ { control: "user: 1000:1000", effect: "Receiver runs as unprivileged user; no root inside container" },
126
+ { control: "cap_drop: ALL", effect: "All Linux capabilities stripped — cannot mount filesystems, load modules, etc." },
127
+ { control: "no-new-privileges", effect: "Prevents setuid-based privilege escalation inside the container" },
128
+ { control: "No Docker socket mounted", effect: "Container cannot control the host Docker daemon or escape to host" },
129
+ { control: "Image pulled from signed registry", effect: "Swapping in a tampered image requires defeating Docker Content Trust" },
130
+ { control: "Sessions established at the Authorizer, not the Receiver", effect: "A locally-modified Receiver cannot invent valid sessions — every session is minted at a separate Blacksands service" },
131
+ { control: "Per-tenant Kafka authorization scope", effect: "Receiver only receives service_authorization messages for its own organization — cannot read or affect other tenants" },
132
+ ],
133
+ honestTamperingNote: "You have root on your own VPS, so you could modify the Receiver locally. Modifications affect only your own traffic — you cannot forge identities for other tenants (the CA private key is held by Blacksands, not the Receiver), cannot read other tenants' traffic (Receivers only receive Kafka messages addressed to their own org), and cannot invent valid sessions (sessions are established at the Authorizer, a separate Blacksands service). For workloads requiring cryptographic proof of binary integrity (TPM-backed remote attestation, signed control-plane messages), ask about Shield Enterprise.",
134
+ };
135
+ }
136
+ function buildDropletDockerCompose(p) {
137
+ return `# Blacksands Bursar — single-VPS deployment (DigitalOcean Droplet)
138
+ #
139
+ # Network model:
140
+ # INBOUND : TCP 443 ONLY from the public internet. mTLS is mandatory —
141
+ # the Receiver terminates TLS and independently verifies every
142
+ # client certificate chain on the inbound handshake. There is
143
+ # NO plaintext HTTP path; port 80 is not exposed. The Receiver
144
+ # IS a public-IP edge proxy doing SNI routing — it is NOT an
145
+ # outbound tunnel.
146
+ # OUTBOUND : TLS to the Blacksands Kafka cluster (the Receiver's only
147
+ # control-plane channel — it consumes service_authorization
148
+ # messages from the Manager via Kafka). It does NOT speak HTTP
149
+ # to the Authorizer; the Authorizer is the MCP-client side.
150
+ #
151
+ # Security hardening applied to the Receiver container:
152
+ # - read_only: true (filesystem immutable at runtime)
153
+ # - tmpfs /certs (mTLS keys in memory only, never on disk)
154
+ # - user: 1000:1000 (non-root inside container)
155
+ # - cap_drop: ALL (all Linux capabilities stripped)
156
+ # - no-new-privileges (prevents setuid escalation)
157
+ # - no Docker socket mount (cannot control host Docker daemon)
158
+ # - signed image from Blacksands registry
159
+
160
+ services:
161
+ receiver:
162
+ image: blacksands/receiver:stable
163
+ container_name: blacksands-receiver
164
+ restart: unless-stopped
165
+ read_only: true
166
+ user: "1000:1000"
167
+ cap_drop:
168
+ - ALL
169
+ security_opt:
170
+ - no-new-privileges:true
171
+ environment:
172
+ # Bootstrap: the setup token is single-use and tells the Receiver where
173
+ # the Blacksands onboarding edge lives, so it can fetch its mTLS cert
174
+ # bundle, RECEIVER_ID, ORGANIZATION_ID, and Kafka credentials.
175
+ # Generated by the receiver_initialize MCP tool.
176
+ RECEIVER_SETUP_TOKEN: \${RECEIVER_SETUP_TOKEN}
177
+ LOG_LEVEL: info
178
+ # Route traffic to the app container via Docker DNS.
179
+ UPSTREAM_HOST: ${p.appName}
180
+ UPSTREAM_PORT: ${p.appPort}
181
+ tmpfs:
182
+ - /tmp:size=64M,mode=1777
183
+ - /var/cache/nginx:size=128M,mode=0700,uid=1000,gid=1000
184
+ - /run:size=16M,mode=0700,uid=1000,gid=1000
185
+ - /certs:size=16M,mode=0700,uid=1000,gid=1000
186
+ ports:
187
+ # The Receiver listens ONLY on 443. All client traffic is mTLS — the
188
+ # Receiver terminates TLS and independently verifies every client
189
+ # certificate chain against the org CA on the inbound handshake.
190
+ # There is no port 80 binding because there is no plaintext path.
191
+ - "443:8443"
192
+ networks:
193
+ - shield-net
194
+ depends_on:
195
+ - app
196
+ healthcheck:
197
+ test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"]
198
+ interval: 30s
199
+ timeout: 5s
200
+ retries: 3
201
+ start_period: 30s
202
+
203
+ app:
204
+ image: ${p.appImage}
205
+ container_name: ${p.appName}
206
+ restart: unless-stopped
207
+ # Your app is NOT exposed to the public internet.
208
+ # Only the Receiver on the shield-net network can reach it.
209
+ expose:
210
+ - "${p.appPort}"
211
+ networks:
212
+ - shield-net
213
+
214
+ networks:
215
+ shield-net:
216
+ driver: bridge
217
+ `;
218
+ }
219
+ function buildDropletWalkthrough(p) {
220
+ return `# Deploy your app + Blacksands Bursar Receiver — DigitalOcean single-VPS
221
+
222
+ **Time:** about 20 minutes. **Cost:** ~$6/month. **Result:** your app reachable from the internet only through Blacksands' zero-trust Receiver.
223
+
224
+ > **Network model in one paragraph (read this first).** The Receiver is a public-IP edge proxy that listens on **TCP 443 only** — every connection is mTLS, and the Receiver independently verifies the client certificate chain against your org CA on each inbound handshake before any bytes from the client reach your app. There is **no plaintext HTTP path**; port 80 is not exposed. SNI on 443 selects the right backend service. Your Droplet **must** allow inbound TCP 443 from the internet, or no traffic ever reaches the Receiver — it is **not** an outbound tunneling client. The Receiver's only outbound control-plane channel is **TLS to the Blacksands Kafka cluster** (where it consumes \`service_authorization\` messages from the Manager). It does not speak HTTP to the Authorizer; that's the MCP-client side of the system. Your *MCP client* (Claude Desktop) talks to the Authorizer to discover this Receiver's URL — your Receiver does not.
225
+
226
+ ---
227
+
228
+ ## Step 1 — Get a Receiver setup token
229
+
230
+ In this Claude chat, say: *"Initialize a Receiver for my app, email it to <your-email>"*.
231
+
232
+ Claude runs \`receiver_initialize\` and returns a **setup token**. Copy it; you'll paste it in Step 6.
233
+
234
+ ---
235
+
236
+ ## Step 2 — Create a DigitalOcean Droplet
237
+
238
+ **Easiest (web UI):**
239
+
240
+ 1. Go to https://cloud.digitalocean.com/droplets/new
241
+ 2. **Image:** *Marketplace → Docker on Ubuntu 22.04*
242
+ 3. **Size:** Basic → Regular → **$6/mo (${p.size})**
243
+ 4. **Region:** **${p.region}** (or the region closest to your users)
244
+ 5. **Authentication:** SSH key — upload your public key (usually \`~/.ssh/id_ed25519.pub\`)
245
+ 6. **Hostname:** \`${p.appName}-shield\`
246
+ 7. Click **Create Droplet** — ready in about 45 seconds
247
+
248
+ **Or with the \`doctl\` CLI:**
249
+
250
+ \`\`\`bash
251
+ doctl compute droplet create ${p.appName}-shield \\
252
+ --image docker-20-04 \\
253
+ --size ${p.size} \\
254
+ --region ${p.region} \\
255
+ --ssh-keys $(doctl compute ssh-key list --format ID --no-header | head -1)
256
+ \`\`\`
257
+
258
+ **Copy the Droplet's public IPv4 address** from the DigitalOcean dashboard.
259
+
260
+ > **Firewall reminder.** Your Droplet must accept **inbound TCP 443 only** from the public internet — the Receiver is a public-IP edge proxy that requires mTLS on every connection. **Do not open port 80**; there is no plaintext HTTP path. New DigitalOcean Droplets default to no Cloud Firewall (so all ports are open), but if you've attached a Cloud Firewall make sure it allows inbound TCP 443 and only 443. The Droplet also needs **outbound TLS** to reach the Blacksands Kafka cluster — that's egress, which DigitalOcean allows by default.
261
+
262
+ ---
263
+
264
+ ## Step 3 — SSH into the Droplet
265
+
266
+ \`\`\`bash
267
+ ssh root@<droplet-ip>
268
+ \`\`\`
269
+
270
+ The Marketplace image has Docker pre-installed. Verify:
271
+
272
+ \`\`\`bash
273
+ docker --version && docker compose version
274
+ \`\`\`
275
+
276
+ ---
277
+
278
+ ## Step 4 — Create the deployment directory
279
+
280
+ \`\`\`bash
281
+ mkdir -p /opt/blacksands && cd /opt/blacksands
282
+ \`\`\`
283
+
284
+ ---
285
+
286
+ ## Step 5 — Save this \`docker-compose.yml\`
287
+
288
+ Paste the file below as \`/opt/blacksands/docker-compose.yml\`:
289
+
290
+ \`\`\`yaml
291
+ ${p.dockerCompose}\`\`\`
292
+
293
+ If your app image, port, or name differ from the defaults, edit the \`app\` service accordingly.
294
+
295
+ ---
296
+
297
+ ## Step 6 — Store your Receiver setup token
298
+
299
+ \`\`\`bash
300
+ cat > /opt/blacksands/.env <<EOF
301
+ RECEIVER_SETUP_TOKEN=${p.token}
302
+ EOF
303
+ chmod 600 /opt/blacksands/.env
304
+ \`\`\`
305
+
306
+ ---
307
+
308
+ ## Step 7 — Launch
309
+
310
+ \`\`\`bash
311
+ docker compose --env-file .env up -d
312
+ \`\`\`
313
+
314
+ Watch the Receiver come up:
315
+
316
+ \`\`\`bash
317
+ docker compose logs -f receiver
318
+ \`\`\`
319
+
320
+ Wait for:
321
+ - \`Receiver: bootstrap complete\` — the setup token was redeemed and the cert bundle / RECEIVER_ID / ORGANIZATION_ID are in memory
322
+ - \`Receiver: Kafka consumer ready\` — the control-plane channel to the Manager is open
323
+ - \`Receiver: listening on :8443 (mTLS only)\` — public ingress is up; mTLS verification active
324
+
325
+ Press **Ctrl-C** when you see those.
326
+
327
+ ---
328
+
329
+ ## Step 8 — Wire up DNS and services
330
+
331
+ Back in this Claude chat:
332
+
333
+ 1. *"Add my app as a service on my Receiver"* → runs \`receiver_onboard_service\` with \`host=${p.appName}\`, \`port=${p.appPort}\`.
334
+ 2. *"What's my Receiver's DNS name?"* → runs \`receiver_get_dns\`.
335
+ 3. *"Check my Receiver's health"* → runs \`receiver_health\`.
336
+
337
+ If you want your own domain instead of the auto-assigned one, create an A record at your DNS provider pointing to the Droplet's IP, then tell Claude the domain.
338
+
339
+ ---
340
+
341
+ ## Verification
342
+
343
+ Your app is now reachable **only** through the Receiver. Test by asking Claude: *"List active sessions on my Receiver"* — you should see traffic flowing through once you hit the URL.
344
+
345
+ ---
346
+
347
+ ## How traffic flows (so the picture is clear)
348
+
349
+ \`\`\`
350
+ INTERNET
351
+
352
+ │ 443 ONLY · mTLS mandatory
353
+
354
+ ┌────────────────┐
355
+ │ Receiver │◀──── (outbound only) Kafka TLS to Blacksands Manager
356
+ │ (this box) │ — receives service_authorization messages,
357
+ │ public IP │ publishes session audit events
358
+ │ · TLS termin. │
359
+ │ · mTLS verify │
360
+ │ · SNI routing │
361
+ └───────┬────────┘
362
+ │ docker network only (encrypted at the edge,
363
+ ▼ cleartext or app-level TLS to your container)
364
+ ┌──────────┐
365
+ │ Your │
366
+ │ App │
367
+ └──────────┘
368
+ \`\`\`
369
+
370
+ **Receiver inbound is TCP 443 only and every connection is mTLS.** On each new connection the Receiver terminates TLS and independently verifies the client certificate chain against your org's CA — it does not trust forwarded headers from any upstream component for cert identity. SNI on 443 selects the right backend service; the connection from the Receiver to your app rides the private Docker network. The Receiver does **not** speak HTTP to the Authorizer; Kafka to the Manager is its only control-plane channel. The Authorizer is what your MCP client (Claude Desktop) talks to to discover this Receiver's URL.
371
+
372
+ ## What's protecting you
373
+
374
+ The \`docker-compose.yml\` applies these controls to the Receiver container:
375
+
376
+ | Control | Why it matters |
377
+ |---|---|
378
+ | **mTLS-only on 443** | Every inbound connection requires a client certificate; plaintext HTTP is not exposed |
379
+ | **Receiver re-verifies the cert chain on every new connection** | The Receiver does not trust any forwarded cert identity — it independently validates the client's chain against your org CA on the inbound TLS handshake |
380
+ | \`read_only: true\` | Receiver binaries + config cannot be modified at runtime |
381
+ | \`tmpfs /certs\` | mTLS keys live in RAM only — never written to disk, gone on restart |
382
+ | \`user: 1000:1000\` | Container runs unprivileged; \`root\` is not available inside |
383
+ | \`cap_drop: ALL\` | No Linux capabilities — can't mount filesystems, load kernel modules, etc. |
384
+ | \`no-new-privileges\` | Prevents setuid-based privilege escalation |
385
+ | No Docker socket | Container can't talk to the Docker daemon or escape to host |
386
+ | Signed image from Blacksands registry | Casual image swapping is detected by Docker Content Trust |
387
+
388
+ **Honest note on local tampering.** You have root on your own Droplet, so you *could* modify the Receiver locally. What that does and doesn't get you:
389
+
390
+ - **It can** weaken protection of *your own* traffic — e.g. logging less, skipping policy checks. That hurts only you.
391
+ - **It cannot** forge identities for other tenants (the Blacksands CA's private key is held by Blacksands, not in this Receiver).
392
+ - **It cannot** read other tenants' traffic — your Receiver only gets Kafka authorization messages addressed to your organization.
393
+ - **It cannot** invent valid sessions out of thin air — every session is established at the Authorizer (a different system Blacksands operates), not the Receiver.
394
+
395
+ For workloads that need cryptographic proof the Receiver hasn't been modified (TPM-backed binary attestation, signed-message verification at the control plane), see **Shield Enterprise**.
396
+
397
+ ---
398
+
399
+ ## Updating
400
+
401
+ **New Receiver release from Blacksands:**
402
+ \`\`\`bash
403
+ cd /opt/blacksands
404
+ docker compose pull receiver
405
+ docker compose up -d receiver
406
+ \`\`\`
407
+
408
+ **New version of your own app:** edit the image tag in \`docker-compose.yml\`, then:
409
+ \`\`\`bash
410
+ docker compose up -d app
411
+ \`\`\`
412
+
413
+ ---
414
+
415
+ ## Troubleshooting
416
+
417
+ - **Receiver won't bootstrap** → setup token likely expired (they're single-use, time-limited). Ask Claude to initialize a fresh one, paste into \`.env\`, \`docker compose up -d receiver\`.
418
+ - **Receiver bootstraps but Kafka consumer never reports ready** → outbound TLS to the Blacksands Kafka cluster is being blocked. Check the Droplet's egress (DigitalOcean Cloud Firewall, host iptables) allows outbound to the Kafka broker host on its TLS port.
419
+ - **App unreachable from outside** → the Droplet's *inbound* TCP 443 must be open (and only 443). The Receiver is a public-IP edge proxy, not a tunnel; if your firewall blocks 443, nothing reaches the Receiver. Check **Droplets → Networking → Firewalls**.
420
+ - **Receiver keeps restarting** → health check failing. Run \`docker compose exec receiver wget -qO- http://localhost:8080/health\` to see the actual error.
421
+
422
+ For anything else, ask Claude — it has tools to query Blacksands' control plane for Receiver status, session logs, and policy enforcement.
423
+ `;
424
+ }
425
+ // ──────────────────────────────────────────────────────────────────────────
426
+ // Local Docker development — laptop dev-only path
427
+ // ──────────────────────────────────────────────────────────────────────────
428
+ function buildLocalDockerDevelopmentGuide(opts) {
429
+ const appImage = opts.appImage || "YOUR_APP_IMAGE:tag";
430
+ const appPort = opts.appPort || 8080;
431
+ const appName = opts.appName || "my-app";
432
+ const dockerCompose = `# Blacksands Bursar — LOCAL DEV ONLY (no Receiver in this file)
433
+ #
434
+ # IMPORTANT: a Blacksands Receiver REQUIRES a stable public IP. Laptops do
435
+ # not have one — they sleep, change networks, and live behind NAT. So this
436
+ # compose file does NOT include the Receiver. It runs your app locally for
437
+ # fast development iteration; once the app is ready you deploy the actual
438
+ # Blacksands-protected version to a real VPS (see target='digitalocean-droplet').
439
+ #
440
+ # If you want to test against a remote Receiver while iterating on the app
441
+ # locally, use a public-IP shim:
442
+ # - ngrok http ${appPort} (gives you a public hostname pointing at this laptop)
443
+ # - cloudflared tunnel (similar; needs a Cloudflare account)
444
+ # These are DEV-ONLY; do not expose production data this way.
445
+
446
+ services:
447
+ app:
448
+ image: ${appImage}
449
+ container_name: ${appName}
450
+ restart: unless-stopped
451
+ ports:
452
+ # Bound to localhost only — DO NOT expose to the LAN. Use ngrok/cloudflared
453
+ # if you need a public URL during development.
454
+ - "127.0.0.1:${appPort}:${appPort}"
455
+ networks:
456
+ - dev-net
457
+
458
+ networks:
459
+ dev-net:
460
+ driver: bridge
461
+ `;
462
+ const walkthroughMarkdown = `# Local Docker development — Blacksands Bursar
463
+
464
+ > **DEV ONLY.** This walkthrough does NOT install a Blacksands Receiver. A real Receiver requires a stable public IP and listens on inbound TCP 443; laptops can't satisfy that requirement. Use this guide while iterating on your app, then move to a real VPS for any traffic that matters. The recommended production target is \`digitalocean-droplet\` (single-VPS, ~$6/mo).
465
+
466
+ ---
467
+
468
+ ## What this gets you
469
+
470
+ A clean local Docker setup where your app:
471
+ - Runs in a container that auto-restarts (\`restart: unless-stopped\`)
472
+ - Binds to \`127.0.0.1\` only (does NOT expose to your LAN)
473
+ - Sits on its own Docker network
474
+
475
+ What it does NOT get you:
476
+ - Any Blacksands protection
477
+ - A way for external users / CI / phones to reach the app
478
+
479
+ ---
480
+
481
+ ## Step 1 — Save the dev compose file
482
+
483
+ \`\`\`yaml
484
+ ${dockerCompose}\`\`\`
485
+
486
+ ---
487
+
488
+ ## Step 2 — Run
489
+
490
+ \`\`\`bash
491
+ docker compose up -d
492
+ docker compose logs -f ${appName}
493
+ curl http://127.0.0.1:${appPort}/healthz # adjust to your healthcheck path
494
+ \`\`\`
495
+
496
+ ---
497
+
498
+ ## Step 3 — (Optional) Remote test against a real Receiver
499
+
500
+ If you want to test the **MCP-protected access path** while still iterating on the app locally, you have two reasonable options:
501
+
502
+ **Option A — ngrok shim (fastest):**
503
+ \`\`\`bash
504
+ brew install ngrok
505
+ ngrok config add-authtoken <your-token>
506
+ ngrok http ${appPort}
507
+ # Get a URL like https://abc123.ngrok-free.app — point your real (remote)
508
+ # Receiver at this URL via receiver_onboard_service.
509
+ \`\`\`
510
+ Limitation: ngrok rotates URLs unless you pay; the cert chain is ngrok's, not yours.
511
+
512
+ **Option B — Run a real Receiver on a $6 VPS, point it at your laptop's tunnel:**
513
+ Use the \`digitalocean-droplet\` target to stand up an actual Receiver, then add a service whose upstream is your ngrok / cloudflared URL. This is closest to your eventual production setup.
514
+
515
+ **Both options are dev-only.** Do not put real user traffic through ngrok-shimmed laptops.
516
+
517
+ ---
518
+
519
+ ## When to graduate
520
+
521
+ Stop using this walkthrough when:
522
+ 1. Your app's behaviour is stable enough that you want it always-on
523
+ 2. You need other people, your phone, or CI to hit it
524
+ 3. You want to actually test Blacksands' Receiver-side controls (mTLS verification, policies)
525
+
526
+ At that point, switch to \`bursar_guide_deployment\` with \`target='digitalocean-droplet'\` for a single-VPS production-ready deployment.
527
+
528
+ ---
529
+
530
+ ## Why a laptop can't host a Receiver
531
+
532
+ For completeness, here's the architectural reason:
533
+
534
+ | Receiver requires | Laptop has | Result |
535
+ |---|---|---|
536
+ | Stable public IP | Behind NAT, IP changes per network | Can't satisfy |
537
+ | Inbound TCP 443 reachable from internet | Most home routers block inbound | Can't satisfy |
538
+ | 24/7 uptime to consume Kafka control-plane messages | Sleeps when lid closes | Can't satisfy |
539
+ | Reproducible environment for Docker Content Trust on the Receiver image | Personal machine | Possible but fragile |
540
+
541
+ A $6/mo Droplet (or any VPS) satisfies all of these. The Blacksands deployment story optimizes for that target.
542
+ `;
543
+ return {
544
+ kind: "guide",
545
+ target: "local-docker-development",
546
+ summary: "DEV ONLY: a local Docker setup for iterating on the app. Does NOT install a Receiver — a Receiver requires a public IP, which laptops do not have. Use this for app development; switch to digitalocean-droplet for any real traffic.",
547
+ estimatedCost: "Free.",
548
+ estimatedTimeMinutes: 5,
549
+ prerequisites: [
550
+ "Docker Desktop installed and running",
551
+ "Your app packaged as a Docker image (or a Dockerfile to build from)",
552
+ "(Optional) ngrok or cloudflared if you want a public URL for remote testing",
553
+ ],
554
+ walkthroughMarkdown,
555
+ dockerCompose,
556
+ verificationSteps: [
557
+ "`docker compose ps` shows the app container running",
558
+ "`curl http://127.0.0.1:<port>/healthz` returns 200 from the laptop",
559
+ "The container does NOT bind to 0.0.0.0 (LAN-exposed) — only 127.0.0.1",
560
+ ],
561
+ securityControls: [
562
+ { control: "127.0.0.1 binding only", effect: "App is reachable from the laptop only — not from the LAN, not from the internet" },
563
+ { control: "Restart policy unless-stopped", effect: "Container survives crashes during dev iteration" },
564
+ { control: "Dedicated Docker network (dev-net)", effect: "Isolated from other compose stacks on the laptop" },
565
+ ],
566
+ honestTamperingNote: "This setup has no Blacksands protection at all — the app is not behind a Receiver, there is no mTLS, no policy enforcement, no audit. It is a dev convenience for iterating on the app itself. Do NOT use it for any traffic that matters. For real protection, deploy the digitalocean-droplet target (or another VPS variant) where a real Receiver can run.",
567
+ };
568
+ }
569
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { DataStore } from "../types";
2
+ export declare function detectDataStores(projectPath: string): Promise<DataStore[]>;
3
+ //# sourceMappingURL=dataStoreDetector.d.ts.map