securityclaw 0.0.1
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/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/README.zh-CN.md +135 -0
- package/admin/public/app.js +148 -0
- package/admin/public/favicon.svg +21 -0
- package/admin/public/index.html +31 -0
- package/admin/public/styles.css +2715 -0
- package/admin/server.ts +1053 -0
- package/bin/install-lib.mjs +88 -0
- package/bin/securityclaw.mjs +66 -0
- package/config/policy.default.yaml +520 -0
- package/index.ts +2662 -0
- package/install.sh +22 -0
- package/openclaw.plugin.json +60 -0
- package/package.json +69 -0
- package/src/admin/build.ts +113 -0
- package/src/admin/console_notice.ts +195 -0
- package/src/admin/dashboard_url_state.ts +80 -0
- package/src/admin/openclaw_session_catalog.ts +137 -0
- package/src/admin/runtime_guard.ts +51 -0
- package/src/admin/skill_interception_store.ts +1606 -0
- package/src/application/commands/approval_commands.ts +189 -0
- package/src/approvals/chat_approval_store.ts +433 -0
- package/src/config/live_config.ts +144 -0
- package/src/config/loader.ts +168 -0
- package/src/config/runtime_override.ts +66 -0
- package/src/config/strategy_store.ts +121 -0
- package/src/config/validator.ts +222 -0
- package/src/domain/models/resource_context.ts +31 -0
- package/src/domain/ports/approval_repository.ts +40 -0
- package/src/domain/ports/notification_port.ts +29 -0
- package/src/domain/ports/openclaw_adapter.ts +22 -0
- package/src/domain/services/account_policy_engine.ts +163 -0
- package/src/domain/services/approval_service.ts +336 -0
- package/src/domain/services/approval_subject_resolver.ts +37 -0
- package/src/domain/services/context_inference_service.ts +502 -0
- package/src/domain/services/file_rule_registry.ts +171 -0
- package/src/domain/services/formatting_service.ts +101 -0
- package/src/domain/services/path_candidate_inference.ts +111 -0
- package/src/domain/services/sensitive_path_registry.ts +288 -0
- package/src/domain/services/sensitivity_label_inference.ts +161 -0
- package/src/domain/services/shell_filesystem_inference.ts +360 -0
- package/src/engine/approval_fsm.ts +104 -0
- package/src/engine/decision_engine.ts +39 -0
- package/src/engine/dlp_engine.ts +91 -0
- package/src/engine/rule_engine.ts +208 -0
- package/src/events/emitter.ts +86 -0
- package/src/events/schema.ts +27 -0
- package/src/hooks/context_guard.ts +36 -0
- package/src/hooks/output_guard.ts +66 -0
- package/src/hooks/persist_guard.ts +69 -0
- package/src/hooks/policy_guard.ts +222 -0
- package/src/hooks/result_guard.ts +88 -0
- package/src/i18n/locale.ts +36 -0
- package/src/index.ts +255 -0
- package/src/infrastructure/adapters/notification_adapter.ts +173 -0
- package/src/infrastructure/adapters/openclaw_adapter_impl.ts +59 -0
- package/src/infrastructure/config/plugin_config_parser.ts +105 -0
- package/src/monitoring/status_store.ts +612 -0
- package/src/types.ts +409 -0
- package/src/utils.ts +97 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
export type HookName =
|
|
2
|
+
| "before_prompt_build"
|
|
3
|
+
| "before_tool_call"
|
|
4
|
+
| "after_tool_call"
|
|
5
|
+
| "tool_result_persist"
|
|
6
|
+
| "message_sending";
|
|
7
|
+
|
|
8
|
+
export type Decision = "allow" | "warn" | "challenge" | "block";
|
|
9
|
+
export type DecisionSource = "rule" | "default" | "approval" | "account" | "file_rule";
|
|
10
|
+
export type FailMode = "open" | "close";
|
|
11
|
+
export type ApprovalStatus = "pending" | "approved" | "rejected" | "expired";
|
|
12
|
+
export type PersistMode = "strict" | "compat";
|
|
13
|
+
export type DlpAction = "mask" | "remove";
|
|
14
|
+
export type DlpMode = "warn" | "block" | "sanitize";
|
|
15
|
+
export type PatternType = "pii" | "secret" | "token" | "credential";
|
|
16
|
+
export type ReasonCode = string;
|
|
17
|
+
export type ResourceScope = "none" | "workspace_inside" | "workspace_outside" | "system";
|
|
18
|
+
export type Severity = "low" | "medium" | "high" | "critical";
|
|
19
|
+
export type AccountPolicyMode = "apply_rules" | "default_allow";
|
|
20
|
+
export type SensitivePathMatchType = "prefix" | "glob" | "regex";
|
|
21
|
+
export type SensitivePathSource = "builtin" | "custom";
|
|
22
|
+
export type ControlDomain =
|
|
23
|
+
| "execution_control"
|
|
24
|
+
| "data_access"
|
|
25
|
+
| "data_egress"
|
|
26
|
+
| "credential_protection"
|
|
27
|
+
| "change_control"
|
|
28
|
+
| "approval_exception"
|
|
29
|
+
| (string & {});
|
|
30
|
+
export type TrustLevel = "trusted" | "untrusted" | "unknown" | (string & {});
|
|
31
|
+
export type DestinationType =
|
|
32
|
+
| "internal"
|
|
33
|
+
| "public"
|
|
34
|
+
| "unknown"
|
|
35
|
+
| "personal_storage"
|
|
36
|
+
| "paste_service"
|
|
37
|
+
| (string & {});
|
|
38
|
+
export type DestinationIpClass = "loopback" | "private" | "public" | "unknown" | (string & {});
|
|
39
|
+
|
|
40
|
+
export interface SecurityContext {
|
|
41
|
+
trace_id: string;
|
|
42
|
+
actor_id: string;
|
|
43
|
+
workspace: string;
|
|
44
|
+
policy_version: string;
|
|
45
|
+
untrusted: boolean;
|
|
46
|
+
tags: string[];
|
|
47
|
+
created_at: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AccountPolicyRecord {
|
|
51
|
+
subject: string;
|
|
52
|
+
mode: AccountPolicyMode;
|
|
53
|
+
is_admin: boolean;
|
|
54
|
+
label?: string;
|
|
55
|
+
session_key?: string;
|
|
56
|
+
session_id?: string;
|
|
57
|
+
agent_id?: string;
|
|
58
|
+
channel?: string;
|
|
59
|
+
chat_type?: string;
|
|
60
|
+
updated_at?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface HookControls {
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
timeout_ms: number;
|
|
66
|
+
fail_mode: FailMode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface VolumeMetrics {
|
|
70
|
+
file_count?: number;
|
|
71
|
+
bytes?: number;
|
|
72
|
+
record_count?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface PolicyMatch {
|
|
76
|
+
identity?: string[];
|
|
77
|
+
scope?: string[];
|
|
78
|
+
tool?: string[];
|
|
79
|
+
tool_group?: string[];
|
|
80
|
+
operation?: string[];
|
|
81
|
+
tags?: string[];
|
|
82
|
+
resource_scope?: ResourceScope[];
|
|
83
|
+
path_prefix?: string[];
|
|
84
|
+
path_glob?: string[];
|
|
85
|
+
path_regex?: string[];
|
|
86
|
+
file_type?: string[];
|
|
87
|
+
asset_labels?: string[];
|
|
88
|
+
data_labels?: string[];
|
|
89
|
+
trust_level?: TrustLevel[];
|
|
90
|
+
destination_type?: DestinationType[];
|
|
91
|
+
dest_domain?: string[];
|
|
92
|
+
dest_ip_class?: DestinationIpClass[];
|
|
93
|
+
tool_args_summary?: string[];
|
|
94
|
+
tool_args_regex?: string[];
|
|
95
|
+
min_file_count?: number;
|
|
96
|
+
min_bytes?: number;
|
|
97
|
+
min_record_count?: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ChallengeConfig {
|
|
101
|
+
ttl_seconds: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ApprovalRequirements {
|
|
105
|
+
ticket_required?: boolean;
|
|
106
|
+
approver_roles?: string[];
|
|
107
|
+
single_use?: boolean;
|
|
108
|
+
trace_binding?: "none" | "trace";
|
|
109
|
+
ttl_seconds?: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface PolicyRule {
|
|
113
|
+
rule_id: string;
|
|
114
|
+
group: string;
|
|
115
|
+
title?: string;
|
|
116
|
+
description?: string;
|
|
117
|
+
severity?: Severity;
|
|
118
|
+
control_domain?: ControlDomain;
|
|
119
|
+
owner?: string;
|
|
120
|
+
playbook_url?: string;
|
|
121
|
+
enabled: boolean;
|
|
122
|
+
priority: number;
|
|
123
|
+
decision?: Decision;
|
|
124
|
+
reason_codes: ReasonCode[];
|
|
125
|
+
match: PolicyMatch;
|
|
126
|
+
challenge?: ChallengeConfig;
|
|
127
|
+
approval_requirements?: ApprovalRequirements;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface ApprovalRequestContext {
|
|
131
|
+
trace_id: string;
|
|
132
|
+
actor_id: string;
|
|
133
|
+
scope: string;
|
|
134
|
+
tool_name?: string;
|
|
135
|
+
tool_group?: string;
|
|
136
|
+
resource_scope: ResourceScope;
|
|
137
|
+
resource_paths: string[];
|
|
138
|
+
reason_codes: ReasonCode[];
|
|
139
|
+
rule_ids: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface ApprovalResolutionMetadata {
|
|
143
|
+
approver_role?: string;
|
|
144
|
+
ticket_id?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ApprovalRequestOptions {
|
|
148
|
+
ttl_seconds?: number;
|
|
149
|
+
reason_codes?: ReasonCode[];
|
|
150
|
+
rule_ids?: string[];
|
|
151
|
+
approval_requirements?: ApprovalRequirements;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ApprovalRecord {
|
|
155
|
+
approval_id: string;
|
|
156
|
+
status: ApprovalStatus;
|
|
157
|
+
requested_at: string;
|
|
158
|
+
expires_at: string;
|
|
159
|
+
request_context: ApprovalRequestContext;
|
|
160
|
+
approval_requirements?: ApprovalRequirements;
|
|
161
|
+
approver?: string;
|
|
162
|
+
approver_role?: string;
|
|
163
|
+
ticket_id?: string;
|
|
164
|
+
decision?: "approved" | "rejected";
|
|
165
|
+
decided_at?: string;
|
|
166
|
+
used_at?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface DlpPatternConfig {
|
|
170
|
+
name: string;
|
|
171
|
+
type: PatternType;
|
|
172
|
+
action: DlpAction;
|
|
173
|
+
regex: string;
|
|
174
|
+
flags?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface SensitivePathRule {
|
|
178
|
+
id: string;
|
|
179
|
+
asset_label: string;
|
|
180
|
+
match_type: SensitivePathMatchType;
|
|
181
|
+
pattern: string;
|
|
182
|
+
source?: SensitivePathSource;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface SensitivePathConfig {
|
|
186
|
+
path_rules: SensitivePathRule[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface SensitivePathStrategyOverride {
|
|
190
|
+
disabled_builtin_ids?: string[];
|
|
191
|
+
custom_path_rules?: SensitivePathRule[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface FileRule {
|
|
195
|
+
id: string;
|
|
196
|
+
directory: string;
|
|
197
|
+
decision: Decision;
|
|
198
|
+
reason_codes?: ReasonCode[];
|
|
199
|
+
updated_at?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface DlpFinding {
|
|
203
|
+
pattern_name: string;
|
|
204
|
+
type: PatternType;
|
|
205
|
+
action: DlpAction;
|
|
206
|
+
path: string;
|
|
207
|
+
match: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface SanitizationAction {
|
|
211
|
+
path: string;
|
|
212
|
+
action: DlpAction | "truncate";
|
|
213
|
+
detail: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface SecurityDecisionEvent {
|
|
217
|
+
schema_version: string;
|
|
218
|
+
event_type: "SecurityDecisionEvent";
|
|
219
|
+
trace_id: string;
|
|
220
|
+
hook: HookName;
|
|
221
|
+
decision: Decision;
|
|
222
|
+
decision_source?: DecisionSource;
|
|
223
|
+
resource_scope?: ResourceScope;
|
|
224
|
+
reason_codes: ReasonCode[];
|
|
225
|
+
latency_ms: number;
|
|
226
|
+
ts: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface DlpConfig {
|
|
230
|
+
on_dlp_hit: DlpMode;
|
|
231
|
+
patterns: DlpPatternConfig[];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface EventSinkConfig {
|
|
235
|
+
webhook_url?: string;
|
|
236
|
+
timeout_ms: number;
|
|
237
|
+
max_buffer: number;
|
|
238
|
+
retry_limit: number;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface SecurityClawConfig {
|
|
242
|
+
version: string;
|
|
243
|
+
policy_version: string;
|
|
244
|
+
environment: string;
|
|
245
|
+
defaults: {
|
|
246
|
+
approval_ttl_seconds: number;
|
|
247
|
+
persist_mode: PersistMode;
|
|
248
|
+
};
|
|
249
|
+
hooks: Record<HookName, HookControls>;
|
|
250
|
+
policies: PolicyRule[];
|
|
251
|
+
sensitivity: SensitivePathConfig;
|
|
252
|
+
file_rules: FileRule[];
|
|
253
|
+
dlp: DlpConfig;
|
|
254
|
+
event_sink: EventSinkConfig;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface HookResult<T = unknown> {
|
|
258
|
+
mutated_payload: T;
|
|
259
|
+
decision: Decision;
|
|
260
|
+
decision_source?: DecisionSource;
|
|
261
|
+
reason_codes: ReasonCode[];
|
|
262
|
+
sanitization_actions: SanitizationAction[];
|
|
263
|
+
latency_ms: number;
|
|
264
|
+
security_context?: SecurityContext;
|
|
265
|
+
approval?: ApprovalRecord;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface BeforePromptBuildInput {
|
|
269
|
+
prompt: unknown;
|
|
270
|
+
actor_id: string;
|
|
271
|
+
workspace: string;
|
|
272
|
+
source?: "external" | "internal";
|
|
273
|
+
trace_id?: string;
|
|
274
|
+
tags?: string[];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface BeforeToolCallInput {
|
|
278
|
+
actor_id: string;
|
|
279
|
+
workspace: string;
|
|
280
|
+
scope: string;
|
|
281
|
+
tool_name: string;
|
|
282
|
+
tool_group?: string;
|
|
283
|
+
operation?: string;
|
|
284
|
+
tags?: string[];
|
|
285
|
+
resource_scope?: ResourceScope;
|
|
286
|
+
resource_paths?: string[];
|
|
287
|
+
file_type?: string;
|
|
288
|
+
asset_labels?: string[];
|
|
289
|
+
data_labels?: string[];
|
|
290
|
+
trust_level?: TrustLevel;
|
|
291
|
+
destination_type?: DestinationType;
|
|
292
|
+
dest_domain?: string;
|
|
293
|
+
dest_ip_class?: DestinationIpClass;
|
|
294
|
+
tool_args_summary?: string;
|
|
295
|
+
volume?: VolumeMetrics;
|
|
296
|
+
security_context?: Partial<SecurityContext>;
|
|
297
|
+
approval_id?: string;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface SchemaExpectation {
|
|
301
|
+
type: "string" | "number" | "boolean" | "object" | "array";
|
|
302
|
+
required?: Record<string, SchemaExpectation>;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface AfterToolCallInput {
|
|
306
|
+
actor_id: string;
|
|
307
|
+
workspace: string;
|
|
308
|
+
scope: string;
|
|
309
|
+
tool_name: string;
|
|
310
|
+
result: unknown;
|
|
311
|
+
security_context?: Partial<SecurityContext>;
|
|
312
|
+
expected_schema?: SchemaExpectation;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface ToolResultPersistInput {
|
|
316
|
+
actor_id: string;
|
|
317
|
+
workspace: string;
|
|
318
|
+
scope: string;
|
|
319
|
+
tool_name: string;
|
|
320
|
+
result: unknown;
|
|
321
|
+
mode?: PersistMode;
|
|
322
|
+
security_context?: Partial<SecurityContext>;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface MessageSendingInput {
|
|
326
|
+
actor_id: string;
|
|
327
|
+
workspace: string;
|
|
328
|
+
scope: string;
|
|
329
|
+
message: unknown;
|
|
330
|
+
restricted_terms?: string[];
|
|
331
|
+
security_context?: Partial<SecurityContext>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface RuleMatch {
|
|
335
|
+
rule: PolicyRule;
|
|
336
|
+
precedence: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export interface DecisionContext {
|
|
340
|
+
actor_id: string;
|
|
341
|
+
scope: string;
|
|
342
|
+
tool_name?: string;
|
|
343
|
+
tool_group?: string;
|
|
344
|
+
operation?: string;
|
|
345
|
+
tags: string[];
|
|
346
|
+
resource_scope: ResourceScope;
|
|
347
|
+
resource_paths: string[];
|
|
348
|
+
file_type?: string;
|
|
349
|
+
asset_labels: string[];
|
|
350
|
+
data_labels: string[];
|
|
351
|
+
trust_level: TrustLevel;
|
|
352
|
+
destination_type?: DestinationType;
|
|
353
|
+
dest_domain?: string;
|
|
354
|
+
dest_ip_class?: DestinationIpClass;
|
|
355
|
+
tool_args_summary?: string;
|
|
356
|
+
volume: VolumeMetrics;
|
|
357
|
+
security_context: SecurityContext;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface DecisionOutcome {
|
|
361
|
+
decision: Decision;
|
|
362
|
+
decision_source: DecisionSource;
|
|
363
|
+
reason_codes: ReasonCode[];
|
|
364
|
+
matched_rules: PolicyRule[];
|
|
365
|
+
challenge_ttl_seconds?: number;
|
|
366
|
+
approval_requirements?: ApprovalRequirements;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export interface GuardComputation<T = unknown> {
|
|
370
|
+
mutated_payload: T;
|
|
371
|
+
decision: Decision;
|
|
372
|
+
decision_source?: DecisionSource;
|
|
373
|
+
reason_codes: ReasonCode[];
|
|
374
|
+
sanitization_actions: SanitizationAction[];
|
|
375
|
+
security_context?: SecurityContext;
|
|
376
|
+
approval?: ApprovalRecord;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface EventSink {
|
|
380
|
+
send(event: SecurityDecisionEvent): Promise<void>;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface SecurityClawPluginOptions {
|
|
384
|
+
config?: SecurityClawConfig;
|
|
385
|
+
config_path?: string;
|
|
386
|
+
event_sink?: EventSink;
|
|
387
|
+
now?: () => number;
|
|
388
|
+
generate_trace_id?: () => string;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export interface PluginHooks {
|
|
392
|
+
before_prompt_build(input: BeforePromptBuildInput): Promise<HookResult<BeforePromptBuildInput>>;
|
|
393
|
+
before_tool_call(input: BeforeToolCallInput): Promise<HookResult<BeforeToolCallInput>>;
|
|
394
|
+
after_tool_call(input: AfterToolCallInput): Promise<HookResult<AfterToolCallInput>>;
|
|
395
|
+
tool_result_persist(input: ToolResultPersistInput): Promise<HookResult<ToolResultPersistInput>>;
|
|
396
|
+
message_sending(input: MessageSendingInput): Promise<HookResult<MessageSendingInput>>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export interface ApprovalService {
|
|
400
|
+
requestApproval(context: DecisionContext, options?: ApprovalRequestOptions): ApprovalRecord;
|
|
401
|
+
resolveApproval(
|
|
402
|
+
approvalId: string,
|
|
403
|
+
approver: string,
|
|
404
|
+
decision: "approved" | "rejected",
|
|
405
|
+
metadata?: ApprovalResolutionMetadata,
|
|
406
|
+
): ApprovalRecord | undefined;
|
|
407
|
+
markApprovalUsed(approvalId: string): ApprovalRecord | undefined;
|
|
408
|
+
getApprovalStatus(approvalId: string): ApprovalRecord | undefined;
|
|
409
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function deepClone<T>(value: T): T {
|
|
4
|
+
return structuredClone(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function deepFreeze<T>(value: T): T {
|
|
8
|
+
if (value && typeof value === "object") {
|
|
9
|
+
Object.freeze(value);
|
|
10
|
+
for (const child of Object.values(value as Record<string, unknown>)) {
|
|
11
|
+
if (child && typeof child === "object" && !Object.isFrozen(child)) {
|
|
12
|
+
deepFreeze(child);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
20
|
+
return Math.min(max, Math.max(min, value));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function nowIso(now: () => number): string {
|
|
24
|
+
return new Date(now()).toISOString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function generateTraceId(): string {
|
|
28
|
+
return randomUUID();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ensureArray<T>(value?: T[]): T[] {
|
|
32
|
+
return Array.isArray(value) ? value : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function delay(ms: number): Promise<void> {
|
|
36
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
40
|
+
const timeout = new Promise<T>((_, reject) => {
|
|
41
|
+
setTimeout(() => reject(new Error(message)), ms);
|
|
42
|
+
});
|
|
43
|
+
return Promise.race([promise, timeout]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function stripInlineComment(line: string): string {
|
|
47
|
+
let quoted = false;
|
|
48
|
+
let quoteChar = "";
|
|
49
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
50
|
+
const char = line[index];
|
|
51
|
+
if ((char === "'" || char === "\"") && line[index - 1] !== "\\") {
|
|
52
|
+
if (!quoted) {
|
|
53
|
+
quoted = true;
|
|
54
|
+
quoteChar = char;
|
|
55
|
+
} else if (quoteChar === char) {
|
|
56
|
+
quoted = false;
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (char === "#" && !quoted) {
|
|
61
|
+
return line.slice(0, index).trimEnd();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return line.trimEnd();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseScalar(raw: string): unknown {
|
|
68
|
+
const value = raw.trim();
|
|
69
|
+
if (value === "true") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (value === "false") {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (value === "null") {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
79
|
+
return Number(value);
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
value.startsWith("\"") && value.endsWith("\"")
|
|
83
|
+
) {
|
|
84
|
+
return JSON.parse(value);
|
|
85
|
+
}
|
|
86
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
87
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
88
|
+
}
|
|
89
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
90
|
+
const inner = value.slice(1, -1).trim();
|
|
91
|
+
if (inner === "") {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return inner.split(",").map((part) => parseScalar(part));
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|