@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,139 @@
1
+ /**
2
+ * Shared types for bursar_install_agent_remotely.
3
+ *
4
+ * The Zod schema in src/server.ts is the source of truth for input shape.
5
+ * The runtime types below are carefully kept in sync — and are what the
6
+ * orchestrator, transports, and config merger pass around.
7
+ */
8
+ export type AgentType = "claude-desktop" | "mcp-server" | "openclaw" | "custom";
9
+ export type TransportType = "ssh" | "bootstrap-token" | "aws-ssm";
10
+ export interface SshTransportSpec {
11
+ type: "ssh";
12
+ host: string;
13
+ port: number;
14
+ username: string;
15
+ authMethod: "private-key" | "ssh-agent" | "password";
16
+ privateKeyPath?: string;
17
+ knownHostsPath?: string;
18
+ hostKeyFingerprint?: string;
19
+ sudo: boolean;
20
+ }
21
+ export interface BootstrapTokenTransportSpec {
22
+ type: "bootstrap-token";
23
+ ttlSeconds: number;
24
+ allowedCidr?: string;
25
+ oneShot: boolean;
26
+ deliverVia: "return" | "email";
27
+ email?: string;
28
+ }
29
+ export interface AwsSsmTransportSpec {
30
+ type: "aws-ssm";
31
+ instanceId: string;
32
+ region?: string;
33
+ }
34
+ export type TransportSpec = SshTransportSpec | BootstrapTokenTransportSpec | AwsSsmTransportSpec;
35
+ export interface InstallInput {
36
+ clientName: string;
37
+ orgId: string;
38
+ agentType: AgentType;
39
+ configPath?: string;
40
+ restartCmd?: string;
41
+ transport: TransportSpec;
42
+ serviceId: string;
43
+ authorizerUrl?: string;
44
+ dryRun: boolean;
45
+ waitForHandshake: boolean;
46
+ handshakeTimeoutMs: number;
47
+ rollbackOnFailure: boolean;
48
+ /**
49
+ * RBAC role for the issued cert. 'master' grants the full 47-tool MCP
50
+ * surface; 'consumer' restricts the agent to the 11 [FREE] tools and
51
+ * is enforced both at the Shield API (requireMcpRole middleware) AND
52
+ * at the MCP server (boot-time tool filtering via .role hint file
53
+ * written by setup.js). Defaults to 'master' when omitted, matching
54
+ * bursar_mcp_clients.role's backward-compat default.
55
+ */
56
+ role?: "master" | "consumer";
57
+ }
58
+ export interface InstalledFileRecord {
59
+ path: string;
60
+ mode: string;
61
+ sha256: string;
62
+ }
63
+ export interface InstallResult {
64
+ status: "installed" | "bootstrap-pending" | "dry-run";
65
+ clientName: string;
66
+ clientId: string;
67
+ fingerprint?: string;
68
+ expires?: string;
69
+ transport: TransportType;
70
+ target: Record<string, unknown>;
71
+ installedFiles: InstalledFileRecord[];
72
+ configPath: string;
73
+ bootstrapUrl?: string;
74
+ bootstrapExpiresAt?: string;
75
+ handshake: {
76
+ verified: boolean;
77
+ verifiedAt?: string;
78
+ receiverUrl?: string;
79
+ waitedMs?: number;
80
+ reason?: string;
81
+ };
82
+ auditId: string;
83
+ revokeHint: string;
84
+ next_steps: string[];
85
+ rollback?: {
86
+ performed: boolean;
87
+ steps: string[];
88
+ errors?: string[];
89
+ };
90
+ warnings?: string[];
91
+ }
92
+ /**
93
+ * Transport interface. Phase A.1 implements BootstrapToken only;
94
+ * SSH and SSM stubs throw to keep the discriminated union complete.
95
+ */
96
+ export interface Transport {
97
+ readonly type: TransportType;
98
+ /**
99
+ * Deliver the credential bundle to the target and write files.
100
+ * For bootstrap-token, this means minting the token — no target-side
101
+ * action is performed by this MCP; the target will redeem later.
102
+ */
103
+ deliver(args: {
104
+ clientName: string;
105
+ orgId: string;
106
+ agentType: AgentType;
107
+ configPath: string;
108
+ serviceId: string;
109
+ authorizerUrl: string;
110
+ /** RBAC role for the issued cert (master | consumer). Optional; defaults
111
+ * to 'master' on the Shield API side when omitted. */
112
+ role?: "master" | "consumer";
113
+ }): Promise<TransportDeliveryResult>;
114
+ /**
115
+ * Roll back any mutations this transport made on the target. Must be
116
+ * idempotent; called with the delivery result from a successful deliver().
117
+ */
118
+ rollback(delivery: TransportDeliveryResult): Promise<{
119
+ steps: string[];
120
+ errors: string[];
121
+ }>;
122
+ }
123
+ export interface TransportDeliveryResult {
124
+ /** status to surface to caller (maps to InstallResult.status) */
125
+ status: "installed" | "bootstrap-pending";
126
+ clientId: string;
127
+ fingerprint?: string;
128
+ expires?: string;
129
+ installedFiles: InstalledFileRecord[];
130
+ bootstrapUrl?: string;
131
+ bootstrapExpiresAt?: string;
132
+ /** If the target will redeem a token later, we don't yet know the config path. */
133
+ appliedConfig: boolean;
134
+ /** Arbitrary per-transport state the rollback step needs. */
135
+ rollbackState?: unknown;
136
+ warnings?: string[];
137
+ next_steps: string[];
138
+ }
139
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Shared types for bursar_install_agent_remotely.
4
+ *
5
+ * The Zod schema in src/server.ts is the source of truth for input shape.
6
+ * The runtime types below are carefully kept in sync — and are what the
7
+ * orchestrator, transports, and config merger pass around.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Authoritative description of the Blacksands broker auth protocol.
3
+ *
4
+ * Why this module exists:
5
+ * The auth chain is BISECTED:
6
+ * 1. Agent does mTLS with the Authorizer
7
+ * 2. Authorizer returns the list of services the agent is authorized for
8
+ * 3. Agent picks one
9
+ * 4. The selected URL points at a Receiver (NOT the backend service)
10
+ * 5. Receiver re-verifies mTLS independently
11
+ * 6. Receiver proxies to the actual backend service
12
+ *
13
+ * LLMs that don't know this protocol improvise from training data —
14
+ * typically attempting `curl https://internal-service.acme.com/...`
15
+ * directly and presenting the cert to the wrong endpoint. This module
16
+ * is the SINGLE SOURCE OF TRUTH the LLM should consult before writing
17
+ * any code that talks to a Blacksands-protected service.
18
+ *
19
+ * Same authoritative-mode pattern as src/shield/deploy/index.ts (the
20
+ * bursar_guide_deployment tool) — proven to override training-data
21
+ * improvisation when the description is strong enough.
22
+ *
23
+ * Surfaced through THREE channels — single source of truth, three reach paths:
24
+ * - The MCP tool `broker_get_protocol_walkthrough` (LLM-driven look-up)
25
+ * - The MCP resource `blacksands://broker-protocol` (client-driven priming)
26
+ * - The README "Accessing protected services" section (human + crawler)
27
+ */
28
+ export type CodeLanguage = "node" | "python" | "curl" | "all";
29
+ export interface CodeExample {
30
+ language: "node" | "python" | "curl";
31
+ filename: string;
32
+ code: string;
33
+ notes: string;
34
+ }
35
+ export interface FlowStep {
36
+ step: number;
37
+ actor: "agent" | "authorizer" | "receiver" | "service";
38
+ action: string;
39
+ cryptoCheck: string;
40
+ whatGoesWrongIfSkipped: string;
41
+ }
42
+ export interface ProtocolWalkthrough {
43
+ kind: "protocol-walkthrough";
44
+ authoritativeNote: string;
45
+ summary: string;
46
+ bisectedFlow: FlowStep[];
47
+ falseModelsToReject: string[];
48
+ codeExamples: CodeExample[];
49
+ commonPitfalls: Array<{
50
+ symptom: string;
51
+ cause: string;
52
+ fix: string;
53
+ }>;
54
+ furtherReading: {
55
+ receiverDocs: string;
56
+ authorizerDocs: string;
57
+ mcpServerInstall: string;
58
+ };
59
+ }
60
+ export interface WalkthroughOpts {
61
+ /** Filter code examples to a specific language. Default: all. */
62
+ language?: CodeLanguage;
63
+ }
64
+ export declare function getProtocolWalkthrough(opts?: WalkthroughOpts): ProtocolWalkthrough;
65
+ //# sourceMappingURL=protocol-walkthrough.d.ts.map
@@ -0,0 +1,392 @@
1
+ "use strict";
2
+ /**
3
+ * Authoritative description of the Blacksands broker auth protocol.
4
+ *
5
+ * Why this module exists:
6
+ * The auth chain is BISECTED:
7
+ * 1. Agent does mTLS with the Authorizer
8
+ * 2. Authorizer returns the list of services the agent is authorized for
9
+ * 3. Agent picks one
10
+ * 4. The selected URL points at a Receiver (NOT the backend service)
11
+ * 5. Receiver re-verifies mTLS independently
12
+ * 6. Receiver proxies to the actual backend service
13
+ *
14
+ * LLMs that don't know this protocol improvise from training data —
15
+ * typically attempting `curl https://internal-service.acme.com/...`
16
+ * directly and presenting the cert to the wrong endpoint. This module
17
+ * is the SINGLE SOURCE OF TRUTH the LLM should consult before writing
18
+ * any code that talks to a Blacksands-protected service.
19
+ *
20
+ * Same authoritative-mode pattern as src/shield/deploy/index.ts (the
21
+ * bursar_guide_deployment tool) — proven to override training-data
22
+ * improvisation when the description is strong enough.
23
+ *
24
+ * Surfaced through THREE channels — single source of truth, three reach paths:
25
+ * - The MCP tool `broker_get_protocol_walkthrough` (LLM-driven look-up)
26
+ * - The MCP resource `blacksands://broker-protocol` (client-driven priming)
27
+ * - The README "Accessing protected services" section (human + crawler)
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.getProtocolWalkthrough = getProtocolWalkthrough;
31
+ function getProtocolWalkthrough(opts = {}) {
32
+ const language = opts.language ?? "all";
33
+ return {
34
+ kind: "protocol-walkthrough",
35
+ authoritativeNote: "This is the AUTHORITATIVE description of the Blacksands broker auth protocol. " +
36
+ "Do NOT improvise from general mTLS or zero-trust knowledge — the protocol is BISECTED " +
37
+ "(Authorizer first, then a separately-verified Receiver), which is unusual and easily " +
38
+ "confused with simpler single-stage mTLS designs. Read the bisectedFlow + " +
39
+ "falseModelsToReject sections before writing any code or instructions for accessing " +
40
+ "a Blacksands-protected service.",
41
+ summary: "To call any Blacksands-protected service, your agent makes TWO mTLS handshakes in " +
42
+ "sequence with the same client cert: one to the Authorizer (which discovers what " +
43
+ "services you're allowed to reach and where they live), and one to the Receiver (a " +
44
+ "public-IP edge proxy that verifies your cert chain a second time and only then " +
45
+ "forwards to the backend). The Receiver address is returned by the Authorizer in " +
46
+ "the service-list response — never hardcoded by the agent. The backend service " +
47
+ "URL is NEVER reachable directly from the agent.",
48
+ bisectedFlow: [
49
+ {
50
+ step: 1,
51
+ actor: "agent",
52
+ action: "Agent opens an HTTPS connection to the Authorizer (e.g. mauth-beta.blacksandscyber.online) " +
53
+ "and presents its mTLS client certificate during the TLS handshake. Cert + auth password " +
54
+ "are POSTed to /api/agent/auth.",
55
+ cryptoCheck: "Authorizer validates: (a) the cert chain against the org CA, (b) the cert isn't revoked, " +
56
+ "(c) the auth password matches the bundle's stored credential.",
57
+ whatGoesWrongIfSkipped: "If the agent skips this step and tries to talk to a service URL directly, no service " +
58
+ "URL is reachable from the public internet — only the Receiver's edge IP is, and it will " +
59
+ "reject any TLS handshake whose SNI doesn't match a known service.",
60
+ },
61
+ {
62
+ step: 2,
63
+ actor: "authorizer",
64
+ action: "On success, Authorizer returns a sessionToken plus a list of services { serviceId, serviceUrl, " +
65
+ "serviceType }. The serviceUrl points at the RECEIVER's public address, not at the backend " +
66
+ "service. The token is stamped with the agent's identity for downstream Receiver verification.",
67
+ cryptoCheck: "Authorizer signs the sessionToken; the Receiver verifies the signature in step 4.",
68
+ whatGoesWrongIfSkipped: "Without the service-list response, the agent has no way to know (a) which Receiver to " +
69
+ "contact for which backend, (b) which backend services this cert is even authorized for. " +
70
+ "The set of allowed services is per-cert, not per-URL — guessing service URLs cannot work.",
71
+ },
72
+ {
73
+ step: 3,
74
+ actor: "agent",
75
+ action: "Agent picks the service it wants from the list (matching by serviceId or serviceType), then " +
76
+ "appends the sessionToken to the serviceUrl as a query parameter (?token=…) and uses that as " +
77
+ "the base URL for all subsequent requests.",
78
+ cryptoCheck: "Token-in-URL is bound to the cert's identity by the Authorizer; binding is checked by the " +
79
+ "Receiver in step 4 before any request bytes are forwarded.",
80
+ whatGoesWrongIfSkipped: "Agents that hardcode a serviceUrl (skipping the discovery in step 2) get a stale Receiver " +
81
+ "address; if the Receiver moves, every cached client breaks.",
82
+ },
83
+ {
84
+ step: 4,
85
+ actor: "receiver",
86
+ action: "Agent opens a SECOND mTLS handshake — this time to the Receiver — and presents the SAME " +
87
+ "client cert. The Receiver listens on TCP 443 only; mTLS is mandatory.",
88
+ cryptoCheck: "Receiver INDEPENDENTLY validates the cert chain against the org CA on every new connection. " +
89
+ "It does not trust forwarded headers or the fact that the Authorizer already validated. " +
90
+ "This is the second 'bisected' check that distinguishes the protocol from single-stage mTLS.",
91
+ whatGoesWrongIfSkipped: "If the agent presents a different cert (or no cert) to the Receiver, the TLS handshake fails " +
92
+ "and no bytes are forwarded. The cert on Receiver-side is the SAME cert that authenticated to " +
93
+ "the Authorizer — never a session-derived ephemeral one.",
94
+ },
95
+ {
96
+ step: 5,
97
+ actor: "receiver",
98
+ action: "Receiver looks up the request's sessionToken (?token=…) in its Redis-cached service-authorization " +
99
+ "table populated by the Manager via Kafka. If the token + cert + serviceId combination is valid, " +
100
+ "the request is forwarded to the backend service.",
101
+ cryptoCheck: "Token + cert fingerprint + serviceId are checked together; mismatched combinations are 403'd.",
102
+ whatGoesWrongIfSkipped: "Without the token, the Receiver may still accept the cert at the TLS layer but reject the " +
103
+ "request at the application layer — symptoms include 401/403 with no clear cause.",
104
+ },
105
+ {
106
+ step: 6,
107
+ actor: "service",
108
+ action: "Backend service (Shield API, your internal app, etc.) receives the request from the Receiver. " +
109
+ "The Receiver may have added X-Client-Cert-CN / X-Client-Cert-Valid headers so the service " +
110
+ "knows the verified identity (used by Shield API for the per-MCP-client RBAC table).",
111
+ cryptoCheck: "The backend service trusts the Receiver's identity assertion (since the Receiver did the heavy " +
112
+ "lifting). It does not re-verify the agent's cert; it relies on the headers.",
113
+ whatGoesWrongIfSkipped: "If the agent attempts to reach the backend service directly (bypassing the Receiver), no " +
114
+ "X-Client-Cert headers are present, and the service rejects the request — that's the entire " +
115
+ "point of the architecture.",
116
+ },
117
+ ],
118
+ falseModelsToReject: [
119
+ "WRONG: 'Agent talks directly to the backend service URL.' " +
120
+ "There is no public path to the backend service. Only the Receiver's public IP is reachable.",
121
+ "WRONG: 'The cert is presented to the backend service.' " +
122
+ "The cert is only ever presented to the Authorizer (step 1) and the Receiver (step 4). The " +
123
+ "service receives forwarded headers, not the cert itself.",
124
+ "WRONG: 'After getting the serviceUrl, skip the Receiver — it's just a transparent proxy.' " +
125
+ "The Receiver is a SEPARATE security boundary that re-verifies the cert chain. Skipping it " +
126
+ "is impossible (the URL points at it; there's no backend URL to skip TO).",
127
+ "WRONG: 'The Authorizer hands out time-limited service certs the agent uses for the backend.' " +
128
+ "No. The agent uses ITS OWN cert for both stages. The Authorizer hands out a sessionToken " +
129
+ "(in the URL), not a derived certificate.",
130
+ "WRONG: 'Once authenticated, cache the serviceUrl forever — re-discovery is unnecessary.' " +
131
+ "Wrong. Service URLs can change (Receiver migrations, new Receivers added/removed); " +
132
+ "sessionTokens have a TTL (~30 minutes). The agent should re-call /api/agent/auth when " +
133
+ "either expires or fails. The MCP server's BrokerConnector handles this transparently if " +
134
+ "you use it; from-scratch implementations need to handle 401/403 → reconnect.",
135
+ "WRONG: 'Like Cloudflare Access — the agent sees a JWT, the proxy verifies, done.' " +
136
+ "Different model. Blacksands uses end-to-end mTLS (cert presented at BOTH stages) plus a " +
137
+ "URL-bound sessionToken. There is no JWT; there is no SSO bounce; the protocol is fully " +
138
+ "machine-to-machine.",
139
+ "WRONG: 'I can use any HTTP client; mTLS is just a TLS option.' " +
140
+ "Practically true, but the client must (a) present the cert to BOTH endpoints, (b) handle " +
141
+ "the URL+token construction from step 2's response, (c) reconnect on 401/403. Most off-the-" +
142
+ "shelf clients require explicit configuration for all three.",
143
+ ],
144
+ codeExamples: filterExamples([
145
+ {
146
+ language: "node",
147
+ filename: "broker-call.js",
148
+ notes: "Node.js with axios + https.Agent. Reuses the same agent (and cert) for BOTH the Authorizer call " +
149
+ "and the Receiver call — that's the bisected handshake in code form.",
150
+ code: NODE_EXAMPLE,
151
+ },
152
+ {
153
+ language: "python",
154
+ filename: "broker_call.py",
155
+ notes: "Python with `requests`. The cert= and verify= kwargs cover the mTLS for both stages. " +
156
+ "The same cert+key pair is passed to both calls.",
157
+ code: PYTHON_EXAMPLE,
158
+ },
159
+ {
160
+ language: "curl",
161
+ filename: "broker-call.sh",
162
+ notes: "curl one-liner showing the full chain: first auth, then service call. Useful for debugging " +
163
+ "and for validating cert/token state from the shell.",
164
+ code: CURL_EXAMPLE,
165
+ },
166
+ ], language),
167
+ commonPitfalls: [
168
+ {
169
+ symptom: "401 from the Authorizer on /api/agent/auth",
170
+ cause: "Either (a) the cert is not presented (httpsAgent missing or misconfigured), or (b) the auth password is wrong, or (c) the cert was revoked.",
171
+ fix: "Verify httpsAgent is passed to the request; verify the auth password matches what's in ~/.blacksands/mcp-certs/.auth-password; if revoked, request a new cert from your Shield admin.",
172
+ },
173
+ {
174
+ symptom: "200 from /api/agent/auth but service.serviceUrl is empty or null",
175
+ cause: "Infrastructure regression — the Manager couldn't resolve the requested serviceId from LDAP, or the service_authorization Kafka message hasn't propagated yet, or the Receiver-side Redis cache is stale.",
176
+ fix: "Re-attempt with a small backoff. If it persists, the cert isn't authorized for that serviceId — check via bursar_list_services or your Shield admin dashboard.",
177
+ },
178
+ {
179
+ symptom: "TLS handshake error when calling the serviceUrl",
180
+ cause: "The agent is connecting WITHOUT the client cert. This is by far the most common bug — many HTTP clients require explicit per-call cert config for the second handshake even if the first one worked.",
181
+ fix: "Pass the SAME httpsAgent / cert / key to the second call as the first. In axios: reuse `httpsAgent`. In Python requests: pass `cert=` to BOTH `auth_response = requests.post(...)` AND `service_response = requests.get(...)`.",
182
+ },
183
+ {
184
+ symptom: "401/403 from the Receiver despite a valid cert",
185
+ cause: "The sessionToken from step 2 wasn't included in the request URL, OR the token expired (~30 minute TTL), OR the cert presented to the Receiver is different from the one used for the Authorizer.",
186
+ fix: "Append `?token=<sessionToken>` to every request through the Receiver. On 401/403 with a previously-working session, re-call /api/agent/auth and use the new token.",
187
+ },
188
+ {
189
+ symptom: "Connection refused when bypassing the Receiver",
190
+ cause: "Working as designed. The backend service is not reachable from the public internet. Only the Receiver is.",
191
+ fix: "Always go through the Receiver URL returned by the Authorizer. Never hardcode the backend service URL.",
192
+ },
193
+ ],
194
+ furtherReading: {
195
+ receiverDocs: "https://shield.blacksandscyber.online/docs/receiver",
196
+ authorizerDocs: "https://shield.blacksandscyber.online/docs/authorizer",
197
+ mcpServerInstall: "claude mcp add shield -- npx -y @blacksandscyber/mcp-server-bursar",
198
+ },
199
+ };
200
+ }
201
+ function filterExamples(all, lang) {
202
+ if (lang === "all")
203
+ return all;
204
+ return all.filter((e) => e.language === lang);
205
+ }
206
+ // ──────────────────────────────────────────────────────────────────────────
207
+ // Code examples — kept as separate constants for readability and so the
208
+ // content tests can grep them.
209
+ // ──────────────────────────────────────────────────────────────────────────
210
+ const NODE_EXAMPLE = `// broker-call.js — Node.js / axios + https.Agent
211
+ //
212
+ // Demonstrates the BISECTED Blacksands auth flow:
213
+ // 1. mTLS to Authorizer → get session token + service URLs
214
+ // 2. mTLS to Receiver → make the actual service call
215
+ //
216
+ // Both calls reuse the SAME httpsAgent (so the SAME cert is presented
217
+ // to both endpoints). This is the linchpin — the agent's identity is
218
+ // verified independently at both stages.
219
+
220
+ const fs = require("fs");
221
+ const https = require("https");
222
+ const axios = require("axios");
223
+
224
+ const AUTHORIZER_URL = "https://mauth-beta.blacksandscyber.online";
225
+ const SERVICE_ID = "shield-api"; // or your org's app id
226
+ const CERT_PATH = process.env.SHIELD_CLIENT_CERT;
227
+ const KEY_PATH = process.env.SHIELD_CLIENT_KEY;
228
+ const CA_PATH = process.env.SHIELD_CA_CERT;
229
+ const AUTH_PASSWORD = process.env.SHIELD_AUTH_PASSWORD;
230
+
231
+ // ONE httpsAgent reused for BOTH handshakes. This is the bisected protocol
232
+ // in code form — same cert, two independent verifications.
233
+ const agent = new https.Agent({
234
+ cert: fs.readFileSync(CERT_PATH),
235
+ key: fs.readFileSync(KEY_PATH),
236
+ ca: fs.readFileSync(CA_PATH),
237
+ rejectUnauthorized: true,
238
+ });
239
+
240
+ async function callBlacksandsService() {
241
+ // ── STEP 1+2: Authorize and discover the service URL ────────────────
242
+ const authRes = await axios.post(
243
+ \`\${AUTHORIZER_URL}/api/agent/auth\`,
244
+ { password: AUTH_PASSWORD, serviceId: SERVICE_ID },
245
+ { httpsAgent: agent, timeout: 30_000 }
246
+ );
247
+
248
+ if (!authRes.data.success || !authRes.data.service?.serviceUrl) {
249
+ throw new Error("Authorizer did not return a service URL — cert may not be authorized for " + SERVICE_ID);
250
+ }
251
+
252
+ // The service URL points at the RECEIVER (NOT at the backend service).
253
+ // It includes a sessionToken bound to this cert's identity.
254
+ const receiverUrl = authRes.data.service.serviceUrl;
255
+
256
+ // Strip ?token=… so we can use the URL as a base; pass the token as a
257
+ // query param on every call instead. (Path concat with ? in the URL
258
+ // breaks: \`https://host?token=X/v1/foo\` is malformed.)
259
+ const [base, qs] = receiverUrl.split("?");
260
+ const token = qs ? new URLSearchParams(qs).get("token") : null;
261
+
262
+ // ── STEP 3+4+5+6: Call the backend through the Receiver ─────────────
263
+ // The same httpsAgent is reused — Receiver does its OWN mTLS handshake
264
+ // and re-verifies the cert chain. Token goes as a query param.
265
+ const apiClient = axios.create({
266
+ baseURL: \`\${base.replace(/\\/$/, "")}/v1\`,
267
+ httpsAgent: agent,
268
+ timeout: 30_000,
269
+ params: token ? { token } : {},
270
+ });
271
+
272
+ // Now make any number of calls — apiClient handles the mTLS + token
273
+ // for each one.
274
+ const orgs = await apiClient.get("/orgs");
275
+ return orgs.data;
276
+ }
277
+
278
+ callBlacksandsService()
279
+ .then((data) => console.log(JSON.stringify(data, null, 2)))
280
+ .catch((err) => {
281
+ console.error("Failed:", err.response?.data || err.message);
282
+ process.exit(1);
283
+ });
284
+ `;
285
+ const PYTHON_EXAMPLE = `# broker_call.py — Python / requests
286
+ #
287
+ # Demonstrates the BISECTED Blacksands auth flow:
288
+ # 1. mTLS to Authorizer -> get session token + service URLs
289
+ # 2. mTLS to Receiver -> make the actual service call
290
+ #
291
+ # Both calls pass the SAME (cert, key) tuple via cert=, so the SAME cert
292
+ # is presented to both endpoints. Don't try to re-use a session object
293
+ # without the cert= kwarg — requests will silently drop the client cert
294
+ # from the second connection.
295
+
296
+ import os
297
+ from urllib.parse import urlsplit, parse_qs
298
+ import requests
299
+
300
+ AUTHORIZER_URL = "https://mauth-beta.blacksandscyber.online"
301
+ SERVICE_ID = "shield-api" # or your org's app id
302
+
303
+ CERT = os.environ["SHIELD_CLIENT_CERT"]
304
+ KEY = os.environ["SHIELD_CLIENT_KEY"]
305
+ CA = os.environ["SHIELD_CA_CERT"]
306
+ AUTH_PASSWORD = os.environ["SHIELD_AUTH_PASSWORD"]
307
+
308
+ # (cert_path, key_path) tuple reused on both calls. cert= covers the mTLS
309
+ # client cert; verify= covers the server cert chain (so the agent verifies
310
+ # the Authorizer's and Receiver's certs against the Blacksands CA).
311
+ mtls_cert = (CERT, KEY)
312
+
313
+ def call_blacksands_service():
314
+ # ── STEP 1+2: Authorize and discover the service URL ────────────────
315
+ auth_res = requests.post(
316
+ f"{AUTHORIZER_URL}/api/agent/auth",
317
+ json={"password": AUTH_PASSWORD, "serviceId": SERVICE_ID},
318
+ cert=mtls_cert,
319
+ verify=CA,
320
+ timeout=30,
321
+ )
322
+ auth_res.raise_for_status()
323
+ auth_data = auth_res.json()
324
+
325
+ if not auth_data.get("success") or not auth_data.get("service", {}).get("serviceUrl"):
326
+ raise RuntimeError(
327
+ f"Authorizer did not return a service URL "
328
+ f"— cert may not be authorized for {SERVICE_ID}"
329
+ )
330
+
331
+ # The service URL points at the RECEIVER (NOT at the backend service).
332
+ # Includes a sessionToken bound to this cert's identity.
333
+ receiver_url = auth_data["service"]["serviceUrl"]
334
+ parts = urlsplit(receiver_url)
335
+ base = f"{parts.scheme}://{parts.netloc}{parts.path.rstrip('/')}"
336
+ token = parse_qs(parts.query).get("token", [None])[0]
337
+
338
+ # ── STEP 3+4+5+6: Call the backend through the Receiver ────────────
339
+ # Same cert= reused — Receiver does its OWN mTLS handshake.
340
+ # Token goes as a query param on every call.
341
+ params = {"token": token} if token else {}
342
+
343
+ orgs_res = requests.get(
344
+ f"{base}/v1/orgs",
345
+ cert=mtls_cert,
346
+ verify=CA,
347
+ params=params,
348
+ timeout=30,
349
+ )
350
+ orgs_res.raise_for_status()
351
+ return orgs_res.json()
352
+
353
+ if __name__ == "__main__":
354
+ import json, sys
355
+ try:
356
+ print(json.dumps(call_blacksands_service(), indent=2))
357
+ except Exception as e:
358
+ print(f"Failed: {e}", file=sys.stderr)
359
+ sys.exit(1)
360
+ `;
361
+ const CURL_EXAMPLE = `# broker-call.sh — full chain in shell
362
+ #
363
+ # Useful for: debugging, validating that your cert bundle works end-to-end,
364
+ # and prototyping before writing real client code.
365
+
366
+ set -euo pipefail
367
+
368
+ AUTHORIZER_URL="https://mauth-beta.blacksandscyber.online"
369
+ SERVICE_ID="shield-api"
370
+ CERT="\${SHIELD_CLIENT_CERT:?env not set}"
371
+ KEY="\${SHIELD_CLIENT_KEY:?env not set}"
372
+ CA="\${SHIELD_CA_CERT:?env not set}"
373
+ AUTH_PASSWORD="\${SHIELD_AUTH_PASSWORD:?env not set}"
374
+
375
+ # ── STEP 1+2: mTLS to Authorizer, get the service list ────────────────
376
+ AUTH_RESPONSE=$(curl -sf \\
377
+ --cert "$CERT" --key "$KEY" --cacert "$CA" \\
378
+ -H "Content-Type: application/json" \\
379
+ -d "{\\"password\\":\\"$AUTH_PASSWORD\\",\\"serviceId\\":\\"$SERVICE_ID\\"}" \\
380
+ "$AUTHORIZER_URL/api/agent/auth")
381
+
382
+ # Extract the Receiver URL + token (jq makes this readable; use python -c if jq isn't installed)
383
+ RECEIVER_URL=$(echo "$AUTH_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); u=d['service']['serviceUrl']; print(u.split('?')[0].rstrip('/'))")
384
+ TOKEN=$(echo "$AUTH_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); u=d['service']['serviceUrl']; print(u.split('token=')[1] if 'token=' in u else '')")
385
+
386
+ # ── STEP 3+4+5+6: Call the backend through the Receiver ──────────────
387
+ # SAME cert reused — Receiver does its own mTLS. Token goes as a query param.
388
+ curl -sf \\
389
+ --cert "$CERT" --key "$KEY" --cacert "$CA" \\
390
+ "$RECEIVER_URL/v1/orgs?token=$TOKEN" | python3 -m json.tool
391
+ `;
392
+ //# sourceMappingURL=protocol-walkthrough.js.map
@@ -0,0 +1,15 @@
1
+ /** Orchestrate app provisioning via Shield API. */
2
+ import { ShieldClient } from "../client";
3
+ export interface ProvisionResult {
4
+ appId: string;
5
+ manifestId: string;
6
+ operationId: string;
7
+ status: string;
8
+ steps: Array<{
9
+ step: string;
10
+ timestamp: string;
11
+ result?: unknown;
12
+ }>;
13
+ }
14
+ export declare function provisionApp(client: ShieldClient, orgId: string, manifest: unknown, appName: string): Promise<ProvisionResult>;
15
+ //# sourceMappingURL=appProvisioner.d.ts.map
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.provisionApp = provisionApp;
4
+ const logger_1 = require("../../shared/logger");
5
+ async function provisionApp(client, orgId, manifest, appName) {
6
+ const steps = [];
7
+ // Step 1: Register app
8
+ logger_1.logger.info("Step 1: Registering application", { appName, orgId });
9
+ const app = await client.createApp(orgId, appName);
10
+ steps.push({ step: "register_app", timestamp: new Date().toISOString(), result: { appId: app.id } });
11
+ // Step 2: Submit manifest
12
+ logger_1.logger.info("Step 2: Submitting security manifest", { appId: app.id });
13
+ const mf = await client.submitManifest(manifest, orgId);
14
+ steps.push({ step: "submit_manifest", timestamp: new Date().toISOString(), result: { manifestId: mf.id } });
15
+ // Step 3: Provision from manifest
16
+ logger_1.logger.info("Step 3: Provisioning security stack", { manifestId: mf.id });
17
+ const { operationId } = await client.provisionManifest(mf.id);
18
+ steps.push({ step: "provision_initiated", timestamp: new Date().toISOString(), result: { operationId } });
19
+ // Step 4: Poll until complete
20
+ logger_1.logger.info("Step 4: Polling operation", { operationId });
21
+ const op = await client.pollOperation(operationId);
22
+ steps.push({ step: "provision_completed", timestamp: new Date().toISOString(), result: op.result });
23
+ return { appId: app.id, manifestId: mf.id, operationId, status: op.status, steps };
24
+ }
25
+ //# sourceMappingURL=appProvisioner.js.map