teamcopilot 0.1.16 → 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/README.md +88 -9
- package/dist/chat/index.js +23 -1
- package/dist/frontend/assets/{cssMode-CH26ItO2.js → cssMode-CM1GmZ3H.js} +1 -1
- package/dist/frontend/assets/{freemarker2-CiRHXG8W.js → freemarker2-C8TeljYR.js} +1 -1
- package/dist/frontend/assets/{handlebars-DXV-JQiR.js → handlebars-B2e-Wzyt.js} +1 -1
- package/dist/frontend/assets/{html-DKdYDRJv.js → html-DtBAvTj2.js} +1 -1
- package/dist/frontend/assets/{htmlMode-D466XPJJ.js → htmlMode-Dta08RE6.js} +1 -1
- package/dist/frontend/assets/index-BirlyHV4.css +1 -0
- package/dist/frontend/assets/{index-CvsPLefz.js → index-Dp0jlIX9.js} +201 -201
- package/dist/frontend/assets/{javascript-D5lHN8tF.js → javascript-BYeHq-2v.js} +1 -1
- package/dist/frontend/assets/{jsonMode-C9Wdxaho.js → jsonMode-DkJo6l8K.js} +1 -1
- package/dist/frontend/assets/{liquid-NIH--tpJ.js → liquid-nmEuajdb.js} +1 -1
- package/dist/frontend/assets/{mdx-xwEbqXME.js → mdx-BJybRyf3.js} +1 -1
- package/dist/frontend/assets/{python-BzErW_b3.js → python-DRAABm9s.js} +1 -1
- package/dist/frontend/assets/{razor-B0v-Bw5B.js → razor-7lH4jzk8.js} +1 -1
- package/dist/frontend/assets/{tsMode-B9YN5EEb.js → tsMode-ClcmdG3S.js} +1 -1
- package/dist/frontend/assets/{typescript-DIMXtHre.js → typescript-D9oav8M6.js} +1 -1
- package/dist/frontend/assets/{xml-DQ5HnppJ.js → xml-B0ks0e6Y.js} +1 -1
- package/dist/frontend/assets/{yaml-BQCOKj13.js → yaml-CCDt1oK4.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/index.js +99 -90
- package/dist/secrets/index.js +74 -0
- package/dist/skills/index.js +43 -1
- package/dist/users/index.js +98 -0
- package/dist/utils/redact.js +52 -5
- package/dist/utils/resource-file-routes.js +2 -4
- package/dist/utils/resource-files.js +10 -2
- package/dist/utils/secret-contract-validation.js +184 -0
- package/dist/utils/secrets.js +127 -0
- package/dist/utils/skill-files.js +7 -0
- package/dist/utils/skill.js +50 -1
- package/dist/utils/workflow-runner.js +19 -4
- package/dist/utils/workflow.js +13 -1
- package/dist/workflows/index.js +10 -1
- package/dist/workspace_files/.opencode/plugins/createSkill.ts +1 -26
- package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +3 -3
- package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +93 -5
- package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +31 -49
- package/dist/workspace_files/.opencode/plugins/listAvailableSecretKeys.ts +107 -0
- package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +2 -2
- package/dist/workspace_files/.opencode/plugins/secret-proxy.ts +818 -0
- package/dist/workspace_files/AGENTS.md +91 -21
- package/package.json +5 -3
- package/prisma/generated/client/edge.js +24 -3
- package/prisma/generated/client/index-browser.js +21 -0
- package/prisma/generated/client/index.d.ts +3139 -128
- package/prisma/generated/client/index.js +24 -3
- package/prisma/generated/client/package.json +1 -1
- package/prisma/generated/client/schema.prisma +27 -0
- package/prisma/generated/client/wasm.js +24 -3
- package/prisma/migrations/20260402060129_add_secret_management/migration.sql +38 -0
- package/prisma/migrations/20260404052800_remove_global_secret_user_fkeys/migration.sql +20 -0
- package/prisma/schema.prisma +27 -0
- package/dist/frontend/assets/index-B8Ip8I8F.css +0 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createApp = createApp;
|
|
6
7
|
BigInt.prototype.toJSON = function () {
|
|
7
8
|
const num = Number(this);
|
|
8
9
|
if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
|
|
@@ -19,6 +20,7 @@ const workflows_1 = __importDefault(require("./workflows"));
|
|
|
19
20
|
const chat_1 = __importDefault(require("./chat"));
|
|
20
21
|
const skills_1 = __importDefault(require("./skills"));
|
|
21
22
|
const users_1 = __importDefault(require("./users"));
|
|
23
|
+
const secrets_1 = __importDefault(require("./secrets"));
|
|
22
24
|
const cronjob_1 = require("./cronjob");
|
|
23
25
|
const opencode_server_1 = require("./opencode-server");
|
|
24
26
|
const path_1 = __importDefault(require("path"));
|
|
@@ -30,97 +32,102 @@ const opencode_auth_1 = require("./utils/opencode-auth");
|
|
|
30
32
|
const opencode_auth_2 = __importDefault(require("./opencode-auth"));
|
|
31
33
|
const jwt_secret_1 = require("./utils/jwt-secret");
|
|
32
34
|
const runtime_paths_1 = require("./utils/runtime-paths");
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// const
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
apiRouter.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (Array.isArray(contentType)) {
|
|
66
|
-
return contentType.some((value) => value.includes("application/json"));
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
};
|
|
70
|
-
res.json = ((body) => {
|
|
71
|
-
if (shouldSkipSanitization()) {
|
|
72
|
-
return originalJson(body);
|
|
73
|
-
}
|
|
74
|
-
return originalJson((0, redact_1.sanitizeForClient)(body));
|
|
75
|
-
});
|
|
76
|
-
res.send = ((body) => {
|
|
77
|
-
if (shouldSkipSanitization()) {
|
|
78
|
-
return originalSend(body);
|
|
79
|
-
}
|
|
80
|
-
if (typeof body === "string") {
|
|
81
|
-
if (hasJsonContentType()) {
|
|
82
|
-
return originalSend(body);
|
|
35
|
+
function createApp() {
|
|
36
|
+
const app = (0, express_1.default)();
|
|
37
|
+
const frontendDistDirectory = (0, runtime_paths_1.getFrontendDistDirectory)();
|
|
38
|
+
app.use(express_1.default.json());
|
|
39
|
+
// Logging middleware
|
|
40
|
+
// app.use((req, res, next) => {
|
|
41
|
+
// const start = Date.now();
|
|
42
|
+
// res.on('finish', async () => {
|
|
43
|
+
// const duration = Date.now() - start;
|
|
44
|
+
// const logData = {
|
|
45
|
+
// method: req.method,
|
|
46
|
+
// path: req.path,
|
|
47
|
+
// statusCode: res.statusCode,
|
|
48
|
+
// duration: `${duration}ms`,
|
|
49
|
+
// userAgent: req.get('user-agent'),
|
|
50
|
+
// ip: req.ip
|
|
51
|
+
// };
|
|
52
|
+
// logInfo(`HTTP Request: ${req.method} ${req.path} ${res.statusCode}`, { meta: logData });
|
|
53
|
+
// });
|
|
54
|
+
// next();
|
|
55
|
+
// });
|
|
56
|
+
// Mount auth routes directly (no sanitization for token responses)
|
|
57
|
+
app.use('/api/auth', auth_1.default);
|
|
58
|
+
const apiRouter = express_1.default.Router();
|
|
59
|
+
apiRouter.use((_req, res, next) => {
|
|
60
|
+
const originalJson = res.json.bind(res);
|
|
61
|
+
const originalSend = res.send.bind(res);
|
|
62
|
+
const shouldSkipSanitization = () => Boolean(res.locals.skipResponseSanitization);
|
|
63
|
+
const hasJsonContentType = () => {
|
|
64
|
+
const contentType = res.getHeader("Content-Type");
|
|
65
|
+
if (typeof contentType === "string") {
|
|
66
|
+
return contentType.includes("application/json");
|
|
83
67
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
68
|
+
if (Array.isArray(contentType)) {
|
|
69
|
+
return contentType.some((value) => value.includes("application/json"));
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
73
|
+
res.json = ((body) => {
|
|
74
|
+
if (shouldSkipSanitization()) {
|
|
75
|
+
return originalJson(body);
|
|
76
|
+
}
|
|
77
|
+
return originalJson((0, redact_1.sanitizeForClient)(body));
|
|
78
|
+
});
|
|
79
|
+
res.send = ((body) => {
|
|
80
|
+
if (shouldSkipSanitization()) {
|
|
88
81
|
return originalSend(body);
|
|
89
82
|
}
|
|
90
|
-
|
|
83
|
+
if (typeof body === "string") {
|
|
84
|
+
if (hasJsonContentType()) {
|
|
85
|
+
return originalSend(body);
|
|
86
|
+
}
|
|
87
|
+
return originalSend((0, redact_1.sanitizeStringContent)(body));
|
|
88
|
+
}
|
|
89
|
+
if (Buffer.isBuffer(body)) {
|
|
90
|
+
if (hasJsonContentType()) {
|
|
91
|
+
return originalSend(body);
|
|
92
|
+
}
|
|
93
|
+
return originalSend(Buffer.from((0, redact_1.sanitizeStringContent)(body.toString("utf-8")), "utf-8"));
|
|
94
|
+
}
|
|
95
|
+
return originalSend((0, redact_1.sanitizeForClient)(body));
|
|
96
|
+
});
|
|
97
|
+
next();
|
|
98
|
+
});
|
|
99
|
+
apiRouter.get("/", (_req, res) => {
|
|
100
|
+
// for healthcheck
|
|
101
|
+
res.send("Hello from the API!");
|
|
102
|
+
});
|
|
103
|
+
apiRouter.get("/workspace", (0, utils_1.apiHandler)(async (_req, res) => {
|
|
104
|
+
const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
|
|
105
|
+
res.json({ workspace_dir: workspaceDir });
|
|
106
|
+
}, false));
|
|
107
|
+
apiRouter.use('/workflows', workflows_1.default);
|
|
108
|
+
apiRouter.use('/chat', chat_1.default);
|
|
109
|
+
apiRouter.use('/skills', skills_1.default);
|
|
110
|
+
apiRouter.use('/users', users_1.default);
|
|
111
|
+
apiRouter.use('/secrets', secrets_1.default);
|
|
112
|
+
apiRouter.use('/opencode-auth', opencode_auth_2.default);
|
|
113
|
+
app.use('/api', apiRouter);
|
|
114
|
+
// Serve static assets (JS, CSS, etc.) with correct MIME types
|
|
115
|
+
app.use(express_1.default.static(frontendDistDirectory));
|
|
116
|
+
// SPA fallback: serve index.html for non-API routes (client-side routing)
|
|
117
|
+
app.get("*", (_req, res) => {
|
|
118
|
+
res.sendFile(path_1.default.join(frontendDistDirectory, "index.html"));
|
|
119
|
+
});
|
|
120
|
+
app.use(async (err, req, res, _) => {
|
|
121
|
+
let status = err.status || 500;
|
|
122
|
+
let doLogging = err.doLogging !== false;
|
|
123
|
+
if (status !== 404 && doLogging) {
|
|
124
|
+
(0, logging_1.logError)({ err, apiPath: req.path, apiMethod: req.method });
|
|
91
125
|
}
|
|
92
|
-
|
|
126
|
+
res.status(status).json({ message: err.message || 'Unknown error' });
|
|
93
127
|
});
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// for healthcheck
|
|
98
|
-
res.send("Hello from the API!");
|
|
99
|
-
});
|
|
100
|
-
apiRouter.get("/workspace", (0, utils_1.apiHandler)(async (_req, res) => {
|
|
101
|
-
const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
|
|
102
|
-
res.json({ workspace_dir: workspaceDir });
|
|
103
|
-
}, false));
|
|
104
|
-
apiRouter.use('/workflows', workflows_1.default);
|
|
105
|
-
apiRouter.use('/chat', chat_1.default);
|
|
106
|
-
apiRouter.use('/skills', skills_1.default);
|
|
107
|
-
apiRouter.use('/users', users_1.default);
|
|
108
|
-
apiRouter.use('/opencode-auth', opencode_auth_2.default);
|
|
109
|
-
app.use('/api', apiRouter);
|
|
110
|
-
// Serve static assets (JS, CSS, etc.) with correct MIME types
|
|
111
|
-
app.use(express_1.default.static(frontendDistDirectory));
|
|
112
|
-
// SPA fallback: serve index.html for non-API routes (client-side routing)
|
|
113
|
-
app.get("*", (_req, res) => {
|
|
114
|
-
res.sendFile(path_1.default.join(frontendDistDirectory, "index.html"));
|
|
115
|
-
});
|
|
116
|
-
app.use(async (err, req, res, _) => {
|
|
117
|
-
let status = err.status || 500;
|
|
118
|
-
let doLogging = err.doLogging !== false;
|
|
119
|
-
if (status !== 404 && doLogging) {
|
|
120
|
-
(0, logging_1.logError)({ err, apiPath: req.path, apiMethod: req.method });
|
|
121
|
-
}
|
|
122
|
-
res.status(status).json({ message: err.message || 'Unknown error' });
|
|
123
|
-
});
|
|
128
|
+
return app;
|
|
129
|
+
}
|
|
130
|
+
const app = createApp();
|
|
124
131
|
let httpServer = null;
|
|
125
132
|
let isShuttingDown = false;
|
|
126
133
|
async function shutdown(exitCode) {
|
|
@@ -163,7 +170,9 @@ process.on("unhandledRejection", (reason) => {
|
|
|
163
170
|
console.error("Unhandled rejection:", reason);
|
|
164
171
|
void shutdown(1);
|
|
165
172
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
if (require.main === module) {
|
|
174
|
+
void bootstrap().catch((err) => {
|
|
175
|
+
console.error("Failed to start server:", err);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const client_1 = __importDefault(require("../prisma/client"));
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const secrets_1 = require("../utils/secrets");
|
|
10
|
+
const router = express_1.default.Router({ mergeParams: true });
|
|
11
|
+
router.get("/global", (0, utils_1.apiHandler)(async (req, res) => {
|
|
12
|
+
const rows = await client_1.default.global_secrets.findMany({
|
|
13
|
+
orderBy: { key: "asc" }
|
|
14
|
+
});
|
|
15
|
+
const shouldMaskValues = req.opencode_session_id === undefined;
|
|
16
|
+
res.json({
|
|
17
|
+
secrets: rows.map((row) => (0, secrets_1.toSecretListItem)(row, shouldMaskValues))
|
|
18
|
+
});
|
|
19
|
+
}, true));
|
|
20
|
+
router.put("/global/:key", (0, utils_1.apiHandler)(async (req, res) => {
|
|
21
|
+
if (req.role !== "Engineer") {
|
|
22
|
+
throw {
|
|
23
|
+
status: 403,
|
|
24
|
+
message: "Only engineers can manage global secrets"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const key = (0, secrets_1.assertSecretKey)(req.params.key);
|
|
28
|
+
const value = typeof req.body?.value === "string" ? req.body.value : "";
|
|
29
|
+
if (value.length === 0) {
|
|
30
|
+
throw {
|
|
31
|
+
status: 400,
|
|
32
|
+
message: "value is required"
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const now = BigInt(Date.now());
|
|
36
|
+
const row = await client_1.default.global_secrets.upsert({
|
|
37
|
+
where: { key },
|
|
38
|
+
create: {
|
|
39
|
+
key,
|
|
40
|
+
value,
|
|
41
|
+
created_by_user_id: req.userId,
|
|
42
|
+
updated_by_user_id: req.userId,
|
|
43
|
+
created_at: now,
|
|
44
|
+
updated_at: now,
|
|
45
|
+
},
|
|
46
|
+
update: {
|
|
47
|
+
value,
|
|
48
|
+
updated_by_user_id: req.userId,
|
|
49
|
+
updated_at: now,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
res.json({
|
|
53
|
+
secret: (0, secrets_1.toSecretListItem)(row, req.opencode_session_id === undefined)
|
|
54
|
+
});
|
|
55
|
+
}, true));
|
|
56
|
+
router.delete("/global/:key", (0, utils_1.apiHandler)(async (req, res) => {
|
|
57
|
+
if (req.role !== "Engineer") {
|
|
58
|
+
throw {
|
|
59
|
+
status: 403,
|
|
60
|
+
message: "Only engineers can manage global secrets"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const key = (0, secrets_1.assertSecretKey)(req.params.key);
|
|
64
|
+
await client_1.default.global_secrets.delete({
|
|
65
|
+
where: { key }
|
|
66
|
+
}).catch(() => {
|
|
67
|
+
throw {
|
|
68
|
+
status: 404,
|
|
69
|
+
message: `Global secret not found for key: ${key}`
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
res.json({ success: true });
|
|
73
|
+
}, true));
|
|
74
|
+
exports.default = router;
|
package/dist/skills/index.js
CHANGED
|
@@ -17,6 +17,8 @@ const skill_files_1 = require("../utils/skill-files");
|
|
|
17
17
|
const resource_file_routes_1 = require("../utils/resource-file-routes");
|
|
18
18
|
const skill_approval_snapshot_1 = require("../utils/skill-approval-snapshot");
|
|
19
19
|
const resource_access_1 = require("../utils/resource-access");
|
|
20
|
+
const secrets_1 = require("../utils/secrets");
|
|
21
|
+
const secret_contract_validation_1 = require("../utils/secret-contract-validation");
|
|
20
22
|
const router = express_1.default.Router({ mergeParams: true });
|
|
21
23
|
const uploadTmpDir = path_1.default.join(os_1.default.tmpdir(), "teamcopilot-skill-uploads");
|
|
22
24
|
fs_1.default.mkdirSync(uploadTmpDir, { recursive: true });
|
|
@@ -77,6 +79,7 @@ router.get("/", (0, index_1.apiHandler)(async (req, res) => {
|
|
|
77
79
|
const slugs = (0, skill_1.listSkillSlugs)();
|
|
78
80
|
const skills = [];
|
|
79
81
|
const creatorIds = new Set();
|
|
82
|
+
const resolvedSecrets = await (0, secrets_1.listResolvedSecretsForUser)(req.userId);
|
|
80
83
|
const metadataBySlug = new Map();
|
|
81
84
|
for (const slug of slugs) {
|
|
82
85
|
const metadata = await (0, skill_1.getOrCreateSkillMetadataAndEnsurePermission)(slug);
|
|
@@ -122,6 +125,8 @@ router.get("/", (0, index_1.apiHandler)(async (req, res) => {
|
|
|
122
125
|
can_edit: accessSummary.can_edit,
|
|
123
126
|
permission_mode: accessSummary.permission_mode,
|
|
124
127
|
is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users,
|
|
128
|
+
required_secrets: manifest.required_secrets,
|
|
129
|
+
missing_required_secrets: (0, secrets_1.resolveSecretsFromResolvedMap)(resolvedSecrets, manifest.required_secrets).missingKeys,
|
|
125
130
|
});
|
|
126
131
|
}
|
|
127
132
|
res.json({ skills });
|
|
@@ -144,6 +149,7 @@ router.get("/:slug", (0, index_1.apiHandler)(async (req, res) => {
|
|
|
144
149
|
const approver = approvedByUserId ? (usersById.get(approvedByUserId) ?? null) : null;
|
|
145
150
|
const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("skill", slug, req.userId);
|
|
146
151
|
const permission = await (0, skill_permissions_1.getSkillAccessPermissionWithUsers)(slug);
|
|
152
|
+
const secretResolution = await (0, secrets_1.resolveSecretsForUser)(req.userId, manifest.required_secrets);
|
|
147
153
|
const permissionMode = (0, permission_common_1.assertCommonPermissionMode)(permission.permission_mode, "skill access");
|
|
148
154
|
const permissions = permissionMode === "everyone"
|
|
149
155
|
? { mode: "everyone" }
|
|
@@ -163,6 +169,8 @@ router.get("/:slug", (0, index_1.apiHandler)(async (req, res) => {
|
|
|
163
169
|
can_edit: accessSummary.can_edit,
|
|
164
170
|
permission_mode: accessSummary.permission_mode,
|
|
165
171
|
is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users,
|
|
172
|
+
required_secrets: manifest.required_secrets,
|
|
173
|
+
missing_required_secrets: secretResolution.missingKeys,
|
|
166
174
|
permissions,
|
|
167
175
|
allowed_users_resolved: permission.allowedUsers.map((row) => ({
|
|
168
176
|
user_id: row.user.id,
|
|
@@ -174,6 +182,40 @@ router.get("/:slug", (0, index_1.apiHandler)(async (req, res) => {
|
|
|
174
182
|
}
|
|
175
183
|
});
|
|
176
184
|
}, true));
|
|
185
|
+
router.get("/:slug/runtime-content", (0, index_1.apiHandler)(async (req, res) => {
|
|
186
|
+
const slug = req.params.slug;
|
|
187
|
+
await assertCanViewSkillFiles(slug, req.userId);
|
|
188
|
+
const { manifest } = await (0, skill_1.readSkillManifestAndEnsurePermissions)(slug);
|
|
189
|
+
const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("skill", slug, req.userId);
|
|
190
|
+
if (!accessSummary.is_approved) {
|
|
191
|
+
throw {
|
|
192
|
+
status: 403,
|
|
193
|
+
message: `Skill "${slug}" is not approved yet. Only approved skills can be read through getSkillContent.`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const skillContent = await (0, skill_files_1.readSkillFileContent)(slug, "SKILL.md");
|
|
197
|
+
if (skillContent.kind !== "text") {
|
|
198
|
+
throw {
|
|
199
|
+
status: 500,
|
|
200
|
+
message: "SKILL.md must be a text file"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
(0, secret_contract_validation_1.validateSkillSecretContract)(skillContent.content ?? "");
|
|
204
|
+
const secretResolution = await (0, secrets_1.resolveSecretsForUser)(req.userId, manifest.required_secrets);
|
|
205
|
+
if (secretResolution.missingKeys.length > 0) {
|
|
206
|
+
throw {
|
|
207
|
+
status: 400,
|
|
208
|
+
message: `I can't use skill "${slug}" because these required secrets are missing: ${secretResolution.missingKeys.join(", ")}. Ask the user to add these keys in TeamCopilot Profile Secrets before using this skill.`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
res.json({
|
|
212
|
+
skill: {
|
|
213
|
+
slug,
|
|
214
|
+
path: skillContent.path,
|
|
215
|
+
content: skillContent.content ?? "",
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}, true));
|
|
177
219
|
router.post("/:slug/approve", (0, index_1.apiHandler)(async (req, res) => {
|
|
178
220
|
const slug = req.params.slug;
|
|
179
221
|
const approvalResult = await (0, skill_approval_snapshot_1.approveSkillWithSnapshot)(slug, req.userId);
|
|
@@ -212,6 +254,7 @@ router.get("/:slug/approval-diff", (0, index_1.apiHandler)(async (req, res) => {
|
|
|
212
254
|
const previousSnapshot = await (0, skill_approval_snapshot_1.loadApprovedSkillSnapshotFromDb)(slug);
|
|
213
255
|
const currentSnapshot = (0, skill_approval_snapshot_1.collectCurrentSkillSnapshot)(slug);
|
|
214
256
|
const diff = (0, skill_approval_snapshot_1.buildSkillApprovalDiffResponse)(previousSnapshot, currentSnapshot);
|
|
257
|
+
res.locals.skipResponseSanitization = true;
|
|
215
258
|
res.json(diff);
|
|
216
259
|
}, true));
|
|
217
260
|
(0, resource_file_routes_1.registerResourceFileRoutes)({
|
|
@@ -230,7 +273,6 @@ router.get("/:slug/approval-diff", (0, index_1.apiHandler)(async (req, res) => {
|
|
|
230
273
|
uploadFileFromTempPath: skill_files_1.uploadSkillFileFromTempPath,
|
|
231
274
|
renamePath: skill_files_1.renameSkillPath,
|
|
232
275
|
deletePath: skill_files_1.deleteSkillPath,
|
|
233
|
-
skipResponseSanitizationForFileContentRead: false,
|
|
234
276
|
});
|
|
235
277
|
const updateSkillPermissionsHandler = (0, index_1.apiHandler)(async (req, res) => {
|
|
236
278
|
const slug = req.params.slug;
|
package/dist/users/index.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const express_1 = __importDefault(require("express"));
|
|
7
7
|
const client_1 = __importDefault(require("../prisma/client"));
|
|
8
8
|
const index_1 = require("../utils/index");
|
|
9
|
+
const secrets_1 = require("../utils/secrets");
|
|
9
10
|
const router = express_1.default.Router({ mergeParams: true });
|
|
10
11
|
router.get("/", (0, index_1.apiHandler)(async (_req, res) => {
|
|
11
12
|
const users = await client_1.default.users.findMany({
|
|
@@ -19,4 +20,101 @@ router.get("/", (0, index_1.apiHandler)(async (_req, res) => {
|
|
|
19
20
|
});
|
|
20
21
|
res.json({ users });
|
|
21
22
|
}, true));
|
|
23
|
+
router.get("/me/secrets", (0, index_1.apiHandler)(async (req, res) => {
|
|
24
|
+
const rows = await client_1.default.user_secrets.findMany({
|
|
25
|
+
where: {
|
|
26
|
+
user_id: req.userId,
|
|
27
|
+
},
|
|
28
|
+
orderBy: { key: "asc" }
|
|
29
|
+
});
|
|
30
|
+
const shouldMaskValues = req.opencode_session_id === undefined;
|
|
31
|
+
res.json({
|
|
32
|
+
secrets: rows.map((row) => (0, secrets_1.toSecretListItem)(row, shouldMaskValues))
|
|
33
|
+
});
|
|
34
|
+
}, true));
|
|
35
|
+
router.get("/me/resolved-secrets", (0, index_1.apiHandler)(async (req, res) => {
|
|
36
|
+
const resolvedSecretMap = await (0, secrets_1.listResolvedSecretsForUser)(req.userId);
|
|
37
|
+
res.json({
|
|
38
|
+
secret_keys: Object.keys(resolvedSecretMap),
|
|
39
|
+
total: Object.keys(resolvedSecretMap).length,
|
|
40
|
+
});
|
|
41
|
+
}, true));
|
|
42
|
+
router.post("/me/resolve-secrets", (0, index_1.apiHandler)(async (req, res) => {
|
|
43
|
+
if (req.opencode_session_id === undefined) {
|
|
44
|
+
throw {
|
|
45
|
+
status: 403,
|
|
46
|
+
message: "This endpoint requires an opencode session token"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const rawKeys = Array.isArray(req.body?.keys) ? req.body.keys : [];
|
|
50
|
+
const requestedKeys = rawKeys
|
|
51
|
+
.filter((key) => typeof key === "string")
|
|
52
|
+
.map((key) => (0, secrets_1.assertSecretKey)(key));
|
|
53
|
+
if (requestedKeys.length === 0) {
|
|
54
|
+
throw {
|
|
55
|
+
status: 400,
|
|
56
|
+
message: "keys is required"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const resolution = await (0, secrets_1.resolveSecretsForUser)(req.userId, requestedKeys);
|
|
60
|
+
if (resolution.missingKeys.length > 0) {
|
|
61
|
+
throw {
|
|
62
|
+
status: 400,
|
|
63
|
+
message: `This command references missing secrets: ${resolution.missingKeys.join(", ")}. Ask the user to add these keys in TeamCopilot Profile Secrets before retrying.`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
res.json({
|
|
67
|
+
secret_map: resolution.secretMap,
|
|
68
|
+
});
|
|
69
|
+
}, true));
|
|
70
|
+
router.put("/me/secrets/:key", (0, index_1.apiHandler)(async (req, res) => {
|
|
71
|
+
const key = (0, secrets_1.assertSecretKey)(req.params.key);
|
|
72
|
+
const value = typeof req.body?.value === "string" ? req.body.value : "";
|
|
73
|
+
if (value.length === 0) {
|
|
74
|
+
throw {
|
|
75
|
+
status: 400,
|
|
76
|
+
message: "value is required"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const now = BigInt(Date.now());
|
|
80
|
+
const row = await client_1.default.user_secrets.upsert({
|
|
81
|
+
where: {
|
|
82
|
+
user_id_key: {
|
|
83
|
+
user_id: req.userId,
|
|
84
|
+
key,
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
create: {
|
|
88
|
+
user_id: req.userId,
|
|
89
|
+
key,
|
|
90
|
+
value,
|
|
91
|
+
created_at: now,
|
|
92
|
+
updated_at: now,
|
|
93
|
+
},
|
|
94
|
+
update: {
|
|
95
|
+
value,
|
|
96
|
+
updated_at: now,
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
res.json({
|
|
100
|
+
secret: (0, secrets_1.toSecretListItem)(row, req.opencode_session_id === undefined)
|
|
101
|
+
});
|
|
102
|
+
}, true));
|
|
103
|
+
router.delete("/me/secrets/:key", (0, index_1.apiHandler)(async (req, res) => {
|
|
104
|
+
const key = (0, secrets_1.assertSecretKey)(req.params.key);
|
|
105
|
+
await client_1.default.user_secrets.delete({
|
|
106
|
+
where: {
|
|
107
|
+
user_id_key: {
|
|
108
|
+
user_id: req.userId,
|
|
109
|
+
key,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}).catch(() => {
|
|
113
|
+
throw {
|
|
114
|
+
status: 404,
|
|
115
|
+
message: `Secret not found for key: ${key}`
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
res.json({ success: true });
|
|
119
|
+
}, true));
|
|
22
120
|
exports.default = router;
|
package/dist/utils/redact.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.sanitizeStringContent = sanitizeStringContent;
|
|
4
4
|
exports.sanitizeForClient = sanitizeForClient;
|
|
5
5
|
const SENSITIVE_KEY_PATTERN = /(token|secret|password|passwd|api[_-]?key|auth|credential)/i;
|
|
6
|
+
const SECRET_PLACEHOLDER_PATTERN = /\{\{SECRET:[A-Z][A-Z0-9_]*\}\}/g;
|
|
6
7
|
function maskValue(value) {
|
|
7
8
|
if (value.startsWith("***")) {
|
|
8
9
|
return value;
|
|
@@ -14,14 +15,40 @@ function maskValue(value) {
|
|
|
14
15
|
function isLikelySensitiveKey(key) {
|
|
15
16
|
return SENSITIVE_KEY_PATTERN.test(key);
|
|
16
17
|
}
|
|
18
|
+
function isProtectedSecretPlaceholderToken(value) {
|
|
19
|
+
return /__TCPLH_\d+__/.test(value.trim());
|
|
20
|
+
}
|
|
21
|
+
function isSecretPlaceholderValue(value) {
|
|
22
|
+
return value.trim().match(/^\{\{SECRET:[A-Z][A-Z0-9_]*\}\}$/) !== null;
|
|
23
|
+
}
|
|
24
|
+
function protectSecretPlaceholders(input) {
|
|
25
|
+
const placeholders = [];
|
|
26
|
+
const protectedText = input.replace(SECRET_PLACEHOLDER_PATTERN, (match) => {
|
|
27
|
+
const index = placeholders.push(match) - 1;
|
|
28
|
+
return `__TCPLH_${index}__`;
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
protectedText,
|
|
32
|
+
restore: (value) => value.replace(/__TCPLH_(\d+)__/g, (_full, rawIndex) => {
|
|
33
|
+
const index = Number(rawIndex);
|
|
34
|
+
return placeholders[index] ?? _full;
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
17
38
|
function sanitizeStringContent(input) {
|
|
18
|
-
|
|
39
|
+
const { protectedText, restore } = protectSecretPlaceholders(input);
|
|
40
|
+
let text = protectedText;
|
|
19
41
|
// Redact credentials embedded in URLs such as:
|
|
20
42
|
// https://user:password@host/path
|
|
21
43
|
text = text.replace(/\b(https?:\/\/)([^\/\s"'@:]+):([^\/\s"'@]+)@/gi, (_full, scheme, username, password) => `${scheme}${username}:${maskValue(password)}@`);
|
|
22
44
|
// Redact Authorization header bearer tokens before generic key/value masking so
|
|
23
45
|
// "Authorization: Bearer <token>" doesn't get partially masked as "***rer".
|
|
24
|
-
text = text.replace(/(\bAuthorization\s*[:=]\s*)(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})/gi, (_full, prefix, bearer, token) =>
|
|
46
|
+
text = text.replace(/(\bAuthorization\s*[:=]\s*)(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})/gi, (_full, prefix, bearer, token) => {
|
|
47
|
+
if (isProtectedSecretPlaceholderToken(token)) {
|
|
48
|
+
return `${prefix}${bearer}${token}`;
|
|
49
|
+
}
|
|
50
|
+
return `${prefix}${bearer}${maskValue(token)}`;
|
|
51
|
+
});
|
|
25
52
|
// Redact sensitive markdown list/label items such as:
|
|
26
53
|
// - **password**: value
|
|
27
54
|
// - **password** = value
|
|
@@ -34,6 +61,9 @@ function sanitizeStringContent(input) {
|
|
|
34
61
|
if (!rawValue) {
|
|
35
62
|
return full;
|
|
36
63
|
}
|
|
64
|
+
if (isProtectedSecretPlaceholderToken(rawValue)) {
|
|
65
|
+
return full;
|
|
66
|
+
}
|
|
37
67
|
const masked = maskValue(rawValue);
|
|
38
68
|
if (doubleQuoted !== undefined) {
|
|
39
69
|
return `${lineLead}${prefix}${openMarker}${key}${closeMarker}${separator}"${masked}"`;
|
|
@@ -54,6 +84,9 @@ function sanitizeStringContent(input) {
|
|
|
54
84
|
if (!rawValue) {
|
|
55
85
|
return full;
|
|
56
86
|
}
|
|
87
|
+
if (isProtectedSecretPlaceholderToken(rawValue)) {
|
|
88
|
+
return full;
|
|
89
|
+
}
|
|
57
90
|
const masked = maskValue(rawValue);
|
|
58
91
|
if (doubleQuoted !== undefined) {
|
|
59
92
|
return `${lead}${assignmentPrefix}"${masked}"`;
|
|
@@ -73,6 +106,9 @@ function sanitizeStringContent(input) {
|
|
|
73
106
|
if (!rawValue) {
|
|
74
107
|
return full;
|
|
75
108
|
}
|
|
109
|
+
if (isProtectedSecretPlaceholderToken(rawValue)) {
|
|
110
|
+
return full;
|
|
111
|
+
}
|
|
76
112
|
if (key.toLowerCase() === "authorization" && /^bearer$/i.test(rawValue)) {
|
|
77
113
|
return full;
|
|
78
114
|
}
|
|
@@ -80,6 +116,9 @@ function sanitizeStringContent(input) {
|
|
|
80
116
|
if (key.toLowerCase() === "authorization" && authorizationMatch) {
|
|
81
117
|
const bearerPrefix = authorizationMatch[1];
|
|
82
118
|
const bearerToken = authorizationMatch[2];
|
|
119
|
+
if (isProtectedSecretPlaceholderToken(bearerToken)) {
|
|
120
|
+
return full;
|
|
121
|
+
}
|
|
83
122
|
const maskedBearer = `${bearerPrefix}${maskValue(bearerToken)}`;
|
|
84
123
|
if (doubleQuoted !== undefined) {
|
|
85
124
|
return `${lead}${assignmentPrefix}"${maskedBearer}"`;
|
|
@@ -99,15 +138,23 @@ function sanitizeStringContent(input) {
|
|
|
99
138
|
return `${lead}${assignmentPrefix}${masked}`;
|
|
100
139
|
});
|
|
101
140
|
// Redact common bearer and provider token forms.
|
|
102
|
-
text = text.replace(/\b(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})\b/gi, (_full, prefix, token) =>
|
|
141
|
+
text = text.replace(/\b(Bearer\s+)([A-Za-z0-9._~+/=-]{8,})\b/gi, (_full, prefix, token) => {
|
|
142
|
+
if (isProtectedSecretPlaceholderToken(token)) {
|
|
143
|
+
return `${prefix}${token}`;
|
|
144
|
+
}
|
|
145
|
+
return `${prefix}${maskValue(token)}`;
|
|
146
|
+
});
|
|
103
147
|
text = text.replace(/\b(sk-[A-Za-z0-9_-]{8,})\b/g, (token) => maskValue(token));
|
|
104
148
|
text = text.replace(/\b(ghp_[A-Za-z0-9]{8,})\b/g, (token) => maskValue(token));
|
|
105
|
-
return text;
|
|
149
|
+
return restore(text);
|
|
106
150
|
}
|
|
107
151
|
function sanitizeObjectRecord(input) {
|
|
108
152
|
const output = {};
|
|
109
153
|
for (const [key, value] of Object.entries(input)) {
|
|
110
|
-
if (typeof value === "string"
|
|
154
|
+
if (typeof value === "string"
|
|
155
|
+
&& isLikelySensitiveKey(key)
|
|
156
|
+
&& !isProtectedSecretPlaceholderToken(value)
|
|
157
|
+
&& !isSecretPlaceholderValue(value)) {
|
|
111
158
|
output[key] = maskValue(value);
|
|
112
159
|
continue;
|
|
113
160
|
}
|
|
@@ -7,7 +7,7 @@ exports.registerResourceFileRoutes = registerResourceFileRoutes;
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const index_1 = require("./index");
|
|
9
9
|
function registerResourceFileRoutes(options) {
|
|
10
|
-
const { router, uploadMiddleware, ensureResourceExists, assertCanView, getEditorAccess, assertCanEdit, listDirectory, readFileContent, saveFileContent, createFileOrFolder, uploadFileFromTempPath, renamePath, deletePath,
|
|
10
|
+
const { router, uploadMiddleware, ensureResourceExists, assertCanView, getEditorAccess, assertCanEdit, listDirectory, readFileContent, saveFileContent, createFileOrFolder, uploadFileFromTempPath, renamePath, deletePath, } = options;
|
|
11
11
|
router.get("/:slug/files/access", (0, index_1.apiHandler)(async (req, res) => {
|
|
12
12
|
const slug = req.params.slug;
|
|
13
13
|
const authReq = req;
|
|
@@ -30,9 +30,7 @@ function registerResourceFileRoutes(options) {
|
|
|
30
30
|
await ensureResourceExists(slug);
|
|
31
31
|
const rawPath = typeof req.query.path === "string" ? req.query.path : undefined;
|
|
32
32
|
const content = readFileContent(slug, rawPath);
|
|
33
|
-
|
|
34
|
-
res.locals.skipResponseSanitization = true;
|
|
35
|
-
}
|
|
33
|
+
res.locals.skipResponseSanitization = true;
|
|
36
34
|
res.json(content);
|
|
37
35
|
}, true));
|
|
38
36
|
router.put("/:slug/files/content", (0, index_1.apiHandler)(async (req, res) => {
|
|
@@ -10,7 +10,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const redact_1 = require("./redact");
|
|
12
12
|
function createResourceFileManager(options) {
|
|
13
|
-
const { getResourcePath, resourceLabel, editorLabel } = options;
|
|
13
|
+
const { getResourcePath, resourceLabel, editorLabel, validateBeforeSave } = options;
|
|
14
14
|
const rootLabel = `${resourceLabel} root`;
|
|
15
15
|
const symlinkError = `${editorLabel} editor does not support symlinks`;
|
|
16
16
|
function getResolvedRootPaths(slug) {
|
|
@@ -345,10 +345,18 @@ function createResourceFileManager(options) {
|
|
|
345
345
|
}
|
|
346
346
|
const name = path_1.default.basename(relativePath);
|
|
347
347
|
const currentRawContent = currentBytes.toString("utf-8");
|
|
348
|
-
const wasServedRedacted = name === ".env"
|
|
348
|
+
const wasServedRedacted = name === ".env";
|
|
349
349
|
const nextContent = wasServedRedacted
|
|
350
350
|
? mergeRedactedEditorContent(currentRawContent, request.content)
|
|
351
351
|
: request.content;
|
|
352
|
+
if (validateBeforeSave) {
|
|
353
|
+
validateBeforeSave({
|
|
354
|
+
slug,
|
|
355
|
+
resourcePath: getResourcePath(slug),
|
|
356
|
+
relativePath,
|
|
357
|
+
nextContent,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
352
360
|
fs_1.default.writeFileSync(absolutePath, nextContent, "utf-8");
|
|
353
361
|
const nextBytes = fs_1.default.readFileSync(absolutePath);
|
|
354
362
|
const nextStat = fs_1.default.statSync(absolutePath);
|