@highflame/policy 2.1.23 → 2.1.25
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/_schemas/ai_gateway/schema.cedarschema +70 -0
- package/_schemas/guardrails/context.json +1 -1
- package/_schemas/guardrails/schema.cedarschema +75 -3
- package/_schemas/mcp_gateway/schema.cedarschema +70 -0
- package/_schemas/sentry/templates/defaults/clipboard.cedar +76 -0
- package/_schemas/sentry/templates/defaults/file_safety.cedar +7 -7
- package/_schemas/sentry/templates/defaults/organization.cedar +10 -159
- package/_schemas/sentry/templates/defaults/pii.cedar +0 -32
- package/_schemas/sentry/templates/defaults/secrets.cedar +155 -0
- package/_schemas/sentry/templates/templates.json +38 -12
- package/dist/engine.d.ts +37 -0
- package/dist/engine.js +56 -0
- package/dist/sentry-defaults.gen.d.ts +1 -1
- package/dist/sentry-defaults.gen.js +284 -188
- package/dist/service-schemas.gen.d.ts +2 -2
- package/dist/service-schemas.gen.js +146 -4
- package/package.json +1 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Secrets Detection Policy (Default)
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Block credential and secret leakage across messages and AI responses.
|
|
5
|
+
// Shield SecretsDetector identifies 18+ secret types via regex.
|
|
6
|
+
//
|
|
7
|
+
// Paste-targeted secret rules live in clipboard.cedar; this file covers
|
|
8
|
+
// non-paste channels (messages, responses, and cross-cutting rules).
|
|
9
|
+
//
|
|
10
|
+
// Category: secrets
|
|
11
|
+
// Namespace: Sentry
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
// Block messages containing secrets
|
|
15
|
+
@id("sentry-org-block-secrets-messages")
|
|
16
|
+
@name("Block messages with secrets")
|
|
17
|
+
@description("Block messages when detection engines identify API keys, tokens, or credential patterns. First line of defense against accidental credential exposure in AI chat interactions.")
|
|
18
|
+
@severity("critical")
|
|
19
|
+
@tags("secrets,credentials,messages,nist-sc-28,nist-ia-5")
|
|
20
|
+
@reject_message("Your message was blocked because it contains detected secrets such as API keys, tokens, or credentials. Remove all secrets before sending to AI services.")
|
|
21
|
+
forbid (
|
|
22
|
+
principal,
|
|
23
|
+
action == Sentry::Action::"send_message",
|
|
24
|
+
resource
|
|
25
|
+
)
|
|
26
|
+
when {
|
|
27
|
+
context has contains_secrets && context.contains_secrets
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Block high-risk secret types across all actions
|
|
31
|
+
@id("sentry-org-block-high-risk-secrets")
|
|
32
|
+
@name("Block high-risk credential types")
|
|
33
|
+
@description("Block content containing cloud provider keys (AWS, GCP, Azure), GitHub tokens, SSH private keys, or database connection strings across all actions. These credential types pose the highest exfiltration risk.")
|
|
34
|
+
@severity("critical")
|
|
35
|
+
@tags("secrets,aws,github,ssh,cloud,nist-ia-5,mitre-t1552")
|
|
36
|
+
@reject_message("Content blocked: high-risk credentials detected (cloud keys, GitHub tokens, SSH keys). Use a secrets manager — never share credentials with AI services.")
|
|
37
|
+
forbid (
|
|
38
|
+
principal,
|
|
39
|
+
action,
|
|
40
|
+
resource
|
|
41
|
+
)
|
|
42
|
+
when {
|
|
43
|
+
context has secret_types &&
|
|
44
|
+
(context.secret_types.contains("aws_access_key") ||
|
|
45
|
+
context.secret_types.contains("aws_secret_key") ||
|
|
46
|
+
context.secret_types.contains("gcp_service_account") ||
|
|
47
|
+
context.secret_types.contains("azure_connection_string") ||
|
|
48
|
+
context.secret_types.contains("github_token") ||
|
|
49
|
+
context.secret_types.contains("github_fine_grained") ||
|
|
50
|
+
context.secret_types.contains("private_key"))
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Block API keys and tokens across all actions
|
|
54
|
+
@id("sentry-org-block-api-keys")
|
|
55
|
+
@name("Block API keys and tokens")
|
|
56
|
+
@description("Block content containing generic API keys, JWT tokens, and OAuth credentials. These are the most commonly leaked credential types when users interact with AI services.")
|
|
57
|
+
@severity("high")
|
|
58
|
+
@tags("secrets,api-key,jwt,oauth,nist-ia-5")
|
|
59
|
+
@reject_message("Content blocked: API keys, JWT tokens, or OAuth credentials detected. These must never be shared with AI services.")
|
|
60
|
+
forbid (
|
|
61
|
+
principal,
|
|
62
|
+
action,
|
|
63
|
+
resource
|
|
64
|
+
)
|
|
65
|
+
when {
|
|
66
|
+
context has secret_types &&
|
|
67
|
+
(context.secret_types.contains("generic_api_key") ||
|
|
68
|
+
context.secret_types.contains("jwt_token") ||
|
|
69
|
+
context.secret_types.contains("openai_key") ||
|
|
70
|
+
context.secret_types.contains("anthropic_key") ||
|
|
71
|
+
context.secret_types.contains("stripe_key"))
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Block SSH key exposure across messages, paste, and file uploads
|
|
75
|
+
@id("sentry-secrets-block-ssh-keys")
|
|
76
|
+
@name("Block SSH key exposure")
|
|
77
|
+
@description("Block when SSH private key content or SSH key file paths are detected. Covers messages, paste, and file uploads. AI chat services must not receive SSH credentials.")
|
|
78
|
+
@severity("critical")
|
|
79
|
+
@tags("secrets,ssh,credentials,nist-ia-5,mitre-t1552")
|
|
80
|
+
@reject_message("Blocked: SSH private key content or key file path detected. AI chat services must not receive SSH credentials.")
|
|
81
|
+
forbid (
|
|
82
|
+
principal,
|
|
83
|
+
action in [Sentry::Action::"send_message", Sentry::Action::"paste_content", Sentry::Action::"upload_file"],
|
|
84
|
+
resource
|
|
85
|
+
)
|
|
86
|
+
when {
|
|
87
|
+
context has secret_types && context.secret_types.contains("ssh_key")
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Block PEM/certificate key exposure across messages, paste, and file uploads
|
|
91
|
+
@id("sentry-secrets-block-pem-keys")
|
|
92
|
+
@name("Block PEM/certificate key exposure")
|
|
93
|
+
@description("Block when PEM private key content or certificate key file paths (.pem, .key, .p12, .pfx) are detected. AI chat services must not receive certificate credentials.")
|
|
94
|
+
@severity("critical")
|
|
95
|
+
@tags("secrets,certificates,pem,nist-ia-5,mitre-t1552")
|
|
96
|
+
@reject_message("Blocked: PEM private key or certificate key file detected. AI chat services must not receive certificate credentials.")
|
|
97
|
+
forbid (
|
|
98
|
+
principal,
|
|
99
|
+
action in [Sentry::Action::"send_message", Sentry::Action::"paste_content", Sentry::Action::"upload_file"],
|
|
100
|
+
resource
|
|
101
|
+
)
|
|
102
|
+
when {
|
|
103
|
+
context has secret_types && context.secret_types.contains("pem_certificate")
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Block bulk secret exposure
|
|
107
|
+
@id("sentry-org-block-bulk-secrets")
|
|
108
|
+
@name("Block bulk secret exposure")
|
|
109
|
+
@description("Block content when 3+ distinct secrets are found. Multiple secrets indicate a configuration dump, .env file paste, or credential harvesting being sent to AI services.")
|
|
110
|
+
@severity("critical")
|
|
111
|
+
@tags("secrets,bulk,data-exfiltration,nist-sc-28")
|
|
112
|
+
@reject_message("Content blocked: multiple credentials detected (3+). Configuration dumps and credential lists must never be shared with AI services.")
|
|
113
|
+
forbid (
|
|
114
|
+
principal,
|
|
115
|
+
action,
|
|
116
|
+
resource
|
|
117
|
+
)
|
|
118
|
+
when {
|
|
119
|
+
context has secret_count && context.secret_count >= 3
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Block detected credential patterns
|
|
123
|
+
@id("sentry-org-block-detected-credentials")
|
|
124
|
+
@name("Block detected credential patterns")
|
|
125
|
+
@description("Block content flagged by detection engine rules for credential exposure, API key leaks, and token exposure. Defense-in-depth behind contains_secrets.")
|
|
126
|
+
@severity("critical")
|
|
127
|
+
@tags("secrets,credentials,detection-rules,nist-ia-5")
|
|
128
|
+
@reject_message("Content blocked: detection engines identified credential patterns including secret exposure, API keys, or token leaks.")
|
|
129
|
+
forbid (
|
|
130
|
+
principal,
|
|
131
|
+
action,
|
|
132
|
+
resource
|
|
133
|
+
)
|
|
134
|
+
when {
|
|
135
|
+
context has detected_threats &&
|
|
136
|
+
(context.detected_threats.contains("secret_exposure") ||
|
|
137
|
+
context.detected_threats.contains("credential_leak") ||
|
|
138
|
+
context.detected_threats.contains("api_key_exposure"))
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Block AI responses when session has leaked secrets
|
|
142
|
+
@id("sentry-org-session-secrets-response")
|
|
143
|
+
@name("Block responses after secret detection")
|
|
144
|
+
@description("Block AI responses when secrets were detected earlier in the session. If credentials were leaked in a previous turn, the AI service may have processed them and could echo or reference them in responses.")
|
|
145
|
+
@severity("high")
|
|
146
|
+
@tags("session,secrets,response-safety,defense-in-depth")
|
|
147
|
+
@reject_message("AI response blocked: secrets were detected in an earlier message in this session. Responses may contain or reference the exposed credentials.")
|
|
148
|
+
forbid (
|
|
149
|
+
principal,
|
|
150
|
+
action == Sentry::Action::"receive_response",
|
|
151
|
+
resource
|
|
152
|
+
)
|
|
153
|
+
when {
|
|
154
|
+
context has session_secrets_detected && context.session_secrets_detected
|
|
155
|
+
};
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"description": "Sentry policy templates for browser AI security",
|
|
5
5
|
"categories": [
|
|
6
|
+
{
|
|
7
|
+
"id": "secrets",
|
|
8
|
+
"name": "Secrets Detection",
|
|
9
|
+
"description": "Detect and block secrets, API keys, tokens, and other credentials in messages and AI responses"
|
|
10
|
+
},
|
|
6
11
|
{
|
|
7
12
|
"id": "pii",
|
|
8
13
|
"name": "PII Detection",
|
|
@@ -23,10 +28,15 @@
|
|
|
23
28
|
"name": "File & Attachment Safety",
|
|
24
29
|
"description": "Enforce document sensitivity controls (MIP labels), block sensitive file uploads, detect secrets and PII in uploaded documents"
|
|
25
30
|
},
|
|
31
|
+
{
|
|
32
|
+
"id": "clipboard",
|
|
33
|
+
"name": "Clipboard Policy",
|
|
34
|
+
"description": "Control paste operations into AI chat services — block paste outright, block when secrets or source code are detected"
|
|
35
|
+
},
|
|
26
36
|
{
|
|
27
37
|
"id": "organization",
|
|
28
38
|
"name": "Organization Rules",
|
|
29
|
-
"description": "
|
|
39
|
+
"description": "Cross-cutting organization-wide rules: source code protection in messages and session-aware threat escalation"
|
|
30
40
|
}
|
|
31
41
|
],
|
|
32
42
|
"defaults": [
|
|
@@ -39,7 +49,9 @@
|
|
|
39
49
|
"severity": "low",
|
|
40
50
|
"tags": ["baseline", "permit-default", "organization"],
|
|
41
51
|
"is_active": true
|
|
42
|
-
}
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"templates": [
|
|
43
55
|
{
|
|
44
56
|
"id": "sentry-semantic-default",
|
|
45
57
|
"name": "Semantic Threat Detection",
|
|
@@ -47,8 +59,7 @@
|
|
|
47
59
|
"category": "semantic",
|
|
48
60
|
"file": "defaults/semantic.cedar",
|
|
49
61
|
"severity": "critical",
|
|
50
|
-
"tags": ["injection", "jailbreak", "owasp-llm01", "owasp-llm02", "baseline"]
|
|
51
|
-
"is_active": true
|
|
62
|
+
"tags": ["injection", "jailbreak", "owasp-llm01", "owasp-llm02", "baseline"]
|
|
52
63
|
},
|
|
53
64
|
{
|
|
54
65
|
"id": "sentry-content-safety-default",
|
|
@@ -57,11 +68,17 @@
|
|
|
57
68
|
"category": "content_safety",
|
|
58
69
|
"file": "defaults/content_safety.cedar",
|
|
59
70
|
"severity": "critical",
|
|
60
|
-
"tags": ["violence", "hate-speech", "sexual", "profanity", "content-safety", "paste-safety", "baseline"]
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
"tags": ["violence", "hate-speech", "sexual", "profanity", "content-safety", "paste-safety", "baseline"]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "sentry-secrets-default",
|
|
75
|
+
"name": "Secrets Detection",
|
|
76
|
+
"description": "Block secrets, API keys, tokens, and credential leakage in messages and AI responses across all interactions",
|
|
77
|
+
"category": "secrets",
|
|
78
|
+
"file": "defaults/secrets.cedar",
|
|
79
|
+
"severity": "critical",
|
|
80
|
+
"tags": ["secrets", "credentials", "api-keys", "data-protection"]
|
|
81
|
+
},
|
|
65
82
|
{
|
|
66
83
|
"id": "sentry-pii-default",
|
|
67
84
|
"name": "PII Detection",
|
|
@@ -80,14 +97,23 @@
|
|
|
80
97
|
"severity": "critical",
|
|
81
98
|
"tags": ["mip", "document-sensitivity", "file-upload", "dlp", "compliance"]
|
|
82
99
|
},
|
|
100
|
+
{
|
|
101
|
+
"id": "sentry-clipboard-default",
|
|
102
|
+
"name": "Clipboard Policy",
|
|
103
|
+
"description": "Control paste into AI chat services: blanket paste blocking, secrets-in-paste blocking, and source-code-in-paste blocking",
|
|
104
|
+
"category": "clipboard",
|
|
105
|
+
"file": "defaults/clipboard.cedar",
|
|
106
|
+
"severity": "high",
|
|
107
|
+
"tags": ["paste", "clipboard", "data-protection", "source-code", "secrets"]
|
|
108
|
+
},
|
|
83
109
|
{
|
|
84
110
|
"id": "sentry-organization-default",
|
|
85
111
|
"name": "Organization Rules",
|
|
86
|
-
"description": "
|
|
112
|
+
"description": "Cross-cutting organization-wide policies: source code protection in messages and session-aware threat escalation",
|
|
87
113
|
"category": "organization",
|
|
88
114
|
"file": "defaults/organization.cedar",
|
|
89
|
-
"severity": "
|
|
90
|
-
"tags": ["
|
|
115
|
+
"severity": "high",
|
|
116
|
+
"tags": ["source-code", "session", "escalation", "organization"]
|
|
91
117
|
}
|
|
92
118
|
]
|
|
93
119
|
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -48,6 +48,19 @@ export interface DeterminingPolicy {
|
|
|
48
48
|
/** All annotations from this policy as key-value pairs */
|
|
49
49
|
annotations: Record<string, string>;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Pairs a Cedar policy text with a globally-unique identifier (typically the
|
|
53
|
+
* originating database row's UUID) used as the cedar PolicySet key.
|
|
54
|
+
*
|
|
55
|
+
* Use with {@link PolicyEngine.loadPoliciesWithIds} to safely load policies
|
|
56
|
+
* from multiple tenants whose @id annotations may collide.
|
|
57
|
+
*/
|
|
58
|
+
export interface PolicyTextWithID {
|
|
59
|
+
/** Cedar policy text (may contain @id and other annotations) */
|
|
60
|
+
text: string;
|
|
61
|
+
/** Unique identifier — must be unique across the input set */
|
|
62
|
+
id: string;
|
|
63
|
+
}
|
|
51
64
|
export declare class Decision {
|
|
52
65
|
readonly effect: "Allow" | "Deny";
|
|
53
66
|
readonly determining_policies: DeterminingPolicy[];
|
|
@@ -92,8 +105,32 @@ export declare class PolicyEngine {
|
|
|
92
105
|
* Load multiple Cedar policy texts (concatenated with newlines).
|
|
93
106
|
* Uses @id annotations as policy IDs when available.
|
|
94
107
|
* Stores all annotations per policy for enriching evaluation results.
|
|
108
|
+
*
|
|
109
|
+
* @deprecated Use {@link loadPoliciesWithIds} for any code that may load
|
|
110
|
+
* policies from multiple tenants. This method's @id-as-PolicyID behavior
|
|
111
|
+
* silently overwrites the cedar PolicySet entry when later policies share
|
|
112
|
+
* an @id with earlier ones (e.g., every tenant's `baseline-permit-all`).
|
|
113
|
+
* Kept for ad-hoc and test usage where uniqueness is guaranteed by the
|
|
114
|
+
* caller.
|
|
95
115
|
*/
|
|
96
116
|
loadPolicies(policies: string[]): void;
|
|
117
|
+
/**
|
|
118
|
+
* Load Cedar policies using caller-supplied unique IDs as the cedar
|
|
119
|
+
* PolicySet key, preventing cross-tenant @id collisions that occur when
|
|
120
|
+
* multiple tenants author policies from a shared template (e.g., each
|
|
121
|
+
* tenant's `baseline-permit-all` sharing the same @id annotation).
|
|
122
|
+
*
|
|
123
|
+
* Each input is parsed independently so the supplied id maps
|
|
124
|
+
* deterministically to its policy. If a single text contains multiple
|
|
125
|
+
* Cedar policies, each is stored under `"<id>#<index>"`. The original
|
|
126
|
+
* @id annotation remains accessible via {@link getPolicyAnnotations}.
|
|
127
|
+
*
|
|
128
|
+
* When `item.id` is empty, the method falls back to @id-annotation-based
|
|
129
|
+
* id assignment (matching {@link loadPolicies} semantics) — useful for
|
|
130
|
+
* tests and ad-hoc loaders. Production multi-tenant callers MUST supply
|
|
131
|
+
* a non-empty id per policy or they reintroduce the collision risk.
|
|
132
|
+
*/
|
|
133
|
+
loadPoliciesWithIds(policies: PolicyTextWithID[]): void;
|
|
97
134
|
/**
|
|
98
135
|
* Load schema from a Cedar schema string.
|
|
99
136
|
*/
|
package/dist/engine.js
CHANGED
|
@@ -226,12 +226,68 @@ export class PolicyEngine {
|
|
|
226
226
|
* Load multiple Cedar policy texts (concatenated with newlines).
|
|
227
227
|
* Uses @id annotations as policy IDs when available.
|
|
228
228
|
* Stores all annotations per policy for enriching evaluation results.
|
|
229
|
+
*
|
|
230
|
+
* @deprecated Use {@link loadPoliciesWithIds} for any code that may load
|
|
231
|
+
* policies from multiple tenants. This method's @id-as-PolicyID behavior
|
|
232
|
+
* silently overwrites the cedar PolicySet entry when later policies share
|
|
233
|
+
* an @id with earlier ones (e.g., every tenant's `baseline-permit-all`).
|
|
234
|
+
* Kept for ad-hoc and test usage where uniqueness is guaranteed by the
|
|
235
|
+
* caller.
|
|
229
236
|
*/
|
|
230
237
|
loadPolicies(policies) {
|
|
231
238
|
const extracted = extractPolicies(policies.join("\n"));
|
|
232
239
|
this.policySet = extracted.policySet;
|
|
233
240
|
this.policyAnnotations = extracted.annotations;
|
|
234
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Load Cedar policies using caller-supplied unique IDs as the cedar
|
|
244
|
+
* PolicySet key, preventing cross-tenant @id collisions that occur when
|
|
245
|
+
* multiple tenants author policies from a shared template (e.g., each
|
|
246
|
+
* tenant's `baseline-permit-all` sharing the same @id annotation).
|
|
247
|
+
*
|
|
248
|
+
* Each input is parsed independently so the supplied id maps
|
|
249
|
+
* deterministically to its policy. If a single text contains multiple
|
|
250
|
+
* Cedar policies, each is stored under `"<id>#<index>"`. The original
|
|
251
|
+
* @id annotation remains accessible via {@link getPolicyAnnotations}.
|
|
252
|
+
*
|
|
253
|
+
* When `item.id` is empty, the method falls back to @id-annotation-based
|
|
254
|
+
* id assignment (matching {@link loadPolicies} semantics) — useful for
|
|
255
|
+
* tests and ad-hoc loaders. Production multi-tenant callers MUST supply
|
|
256
|
+
* a non-empty id per policy or they reintroduce the collision risk.
|
|
257
|
+
*/
|
|
258
|
+
loadPoliciesWithIds(policies) {
|
|
259
|
+
const policyMap = {};
|
|
260
|
+
const annotationsMap = new Map();
|
|
261
|
+
let posIdx = 0;
|
|
262
|
+
for (const item of policies) {
|
|
263
|
+
const parts = cedar.policySetTextToParts(item.text);
|
|
264
|
+
if (parts.type !== "success" || parts.policies.length === 0) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
for (let subIdx = 0; subIdx < parts.policies.length; subIdx++) {
|
|
268
|
+
const policy = parts.policies[subIdx];
|
|
269
|
+
const policyAnnotations = extractAnnotationsFromText(policy);
|
|
270
|
+
let id;
|
|
271
|
+
if (item.id !== "") {
|
|
272
|
+
id = subIdx === 0 ? item.id : `${item.id}#${subIdx}`;
|
|
273
|
+
}
|
|
274
|
+
else if (policyAnnotations["id"]) {
|
|
275
|
+
id = policyAnnotations["id"];
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
id = `policy${posIdx}`;
|
|
279
|
+
}
|
|
280
|
+
if (annotationsMap.has(id)) {
|
|
281
|
+
throw new Error(`duplicate policy ID detected: ${id}`);
|
|
282
|
+
}
|
|
283
|
+
policyMap[id] = policy;
|
|
284
|
+
annotationsMap.set(id, policyAnnotations);
|
|
285
|
+
posIdx++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.policySet = policyMap;
|
|
289
|
+
this.policyAnnotations = annotationsMap;
|
|
290
|
+
}
|
|
235
291
|
/**
|
|
236
292
|
* Load schema from a Cedar schema string.
|
|
237
293
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Sentry policy category identifiers.
|
|
3
3
|
* Maps to UI tab names in Studio.
|
|
4
4
|
*/
|
|
5
|
-
export type SentryCategory = 'pii' | 'semantic' | 'content_safety' | 'file_safety' | 'organization';
|
|
5
|
+
export type SentryCategory = 'secrets' | 'pii' | 'semantic' | 'content_safety' | 'file_safety' | 'clipboard' | 'organization';
|
|
6
6
|
/**
|
|
7
7
|
* Category metadata for UI display.
|
|
8
8
|
*/
|