@hasna/connectors 0.1.0 → 0.2.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 +191 -0
- package/README.md +19 -13
- package/bin/index.js +171 -69
- package/bin/mcp.js +24 -18
- package/bin/serve.js +94 -32
- package/connectors/connect-anthropic/package.json +2 -2
- package/connectors/connect-aws/package.json +2 -2
- package/connectors/connect-brandsight/package.json +2 -2
- package/connectors/connect-cloudflare/.env.example +0 -5
- package/connectors/connect-cloudflare/package.json +2 -2
- package/connectors/connect-discord/package.json +2 -2
- package/connectors/connect-docker/package.json +2 -2
- package/connectors/connect-e2b/package.json +2 -2
- package/connectors/connect-elevenlabs/package.json +2 -2
- package/connectors/connect-exa/package.json +2 -2
- package/connectors/connect-figma/package.json +2 -2
- package/connectors/connect-firecrawl/package.json +2 -2
- package/connectors/connect-github/package.json +2 -2
- package/connectors/connect-gmail/package.json +2 -2
- package/connectors/connect-google/package.json +2 -2
- package/connectors/connect-googlecalendar/package.json +2 -2
- package/connectors/connect-googlecloud/package.json +2 -2
- package/connectors/connect-googlecontacts/package.json +2 -2
- package/connectors/connect-googledocs/package.json +2 -2
- package/connectors/connect-googledrive/package.json +2 -2
- package/connectors/connect-googlegemini/package.json +2 -2
- package/connectors/connect-googlesheets/package.json +2 -2
- package/connectors/connect-googletasks/package.json +2 -2
- package/connectors/connect-hedra/package.json +2 -2
- package/connectors/connect-heygen/package.json +2 -2
- package/connectors/connect-huggingface/package.json +2 -2
- package/connectors/connect-icons8/package.json +2 -2
- package/connectors/connect-maropost/package.json +2 -2
- package/connectors/connect-mercury/package.json +2 -2
- package/connectors/connect-meta/package.json +2 -2
- package/connectors/connect-midjourney/package.json +2 -2
- package/connectors/connect-mistral/package.json +2 -2
- package/connectors/connect-mixpanel/package.json +2 -2
- package/connectors/connect-notion/.env.example +0 -5
- package/connectors/connect-notion/package.json +2 -2
- package/connectors/connect-openai/package.json +2 -2
- package/connectors/connect-openweathermap/package.json +2 -2
- package/connectors/connect-pandadoc/package.json +2 -2
- package/connectors/connect-quo/package.json +2 -2
- package/connectors/connect-reddit/package.json +2 -2
- package/connectors/connect-reducto/package.json +1 -1
- package/connectors/connect-resend/package.json +2 -2
- package/connectors/connect-revolut/package.json +2 -2
- package/connectors/connect-sedo/package.json +2 -2
- package/connectors/connect-sentry/package.json +2 -2
- package/connectors/connect-shadcn/package.json +2 -2
- package/connectors/connect-snap/package.json +2 -2
- package/connectors/connect-stabilityai/package.json +2 -2
- package/connectors/connect-stripe/package.json +2 -2
- package/connectors/connect-stripeatlas/package.json +2 -2
- package/connectors/connect-substack/package.json +2 -2
- package/connectors/connect-tiktok/package.json +2 -2
- package/connectors/connect-tinker/package.json +2 -2
- package/connectors/connect-twilio/package.json +4 -4
- package/connectors/connect-uspto/package.json +2 -2
- package/connectors/connect-x/package.json +2 -2
- package/connectors/connect-xads/package.json +2 -2
- package/connectors/connect-xai/package.json +2 -2
- package/connectors/connect-youtube/package.json +2 -2
- package/connectors/connect-zoom/package.json +2 -2
- package/dist/cli/cli.test.d.ts +1 -0
- package/dist/cli/components/App.d.ts +6 -0
- package/dist/cli/components/CategorySelect.d.ts +6 -0
- package/dist/cli/components/ConnectorSelect.d.ts +10 -0
- package/dist/cli/components/Header.d.ts +6 -0
- package/dist/cli/components/InstallProgress.d.ts +8 -0
- package/dist/cli/components/SearchView.d.ts +8 -0
- package/dist/cli/components/components.test.d.ts +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -16
- package/dist/lib/installer.d.ts +55 -0
- package/dist/lib/installer.test.d.ts +1 -0
- package/dist/lib/registry.d.ts +18 -0
- package/dist/lib/registry.test.d.ts +1 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/mcp.test.d.ts +1 -0
- package/dist/server/auth.d.ts +70 -0
- package/dist/server/dashboard.d.ts +5 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/serve.d.ts +12 -0
- package/dist/server/server.test.d.ts +1 -0
- package/package.json +5 -4
- package/connectors/connect-browseruse/.env.example +0 -7
- package/connectors/connect-browseruse/.npmrc.example +0 -2
- package/connectors/connect-browseruse/AGENTS.md +0 -172
- package/connectors/connect-browseruse/CLAUDE.md +0 -172
- package/connectors/connect-browseruse/GEMINI.md +0 -172
- package/connectors/connect-browseruse/README.md +0 -153
- package/connectors/connect-browseruse/package.json +0 -52
- package/connectors/connect-browseruse/src/api/billing.ts +0 -32
- package/connectors/connect-browseruse/src/api/browsers.ts +0 -50
- package/connectors/connect-browseruse/src/api/client.ts +0 -168
- package/connectors/connect-browseruse/src/api/files.ts +0 -70
- package/connectors/connect-browseruse/src/api/index.ts +0 -95
- package/connectors/connect-browseruse/src/api/profiles.ts +0 -59
- package/connectors/connect-browseruse/src/api/sessions.ts +0 -88
- package/connectors/connect-browseruse/src/api/skills.ts +0 -194
- package/connectors/connect-browseruse/src/api/tasks.ts +0 -127
- package/connectors/connect-browseruse/src/cli/index.ts +0 -888
- package/connectors/connect-browseruse/src/index.ts +0 -35
- package/connectors/connect-browseruse/src/types/index.ts +0 -312
- package/connectors/connect-browseruse/src/utils/config.ts +0 -212
- package/connectors/connect-browseruse/src/utils/output.ts +0 -119
- package/connectors/connect-browseruse/tsconfig.json +0 -16
package/bin/index.js
CHANGED
|
@@ -2060,13 +2060,6 @@ var init_registry = __esm(() => {
|
|
|
2060
2060
|
category: "Developer Tools",
|
|
2061
2061
|
tags: ["scraping", "web"]
|
|
2062
2062
|
},
|
|
2063
|
-
{
|
|
2064
|
-
name: "browseruse",
|
|
2065
|
-
displayName: "Browser Use",
|
|
2066
|
-
description: "Browser automation for AI",
|
|
2067
|
-
category: "Developer Tools",
|
|
2068
|
-
tags: ["browser", "automation"]
|
|
2069
|
-
},
|
|
2070
2063
|
{
|
|
2071
2064
|
name: "shadcn",
|
|
2072
2065
|
displayName: "shadcn/ui",
|
|
@@ -2336,9 +2329,9 @@ var init_registry = __esm(() => {
|
|
|
2336
2329
|
{
|
|
2337
2330
|
name: "tinker",
|
|
2338
2331
|
displayName: "Tinker",
|
|
2339
|
-
description: "
|
|
2340
|
-
category: "
|
|
2341
|
-
tags: ["
|
|
2332
|
+
description: "LLM fine-tuning and training API",
|
|
2333
|
+
category: "AI & ML",
|
|
2334
|
+
tags: ["ai", "llm", "fine-tuning"]
|
|
2342
2335
|
},
|
|
2343
2336
|
{
|
|
2344
2337
|
name: "sedo",
|
|
@@ -4005,7 +3998,7 @@ var require_cli_spinners = __commonJS((exports, module) => {
|
|
|
4005
3998
|
});
|
|
4006
3999
|
|
|
4007
4000
|
// src/lib/installer.ts
|
|
4008
|
-
import { existsSync as existsSync2, cpSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4001
|
+
import { existsSync as existsSync2, cpSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, readdirSync, statSync, rmSync } from "fs";
|
|
4009
4002
|
import { join as join2, dirname as dirname2 } from "path";
|
|
4010
4003
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4011
4004
|
function resolveConnectorsDir() {
|
|
@@ -4023,6 +4016,13 @@ function getConnectorPath(name) {
|
|
|
4023
4016
|
}
|
|
4024
4017
|
function installConnector(name, options = {}) {
|
|
4025
4018
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
4019
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
4020
|
+
return {
|
|
4021
|
+
connector: name,
|
|
4022
|
+
success: false,
|
|
4023
|
+
error: `Invalid connector name '${name}'`
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
4026
|
const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
|
|
4027
4027
|
const sourcePath = getConnectorPath(name);
|
|
4028
4028
|
const destDir = join2(targetDir, ".connectors");
|
|
@@ -4063,7 +4063,6 @@ function installConnector(name, options = {}) {
|
|
|
4063
4063
|
}
|
|
4064
4064
|
function updateConnectorsIndex(connectorsDir) {
|
|
4065
4065
|
const indexPath = join2(connectorsDir, "index.ts");
|
|
4066
|
-
const { readdirSync } = __require("fs");
|
|
4067
4066
|
const connectors = readdirSync(connectorsDir).filter((f) => f.startsWith("connect-") && !f.includes("."));
|
|
4068
4067
|
const exports = connectors.map((c) => {
|
|
4069
4068
|
const name = c.replace("connect-", "");
|
|
@@ -4084,7 +4083,6 @@ function getInstalledConnectors(targetDir = process.cwd()) {
|
|
|
4084
4083
|
if (!existsSync2(connectorsDir)) {
|
|
4085
4084
|
return [];
|
|
4086
4085
|
}
|
|
4087
|
-
const { readdirSync, statSync } = __require("fs");
|
|
4088
4086
|
return readdirSync(connectorsDir).filter((f) => {
|
|
4089
4087
|
const fullPath = join2(connectorsDir, f);
|
|
4090
4088
|
return f.startsWith("connect-") && statSync(fullPath).isDirectory();
|
|
@@ -4133,7 +4131,6 @@ function parseEnvVarsTable(section) {
|
|
|
4133
4131
|
return vars;
|
|
4134
4132
|
}
|
|
4135
4133
|
function removeConnector(name, targetDir = process.cwd()) {
|
|
4136
|
-
const { rmSync } = __require("fs");
|
|
4137
4134
|
const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
|
|
4138
4135
|
const connectorsDir = join2(targetDir, ".connectors");
|
|
4139
4136
|
const connectorPath = join2(connectorsDir, connectorName);
|
|
@@ -4152,6 +4149,7 @@ var init_installer = __esm(() => {
|
|
|
4152
4149
|
|
|
4153
4150
|
// src/server/auth.ts
|
|
4154
4151
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
4152
|
+
import { randomBytes } from "crypto";
|
|
4155
4153
|
import { homedir } from "os";
|
|
4156
4154
|
import { join as join3 } from "path";
|
|
4157
4155
|
function getAuthType(name) {
|
|
@@ -4249,14 +4247,22 @@ function saveApiKey(name, key, field) {
|
|
|
4249
4247
|
const profileFile = join3(configDir, "profiles", `${profile}.json`);
|
|
4250
4248
|
const profileDir = join3(configDir, "profiles", profile);
|
|
4251
4249
|
if (existsSync3(profileFile)) {
|
|
4252
|
-
|
|
4250
|
+
let config = {};
|
|
4251
|
+
try {
|
|
4252
|
+
config = JSON.parse(readFileSync3(profileFile, "utf-8"));
|
|
4253
|
+
} catch {}
|
|
4253
4254
|
config[keyField] = key;
|
|
4254
4255
|
writeFileSync2(profileFile, JSON.stringify(config, null, 2));
|
|
4255
4256
|
return;
|
|
4256
4257
|
}
|
|
4257
4258
|
if (existsSync3(profileDir)) {
|
|
4258
4259
|
const configFile = join3(profileDir, "config.json");
|
|
4259
|
-
|
|
4260
|
+
let config = {};
|
|
4261
|
+
if (existsSync3(configFile)) {
|
|
4262
|
+
try {
|
|
4263
|
+
config = JSON.parse(readFileSync3(configFile, "utf-8"));
|
|
4264
|
+
} catch {}
|
|
4265
|
+
}
|
|
4260
4266
|
config[keyField] = key;
|
|
4261
4267
|
writeFileSync2(configFile, JSON.stringify(config, null, 2));
|
|
4262
4268
|
return;
|
|
@@ -4300,16 +4306,33 @@ function getOAuthStartUrl(name, redirectUri) {
|
|
|
4300
4306
|
const scopes = GOOGLE_SCOPES[name];
|
|
4301
4307
|
if (!scopes)
|
|
4302
4308
|
return null;
|
|
4309
|
+
const state = randomBytes(32).toString("hex");
|
|
4310
|
+
oauthStateStore.set(state, { connector: name, createdAt: Date.now() });
|
|
4311
|
+
const tenMinutesAgo = Date.now() - 10 * 60 * 1000;
|
|
4312
|
+
for (const [key, val] of oauthStateStore) {
|
|
4313
|
+
if (val.createdAt < tenMinutesAgo)
|
|
4314
|
+
oauthStateStore.delete(key);
|
|
4315
|
+
}
|
|
4303
4316
|
const params = new URLSearchParams({
|
|
4304
4317
|
client_id: oauthConfig.clientId,
|
|
4305
4318
|
redirect_uri: redirectUri,
|
|
4306
4319
|
response_type: "code",
|
|
4307
4320
|
scope: scopes,
|
|
4308
4321
|
access_type: "offline",
|
|
4309
|
-
prompt: "consent"
|
|
4322
|
+
prompt: "consent",
|
|
4323
|
+
state
|
|
4310
4324
|
});
|
|
4311
4325
|
return `${GOOGLE_AUTH_URL}?${params.toString()}`;
|
|
4312
4326
|
}
|
|
4327
|
+
function validateOAuthState(state, expectedConnector) {
|
|
4328
|
+
if (!state)
|
|
4329
|
+
return false;
|
|
4330
|
+
const entry = oauthStateStore.get(state);
|
|
4331
|
+
if (!entry || entry.connector !== expectedConnector)
|
|
4332
|
+
return false;
|
|
4333
|
+
oauthStateStore.delete(state);
|
|
4334
|
+
return Date.now() - entry.createdAt < 10 * 60 * 1000;
|
|
4335
|
+
}
|
|
4313
4336
|
async function exchangeOAuthCode(name, code, redirectUri) {
|
|
4314
4337
|
const oauthConfig = getOAuthConfig(name);
|
|
4315
4338
|
if (!oauthConfig.clientId || !oauthConfig.clientSecret) {
|
|
@@ -4324,7 +4347,8 @@ async function exchangeOAuthCode(name, code, redirectUri) {
|
|
|
4324
4347
|
client_secret: oauthConfig.clientSecret,
|
|
4325
4348
|
redirect_uri: redirectUri,
|
|
4326
4349
|
grant_type: "authorization_code"
|
|
4327
|
-
})
|
|
4350
|
+
}),
|
|
4351
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT)
|
|
4328
4352
|
});
|
|
4329
4353
|
if (!response.ok) {
|
|
4330
4354
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
@@ -4366,7 +4390,8 @@ async function refreshOAuthToken(name) {
|
|
|
4366
4390
|
client_secret: oauthConfig.clientSecret,
|
|
4367
4391
|
refresh_token: currentTokens.refreshToken,
|
|
4368
4392
|
grant_type: "refresh_token"
|
|
4369
|
-
})
|
|
4393
|
+
}),
|
|
4394
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT)
|
|
4370
4395
|
});
|
|
4371
4396
|
if (!response.ok) {
|
|
4372
4397
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
@@ -4383,9 +4408,10 @@ async function refreshOAuthToken(name) {
|
|
|
4383
4408
|
saveOAuthTokens(name, tokens);
|
|
4384
4409
|
return tokens;
|
|
4385
4410
|
}
|
|
4386
|
-
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth", GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token", GOOGLE_SCOPES;
|
|
4411
|
+
var FETCH_TIMEOUT = 1e4, oauthStateStore, GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth", GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token", GOOGLE_SCOPES;
|
|
4387
4412
|
var init_auth = __esm(() => {
|
|
4388
4413
|
init_installer();
|
|
4414
|
+
oauthStateStore = new Map;
|
|
4389
4415
|
GOOGLE_SCOPES = {
|
|
4390
4416
|
gmail: [
|
|
4391
4417
|
"https://www.googleapis.com/auth/gmail.readonly",
|
|
@@ -4453,18 +4479,25 @@ function resolveDashboardDir() {
|
|
|
4453
4479
|
}
|
|
4454
4480
|
return join4(process.cwd(), "dashboard", "dist");
|
|
4455
4481
|
}
|
|
4456
|
-
function json(data, status = 200) {
|
|
4482
|
+
function json(data, status = 200, port) {
|
|
4457
4483
|
return new Response(JSON.stringify(data), {
|
|
4458
4484
|
status,
|
|
4459
|
-
headers: {
|
|
4485
|
+
headers: {
|
|
4486
|
+
"Content-Type": "application/json",
|
|
4487
|
+
"Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
|
|
4488
|
+
...SECURITY_HEADERS
|
|
4489
|
+
}
|
|
4460
4490
|
});
|
|
4461
4491
|
}
|
|
4462
4492
|
function htmlResponse(content, status = 200) {
|
|
4463
4493
|
return new Response(content, {
|
|
4464
4494
|
status,
|
|
4465
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
4495
|
+
headers: { "Content-Type": "text/html; charset=utf-8", ...SECURITY_HEADERS }
|
|
4466
4496
|
});
|
|
4467
4497
|
}
|
|
4498
|
+
function isValidConnectorName(name) {
|
|
4499
|
+
return /^[a-z0-9-]+$/.test(name);
|
|
4500
|
+
}
|
|
4468
4501
|
function getAllConnectorsWithAuth() {
|
|
4469
4502
|
const installed = new Set(getInstalledConnectors());
|
|
4470
4503
|
return CONNECTORS.map((meta) => {
|
|
@@ -4506,7 +4539,16 @@ async function startServer(port, options) {
|
|
|
4506
4539
|
const dashboardDir = resolveDashboardDir();
|
|
4507
4540
|
const dashboardExists = existsSync4(dashboardDir);
|
|
4508
4541
|
if (!dashboardExists) {
|
|
4509
|
-
console.error(`
|
|
4542
|
+
console.error(`
|
|
4543
|
+
Dashboard not found at: ${dashboardDir}`);
|
|
4544
|
+
console.error(`Run this to build it:
|
|
4545
|
+
`);
|
|
4546
|
+
console.error(` cd dashboard && bun install && bun run build
|
|
4547
|
+
`);
|
|
4548
|
+
console.error(`Or from the project root:
|
|
4549
|
+
`);
|
|
4550
|
+
console.error(` bun run build:dashboard
|
|
4551
|
+
`);
|
|
4510
4552
|
}
|
|
4511
4553
|
const server = Bun.serve({
|
|
4512
4554
|
port,
|
|
@@ -4515,14 +4557,16 @@ async function startServer(port, options) {
|
|
|
4515
4557
|
const path = url2.pathname;
|
|
4516
4558
|
const method = req.method;
|
|
4517
4559
|
if (path === "/api/connectors" && method === "GET") {
|
|
4518
|
-
return json(getAllConnectorsWithAuth());
|
|
4560
|
+
return json(getAllConnectorsWithAuth(), 200, port);
|
|
4519
4561
|
}
|
|
4520
4562
|
const singleMatch = path.match(/^\/api\/connectors\/([^/]+)$/);
|
|
4521
4563
|
if (singleMatch && method === "GET") {
|
|
4522
4564
|
const name = singleMatch[1];
|
|
4565
|
+
if (!isValidConnectorName(name))
|
|
4566
|
+
return json({ error: "Invalid connector name" }, 400, port);
|
|
4523
4567
|
const meta = getConnector(name);
|
|
4524
4568
|
if (!meta)
|
|
4525
|
-
return json({ error: `Connector '${name}' not found` }, 404);
|
|
4569
|
+
return json({ error: `Connector '${name}' not found` }, 404, port);
|
|
4526
4570
|
const auth = getAuthStatus(name);
|
|
4527
4571
|
const docs = getConnectorDocs(name);
|
|
4528
4572
|
return json({
|
|
@@ -4533,29 +4577,36 @@ async function startServer(port, options) {
|
|
|
4533
4577
|
version: meta.version,
|
|
4534
4578
|
auth,
|
|
4535
4579
|
overview: docs?.overview || null
|
|
4536
|
-
});
|
|
4580
|
+
}, 200, port);
|
|
4537
4581
|
}
|
|
4538
4582
|
const keyMatch = path.match(/^\/api\/connectors\/([^/]+)\/key$/);
|
|
4539
4583
|
if (keyMatch && method === "POST") {
|
|
4540
4584
|
const name = keyMatch[1];
|
|
4585
|
+
if (!isValidConnectorName(name))
|
|
4586
|
+
return json({ error: "Invalid connector name" }, 400, port);
|
|
4541
4587
|
try {
|
|
4588
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
4589
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
4590
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
4542
4591
|
const body = await req.json();
|
|
4543
4592
|
if (!body.key)
|
|
4544
|
-
return json({ error: "Missing 'key' in request body" }, 400);
|
|
4593
|
+
return json({ error: "Missing 'key' in request body" }, 400, port);
|
|
4545
4594
|
saveApiKey(name, body.key, body.field);
|
|
4546
|
-
return json({ success: true });
|
|
4595
|
+
return json({ success: true }, 200, port);
|
|
4547
4596
|
} catch (e) {
|
|
4548
|
-
return json({ error: e instanceof Error ? e.message : "Failed to save key" }, 500);
|
|
4597
|
+
return json({ error: e instanceof Error ? e.message : "Failed to save key" }, 500, port);
|
|
4549
4598
|
}
|
|
4550
4599
|
}
|
|
4551
4600
|
const refreshMatch = path.match(/^\/api\/connectors\/([^/]+)\/refresh$/);
|
|
4552
4601
|
if (refreshMatch && method === "POST") {
|
|
4553
4602
|
const name = refreshMatch[1];
|
|
4603
|
+
if (!isValidConnectorName(name))
|
|
4604
|
+
return json({ error: "Invalid connector name" }, 400, port);
|
|
4554
4605
|
try {
|
|
4555
4606
|
const tokens = await refreshOAuthToken(name);
|
|
4556
|
-
return json({ success: true, expiresAt: tokens.expiresAt });
|
|
4607
|
+
return json({ success: true, expiresAt: tokens.expiresAt }, 200, port);
|
|
4557
4608
|
} catch (e) {
|
|
4558
|
-
return json({ success: false, error: e instanceof Error ? e.message : "Failed to refresh" }, 500);
|
|
4609
|
+
return json({ success: false, error: e instanceof Error ? e.message : "Failed to refresh" }, 500, port);
|
|
4559
4610
|
}
|
|
4560
4611
|
}
|
|
4561
4612
|
const oauthStartMatch = path.match(/^\/oauth\/([^/]+)\/start$/);
|
|
@@ -4573,9 +4624,13 @@ async function startServer(port, options) {
|
|
|
4573
4624
|
const name = oauthCallbackMatch[1];
|
|
4574
4625
|
const code = url2.searchParams.get("code");
|
|
4575
4626
|
const error = url2.searchParams.get("error");
|
|
4627
|
+
const state = url2.searchParams.get("state");
|
|
4576
4628
|
if (error) {
|
|
4577
4629
|
return htmlResponse(errorPage("Authentication Failed", error, "You can close this window."));
|
|
4578
4630
|
}
|
|
4631
|
+
if (!validateOAuthState(state, name)) {
|
|
4632
|
+
return htmlResponse(errorPage("Invalid State", "CSRF validation failed. The OAuth state parameter is missing or invalid.", "Please try again from the dashboard."));
|
|
4633
|
+
}
|
|
4579
4634
|
if (!code) {
|
|
4580
4635
|
return htmlResponse(errorPage("Missing Authorization Code", "No code received from the OAuth provider.", "You can close this window and try again."));
|
|
4581
4636
|
}
|
|
@@ -4589,7 +4644,7 @@ async function startServer(port, options) {
|
|
|
4589
4644
|
<p style="color:#666;font-size:14px;">You can close this window and return to the dashboard.</p>
|
|
4590
4645
|
<script>
|
|
4591
4646
|
if (window.opener) {
|
|
4592
|
-
window.opener.postMessage({ type: 'oauth-complete', connector: '${name}' }, '
|
|
4647
|
+
window.opener.postMessage({ type: 'oauth-complete', connector: '${name}' }, 'http://localhost:${port}');
|
|
4593
4648
|
}
|
|
4594
4649
|
</script>
|
|
4595
4650
|
</div>
|
|
@@ -4601,7 +4656,7 @@ async function startServer(port, options) {
|
|
|
4601
4656
|
if (method === "OPTIONS") {
|
|
4602
4657
|
return new Response(null, {
|
|
4603
4658
|
headers: {
|
|
4604
|
-
"Access-Control-Allow-Origin":
|
|
4659
|
+
"Access-Control-Allow-Origin": `http://localhost:${port}`,
|
|
4605
4660
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
4606
4661
|
"Access-Control-Allow-Headers": "Content-Type"
|
|
4607
4662
|
}
|
|
@@ -4619,9 +4674,15 @@ async function startServer(port, options) {
|
|
|
4619
4674
|
if (res)
|
|
4620
4675
|
return res;
|
|
4621
4676
|
}
|
|
4622
|
-
return json({ error: "Not found" }, 404);
|
|
4677
|
+
return json({ error: "Not found" }, 404, port);
|
|
4623
4678
|
}
|
|
4624
4679
|
});
|
|
4680
|
+
const shutdown = () => {
|
|
4681
|
+
server.stop();
|
|
4682
|
+
process.exit(0);
|
|
4683
|
+
};
|
|
4684
|
+
process.on("SIGINT", shutdown);
|
|
4685
|
+
process.on("SIGTERM", shutdown);
|
|
4625
4686
|
const url = `http://localhost:${port}`;
|
|
4626
4687
|
console.log(`Connectors Dashboard running at ${url}`);
|
|
4627
4688
|
if (shouldOpen) {
|
|
@@ -4632,7 +4693,7 @@ async function startServer(port, options) {
|
|
|
4632
4693
|
} catch {}
|
|
4633
4694
|
}
|
|
4634
4695
|
}
|
|
4635
|
-
var MIME_TYPES;
|
|
4696
|
+
var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE;
|
|
4636
4697
|
var init_serve = __esm(() => {
|
|
4637
4698
|
init_registry();
|
|
4638
4699
|
init_installer();
|
|
@@ -4649,6 +4710,11 @@ var init_serve = __esm(() => {
|
|
|
4649
4710
|
".woff": "font/woff",
|
|
4650
4711
|
".woff2": "font/woff2"
|
|
4651
4712
|
};
|
|
4713
|
+
SECURITY_HEADERS = {
|
|
4714
|
+
"X-Content-Type-Options": "nosniff",
|
|
4715
|
+
"X-Frame-Options": "DENY"
|
|
4716
|
+
};
|
|
4717
|
+
MAX_BODY_SIZE = 1024 * 1024;
|
|
4652
4718
|
});
|
|
4653
4719
|
|
|
4654
4720
|
// src/cli/index.tsx
|
|
@@ -5123,10 +5189,12 @@ function CategorySelect({ onSelect, onBack }) {
|
|
|
5123
5189
|
return /* @__PURE__ */ jsxDEV2(Box4, {
|
|
5124
5190
|
flexDirection: "column",
|
|
5125
5191
|
children: [
|
|
5126
|
-
/* @__PURE__ */ jsxDEV2(
|
|
5127
|
-
bold: true,
|
|
5192
|
+
/* @__PURE__ */ jsxDEV2(Box4, {
|
|
5128
5193
|
marginBottom: 1,
|
|
5129
|
-
children:
|
|
5194
|
+
children: /* @__PURE__ */ jsxDEV2(Text4, {
|
|
5195
|
+
bold: true,
|
|
5196
|
+
children: "Select a category:"
|
|
5197
|
+
}, undefined, false, undefined, this)
|
|
5130
5198
|
}, undefined, false, undefined, this),
|
|
5131
5199
|
/* @__PURE__ */ jsxDEV2(SelectInput_default, {
|
|
5132
5200
|
items,
|
|
@@ -5186,10 +5254,12 @@ function ConnectorSelect({
|
|
|
5186
5254
|
return /* @__PURE__ */ jsxDEV3(Box5, {
|
|
5187
5255
|
flexDirection: "column",
|
|
5188
5256
|
children: [
|
|
5189
|
-
/* @__PURE__ */ jsxDEV3(
|
|
5190
|
-
bold: true,
|
|
5257
|
+
/* @__PURE__ */ jsxDEV3(Box5, {
|
|
5191
5258
|
marginBottom: 1,
|
|
5192
|
-
children:
|
|
5259
|
+
children: /* @__PURE__ */ jsxDEV3(Text5, {
|
|
5260
|
+
bold: true,
|
|
5261
|
+
children: "Select connectors to install:"
|
|
5262
|
+
}, undefined, false, undefined, this)
|
|
5193
5263
|
}, undefined, false, undefined, this),
|
|
5194
5264
|
/* @__PURE__ */ jsxDEV3(Box5, {
|
|
5195
5265
|
children: [
|
|
@@ -5550,17 +5620,19 @@ function SearchView({
|
|
|
5550
5620
|
results.length > 0 && /* @__PURE__ */ jsxDEV4(Box6, {
|
|
5551
5621
|
flexDirection: "column",
|
|
5552
5622
|
children: [
|
|
5553
|
-
/* @__PURE__ */ jsxDEV4(
|
|
5554
|
-
dimColor: true,
|
|
5623
|
+
/* @__PURE__ */ jsxDEV4(Box6, {
|
|
5555
5624
|
marginBottom: 1,
|
|
5556
|
-
children:
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5625
|
+
children: /* @__PURE__ */ jsxDEV4(Text7, {
|
|
5626
|
+
dimColor: true,
|
|
5627
|
+
children: [
|
|
5628
|
+
"Found ",
|
|
5629
|
+
results.length,
|
|
5630
|
+
" connector(s)",
|
|
5631
|
+
mode === "search" ? " \u2014 press \u2193 to select" : "",
|
|
5632
|
+
":"
|
|
5633
|
+
]
|
|
5634
|
+
}, undefined, true, undefined, this)
|
|
5635
|
+
}, undefined, false, undefined, this),
|
|
5564
5636
|
/* @__PURE__ */ jsxDEV4(Box6, {
|
|
5565
5637
|
children: [
|
|
5566
5638
|
/* @__PURE__ */ jsxDEV4(Box6, {
|
|
@@ -5909,9 +5981,11 @@ function App({ initialConnectors, overwrite = false }) {
|
|
|
5909
5981
|
view === "main" && /* @__PURE__ */ jsxDEV6(Box8, {
|
|
5910
5982
|
flexDirection: "column",
|
|
5911
5983
|
children: [
|
|
5912
|
-
/* @__PURE__ */ jsxDEV6(
|
|
5984
|
+
/* @__PURE__ */ jsxDEV6(Box8, {
|
|
5913
5985
|
marginBottom: 1,
|
|
5914
|
-
children:
|
|
5986
|
+
children: /* @__PURE__ */ jsxDEV6(Text10, {
|
|
5987
|
+
children: "What would you like to do?"
|
|
5988
|
+
}, undefined, false, undefined, this)
|
|
5915
5989
|
}, undefined, false, undefined, this),
|
|
5916
5990
|
/* @__PURE__ */ jsxDEV6(SelectInput_default, {
|
|
5917
5991
|
items: mainMenuItems,
|
|
@@ -5957,11 +6031,13 @@ function App({ initialConnectors, overwrite = false }) {
|
|
|
5957
6031
|
view === "done" && /* @__PURE__ */ jsxDEV6(Box8, {
|
|
5958
6032
|
flexDirection: "column",
|
|
5959
6033
|
children: [
|
|
5960
|
-
/* @__PURE__ */ jsxDEV6(
|
|
5961
|
-
bold: true,
|
|
5962
|
-
color: "green",
|
|
6034
|
+
/* @__PURE__ */ jsxDEV6(Box8, {
|
|
5963
6035
|
marginBottom: 1,
|
|
5964
|
-
children:
|
|
6036
|
+
children: /* @__PURE__ */ jsxDEV6(Text10, {
|
|
6037
|
+
bold: true,
|
|
6038
|
+
color: "green",
|
|
6039
|
+
children: "Installation complete!"
|
|
6040
|
+
}, undefined, false, undefined, this)
|
|
5965
6041
|
}, undefined, false, undefined, this),
|
|
5966
6042
|
results.filter((r) => r.success).length > 0 && /* @__PURE__ */ jsxDEV6(Box8, {
|
|
5967
6043
|
flexDirection: "column",
|
|
@@ -6049,7 +6125,7 @@ import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
|
|
|
6049
6125
|
loadConnectorVersions();
|
|
6050
6126
|
var isTTY = process.stdout.isTTY ?? false;
|
|
6051
6127
|
var program2 = new Command;
|
|
6052
|
-
program2.name("connectors").description("Install API connectors for your project").version("0.
|
|
6128
|
+
program2.name("connectors").description("Install API connectors for your project").version("0.2.0");
|
|
6053
6129
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive connector browser").action(() => {
|
|
6054
6130
|
if (!isTTY) {
|
|
6055
6131
|
console.log(`Non-interactive environment detected. Use a subcommand:
|
|
@@ -6085,15 +6161,23 @@ program2.command("install").alias("add").argument("[connectors...]", "Connectors
|
|
|
6085
6161
|
console.log(chalk2.bold(`
|
|
6086
6162
|
Installing connectors...
|
|
6087
6163
|
`));
|
|
6164
|
+
const succeeded = [];
|
|
6088
6165
|
for (const result of results) {
|
|
6089
6166
|
if (result.success) {
|
|
6090
6167
|
console.log(chalk2.green(`\u2713 ${result.connector}`));
|
|
6168
|
+
succeeded.push(result.connector);
|
|
6091
6169
|
} else {
|
|
6092
6170
|
console.log(chalk2.red(`\u2717 ${result.connector}: ${result.error}`));
|
|
6093
6171
|
}
|
|
6094
6172
|
}
|
|
6095
|
-
|
|
6096
|
-
|
|
6173
|
+
if (succeeded.length > 0) {
|
|
6174
|
+
console.log(chalk2.bold(`
|
|
6175
|
+
Next steps:`));
|
|
6176
|
+
const importNames = succeeded.join(", ");
|
|
6177
|
+
console.log(chalk2.dim(` 1. Import: `) + `import { ${importNames} } from './.connectors'`);
|
|
6178
|
+
console.log(chalk2.dim(` 2. Set key: `) + `connectors docs ${succeeded[0]}` + chalk2.dim(` (see env vars)`));
|
|
6179
|
+
console.log(chalk2.dim(` 3. Explore: `) + `connectors serve` + chalk2.dim(` (dashboard for auth management)`));
|
|
6180
|
+
}
|
|
6097
6181
|
process.exit(results.every((r) => r.success) ? 0 : 1);
|
|
6098
6182
|
});
|
|
6099
6183
|
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available connectors", false).option("-i, --installed", "Show only installed connectors", false).option("--json", "Output as JSON", false).description("List available or installed connectors").action((options) => {
|
|
@@ -6319,7 +6403,7 @@ Categories:
|
|
|
6319
6403
|
console.log(` ${category} (${count})`);
|
|
6320
6404
|
}
|
|
6321
6405
|
});
|
|
6322
|
-
program2.command("serve").alias("dashboard").option("-p, --port <port>", "Port to run the dashboard on", "19426").option("--open", "Open dashboard in browser (default)", true).option("--no-open", "Don't open browser automatically").description("Start local dashboard for connector auth management").action(async (options) => {
|
|
6406
|
+
program2.command("serve").alias("dashboard").alias("open").option("-p, --port <port>", "Port to run the dashboard on", "19426").option("--open", "Open dashboard in browser (default)", true).option("--no-open", "Don't open browser automatically").description("Start local dashboard for connector auth management").action(async (options) => {
|
|
6323
6407
|
const port = parseInt(options.port, 10);
|
|
6324
6408
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
6325
6409
|
console.log(chalk2.red("Invalid port number"));
|
|
@@ -6332,14 +6416,32 @@ Starting Connectors Dashboard...
|
|
|
6332
6416
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
6333
6417
|
await startServer2(port, { open: options.open });
|
|
6334
6418
|
});
|
|
6335
|
-
program2.command("
|
|
6336
|
-
const
|
|
6337
|
-
if (
|
|
6338
|
-
|
|
6339
|
-
|
|
6419
|
+
program2.command("update").description("Update all installed connectors to the latest version from the package").option("--json", "Output as JSON", false).action((options) => {
|
|
6420
|
+
const installed = getInstalledConnectors();
|
|
6421
|
+
if (installed.length === 0) {
|
|
6422
|
+
if (options.json) {
|
|
6423
|
+
console.log(JSON.stringify({ updated: [] }));
|
|
6424
|
+
} else {
|
|
6425
|
+
console.log(chalk2.dim("No connectors installed. Run: connectors install <name>"));
|
|
6426
|
+
}
|
|
6340
6427
|
return;
|
|
6341
6428
|
}
|
|
6342
|
-
const
|
|
6343
|
-
|
|
6429
|
+
const results = installed.map((name) => installConnector(name, { overwrite: true }));
|
|
6430
|
+
if (options.json) {
|
|
6431
|
+
console.log(JSON.stringify(results, null, 2));
|
|
6432
|
+
process.exit(results.every((r) => r.success) ? 0 : 1);
|
|
6433
|
+
return;
|
|
6434
|
+
}
|
|
6435
|
+
console.log(chalk2.bold(`
|
|
6436
|
+
Updating ${installed.length} connector(s)...
|
|
6437
|
+
`));
|
|
6438
|
+
for (const result of results) {
|
|
6439
|
+
if (result.success) {
|
|
6440
|
+
console.log(chalk2.green(`\u2713 ${result.connector}`));
|
|
6441
|
+
} else {
|
|
6442
|
+
console.log(chalk2.red(`\u2717 ${result.connector}: ${result.error}`));
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
process.exit(results.every((r) => r.success) ? 0 : 1);
|
|
6344
6446
|
});
|
|
6345
6447
|
program2.parse();
|
package/bin/mcp.js
CHANGED
|
@@ -26,7 +26,6 @@ var __export = (target, all) => {
|
|
|
26
26
|
set: (newValue) => all[name] = () => newValue
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
|
-
var __require = import.meta.require;
|
|
30
29
|
|
|
31
30
|
// node_modules/ajv/dist/compile/codegen/code.js
|
|
32
31
|
var require_code = __commonJS((exports) => {
|
|
@@ -4570,6 +4569,7 @@ var require_limitLength = __commonJS((exports) => {
|
|
|
4570
4569
|
var require_pattern = __commonJS((exports) => {
|
|
4571
4570
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4572
4571
|
var code_1 = require_code2();
|
|
4572
|
+
var util_1 = require_util();
|
|
4573
4573
|
var codegen_1 = require_codegen();
|
|
4574
4574
|
var error2 = {
|
|
4575
4575
|
message: ({ schemaCode }) => (0, codegen_1.str)`must match pattern "${schemaCode}"`,
|
|
@@ -4582,10 +4582,18 @@ var require_pattern = __commonJS((exports) => {
|
|
|
4582
4582
|
$data: true,
|
|
4583
4583
|
error: error2,
|
|
4584
4584
|
code(cxt) {
|
|
4585
|
-
const { data, $data, schema, schemaCode, it } = cxt;
|
|
4585
|
+
const { gen, data, $data, schema, schemaCode, it } = cxt;
|
|
4586
4586
|
const u = it.opts.unicodeRegExp ? "u" : "";
|
|
4587
|
-
|
|
4588
|
-
|
|
4587
|
+
if ($data) {
|
|
4588
|
+
const { regExp } = it.opts.code;
|
|
4589
|
+
const regExpCode = regExp.code === "new RegExp" ? (0, codegen_1._)`new RegExp` : (0, util_1.useFunc)(gen, regExp);
|
|
4590
|
+
const valid = gen.let("valid");
|
|
4591
|
+
gen.try(() => gen.assign(valid, (0, codegen_1._)`${regExpCode}(${schemaCode}, ${u}).test(${data})`), () => gen.assign(valid, false));
|
|
4592
|
+
cxt.fail$data((0, codegen_1._)`!${valid}`);
|
|
4593
|
+
} else {
|
|
4594
|
+
const regExp = (0, code_1.usePattern)(cxt, schema);
|
|
4595
|
+
cxt.fail$data((0, codegen_1._)`!${regExp}.test(${data})`);
|
|
4596
|
+
}
|
|
4589
4597
|
}
|
|
4590
4598
|
};
|
|
4591
4599
|
exports.default = def;
|
|
@@ -19549,13 +19557,6 @@ var CONNECTORS = [
|
|
|
19549
19557
|
category: "Developer Tools",
|
|
19550
19558
|
tags: ["scraping", "web"]
|
|
19551
19559
|
},
|
|
19552
|
-
{
|
|
19553
|
-
name: "browseruse",
|
|
19554
|
-
displayName: "Browser Use",
|
|
19555
|
-
description: "Browser automation for AI",
|
|
19556
|
-
category: "Developer Tools",
|
|
19557
|
-
tags: ["browser", "automation"]
|
|
19558
|
-
},
|
|
19559
19560
|
{
|
|
19560
19561
|
name: "shadcn",
|
|
19561
19562
|
displayName: "shadcn/ui",
|
|
@@ -19825,9 +19826,9 @@ var CONNECTORS = [
|
|
|
19825
19826
|
{
|
|
19826
19827
|
name: "tinker",
|
|
19827
19828
|
displayName: "Tinker",
|
|
19828
|
-
description: "
|
|
19829
|
-
category: "
|
|
19830
|
-
tags: ["
|
|
19829
|
+
description: "LLM fine-tuning and training API",
|
|
19830
|
+
category: "AI & ML",
|
|
19831
|
+
tags: ["ai", "llm", "fine-tuning"]
|
|
19831
19832
|
},
|
|
19832
19833
|
{
|
|
19833
19834
|
name: "sedo",
|
|
@@ -19886,7 +19887,7 @@ function loadConnectorVersions() {
|
|
|
19886
19887
|
}
|
|
19887
19888
|
|
|
19888
19889
|
// src/lib/installer.ts
|
|
19889
|
-
import { existsSync as existsSync2, cpSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
19890
|
+
import { existsSync as existsSync2, cpSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, readdirSync, statSync, rmSync } from "fs";
|
|
19890
19891
|
import { join as join2, dirname as dirname2 } from "path";
|
|
19891
19892
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
19892
19893
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
@@ -19906,6 +19907,13 @@ function getConnectorPath(name) {
|
|
|
19906
19907
|
}
|
|
19907
19908
|
function installConnector(name, options = {}) {
|
|
19908
19909
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
19910
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
19911
|
+
return {
|
|
19912
|
+
connector: name,
|
|
19913
|
+
success: false,
|
|
19914
|
+
error: `Invalid connector name '${name}'`
|
|
19915
|
+
};
|
|
19916
|
+
}
|
|
19909
19917
|
const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
|
|
19910
19918
|
const sourcePath = getConnectorPath(name);
|
|
19911
19919
|
const destDir = join2(targetDir, ".connectors");
|
|
@@ -19946,7 +19954,6 @@ function installConnector(name, options = {}) {
|
|
|
19946
19954
|
}
|
|
19947
19955
|
function updateConnectorsIndex(connectorsDir) {
|
|
19948
19956
|
const indexPath = join2(connectorsDir, "index.ts");
|
|
19949
|
-
const { readdirSync } = __require("fs");
|
|
19950
19957
|
const connectors = readdirSync(connectorsDir).filter((f) => f.startsWith("connect-") && !f.includes("."));
|
|
19951
19958
|
const exports = connectors.map((c) => {
|
|
19952
19959
|
const name = c.replace("connect-", "");
|
|
@@ -19967,7 +19974,6 @@ function getInstalledConnectors(targetDir = process.cwd()) {
|
|
|
19967
19974
|
if (!existsSync2(connectorsDir)) {
|
|
19968
19975
|
return [];
|
|
19969
19976
|
}
|
|
19970
|
-
const { readdirSync, statSync } = __require("fs");
|
|
19971
19977
|
return readdirSync(connectorsDir).filter((f) => {
|
|
19972
19978
|
const fullPath = join2(connectorsDir, f);
|
|
19973
19979
|
return f.startsWith("connect-") && statSync(fullPath).isDirectory();
|
|
@@ -20016,7 +20022,6 @@ function parseEnvVarsTable(section) {
|
|
|
20016
20022
|
return vars;
|
|
20017
20023
|
}
|
|
20018
20024
|
function removeConnector(name, targetDir = process.cwd()) {
|
|
20019
|
-
const { rmSync } = __require("fs");
|
|
20020
20025
|
const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
|
|
20021
20026
|
const connectorsDir = join2(targetDir, ".connectors");
|
|
20022
20027
|
const connectorPath = join2(connectorsDir, connectorName);
|
|
@@ -20032,6 +20037,7 @@ function removeConnector(name, targetDir = process.cwd()) {
|
|
|
20032
20037
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
20033
20038
|
import { homedir } from "os";
|
|
20034
20039
|
import { join as join3 } from "path";
|
|
20040
|
+
var oauthStateStore = new Map;
|
|
20035
20041
|
var GOOGLE_SCOPES = {
|
|
20036
20042
|
gmail: [
|
|
20037
20043
|
"https://www.googleapis.com/auth/gmail.readonly",
|