@sogni-ai/sogni-creative-agent-skill 2.0.0 → 2.0.1
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/SKILL.md +1 -1
- package/desktop-extension/manifest.json +1 -1
- package/desktop-extension/server/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/ssrf-guard.mjs +157 -0
- package/version.mjs +1 -1
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sogni-creative-agent-skill
|
|
3
|
-
version: "2.0.
|
|
3
|
+
version: "2.0.1"
|
|
4
4
|
description: Sogni Creative Agent Skill: Creative AI superpowers for all AI agent runtimes. Generates images and videos using Sogni AI's decentralized GPU network. Supports personas (named people with saved reference photos and voice clips), persistent memories (user preferences across sessions), custom personality, style transfer, angle synthesis, and multi-step creative workflows. Ask the agent to "draw", "generate", "create an image", "make a video/animate", "apply a style", or "generate me as a superhero".
|
|
5
5
|
homepage: https://sogni.ai
|
|
6
6
|
metadata:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "sogni-creative-agent-skill",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.1",
|
|
5
5
|
"display_name": "Sogni Creative Agent Skill — Image & Video Generation",
|
|
6
6
|
"description": "Creative AI superpowers for all AI agent runtimes, powered by Sogni AI's decentralized GPU network",
|
|
7
7
|
"long_description": "Sogni Creative Agent Skill gives any AI agent (Claude Code/Desktop, OpenClaw, Hermes Agent, Manus AI, and more) image generation, image editing, video generation, photobooth face transfer, persona memory, and other creative-media tools. Uses Spark tokens — claim 50 free daily at https://app.sogni.ai/",
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sogni-ai/sogni-creative-agent-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Sogni Creative Agent Skill: creative AI superpowers for all AI agent runtimes (Claude Code, Claude Desktop, OpenClaw, Hermes Agent, Manus AI, and more). Powered by Sogni AI's decentralized GPU network.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "sogni-agent.mjs",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"openclaw-plugin.mjs",
|
|
54
54
|
"openclaw.plugin.json",
|
|
55
55
|
"env.mjs",
|
|
56
|
+
"ssrf-guard.mjs",
|
|
56
57
|
"mcp-server.mjs",
|
|
57
58
|
"sogni-agent.mjs",
|
|
58
59
|
"Support/Claude/claude_desktop_config.json",
|
package/ssrf-guard.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF Guard (ESM) — server-side URL validator to prevent Server-Side Request Forgery.
|
|
3
|
+
*
|
|
4
|
+
* Blocks non-http(s) schemes, credentials in URLs, loopback / RFC1918 / link-local /
|
|
5
|
+
* CGNAT / multicast / reserved IP ranges, cloud metadata endpoints, and DNS rebinding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import dns from 'node:dns';
|
|
9
|
+
import net from 'node:net';
|
|
10
|
+
|
|
11
|
+
const dnsPromises = dns.promises;
|
|
12
|
+
|
|
13
|
+
// IPv4 CIDR blocks to reject. [networkAddress, prefixLength]
|
|
14
|
+
const BLOCKED_V4_CIDRS = [
|
|
15
|
+
['0.0.0.0', 8], // "this network"
|
|
16
|
+
['10.0.0.0', 8], // RFC1918 private
|
|
17
|
+
['100.64.0.0', 10], // CGNAT (RFC6598)
|
|
18
|
+
['127.0.0.0', 8], // loopback
|
|
19
|
+
['169.254.0.0', 16], // link-local — includes 169.254.169.254 (AWS/GCP/Azure IMDS)
|
|
20
|
+
['172.16.0.0', 12], // RFC1918 private
|
|
21
|
+
['192.0.0.0', 24], // IETF protocol assignments
|
|
22
|
+
['192.0.2.0', 24], // TEST-NET-1
|
|
23
|
+
['192.168.0.0', 16], // RFC1918 private
|
|
24
|
+
['198.18.0.0', 15], // benchmarking
|
|
25
|
+
['198.51.100.0', 24], // TEST-NET-2
|
|
26
|
+
['203.0.113.0', 24], // TEST-NET-3
|
|
27
|
+
['224.0.0.0', 4], // multicast
|
|
28
|
+
['240.0.0.0', 4], // reserved (Class E)
|
|
29
|
+
['255.255.255.255', 32], // limited broadcast
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function ipv4ToInt(ip) {
|
|
33
|
+
const parts = ip.split('.');
|
|
34
|
+
if (parts.length !== 4) return null;
|
|
35
|
+
let n = 0;
|
|
36
|
+
for (const p of parts) {
|
|
37
|
+
if (!/^\d+$/.test(p)) return null;
|
|
38
|
+
const octet = Number(p);
|
|
39
|
+
if (octet < 0 || octet > 255) return null;
|
|
40
|
+
n = n * 256 + octet;
|
|
41
|
+
}
|
|
42
|
+
return n >>> 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isBlockedV4(ip) {
|
|
46
|
+
const ipInt = ipv4ToInt(ip);
|
|
47
|
+
if (ipInt === null) return true; // unparseable -> block
|
|
48
|
+
for (const [base, bits] of BLOCKED_V4_CIDRS) {
|
|
49
|
+
const baseInt = ipv4ToInt(base);
|
|
50
|
+
if (baseInt === null) continue;
|
|
51
|
+
const mask =
|
|
52
|
+
bits === 0 ? 0 :
|
|
53
|
+
bits === 32 ? 0xffffffff :
|
|
54
|
+
(~0 << (32 - bits)) >>> 0;
|
|
55
|
+
if ((ipInt & mask) === (baseInt & mask)) return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isBlockedV6(ip) {
|
|
61
|
+
const lc = ip.toLowerCase();
|
|
62
|
+
if (lc === '::1' || lc === '::') return true; // loopback / unspecified
|
|
63
|
+
if (lc.startsWith('::ffff:')) { // IPv4-mapped -> check the v4
|
|
64
|
+
const v4 = lc.slice(7);
|
|
65
|
+
if (net.isIPv4(v4)) return isBlockedV4(v4);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (/^f[cd][0-9a-f]{2}:/.test(lc)) return true; // fc00::/7 unique-local
|
|
69
|
+
if (/^fe[89ab][0-9a-f]:/.test(lc)) return true; // fe80::/10 link-local
|
|
70
|
+
if (/^ff[0-9a-f]{2}:/.test(lc)) return true; // ff00::/8 multicast
|
|
71
|
+
if (lc.startsWith('2001:db8:')) return true; // documentation
|
|
72
|
+
if (lc.startsWith('64:ff9b:')) return true; // NAT64
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isBlockedIp(ip) {
|
|
77
|
+
const family = net.isIP(ip);
|
|
78
|
+
if (family === 4) return isBlockedV4(ip);
|
|
79
|
+
if (family === 6) return isBlockedV6(ip);
|
|
80
|
+
return true; // unparseable -> block
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validates a URL is safe to fetch from a server.
|
|
85
|
+
* Throws Error on unsafe URLs. Returns the parsed URL on success.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} input
|
|
88
|
+
* @param {object} [options]
|
|
89
|
+
* @param {string[]} [options.allowedProtocols=['http:','https:']]
|
|
90
|
+
* @param {string[]} [options.allowedHosts] - optional exact-match hostname allowlist (lowercase)
|
|
91
|
+
* @returns {Promise<URL>}
|
|
92
|
+
*/
|
|
93
|
+
export async function assertSafeUrl(input, options = {}) {
|
|
94
|
+
const allowedProtocols = options.allowedProtocols || ['http:', 'https:'];
|
|
95
|
+
const allowedHosts = options.allowedHosts;
|
|
96
|
+
|
|
97
|
+
if (typeof input !== 'string' || input.length === 0) {
|
|
98
|
+
throw new Error('SSRF guard: URL must be a non-empty string');
|
|
99
|
+
}
|
|
100
|
+
if (input.length > 2048) {
|
|
101
|
+
throw new Error('SSRF guard: URL too long');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = new URL(input);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error('SSRF guard: invalid URL');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
112
|
+
throw new Error(`SSRF guard: protocol ${parsed.protocol} not allowed`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (parsed.username || parsed.password) {
|
|
116
|
+
throw new Error('SSRF guard: URL must not contain credentials');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Strip brackets from IPv6 literals so net.isIP and dns.lookup see the raw addr.
|
|
120
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
|
|
121
|
+
if (!hostname) {
|
|
122
|
+
throw new Error('SSRF guard: missing hostname');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (allowedHosts && !allowedHosts.includes(hostname.toLowerCase())) {
|
|
126
|
+
throw new Error(`SSRF guard: host ${hostname} not in allowlist`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If hostname is already an IP literal, validate directly.
|
|
130
|
+
if (net.isIP(hostname)) {
|
|
131
|
+
if (isBlockedIp(hostname)) {
|
|
132
|
+
throw new Error(`SSRF guard: blocked IP ${hostname}`);
|
|
133
|
+
}
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Resolve all A/AAAA records and validate every one (defense vs DNS rebinding
|
|
138
|
+
// and split-horizon DNS that returns mixed public/private addresses).
|
|
139
|
+
let addrs;
|
|
140
|
+
try {
|
|
141
|
+
addrs = await dnsPromises.lookup(hostname, { all: true, verbatim: true });
|
|
142
|
+
} catch {
|
|
143
|
+
throw new Error(`SSRF guard: DNS lookup failed for ${hostname}`);
|
|
144
|
+
}
|
|
145
|
+
if (!addrs || addrs.length === 0) {
|
|
146
|
+
throw new Error(`SSRF guard: no DNS records for ${hostname}`);
|
|
147
|
+
}
|
|
148
|
+
for (const a of addrs) {
|
|
149
|
+
if (isBlockedIp(a.address)) {
|
|
150
|
+
throw new Error(`SSRF guard: ${hostname} resolves to blocked IP ${a.address}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { isBlockedIp };
|
package/version.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '2.0.
|
|
1
|
+
export const PACKAGE_VERSION = '2.0.1';
|