artifacty 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +52 -0
- package/CLAUDE.md +64 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/docs/artifact-schema-v1.md +96 -0
- package/docs/assets/artifacty.png +0 -0
- package/docs/integrations.md +196 -0
- package/docs/release-checklist.md +30 -0
- package/package.json +55 -0
- package/scripts/smoke.sh +104 -0
- package/src/cli.js +348 -0
- package/src/client/editor.js +260 -0
- package/src/lib/backup.js +75 -0
- package/src/lib/check.js +124 -0
- package/src/lib/converters.js +586 -0
- package/src/lib/diff.js +69 -0
- package/src/lib/editor-assets.js +59 -0
- package/src/lib/i18n.js +175 -0
- package/src/lib/installer.js +187 -0
- package/src/lib/render.js +1181 -0
- package/src/lib/security.js +114 -0
- package/src/lib/server-state.js +53 -0
- package/src/lib/service.js +105 -0
- package/src/lib/storage.js +846 -0
- package/src/mcp-server.js +495 -0
- package/src/server.js +576 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const TOKEN_HEADER = "x-artifacty-token";
|
|
2
|
+
|
|
3
|
+
const SECRET_PATTERNS = [
|
|
4
|
+
{ type: "anthropic-api-key", pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
5
|
+
{ type: "openai-api-key", pattern: /\bsk-(?!ant-)[A-Za-z0-9_-]{20,}\b/g },
|
|
6
|
+
{ type: "github-token", pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
|
|
7
|
+
{ type: "aws-access-key", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
8
|
+
{ type: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/g },
|
|
9
|
+
{ type: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
|
|
10
|
+
{ type: "private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g }
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function securityConfig(options = {}) {
|
|
14
|
+
return {
|
|
15
|
+
apiToken: options.apiToken || process.env.ARTIFACTY_API_TOKEN || "",
|
|
16
|
+
shareMode: options.shareMode || process.env.ARTIFACTY_SHARE_MODE || "local",
|
|
17
|
+
allowSecrets: Boolean(options.allowSecrets || process.env.ARTIFACTY_ALLOW_SECRETS === "true")
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateServerExposure({ host, config = securityConfig() }) {
|
|
22
|
+
const local = isLoopbackHost(host);
|
|
23
|
+
if (local) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!["lan", "team"].includes(config.shareMode)) {
|
|
27
|
+
throw new Error("Non-local host requires ARTIFACTY_SHARE_MODE=lan or team");
|
|
28
|
+
}
|
|
29
|
+
if (!config.apiToken) {
|
|
30
|
+
throw new Error("Non-local host requires ARTIFACTY_API_TOKEN");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function requireToken({ request, url, body = {}, config = securityConfig() }) {
|
|
35
|
+
if (!config.apiToken) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const provided = extractToken({ request, url, body });
|
|
39
|
+
if (provided !== config.apiToken) {
|
|
40
|
+
throw Object.assign(new Error("Artifacty API token required"), {
|
|
41
|
+
code: "AUTH_REQUIRED",
|
|
42
|
+
statusCode: 401
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function scanForSecrets(content) {
|
|
48
|
+
const text = String(content ?? "");
|
|
49
|
+
const findings = [];
|
|
50
|
+
for (const { type, pattern } of SECRET_PATTERNS) {
|
|
51
|
+
pattern.lastIndex = 0;
|
|
52
|
+
let match = pattern.exec(text);
|
|
53
|
+
while (match) {
|
|
54
|
+
findings.push({
|
|
55
|
+
type,
|
|
56
|
+
index: match.index,
|
|
57
|
+
preview: redactSecret(match[0])
|
|
58
|
+
});
|
|
59
|
+
match = pattern.exec(text);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return findings;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function assertNoSecrets(input = {}, options = {}) {
|
|
66
|
+
const findings = scanForSecrets(input.content);
|
|
67
|
+
const allowSecrets =
|
|
68
|
+
options.allowSecrets ||
|
|
69
|
+
input.allowSecrets === true ||
|
|
70
|
+
input.metadata?.secretScan?.allowSecrets === true;
|
|
71
|
+
|
|
72
|
+
if (findings.length > 0 && !allowSecrets) {
|
|
73
|
+
throw Object.assign(new Error(`Secret scan blocked artifact content: ${findings.map((finding) => finding.type).join(", ")}`), {
|
|
74
|
+
code: "SECRET_DETECTED",
|
|
75
|
+
statusCode: 400,
|
|
76
|
+
findings
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
status: findings.length > 0 ? "allowed" : "passed",
|
|
82
|
+
findingCount: findings.length,
|
|
83
|
+
findings: findings.map((finding) => ({
|
|
84
|
+
type: finding.type,
|
|
85
|
+
preview: finding.preview
|
|
86
|
+
}))
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function isLoopbackHost(host) {
|
|
91
|
+
const normalized = String(host || "").toLowerCase();
|
|
92
|
+
return normalized === "localhost" ||
|
|
93
|
+
normalized === "127.0.0.1" ||
|
|
94
|
+
normalized === "::1" ||
|
|
95
|
+
normalized === "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractToken({ request, url, body }) {
|
|
99
|
+
const authorization = request.headers.authorization || "";
|
|
100
|
+
if (authorization.toLowerCase().startsWith("bearer ")) {
|
|
101
|
+
return authorization.slice(7).trim();
|
|
102
|
+
}
|
|
103
|
+
return request.headers[TOKEN_HEADER] ||
|
|
104
|
+
url.searchParams.get("token") ||
|
|
105
|
+
body._token ||
|
|
106
|
+
"";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function redactSecret(value) {
|
|
110
|
+
if (value.length <= 10) {
|
|
111
|
+
return "***";
|
|
112
|
+
}
|
|
113
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PUBLIC_URL = "http://127.0.0.1:8787";
|
|
5
|
+
const SERVER_STATE_FILE = "server.json";
|
|
6
|
+
|
|
7
|
+
export async function writeServerState(store, state) {
|
|
8
|
+
const next = {
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
url: normalizePublicBaseUrl(state.url),
|
|
11
|
+
host: state.host,
|
|
12
|
+
port: state.port,
|
|
13
|
+
requestedPort: state.requestedPort,
|
|
14
|
+
portFallback: Boolean(state.portFallback),
|
|
15
|
+
pid: process.pid,
|
|
16
|
+
updatedAt: new Date().toISOString()
|
|
17
|
+
};
|
|
18
|
+
await mkdir(store.home, { recursive: true });
|
|
19
|
+
await writeFile(serverStatePath(store), `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
20
|
+
return next;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function readServerState(store) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(await readFile(serverStatePath(store), "utf8"));
|
|
26
|
+
if (typeof parsed.url !== "string" || !/^https?:\/\//.test(parsed.url)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
...parsed,
|
|
31
|
+
url: normalizePublicBaseUrl(parsed.url)
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function resolvePublicBaseUrl(store, options = {}) {
|
|
39
|
+
const configured = options.url || process.env.ARTIFACTY_URL;
|
|
40
|
+
if (configured) {
|
|
41
|
+
return normalizePublicBaseUrl(configured);
|
|
42
|
+
}
|
|
43
|
+
const state = await readServerState(store);
|
|
44
|
+
return state?.url || DEFAULT_PUBLIC_URL;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function serverStatePath(store) {
|
|
48
|
+
return path.join(store.home, SERVER_STATE_FILE);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePublicBaseUrl(value) {
|
|
52
|
+
return String(value || DEFAULT_PUBLIC_URL).replace(/\/+$/, "");
|
|
53
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LABEL = "com.artifacty.server";
|
|
7
|
+
|
|
8
|
+
export async function serviceCommand(action, options = {}) {
|
|
9
|
+
const plistPath = path.resolve(options.plistPath || path.join(homedir(), "Library", "LaunchAgents", `${DEFAULT_LABEL}.plist`));
|
|
10
|
+
const plist = createLaunchAgentPlist(options);
|
|
11
|
+
|
|
12
|
+
if (action === "plist") {
|
|
13
|
+
return { action, path: plistPath, content: plist, dryRun: true };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (action === "install") {
|
|
17
|
+
const existing = await readFile(plistPath, "utf8").catch(() => "");
|
|
18
|
+
const changed = existing !== plist;
|
|
19
|
+
if (!options.dryRun) {
|
|
20
|
+
await mkdir(path.dirname(plistPath), { recursive: true });
|
|
21
|
+
await writeFile(plistPath, plist, "utf8");
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
action,
|
|
25
|
+
path: plistPath,
|
|
26
|
+
changed,
|
|
27
|
+
dryRun: Boolean(options.dryRun),
|
|
28
|
+
content: options.dryRun ? plist : undefined,
|
|
29
|
+
nextSteps: [
|
|
30
|
+
`launchctl load ${plistPath}`,
|
|
31
|
+
`launchctl unload ${plistPath}`
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (action === "uninstall") {
|
|
37
|
+
const existed = existsSync(plistPath);
|
|
38
|
+
if (!options.dryRun) {
|
|
39
|
+
await rm(plistPath, { force: true });
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
action,
|
|
43
|
+
path: plistPath,
|
|
44
|
+
changed: existed,
|
|
45
|
+
dryRun: Boolean(options.dryRun)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error("service requires action: plist, install, or uninstall");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createLaunchAgentPlist(options = {}) {
|
|
53
|
+
const projectDir = path.resolve(options.projectDir || process.cwd());
|
|
54
|
+
const nodePath = process.execPath;
|
|
55
|
+
const serverPath = path.resolve(options.serverPath || path.join(projectDir, "src", "server.js"));
|
|
56
|
+
const host = options.host || process.env.ARTIFACTY_HOST || "127.0.0.1";
|
|
57
|
+
const port = options.port || process.env.ARTIFACTY_PORT;
|
|
58
|
+
const home = path.resolve(options.home || process.env.ARTIFACTY_HOME || path.join(homedir(), ".artifacty"));
|
|
59
|
+
const logDir = path.join(home, "logs");
|
|
60
|
+
const portArguments = port
|
|
61
|
+
? ` <string>--port</string>
|
|
62
|
+
<string>${escapeXml(port)}</string>
|
|
63
|
+
`
|
|
64
|
+
: "";
|
|
65
|
+
|
|
66
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
67
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
68
|
+
<plist version="1.0">
|
|
69
|
+
<dict>
|
|
70
|
+
<key>Label</key>
|
|
71
|
+
<string>${DEFAULT_LABEL}</string>
|
|
72
|
+
<key>ProgramArguments</key>
|
|
73
|
+
<array>
|
|
74
|
+
<string>${escapeXml(nodePath)}</string>
|
|
75
|
+
<string>${escapeXml(serverPath)}</string>
|
|
76
|
+
<string>--host</string>
|
|
77
|
+
<string>${escapeXml(host)}</string>
|
|
78
|
+
${portArguments} <string>--home</string>
|
|
79
|
+
<string>${escapeXml(home)}</string>
|
|
80
|
+
</array>
|
|
81
|
+
<key>EnvironmentVariables</key>
|
|
82
|
+
<dict>
|
|
83
|
+
<key>ARTIFACTY_HOME</key>
|
|
84
|
+
<string>${escapeXml(home)}</string>
|
|
85
|
+
</dict>
|
|
86
|
+
<key>RunAtLoad</key>
|
|
87
|
+
<true/>
|
|
88
|
+
<key>KeepAlive</key>
|
|
89
|
+
<true/>
|
|
90
|
+
<key>StandardOutPath</key>
|
|
91
|
+
<string>${escapeXml(path.join(logDir, "server.out.log"))}</string>
|
|
92
|
+
<key>StandardErrorPath</key>
|
|
93
|
+
<string>${escapeXml(path.join(logDir, "server.err.log"))}</string>
|
|
94
|
+
</dict>
|
|
95
|
+
</plist>
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function escapeXml(value) {
|
|
100
|
+
return String(value)
|
|
101
|
+
.replaceAll("&", "&")
|
|
102
|
+
.replaceAll("<", "<")
|
|
103
|
+
.replaceAll(">", ">")
|
|
104
|
+
.replaceAll('"', """);
|
|
105
|
+
}
|