multicorn-shield 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 +21 -0
- package/README.md +453 -0
- package/dist/index.cjs +2507 -0
- package/dist/index.d.cts +2182 -0
- package/dist/index.d.ts +2182 -0
- package/dist/index.js +2477 -0
- package/dist/multicorn-proxy.js +1153 -0
- package/dist/openclaw-hook/HOOK.md +75 -0
- package/dist/openclaw-hook/handler.js +447 -0
- package/dist/openclaw-plugin/index.js +692 -0
- package/dist/openclaw-plugin/openclaw.plugin.json +51 -0
- package/package.json +122 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2507 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var lit = require('lit');
|
|
4
|
+
var decorators_js = require('lit/decorators.js');
|
|
5
|
+
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
8
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
9
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
10
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
11
|
+
if (decorator = decorators[i])
|
|
12
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
13
|
+
if (kind && result) __defProp(target, key, result);
|
|
14
|
+
return result;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/types/index.ts
|
|
18
|
+
var AGENT_STATUSES = {
|
|
19
|
+
Active: "active",
|
|
20
|
+
Paused: "paused",
|
|
21
|
+
Revoked: "revoked"
|
|
22
|
+
};
|
|
23
|
+
var PERMISSION_LEVELS = {
|
|
24
|
+
Read: "read",
|
|
25
|
+
Write: "write",
|
|
26
|
+
Execute: "execute",
|
|
27
|
+
Publish: "publish",
|
|
28
|
+
Create: "create"
|
|
29
|
+
};
|
|
30
|
+
var ACTION_STATUSES = {
|
|
31
|
+
Approved: "approved",
|
|
32
|
+
Blocked: "blocked",
|
|
33
|
+
Pending: "pending",
|
|
34
|
+
Flagged: "flagged",
|
|
35
|
+
RequiresApproval: "requires_approval"
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/scopes/scope-definitions.ts
|
|
39
|
+
var BUILT_IN_SERVICES = {
|
|
40
|
+
gmail: {
|
|
41
|
+
name: "gmail",
|
|
42
|
+
description: "Google Gmail: email reading, composing, and sending",
|
|
43
|
+
capabilities: [PERMISSION_LEVELS.Read, PERMISSION_LEVELS.Write, PERMISSION_LEVELS.Execute]
|
|
44
|
+
},
|
|
45
|
+
calendar: {
|
|
46
|
+
name: "calendar",
|
|
47
|
+
description: "Google Calendar: event viewing, creation, and management",
|
|
48
|
+
capabilities: [PERMISSION_LEVELS.Read, PERMISSION_LEVELS.Write, PERMISSION_LEVELS.Execute]
|
|
49
|
+
},
|
|
50
|
+
slack: {
|
|
51
|
+
name: "slack",
|
|
52
|
+
description: "Slack: message reading, posting, and workflow triggers",
|
|
53
|
+
capabilities: [PERMISSION_LEVELS.Read, PERMISSION_LEVELS.Write, PERMISSION_LEVELS.Execute]
|
|
54
|
+
},
|
|
55
|
+
drive: {
|
|
56
|
+
name: "drive",
|
|
57
|
+
description: "Google Drive: file browsing, uploading, and sharing",
|
|
58
|
+
capabilities: [PERMISSION_LEVELS.Read, PERMISSION_LEVELS.Write]
|
|
59
|
+
},
|
|
60
|
+
payments: {
|
|
61
|
+
name: "payments",
|
|
62
|
+
description: "Payment processing: balance enquiries and transaction execution",
|
|
63
|
+
capabilities: [PERMISSION_LEVELS.Read, PERMISSION_LEVELS.Execute]
|
|
64
|
+
},
|
|
65
|
+
github: {
|
|
66
|
+
name: "github",
|
|
67
|
+
description: "GitHub: repository access, issues, and pull requests",
|
|
68
|
+
capabilities: [PERMISSION_LEVELS.Read, PERMISSION_LEVELS.Write, PERMISSION_LEVELS.Execute]
|
|
69
|
+
},
|
|
70
|
+
jira: {
|
|
71
|
+
name: "jira",
|
|
72
|
+
description: "Jira: issue tracking, sprint management, and reporting",
|
|
73
|
+
capabilities: [PERMISSION_LEVELS.Read, PERMISSION_LEVELS.Write]
|
|
74
|
+
},
|
|
75
|
+
web: {
|
|
76
|
+
name: "web",
|
|
77
|
+
description: "Web publishing: content accessible on the open internet",
|
|
78
|
+
capabilities: [PERMISSION_LEVELS.Publish]
|
|
79
|
+
},
|
|
80
|
+
public_content: {
|
|
81
|
+
name: "public_content",
|
|
82
|
+
description: "Public content creation: blog posts, social media, public repositories",
|
|
83
|
+
capabilities: [PERMISSION_LEVELS.Create]
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var SERVICE_NAME_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
87
|
+
function createScopeRegistry() {
|
|
88
|
+
const services = /* @__PURE__ */ new Map();
|
|
89
|
+
const validPermissionLevels = new Set(Object.values(PERMISSION_LEVELS));
|
|
90
|
+
for (const service of Object.values(BUILT_IN_SERVICES)) {
|
|
91
|
+
services.set(service.name, service);
|
|
92
|
+
}
|
|
93
|
+
function validateDefinition(definition) {
|
|
94
|
+
if (definition.name.length === 0) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'Service name must not be empty. Provide a lowercase identifier such as "my-service".'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (!SERVICE_NAME_PATTERN.test(definition.name)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Invalid service name "${definition.name}". Service names must start with a lowercase letter and contain only lowercase letters, digits, hyphens, or underscores (e.g. "my-service", "analytics2").`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (definition.capabilities.length === 0) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Service "${definition.name}" must declare at least one capability (${[...validPermissionLevels].join(", ")}).`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
110
|
+
for (const cap of definition.capabilities) {
|
|
111
|
+
if (!validPermissionLevels.has(cap)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Invalid capability "${cap}" for service "${definition.name}". Valid capabilities are: ${[...validPermissionLevels].join(", ")}.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (seen.has(cap)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Duplicate capability "${cap}" in service "${definition.name}". Each capability should be listed only once.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
seen.add(cap);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
register(definition) {
|
|
126
|
+
validateDefinition(definition);
|
|
127
|
+
if (services.has(definition.name)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Service "${definition.name}" is already registered. Choose a unique name for your custom service.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
services.set(definition.name, {
|
|
133
|
+
name: definition.name,
|
|
134
|
+
description: definition.description,
|
|
135
|
+
capabilities: [...definition.capabilities]
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
get(serviceName) {
|
|
139
|
+
return services.get(serviceName);
|
|
140
|
+
},
|
|
141
|
+
has(serviceName) {
|
|
142
|
+
return services.has(serviceName);
|
|
143
|
+
},
|
|
144
|
+
getAllServices() {
|
|
145
|
+
return [...services.values()];
|
|
146
|
+
},
|
|
147
|
+
isValidScope(scope) {
|
|
148
|
+
const service = services.get(scope.service);
|
|
149
|
+
if (!service) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return service.capabilities.includes(scope.permissionLevel);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/scopes/scope-parser.ts
|
|
158
|
+
var VALID_PERMISSION_LEVELS = new Set(Object.values(PERMISSION_LEVELS));
|
|
159
|
+
var PERMISSION_LEVEL_LIST = [...VALID_PERMISSION_LEVELS].join(", ");
|
|
160
|
+
var ScopeParseError = class extends Error {
|
|
161
|
+
constructor(message, input) {
|
|
162
|
+
super(message);
|
|
163
|
+
this.name = "ScopeParseError";
|
|
164
|
+
this.input = input;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
function doParse(input) {
|
|
168
|
+
if (input.length === 0) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: 'Scope string must not be empty. Expected format: "permission:service" (e.g. "read:gmail").'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (/\s/.test(input)) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: `Scope string "${input}" contains whitespace. Remove any spaces, tabs, or newlines. Expected format: "permission:service" (e.g. "read:gmail").`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const colonIndex = input.indexOf(":");
|
|
181
|
+
if (colonIndex === -1) {
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
error: `Invalid scope string "${input}": missing ":" separator. Expected format: "permission:service" (e.g. "read:gmail").`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (input.includes(":", colonIndex + 1)) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
error: `Invalid scope string "${input}": contains multiple ":" separators. Expected exactly one ":" separating permission and service (e.g. "read:gmail").`
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const permission = input.slice(0, colonIndex);
|
|
194
|
+
const service = input.slice(colonIndex + 1);
|
|
195
|
+
if (permission.length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
error: `Invalid scope string "${input}": permission level is empty. Provide one of: ${PERMISSION_LEVEL_LIST} (e.g. "read:gmail").`
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (!VALID_PERMISSION_LEVELS.has(permission)) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
error: `Unknown permission level "${permission}" in scope string "${input}". Valid permission levels are: ${PERMISSION_LEVEL_LIST}.`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (service.length === 0) {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: `Invalid scope string "${input}": service name is empty. Provide a service name after the ":" (e.g. "read:gmail").`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (!SERVICE_NAME_PATTERN.test(service)) {
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error: `Invalid service name "${service}" in scope string "${input}". Service names must start with a lowercase letter and contain only lowercase letters, digits, hyphens, or underscores (e.g. "gmail", "my-service").`
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
scope: {
|
|
222
|
+
service,
|
|
223
|
+
permissionLevel: permission
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function parseScope(input) {
|
|
228
|
+
const result = doParse(input);
|
|
229
|
+
if (result.success) {
|
|
230
|
+
return result.scope;
|
|
231
|
+
}
|
|
232
|
+
throw new ScopeParseError(result.error, input);
|
|
233
|
+
}
|
|
234
|
+
function parseScopes(inputs) {
|
|
235
|
+
const results = [];
|
|
236
|
+
for (const input of inputs) {
|
|
237
|
+
results.push(parseScope(input));
|
|
238
|
+
}
|
|
239
|
+
return results;
|
|
240
|
+
}
|
|
241
|
+
function tryParseScope(input) {
|
|
242
|
+
return doParse(input);
|
|
243
|
+
}
|
|
244
|
+
function formatScope(scope) {
|
|
245
|
+
return `${scope.permissionLevel}:${scope.service}`;
|
|
246
|
+
}
|
|
247
|
+
function isValidScopeString(input) {
|
|
248
|
+
return doParse(input).success;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/scopes/scope-validator.ts
|
|
252
|
+
function validateScopeAccess(grantedScopes, requested) {
|
|
253
|
+
const isGranted = grantedScopes.some(
|
|
254
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
255
|
+
);
|
|
256
|
+
if (isGranted) {
|
|
257
|
+
return { allowed: true };
|
|
258
|
+
}
|
|
259
|
+
const serviceScopes = grantedScopes.filter((g) => g.service === requested.service);
|
|
260
|
+
if (serviceScopes.length > 0) {
|
|
261
|
+
const grantedLevels = serviceScopes.map((g) => `"${g.permissionLevel}"`).join(", ");
|
|
262
|
+
return {
|
|
263
|
+
allowed: false,
|
|
264
|
+
reason: `Permission "${requested.permissionLevel}" is not granted for service "${requested.service}". Currently granted permission level(s): ${grantedLevels}. Requested scope "${formatScope(requested)}" requires explicit consent.`
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
allowed: false,
|
|
269
|
+
reason: `No permissions granted for service "${requested.service}". The agent has not been authorised to access this service. Request scope "${formatScope(requested)}" via the consent screen.`
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function validateAllScopesAccess(grantedScopes, requestedScopes) {
|
|
273
|
+
if (requestedScopes.length === 0) {
|
|
274
|
+
return { allowed: true };
|
|
275
|
+
}
|
|
276
|
+
for (const requested of requestedScopes) {
|
|
277
|
+
const result = validateScopeAccess(grantedScopes, requested);
|
|
278
|
+
if (!result.allowed) {
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { allowed: true };
|
|
283
|
+
}
|
|
284
|
+
function hasScope(grantedScopes, requested) {
|
|
285
|
+
return grantedScopes.some(
|
|
286
|
+
(granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/consent/scope-labels.ts
|
|
291
|
+
var SERVICE_DISPLAY_NAMES = {
|
|
292
|
+
gmail: "Gmail",
|
|
293
|
+
calendar: "Google Calendar",
|
|
294
|
+
slack: "Slack",
|
|
295
|
+
drive: "Google Drive",
|
|
296
|
+
payments: "Payments",
|
|
297
|
+
github: "GitHub",
|
|
298
|
+
jira: "Jira",
|
|
299
|
+
web: "Web",
|
|
300
|
+
public_content: "Public Content"
|
|
301
|
+
};
|
|
302
|
+
var SERVICE_ICONS = {
|
|
303
|
+
gmail: "\u{1F4E7}",
|
|
304
|
+
calendar: "\u{1F4C5}",
|
|
305
|
+
slack: "\u{1F4AC}",
|
|
306
|
+
drive: "\u{1F4C1}",
|
|
307
|
+
payments: "\u{1F4B3}",
|
|
308
|
+
github: "\u{1F419}",
|
|
309
|
+
jira: "\u{1F3AF}",
|
|
310
|
+
web: "\u{1F310}",
|
|
311
|
+
public_content: "\u{1F4E2}"
|
|
312
|
+
};
|
|
313
|
+
var PERMISSION_DESCRIPTIONS = {
|
|
314
|
+
[PERMISSION_LEVELS.Read]: "Read",
|
|
315
|
+
[PERMISSION_LEVELS.Write]: "Create and modify",
|
|
316
|
+
[PERMISSION_LEVELS.Execute]: "Execute actions",
|
|
317
|
+
[PERMISSION_LEVELS.Publish]: "Publish",
|
|
318
|
+
[PERMISSION_LEVELS.Create]: "Create"
|
|
319
|
+
};
|
|
320
|
+
var PERMISSION_FULL_DESCRIPTIONS = {
|
|
321
|
+
[PERMISSION_LEVELS.Read]: (serviceName) => `Read your ${serviceName}`,
|
|
322
|
+
[PERMISSION_LEVELS.Write]: (serviceName) => `Create and modify ${serviceName} content`,
|
|
323
|
+
[PERMISSION_LEVELS.Execute]: (serviceName) => {
|
|
324
|
+
if (serviceName.toLowerCase().includes("payment")) {
|
|
325
|
+
return "Make purchases on your behalf";
|
|
326
|
+
}
|
|
327
|
+
return `Execute actions in ${serviceName}`;
|
|
328
|
+
},
|
|
329
|
+
[PERMISSION_LEVELS.Publish]: (serviceName, rawServiceName) => {
|
|
330
|
+
if (rawServiceName.toLowerCase() === "web") {
|
|
331
|
+
return "Publish content to the open internet";
|
|
332
|
+
}
|
|
333
|
+
return `Publish ${serviceName} content`;
|
|
334
|
+
},
|
|
335
|
+
[PERMISSION_LEVELS.Create]: (serviceName, rawServiceName) => {
|
|
336
|
+
if (rawServiceName.toLowerCase() === "public_content") {
|
|
337
|
+
return "Create content that is immediately public";
|
|
338
|
+
}
|
|
339
|
+
return `Create ${serviceName}`;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
function getServiceDisplayName(serviceName) {
|
|
343
|
+
return SERVICE_DISPLAY_NAMES[serviceName] ?? capitalizeServiceName(serviceName);
|
|
344
|
+
}
|
|
345
|
+
function getServiceIcon(serviceName) {
|
|
346
|
+
return SERVICE_ICONS[serviceName] ?? "\u{1F50C}";
|
|
347
|
+
}
|
|
348
|
+
function getPermissionLabel(permissionLevel) {
|
|
349
|
+
return PERMISSION_DESCRIPTIONS[permissionLevel];
|
|
350
|
+
}
|
|
351
|
+
function getScopeLabel(scope) {
|
|
352
|
+
const serviceDisplayName = getServiceDisplayName(scope.service);
|
|
353
|
+
const descriptionFn = PERMISSION_FULL_DESCRIPTIONS[scope.permissionLevel];
|
|
354
|
+
return descriptionFn(serviceDisplayName, scope.service);
|
|
355
|
+
}
|
|
356
|
+
function getScopeShortLabel(scope) {
|
|
357
|
+
const serviceDisplayName = getServiceDisplayName(scope.service);
|
|
358
|
+
const permissionLabel = getPermissionLabel(scope.permissionLevel);
|
|
359
|
+
return `${serviceDisplayName}: ${permissionLabel}`;
|
|
360
|
+
}
|
|
361
|
+
function capitalizeServiceName(serviceName) {
|
|
362
|
+
if (serviceName.length === 0) {
|
|
363
|
+
return serviceName;
|
|
364
|
+
}
|
|
365
|
+
return serviceName.charAt(0).toUpperCase() + serviceName.slice(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/scopes/scope-metadata.ts
|
|
369
|
+
var SCOPE_METADATA = /* @__PURE__ */ new Map([
|
|
370
|
+
[
|
|
371
|
+
"publish:web",
|
|
372
|
+
{
|
|
373
|
+
riskLevel: "high",
|
|
374
|
+
requiresExplicitOptIn: true,
|
|
375
|
+
warningMessage: "This agent is requesting permission to publish content publicly on the internet"
|
|
376
|
+
}
|
|
377
|
+
],
|
|
378
|
+
[
|
|
379
|
+
"create:public_content",
|
|
380
|
+
{
|
|
381
|
+
riskLevel: "high",
|
|
382
|
+
requiresExplicitOptIn: true,
|
|
383
|
+
warningMessage: "This agent is requesting permission to publish content publicly on the internet"
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
]);
|
|
387
|
+
function getScopeMetadata(scopeString) {
|
|
388
|
+
return SCOPE_METADATA.get(scopeString);
|
|
389
|
+
}
|
|
390
|
+
function isHighRiskScope(scopeString) {
|
|
391
|
+
const metadata = getScopeMetadata(scopeString);
|
|
392
|
+
return metadata?.riskLevel === "high";
|
|
393
|
+
}
|
|
394
|
+
function requiresExplicitOptIn(scopeString) {
|
|
395
|
+
const metadata = getScopeMetadata(scopeString);
|
|
396
|
+
return metadata?.requiresExplicitOptIn ?? false;
|
|
397
|
+
}
|
|
398
|
+
function getScopeWarning(scopeString) {
|
|
399
|
+
const metadata = getScopeMetadata(scopeString);
|
|
400
|
+
return metadata?.warningMessage;
|
|
401
|
+
}
|
|
402
|
+
var SHIELD_COLORS = {
|
|
403
|
+
bg: "#0d0d14",
|
|
404
|
+
surface: "#14141f",
|
|
405
|
+
surfaceHover: "#1a1a2e",
|
|
406
|
+
border: "#2a2a3d",
|
|
407
|
+
borderLight: "#3a3a52",
|
|
408
|
+
text: "#e8e8f0",
|
|
409
|
+
textMuted: "#8888a0",
|
|
410
|
+
textDim: "#5a5a72",
|
|
411
|
+
accent: "#8b5cf6",
|
|
412
|
+
accentLight: "#a78bfa",
|
|
413
|
+
accentDim: "rgba(139, 92, 246, 0.12)",
|
|
414
|
+
accentGlow: "rgba(139, 92, 246, 0.25)",
|
|
415
|
+
green: "#22c55e",
|
|
416
|
+
greenDim: "rgba(34, 197, 94, 0.12)",
|
|
417
|
+
amber: "#f59e0b",
|
|
418
|
+
amberDim: "rgba(245, 158, 11, 0.12)",
|
|
419
|
+
red: "#ef4444",
|
|
420
|
+
redDim: "rgba(239, 68, 68, 0.12)"
|
|
421
|
+
};
|
|
422
|
+
var consentStyles = lit.css`
|
|
423
|
+
:host {
|
|
424
|
+
display: block;
|
|
425
|
+
font-family:
|
|
426
|
+
"DM Sans",
|
|
427
|
+
system-ui,
|
|
428
|
+
-apple-system,
|
|
429
|
+
BlinkMacSystemFont,
|
|
430
|
+
"Segoe UI",
|
|
431
|
+
Roboto,
|
|
432
|
+
sans-serif;
|
|
433
|
+
/* SECURITY: Every unsafeCSS() call below uses compile-time constants from
|
|
434
|
+
SHIELD_COLORS. Never pass user input or dynamic values to unsafeCSS() as
|
|
435
|
+
it bypasses Lit's CSS sanitisation and would create a CSS injection vector. */
|
|
436
|
+
color: ${lit.unsafeCSS(SHIELD_COLORS.text)};
|
|
437
|
+
--shield-bg: ${lit.unsafeCSS(SHIELD_COLORS.bg)};
|
|
438
|
+
--shield-surface: ${lit.unsafeCSS(SHIELD_COLORS.surface)};
|
|
439
|
+
--shield-surface-hover: ${lit.unsafeCSS(SHIELD_COLORS.surfaceHover)};
|
|
440
|
+
--shield-border: ${lit.unsafeCSS(SHIELD_COLORS.border)};
|
|
441
|
+
--shield-border-light: ${lit.unsafeCSS(SHIELD_COLORS.borderLight)};
|
|
442
|
+
--shield-text: ${lit.unsafeCSS(SHIELD_COLORS.text)};
|
|
443
|
+
--shield-text-muted: ${lit.unsafeCSS(SHIELD_COLORS.textMuted)};
|
|
444
|
+
--shield-text-dim: ${lit.unsafeCSS(SHIELD_COLORS.textDim)};
|
|
445
|
+
--shield-accent: ${lit.unsafeCSS(SHIELD_COLORS.accent)};
|
|
446
|
+
--shield-accent-light: ${lit.unsafeCSS(SHIELD_COLORS.accentLight)};
|
|
447
|
+
--shield-accent-dim: ${lit.unsafeCSS(SHIELD_COLORS.accentDim)};
|
|
448
|
+
--shield-accent-glow: ${lit.unsafeCSS(SHIELD_COLORS.accentGlow)};
|
|
449
|
+
--shield-green: ${lit.unsafeCSS(SHIELD_COLORS.green)};
|
|
450
|
+
--shield-green-dim: ${lit.unsafeCSS(SHIELD_COLORS.greenDim)};
|
|
451
|
+
--shield-amber: ${lit.unsafeCSS(SHIELD_COLORS.amber)};
|
|
452
|
+
--shield-amber-dim: ${lit.unsafeCSS(SHIELD_COLORS.amberDim)};
|
|
453
|
+
--shield-red: ${lit.unsafeCSS(SHIELD_COLORS.red)};
|
|
454
|
+
--shield-red-dim: ${lit.unsafeCSS(SHIELD_COLORS.redDim)};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Modal backdrop */
|
|
458
|
+
.backdrop {
|
|
459
|
+
position: fixed;
|
|
460
|
+
top: 0;
|
|
461
|
+
left: 0;
|
|
462
|
+
right: 0;
|
|
463
|
+
bottom: 0;
|
|
464
|
+
background: rgba(0, 0, 0, 0.75);
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
justify-content: center;
|
|
468
|
+
z-index: 10000;
|
|
469
|
+
padding: 16px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* Main card container */
|
|
473
|
+
.card {
|
|
474
|
+
width: 100%;
|
|
475
|
+
max-width: 420px;
|
|
476
|
+
background: var(--shield-surface);
|
|
477
|
+
border-radius: 20px;
|
|
478
|
+
border: 1px solid var(--shield-border);
|
|
479
|
+
overflow: hidden;
|
|
480
|
+
box-shadow:
|
|
481
|
+
0 0 80px var(--shield-accent-glow),
|
|
482
|
+
0 20px 60px rgba(0, 0, 0, 0.5);
|
|
483
|
+
display: flex;
|
|
484
|
+
flex-direction: column;
|
|
485
|
+
max-height: calc(100vh - 32px);
|
|
486
|
+
overflow-y: auto;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Inline mode: no backdrop, fill parent width */
|
|
490
|
+
:host([mode="inline"]) .backdrop {
|
|
491
|
+
position: static;
|
|
492
|
+
background: transparent;
|
|
493
|
+
padding: 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
:host([mode="inline"]) .card {
|
|
497
|
+
max-width: 100%;
|
|
498
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* Header section */
|
|
502
|
+
.header {
|
|
503
|
+
padding: 24px 24px 20px;
|
|
504
|
+
border-bottom: 1px solid var(--shield-border);
|
|
505
|
+
background: linear-gradient(180deg, rgba(139, 92, 246, 0.06) 0%, transparent 100%);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.header-top {
|
|
509
|
+
display: flex;
|
|
510
|
+
justify-content: space-between;
|
|
511
|
+
align-items: flex-start;
|
|
512
|
+
margin-bottom: 20px;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.verified-badge {
|
|
516
|
+
padding: 3px 10px;
|
|
517
|
+
border-radius: 20px;
|
|
518
|
+
background: var(--shield-green-dim);
|
|
519
|
+
font-size: 11px;
|
|
520
|
+
color: var(--shield-green);
|
|
521
|
+
font-weight: 500;
|
|
522
|
+
text-transform: uppercase;
|
|
523
|
+
letter-spacing: 0.05em;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.agent-info {
|
|
527
|
+
display: flex;
|
|
528
|
+
align-items: center;
|
|
529
|
+
gap: 12px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.agent-icon {
|
|
533
|
+
width: 44px;
|
|
534
|
+
height: 44px;
|
|
535
|
+
border-radius: 12px;
|
|
536
|
+
background: linear-gradient(135deg, var(--shield-accent), #6d28d9);
|
|
537
|
+
display: flex;
|
|
538
|
+
align-items: center;
|
|
539
|
+
justify-content: center;
|
|
540
|
+
font-size: 20px;
|
|
541
|
+
flex-shrink: 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.agent-name {
|
|
545
|
+
font-weight: 600;
|
|
546
|
+
font-size: 16px;
|
|
547
|
+
color: var(--shield-text);
|
|
548
|
+
margin: 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.agent-subtitle {
|
|
552
|
+
font-size: 12px;
|
|
553
|
+
color: var(--shield-text-muted);
|
|
554
|
+
margin: 2px 0 0 0;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/* High-risk warning callout */
|
|
558
|
+
.high-risk-warning {
|
|
559
|
+
display: flex;
|
|
560
|
+
align-items: flex-start;
|
|
561
|
+
gap: 12px;
|
|
562
|
+
padding: 14px 24px;
|
|
563
|
+
margin: 0;
|
|
564
|
+
background: var(--shield-amber-dim);
|
|
565
|
+
border-top: 1px solid var(--shield-border);
|
|
566
|
+
border-bottom: 1px solid var(--shield-border);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.warning-icon {
|
|
570
|
+
font-size: 18px;
|
|
571
|
+
flex-shrink: 0;
|
|
572
|
+
margin-top: 1px;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.warning-text {
|
|
576
|
+
font-size: 12.5px;
|
|
577
|
+
color: var(--shield-text);
|
|
578
|
+
line-height: 1.5;
|
|
579
|
+
margin: 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* Permissions section */
|
|
583
|
+
.permissions {
|
|
584
|
+
padding: 8px 24px 0;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.permissions-title {
|
|
588
|
+
font-size: 11px;
|
|
589
|
+
font-weight: 600;
|
|
590
|
+
color: var(--shield-text-dim);
|
|
591
|
+
text-transform: uppercase;
|
|
592
|
+
letter-spacing: 0.08em;
|
|
593
|
+
margin: 12px 0 4px 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.permission-row {
|
|
597
|
+
display: flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
gap: 14px;
|
|
600
|
+
padding: 14px 0;
|
|
601
|
+
border-bottom: 1px solid var(--shield-border);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.permission-row:last-child {
|
|
605
|
+
border-bottom: none;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.permission-icon {
|
|
609
|
+
width: 36px;
|
|
610
|
+
height: 36px;
|
|
611
|
+
border-radius: 10px;
|
|
612
|
+
background: var(--shield-accent-dim);
|
|
613
|
+
display: flex;
|
|
614
|
+
align-items: center;
|
|
615
|
+
justify-content: center;
|
|
616
|
+
font-size: 16px;
|
|
617
|
+
flex-shrink: 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.permission-content {
|
|
621
|
+
flex: 1;
|
|
622
|
+
min-width: 0;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.permission-title {
|
|
626
|
+
font-weight: 500;
|
|
627
|
+
font-size: 13.5px;
|
|
628
|
+
color: var(--shield-text);
|
|
629
|
+
margin: 0;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.permission-description {
|
|
633
|
+
font-size: 11.5px;
|
|
634
|
+
color: var(--shield-text-muted);
|
|
635
|
+
margin: 2px 0 0 0;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.permission-levels {
|
|
639
|
+
display: flex;
|
|
640
|
+
gap: 6px;
|
|
641
|
+
margin-top: 8px;
|
|
642
|
+
flex-wrap: wrap;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.permission-level-button {
|
|
646
|
+
padding: 3px 10px;
|
|
647
|
+
border-radius: 6px;
|
|
648
|
+
border: 1px solid var(--shield-border);
|
|
649
|
+
background: transparent;
|
|
650
|
+
color: var(--shield-text-dim);
|
|
651
|
+
font-family: inherit;
|
|
652
|
+
font-size: 11px;
|
|
653
|
+
cursor: pointer;
|
|
654
|
+
transition: all 0.15s;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.permission-level-button:focus-visible {
|
|
658
|
+
outline: 2px solid var(--shield-accent);
|
|
659
|
+
outline-offset: 2px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.permission-level-button.active {
|
|
663
|
+
border-color: var(--shield-accent);
|
|
664
|
+
background: var(--shield-accent-dim);
|
|
665
|
+
color: var(--shield-accent-light);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/* Toggle switch */
|
|
669
|
+
.toggle {
|
|
670
|
+
width: 40px;
|
|
671
|
+
height: 22px;
|
|
672
|
+
border-radius: 11px;
|
|
673
|
+
border: none;
|
|
674
|
+
background: var(--shield-border);
|
|
675
|
+
cursor: pointer;
|
|
676
|
+
position: relative;
|
|
677
|
+
transition: background 0.2s;
|
|
678
|
+
flex-shrink: 0;
|
|
679
|
+
padding: 0;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.toggle:focus-visible {
|
|
683
|
+
outline: 2px solid var(--shield-accent);
|
|
684
|
+
outline-offset: 2px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.toggle.enabled {
|
|
688
|
+
background: var(--shield-accent);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.toggle-thumb {
|
|
692
|
+
width: 16px;
|
|
693
|
+
height: 16px;
|
|
694
|
+
border-radius: 8px;
|
|
695
|
+
background: #fff;
|
|
696
|
+
position: absolute;
|
|
697
|
+
top: 3px;
|
|
698
|
+
left: 3px;
|
|
699
|
+
transition: left 0.2s;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.toggle.enabled .toggle-thumb {
|
|
703
|
+
left: 21px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* Spending limit section */
|
|
707
|
+
.spending-limit {
|
|
708
|
+
padding: 16px 24px;
|
|
709
|
+
border-top: 1px solid var(--shield-border);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.spending-limit-header {
|
|
713
|
+
display: flex;
|
|
714
|
+
justify-content: space-between;
|
|
715
|
+
align-items: center;
|
|
716
|
+
margin-bottom: 12px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.spending-limit-label {
|
|
720
|
+
font-weight: 500;
|
|
721
|
+
font-size: 13px;
|
|
722
|
+
color: var(--shield-text);
|
|
723
|
+
margin: 0;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.spending-limit-description {
|
|
727
|
+
font-size: 11.5px;
|
|
728
|
+
color: var(--shield-text-muted);
|
|
729
|
+
margin: 2px 0 0 0;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.spending-limit-amount {
|
|
733
|
+
font-family: "DM Mono", "Courier New", monospace;
|
|
734
|
+
font-size: 18px;
|
|
735
|
+
font-weight: 600;
|
|
736
|
+
color: var(--shield-accent);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/* Editable spending limit control */
|
|
740
|
+
.spending-limit-control {
|
|
741
|
+
display: flex;
|
|
742
|
+
align-items: center;
|
|
743
|
+
gap: 6px;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.spend-step-btn {
|
|
747
|
+
width: 28px;
|
|
748
|
+
height: 28px;
|
|
749
|
+
border-radius: 8px;
|
|
750
|
+
border: 1px solid var(--shield-border);
|
|
751
|
+
background: transparent;
|
|
752
|
+
color: var(--shield-text-muted);
|
|
753
|
+
font-size: 16px;
|
|
754
|
+
font-family: inherit;
|
|
755
|
+
cursor: pointer;
|
|
756
|
+
display: flex;
|
|
757
|
+
align-items: center;
|
|
758
|
+
justify-content: center;
|
|
759
|
+
transition: all 0.15s;
|
|
760
|
+
padding: 0;
|
|
761
|
+
line-height: 1;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.spend-step-btn:hover:not(:disabled) {
|
|
765
|
+
background: var(--shield-surface-hover);
|
|
766
|
+
color: var(--shield-text);
|
|
767
|
+
border-color: var(--shield-accent);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.spend-step-btn:disabled {
|
|
771
|
+
opacity: 0.3;
|
|
772
|
+
cursor: default;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.spend-step-btn:focus-visible {
|
|
776
|
+
outline: 2px solid var(--shield-accent);
|
|
777
|
+
outline-offset: 2px;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.spend-input-wrap {
|
|
781
|
+
display: flex;
|
|
782
|
+
align-items: center;
|
|
783
|
+
gap: 1px;
|
|
784
|
+
background: var(--shield-accent-dim);
|
|
785
|
+
border: 1px solid var(--shield-accent);
|
|
786
|
+
border-radius: 8px;
|
|
787
|
+
padding: 4px 10px;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.spend-currency {
|
|
791
|
+
font-family: "DM Mono", "Courier New", monospace;
|
|
792
|
+
font-size: 16px;
|
|
793
|
+
font-weight: 600;
|
|
794
|
+
color: var(--shield-accent);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.spend-input {
|
|
798
|
+
font-family: "DM Mono", "Courier New", monospace;
|
|
799
|
+
font-size: 16px;
|
|
800
|
+
font-weight: 600;
|
|
801
|
+
color: var(--shield-accent-light);
|
|
802
|
+
background: transparent;
|
|
803
|
+
border: none;
|
|
804
|
+
outline: none;
|
|
805
|
+
width: 60px;
|
|
806
|
+
text-align: left;
|
|
807
|
+
padding: 0;
|
|
808
|
+
-moz-appearance: textfield;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.spend-input::-webkit-outer-spin-button,
|
|
812
|
+
.spend-input::-webkit-inner-spin-button {
|
|
813
|
+
-webkit-appearance: none;
|
|
814
|
+
margin: 0;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.spending-limit-hint {
|
|
818
|
+
font-size: 11px;
|
|
819
|
+
color: var(--shield-text-dim);
|
|
820
|
+
margin-top: 8px;
|
|
821
|
+
text-align: right;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* Actions section */
|
|
825
|
+
.actions {
|
|
826
|
+
padding: 12px 24px 24px;
|
|
827
|
+
display: flex;
|
|
828
|
+
gap: 10px;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.button {
|
|
832
|
+
padding: 12px 0;
|
|
833
|
+
border-radius: 12px;
|
|
834
|
+
font-family: inherit;
|
|
835
|
+
font-weight: 500;
|
|
836
|
+
font-size: 14px;
|
|
837
|
+
cursor: pointer;
|
|
838
|
+
transition: all 0.2s;
|
|
839
|
+
border: none;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.button:focus-visible {
|
|
843
|
+
outline: 2px solid var(--shield-accent);
|
|
844
|
+
outline-offset: 2px;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.button-secondary {
|
|
848
|
+
flex: 1;
|
|
849
|
+
background: transparent;
|
|
850
|
+
border: 1px solid var(--shield-border);
|
|
851
|
+
color: var(--shield-text-muted);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.button-secondary:hover {
|
|
855
|
+
background: var(--shield-surface-hover);
|
|
856
|
+
color: var(--shield-text);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.button-primary {
|
|
860
|
+
flex: 2;
|
|
861
|
+
background: linear-gradient(135deg, var(--shield-accent), #6d28d9);
|
|
862
|
+
color: #fff;
|
|
863
|
+
font-weight: 600;
|
|
864
|
+
box-shadow: 0 4px 20px var(--shield-accent-glow);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.button-primary:hover {
|
|
868
|
+
box-shadow: 0 6px 24px var(--shield-accent-glow);
|
|
869
|
+
transform: translateY(-1px);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.button-primary:active {
|
|
873
|
+
transform: translateY(0);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/* Reduced motion support */
|
|
877
|
+
@media (prefers-reduced-motion: reduce) {
|
|
878
|
+
*,
|
|
879
|
+
*::before,
|
|
880
|
+
*::after {
|
|
881
|
+
animation-duration: 0.01ms !important;
|
|
882
|
+
animation-iteration-count: 1 !important;
|
|
883
|
+
transition-duration: 0.01ms !important;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/* Responsive: small viewports (≤480px) */
|
|
888
|
+
@media (max-width: 480px) {
|
|
889
|
+
.card {
|
|
890
|
+
max-width: 100%;
|
|
891
|
+
border-radius: 16px;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.header {
|
|
895
|
+
padding: 20px 16px 16px;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.permissions {
|
|
899
|
+
padding: 8px 16px 0;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.permission-row {
|
|
903
|
+
gap: 10px;
|
|
904
|
+
padding: 12px 0;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.permission-icon {
|
|
908
|
+
width: 32px;
|
|
909
|
+
height: 32px;
|
|
910
|
+
font-size: 14px;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.spending-limit {
|
|
914
|
+
padding: 12px 16px;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.actions {
|
|
918
|
+
padding: 12px 16px 20px;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/* Responsive: very small viewports (≤375px) */
|
|
923
|
+
@media (max-width: 375px) {
|
|
924
|
+
.header {
|
|
925
|
+
padding: 16px 12px 12px;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.agent-icon {
|
|
929
|
+
width: 36px;
|
|
930
|
+
height: 36px;
|
|
931
|
+
font-size: 16px;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.agent-name {
|
|
935
|
+
font-size: 14px;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.permissions {
|
|
939
|
+
padding: 4px 12px 0;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.permission-row {
|
|
943
|
+
gap: 8px;
|
|
944
|
+
padding: 10px 0;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.permission-description {
|
|
948
|
+
font-size: 10.5px;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
.spending-limit {
|
|
952
|
+
padding: 10px 12px;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.spending-limit-amount {
|
|
956
|
+
font-size: 16px;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.actions {
|
|
960
|
+
padding: 10px 12px 16px;
|
|
961
|
+
flex-direction: column;
|
|
962
|
+
gap: 8px;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.button {
|
|
966
|
+
padding: 10px 0;
|
|
967
|
+
font-size: 13px;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.button-primary,
|
|
971
|
+
.button-secondary {
|
|
972
|
+
flex: 1;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/* Hidden class for conditional rendering */
|
|
977
|
+
.hidden {
|
|
978
|
+
display: none !important;
|
|
979
|
+
}
|
|
980
|
+
`;
|
|
981
|
+
|
|
982
|
+
// src/consent/focus-trap.ts
|
|
983
|
+
var FOCUSABLE_SELECTOR = [
|
|
984
|
+
"button:not([disabled])",
|
|
985
|
+
"[href]",
|
|
986
|
+
"input:not([disabled])",
|
|
987
|
+
"select:not([disabled])",
|
|
988
|
+
"textarea:not([disabled])",
|
|
989
|
+
"[tabindex]:not([tabindex='-1'])"
|
|
990
|
+
].join(", ");
|
|
991
|
+
function createFocusTrap(container, initialFocus) {
|
|
992
|
+
let previousActiveElement = null;
|
|
993
|
+
let keydownHandler = null;
|
|
994
|
+
function getFocusableElements() {
|
|
995
|
+
const elements = [];
|
|
996
|
+
const directElements = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
|
|
997
|
+
elements.push(...directElements);
|
|
998
|
+
if (container.shadowRoot) {
|
|
999
|
+
const shadowElements = Array.from(
|
|
1000
|
+
container.shadowRoot.querySelectorAll(FOCUSABLE_SELECTOR)
|
|
1001
|
+
);
|
|
1002
|
+
elements.push(...shadowElements);
|
|
1003
|
+
}
|
|
1004
|
+
return elements.filter((el) => {
|
|
1005
|
+
const style = window.getComputedStyle(el);
|
|
1006
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
function handleKeyDown(e) {
|
|
1010
|
+
if (e.key !== "Tab") {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const focusableElements = getFocusableElements();
|
|
1014
|
+
if (focusableElements.length === 0) {
|
|
1015
|
+
e.preventDefault();
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const firstElement = focusableElements[0];
|
|
1019
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
1020
|
+
if (!firstElement || !lastElement) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const currentElement = document.activeElement;
|
|
1024
|
+
if (e.shiftKey && currentElement === firstElement) {
|
|
1025
|
+
e.preventDefault();
|
|
1026
|
+
lastElement.focus();
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (!e.shiftKey && currentElement === lastElement) {
|
|
1030
|
+
e.preventDefault();
|
|
1031
|
+
firstElement.focus();
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (!focusableElements.includes(currentElement)) {
|
|
1035
|
+
e.preventDefault();
|
|
1036
|
+
firstElement.focus();
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
activate() {
|
|
1041
|
+
previousActiveElement = document.activeElement;
|
|
1042
|
+
keydownHandler = handleKeyDown;
|
|
1043
|
+
container.addEventListener("keydown", keydownHandler, true);
|
|
1044
|
+
const focusableElements = getFocusableElements();
|
|
1045
|
+
if (focusableElements.length > 0) {
|
|
1046
|
+
const firstElement = focusableElements[0];
|
|
1047
|
+
if (firstElement == null) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const target = initialFocus ?? firstElement;
|
|
1051
|
+
if (focusableElements.includes(target)) {
|
|
1052
|
+
target.focus();
|
|
1053
|
+
} else {
|
|
1054
|
+
firstElement.focus();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
deactivate() {
|
|
1059
|
+
if (keydownHandler) {
|
|
1060
|
+
container.removeEventListener("keydown", keydownHandler, true);
|
|
1061
|
+
keydownHandler = null;
|
|
1062
|
+
}
|
|
1063
|
+
if (previousActiveElement && document.body.contains(previousActiveElement)) {
|
|
1064
|
+
previousActiveElement.focus();
|
|
1065
|
+
}
|
|
1066
|
+
previousActiveElement = null;
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/consent/multicorn-consent.ts
|
|
1072
|
+
var CONSENT_ELEMENT_TAG = "multicorn-consent";
|
|
1073
|
+
var DEFAULT_AGENT_COLOR = "#8b5cf6";
|
|
1074
|
+
var HEX_COLOR_PATTERN = /^#[\da-fA-F]{3,8}$/;
|
|
1075
|
+
function sanitizeColor(color) {
|
|
1076
|
+
return HEX_COLOR_PATTERN.test(color) ? color : DEFAULT_AGENT_COLOR;
|
|
1077
|
+
}
|
|
1078
|
+
function groupScopesByService(scopes) {
|
|
1079
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1080
|
+
for (const scope of scopes) {
|
|
1081
|
+
if (!grouped.has(scope.service)) {
|
|
1082
|
+
grouped.set(scope.service, /* @__PURE__ */ new Set());
|
|
1083
|
+
}
|
|
1084
|
+
const serviceSet = grouped.get(scope.service);
|
|
1085
|
+
if (serviceSet) {
|
|
1086
|
+
serviceSet.add(scope.permissionLevel);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return grouped;
|
|
1090
|
+
}
|
|
1091
|
+
function scopeKey(scope) {
|
|
1092
|
+
return `${scope.service}:${scope.permissionLevel}`;
|
|
1093
|
+
}
|
|
1094
|
+
exports.MulticornConsent = class MulticornConsent extends lit.LitElement {
|
|
1095
|
+
constructor() {
|
|
1096
|
+
super(...arguments);
|
|
1097
|
+
this.agentName = "";
|
|
1098
|
+
this.agentColor = DEFAULT_AGENT_COLOR;
|
|
1099
|
+
this.scopes = [];
|
|
1100
|
+
this.spendLimit = 0;
|
|
1101
|
+
this.mode = "modal";
|
|
1102
|
+
this._grantedScopes = /* @__PURE__ */ new Set();
|
|
1103
|
+
this._adjustedSpendLimit = 0;
|
|
1104
|
+
this._isOpen = true;
|
|
1105
|
+
/**
|
|
1106
|
+
* Focus trap instance (only used in modal mode).
|
|
1107
|
+
*/
|
|
1108
|
+
this._focusTrap = null;
|
|
1109
|
+
/**
|
|
1110
|
+
* Previously focused element (for restoring focus after modal closes).
|
|
1111
|
+
*/
|
|
1112
|
+
this._previousActiveElement = null;
|
|
1113
|
+
/**
|
|
1114
|
+
* Handle keyboard events (Escape to deny).
|
|
1115
|
+
*/
|
|
1116
|
+
this._handleKeyDown = (e) => {
|
|
1117
|
+
if (e.key === "Escape" && this.mode === "modal" && this._isOpen) {
|
|
1118
|
+
e.preventDefault();
|
|
1119
|
+
this._emitDenied();
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
/**
|
|
1123
|
+
* Handle direct input of the spending limit.
|
|
1124
|
+
*/
|
|
1125
|
+
this._handleSpendLimitInput = (e) => {
|
|
1126
|
+
const input = e.target;
|
|
1127
|
+
const value = parseInt(input.value, 10);
|
|
1128
|
+
this._adjustedSpendLimit = Number.isNaN(value) ? 0 : Math.max(0, Math.min(this.spendLimit, value));
|
|
1129
|
+
};
|
|
1130
|
+
/**
|
|
1131
|
+
* Handle the "Authorize" button click.
|
|
1132
|
+
*/
|
|
1133
|
+
this._handleAuthorize = () => {
|
|
1134
|
+
const granted = [];
|
|
1135
|
+
const denied = [];
|
|
1136
|
+
const scopes = Array.isArray(this.scopes) ? this.scopes : [];
|
|
1137
|
+
for (const scope of scopes) {
|
|
1138
|
+
if (this._isScopeGranted(scope)) {
|
|
1139
|
+
granted.push(scope);
|
|
1140
|
+
} else {
|
|
1141
|
+
denied.push(scope);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (granted.length === 0) {
|
|
1145
|
+
this._emitDenied();
|
|
1146
|
+
} else if (denied.length === 0) {
|
|
1147
|
+
this._emitGranted(granted);
|
|
1148
|
+
} else {
|
|
1149
|
+
this._emitPartial(granted, denied);
|
|
1150
|
+
}
|
|
1151
|
+
if (this.mode === "modal") {
|
|
1152
|
+
this._isOpen = false;
|
|
1153
|
+
this._cleanupFocusTrap();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
/**
|
|
1157
|
+
* Handle the "Deny" button click.
|
|
1158
|
+
*/
|
|
1159
|
+
this._handleDeny = () => {
|
|
1160
|
+
this._emitDenied();
|
|
1161
|
+
if (this.mode === "modal") {
|
|
1162
|
+
this._isOpen = false;
|
|
1163
|
+
this._cleanupFocusTrap();
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
connectedCallback() {
|
|
1168
|
+
super.connectedCallback();
|
|
1169
|
+
if (Array.isArray(this.scopes) && this.scopes.length > 0) {
|
|
1170
|
+
this._initializeGrantedScopes();
|
|
1171
|
+
}
|
|
1172
|
+
this.addEventListener("keydown", this._handleKeyDown);
|
|
1173
|
+
}
|
|
1174
|
+
disconnectedCallback() {
|
|
1175
|
+
super.disconnectedCallback();
|
|
1176
|
+
this.removeEventListener("keydown", this._handleKeyDown);
|
|
1177
|
+
this._cleanupFocusTrap();
|
|
1178
|
+
}
|
|
1179
|
+
willUpdate(changedProperties) {
|
|
1180
|
+
super.willUpdate(changedProperties);
|
|
1181
|
+
if (changedProperties.has("scopes")) {
|
|
1182
|
+
if (!Array.isArray(this.scopes)) {
|
|
1183
|
+
const scopesAttr = this.getAttribute("scopes");
|
|
1184
|
+
if (scopesAttr) {
|
|
1185
|
+
try {
|
|
1186
|
+
const parsed = JSON.parse(scopesAttr);
|
|
1187
|
+
if (Array.isArray(parsed)) {
|
|
1188
|
+
this.scopes = parsed;
|
|
1189
|
+
}
|
|
1190
|
+
} catch {
|
|
1191
|
+
this.scopes = [];
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
this.scopes = [];
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
this._initializeGrantedScopes();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
updated(changedProperties) {
|
|
1201
|
+
super.updated(changedProperties);
|
|
1202
|
+
if (changedProperties.has("mode")) {
|
|
1203
|
+
this._updateFocusTrap();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
firstUpdated(changedProperties) {
|
|
1207
|
+
super.firstUpdated(changedProperties);
|
|
1208
|
+
this._updateFocusTrap();
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Initialize the granted scopes set with all requested scopes.
|
|
1212
|
+
* High-risk scopes that require explicit opt-in are defaulted to OFF.
|
|
1213
|
+
*/
|
|
1214
|
+
_initializeGrantedScopes() {
|
|
1215
|
+
const scopes = Array.isArray(this.scopes) ? this.scopes : [];
|
|
1216
|
+
const granted = /* @__PURE__ */ new Set();
|
|
1217
|
+
for (const scope of scopes) {
|
|
1218
|
+
const scopeStr = formatScope(scope);
|
|
1219
|
+
if (!requiresExplicitOptIn(scopeStr)) {
|
|
1220
|
+
granted.add(scopeKey(scope));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
this._grantedScopes = granted;
|
|
1224
|
+
this._adjustedSpendLimit = this.spendLimit;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Update the focus trap based on current mode and open state.
|
|
1228
|
+
*/
|
|
1229
|
+
_updateFocusTrap() {
|
|
1230
|
+
this._cleanupFocusTrap();
|
|
1231
|
+
if (this.mode === "modal" && this._isOpen) {
|
|
1232
|
+
const firstFocusable = this.shadowRoot?.querySelector(
|
|
1233
|
+
'button, [href], input, [tabindex]:not([tabindex="-1"])'
|
|
1234
|
+
);
|
|
1235
|
+
this._focusTrap = createFocusTrap(this, firstFocusable ?? void 0);
|
|
1236
|
+
this._focusTrap.activate();
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Clean up the focus trap.
|
|
1241
|
+
*/
|
|
1242
|
+
_cleanupFocusTrap() {
|
|
1243
|
+
if (this._focusTrap) {
|
|
1244
|
+
this._focusTrap.deactivate();
|
|
1245
|
+
this._focusTrap = null;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Toggle a specific scope's granted state.
|
|
1250
|
+
*
|
|
1251
|
+
* @param scope - The scope to toggle.
|
|
1252
|
+
*/
|
|
1253
|
+
_toggleScope(scope) {
|
|
1254
|
+
const key = scopeKey(scope);
|
|
1255
|
+
if (this._grantedScopes.has(key)) {
|
|
1256
|
+
this._grantedScopes.delete(key);
|
|
1257
|
+
} else {
|
|
1258
|
+
this._grantedScopes.add(key);
|
|
1259
|
+
}
|
|
1260
|
+
this._grantedScopes = new Set(this._grantedScopes);
|
|
1261
|
+
this.requestUpdate();
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Check if a scope is currently granted.
|
|
1265
|
+
*
|
|
1266
|
+
* @param scope - The scope to check.
|
|
1267
|
+
* @returns True if the scope is granted.
|
|
1268
|
+
*/
|
|
1269
|
+
_isScopeGranted(scope) {
|
|
1270
|
+
return this._grantedScopes.has(scopeKey(scope));
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Adjust the spending limit.
|
|
1274
|
+
* Clamps between 0 and the original requested spendLimit.
|
|
1275
|
+
*/
|
|
1276
|
+
_adjustSpendLimit(delta) {
|
|
1277
|
+
this._adjustedSpendLimit = Math.max(
|
|
1278
|
+
0,
|
|
1279
|
+
Math.min(this.spendLimit, this._adjustedSpendLimit + delta)
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Emit a consent-granted event.
|
|
1284
|
+
*/
|
|
1285
|
+
_emitGranted(grantedScopes) {
|
|
1286
|
+
const detail = {
|
|
1287
|
+
grantedScopes,
|
|
1288
|
+
spendLimit: this._adjustedSpendLimit,
|
|
1289
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1290
|
+
};
|
|
1291
|
+
this.dispatchEvent(
|
|
1292
|
+
new CustomEvent("consent-granted", {
|
|
1293
|
+
detail,
|
|
1294
|
+
bubbles: true,
|
|
1295
|
+
composed: true
|
|
1296
|
+
})
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Emit a consent-partial event.
|
|
1301
|
+
*/
|
|
1302
|
+
_emitPartial(grantedScopes, deniedScopes) {
|
|
1303
|
+
const detail = {
|
|
1304
|
+
grantedScopes,
|
|
1305
|
+
deniedScopes,
|
|
1306
|
+
spendLimit: this._adjustedSpendLimit,
|
|
1307
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1308
|
+
};
|
|
1309
|
+
this.dispatchEvent(
|
|
1310
|
+
new CustomEvent("consent-partial", {
|
|
1311
|
+
detail,
|
|
1312
|
+
bubbles: true,
|
|
1313
|
+
composed: true
|
|
1314
|
+
})
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Emit a consent-denied event.
|
|
1319
|
+
*/
|
|
1320
|
+
_emitDenied() {
|
|
1321
|
+
const scopes = Array.isArray(this.scopes) ? this.scopes : [];
|
|
1322
|
+
const detail = {
|
|
1323
|
+
deniedScopes: [...scopes],
|
|
1324
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1325
|
+
};
|
|
1326
|
+
this.dispatchEvent(
|
|
1327
|
+
new CustomEvent("consent-denied", {
|
|
1328
|
+
detail,
|
|
1329
|
+
bubbles: true,
|
|
1330
|
+
composed: true
|
|
1331
|
+
})
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Get parsed scopes (already handled by property converter).
|
|
1336
|
+
*/
|
|
1337
|
+
_getParsedScopes() {
|
|
1338
|
+
return Array.isArray(this.scopes) ? this.scopes : [];
|
|
1339
|
+
}
|
|
1340
|
+
render() {
|
|
1341
|
+
const parsedScopes = this._getParsedScopes();
|
|
1342
|
+
if (this.mode === "modal" && !this._isOpen) {
|
|
1343
|
+
return lit.html``;
|
|
1344
|
+
}
|
|
1345
|
+
if (parsedScopes.length === 0) {
|
|
1346
|
+
return lit.html`
|
|
1347
|
+
<div class="card">
|
|
1348
|
+
<div class="header">
|
|
1349
|
+
<h2 class="agent-name">${this.agentName || "Agent"}</h2>
|
|
1350
|
+
<p class="agent-subtitle" style="color: var(--shield-text-dim);">
|
|
1351
|
+
No permissions requested.
|
|
1352
|
+
</p>
|
|
1353
|
+
</div>
|
|
1354
|
+
</div>
|
|
1355
|
+
`;
|
|
1356
|
+
}
|
|
1357
|
+
const groupedScopes = groupScopesByService(parsedScopes);
|
|
1358
|
+
const isModal = this.mode === "modal" && this._isOpen;
|
|
1359
|
+
const highRiskScopes = parsedScopes.filter((scope) => isHighRiskScope(formatScope(scope)));
|
|
1360
|
+
const firstHighRiskScope = highRiskScopes[0];
|
|
1361
|
+
const warningMessage = firstHighRiskScope !== void 0 ? getScopeWarning(formatScope(firstHighRiskScope)) : void 0;
|
|
1362
|
+
const content = lit.html`
|
|
1363
|
+
<div
|
|
1364
|
+
class="card"
|
|
1365
|
+
role="dialog"
|
|
1366
|
+
aria-modal="${isModal ? "true" : "false"}"
|
|
1367
|
+
aria-labelledby="agent-name"
|
|
1368
|
+
>
|
|
1369
|
+
<!-- Header -->
|
|
1370
|
+
<div class="header">
|
|
1371
|
+
<div class="header-top">
|
|
1372
|
+
<div class="verified-badge">Verified Agent</div>
|
|
1373
|
+
</div>
|
|
1374
|
+
<div class="agent-info">
|
|
1375
|
+
<div
|
|
1376
|
+
class="agent-icon"
|
|
1377
|
+
style="background: linear-gradient(135deg, ${sanitizeColor(
|
|
1378
|
+
this.agentColor
|
|
1379
|
+
)}, #6d28d9);"
|
|
1380
|
+
>
|
|
1381
|
+
🤖
|
|
1382
|
+
</div>
|
|
1383
|
+
<div>
|
|
1384
|
+
<h2 class="agent-name" id="agent-name">${this.agentName || "Agent"}</h2>
|
|
1385
|
+
<p class="agent-subtitle">wants access to your services</p>
|
|
1386
|
+
</div>
|
|
1387
|
+
</div>
|
|
1388
|
+
</div>
|
|
1389
|
+
|
|
1390
|
+
<!-- High-Risk Warning -->
|
|
1391
|
+
${warningMessage ? lit.html`
|
|
1392
|
+
<div class="high-risk-warning">
|
|
1393
|
+
<div class="warning-icon">⚠️</div>
|
|
1394
|
+
<div class="warning-text">${warningMessage}</div>
|
|
1395
|
+
</div>
|
|
1396
|
+
` : lit.html``}
|
|
1397
|
+
|
|
1398
|
+
<!-- Permissions -->
|
|
1399
|
+
<div class="permissions">
|
|
1400
|
+
<div class="permissions-title">Permissions</div>
|
|
1401
|
+
${Array.from(groupedScopes.entries()).map(
|
|
1402
|
+
([service, permissionLevels]) => {
|
|
1403
|
+
const serviceDisplayName = getServiceDisplayName(service);
|
|
1404
|
+
const serviceIcon = getServiceIcon(service);
|
|
1405
|
+
const serviceScopes = parsedScopes.filter((s) => s.service === service);
|
|
1406
|
+
return lit.html`
|
|
1407
|
+
<div class="permission-row">
|
|
1408
|
+
<div class="permission-icon">${serviceIcon}</div>
|
|
1409
|
+
<div class="permission-content">
|
|
1410
|
+
<div class="permission-title">${serviceDisplayName}</div>
|
|
1411
|
+
<div class="permission-description">
|
|
1412
|
+
${serviceScopes.map((scope) => getScopeLabel(scope)).join(", ")}
|
|
1413
|
+
</div>
|
|
1414
|
+
<div class="permission-levels">
|
|
1415
|
+
${Array.from(permissionLevels).map((level) => {
|
|
1416
|
+
const scope = { service, permissionLevel: level };
|
|
1417
|
+
const isGranted = this._isScopeGranted(scope);
|
|
1418
|
+
const label = getScopeShortLabel(scope);
|
|
1419
|
+
return lit.html`
|
|
1420
|
+
<button
|
|
1421
|
+
class="permission-level-button ${isGranted ? "active" : ""}"
|
|
1422
|
+
@click="${() => {
|
|
1423
|
+
this._toggleScope(scope);
|
|
1424
|
+
}}"
|
|
1425
|
+
aria-label="Toggle ${label}"
|
|
1426
|
+
type="button"
|
|
1427
|
+
>
|
|
1428
|
+
${level.charAt(0).toUpperCase() + level.slice(1)}
|
|
1429
|
+
</button>
|
|
1430
|
+
`;
|
|
1431
|
+
})}
|
|
1432
|
+
</div>
|
|
1433
|
+
</div>
|
|
1434
|
+
<button
|
|
1435
|
+
class="toggle ${this._areAllServiceScopesGranted(service, serviceScopes) ? "enabled" : ""}"
|
|
1436
|
+
@click="${() => {
|
|
1437
|
+
this._toggleAllServiceScopes(service, serviceScopes);
|
|
1438
|
+
}}"
|
|
1439
|
+
aria-label="Toggle all permissions for ${serviceDisplayName}"
|
|
1440
|
+
type="button"
|
|
1441
|
+
>
|
|
1442
|
+
<div class="toggle-thumb"></div>
|
|
1443
|
+
</button>
|
|
1444
|
+
</div>
|
|
1445
|
+
`;
|
|
1446
|
+
}
|
|
1447
|
+
)}
|
|
1448
|
+
</div>
|
|
1449
|
+
|
|
1450
|
+
<!-- Spending Limit -->
|
|
1451
|
+
${this.spendLimit > 0 ? lit.html`
|
|
1452
|
+
<div class="spending-limit">
|
|
1453
|
+
<div class="spending-limit-header">
|
|
1454
|
+
<div>
|
|
1455
|
+
<div class="spending-limit-label">Spending limit</div>
|
|
1456
|
+
<div class="spending-limit-description">Per transaction without approval</div>
|
|
1457
|
+
</div>
|
|
1458
|
+
<div class="spending-limit-control">
|
|
1459
|
+
<button
|
|
1460
|
+
class="spend-step-btn"
|
|
1461
|
+
@click="${() => {
|
|
1462
|
+
this._adjustSpendLimit(-10);
|
|
1463
|
+
}}"
|
|
1464
|
+
aria-label="Decrease spending limit"
|
|
1465
|
+
type="button"
|
|
1466
|
+
?disabled="${this._adjustedSpendLimit <= 0}"
|
|
1467
|
+
>
|
|
1468
|
+
−
|
|
1469
|
+
</button>
|
|
1470
|
+
<div class="spend-input-wrap">
|
|
1471
|
+
<span class="spend-currency">$</span>
|
|
1472
|
+
<input
|
|
1473
|
+
class="spend-input"
|
|
1474
|
+
type="number"
|
|
1475
|
+
min="0"
|
|
1476
|
+
max="${this.spendLimit}"
|
|
1477
|
+
.value="${String(this._adjustedSpendLimit)}"
|
|
1478
|
+
@change="${this._handleSpendLimitInput}"
|
|
1479
|
+
aria-label="Spending limit amount"
|
|
1480
|
+
/>
|
|
1481
|
+
</div>
|
|
1482
|
+
<button
|
|
1483
|
+
class="spend-step-btn"
|
|
1484
|
+
@click="${() => {
|
|
1485
|
+
this._adjustSpendLimit(10);
|
|
1486
|
+
}}"
|
|
1487
|
+
aria-label="Increase spending limit"
|
|
1488
|
+
type="button"
|
|
1489
|
+
?disabled="${this._adjustedSpendLimit >= this.spendLimit}"
|
|
1490
|
+
>
|
|
1491
|
+
+
|
|
1492
|
+
</button>
|
|
1493
|
+
</div>
|
|
1494
|
+
</div>
|
|
1495
|
+
${this._adjustedSpendLimit < this.spendLimit ? lit.html`<div class="spending-limit-hint">
|
|
1496
|
+
Agent requested $${this.spendLimit}
|
|
1497
|
+
</div>` : lit.html``}
|
|
1498
|
+
</div>
|
|
1499
|
+
` : lit.html``}
|
|
1500
|
+
|
|
1501
|
+
<!-- Actions -->
|
|
1502
|
+
<div class="actions">
|
|
1503
|
+
<button class="button button-secondary" @click="${this._handleDeny}" type="button">
|
|
1504
|
+
Deny
|
|
1505
|
+
</button>
|
|
1506
|
+
<button class="button button-primary" @click="${this._handleAuthorize}" type="button">
|
|
1507
|
+
Authorize Agent
|
|
1508
|
+
</button>
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>
|
|
1511
|
+
`;
|
|
1512
|
+
if (isModal) {
|
|
1513
|
+
return lit.html` <div class="backdrop">${content}</div> `;
|
|
1514
|
+
}
|
|
1515
|
+
return content;
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Check if all scopes for a service are granted.
|
|
1519
|
+
*/
|
|
1520
|
+
_areAllServiceScopesGranted(service, serviceScopes) {
|
|
1521
|
+
return serviceScopes.every((scope) => this._isScopeGranted(scope));
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Toggle all scopes for a service.
|
|
1525
|
+
*/
|
|
1526
|
+
_toggleAllServiceScopes(service, serviceScopes) {
|
|
1527
|
+
const allGranted = this._areAllServiceScopesGranted(service, serviceScopes);
|
|
1528
|
+
for (const scope of serviceScopes) {
|
|
1529
|
+
const key = scopeKey(scope);
|
|
1530
|
+
if (allGranted) {
|
|
1531
|
+
this._grantedScopes.delete(key);
|
|
1532
|
+
} else {
|
|
1533
|
+
this._grantedScopes.add(key);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
this._grantedScopes = new Set(this._grantedScopes);
|
|
1537
|
+
this.requestUpdate();
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
exports.MulticornConsent.styles = [consentStyles];
|
|
1541
|
+
__decorateClass([
|
|
1542
|
+
decorators_js.property({ type: String, attribute: "agent-name" })
|
|
1543
|
+
], exports.MulticornConsent.prototype, "agentName", 2);
|
|
1544
|
+
__decorateClass([
|
|
1545
|
+
decorators_js.property({ type: String, attribute: "agent-color" })
|
|
1546
|
+
], exports.MulticornConsent.prototype, "agentColor", 2);
|
|
1547
|
+
__decorateClass([
|
|
1548
|
+
decorators_js.property({ type: Array, attribute: "scopes" })
|
|
1549
|
+
], exports.MulticornConsent.prototype, "scopes", 2);
|
|
1550
|
+
__decorateClass([
|
|
1551
|
+
decorators_js.property({ type: Number, attribute: "spend-limit" })
|
|
1552
|
+
], exports.MulticornConsent.prototype, "spendLimit", 2);
|
|
1553
|
+
__decorateClass([
|
|
1554
|
+
decorators_js.property({ type: String })
|
|
1555
|
+
], exports.MulticornConsent.prototype, "mode", 2);
|
|
1556
|
+
__decorateClass([
|
|
1557
|
+
decorators_js.state()
|
|
1558
|
+
], exports.MulticornConsent.prototype, "_grantedScopes", 2);
|
|
1559
|
+
__decorateClass([
|
|
1560
|
+
decorators_js.state()
|
|
1561
|
+
], exports.MulticornConsent.prototype, "_adjustedSpendLimit", 2);
|
|
1562
|
+
__decorateClass([
|
|
1563
|
+
decorators_js.state()
|
|
1564
|
+
], exports.MulticornConsent.prototype, "_isOpen", 2);
|
|
1565
|
+
exports.MulticornConsent = __decorateClass([
|
|
1566
|
+
decorators_js.customElement(CONSENT_ELEMENT_TAG)
|
|
1567
|
+
], exports.MulticornConsent);
|
|
1568
|
+
|
|
1569
|
+
// src/logger/action-logger.ts
|
|
1570
|
+
function createActionLogger(config) {
|
|
1571
|
+
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
1572
|
+
throw new Error(
|
|
1573
|
+
"[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
|
|
1577
|
+
const timeout = config.timeout ?? 5e3;
|
|
1578
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
1579
|
+
throw new Error(
|
|
1580
|
+
`[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
const endpoint = `${baseUrl}/api/v1/actions`;
|
|
1584
|
+
const batchEnabled = config.batchMode?.enabled ?? false;
|
|
1585
|
+
const maxBatchSize = config.batchMode?.maxSize ?? 10;
|
|
1586
|
+
const flushInterval = config.batchMode?.flushIntervalMs ?? 5e3;
|
|
1587
|
+
const queue = [];
|
|
1588
|
+
let flushTimer;
|
|
1589
|
+
let isShutdown = false;
|
|
1590
|
+
async function sendActions(actions) {
|
|
1591
|
+
if (actions.length === 0) return;
|
|
1592
|
+
const convertAction = (action) => ({
|
|
1593
|
+
agent: action.agent,
|
|
1594
|
+
service: action.service,
|
|
1595
|
+
actionType: action.actionType,
|
|
1596
|
+
status: action.status,
|
|
1597
|
+
...action.cost !== void 0 ? { cost: action.cost } : {},
|
|
1598
|
+
...action.metadata !== void 0 ? { metadata: action.metadata } : {}
|
|
1599
|
+
});
|
|
1600
|
+
const convertedActions = actions.map(convertAction);
|
|
1601
|
+
const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
|
|
1602
|
+
let lastError;
|
|
1603
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1604
|
+
try {
|
|
1605
|
+
const controller = new AbortController();
|
|
1606
|
+
const timeoutId = setTimeout(() => {
|
|
1607
|
+
controller.abort();
|
|
1608
|
+
}, timeout);
|
|
1609
|
+
const response = await fetch(endpoint, {
|
|
1610
|
+
method: "POST",
|
|
1611
|
+
headers: {
|
|
1612
|
+
"Content-Type": "application/json",
|
|
1613
|
+
"X-Multicorn-Key": config.apiKey
|
|
1614
|
+
},
|
|
1615
|
+
body: JSON.stringify(payload),
|
|
1616
|
+
signal: controller.signal
|
|
1617
|
+
});
|
|
1618
|
+
clearTimeout(timeoutId);
|
|
1619
|
+
if (response.ok) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (response.status >= 400 && response.status < 500) {
|
|
1623
|
+
const body = await response.text().catch(() => "");
|
|
1624
|
+
throw new Error(
|
|
1625
|
+
`[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
if (response.status >= 500 && attempt === 0) {
|
|
1629
|
+
lastError = new Error(
|
|
1630
|
+
`[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
|
|
1631
|
+
);
|
|
1632
|
+
await sleep(100 * Math.pow(2, attempt));
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
|
|
1637
|
+
);
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
if (error instanceof Error) {
|
|
1640
|
+
if (error.name === "AbortError") {
|
|
1641
|
+
lastError = new Error(
|
|
1642
|
+
`[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
|
|
1643
|
+
);
|
|
1644
|
+
} else if (error.message.includes("Client error") || error.message.includes("Server error")) {
|
|
1645
|
+
lastError = error;
|
|
1646
|
+
} else {
|
|
1647
|
+
lastError = new Error(
|
|
1648
|
+
`[ActionLogger] Network error: ${error.message}. Check your network connection and API endpoint.`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
} else {
|
|
1652
|
+
lastError = new Error(`[ActionLogger] Unknown error: ${String(error)}`);
|
|
1653
|
+
}
|
|
1654
|
+
if (attempt === 0 && !lastError.message.includes("Client error")) {
|
|
1655
|
+
await sleep(100 * Math.pow(2, attempt));
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (lastError) {
|
|
1662
|
+
if (config.onError) {
|
|
1663
|
+
config.onError(lastError);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
async function flushQueue() {
|
|
1668
|
+
if (queue.length === 0) return;
|
|
1669
|
+
const actions = queue.map((item) => item.payload);
|
|
1670
|
+
queue.length = 0;
|
|
1671
|
+
await sendActions(actions);
|
|
1672
|
+
}
|
|
1673
|
+
function startFlushTimer() {
|
|
1674
|
+
if (flushTimer !== void 0) return;
|
|
1675
|
+
flushTimer = setInterval(() => {
|
|
1676
|
+
flushQueue().catch(() => {
|
|
1677
|
+
});
|
|
1678
|
+
}, flushInterval);
|
|
1679
|
+
const timer = flushTimer;
|
|
1680
|
+
if (typeof timer.unref === "function") {
|
|
1681
|
+
timer.unref();
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function stopFlushTimer() {
|
|
1685
|
+
if (flushTimer) {
|
|
1686
|
+
clearInterval(flushTimer);
|
|
1687
|
+
flushTimer = void 0;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
if (batchEnabled) {
|
|
1691
|
+
startFlushTimer();
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
logAction(action) {
|
|
1695
|
+
if (isShutdown) {
|
|
1696
|
+
throw new Error(
|
|
1697
|
+
"[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
if (action.agent.trim().length === 0) {
|
|
1701
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
|
|
1702
|
+
}
|
|
1703
|
+
if (action.service.trim().length === 0) {
|
|
1704
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
|
|
1705
|
+
}
|
|
1706
|
+
if (action.actionType.trim().length === 0) {
|
|
1707
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
|
|
1708
|
+
}
|
|
1709
|
+
if (action.status.trim().length === 0) {
|
|
1710
|
+
throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
|
|
1711
|
+
}
|
|
1712
|
+
if (batchEnabled) {
|
|
1713
|
+
queue.push({ payload: action, timestamp: Date.now() });
|
|
1714
|
+
if (queue.length >= maxBatchSize) {
|
|
1715
|
+
flushQueue().catch(() => {
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
} else {
|
|
1719
|
+
sendActions([action]).catch(() => {
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
return Promise.resolve();
|
|
1723
|
+
},
|
|
1724
|
+
async flush() {
|
|
1725
|
+
if (!batchEnabled) return;
|
|
1726
|
+
await flushQueue();
|
|
1727
|
+
},
|
|
1728
|
+
async shutdown() {
|
|
1729
|
+
if (isShutdown) return;
|
|
1730
|
+
isShutdown = true;
|
|
1731
|
+
stopFlushTimer();
|
|
1732
|
+
if (batchEnabled) {
|
|
1733
|
+
await flushQueue();
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function sleep(ms) {
|
|
1739
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/spending/spending-checker.ts
|
|
1743
|
+
function createSpendingChecker(config) {
|
|
1744
|
+
validateLimits(config.limits);
|
|
1745
|
+
let dailySpendCents = 0;
|
|
1746
|
+
let monthlySpendCents = 0;
|
|
1747
|
+
let lastDailyReset = /* @__PURE__ */ new Date();
|
|
1748
|
+
let lastMonthlyReset = /* @__PURE__ */ new Date();
|
|
1749
|
+
function validateAmount(amountCents, context) {
|
|
1750
|
+
if (!Number.isInteger(amountCents)) {
|
|
1751
|
+
throw new Error(
|
|
1752
|
+
`[SpendingChecker] ${context} must be an integer (cents). Received: ${String(amountCents)}. Convert dollars to cents by multiplying by 100.`
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
if (amountCents < 0) {
|
|
1756
|
+
throw new Error(
|
|
1757
|
+
`[SpendingChecker] ${context} must be non-negative. Received: ${String(amountCents)} cents.`
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
function formatCents(cents) {
|
|
1762
|
+
const dollars = cents / 100;
|
|
1763
|
+
return `$${dollars.toLocaleString("en-US", {
|
|
1764
|
+
minimumFractionDigits: 2,
|
|
1765
|
+
maximumFractionDigits: 2
|
|
1766
|
+
})}`;
|
|
1767
|
+
}
|
|
1768
|
+
function checkAndResetDaily() {
|
|
1769
|
+
const now = /* @__PURE__ */ new Date();
|
|
1770
|
+
if (shouldResetDaily(lastDailyReset, now)) {
|
|
1771
|
+
dailySpendCents = 0;
|
|
1772
|
+
lastDailyReset = now;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function checkAndResetMonthly() {
|
|
1776
|
+
const now = /* @__PURE__ */ new Date();
|
|
1777
|
+
if (shouldResetMonthly(lastMonthlyReset, now)) {
|
|
1778
|
+
monthlySpendCents = 0;
|
|
1779
|
+
lastMonthlyReset = now;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function shouldResetDaily(lastReset, now) {
|
|
1783
|
+
return lastReset.getDate() !== now.getDate() || lastReset.getMonth() !== now.getMonth() || lastReset.getFullYear() !== now.getFullYear();
|
|
1784
|
+
}
|
|
1785
|
+
function shouldResetMonthly(lastReset, now) {
|
|
1786
|
+
return lastReset.getMonth() !== now.getMonth() || lastReset.getFullYear() !== now.getFullYear();
|
|
1787
|
+
}
|
|
1788
|
+
function calculateRemainingBudget() {
|
|
1789
|
+
return {
|
|
1790
|
+
transaction: config.limits.perTransaction,
|
|
1791
|
+
daily: Math.max(0, config.limits.perDay - dailySpendCents),
|
|
1792
|
+
monthly: Math.max(0, config.limits.perMonth - monthlySpendCents)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
return {
|
|
1796
|
+
checkSpend(amountCents) {
|
|
1797
|
+
validateAmount(amountCents, "Spend amount");
|
|
1798
|
+
checkAndResetDaily();
|
|
1799
|
+
checkAndResetMonthly();
|
|
1800
|
+
if (amountCents > config.limits.perTransaction) {
|
|
1801
|
+
return {
|
|
1802
|
+
allowed: false,
|
|
1803
|
+
reason: `Action blocked: ${formatCents(amountCents)} exceeds per-transaction limit of ${formatCents(config.limits.perTransaction)}`,
|
|
1804
|
+
remainingBudget: calculateRemainingBudget()
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
const projectedDaily = dailySpendCents + amountCents;
|
|
1808
|
+
if (projectedDaily > config.limits.perDay) {
|
|
1809
|
+
return {
|
|
1810
|
+
allowed: false,
|
|
1811
|
+
reason: `Action blocked: ${formatCents(amountCents)} would exceed per-day limit. Current spend today: ${formatCents(dailySpendCents)}, limit: ${formatCents(config.limits.perDay)}`,
|
|
1812
|
+
remainingBudget: calculateRemainingBudget()
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const projectedMonthly = monthlySpendCents + amountCents;
|
|
1816
|
+
if (projectedMonthly > config.limits.perMonth) {
|
|
1817
|
+
return {
|
|
1818
|
+
allowed: false,
|
|
1819
|
+
reason: `Action blocked: ${formatCents(amountCents)} would exceed per-month limit. Current spend this month: ${formatCents(monthlySpendCents)}, limit: ${formatCents(config.limits.perMonth)}`,
|
|
1820
|
+
remainingBudget: calculateRemainingBudget()
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
return {
|
|
1824
|
+
allowed: true,
|
|
1825
|
+
remainingBudget: calculateRemainingBudget()
|
|
1826
|
+
};
|
|
1827
|
+
},
|
|
1828
|
+
recordSpend(amountCents) {
|
|
1829
|
+
validateAmount(amountCents, "Spend amount");
|
|
1830
|
+
checkAndResetDaily();
|
|
1831
|
+
checkAndResetMonthly();
|
|
1832
|
+
dailySpendCents += amountCents;
|
|
1833
|
+
monthlySpendCents += amountCents;
|
|
1834
|
+
},
|
|
1835
|
+
getCurrentSpend() {
|
|
1836
|
+
checkAndResetDaily();
|
|
1837
|
+
checkAndResetMonthly();
|
|
1838
|
+
return {
|
|
1839
|
+
daily: dailySpendCents,
|
|
1840
|
+
monthly: monthlySpendCents
|
|
1841
|
+
};
|
|
1842
|
+
},
|
|
1843
|
+
reset() {
|
|
1844
|
+
dailySpendCents = 0;
|
|
1845
|
+
monthlySpendCents = 0;
|
|
1846
|
+
lastDailyReset = /* @__PURE__ */ new Date();
|
|
1847
|
+
lastMonthlyReset = /* @__PURE__ */ new Date();
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
function validateLimits(limits) {
|
|
1852
|
+
const checks = [
|
|
1853
|
+
{ value: limits.perTransaction, name: "perTransaction" },
|
|
1854
|
+
{ value: limits.perDay, name: "perDay" },
|
|
1855
|
+
{ value: limits.perMonth, name: "perMonth" }
|
|
1856
|
+
];
|
|
1857
|
+
for (const check of checks) {
|
|
1858
|
+
if (!Number.isInteger(check.value)) {
|
|
1859
|
+
throw new Error(
|
|
1860
|
+
`[SpendingChecker] Limit "${check.name}" must be an integer (cents). Received: ${String(check.value)}. All limits must be specified in integer cents.`
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
if (check.value < 0) {
|
|
1864
|
+
throw new Error(
|
|
1865
|
+
`[SpendingChecker] Limit "${check.name}" must be non-negative. Received: ${String(check.value)} cents.`
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function dollarsToCents(dollars) {
|
|
1871
|
+
return Math.round(dollars * 100);
|
|
1872
|
+
}
|
|
1873
|
+
function centsToDollars(cents) {
|
|
1874
|
+
const dollars = cents / 100;
|
|
1875
|
+
return `$${dollars.toLocaleString("en-US", {
|
|
1876
|
+
minimumFractionDigits: 2,
|
|
1877
|
+
maximumFractionDigits: 2
|
|
1878
|
+
})}`;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// src/scopes/content-review-detector.ts
|
|
1882
|
+
function requiresContentReview(scope) {
|
|
1883
|
+
if (scope.service === "web" && scope.permissionLevel === "publish") {
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
if (scope.service === "public_content" && scope.permissionLevel === "create") {
|
|
1887
|
+
return true;
|
|
1888
|
+
}
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
function isPublicContentAction(toolName, service) {
|
|
1892
|
+
const lowerToolName = toolName.toLowerCase();
|
|
1893
|
+
const lowerService = service.toLowerCase();
|
|
1894
|
+
if (lowerService === "web" || lowerService === "public_content") {
|
|
1895
|
+
return true;
|
|
1896
|
+
}
|
|
1897
|
+
const publicContentIndicators = [
|
|
1898
|
+
"publish",
|
|
1899
|
+
"public",
|
|
1900
|
+
"web",
|
|
1901
|
+
"blog",
|
|
1902
|
+
"post",
|
|
1903
|
+
"article",
|
|
1904
|
+
"social",
|
|
1905
|
+
"twitter",
|
|
1906
|
+
"facebook",
|
|
1907
|
+
"linkedin",
|
|
1908
|
+
"github_pages",
|
|
1909
|
+
"deploy"
|
|
1910
|
+
];
|
|
1911
|
+
return publicContentIndicators.some((indicator) => lowerToolName.includes(indicator));
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/mcp/mcp-adapter.ts
|
|
1915
|
+
function isBlockedResult(result) {
|
|
1916
|
+
return "blocked" in result;
|
|
1917
|
+
}
|
|
1918
|
+
function createMcpAdapter(config) {
|
|
1919
|
+
const permissionLevel = config.requiredPermissionLevel ?? PERMISSION_LEVELS.Execute;
|
|
1920
|
+
function mapPublishingService(service, toolName) {
|
|
1921
|
+
const lowerService = service.toLowerCase();
|
|
1922
|
+
const lowerToolName = toolName.toLowerCase();
|
|
1923
|
+
const publishActions = [
|
|
1924
|
+
"deploy",
|
|
1925
|
+
"publish",
|
|
1926
|
+
"release",
|
|
1927
|
+
"go_live",
|
|
1928
|
+
"make_public",
|
|
1929
|
+
"pages_deploy",
|
|
1930
|
+
"github_pages"
|
|
1931
|
+
];
|
|
1932
|
+
if (publishActions.some((action) => lowerToolName.includes(action))) {
|
|
1933
|
+
return { mappedService: "web", mappedPermissionLevel: PERMISSION_LEVELS.Publish };
|
|
1934
|
+
}
|
|
1935
|
+
const createActions = [
|
|
1936
|
+
"post",
|
|
1937
|
+
"tweet",
|
|
1938
|
+
"status",
|
|
1939
|
+
"update",
|
|
1940
|
+
"share",
|
|
1941
|
+
"comment",
|
|
1942
|
+
"reply",
|
|
1943
|
+
"commit"
|
|
1944
|
+
// Public commits are immediately visible
|
|
1945
|
+
];
|
|
1946
|
+
if (createActions.some((action) => lowerToolName.includes(action))) {
|
|
1947
|
+
return { mappedService: "public_content", mappedPermissionLevel: PERMISSION_LEVELS.Create };
|
|
1948
|
+
}
|
|
1949
|
+
if (lowerService === "github" && lowerToolName.includes("pages")) {
|
|
1950
|
+
return { mappedService: "web", mappedPermissionLevel: PERMISSION_LEVELS.Publish };
|
|
1951
|
+
}
|
|
1952
|
+
const socialMediaServices = ["twitter", "x", "facebook", "linkedin", "instagram"];
|
|
1953
|
+
if (socialMediaServices.includes(lowerService)) {
|
|
1954
|
+
return { mappedService: "public_content", mappedPermissionLevel: PERMISSION_LEVELS.Create };
|
|
1955
|
+
}
|
|
1956
|
+
const blogServices = ["wordpress", "medium", "blogger", "ghost"];
|
|
1957
|
+
if (blogServices.includes(lowerService)) {
|
|
1958
|
+
if (publishActions.some((action) => lowerToolName.includes(action))) {
|
|
1959
|
+
return { mappedService: "web", mappedPermissionLevel: PERMISSION_LEVELS.Publish };
|
|
1960
|
+
}
|
|
1961
|
+
return { mappedService: "public_content", mappedPermissionLevel: PERMISSION_LEVELS.Create };
|
|
1962
|
+
}
|
|
1963
|
+
return { mappedService: service, mappedPermissionLevel: permissionLevel };
|
|
1964
|
+
}
|
|
1965
|
+
function deriveService(toolName) {
|
|
1966
|
+
if (config.extractService !== void 0) {
|
|
1967
|
+
return config.extractService(toolName);
|
|
1968
|
+
}
|
|
1969
|
+
const underscoreIndex = toolName.indexOf("_");
|
|
1970
|
+
return underscoreIndex === -1 ? toolName : toolName.slice(0, underscoreIndex);
|
|
1971
|
+
}
|
|
1972
|
+
function deriveAction(toolName) {
|
|
1973
|
+
if (config.extractAction !== void 0) {
|
|
1974
|
+
return config.extractAction(toolName);
|
|
1975
|
+
}
|
|
1976
|
+
const underscoreIndex = toolName.indexOf("_");
|
|
1977
|
+
return underscoreIndex === -1 ? "call" : toolName.slice(underscoreIndex + 1);
|
|
1978
|
+
}
|
|
1979
|
+
async function recordAction(service, action, status) {
|
|
1980
|
+
if (config.logger === void 0) return;
|
|
1981
|
+
await config.logger.logAction({
|
|
1982
|
+
agent: config.agentId,
|
|
1983
|
+
service,
|
|
1984
|
+
actionType: action,
|
|
1985
|
+
status
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
async function checkAutoApproveStatus() {
|
|
1989
|
+
if (config.checkAutoApprove !== void 0) {
|
|
1990
|
+
const result = config.checkAutoApprove(config.agentId);
|
|
1991
|
+
return result instanceof Promise ? await result : result;
|
|
1992
|
+
}
|
|
1993
|
+
if (config.baseUrl && config.apiKey) {
|
|
1994
|
+
try {
|
|
1995
|
+
const agentId = config.agentId;
|
|
1996
|
+
const endpoint = `${config.baseUrl}/api/v1/agents/${encodeURIComponent(agentId)}`;
|
|
1997
|
+
const controller = new AbortController();
|
|
1998
|
+
const timeoutId = setTimeout(() => {
|
|
1999
|
+
controller.abort();
|
|
2000
|
+
}, 5e3);
|
|
2001
|
+
try {
|
|
2002
|
+
const response = await fetch(endpoint, {
|
|
2003
|
+
method: "GET",
|
|
2004
|
+
headers: {
|
|
2005
|
+
"Content-Type": "application/json",
|
|
2006
|
+
"X-Multicorn-Key": config.apiKey
|
|
2007
|
+
},
|
|
2008
|
+
signal: controller.signal
|
|
2009
|
+
});
|
|
2010
|
+
clearTimeout(timeoutId);
|
|
2011
|
+
if (response.ok) {
|
|
2012
|
+
const data = await response.json();
|
|
2013
|
+
return data.data?.public_content_auto_approve ?? false;
|
|
2014
|
+
}
|
|
2015
|
+
} catch {
|
|
2016
|
+
clearTimeout(timeoutId);
|
|
2017
|
+
return false;
|
|
2018
|
+
}
|
|
2019
|
+
} catch {
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return false;
|
|
2024
|
+
}
|
|
2025
|
+
return {
|
|
2026
|
+
async intercept(toolCall, handler) {
|
|
2027
|
+
const derivedService = deriveService(toolCall.toolName);
|
|
2028
|
+
const action = deriveAction(toolCall.toolName);
|
|
2029
|
+
const { mappedService, mappedPermissionLevel } = mapPublishingService(
|
|
2030
|
+
derivedService,
|
|
2031
|
+
toolCall.toolName
|
|
2032
|
+
);
|
|
2033
|
+
const requestedScope = {
|
|
2034
|
+
service: mappedService,
|
|
2035
|
+
permissionLevel: mappedPermissionLevel
|
|
2036
|
+
};
|
|
2037
|
+
const validation = validateScopeAccess(config.grantedScopes, requestedScope);
|
|
2038
|
+
if (!validation.allowed) {
|
|
2039
|
+
await recordAction(mappedService, action, ACTION_STATUSES.Blocked);
|
|
2040
|
+
return {
|
|
2041
|
+
blocked: true,
|
|
2042
|
+
reason: validation.reason ?? `Action blocked: "${config.agentId}" does not have "${mappedPermissionLevel}" permission for "${mappedService}".`,
|
|
2043
|
+
toolName: toolCall.toolName,
|
|
2044
|
+
service: mappedService,
|
|
2045
|
+
action
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
const needsReview = requiresContentReview(requestedScope) || isPublicContentAction(toolCall.toolName, mappedService);
|
|
2049
|
+
if (needsReview) {
|
|
2050
|
+
const autoApprove = await checkAutoApproveStatus();
|
|
2051
|
+
if (!autoApprove) {
|
|
2052
|
+
const metadata = {
|
|
2053
|
+
toolName: toolCall.toolName,
|
|
2054
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
2055
|
+
requiresReview: true
|
|
2056
|
+
};
|
|
2057
|
+
if (config.logger) {
|
|
2058
|
+
await config.logger.logAction({
|
|
2059
|
+
agent: config.agentId,
|
|
2060
|
+
service: mappedService,
|
|
2061
|
+
actionType: action,
|
|
2062
|
+
status: ACTION_STATUSES.RequiresApproval,
|
|
2063
|
+
metadata
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
return {
|
|
2067
|
+
blocked: true,
|
|
2068
|
+
reason: `Action requires content review before execution. The action has been queued for review. Check your dashboard to approve or block it.`,
|
|
2069
|
+
toolName: toolCall.toolName,
|
|
2070
|
+
service: mappedService,
|
|
2071
|
+
action
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
await recordAction(mappedService, action, ACTION_STATUSES.Approved);
|
|
2076
|
+
return handler(toolCall);
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/multicorn-shield.ts
|
|
2082
|
+
var API_KEY_PREFIX = "mcs_";
|
|
2083
|
+
function formatScope2(scope) {
|
|
2084
|
+
return `${scope.service}:${scope.permissionLevel}`;
|
|
2085
|
+
}
|
|
2086
|
+
function formatScopes(scopes) {
|
|
2087
|
+
return scopes.map(formatScope2);
|
|
2088
|
+
}
|
|
2089
|
+
var MIN_API_KEY_LENGTH = 16;
|
|
2090
|
+
var MulticornShield = class {
|
|
2091
|
+
// Private class fields for true runtime privacy.
|
|
2092
|
+
// #apiKey is unreachable outside this class at the JS level, not just compile time.
|
|
2093
|
+
#apiKey;
|
|
2094
|
+
#baseUrl;
|
|
2095
|
+
#logger;
|
|
2096
|
+
#onError;
|
|
2097
|
+
#grantedScopes = /* @__PURE__ */ new Map();
|
|
2098
|
+
#spendingCheckers = /* @__PURE__ */ new Map();
|
|
2099
|
+
#consentContainer = null;
|
|
2100
|
+
#isDestroyed = false;
|
|
2101
|
+
/**
|
|
2102
|
+
* Create a new MulticornShield instance.
|
|
2103
|
+
*
|
|
2104
|
+
* @param config - SDK configuration options.
|
|
2105
|
+
* @throws {Error} If the API key is missing, incorrectly formatted, or too short.
|
|
2106
|
+
*
|
|
2107
|
+
* @example
|
|
2108
|
+
* ```ts
|
|
2109
|
+
* const shield = new MulticornShield({ apiKey: 'mcs_your_key_here' });
|
|
2110
|
+
* ```
|
|
2111
|
+
*/
|
|
2112
|
+
constructor(config) {
|
|
2113
|
+
validateApiKey(config.apiKey);
|
|
2114
|
+
const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
|
|
2115
|
+
validateBaseUrl(baseUrl);
|
|
2116
|
+
this.#apiKey = config.apiKey;
|
|
2117
|
+
this.#baseUrl = baseUrl;
|
|
2118
|
+
this.#onError = config.onError;
|
|
2119
|
+
this.#logger = createActionLogger({
|
|
2120
|
+
apiKey: this.#apiKey,
|
|
2121
|
+
...config.baseUrl !== void 0 ? { baseUrl: config.baseUrl } : {},
|
|
2122
|
+
...config.timeout !== void 0 ? { timeout: config.timeout } : {},
|
|
2123
|
+
...config.batchMode !== void 0 ? { batchMode: config.batchMode } : {}
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Show the consent screen and wait for the user's decision.
|
|
2128
|
+
*
|
|
2129
|
+
* Mounts the `<multicorn-consent>` web component to the document body,
|
|
2130
|
+
* resolves with the user's decision (granted scopes, approved spend limit,
|
|
2131
|
+
* and a timestamp), then removes the element from the DOM.
|
|
2132
|
+
*
|
|
2133
|
+
* Granted scopes are stored internally and enforced on every subsequent
|
|
2134
|
+
* {@link logAction} call.
|
|
2135
|
+
*
|
|
2136
|
+
* @param options - What to request consent for.
|
|
2137
|
+
* @returns The user's consent decision including which scopes were granted.
|
|
2138
|
+
* @throws {ScopeParseError} If any scope string is malformed.
|
|
2139
|
+
* @throws {Error} If the instance has been destroyed.
|
|
2140
|
+
*
|
|
2141
|
+
* @example
|
|
2142
|
+
* ```ts
|
|
2143
|
+
* const decision = await shield.requestConsent({
|
|
2144
|
+
* agent: 'OpenClaw',
|
|
2145
|
+
* scopes: ['read:gmail', 'write:calendar'],
|
|
2146
|
+
* spendLimit: 200,
|
|
2147
|
+
* });
|
|
2148
|
+
*
|
|
2149
|
+
* console.log(decision.grantedScopes.map(s => `${s.permissionLevel}:${s.service}`));
|
|
2150
|
+
* ```
|
|
2151
|
+
*/
|
|
2152
|
+
async requestConsent(options) {
|
|
2153
|
+
this.#assertNotDestroyed();
|
|
2154
|
+
const parsedScopes = parseScopes(options.scopes);
|
|
2155
|
+
const scopeRequest = {
|
|
2156
|
+
agentName: options.agent,
|
|
2157
|
+
scopes: parsedScopes,
|
|
2158
|
+
spendLimit: options.spendLimit ?? 0
|
|
2159
|
+
};
|
|
2160
|
+
return new Promise((resolve) => {
|
|
2161
|
+
const container = document.createElement("div");
|
|
2162
|
+
this.#consentContainer = container;
|
|
2163
|
+
document.body.appendChild(container);
|
|
2164
|
+
const element = document.createElement(CONSENT_ELEMENT_TAG);
|
|
2165
|
+
element.agentName = options.agent;
|
|
2166
|
+
element.scopes = [...parsedScopes];
|
|
2167
|
+
element.spendLimit = options.spendLimit ?? 0;
|
|
2168
|
+
if (options.agentColor !== void 0) {
|
|
2169
|
+
element.agentColor = options.agentColor;
|
|
2170
|
+
}
|
|
2171
|
+
const cleanup = () => {
|
|
2172
|
+
element.remove();
|
|
2173
|
+
container.remove();
|
|
2174
|
+
if (this.#consentContainer === container) {
|
|
2175
|
+
this.#consentContainer = null;
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
element.addEventListener("consent-granted", (event) => {
|
|
2179
|
+
const detail = event.detail;
|
|
2180
|
+
this.#grantedScopes.set(options.agent, [...detail.grantedScopes]);
|
|
2181
|
+
if (detail.spendLimit > 0) {
|
|
2182
|
+
this.#setupSpendingChecker(options.agent, detail.spendLimit);
|
|
2183
|
+
}
|
|
2184
|
+
void this.#postConsentToBackend(
|
|
2185
|
+
options.agent,
|
|
2186
|
+
detail.grantedScopes,
|
|
2187
|
+
[],
|
|
2188
|
+
detail.spendLimit,
|
|
2189
|
+
detail.timestamp
|
|
2190
|
+
).catch((error) => {
|
|
2191
|
+
this.#onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
2192
|
+
});
|
|
2193
|
+
cleanup();
|
|
2194
|
+
resolve({
|
|
2195
|
+
scopeRequest,
|
|
2196
|
+
grantedScopes: detail.grantedScopes,
|
|
2197
|
+
timestamp: detail.timestamp
|
|
2198
|
+
});
|
|
2199
|
+
});
|
|
2200
|
+
element.addEventListener("consent-partial", (event) => {
|
|
2201
|
+
const detail = event.detail;
|
|
2202
|
+
this.#grantedScopes.set(options.agent, [...detail.grantedScopes]);
|
|
2203
|
+
if (detail.spendLimit > 0) {
|
|
2204
|
+
this.#setupSpendingChecker(options.agent, detail.spendLimit);
|
|
2205
|
+
}
|
|
2206
|
+
void this.#postConsentToBackend(
|
|
2207
|
+
options.agent,
|
|
2208
|
+
detail.grantedScopes,
|
|
2209
|
+
detail.deniedScopes,
|
|
2210
|
+
detail.spendLimit,
|
|
2211
|
+
detail.timestamp
|
|
2212
|
+
).catch((error) => {
|
|
2213
|
+
this.#onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
2214
|
+
});
|
|
2215
|
+
cleanup();
|
|
2216
|
+
resolve({
|
|
2217
|
+
scopeRequest,
|
|
2218
|
+
grantedScopes: detail.grantedScopes,
|
|
2219
|
+
timestamp: detail.timestamp
|
|
2220
|
+
});
|
|
2221
|
+
});
|
|
2222
|
+
element.addEventListener("consent-denied", (event) => {
|
|
2223
|
+
const detail = event.detail;
|
|
2224
|
+
this.#grantedScopes.set(options.agent, []);
|
|
2225
|
+
cleanup();
|
|
2226
|
+
resolve({
|
|
2227
|
+
scopeRequest,
|
|
2228
|
+
grantedScopes: [],
|
|
2229
|
+
timestamp: detail.timestamp
|
|
2230
|
+
});
|
|
2231
|
+
});
|
|
2232
|
+
container.appendChild(element);
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Log an action taken by an agent.
|
|
2237
|
+
*
|
|
2238
|
+
* Verifies that the agent has a granted permission for the target service
|
|
2239
|
+
* before submitting the log entry. Throws with a descriptive error if
|
|
2240
|
+
* access was never granted or has been revoked. Actions are never silently
|
|
2241
|
+
* discarded.
|
|
2242
|
+
*
|
|
2243
|
+
* @param action - The action to log.
|
|
2244
|
+
* @returns Resolves when the log entry has been submitted (or queued in batch mode).
|
|
2245
|
+
* @throws {Error} If the agent does not have a granted scope for the service.
|
|
2246
|
+
* @throws {Error} If the instance has been destroyed.
|
|
2247
|
+
*
|
|
2248
|
+
* @example
|
|
2249
|
+
* ```ts
|
|
2250
|
+
* await shield.logAction({
|
|
2251
|
+
* agent: 'OpenClaw',
|
|
2252
|
+
* service: 'gmail',
|
|
2253
|
+
* action: 'send_email',
|
|
2254
|
+
* status: 'approved',
|
|
2255
|
+
* });
|
|
2256
|
+
* ```
|
|
2257
|
+
*/
|
|
2258
|
+
async logAction(action) {
|
|
2259
|
+
this.#assertNotDestroyed();
|
|
2260
|
+
const granted = this.#grantedScopes.get(action.agent) ?? [];
|
|
2261
|
+
const hasPermissionForService = granted.some((s) => s.service === action.service);
|
|
2262
|
+
const payload = {
|
|
2263
|
+
agent: action.agent,
|
|
2264
|
+
service: action.service,
|
|
2265
|
+
actionType: action.action,
|
|
2266
|
+
status: hasPermissionForService ? action.status : "blocked",
|
|
2267
|
+
...action.cost !== void 0 ? { cost: action.cost } : {},
|
|
2268
|
+
...action.metadata !== void 0 ? { metadata: action.metadata } : {}
|
|
2269
|
+
};
|
|
2270
|
+
await this.#logger.logAction(payload);
|
|
2271
|
+
if (!hasPermissionForService) {
|
|
2272
|
+
const grantedServiceList = [...new Set(granted.map((s) => s.service))].join(", ") || "none";
|
|
2273
|
+
throw new Error(
|
|
2274
|
+
`[MulticornShield] Agent "${action.agent}" does not have permission to access "${action.service}". Services with granted access: ${grantedServiceList}. Call requestConsent() to grant access.`
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* POST granted scopes to the backend consent endpoint.
|
|
2280
|
+
* @private
|
|
2281
|
+
*/
|
|
2282
|
+
async #postConsentToBackend(agentName, grantedScopes, deniedScopes, spendLimit, timestamp) {
|
|
2283
|
+
this.#assertNotDestroyed();
|
|
2284
|
+
if (grantedScopes.length === 0) {
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const endpoint = `${this.#baseUrl}/api/v1/consent`;
|
|
2288
|
+
const grantedScopeStrings = formatScopes(grantedScopes);
|
|
2289
|
+
const deniedScopeStrings = formatScopes(deniedScopes);
|
|
2290
|
+
const spendLimitCents = spendLimit > 0 ? Math.round(spendLimit * 100) : null;
|
|
2291
|
+
const payload = {
|
|
2292
|
+
agent: agentName,
|
|
2293
|
+
granted_scopes: grantedScopeStrings,
|
|
2294
|
+
denied_scopes: deniedScopeStrings,
|
|
2295
|
+
spend_limit: spendLimitCents,
|
|
2296
|
+
timestamp
|
|
2297
|
+
};
|
|
2298
|
+
const controller = new AbortController();
|
|
2299
|
+
const timeoutId = setTimeout(() => {
|
|
2300
|
+
controller.abort();
|
|
2301
|
+
}, 1e4);
|
|
2302
|
+
try {
|
|
2303
|
+
const response = await fetch(endpoint, {
|
|
2304
|
+
method: "POST",
|
|
2305
|
+
headers: {
|
|
2306
|
+
"Content-Type": "application/json",
|
|
2307
|
+
"X-Multicorn-Key": this.#apiKey
|
|
2308
|
+
},
|
|
2309
|
+
body: JSON.stringify(payload),
|
|
2310
|
+
signal: controller.signal
|
|
2311
|
+
});
|
|
2312
|
+
clearTimeout(timeoutId);
|
|
2313
|
+
if (!response.ok) {
|
|
2314
|
+
const rawBody = await response.text().catch(() => "");
|
|
2315
|
+
const safeBody = rawBody.length > 200 ? rawBody.slice(0, 200) + "\u2026" : rawBody;
|
|
2316
|
+
throw new Error(
|
|
2317
|
+
`Failed to store consent: HTTP ${String(response.status)}.` + (safeBody.length > 0 ? ` Server response: ${safeBody}` : "")
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
clearTimeout(timeoutId);
|
|
2322
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2323
|
+
throw new Error("Consent POST request timed out");
|
|
2324
|
+
}
|
|
2325
|
+
throw error;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Immediately revoke a specific scope for an agent.
|
|
2330
|
+
*
|
|
2331
|
+
* Any subsequent {@link logAction} calls that require access to that service
|
|
2332
|
+
* will be rejected. The revocation takes effect synchronously.
|
|
2333
|
+
*
|
|
2334
|
+
* @param agentId - The agent whose scope should be revoked.
|
|
2335
|
+
* @param scope - The scope string to revoke (e.g. `"write:calendar"`).
|
|
2336
|
+
* @throws {ScopeParseError} If the scope string is malformed.
|
|
2337
|
+
* @throws {Error} If the instance has been destroyed.
|
|
2338
|
+
*
|
|
2339
|
+
* @example
|
|
2340
|
+
* ```ts
|
|
2341
|
+
* // OpenClaw can no longer write to calendar
|
|
2342
|
+
* shield.revokeScope('OpenClaw', 'write:calendar');
|
|
2343
|
+
* ```
|
|
2344
|
+
*/
|
|
2345
|
+
revokeScope(agentId, scope) {
|
|
2346
|
+
this.#assertNotDestroyed();
|
|
2347
|
+
const parsed = parseScope(scope);
|
|
2348
|
+
const current = this.#grantedScopes.get(agentId) ?? [];
|
|
2349
|
+
this.#grantedScopes.set(
|
|
2350
|
+
agentId,
|
|
2351
|
+
current.filter(
|
|
2352
|
+
(s) => !(s.service === parsed.service && s.permissionLevel === parsed.permissionLevel)
|
|
2353
|
+
)
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Return the current granted scopes for an agent.
|
|
2358
|
+
*
|
|
2359
|
+
* @param agentId - The agent to query.
|
|
2360
|
+
* @returns The scopes granted to this agent. Empty array if none have been granted.
|
|
2361
|
+
* @throws {Error} If the instance has been destroyed.
|
|
2362
|
+
*
|
|
2363
|
+
* @example
|
|
2364
|
+
* ```ts
|
|
2365
|
+
* const scopes = shield.getGrantedScopes('OpenClaw');
|
|
2366
|
+
* // [{ service: 'gmail', permissionLevel: 'read' }, ...]
|
|
2367
|
+
* ```
|
|
2368
|
+
*/
|
|
2369
|
+
getGrantedScopes(agentId) {
|
|
2370
|
+
this.#assertNotDestroyed();
|
|
2371
|
+
return this.#grantedScopes.get(agentId) ?? [];
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Pre-check whether a proposed spend would be allowed for an agent.
|
|
2375
|
+
*
|
|
2376
|
+
* This is a read-only check. It does **not** record the spend.
|
|
2377
|
+
* Call this before executing a transaction to surface limit violations
|
|
2378
|
+
* early. If no spending limit was configured via {@link requestConsent},
|
|
2379
|
+
* all amounts are allowed.
|
|
2380
|
+
*
|
|
2381
|
+
* @param agentId - The agent proposing the spend.
|
|
2382
|
+
* @param amount - The proposed spend amount in dollars (e.g. `49.99`).
|
|
2383
|
+
* @returns Whether the spend is allowed and what budget remains.
|
|
2384
|
+
* @throws {Error} If the instance has been destroyed.
|
|
2385
|
+
*
|
|
2386
|
+
* @example
|
|
2387
|
+
* ```ts
|
|
2388
|
+
* const result = shield.checkSpending('OpenClaw', 50);
|
|
2389
|
+
* if (!result.allowed) {
|
|
2390
|
+
* console.error(result.reason);
|
|
2391
|
+
* }
|
|
2392
|
+
* ```
|
|
2393
|
+
*/
|
|
2394
|
+
checkSpending(agentId, amount) {
|
|
2395
|
+
this.#assertNotDestroyed();
|
|
2396
|
+
const checker = this.#spendingCheckers.get(agentId);
|
|
2397
|
+
if (checker === void 0) {
|
|
2398
|
+
return {
|
|
2399
|
+
allowed: true,
|
|
2400
|
+
remainingBudget: {
|
|
2401
|
+
transaction: Number.MAX_SAFE_INTEGER,
|
|
2402
|
+
daily: Number.MAX_SAFE_INTEGER,
|
|
2403
|
+
monthly: Number.MAX_SAFE_INTEGER
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
return checker.checkSpend(dollarsToCents(amount));
|
|
2408
|
+
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Clean up the SDK instance.
|
|
2411
|
+
*
|
|
2412
|
+
* Flushes any pending log entries, removes the consent screen from the
|
|
2413
|
+
* DOM if it is still open, and marks the instance as destroyed. All
|
|
2414
|
+
* subsequent method calls will throw after `destroy()` is called.
|
|
2415
|
+
*
|
|
2416
|
+
* Safe to call multiple times. Subsequent calls are no-ops.
|
|
2417
|
+
*
|
|
2418
|
+
* @example
|
|
2419
|
+
* ```ts
|
|
2420
|
+
* // In a SPA teardown hook:
|
|
2421
|
+
* shield.destroy();
|
|
2422
|
+
* ```
|
|
2423
|
+
*/
|
|
2424
|
+
destroy() {
|
|
2425
|
+
if (this.#isDestroyed) return;
|
|
2426
|
+
this.#isDestroyed = true;
|
|
2427
|
+
this.#logger.shutdown().catch(() => {
|
|
2428
|
+
});
|
|
2429
|
+
if (this.#consentContainer !== null) {
|
|
2430
|
+
this.#consentContainer.remove();
|
|
2431
|
+
this.#consentContainer = null;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
// Private helpers
|
|
2435
|
+
#assertNotDestroyed() {
|
|
2436
|
+
if (this.#isDestroyed) {
|
|
2437
|
+
throw new Error(
|
|
2438
|
+
"[MulticornShield] This instance has been destroyed. Create a new MulticornShield instance."
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
#setupSpendingChecker(agentId, spendLimitDollars) {
|
|
2443
|
+
const limitCents = dollarsToCents(spendLimitDollars);
|
|
2444
|
+
this.#spendingCheckers.set(
|
|
2445
|
+
agentId,
|
|
2446
|
+
createSpendingChecker({
|
|
2447
|
+
limits: {
|
|
2448
|
+
perTransaction: limitCents,
|
|
2449
|
+
// Daily and monthly limits are derived from the per-transaction limit
|
|
2450
|
+
// as sensible defaults. Developers needing tighter controls can use
|
|
2451
|
+
// the spending module directly.
|
|
2452
|
+
perDay: limitCents * 10,
|
|
2453
|
+
perMonth: limitCents * 100
|
|
2454
|
+
}
|
|
2455
|
+
})
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
function validateBaseUrl(baseUrl) {
|
|
2460
|
+
if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
|
|
2461
|
+
throw new Error(
|
|
2462
|
+
`[MulticornShield] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
function validateApiKey(apiKey) {
|
|
2467
|
+
if (!apiKey.startsWith(API_KEY_PREFIX)) {
|
|
2468
|
+
throw new Error(
|
|
2469
|
+
`[MulticornShield] Invalid API key format. Keys must start with "${API_KEY_PREFIX}". Find your API key in the Multicorn dashboard under Settings \u2192 API Keys.`
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
if (apiKey.length < MIN_API_KEY_LENGTH) {
|
|
2473
|
+
throw new Error(
|
|
2474
|
+
`[MulticornShield] API key is too short (${String(apiKey.length)} characters). Minimum length is ${String(MIN_API_KEY_LENGTH)} characters. Find your API key in the Multicorn dashboard under Settings \u2192 API Keys.`
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
exports.ACTION_STATUSES = ACTION_STATUSES;
|
|
2480
|
+
exports.AGENT_STATUSES = AGENT_STATUSES;
|
|
2481
|
+
exports.BUILT_IN_SERVICES = BUILT_IN_SERVICES;
|
|
2482
|
+
exports.CONSENT_ELEMENT_TAG = CONSENT_ELEMENT_TAG;
|
|
2483
|
+
exports.MulticornShield = MulticornShield;
|
|
2484
|
+
exports.PERMISSION_LEVELS = PERMISSION_LEVELS;
|
|
2485
|
+
exports.SERVICE_NAME_PATTERN = SERVICE_NAME_PATTERN;
|
|
2486
|
+
exports.ScopeParseError = ScopeParseError;
|
|
2487
|
+
exports.centsToDollars = centsToDollars;
|
|
2488
|
+
exports.createActionLogger = createActionLogger;
|
|
2489
|
+
exports.createFocusTrap = createFocusTrap;
|
|
2490
|
+
exports.createMcpAdapter = createMcpAdapter;
|
|
2491
|
+
exports.createScopeRegistry = createScopeRegistry;
|
|
2492
|
+
exports.createSpendingChecker = createSpendingChecker;
|
|
2493
|
+
exports.dollarsToCents = dollarsToCents;
|
|
2494
|
+
exports.formatScope = formatScope;
|
|
2495
|
+
exports.getPermissionLabel = getPermissionLabel;
|
|
2496
|
+
exports.getScopeLabel = getScopeLabel;
|
|
2497
|
+
exports.getScopeShortLabel = getScopeShortLabel;
|
|
2498
|
+
exports.getServiceDisplayName = getServiceDisplayName;
|
|
2499
|
+
exports.getServiceIcon = getServiceIcon;
|
|
2500
|
+
exports.hasScope = hasScope;
|
|
2501
|
+
exports.isBlockedResult = isBlockedResult;
|
|
2502
|
+
exports.isValidScopeString = isValidScopeString;
|
|
2503
|
+
exports.parseScope = parseScope;
|
|
2504
|
+
exports.parseScopes = parseScopes;
|
|
2505
|
+
exports.tryParseScope = tryParseScope;
|
|
2506
|
+
exports.validateAllScopesAccess = validateAllScopesAccess;
|
|
2507
|
+
exports.validateScopeAccess = validateScopeAccess;
|