@blekline/contracts 0.1.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.
- package/LICENSE +5 -0
- package/README.md +21 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +14 -0
- package/dist/enforce-local.d.ts +9 -0
- package/dist/enforce-local.d.ts.map +1 -0
- package/dist/enforce-local.js +108 -0
- package/dist/enforcement.d.ts +30 -0
- package/dist/enforcement.d.ts.map +1 -0
- package/dist/enforcement.js +7 -0
- package/dist/events.d.ts +39 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +16 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/mask.d.ts +21 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +5 -0
- package/dist/mcp-policy.d.ts +11 -0
- package/dist/mcp-policy.d.ts.map +1 -0
- package/dist/mcp-policy.js +38 -0
- package/dist/policy.d.ts +15 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +1 -0
- package/dist/secret-patterns.d.ts +16 -0
- package/dist/secret-patterns.d.ts.map +1 -0
- package/dist/secret-patterns.js +32 -0
- package/openapi.yaml +248 -0
- package/package.json +33 -0
- package/src/auth.ts +21 -0
- package/src/enforce-local.ts +127 -0
- package/src/enforcement.ts +27 -0
- package/src/events.ts +19 -0
- package/src/index.ts +8 -0
- package/src/mask.ts +31 -0
- package/src/mcp-policy.ts +47 -0
- package/src/policy.ts +17 -0
- package/src/secret-patterns.ts +43 -0
- package/tsconfig.json +14 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @blekline/contracts
|
|
2
|
+
|
|
3
|
+
Shared types, Zod schemas, secret patterns, and local MCP tool enforcement for the Blekline ingress platform.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
- `maskRequestSchema`, `MaskResponse`
|
|
8
|
+
- `eventIngestSchema`, `EventIngest`
|
|
9
|
+
- `enforceToolCallRequestSchema`, `enforceToolCallLocally`
|
|
10
|
+
- `McpToolPolicy`, `resolveMcpToolPolicyDecision`
|
|
11
|
+
- `scanTextForSecrets`, `BLEKLINE_HEADERS`
|
|
12
|
+
|
|
13
|
+
## OpenAPI
|
|
14
|
+
|
|
15
|
+
Machine-readable API spec: [`openapi.yaml`](./openapi.yaml)
|
|
16
|
+
|
|
17
|
+
## Build
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm --filter @blekline/contracts build
|
|
21
|
+
```
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const WORKSPACE_TOOL_SCOPES: readonly ["mask:write", "events:write", "events:read", "integrations:write"];
|
|
2
|
+
export type WorkspaceToolScope = (typeof WORKSPACE_TOOL_SCOPES)[number];
|
|
3
|
+
export declare const BLEKLINE_HEADERS: {
|
|
4
|
+
readonly workspaceToken: "x-blekline-workspace-token";
|
|
5
|
+
readonly workspaceId: "x-blekline-workspace-id";
|
|
6
|
+
readonly requestId: "x-request-id";
|
|
7
|
+
readonly clientSurface: "x-blekline-client-surface";
|
|
8
|
+
readonly modelProvider: "x-blekline-model-provider";
|
|
9
|
+
readonly modelId: "x-blekline-model-id";
|
|
10
|
+
};
|
|
11
|
+
export type ClientSurface = "cursor" | "claude-desktop" | "codex" | "sdk" | "extension" | "unknown";
|
|
12
|
+
export type ModelProvider = "anthropic" | "openai" | "google" | "xai" | "cursor" | "unknown";
|
|
13
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,8EAKxB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE,eAAO,MAAM,gBAAgB;;;;;;;CAOnB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,gBAAgB,GAAG,OAAO,GAAG,KAAK,GAAG,WAAW,GAAG,SAAS,CAAC;AAEpG,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const WORKSPACE_TOOL_SCOPES = [
|
|
2
|
+
"mask:write",
|
|
3
|
+
"events:write",
|
|
4
|
+
"events:read",
|
|
5
|
+
"integrations:write",
|
|
6
|
+
];
|
|
7
|
+
export const BLEKLINE_HEADERS = {
|
|
8
|
+
workspaceToken: "x-blekline-workspace-token",
|
|
9
|
+
workspaceId: "x-blekline-workspace-id",
|
|
10
|
+
requestId: "x-request-id",
|
|
11
|
+
clientSurface: "x-blekline-client-surface",
|
|
12
|
+
modelProvider: "x-blekline-model-provider",
|
|
13
|
+
modelId: "x-blekline-model-id",
|
|
14
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EnforceToolCallResult } from "./enforcement.js";
|
|
2
|
+
import type { McpToolPolicy } from "./mcp-policy.js";
|
|
3
|
+
export declare function enforceToolCallLocally(input: {
|
|
4
|
+
toolName: string;
|
|
5
|
+
arguments: Record<string, unknown>;
|
|
6
|
+
requestId: string;
|
|
7
|
+
mcpToolPolicy?: McpToolPolicy;
|
|
8
|
+
}): EnforceToolCallResult;
|
|
9
|
+
//# sourceMappingURL=enforce-local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforce-local.d.ts","sourceRoot":"","sources":["../src/enforce-local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAsC,MAAM,kBAAkB,CAAC;AAClG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA+DrD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B,GAAG,qBAAqB,CAyDxB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { resolveMcpToolPolicyDecision } from "./mcp-policy.js";
|
|
2
|
+
import { scanTextForSecrets } from "./secret-patterns.js";
|
|
3
|
+
const DESTRUCTIVE_RE = /\brm\s+-rf\b|\bformat\s+c:\b|\bdrop\s+database\b/i;
|
|
4
|
+
function stringifyArgs(args) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.stringify(args);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return String(args);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function maskStringValue(input) {
|
|
13
|
+
const findings = scanTextForSecrets(input);
|
|
14
|
+
if (findings.length === 0)
|
|
15
|
+
return { text: input, count: 0 };
|
|
16
|
+
let out = input;
|
|
17
|
+
let offset = 0;
|
|
18
|
+
let count = 0;
|
|
19
|
+
const sorted = [...findings].sort((a, b) => a.start - b.start);
|
|
20
|
+
for (const f of sorted) {
|
|
21
|
+
const start = f.start + offset;
|
|
22
|
+
const end = f.end + offset;
|
|
23
|
+
const token = `[${f.label}]`;
|
|
24
|
+
out = out.slice(0, start) + token + out.slice(end);
|
|
25
|
+
offset += token.length - (f.end - f.start);
|
|
26
|
+
count += 1;
|
|
27
|
+
}
|
|
28
|
+
return { text: out, count };
|
|
29
|
+
}
|
|
30
|
+
function maskDeep(value, fieldPath, findings) {
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
const scan = scanTextForSecrets(value);
|
|
33
|
+
for (const s of scan) {
|
|
34
|
+
findings.push({ id: s.id, label: s.label, field: fieldPath });
|
|
35
|
+
}
|
|
36
|
+
const masked = maskStringValue(value);
|
|
37
|
+
return { value: masked.text, count: masked.count };
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
let count = 0;
|
|
41
|
+
const next = value.map((item, i) => {
|
|
42
|
+
const r = maskDeep(item, `${fieldPath}[${i}]`, findings);
|
|
43
|
+
count += r.count;
|
|
44
|
+
return r.value;
|
|
45
|
+
});
|
|
46
|
+
return { value: next, count };
|
|
47
|
+
}
|
|
48
|
+
if (value && typeof value === "object") {
|
|
49
|
+
let count = 0;
|
|
50
|
+
const next = {};
|
|
51
|
+
for (const [k, v] of Object.entries(value)) {
|
|
52
|
+
const r = maskDeep(v, fieldPath ? `${fieldPath}.${k}` : k, findings);
|
|
53
|
+
count += r.count;
|
|
54
|
+
next[k] = r.value;
|
|
55
|
+
}
|
|
56
|
+
return { value: next, count };
|
|
57
|
+
}
|
|
58
|
+
return { value, count: 0 };
|
|
59
|
+
}
|
|
60
|
+
export function enforceToolCallLocally(input) {
|
|
61
|
+
const findings = [];
|
|
62
|
+
const blob = stringifyArgs(input.arguments);
|
|
63
|
+
if (DESTRUCTIVE_RE.test(blob)) {
|
|
64
|
+
findings.push({ id: "destructive_command", label: "DESTRUCTIVE", field: "arguments" });
|
|
65
|
+
return {
|
|
66
|
+
action: "block",
|
|
67
|
+
maskedArguments: input.arguments,
|
|
68
|
+
findings,
|
|
69
|
+
entitiesMasked: 0,
|
|
70
|
+
riskTier: "high",
|
|
71
|
+
requestId: input.requestId,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const secretScan = scanTextForSecrets(blob);
|
|
75
|
+
for (const s of secretScan) {
|
|
76
|
+
findings.push({ id: s.id, label: s.label });
|
|
77
|
+
}
|
|
78
|
+
const masked = maskDeep(input.arguments, "", findings);
|
|
79
|
+
const entitiesMasked = masked.count;
|
|
80
|
+
let action = "allow";
|
|
81
|
+
let riskTier = "low";
|
|
82
|
+
const hasHighRiskSecret = secretScan.some((s) => ["aws_access_key", "openai_sk", "openai_sk_proj", "stripe_sk", "jwt", "ssn"].includes(s.id));
|
|
83
|
+
if (hasHighRiskSecret && entitiesMasked > 0) {
|
|
84
|
+
action = "mask";
|
|
85
|
+
riskTier = "high";
|
|
86
|
+
}
|
|
87
|
+
else if (entitiesMasked > 0) {
|
|
88
|
+
action = "mask";
|
|
89
|
+
riskTier = "medium";
|
|
90
|
+
}
|
|
91
|
+
if (input.mcpToolPolicy) {
|
|
92
|
+
action = resolveMcpToolPolicyDecision(input.mcpToolPolicy, input.toolName, action);
|
|
93
|
+
if (action === "block") {
|
|
94
|
+
riskTier = "high";
|
|
95
|
+
if (!findings.some((f) => f.id === "policy_denied")) {
|
|
96
|
+
findings.push({ id: "policy_denied", label: "POLICY", field: "toolName" });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
action,
|
|
102
|
+
maskedArguments: masked.value,
|
|
103
|
+
findings,
|
|
104
|
+
entitiesMasked,
|
|
105
|
+
riskTier,
|
|
106
|
+
requestId: input.requestId,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export type EnforcementAction = "allow" | "mask" | "block";
|
|
3
|
+
export declare const enforceToolCallRequestSchema: z.ZodObject<{
|
|
4
|
+
toolName: z.ZodString;
|
|
5
|
+
arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
6
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
7
|
+
clientSurface: z.ZodOptional<z.ZodEnum<{
|
|
8
|
+
cursor: "cursor";
|
|
9
|
+
"claude-desktop": "claude-desktop";
|
|
10
|
+
codex: "codex";
|
|
11
|
+
sdk: "sdk";
|
|
12
|
+
extension: "extension";
|
|
13
|
+
unknown: "unknown";
|
|
14
|
+
}>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type EnforceToolCallRequest = z.infer<typeof enforceToolCallRequestSchema>;
|
|
17
|
+
export type ToolCallFinding = {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
field?: string;
|
|
21
|
+
};
|
|
22
|
+
export type EnforceToolCallResult = {
|
|
23
|
+
action: EnforcementAction;
|
|
24
|
+
maskedArguments: Record<string, unknown>;
|
|
25
|
+
findings: ToolCallFinding[];
|
|
26
|
+
entitiesMasked: number;
|
|
27
|
+
riskTier: "low" | "medium" | "high";
|
|
28
|
+
requestId: string;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=enforcement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforcement.d.ts","sourceRoot":"","sources":["../src/enforcement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,eAAO,MAAM,4BAA4B;;;;;;;;;;;;iBAKvC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const enforceToolCallRequestSchema = z.object({
|
|
3
|
+
toolName: z.string().min(1).max(120),
|
|
4
|
+
arguments: z.record(z.string(), z.unknown()),
|
|
5
|
+
platform: z.string().max(40).optional(),
|
|
6
|
+
clientSurface: z.enum(["cursor", "claude-desktop", "codex", "sdk", "extension", "unknown"]).optional(),
|
|
7
|
+
});
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const eventIngestSchema: z.ZodObject<{
|
|
3
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
4
|
+
kind: z.ZodOptional<z.ZodString>;
|
|
5
|
+
entitiesMasked: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
riskTier: z.ZodOptional<z.ZodEnum<{
|
|
7
|
+
low: "low";
|
|
8
|
+
medium: "medium";
|
|
9
|
+
high: "high";
|
|
10
|
+
}>>;
|
|
11
|
+
sourceHost: z.ZodOptional<z.ZodString>;
|
|
12
|
+
action: z.ZodOptional<z.ZodString>;
|
|
13
|
+
maskProvider: z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
azure: "azure";
|
|
15
|
+
fallback_local: "fallback_local";
|
|
16
|
+
}>>;
|
|
17
|
+
maskPhase: z.ZodOptional<z.ZodString>;
|
|
18
|
+
clientSurface: z.ZodOptional<z.ZodEnum<{
|
|
19
|
+
cursor: "cursor";
|
|
20
|
+
"claude-desktop": "claude-desktop";
|
|
21
|
+
codex: "codex";
|
|
22
|
+
sdk: "sdk";
|
|
23
|
+
extension: "extension";
|
|
24
|
+
unknown: "unknown";
|
|
25
|
+
}>>;
|
|
26
|
+
modelProvider: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
cursor: "cursor";
|
|
28
|
+
unknown: "unknown";
|
|
29
|
+
anthropic: "anthropic";
|
|
30
|
+
openai: "openai";
|
|
31
|
+
google: "google";
|
|
32
|
+
xai: "xai";
|
|
33
|
+
}>>;
|
|
34
|
+
modelId: z.ZodOptional<z.ZodString>;
|
|
35
|
+
mcpToolName: z.ZodOptional<z.ZodString>;
|
|
36
|
+
downstreamServer: z.ZodOptional<z.ZodString>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
export type EventIngest = z.infer<typeof eventIngestSchema>;
|
|
39
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAc5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const eventIngestSchema = z.object({
|
|
3
|
+
platform: z.string().max(40).optional(),
|
|
4
|
+
kind: z.string().max(48).optional(),
|
|
5
|
+
entitiesMasked: z.number().int().min(0).max(5000).optional(),
|
|
6
|
+
riskTier: z.enum(["low", "medium", "high"]).optional(),
|
|
7
|
+
sourceHost: z.string().max(253).optional(),
|
|
8
|
+
action: z.string().max(48).optional(),
|
|
9
|
+
maskProvider: z.enum(["azure", "fallback_local"]).optional(),
|
|
10
|
+
maskPhase: z.string().max(48).optional(),
|
|
11
|
+
clientSurface: z.enum(["cursor", "claude-desktop", "codex", "sdk", "extension", "unknown"]).optional(),
|
|
12
|
+
modelProvider: z.enum(["anthropic", "openai", "google", "xai", "cursor", "unknown"]).optional(),
|
|
13
|
+
modelId: z.string().max(80).optional(),
|
|
14
|
+
mcpToolName: z.string().max(120).optional(),
|
|
15
|
+
downstreamServer: z.string().max(80).optional(),
|
|
16
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./mcp-policy.js";
|
|
2
|
+
export * from "./auth.js";
|
|
3
|
+
export * from "./mask.js";
|
|
4
|
+
export * from "./events.js";
|
|
5
|
+
export * from "./policy.js";
|
|
6
|
+
export * from "./enforcement.js";
|
|
7
|
+
export * from "./enforce-local.js";
|
|
8
|
+
export * from "./secret-patterns.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./mcp-policy.js";
|
|
2
|
+
export * from "./auth.js";
|
|
3
|
+
export * from "./mask.js";
|
|
4
|
+
export * from "./events.js";
|
|
5
|
+
export * from "./policy.js";
|
|
6
|
+
export * from "./enforcement.js";
|
|
7
|
+
export * from "./enforce-local.js";
|
|
8
|
+
export * from "./secret-patterns.js";
|
package/dist/mask.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const maskRequestSchema: z.ZodObject<{
|
|
3
|
+
text: z.ZodString;
|
|
4
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type MaskRequest = z.infer<typeof maskRequestSchema>;
|
|
7
|
+
export type WireRedactionAction = "mask_and_send" | "mask_and_confirm" | "block_and_review";
|
|
8
|
+
export type MaskResponse = {
|
|
9
|
+
maskedText: string;
|
|
10
|
+
tokenMap: Record<string, string>;
|
|
11
|
+
entitiesMasked: number;
|
|
12
|
+
platform: string;
|
|
13
|
+
provider: "azure" | "fallback_local";
|
|
14
|
+
requestId: string;
|
|
15
|
+
piiLanguage?: string;
|
|
16
|
+
decision?: WireRedactionAction;
|
|
17
|
+
blocked?: boolean;
|
|
18
|
+
blockReason?: string;
|
|
19
|
+
};
|
|
20
|
+
export type MaskErrorCode = "billing_required" | "credits_exhausted" | "prompt_limit_exceeded" | "mask_fallback_blocked" | "high_risk_literal_remaining" | "plan_upgrade_required";
|
|
21
|
+
//# sourceMappingURL=mask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,iBAAiB;;;iBAG5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,MAAM,MAAM,mBAAmB,GAAG,eAAe,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AAE5F,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,kBAAkB,GAClB,mBAAmB,GACnB,uBAAuB,GACvB,uBAAuB,GACvB,6BAA6B,GAC7B,uBAAuB,CAAC"}
|
package/dist/mask.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type McpToolPolicyAction = "allow" | "mask" | "block";
|
|
2
|
+
export type McpToolPolicy = {
|
|
3
|
+
allowedTools: string[];
|
|
4
|
+
deniedTools: string[];
|
|
5
|
+
defaultAction: McpToolPolicyAction;
|
|
6
|
+
};
|
|
7
|
+
export declare const DEFAULT_MCP_TOOL_POLICY: McpToolPolicy;
|
|
8
|
+
export declare function normalizeMcpToolPolicy(raw: unknown): McpToolPolicy;
|
|
9
|
+
/** Resolve workspace MCP tool policy before local/cloud enforcement. */
|
|
10
|
+
export declare function resolveMcpToolPolicyDecision(policy: McpToolPolicy, toolName: string, localAction: McpToolPolicyAction): McpToolPolicyAction;
|
|
11
|
+
//# sourceMappingURL=mcp-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-policy.d.ts","sourceRoot":"","sources":["../src/mcp-policy.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,mBAAmB,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,aAIrC,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAclE;AAED,wEAAwE;AACxE,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,mBAAmB,GAC/B,mBAAmB,CAWrB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const DEFAULT_MCP_TOOL_POLICY = {
|
|
2
|
+
allowedTools: [],
|
|
3
|
+
deniedTools: [],
|
|
4
|
+
defaultAction: "mask",
|
|
5
|
+
};
|
|
6
|
+
export function normalizeMcpToolPolicy(raw) {
|
|
7
|
+
if (!raw || typeof raw !== "object")
|
|
8
|
+
return { ...DEFAULT_MCP_TOOL_POLICY };
|
|
9
|
+
const o = raw;
|
|
10
|
+
const allowedTools = Array.isArray(o.allowedTools)
|
|
11
|
+
? o.allowedTools.filter((t) => typeof t === "string").map((t) => t.trim()).filter(Boolean).slice(0, 64)
|
|
12
|
+
: [];
|
|
13
|
+
const deniedTools = Array.isArray(o.deniedTools)
|
|
14
|
+
? o.deniedTools.filter((t) => typeof t === "string").map((t) => t.trim()).filter(Boolean).slice(0, 64)
|
|
15
|
+
: [];
|
|
16
|
+
const defaultAction = o.defaultAction === "allow" || o.defaultAction === "mask" || o.defaultAction === "block"
|
|
17
|
+
? o.defaultAction
|
|
18
|
+
: "mask";
|
|
19
|
+
return { allowedTools, deniedTools, defaultAction };
|
|
20
|
+
}
|
|
21
|
+
/** Resolve workspace MCP tool policy before local/cloud enforcement. */
|
|
22
|
+
export function resolveMcpToolPolicyDecision(policy, toolName, localAction) {
|
|
23
|
+
const name = toolName.trim().toLowerCase();
|
|
24
|
+
if (policy.deniedTools.some((t) => t.toLowerCase() === name))
|
|
25
|
+
return "block";
|
|
26
|
+
if (policy.allowedTools.length > 0) {
|
|
27
|
+
const allowed = policy.allowedTools.some((t) => t.toLowerCase() === name);
|
|
28
|
+
if (!allowed)
|
|
29
|
+
return "block";
|
|
30
|
+
}
|
|
31
|
+
if (localAction === "block")
|
|
32
|
+
return "block";
|
|
33
|
+
if (policy.defaultAction === "block")
|
|
34
|
+
return "block";
|
|
35
|
+
if (localAction === "mask")
|
|
36
|
+
return "mask";
|
|
37
|
+
return policy.defaultAction;
|
|
38
|
+
}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WireRedactionAction } from "./mask.js";
|
|
2
|
+
export type PromptRiskTier = "low" | "medium" | "high";
|
|
3
|
+
export type PolicySimulation = {
|
|
4
|
+
platform: string;
|
|
5
|
+
risk: PromptRiskTier;
|
|
6
|
+
action: WireRedactionAction;
|
|
7
|
+
matchedKeywords: string[];
|
|
8
|
+
shieldEnabled: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type PolicySimulateRequest = {
|
|
11
|
+
prompt: string;
|
|
12
|
+
platform?: string;
|
|
13
|
+
sourceHost?: string;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC"}
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Portable secret/PII patterns for local fast scan (aligned with webapp detectors). */
|
|
2
|
+
export type SecretPattern = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
pattern: RegExp;
|
|
6
|
+
};
|
|
7
|
+
export declare function buildSecretPatterns(): SecretPattern[];
|
|
8
|
+
export type ScanFinding = {
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
match: string;
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function scanTextForSecrets(text: string): ScanFinding[];
|
|
16
|
+
//# sourceMappingURL=secret-patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-patterns.d.ts","sourceRoot":"","sources":["../src/secret-patterns.ts"],"names":[],"mappings":"AAAA,wFAAwF;AAExF,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3E,wBAAgB,mBAAmB,IAAI,aAAa,EAAE,CAgBrD;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAY9D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Portable secret/PII patterns for local fast scan (aligned with webapp detectors). */
|
|
2
|
+
export function buildSecretPatterns() {
|
|
3
|
+
return [
|
|
4
|
+
{ id: "aws_access_key", label: "AWS_KEY", pattern: /\b(?:AKIA|ASIA|AIDA|AROA|AGPA|AIPA|ANPA|ANVA|APKA|ASCA|ACCA)[A-Z0-9]{16}\b/g },
|
|
5
|
+
{ id: "github_pat", label: "GITHUB", pattern: /\bgh[oprus]_[A-Za-z0-9_]{36,255}\b/g },
|
|
6
|
+
{ id: "github_pat_fine", label: "GITHUB_FINE", pattern: /\bgithub_pat_[A-Za-z0-9_]{80,500}\b/g },
|
|
7
|
+
{ id: "openai_sk", label: "OPENAI", pattern: /\bsk-[A-Za-z0-9]{20,}\b/g },
|
|
8
|
+
{ id: "openai_sk_proj", label: "OPENAI_PROJ", pattern: /\bsk-proj-[A-Za-z0-9_-]{20,500}\b/g },
|
|
9
|
+
{ id: "stripe_sk", label: "STRIPE", pattern: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,128}\b/g },
|
|
10
|
+
{ id: "slack_token", label: "SLACK", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,128}\b/g },
|
|
11
|
+
{ id: "google_api_key", label: "GOOGLE_KEY", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
12
|
+
{ id: "jwt", label: "JWT", pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
|
|
13
|
+
{ id: "email", label: "EMAIL", pattern: /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi },
|
|
14
|
+
{ id: "ssn", label: "SSN", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
15
|
+
{ id: "iban", label: "IBAN", pattern: /\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]){11,30}\b/gi },
|
|
16
|
+
{ id: "card", label: "CARD", pattern: /\b(?:\d[ -]?){12,19}\b/g },
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
export function scanTextForSecrets(text) {
|
|
20
|
+
const findings = [];
|
|
21
|
+
for (const { id, label, pattern } of buildSecretPatterns()) {
|
|
22
|
+
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
23
|
+
let m;
|
|
24
|
+
while ((m = re.exec(text)) !== null) {
|
|
25
|
+
const match = m[0];
|
|
26
|
+
if (match.includes("[") && match.includes("]"))
|
|
27
|
+
continue;
|
|
28
|
+
findings.push({ id, label, match, start: m.index, end: m.index + match.length });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return findings;
|
|
32
|
+
}
|
package/openapi.yaml
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
openapi: 3.1.0
|
|
2
|
+
info:
|
|
3
|
+
title: Blekline Ingress Control Plane API
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: |
|
|
6
|
+
Enterprise ingress governance for prompts, events, MCP tool calls, and universal LLM proxies.
|
|
7
|
+
servers:
|
|
8
|
+
- url: https://app.blekline.com
|
|
9
|
+
- url: http://localhost:3000
|
|
10
|
+
security:
|
|
11
|
+
- workspaceToken: []
|
|
12
|
+
components:
|
|
13
|
+
securitySchemes:
|
|
14
|
+
workspaceToken:
|
|
15
|
+
type: apiKey
|
|
16
|
+
in: header
|
|
17
|
+
name: x-blekline-workspace-token
|
|
18
|
+
schemas:
|
|
19
|
+
MaskRequest:
|
|
20
|
+
type: object
|
|
21
|
+
required: [text]
|
|
22
|
+
properties:
|
|
23
|
+
text:
|
|
24
|
+
type: string
|
|
25
|
+
platform:
|
|
26
|
+
type: string
|
|
27
|
+
MaskResponse:
|
|
28
|
+
type: object
|
|
29
|
+
properties:
|
|
30
|
+
maskedText:
|
|
31
|
+
type: string
|
|
32
|
+
entitiesMasked:
|
|
33
|
+
type: integer
|
|
34
|
+
decision:
|
|
35
|
+
type: string
|
|
36
|
+
enum: [mask_and_send, mask_and_confirm, block_and_review]
|
|
37
|
+
provider:
|
|
38
|
+
type: string
|
|
39
|
+
requestId:
|
|
40
|
+
type: string
|
|
41
|
+
latencyMs:
|
|
42
|
+
type: number
|
|
43
|
+
description: Total request latency in milliseconds
|
|
44
|
+
maskPath:
|
|
45
|
+
type: string
|
|
46
|
+
enum: [azure_first, local_first, local_only]
|
|
47
|
+
region:
|
|
48
|
+
type: string
|
|
49
|
+
description: Ingress region label (x-blekline-ingress-region)
|
|
50
|
+
EnforceToolCallRequest:
|
|
51
|
+
type: object
|
|
52
|
+
required: [toolName, arguments]
|
|
53
|
+
properties:
|
|
54
|
+
toolName:
|
|
55
|
+
type: string
|
|
56
|
+
arguments:
|
|
57
|
+
type: object
|
|
58
|
+
platform:
|
|
59
|
+
type: string
|
|
60
|
+
clientSurface:
|
|
61
|
+
type: string
|
|
62
|
+
enum: [cursor, claude-desktop, codex, sdk, extension, unknown]
|
|
63
|
+
EnforceToolCallResult:
|
|
64
|
+
type: object
|
|
65
|
+
properties:
|
|
66
|
+
action:
|
|
67
|
+
type: string
|
|
68
|
+
enum: [allow, mask, block]
|
|
69
|
+
maskedArguments:
|
|
70
|
+
type: object
|
|
71
|
+
entitiesMasked:
|
|
72
|
+
type: integer
|
|
73
|
+
riskTier:
|
|
74
|
+
type: string
|
|
75
|
+
enum: [low, medium, high]
|
|
76
|
+
requestId:
|
|
77
|
+
type: string
|
|
78
|
+
McpToolPolicy:
|
|
79
|
+
type: object
|
|
80
|
+
properties:
|
|
81
|
+
allowedTools:
|
|
82
|
+
type: array
|
|
83
|
+
items:
|
|
84
|
+
type: string
|
|
85
|
+
deniedTools:
|
|
86
|
+
type: array
|
|
87
|
+
items:
|
|
88
|
+
type: string
|
|
89
|
+
defaultAction:
|
|
90
|
+
type: string
|
|
91
|
+
enum: [allow, mask, block]
|
|
92
|
+
paths:
|
|
93
|
+
/api/mask:
|
|
94
|
+
post:
|
|
95
|
+
summary: Mask prompt text
|
|
96
|
+
operationId: maskPrompt
|
|
97
|
+
requestBody:
|
|
98
|
+
required: true
|
|
99
|
+
content:
|
|
100
|
+
application/json:
|
|
101
|
+
schema:
|
|
102
|
+
$ref: "#/components/schemas/MaskRequest"
|
|
103
|
+
responses:
|
|
104
|
+
"200":
|
|
105
|
+
description: Masked prompt
|
|
106
|
+
content:
|
|
107
|
+
application/json:
|
|
108
|
+
schema:
|
|
109
|
+
$ref: "#/components/schemas/MaskResponse"
|
|
110
|
+
/api/events:
|
|
111
|
+
post:
|
|
112
|
+
summary: Emit governance event (metadata-only)
|
|
113
|
+
operationId: emitEvent
|
|
114
|
+
requestBody:
|
|
115
|
+
required: true
|
|
116
|
+
content:
|
|
117
|
+
application/json:
|
|
118
|
+
schema:
|
|
119
|
+
type: object
|
|
120
|
+
required: [kind, platform]
|
|
121
|
+
properties:
|
|
122
|
+
kind:
|
|
123
|
+
type: string
|
|
124
|
+
platform:
|
|
125
|
+
type: string
|
|
126
|
+
entitiesMasked:
|
|
127
|
+
type: integer
|
|
128
|
+
riskTier:
|
|
129
|
+
type: string
|
|
130
|
+
action:
|
|
131
|
+
type: string
|
|
132
|
+
clientSurface:
|
|
133
|
+
type: string
|
|
134
|
+
modelProvider:
|
|
135
|
+
type: string
|
|
136
|
+
modelId:
|
|
137
|
+
type: string
|
|
138
|
+
responses:
|
|
139
|
+
"200":
|
|
140
|
+
description: Event accepted
|
|
141
|
+
/api/mcp/enforce-tool-call:
|
|
142
|
+
post:
|
|
143
|
+
summary: Enforce MCP tool call policy
|
|
144
|
+
operationId: enforceToolCall
|
|
145
|
+
requestBody:
|
|
146
|
+
required: true
|
|
147
|
+
content:
|
|
148
|
+
application/json:
|
|
149
|
+
schema:
|
|
150
|
+
$ref: "#/components/schemas/EnforceToolCallRequest"
|
|
151
|
+
responses:
|
|
152
|
+
"200":
|
|
153
|
+
description: Enforcement decision
|
|
154
|
+
content:
|
|
155
|
+
application/json:
|
|
156
|
+
schema:
|
|
157
|
+
$ref: "#/components/schemas/EnforceToolCallResult"
|
|
158
|
+
/api/policy/simulate:
|
|
159
|
+
post:
|
|
160
|
+
summary: Simulate redaction policy on a prompt
|
|
161
|
+
operationId: simulatePolicy
|
|
162
|
+
requestBody:
|
|
163
|
+
required: true
|
|
164
|
+
content:
|
|
165
|
+
application/json:
|
|
166
|
+
schema:
|
|
167
|
+
type: object
|
|
168
|
+
required: [prompt]
|
|
169
|
+
properties:
|
|
170
|
+
prompt:
|
|
171
|
+
type: string
|
|
172
|
+
platform:
|
|
173
|
+
type: string
|
|
174
|
+
sourceHost:
|
|
175
|
+
type: string
|
|
176
|
+
responses:
|
|
177
|
+
"200":
|
|
178
|
+
description: Simulation result
|
|
179
|
+
/api/workspace/mcp-policy:
|
|
180
|
+
get:
|
|
181
|
+
summary: Read workspace MCP tool policy
|
|
182
|
+
operationId: getMcpToolPolicy
|
|
183
|
+
responses:
|
|
184
|
+
"200":
|
|
185
|
+
description: Current policy
|
|
186
|
+
content:
|
|
187
|
+
application/json:
|
|
188
|
+
schema:
|
|
189
|
+
type: object
|
|
190
|
+
properties:
|
|
191
|
+
mcpToolPolicy:
|
|
192
|
+
$ref: "#/components/schemas/McpToolPolicy"
|
|
193
|
+
post:
|
|
194
|
+
summary: Update workspace MCP tool policy
|
|
195
|
+
operationId: updateMcpToolPolicy
|
|
196
|
+
requestBody:
|
|
197
|
+
required: true
|
|
198
|
+
content:
|
|
199
|
+
application/json:
|
|
200
|
+
schema:
|
|
201
|
+
$ref: "#/components/schemas/McpToolPolicy"
|
|
202
|
+
responses:
|
|
203
|
+
"200":
|
|
204
|
+
description: Updated policy
|
|
205
|
+
/api/ingress/v1/chat/completions:
|
|
206
|
+
post:
|
|
207
|
+
summary: OpenAI-compatible ingress proxy (masks user messages)
|
|
208
|
+
operationId: ingressOpenAiChatCompletions
|
|
209
|
+
requestBody:
|
|
210
|
+
required: true
|
|
211
|
+
content:
|
|
212
|
+
application/json:
|
|
213
|
+
schema:
|
|
214
|
+
type: object
|
|
215
|
+
properties:
|
|
216
|
+
model:
|
|
217
|
+
type: string
|
|
218
|
+
messages:
|
|
219
|
+
type: array
|
|
220
|
+
items:
|
|
221
|
+
type: object
|
|
222
|
+
responses:
|
|
223
|
+
"200":
|
|
224
|
+
description: Upstream completion response
|
|
225
|
+
"403":
|
|
226
|
+
description: Blocked by ingress policy
|
|
227
|
+
/api/ingress/v1/messages:
|
|
228
|
+
post:
|
|
229
|
+
summary: Anthropic Messages API ingress proxy
|
|
230
|
+
operationId: ingressAnthropicMessages
|
|
231
|
+
requestBody:
|
|
232
|
+
required: true
|
|
233
|
+
content:
|
|
234
|
+
application/json:
|
|
235
|
+
schema:
|
|
236
|
+
type: object
|
|
237
|
+
properties:
|
|
238
|
+
model:
|
|
239
|
+
type: string
|
|
240
|
+
messages:
|
|
241
|
+
type: array
|
|
242
|
+
items:
|
|
243
|
+
type: object
|
|
244
|
+
responses:
|
|
245
|
+
"200":
|
|
246
|
+
description: Upstream message response
|
|
247
|
+
"403":
|
|
248
|
+
description: Blocked by ingress policy
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blekline/contracts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"private": false,
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Blekline/blekline-oss.git",
|
|
9
|
+
"directory": "packages/contracts"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.7.3"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"zod": "^4.3.6"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const WORKSPACE_TOOL_SCOPES = [
|
|
2
|
+
"mask:write",
|
|
3
|
+
"events:write",
|
|
4
|
+
"events:read",
|
|
5
|
+
"integrations:write",
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
export type WorkspaceToolScope = (typeof WORKSPACE_TOOL_SCOPES)[number];
|
|
9
|
+
|
|
10
|
+
export const BLEKLINE_HEADERS = {
|
|
11
|
+
workspaceToken: "x-blekline-workspace-token",
|
|
12
|
+
workspaceId: "x-blekline-workspace-id",
|
|
13
|
+
requestId: "x-request-id",
|
|
14
|
+
clientSurface: "x-blekline-client-surface",
|
|
15
|
+
modelProvider: "x-blekline-model-provider",
|
|
16
|
+
modelId: "x-blekline-model-id",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type ClientSurface = "cursor" | "claude-desktop" | "codex" | "sdk" | "extension" | "unknown";
|
|
20
|
+
|
|
21
|
+
export type ModelProvider = "anthropic" | "openai" | "google" | "xai" | "cursor" | "unknown";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { EnforceToolCallResult, EnforcementAction, ToolCallFinding } from "./enforcement.js";
|
|
2
|
+
import type { McpToolPolicy } from "./mcp-policy.js";
|
|
3
|
+
import { resolveMcpToolPolicyDecision } from "./mcp-policy.js";
|
|
4
|
+
import { scanTextForSecrets } from "./secret-patterns.js";
|
|
5
|
+
|
|
6
|
+
const DESTRUCTIVE_RE = /\brm\s+-rf\b|\bformat\s+c:\b|\bdrop\s+database\b/i;
|
|
7
|
+
|
|
8
|
+
function stringifyArgs(args: Record<string, unknown>): string {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify(args);
|
|
11
|
+
} catch {
|
|
12
|
+
return String(args);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function maskStringValue(input: string): { text: string; count: number } {
|
|
17
|
+
const findings = scanTextForSecrets(input);
|
|
18
|
+
if (findings.length === 0) return { text: input, count: 0 };
|
|
19
|
+
let out = input;
|
|
20
|
+
let offset = 0;
|
|
21
|
+
let count = 0;
|
|
22
|
+
const sorted = [...findings].sort((a, b) => a.start - b.start);
|
|
23
|
+
for (const f of sorted) {
|
|
24
|
+
const start = f.start + offset;
|
|
25
|
+
const end = f.end + offset;
|
|
26
|
+
const token = `[${f.label}]`;
|
|
27
|
+
out = out.slice(0, start) + token + out.slice(end);
|
|
28
|
+
offset += token.length - (f.end - f.start);
|
|
29
|
+
count += 1;
|
|
30
|
+
}
|
|
31
|
+
return { text: out, count };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function maskDeep(value: unknown, fieldPath: string, findings: ToolCallFinding[]): { value: unknown; count: number } {
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
const scan = scanTextForSecrets(value);
|
|
37
|
+
for (const s of scan) {
|
|
38
|
+
findings.push({ id: s.id, label: s.label, field: fieldPath });
|
|
39
|
+
}
|
|
40
|
+
const masked = maskStringValue(value);
|
|
41
|
+
return { value: masked.text, count: masked.count };
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
let count = 0;
|
|
45
|
+
const next = value.map((item, i) => {
|
|
46
|
+
const r = maskDeep(item, `${fieldPath}[${i}]`, findings);
|
|
47
|
+
count += r.count;
|
|
48
|
+
return r.value;
|
|
49
|
+
});
|
|
50
|
+
return { value: next, count };
|
|
51
|
+
}
|
|
52
|
+
if (value && typeof value === "object") {
|
|
53
|
+
let count = 0;
|
|
54
|
+
const next: Record<string, unknown> = {};
|
|
55
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
56
|
+
const r = maskDeep(v, fieldPath ? `${fieldPath}.${k}` : k, findings);
|
|
57
|
+
count += r.count;
|
|
58
|
+
next[k] = r.value;
|
|
59
|
+
}
|
|
60
|
+
return { value: next, count };
|
|
61
|
+
}
|
|
62
|
+
return { value, count: 0 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function enforceToolCallLocally(input: {
|
|
66
|
+
toolName: string;
|
|
67
|
+
arguments: Record<string, unknown>;
|
|
68
|
+
requestId: string;
|
|
69
|
+
mcpToolPolicy?: McpToolPolicy;
|
|
70
|
+
}): EnforceToolCallResult {
|
|
71
|
+
const findings: ToolCallFinding[] = [];
|
|
72
|
+
const blob = stringifyArgs(input.arguments);
|
|
73
|
+
|
|
74
|
+
if (DESTRUCTIVE_RE.test(blob)) {
|
|
75
|
+
findings.push({ id: "destructive_command", label: "DESTRUCTIVE", field: "arguments" });
|
|
76
|
+
return {
|
|
77
|
+
action: "block",
|
|
78
|
+
maskedArguments: input.arguments,
|
|
79
|
+
findings,
|
|
80
|
+
entitiesMasked: 0,
|
|
81
|
+
riskTier: "high",
|
|
82
|
+
requestId: input.requestId,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const secretScan = scanTextForSecrets(blob);
|
|
87
|
+
for (const s of secretScan) {
|
|
88
|
+
findings.push({ id: s.id, label: s.label });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const masked = maskDeep(input.arguments, "", findings);
|
|
92
|
+
const entitiesMasked = masked.count;
|
|
93
|
+
|
|
94
|
+
let action: EnforcementAction = "allow";
|
|
95
|
+
let riskTier: "low" | "medium" | "high" = "low";
|
|
96
|
+
|
|
97
|
+
const hasHighRiskSecret = secretScan.some((s) =>
|
|
98
|
+
["aws_access_key", "openai_sk", "openai_sk_proj", "stripe_sk", "jwt", "ssn"].includes(s.id)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (hasHighRiskSecret && entitiesMasked > 0) {
|
|
102
|
+
action = "mask";
|
|
103
|
+
riskTier = "high";
|
|
104
|
+
} else if (entitiesMasked > 0) {
|
|
105
|
+
action = "mask";
|
|
106
|
+
riskTier = "medium";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (input.mcpToolPolicy) {
|
|
110
|
+
action = resolveMcpToolPolicyDecision(input.mcpToolPolicy, input.toolName, action);
|
|
111
|
+
if (action === "block") {
|
|
112
|
+
riskTier = "high";
|
|
113
|
+
if (!findings.some((f) => f.id === "policy_denied")) {
|
|
114
|
+
findings.push({ id: "policy_denied", label: "POLICY", field: "toolName" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
action,
|
|
121
|
+
maskedArguments: masked.value as Record<string, unknown>,
|
|
122
|
+
findings,
|
|
123
|
+
entitiesMasked,
|
|
124
|
+
riskTier,
|
|
125
|
+
requestId: input.requestId,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export type EnforcementAction = "allow" | "mask" | "block";
|
|
4
|
+
|
|
5
|
+
export const enforceToolCallRequestSchema = z.object({
|
|
6
|
+
toolName: z.string().min(1).max(120),
|
|
7
|
+
arguments: z.record(z.string(), z.unknown()),
|
|
8
|
+
platform: z.string().max(40).optional(),
|
|
9
|
+
clientSurface: z.enum(["cursor", "claude-desktop", "codex", "sdk", "extension", "unknown"]).optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type EnforceToolCallRequest = z.infer<typeof enforceToolCallRequestSchema>;
|
|
13
|
+
|
|
14
|
+
export type ToolCallFinding = {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
field?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type EnforceToolCallResult = {
|
|
21
|
+
action: EnforcementAction;
|
|
22
|
+
maskedArguments: Record<string, unknown>;
|
|
23
|
+
findings: ToolCallFinding[];
|
|
24
|
+
entitiesMasked: number;
|
|
25
|
+
riskTier: "low" | "medium" | "high";
|
|
26
|
+
requestId: string;
|
|
27
|
+
};
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const eventIngestSchema = z.object({
|
|
4
|
+
platform: z.string().max(40).optional(),
|
|
5
|
+
kind: z.string().max(48).optional(),
|
|
6
|
+
entitiesMasked: z.number().int().min(0).max(5000).optional(),
|
|
7
|
+
riskTier: z.enum(["low", "medium", "high"]).optional(),
|
|
8
|
+
sourceHost: z.string().max(253).optional(),
|
|
9
|
+
action: z.string().max(48).optional(),
|
|
10
|
+
maskProvider: z.enum(["azure", "fallback_local"]).optional(),
|
|
11
|
+
maskPhase: z.string().max(48).optional(),
|
|
12
|
+
clientSurface: z.enum(["cursor", "claude-desktop", "codex", "sdk", "extension", "unknown"]).optional(),
|
|
13
|
+
modelProvider: z.enum(["anthropic", "openai", "google", "xai", "cursor", "unknown"]).optional(),
|
|
14
|
+
modelId: z.string().max(80).optional(),
|
|
15
|
+
mcpToolName: z.string().max(120).optional(),
|
|
16
|
+
downstreamServer: z.string().max(80).optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type EventIngest = z.infer<typeof eventIngestSchema>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./mcp-policy.js";
|
|
2
|
+
export * from "./auth.js";
|
|
3
|
+
export * from "./mask.js";
|
|
4
|
+
export * from "./events.js";
|
|
5
|
+
export * from "./policy.js";
|
|
6
|
+
export * from "./enforcement.js";
|
|
7
|
+
export * from "./enforce-local.js";
|
|
8
|
+
export * from "./secret-patterns.js";
|
package/src/mask.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const maskRequestSchema = z.object({
|
|
4
|
+
text: z.string().min(1),
|
|
5
|
+
platform: z.string().max(40).optional(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export type MaskRequest = z.infer<typeof maskRequestSchema>;
|
|
9
|
+
|
|
10
|
+
export type WireRedactionAction = "mask_and_send" | "mask_and_confirm" | "block_and_review";
|
|
11
|
+
|
|
12
|
+
export type MaskResponse = {
|
|
13
|
+
maskedText: string;
|
|
14
|
+
tokenMap: Record<string, string>;
|
|
15
|
+
entitiesMasked: number;
|
|
16
|
+
platform: string;
|
|
17
|
+
provider: "azure" | "fallback_local";
|
|
18
|
+
requestId: string;
|
|
19
|
+
piiLanguage?: string;
|
|
20
|
+
decision?: WireRedactionAction;
|
|
21
|
+
blocked?: boolean;
|
|
22
|
+
blockReason?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type MaskErrorCode =
|
|
26
|
+
| "billing_required"
|
|
27
|
+
| "credits_exhausted"
|
|
28
|
+
| "prompt_limit_exceeded"
|
|
29
|
+
| "mask_fallback_blocked"
|
|
30
|
+
| "high_risk_literal_remaining"
|
|
31
|
+
| "plan_upgrade_required";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type McpToolPolicyAction = "allow" | "mask" | "block";
|
|
2
|
+
|
|
3
|
+
export type McpToolPolicy = {
|
|
4
|
+
allowedTools: string[];
|
|
5
|
+
deniedTools: string[];
|
|
6
|
+
defaultAction: McpToolPolicyAction;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_MCP_TOOL_POLICY: McpToolPolicy = {
|
|
10
|
+
allowedTools: [],
|
|
11
|
+
deniedTools: [],
|
|
12
|
+
defaultAction: "mask",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function normalizeMcpToolPolicy(raw: unknown): McpToolPolicy {
|
|
16
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULT_MCP_TOOL_POLICY };
|
|
17
|
+
const o = raw as Record<string, unknown>;
|
|
18
|
+
const allowedTools = Array.isArray(o.allowedTools)
|
|
19
|
+
? o.allowedTools.filter((t): t is string => typeof t === "string").map((t) => t.trim()).filter(Boolean).slice(0, 64)
|
|
20
|
+
: [];
|
|
21
|
+
const deniedTools = Array.isArray(o.deniedTools)
|
|
22
|
+
? o.deniedTools.filter((t): t is string => typeof t === "string").map((t) => t.trim()).filter(Boolean).slice(0, 64)
|
|
23
|
+
: [];
|
|
24
|
+
const defaultAction =
|
|
25
|
+
o.defaultAction === "allow" || o.defaultAction === "mask" || o.defaultAction === "block"
|
|
26
|
+
? o.defaultAction
|
|
27
|
+
: "mask";
|
|
28
|
+
return { allowedTools, deniedTools, defaultAction };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolve workspace MCP tool policy before local/cloud enforcement. */
|
|
32
|
+
export function resolveMcpToolPolicyDecision(
|
|
33
|
+
policy: McpToolPolicy,
|
|
34
|
+
toolName: string,
|
|
35
|
+
localAction: McpToolPolicyAction
|
|
36
|
+
): McpToolPolicyAction {
|
|
37
|
+
const name = toolName.trim().toLowerCase();
|
|
38
|
+
if (policy.deniedTools.some((t) => t.toLowerCase() === name)) return "block";
|
|
39
|
+
if (policy.allowedTools.length > 0) {
|
|
40
|
+
const allowed = policy.allowedTools.some((t) => t.toLowerCase() === name);
|
|
41
|
+
if (!allowed) return "block";
|
|
42
|
+
}
|
|
43
|
+
if (localAction === "block") return "block";
|
|
44
|
+
if (policy.defaultAction === "block") return "block";
|
|
45
|
+
if (localAction === "mask") return "mask";
|
|
46
|
+
return policy.defaultAction;
|
|
47
|
+
}
|
package/src/policy.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WireRedactionAction } from "./mask.js";
|
|
2
|
+
|
|
3
|
+
export type PromptRiskTier = "low" | "medium" | "high";
|
|
4
|
+
|
|
5
|
+
export type PolicySimulation = {
|
|
6
|
+
platform: string;
|
|
7
|
+
risk: PromptRiskTier;
|
|
8
|
+
action: WireRedactionAction;
|
|
9
|
+
matchedKeywords: string[];
|
|
10
|
+
shieldEnabled: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type PolicySimulateRequest = {
|
|
14
|
+
prompt: string;
|
|
15
|
+
platform?: string;
|
|
16
|
+
sourceHost?: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Portable secret/PII patterns for local fast scan (aligned with webapp detectors). */
|
|
2
|
+
|
|
3
|
+
export type SecretPattern = { id: string; label: string; pattern: RegExp };
|
|
4
|
+
|
|
5
|
+
export function buildSecretPatterns(): SecretPattern[] {
|
|
6
|
+
return [
|
|
7
|
+
{ id: "aws_access_key", label: "AWS_KEY", pattern: /\b(?:AKIA|ASIA|AIDA|AROA|AGPA|AIPA|ANPA|ANVA|APKA|ASCA|ACCA)[A-Z0-9]{16}\b/g },
|
|
8
|
+
{ id: "github_pat", label: "GITHUB", pattern: /\bgh[oprus]_[A-Za-z0-9_]{36,255}\b/g },
|
|
9
|
+
{ id: "github_pat_fine", label: "GITHUB_FINE", pattern: /\bgithub_pat_[A-Za-z0-9_]{80,500}\b/g },
|
|
10
|
+
{ id: "openai_sk", label: "OPENAI", pattern: /\bsk-[A-Za-z0-9]{20,}\b/g },
|
|
11
|
+
{ id: "openai_sk_proj", label: "OPENAI_PROJ", pattern: /\bsk-proj-[A-Za-z0-9_-]{20,500}\b/g },
|
|
12
|
+
{ id: "stripe_sk", label: "STRIPE", pattern: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,128}\b/g },
|
|
13
|
+
{ id: "slack_token", label: "SLACK", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,128}\b/g },
|
|
14
|
+
{ id: "google_api_key", label: "GOOGLE_KEY", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
15
|
+
{ id: "jwt", label: "JWT", pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
|
|
16
|
+
{ id: "email", label: "EMAIL", pattern: /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi },
|
|
17
|
+
{ id: "ssn", label: "SSN", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
18
|
+
{ id: "iban", label: "IBAN", pattern: /\b[A-Z]{2}\d{2}(?:[ -]?[A-Z0-9]){11,30}\b/gi },
|
|
19
|
+
{ id: "card", label: "CARD", pattern: /\b(?:\d[ -]?){12,19}\b/g },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ScanFinding = {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
match: string;
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function scanTextForSecrets(text: string): ScanFinding[] {
|
|
32
|
+
const findings: ScanFinding[] = [];
|
|
33
|
+
for (const { id, label, pattern } of buildSecretPatterns()) {
|
|
34
|
+
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
35
|
+
let m: RegExpExecArray | null;
|
|
36
|
+
while ((m = re.exec(text)) !== null) {
|
|
37
|
+
const match = m[0];
|
|
38
|
+
if (match.includes("[") && match.includes("]")) continue;
|
|
39
|
+
findings.push({ id, label, match, start: m.index, end: m.index + match.length });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return findings;
|
|
43
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|