@carto-knowledge/runner 0.2.3 → 0.2.5
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/dist/index.cjs +342 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +151 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +305 -0
- package/dist/index.js.map +1 -0
- package/package.json +22 -14
- package/.turbo/turbo-typecheck.log +0 -6
- package/bun.lock +0 -38
- package/src/allowlist.test.ts +0 -126
- package/src/allowlist.ts +0 -105
- package/src/envelope.ts +0 -62
- package/src/index.ts +0 -12
- package/src/runner.test.ts +0 -95
- package/src/runner.ts +0 -169
- package/src/runtime.ts +0 -90
- package/src/tokenizer.test.ts +0 -83
- package/src/tokenizer.ts +0 -75
- package/tsconfig.json +0 -15
package/dist/index.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/runner.ts
|
|
2
|
+
import { Cli } from "clipanion";
|
|
3
|
+
|
|
4
|
+
// src/tokenizer.ts
|
|
5
|
+
function tokenize(commandLine) {
|
|
6
|
+
const tokens = [];
|
|
7
|
+
let current = "";
|
|
8
|
+
let inQuote = null;
|
|
9
|
+
let escaped = false;
|
|
10
|
+
for (let i = 0; i < commandLine.length; i++) {
|
|
11
|
+
const char = commandLine[i];
|
|
12
|
+
if (escaped) {
|
|
13
|
+
current += char;
|
|
14
|
+
escaped = false;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (char === "\\") {
|
|
18
|
+
escaped = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (inQuote) {
|
|
22
|
+
if (char === inQuote) {
|
|
23
|
+
tokens.push(current);
|
|
24
|
+
current = "";
|
|
25
|
+
inQuote = null;
|
|
26
|
+
} else {
|
|
27
|
+
current += char;
|
|
28
|
+
}
|
|
29
|
+
} else if (char === '"' || char === "'") {
|
|
30
|
+
inQuote = char;
|
|
31
|
+
} else if (char === " " || char === " ") {
|
|
32
|
+
if (current) {
|
|
33
|
+
tokens.push(current);
|
|
34
|
+
current = "";
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
current += char;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (current) {
|
|
41
|
+
tokens.push(current);
|
|
42
|
+
}
|
|
43
|
+
return tokens;
|
|
44
|
+
}
|
|
45
|
+
function extractCommandPath(tokens) {
|
|
46
|
+
const path = [];
|
|
47
|
+
for (const token of tokens) {
|
|
48
|
+
if (token.startsWith("-")) break;
|
|
49
|
+
path.push(token);
|
|
50
|
+
}
|
|
51
|
+
return path.join(" ");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/allowlist.ts
|
|
55
|
+
function checkAllowlist(commandPath, config) {
|
|
56
|
+
const normalizedPath = commandPath.toLowerCase().trim();
|
|
57
|
+
if (config.commands.some((cmd) => cmd.toLowerCase() === normalizedPath)) {
|
|
58
|
+
return { allowed: true };
|
|
59
|
+
}
|
|
60
|
+
if (config.commands.some((cmd) => normalizedPath.startsWith(cmd.toLowerCase() + " "))) {
|
|
61
|
+
return { allowed: true };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
allowed: false,
|
|
65
|
+
error: {
|
|
66
|
+
code: "COMMAND_NOT_ALLOWED",
|
|
67
|
+
message: `Command '${commandPath}' is not in the allowlist. Allowed commands: ${config.commands.join(", ")}`
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
var DEFAULT_ALLOWLIST = {
|
|
72
|
+
commands: [
|
|
73
|
+
// Folder reads
|
|
74
|
+
"folder tree",
|
|
75
|
+
"folder list",
|
|
76
|
+
"folder get",
|
|
77
|
+
// Item reads
|
|
78
|
+
"item search",
|
|
79
|
+
"item get",
|
|
80
|
+
"item list",
|
|
81
|
+
"item get-by-uri",
|
|
82
|
+
"item segments",
|
|
83
|
+
// Edge reads
|
|
84
|
+
"edge list",
|
|
85
|
+
"edge get",
|
|
86
|
+
// Share link reads
|
|
87
|
+
"share-link list",
|
|
88
|
+
"share-link analytics",
|
|
89
|
+
// Librarian
|
|
90
|
+
"librarian chat"
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
var WRITE_ALLOWLIST = {
|
|
94
|
+
commands: [
|
|
95
|
+
...DEFAULT_ALLOWLIST.commands,
|
|
96
|
+
// Folder writes
|
|
97
|
+
"folder create",
|
|
98
|
+
"folder update",
|
|
99
|
+
"folder delete",
|
|
100
|
+
"folder move",
|
|
101
|
+
"folder restructure",
|
|
102
|
+
"folder create-tree",
|
|
103
|
+
// Item writes
|
|
104
|
+
"item create",
|
|
105
|
+
"item update",
|
|
106
|
+
"item delete",
|
|
107
|
+
"item update content",
|
|
108
|
+
"item upload",
|
|
109
|
+
"item assign",
|
|
110
|
+
"item move",
|
|
111
|
+
// Edge writes
|
|
112
|
+
"edge create",
|
|
113
|
+
"edge update",
|
|
114
|
+
"edge delete",
|
|
115
|
+
// Share link writes
|
|
116
|
+
"share-link create",
|
|
117
|
+
"share-link delete"
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/envelope.ts
|
|
122
|
+
function successEnvelope(command, result, requestId) {
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
command,
|
|
126
|
+
result,
|
|
127
|
+
requestId,
|
|
128
|
+
schemaVersion: 1
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function errorEnvelope(command, error, requestId) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
command,
|
|
135
|
+
error,
|
|
136
|
+
requestId,
|
|
137
|
+
schemaVersion: 1
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
var ErrorCodes = {
|
|
141
|
+
COMMAND_NOT_ALLOWED: "COMMAND_NOT_ALLOWED",
|
|
142
|
+
COMMAND_NOT_FOUND: "COMMAND_NOT_FOUND",
|
|
143
|
+
INVALID_ARGUMENTS: "INVALID_ARGUMENTS",
|
|
144
|
+
AUTH_ERROR: "AUTH_ERROR",
|
|
145
|
+
EXECUTION_ERROR: "EXECUTION_ERROR",
|
|
146
|
+
PARSE_ERROR: "PARSE_ERROR"
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/runtime.ts
|
|
150
|
+
var WorkerRuntime = class {
|
|
151
|
+
constructor(authToken, apiBaseUrl) {
|
|
152
|
+
this.authToken = authToken;
|
|
153
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
154
|
+
}
|
|
155
|
+
isInteractive = false;
|
|
156
|
+
isToolMode = true;
|
|
157
|
+
async getAuthToken() {
|
|
158
|
+
return this.authToken;
|
|
159
|
+
}
|
|
160
|
+
log(message) {
|
|
161
|
+
console.log(`[carto-cli] ${message}`);
|
|
162
|
+
}
|
|
163
|
+
warn(message) {
|
|
164
|
+
console.warn(`[carto-cli] ${message}`);
|
|
165
|
+
}
|
|
166
|
+
error(message) {
|
|
167
|
+
console.error(`[carto-cli] ${message}`);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var CliRuntime = class {
|
|
171
|
+
constructor(config) {
|
|
172
|
+
this.config = config;
|
|
173
|
+
this.isInteractive = process.stdout.isTTY ?? false;
|
|
174
|
+
}
|
|
175
|
+
isInteractive;
|
|
176
|
+
isToolMode = false;
|
|
177
|
+
get apiBaseUrl() {
|
|
178
|
+
return this.config.apiBaseUrl;
|
|
179
|
+
}
|
|
180
|
+
async getAuthToken() {
|
|
181
|
+
if (!this.config.authToken) {
|
|
182
|
+
throw new Error("Not authenticated. Run `carto auth login` first.");
|
|
183
|
+
}
|
|
184
|
+
return this.config.authToken;
|
|
185
|
+
}
|
|
186
|
+
log(message) {
|
|
187
|
+
console.log(message);
|
|
188
|
+
}
|
|
189
|
+
warn(message) {
|
|
190
|
+
console.warn(message);
|
|
191
|
+
}
|
|
192
|
+
error(message) {
|
|
193
|
+
console.error(message);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// src/runner.ts
|
|
198
|
+
import { allCommands } from "@carto-knowledge/commands";
|
|
199
|
+
import { CartoClient } from "@carto-knowledge/core";
|
|
200
|
+
async function runInProcess(commandLine, options) {
|
|
201
|
+
const { requestId, authToken, apiBaseUrl, allowlist = DEFAULT_ALLOWLIST } = options;
|
|
202
|
+
if (!commandLine || typeof commandLine !== "string") {
|
|
203
|
+
return errorEnvelope("", {
|
|
204
|
+
code: ErrorCodes.INVALID_ARGUMENTS,
|
|
205
|
+
message: "commandLine must be a non-empty string"
|
|
206
|
+
}, requestId);
|
|
207
|
+
}
|
|
208
|
+
if (!authToken) {
|
|
209
|
+
return errorEnvelope("", {
|
|
210
|
+
code: ErrorCodes.AUTH_ERROR,
|
|
211
|
+
message: "authToken is required"
|
|
212
|
+
}, requestId);
|
|
213
|
+
}
|
|
214
|
+
const tokens = tokenize(commandLine.trim());
|
|
215
|
+
if (tokens.length === 0) {
|
|
216
|
+
return errorEnvelope("", {
|
|
217
|
+
code: ErrorCodes.INVALID_ARGUMENTS,
|
|
218
|
+
message: "Empty command line"
|
|
219
|
+
}, requestId);
|
|
220
|
+
}
|
|
221
|
+
const commandPath = extractCommandPath(tokens);
|
|
222
|
+
const allowlistResult = checkAllowlist(commandPath, allowlist);
|
|
223
|
+
if (!allowlistResult.allowed) {
|
|
224
|
+
return errorEnvelope(commandPath, allowlistResult.error, requestId);
|
|
225
|
+
}
|
|
226
|
+
const augmentedTokens = [
|
|
227
|
+
...tokens,
|
|
228
|
+
"--format",
|
|
229
|
+
"json"
|
|
230
|
+
];
|
|
231
|
+
const cli = new Cli({
|
|
232
|
+
binaryName: "carto",
|
|
233
|
+
enableCapture: true
|
|
234
|
+
});
|
|
235
|
+
for (const CommandClass of allCommands) {
|
|
236
|
+
cli.register(CommandClass);
|
|
237
|
+
}
|
|
238
|
+
let stdoutData = "";
|
|
239
|
+
let stderrData = "";
|
|
240
|
+
const stdout = {
|
|
241
|
+
write(chunk) {
|
|
242
|
+
stdoutData += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const stderr = {
|
|
247
|
+
write(chunk) {
|
|
248
|
+
stderrData += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const runtime = new WorkerRuntime(authToken, apiBaseUrl);
|
|
253
|
+
const client = new CartoClient({
|
|
254
|
+
baseUrl: apiBaseUrl,
|
|
255
|
+
getAuthToken: async () => authToken
|
|
256
|
+
});
|
|
257
|
+
const context = {
|
|
258
|
+
stdin: process.stdin,
|
|
259
|
+
stdout,
|
|
260
|
+
stderr,
|
|
261
|
+
env: {},
|
|
262
|
+
colorDepth: 1,
|
|
263
|
+
client,
|
|
264
|
+
runtime
|
|
265
|
+
};
|
|
266
|
+
try {
|
|
267
|
+
const exitCode = await cli.run(augmentedTokens, context);
|
|
268
|
+
const output = stdoutData.trim();
|
|
269
|
+
const errorOutput = stderrData.trim();
|
|
270
|
+
if (exitCode === 0) {
|
|
271
|
+
try {
|
|
272
|
+
const result = output ? JSON.parse(output) : null;
|
|
273
|
+
return successEnvelope(commandPath, result, requestId);
|
|
274
|
+
} catch {
|
|
275
|
+
return successEnvelope(commandPath, { text: output }, requestId);
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
return errorEnvelope(commandPath, {
|
|
279
|
+
code: ErrorCodes.EXECUTION_ERROR,
|
|
280
|
+
message: errorOutput || output || `Command exited with code ${exitCode}`
|
|
281
|
+
}, requestId);
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
285
|
+
console.error(`[carto-cli] Execution error for '${commandPath}':`, error);
|
|
286
|
+
return errorEnvelope(commandPath, {
|
|
287
|
+
code: ErrorCodes.EXECUTION_ERROR,
|
|
288
|
+
message
|
|
289
|
+
}, requestId);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export {
|
|
293
|
+
CliRuntime,
|
|
294
|
+
DEFAULT_ALLOWLIST,
|
|
295
|
+
ErrorCodes,
|
|
296
|
+
WRITE_ALLOWLIST,
|
|
297
|
+
WorkerRuntime,
|
|
298
|
+
checkAllowlist,
|
|
299
|
+
errorEnvelope,
|
|
300
|
+
extractCommandPath,
|
|
301
|
+
runInProcess,
|
|
302
|
+
successEnvelope,
|
|
303
|
+
tokenize
|
|
304
|
+
};
|
|
305
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runner.ts","../src/tokenizer.ts","../src/allowlist.ts","../src/envelope.ts","../src/runtime.ts"],"sourcesContent":["// ABOUTME: Executes CLI commands in-process without subprocess spawning\n// ABOUTME: Designed for Cloudflare Worker compatibility\n\nimport { Cli } from 'clipanion';\nimport { tokenize, extractCommandPath } from './tokenizer';\nimport { checkAllowlist, AllowlistConfig, DEFAULT_ALLOWLIST } from './allowlist';\nimport { successEnvelope, errorEnvelope, CommandOutput, ErrorCodes } from './envelope';\nimport { WorkerRuntime } from './runtime';\nimport { allCommands } from '@carto-knowledge/commands';\nimport { CartoClient } from '@carto-knowledge/core';\n\nexport interface RunOptions {\n /** Correlation ID for tracing */\n requestId?: string;\n\n /** Auth token for API calls */\n authToken: string;\n\n /** Base URL for Carto API */\n apiBaseUrl: string;\n\n /** Command allowlist (defaults to read-only operations) */\n allowlist?: AllowlistConfig;\n}\n\n/**\n * Executes a CLI command in-process.\n *\n * Designed for Worker runtime:\n * - No subprocess spawning\n * - Captures output to memory streams\n * - Forces JSON output and non-interactive mode\n * - Validates against allowlist\n *\n * @param commandLine - Command string, e.g., \"folder tree --library my-lib\"\n * @param options - Execution options including auth and allowlist\n * @returns Structured command output envelope\n */\nexport async function runInProcess(\n commandLine: string,\n options: RunOptions\n): Promise<CommandOutput> {\n const { requestId, authToken, apiBaseUrl, allowlist = DEFAULT_ALLOWLIST } = options;\n\n // Defensive: Validate inputs\n if (!commandLine || typeof commandLine !== 'string') {\n return errorEnvelope('', {\n code: ErrorCodes.INVALID_ARGUMENTS,\n message: 'commandLine must be a non-empty string',\n }, requestId);\n }\n\n if (!authToken) {\n return errorEnvelope('', {\n code: ErrorCodes.AUTH_ERROR,\n message: 'authToken is required',\n }, requestId);\n }\n\n // Parse command line\n const tokens = tokenize(commandLine.trim());\n\n if (tokens.length === 0) {\n return errorEnvelope('', {\n code: ErrorCodes.INVALID_ARGUMENTS,\n message: 'Empty command line',\n }, requestId);\n }\n\n const commandPath = extractCommandPath(tokens);\n\n // Check allowlist\n const allowlistResult = checkAllowlist(commandPath, allowlist);\n if (!allowlistResult.allowed) {\n return errorEnvelope(commandPath, allowlistResult.error!, requestId);\n }\n\n // Force tool-mode flags\n const augmentedTokens = [\n ...tokens,\n '--format', 'json',\n ];\n\n // Create CLI instance\n const cli = new Cli({\n binaryName: 'carto',\n enableCapture: true,\n });\n\n // Register all commands\n for (const CommandClass of allCommands) {\n cli.register(CommandClass);\n }\n\n // Capture output to memory using simple buffers (Worker-compatible)\n let stdoutData = '';\n let stderrData = '';\n\n const stdout = {\n write(chunk: string | Uint8Array): boolean {\n stdoutData += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);\n return true;\n },\n };\n\n const stderr = {\n write(chunk: string | Uint8Array): boolean {\n stderrData += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);\n return true;\n },\n };\n\n // Create runtime and client\n const runtime = new WorkerRuntime(authToken, apiBaseUrl);\n const client = new CartoClient({\n baseUrl: apiBaseUrl,\n getAuthToken: async () => authToken,\n });\n\n // Create context matching CartoCommandContext\n // Type assertion needed because we use minimal stream stubs for Worker compatibility\n // Clipanion only uses the write() method at runtime\n const context = {\n stdin: process.stdin,\n stdout,\n stderr,\n env: {},\n colorDepth: 1,\n client,\n runtime,\n } as unknown as Parameters<typeof cli.run>[1];\n\n try {\n const exitCode = await cli.run(augmentedTokens, context);\n const output = stdoutData.trim();\n const errorOutput = stderrData.trim();\n\n if (exitCode === 0) {\n // Parse JSON output from command\n try {\n const result = output ? JSON.parse(output) : null;\n return successEnvelope(commandPath, result, requestId);\n } catch {\n // Command succeeded but output wasn't JSON (shouldn't happen in tool mode)\n return successEnvelope(commandPath, { text: output }, requestId);\n }\n } else {\n // Command failed\n return errorEnvelope(commandPath, {\n code: ErrorCodes.EXECUTION_ERROR,\n message: errorOutput || output || `Command exited with code ${exitCode}`,\n }, requestId);\n }\n } catch (error) {\n // Unexpected error during execution\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[carto-cli] Execution error for '${commandPath}':`, error);\n\n return errorEnvelope(commandPath, {\n code: ErrorCodes.EXECUTION_ERROR,\n message,\n }, requestId);\n }\n}\n\n// Re-export for convenience\nexport { DEFAULT_ALLOWLIST, WRITE_ALLOWLIST } from './allowlist';\nexport type { AllowlistConfig } from './allowlist';\nexport type { CommandOutput } from './envelope';\n","// ABOUTME: Tokenizes command line strings for in-process CLI execution\n// ABOUTME: Handles quoted strings and shell-like argument parsing\n\n/**\n * Tokenizes a command line string, handling quoted strings.\n *\n * Examples:\n * - \"folder tree\" => ['folder', 'tree']\n * - \"--name 'My Folder'\" => ['--name', 'My Folder']\n * - '--name \"My Folder\"' => ['--name', 'My Folder']\n * - \"folder create --name 'Test's Folder'\" => handles escaped quotes\n */\nexport function tokenize(commandLine: string): string[] {\n const tokens: string[] = [];\n let current = '';\n let inQuote: string | null = null;\n let escaped = false;\n\n for (let i = 0; i < commandLine.length; i++) {\n const char = commandLine[i];\n\n if (escaped) {\n current += char;\n escaped = false;\n continue;\n }\n\n if (char === '\\\\') {\n escaped = true;\n continue;\n }\n\n if (inQuote) {\n if (char === inQuote) {\n // Empty quoted string should still produce a token\n tokens.push(current);\n current = '';\n inQuote = null;\n } else {\n current += char;\n }\n } else if (char === '\"' || char === \"'\") {\n inQuote = char;\n } else if (char === ' ' || char === '\\t') {\n if (current) {\n tokens.push(current);\n current = '';\n }\n } else {\n current += char;\n }\n }\n\n if (current) {\n tokens.push(current);\n }\n\n return tokens;\n}\n\n/**\n * Extracts the command path from tokens (before any options).\n *\n * Examples:\n * - ['folder', 'tree', '--library', 'x'] => 'folder tree'\n * - ['item', 'search', '-q', 'hello'] => 'item search'\n */\nexport function extractCommandPath(tokens: string[]): string {\n const path: string[] = [];\n for (const token of tokens) {\n if (token.startsWith('-')) break;\n path.push(token);\n }\n return path.join(' ');\n}\n","// ABOUTME: Validates commands against an allowlist for tool-mode execution\n// ABOUTME: Prevents unauthorized command execution in Worker context\n\nexport interface AllowlistConfig {\n commands: string[]; // e.g., ['folder tree', 'item search']\n}\n\nexport interface AllowlistResult {\n allowed: boolean;\n error?: {\n code: 'COMMAND_NOT_ALLOWED';\n message: string;\n };\n}\n\n/**\n * Checks if a command matches the allowlist.\n *\n * Defensive validation:\n * - Normalizes command path (lowercase, trim)\n * - Supports prefix matching for subcommands\n */\nexport function checkAllowlist(\n commandPath: string,\n config: AllowlistConfig\n): AllowlistResult {\n const normalizedPath = commandPath.toLowerCase().trim();\n\n // Check exact match\n if (config.commands.some(cmd => cmd.toLowerCase() === normalizedPath)) {\n return { allowed: true };\n }\n\n // Check prefix match (e.g., 'folder' allows 'folder tree', 'folder create')\n if (config.commands.some(cmd => normalizedPath.startsWith(cmd.toLowerCase() + ' '))) {\n return { allowed: true };\n }\n\n return {\n allowed: false,\n error: {\n code: 'COMMAND_NOT_ALLOWED',\n message: `Command '${commandPath}' is not in the allowlist. ` +\n `Allowed commands: ${config.commands.join(', ')}`,\n },\n };\n}\n\n/**\n * Default allowlist for Worker tool mode.\n * Read-only operations by default.\n */\nexport const DEFAULT_ALLOWLIST: AllowlistConfig = {\n commands: [\n // Folder reads\n 'folder tree',\n 'folder list',\n 'folder get',\n // Item reads\n 'item search',\n 'item get',\n 'item list',\n 'item get-by-uri',\n 'item segments',\n // Edge reads\n 'edge list',\n 'edge get',\n // Share link reads\n 'share-link list',\n 'share-link analytics',\n // Librarian\n 'librarian chat',\n ],\n};\n\n/**\n * Extended allowlist including write operations.\n * Use when agent needs to modify data.\n */\nexport const WRITE_ALLOWLIST: AllowlistConfig = {\n commands: [\n ...DEFAULT_ALLOWLIST.commands,\n // Folder writes\n 'folder create',\n 'folder update',\n 'folder delete',\n 'folder move',\n 'folder restructure',\n 'folder create-tree',\n // Item writes\n 'item create',\n 'item update',\n 'item delete',\n 'item update content',\n 'item upload',\n 'item assign',\n 'item move',\n // Edge writes\n 'edge create',\n 'edge update',\n 'edge delete',\n // Share link writes\n 'share-link create',\n 'share-link delete',\n ],\n};\n","// ABOUTME: Wraps CLI command outputs in structured JSON envelopes\n// ABOUTME: Provides consistent response format for Mastra tool integration\n\nexport interface CommandError {\n code: string;\n message: string;\n}\n\nexport interface CommandOutput<T = unknown> {\n ok: boolean;\n command: string;\n result?: T;\n error?: CommandError;\n requestId?: string;\n schemaVersion: 1;\n}\n\n/**\n * Creates a successful output envelope.\n */\nexport function successEnvelope<T>(\n command: string,\n result: T,\n requestId?: string\n): CommandOutput<T> {\n return {\n ok: true,\n command,\n result,\n requestId,\n schemaVersion: 1,\n };\n}\n\n/**\n * Creates an error output envelope.\n */\nexport function errorEnvelope(\n command: string,\n error: CommandError,\n requestId?: string\n): CommandOutput<never> {\n return {\n ok: false,\n command,\n error,\n requestId,\n schemaVersion: 1,\n };\n}\n\n/**\n * Common error codes for CLI operations.\n */\nexport const ErrorCodes = {\n COMMAND_NOT_ALLOWED: 'COMMAND_NOT_ALLOWED',\n COMMAND_NOT_FOUND: 'COMMAND_NOT_FOUND',\n INVALID_ARGUMENTS: 'INVALID_ARGUMENTS',\n AUTH_ERROR: 'AUTH_ERROR',\n EXECUTION_ERROR: 'EXECUTION_ERROR',\n PARSE_ERROR: 'PARSE_ERROR',\n} as const;\n","// ABOUTME: Abstracts runtime differences between CLI and Worker execution\n// ABOUTME: Provides consistent interface for auth, logging, and config\n\nexport interface CartoRuntime {\n // Authentication\n getAuthToken(): Promise<string>;\n\n // API access\n apiBaseUrl: string;\n\n // Environment flags\n isInteractive: boolean;\n isToolMode: boolean;\n\n // Logging (respects --quiet, routes to correct streams)\n log(message: string): void;\n warn(message: string): void;\n error(message: string): void;\n}\n\n/**\n * Worker runtime - receives auth from request context.\n * Non-interactive, always tool mode.\n */\nexport class WorkerRuntime implements CartoRuntime {\n isInteractive = false;\n isToolMode = true;\n\n constructor(\n private authToken: string,\n public apiBaseUrl: string,\n ) {}\n\n async getAuthToken(): Promise<string> {\n return this.authToken;\n }\n\n log(message: string): void {\n console.log(`[carto-cli] ${message}`);\n }\n\n warn(message: string): void {\n console.warn(`[carto-cli] ${message}`);\n }\n\n error(message: string): void {\n console.error(`[carto-cli] ${message}`);\n }\n}\n\n/**\n * CLI runtime - reads from config file or env vars.\n * Interactive when terminal attached.\n *\n * Compatible with both Bun and Node.js runtimes.\n */\nexport class CliRuntime implements CartoRuntime {\n isInteractive: boolean;\n isToolMode = false;\n\n constructor(\n private config: { apiBaseUrl: string; authToken?: string },\n ) {\n // Works in both Bun and Node.js\n this.isInteractive = process.stdout.isTTY ?? false;\n }\n\n get apiBaseUrl(): string {\n return this.config.apiBaseUrl;\n }\n\n async getAuthToken(): Promise<string> {\n if (!this.config.authToken) {\n throw new Error('Not authenticated. Run `carto auth login` first.');\n }\n return this.config.authToken;\n }\n\n log(message: string): void {\n console.log(message);\n }\n\n warn(message: string): void {\n console.warn(message);\n }\n\n error(message: string): void {\n console.error(message);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carto-knowledge/runner",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "./
|
|
6
|
-
"
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
11
13
|
}
|
|
12
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
13
18
|
"publishConfig": {
|
|
14
19
|
"access": "public"
|
|
15
20
|
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"test": "bun test",
|
|
18
|
-
"test:watch": "bun test --watch",
|
|
19
|
-
"typecheck": "bun x tsc --noEmit"
|
|
20
|
-
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"
|
|
23
|
-
"@carto-knowledge/commands": "
|
|
24
|
-
"
|
|
22
|
+
"clipanion": "^4.0.0-rc.4",
|
|
23
|
+
"@carto-knowledge/commands": "0.2.5",
|
|
24
|
+
"@carto-knowledge/core": "0.2.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/bun": "latest",
|
|
28
28
|
"bun-types": "latest",
|
|
29
|
+
"tsup": "^8.3.5",
|
|
29
30
|
"typescript": "^5.4.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"test:watch": "bun test --watch",
|
|
36
|
+
"typecheck": "bun x tsc --noEmit",
|
|
37
|
+
"test:runtime": "pnpm build && node --input-type=module -e \"import('./dist/index.js')\""
|
|
30
38
|
}
|
|
31
|
-
}
|
|
39
|
+
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @carto/runner@0.1.0 typecheck /Users/pureicis/dev/carto/frontend_v1/packages/carto-runner
|
|
3
|
-
> bun x tsc --noEmit
|
|
4
|
-
|
|
5
|
-
../carto-commands/src/librarian/chat.ts(39,21): error TS2339: Property 'folderId' does not exist on type 'CreateConversationRequest'.
|
|
6
|
-
ELIFECYCLE Command failed with exit code 2.
|
package/bun.lock
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"workspaces": {
|
|
4
|
-
"": {
|
|
5
|
-
"name": "@carto/runner",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"@carto-knowledge/commands": "^0.1.0",
|
|
8
|
-
"@carto-knowledge/core": "^0.1.0",
|
|
9
|
-
"clipanion": "^4.0.0-rc.4",
|
|
10
|
-
},
|
|
11
|
-
"devDependencies": {
|
|
12
|
-
"@types/bun": "latest",
|
|
13
|
-
"typescript": "^5.4.0",
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
"packages": {
|
|
18
|
-
"@carto-knowledge/commands": ["@carto-knowledge/commands@0.1.0", "", { "dependencies": { "@carto-knowledge/core": "^0.1.0", "clipanion": "^4.0.0-rc.4" } }, "sha512-/6JxxaDdyYsyh+Y8Bw9WN4ZMa3yowQN+TBDl8NkymcMyYHnQ6g8DHHOm0vEt0s6U45AlmHlxXn8dNJXHS96I6Q=="],
|
|
19
|
-
|
|
20
|
-
"@carto-knowledge/core": ["@carto-knowledge/core@0.1.0", "", { "dependencies": { "zod": "^3.23.0" } }, "sha512-MVYEeSWZY4uNKwVF/WvJEf4vqMNNeYnCnxaR43m4UL5PzIPRdu8+Q/0DLUAzwLktqHXpv9kXIFsVZETXfC7J3g=="],
|
|
21
|
-
|
|
22
|
-
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
|
23
|
-
|
|
24
|
-
"@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
|
|
25
|
-
|
|
26
|
-
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
|
27
|
-
|
|
28
|
-
"clipanion": ["clipanion@4.0.0-rc.4", "", { "dependencies": { "typanion": "^3.8.0" } }, "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q=="],
|
|
29
|
-
|
|
30
|
-
"typanion": ["typanion@3.14.0", "", {}, "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug=="],
|
|
31
|
-
|
|
32
|
-
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
33
|
-
|
|
34
|
-
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
35
|
-
|
|
36
|
-
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
37
|
-
}
|
|
38
|
-
}
|
package/src/allowlist.test.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// ABOUTME: Tests for command allowlist validation
|
|
2
|
-
// ABOUTME: Validates command filtering for tool-mode execution
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect } from 'bun:test';
|
|
5
|
-
import { checkAllowlist, DEFAULT_ALLOWLIST, WRITE_ALLOWLIST } from './allowlist';
|
|
6
|
-
|
|
7
|
-
describe('checkAllowlist', () => {
|
|
8
|
-
it('allows exact match', () => {
|
|
9
|
-
const result = checkAllowlist('folder tree', { commands: ['folder tree'] });
|
|
10
|
-
expect(result.allowed).toBe(true);
|
|
11
|
-
expect(result.error).toBeUndefined();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('allows case-insensitive match', () => {
|
|
15
|
-
const result = checkAllowlist('FOLDER TREE', { commands: ['folder tree'] });
|
|
16
|
-
expect(result.allowed).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('rejects non-matching command', () => {
|
|
20
|
-
const result = checkAllowlist('folder delete', { commands: ['folder tree'] });
|
|
21
|
-
expect(result.allowed).toBe(false);
|
|
22
|
-
expect(result.error?.code).toBe('COMMAND_NOT_ALLOWED');
|
|
23
|
-
expect(result.error?.message).toContain('folder delete');
|
|
24
|
-
expect(result.error?.message).toContain('folder tree');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('allows prefix match', () => {
|
|
28
|
-
const result = checkAllowlist('folder tree', { commands: ['folder'] });
|
|
29
|
-
expect(result.allowed).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('handles empty allowlist', () => {
|
|
33
|
-
const result = checkAllowlist('anything', { commands: [] });
|
|
34
|
-
expect(result.allowed).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('trims whitespace', () => {
|
|
38
|
-
const result = checkAllowlist(' folder tree ', { commands: ['folder tree'] });
|
|
39
|
-
expect(result.allowed).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('allows multiple commands in allowlist', () => {
|
|
43
|
-
const result = checkAllowlist('item search', { commands: ['folder tree', 'item search', 'librarian chat'] });
|
|
44
|
-
expect(result.allowed).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('does not allow partial command name match', () => {
|
|
48
|
-
// 'fold' should not match 'folder tree'
|
|
49
|
-
const result = checkAllowlist('fold', { commands: ['folder tree'] });
|
|
50
|
-
expect(result.allowed).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('DEFAULT_ALLOWLIST', () => {
|
|
55
|
-
it('includes read-only folder operations', () => {
|
|
56
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('folder tree');
|
|
57
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('folder list');
|
|
58
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('folder get');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('includes read-only item operations', () => {
|
|
62
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('item search');
|
|
63
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('item get');
|
|
64
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('item list');
|
|
65
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('item get-by-uri');
|
|
66
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('item segments');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('includes read-only edge operations', () => {
|
|
70
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('edge list');
|
|
71
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('edge get');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('includes read-only share-link operations', () => {
|
|
75
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('share-link list');
|
|
76
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('share-link analytics');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('includes librarian chat', () => {
|
|
80
|
-
expect(DEFAULT_ALLOWLIST.commands).toContain('librarian chat');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('does not include write operations', () => {
|
|
84
|
-
expect(DEFAULT_ALLOWLIST.commands).not.toContain('folder create');
|
|
85
|
-
expect(DEFAULT_ALLOWLIST.commands).not.toContain('item create');
|
|
86
|
-
expect(DEFAULT_ALLOWLIST.commands).not.toContain('edge create');
|
|
87
|
-
expect(DEFAULT_ALLOWLIST.commands).not.toContain('share-link create');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('WRITE_ALLOWLIST', () => {
|
|
92
|
-
it('includes all read operations', () => {
|
|
93
|
-
for (const cmd of DEFAULT_ALLOWLIST.commands) {
|
|
94
|
-
expect(WRITE_ALLOWLIST.commands).toContain(cmd);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('includes folder write operations', () => {
|
|
99
|
-
expect(WRITE_ALLOWLIST.commands).toContain('folder create');
|
|
100
|
-
expect(WRITE_ALLOWLIST.commands).toContain('folder update');
|
|
101
|
-
expect(WRITE_ALLOWLIST.commands).toContain('folder delete');
|
|
102
|
-
expect(WRITE_ALLOWLIST.commands).toContain('folder move');
|
|
103
|
-
expect(WRITE_ALLOWLIST.commands).toContain('folder restructure');
|
|
104
|
-
expect(WRITE_ALLOWLIST.commands).toContain('folder create-tree');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('includes item write operations', () => {
|
|
108
|
-
expect(WRITE_ALLOWLIST.commands).toContain('item create');
|
|
109
|
-
expect(WRITE_ALLOWLIST.commands).toContain('item update');
|
|
110
|
-
expect(WRITE_ALLOWLIST.commands).toContain('item delete');
|
|
111
|
-
expect(WRITE_ALLOWLIST.commands).toContain('item update content');
|
|
112
|
-
expect(WRITE_ALLOWLIST.commands).toContain('item upload');
|
|
113
|
-
expect(WRITE_ALLOWLIST.commands).toContain('item assign');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('includes edge write operations', () => {
|
|
117
|
-
expect(WRITE_ALLOWLIST.commands).toContain('edge create');
|
|
118
|
-
expect(WRITE_ALLOWLIST.commands).toContain('edge update');
|
|
119
|
-
expect(WRITE_ALLOWLIST.commands).toContain('edge delete');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('includes share-link write operations', () => {
|
|
123
|
-
expect(WRITE_ALLOWLIST.commands).toContain('share-link create');
|
|
124
|
-
expect(WRITE_ALLOWLIST.commands).toContain('share-link delete');
|
|
125
|
-
});
|
|
126
|
-
});
|
package/src/allowlist.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// ABOUTME: Validates commands against an allowlist for tool-mode execution
|
|
2
|
-
// ABOUTME: Prevents unauthorized command execution in Worker context
|
|
3
|
-
|
|
4
|
-
export interface AllowlistConfig {
|
|
5
|
-
commands: string[]; // e.g., ['folder tree', 'item search']
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface AllowlistResult {
|
|
9
|
-
allowed: boolean;
|
|
10
|
-
error?: {
|
|
11
|
-
code: 'COMMAND_NOT_ALLOWED';
|
|
12
|
-
message: string;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Checks if a command matches the allowlist.
|
|
18
|
-
*
|
|
19
|
-
* Defensive validation:
|
|
20
|
-
* - Normalizes command path (lowercase, trim)
|
|
21
|
-
* - Supports prefix matching for subcommands
|
|
22
|
-
*/
|
|
23
|
-
export function checkAllowlist(
|
|
24
|
-
commandPath: string,
|
|
25
|
-
config: AllowlistConfig
|
|
26
|
-
): AllowlistResult {
|
|
27
|
-
const normalizedPath = commandPath.toLowerCase().trim();
|
|
28
|
-
|
|
29
|
-
// Check exact match
|
|
30
|
-
if (config.commands.some(cmd => cmd.toLowerCase() === normalizedPath)) {
|
|
31
|
-
return { allowed: true };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Check prefix match (e.g., 'folder' allows 'folder tree', 'folder create')
|
|
35
|
-
if (config.commands.some(cmd => normalizedPath.startsWith(cmd.toLowerCase() + ' '))) {
|
|
36
|
-
return { allowed: true };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
allowed: false,
|
|
41
|
-
error: {
|
|
42
|
-
code: 'COMMAND_NOT_ALLOWED',
|
|
43
|
-
message: `Command '${commandPath}' is not in the allowlist. ` +
|
|
44
|
-
`Allowed commands: ${config.commands.join(', ')}`,
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Default allowlist for Worker tool mode.
|
|
51
|
-
* Read-only operations by default.
|
|
52
|
-
*/
|
|
53
|
-
export const DEFAULT_ALLOWLIST: AllowlistConfig = {
|
|
54
|
-
commands: [
|
|
55
|
-
// Folder reads
|
|
56
|
-
'folder tree',
|
|
57
|
-
'folder list',
|
|
58
|
-
'folder get',
|
|
59
|
-
// Item reads
|
|
60
|
-
'item search',
|
|
61
|
-
'item get',
|
|
62
|
-
'item list',
|
|
63
|
-
'item get-by-uri',
|
|
64
|
-
'item segments',
|
|
65
|
-
// Edge reads
|
|
66
|
-
'edge list',
|
|
67
|
-
'edge get',
|
|
68
|
-
// Share link reads
|
|
69
|
-
'share-link list',
|
|
70
|
-
'share-link analytics',
|
|
71
|
-
// Librarian
|
|
72
|
-
'librarian chat',
|
|
73
|
-
],
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Extended allowlist including write operations.
|
|
78
|
-
* Use when agent needs to modify data.
|
|
79
|
-
*/
|
|
80
|
-
export const WRITE_ALLOWLIST: AllowlistConfig = {
|
|
81
|
-
commands: [
|
|
82
|
-
...DEFAULT_ALLOWLIST.commands,
|
|
83
|
-
// Folder writes
|
|
84
|
-
'folder create',
|
|
85
|
-
'folder update',
|
|
86
|
-
'folder delete',
|
|
87
|
-
'folder move',
|
|
88
|
-
'folder restructure',
|
|
89
|
-
'folder create-tree',
|
|
90
|
-
// Item writes
|
|
91
|
-
'item create',
|
|
92
|
-
'item update',
|
|
93
|
-
'item delete',
|
|
94
|
-
'item update content',
|
|
95
|
-
'item upload',
|
|
96
|
-
'item assign',
|
|
97
|
-
// Edge writes
|
|
98
|
-
'edge create',
|
|
99
|
-
'edge update',
|
|
100
|
-
'edge delete',
|
|
101
|
-
// Share link writes
|
|
102
|
-
'share-link create',
|
|
103
|
-
'share-link delete',
|
|
104
|
-
],
|
|
105
|
-
};
|