-
Notifications
You must be signed in to change notification settings - Fork 662
231 lines (215 loc) · 12.4 KB
/
Copy pathverify-fork.yml
File metadata and controls
231 lines (215 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
name: Validate Fork PRs
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
jobs:
verify:
name: Verify tokens (fork-safe)
runs-on: ubuntu-latest
env:
# Mirror of _config/ui/app/_utils/svgSafety.ts and .github/scripts/forbidden-svg-pattern.mjs — keep all three in sync.
SVG_FORBIDDEN_PATTERN: 'data:image/(png|jpe?g|gif|webp|bmp|avif)|data:[^,]{0,256};[[:space:]]*base64|href[[:space:]]*=[[:space:]]*["'']?(https?:|//)|<script|javascript:|[^a-zA-Z0-9_-]on[a-z]+[[:space:]]*='
steps:
# Safe by design: the token is contents:read only and no code
# from this checkout is ever executed, only grepped. Never add
# npm install or run scripts from this checkout.
- name: Checkout PR code
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
allow-unsafe-pr-checkout: true
- name: Check for forbidden changes
if: github.event.pull_request.head.repo.full_name != github.repository
env:
BASE_REF: ${{ github.base_ref }}
run: |
git fetch origin "$BASE_REF"
CHANGED=$(git diff --name-only "origin/$BASE_REF"...HEAD -- '_config/')
if [ -n "$CHANGED" ]; then
echo "Error: Changes to _config/ directory are not allowed in fork PRs"
echo "Modified files:"
echo "$CHANGED"
exit 1
fi
echo "Ok: no forbidden changes detected"
- name: Check file locations
if: github.event.pull_request.head.repo.full_name != github.repository
env:
BASE_REF: ${{ github.base_ref }}
run: |
git fetch origin "$BASE_REF"
CHANGED=$(git diff --name-only "origin/$BASE_REF"...HEAD)
MISPLACED=$(echo "$CHANGED" | grep -vE '^(tokens|chains)/' || true)
if [ -n "$MISPLACED" ]; then
echo "Error: Fork PRs may only add or modify files under tokens/ or chains/"
echo "Misplaced files:"
echo "$MISPLACED"
echo "Token assets belong in tokens/<chainID>/<tokenAddress>/ (e.g. tokens/8453/0x1234.../logo.svg)"
exit 1
fi
echo "Ok: all changed files are in allowed locations"
- name: Verify changed SVGs
env:
BASE_REF: ${{ github.base_ref }}
run: |
git fetch origin "$BASE_REF"
CHANGED_SVGS=$(git diff --name-only "origin/$BASE_REF"...HEAD -- '*.svg')
FAILED=0
while IFS= read -r svg; do
[ -n "$svg" ] || continue
[ -f "$svg" ] || continue
if grep -qziE "$SVG_FORBIDDEN_PATTERN" "$svg"; then
echo "Error: $svg contains a base64 image, external link or script"
FAILED=1
fi
if [ "$(wc -c < "$svg")" -gt 153600 ]; then
echo "Error: $svg is larger than 150KB"
FAILED=1
fi
done <<< "$CHANGED_SVGS"
if [ $FAILED -eq 1 ]; then exit 1; fi
echo "Ok: no base64 images, external links or scripts in changed SVGs"
- name: Verify tokens
run: |
FAILED=0
while IFS= read -r -d '' svg; do
if grep -qziE "$SVG_FORBIDDEN_PATTERN" "$svg"; then
echo "Error: $svg contains a base64 image, external link or script"
FAILED=1
fi
done < <(find ./tokens -name "logo.svg" -print0)
while IFS= read -r -d '' dir; do
name=$(basename "$dir")
lower=$(echo "$name" | tr '[:upper:]' '[:lower:]')
if [ "$name" != "$lower" ]; then
echo "Error: $name is not lowercased. Should be $lower"
FAILED=1
fi
for file in logo-128.png logo-32.png logo.svg; do
if [ ! -f "$dir/$file" ]; then
echo "Error: $dir is missing $file"
FAILED=1
fi
done
done < <(find ./tokens -type d -name "0x*" -print0)
while IFS= read -r -d '' dir; do
for file in logo-128.png logo-32.png logo.svg; do
if [ ! -f "$dir/$file" ]; then
echo "Error: $dir is missing $file"
FAILED=1
fi
done
done < <(find ./tokens/1151111081099710 -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
if [ $FAILED -eq 1 ]; then exit 1; fi
echo "Ok: all files match schema definitions!"
- name: Verify chains
run: |
FAILED=0
while IFS= read -r -d '' dir; do
name=$(basename "$dir")
if [[ "$name" == _* ]] || ! [[ "$name" =~ ^[a-zA-Z0-9]+$ ]]; then
continue
fi
for file in logo-128.png logo-32.png logo.svg; do
if [ ! -f "$dir/$file" ]; then
echo "Error: Chain $dir is missing $file"
FAILED=1
fi
done
if [ -f "$dir/logo.svg" ]; then
if grep -qziE "$SVG_FORBIDDEN_PATTERN" "$dir/logo.svg"; then
echo "Error: $dir/logo.svg contains a base64 image, external link or script"
FAILED=1
fi
fi
done < <(find ./chains -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
if [ $FAILED -eq 1 ]; then exit 1; fi
echo "Ok: all chain directories are valid!"
- name: Check allowed files
run: |
FAILED=0
while IFS= read -r -d '' f; do
case "$(basename "$f")" in
logo.svg|logo-32.png|logo-128.png|logo-alt.svg|logo-alt-32.png|logo-alt-128.png|info.json) ;;
*) echo "Error: $f is not allowed (token folders: logo.svg, logo-32.png, logo-128.png, logo-alt.*, info.json)"; FAILED=1 ;;
esac
done < <(find ./tokens -mindepth 3 -type f -print0)
while IFS= read -r -d '' f; do
case "$(basename "$f")" in
logo.svg|logo-32.png|logo-128.png|logo-alt.svg|logo-alt-32.png|logo-alt-128.png) ;;
*) echo "Error: $f is not allowed (chain folders: logo.svg, logo-32.png, logo-128.png, logo-alt.*)"; FAILED=1 ;;
esac
done < <(find ./chains -mindepth 2 -type f -print0)
# Files ABOVE the token/chain folder depth: only the generated indexes belong there.
while IFS= read -r -d '' f; do
case "$(basename "$f")" in
list.json|_info.json) ;;
*) echo "Error: $f is not allowed above token folders (only list.json/_info.json)"; FAILED=1 ;;
esac
done < <(find ./tokens -maxdepth 2 -type f -print0)
while IFS= read -r -d '' f; do
case "$(basename "$f")" in
list.json|_info.json) ;;
*) echo "Error: $f is not allowed at the chains root (only list.json/_info.json)"; FAILED=1 ;;
esac
done < <(find ./chains -maxdepth 1 -type f -print0)
if [ $FAILED -eq 1 ]; then exit 1; fi
echo "Ok: only allowed files in token/chain folders"
# Fork-safe: the script below lives in this trusted workflow (not the PR checkout) and
# only READS folder names + info.json as data — JSON.parse cannot execute code. No PR
# code runs and nothing is installed. VALIDATE_SCRIPT is a static constant, not input.
- name: Verify Solana addresses and info.json schema
env:
VALIDATE_SCRIPT: |
const fs = require('fs');
const path = require('path');
const TOKENS = './tokens';
const SOLANA = '1151111081099710';
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const ALLOWED = new Set(['name', 'symbol', 'decimals', 'description', 'website', 'tags']);
let failed = false;
function base58Length(value) {
const bytes = [];
for (const char of value) {
let carry = B58.indexOf(char);
if (carry === -1) return -1;
for (let j = 0; j < bytes.length; j++) { carry += bytes[j] * 58; bytes[j] = carry & 0xff; carry >>= 8; }
while (carry > 0) { bytes.push(carry & 0xff); carry >>= 8; }
}
for (let i = 0; i < value.length && value[i] === '1'; i++) bytes.push(0);
return bytes.length;
}
function checkInfo(dir) {
const p = path.join(dir, 'info.json');
if (!fs.existsSync(p)) return;
let info;
try { info = JSON.parse(fs.readFileSync(p, 'utf8')); }
catch (e) { console.error('Error: ' + p + ' is not valid JSON'); failed = true; return; }
if (typeof info.name !== 'string' || !info.name) { console.error('Error: ' + p + ' needs a non-empty string name'); failed = true; }
if (typeof info.symbol !== 'string' || !info.symbol) { console.error('Error: ' + p + ' needs a non-empty string symbol'); failed = true; }
if (!Number.isInteger(info.decimals) || info.decimals < 0) { console.error('Error: ' + p + ' needs a non-negative integer decimals'); failed = true; }
if (info.description !== undefined && typeof info.description !== 'string') { console.error('Error: ' + p + ' description must be a string'); failed = true; }
if (info.website !== undefined && typeof info.website !== 'string') { console.error('Error: ' + p + ' website must be a string'); failed = true; }
if (info.tags !== undefined && (!Array.isArray(info.tags) || info.tags.some(t => typeof t !== 'string'))) { console.error('Error: ' + p + ' tags must be an array of strings'); failed = true; }
for (const k of Object.keys(info)) { if (!ALLOWED.has(k)) { console.error('Error: ' + p + ' has unknown field ' + k); failed = true; } }
}
for (const chain of fs.readdirSync(TOKENS)) {
const chainDir = path.join(TOKENS, chain);
if (!fs.statSync(chainDir).isDirectory()) continue;
for (const name of fs.readdirSync(chainDir)) {
if (name.startsWith('.') || name.startsWith('_')) continue;
const dir = path.join(chainDir, name);
if (!fs.statSync(dir).isDirectory()) continue;
if (chain === SOLANA && !name.startsWith('0x') && base58Length(name) !== 32) {
console.error('Error: ' + dir + ' is not a valid base58 Solana address (case-sensitive; lowercased/corrupted rejected)');
failed = true;
}
checkInfo(dir);
}
}
if (failed) process.exit(1);
console.log('Ok: Solana addresses and info.json schemas valid');
run: node -e "$VALIDATE_SCRIPT"