
شرح مفصّل لتحدي CTF: هندسة عكسية لتطبيق SPA مبني على React، وتحليل صور SVG، وبناء حلّال آلي باستخدام المطابقة الجشعة، وتحقيق علامة كاملة.
النتيجة: 200/200 (100%) — الرمز: WKX-9245FA70-200 التاريخ: 24 مارس 2026
أعلن منشور على LinkedIn عن تحدٍّ خاص لبرنامج Wix KickstartX — وهو برنامج للمطوّرين المبتدئين. القواعد:
wix-kickstartx-challenge-2026.base44.app/ و wixkickstart.comالموقع الرئيسي wixkickstart.com هو موقع مستضاف على Wix (يُقدَّم من جانب الخادم بواسطة محرك Thunderbolt الخاص بـ Wix). لا يُرجع أمر curl العادي سوى هيكل JavaScript — دون أي محتوى فعلي. استخدمنا متصفح Chromium بدون واجهة رسومية (headless) لعرضه:
chromium --headless --disable-gpu --no-sandbox --virtual-time-budget=10000 \
--dump-dom "https://wixkickstart.com" > /tmp/wix_rendered.html
من DOM المعروض، استخرجنا iframe مضمّنًا:
اتضح أنه رسم متحرك جسيمات p5.js (عنصر جمالي في القسم الرئيسي)، وليس التحدي نفسه. التحدي الفعلي موجود في تطبيق Base44.
يعمل التحدي على Base44 — وهي منصة تطبيقات منخفضة الكود (low-code). التطبيق هو تطبيق React أحادي الصفحة، وكل المنطق في حزمة JS واحدة:
https://wix-kickstartx-challenge-2026.base44.app/assets/index-oTG160r9.js
الحجم: 410,809 بايت (React مُصغَّر + منطق التطبيق + Base44 SDK)
من خلال البحث في حزمة JS، قمنا بتخطيط الـ API بالكامل:
| Function | الغرض |
|---|---|
startGame | ينشئ جلسة لعبة، ويعيد 200 صورة + 200 وصف |
scoreGame | يقبل {matches: {}, sessionId: ""}، ويعيد {correctCount: N} |
generateToken | يأخذ معرّف الجلسة، ويعيد رمز الإكمال |
getLeaderboard | يعيد أعلى الدرجات |
saveNickname | يحفظ الاسم المعروض للوحة المتصدرين |
الكيانات: GameSession، Participant
نمط عنوان API:
POST /api/apps/{appId}/functions/{functionName}
معرّف التطبيق: 69aea07cbcb9a3dd1039a58d
const or = 1800; // Time limit: 1800 seconds (30 minutes)
const rm = 200; // Total items: 200 image-description pairs
اكتشاف بالغ الأهمية — التطبيق يحتوي على وضع إرسال جماعي JSON. من المصدر المُصغَّر:
function Ik({onSubmit:r, onClose:n}) {
// ...
h = JSON.parse(s) // Parse JSON input
// Validation: must be object like { "IMG-001": "DESC-042", ... }
r(h) // Submit all matches at once
}
النص النموذجي داخل النافذة المنبثقة:
{
"IMG-001": "DESC-042",
"IMG-002": "DESC-017",
...
}
هذا يعني أننا لا نحتاج إلى النقر 200 مرة في الواجهة — يمكننا إرسال خريطة JSON لجميع المطابقات الـ200 برمجيًا.
يستخدم SDK الخاص بـ Base44 مصادقة JWT:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
حمولة JWT تحتوي على:
{
"sub": "[email protected]",
"exp": 1782162460,
"iat": 1774386460
}
الترويسات المطلوبة لجميع استدعاءات API:
Content-Type: application/json
Authorization: Bearer {jwt_token}
X-App-Id: 69aea07cbcb9a3dd1039a58d
Base44-Functions-Version: prod
X-Origin-URL: https://wix-kickstartx-challenge-2026.base44.app/
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/startGame
Status: 200
Transferred: 62.87 kB compressed (2.20 MB decompressed)
Content-Encoding: br (Brotli)
بنية الاستجابة:
{
"status": "active",
"sessionId": "69c2fd218e5b26f307c941c9",
"startedAt": "2026-03-24T21:07:45.101Z",
"imageOrder": ["IMG-154", "IMG-102", ...], // 200 items (display order)
"descOrder": ["DESC-125", "DESC-109", ...], // 200 items (display order)
"imagesData": { "IMG-001": "data:image/svg+xml;base64,...", ... }, // 200 SVGs
"descriptionsData": { "DESC-105": "milky field, overlaid with...", ... } // 200 texts
}
كل صورة هي SVG بأبعاد 200x200 تحتوي على:
مثال لـ SVG مفكوك الترميز (IMG-001):
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<defs>
<linearGradient id="bg" ...>
<stop offset="0%" stop-color="#f5f0eb"/>
<stop offset="100%" stop-color="#e8e0d5"/>
</linearGradient>
</defs>
<rect width="200" height="200" fill="url(#bg)"/>
<!-- Optional overlay lines/dots/rings here -->
<polygon points="..." fill="#E2725B" transform="translate(128.6,185.7) rotate(90)"
style="opacity:0.5;filter:drop-shadow(...)"/>
<!-- More shapes... -->
</svg>
كل وصف يتبع نمطًا صارمًا:
{background} field[, overlaid with {overlay}]. {N} elements total:
{size} {opacity} {color} {shape} ({rotation}, at {position}) ·
{size} {opacity} {color} {shape} ({rotation}, at {position}) · ...
مثال:
milky field, overlaid with tilted cross-lines. 10 elements total:
minuscule nearly solid dim gray pike (steeply angled, at center inner-left) ·
substantial solid azure pike (diagonal, at top center) · ...
احتجنا إلى ربط خصائص SVG بمفردات الوصف. تطلّب ذلك تحليل جميع الصور الـ200 وجميع الأوصاف الـ200 للعثور على التطابقات الدقيقة.
من خلال عدّ مرات الظهور في الجانبين، أنشأنا تطابقًا 1:1:
| لون بداية تدرج SVG | العدد | كلمة الوصف | العدد |
|---|---|---|---|
#0a1628 | 28 | pitch | 28 |
#eef2f7 | 20 | frosted | 20 |
#0d0d0d | 19 | tenebrous | 19 |
#fef9f0 | 18 | pearlescent | 18 |
#1e0a2e | 18 | nocturnal | 18 |
#0a1a0a | 17 | midnight | 17 |
#1a0a0a | 15 | inky | 15 |
#f5f0eb | 14 | milky | 14 |
#f7f3ee | 14 | ethereal | 14 |
#f0f0f0 | 14 | radiant | 14 |
#f0f7f4 | 13 | glowing | 13 |
#1a1a2e | 10 | somber | 10 |
الطريقة: عدّ ألوان بداية التدرج الفريدة في جميع ملفات SVG الـ200، وعدّ الكلمة الأولى الفريدة في جميع الأوصاف الـ200، ثم المطابقة بناءً على العدد.
تراكبات SVG مُنفَّذة بطرق مختلفة حسب النوع:
| نمط SVG | طريقة الاكتشاف | العدد | اسم الوصف | العدد |
|---|---|---|---|---|
عناصر <line> أفقية (dy=0) | فحص x1,y1,x2,y2 | 35 | striped overlay | 35 |
عناصر <line> عمودية (dx=0) | فحص x1,y1,x2,y2 | 34 | lattice pattern | 34 |
<circle> مع opacity="0.06" (نقاط) | عدّ الدوائر منخفضة الشفافية | 30 | stippled layer | 30 |
| لا توجد عناصر تراكب إطلاقًا | لا خطوط، ولا أشكال خلفية | 28 | (none) | 28 |
عناصر <line> قطرية | فحص اتجاه الميل | 27 | tilted cross-lines | 27 |
<polygon>/<path> صغيرة مع opacity="0.06" | أشكال بسمة شفافية منخفضة | 25 | arrow-band texture | 25 |
<circle> مع fill="none" stroke="#888" | دوائر بحدود فقط | 21 | ringed pattern | 21 |
الملاحظة الأساسية: عناصر نمط الخلفية تستخدم opacity كـ سمة XML (مثل opacity="0.06")، بينما تستخدم الأشكال الأمامية opacity داخل سمة style (مثل style="opacity:0.7"). كان هذا التمييز حاسمًا لفصل أنماط الخلفية عن الأشكال الفعلية.
جميع ملفات SVG الـ200 تستخدم 28 لون تعبئة hex فريدًا بالضبط. وجميع الأوصاف الـ200 تستخدم 28 اسم لون فريدًا بالضبط.
| Hex | اسم الوصف | Hex | اسم الوصف | |
|---|---|---|---|---|
#708090 | blue-gray | #cd7f32 | brass | |
#f5f5f5 | near white | #dc143c | fiery red | |
#00bcd4 | electric cyan | #2196f3 | azure | |
#ff6b6b | salmon pink | #228b22 | rich green | |
#98ff98 | pale green | #b0b0b0 | platinum | |
#0f52ba | deep sapphire | #800020 | oxblood | |
#ff8c00 | deep orange | #4b0082 | dark purple | |
#e2725b | terra rosa | #ffbf00 | marigold | |
#b7410e | russet | #ff69b4 | candy pink | |
#ffd700 | bright gold | #c0c0c0 | tin | |
#6b8e23 | moss | #008080 | deep teal | |
#40e0d0 | pale teal | #0047ab | royal blue | |
#e34234 | burnt sienna | #a0522d | deep red | |
#4a4a4a | dim gray | #36454f | dark gray |
| عنصر SVG | منطق الاكتشاف | اسم الوصف |
|---|---|---|
<polygon> 3 رؤوس | عدّ أزواج النقاط المفصولة بمسافات | pike |
<polygon> 4 رؤوس | tilted square | |
<polygon> 5 رؤوس | quint form | |
<polygon> 6 رؤوس | bee cell | |
<polygon> 10 رؤوس | asterisk | |
<rect> (ليس خلفية) | له width/height، وليس 200x200 | tilted square |
<circle> | اسم الوسم | disc |
<path> مع fill-rule="evenodd" | مساران قوسيان متحدا المركز | donut |
<path> مع قوس واحد + Z | مسار نصف دائرة | half-disc |
<path> مع 6+ أوامر L | شكل صليب/زائد | crosshair |
<path> مع 3-5 أوامر L | شكل شبيه بالسهم | pointer |
استنادًا إلى أقصى نصف قطر لرؤوس الشكل من نقطة الأصل:
| نطاق نصف القطر | اسم الوصف |
|---|---|
| 0–12 | minuscule |
| 13–16 | modest |
| 17–20 | mid-sized |
| 21–25 | substantial |
| 26+ | massive |
| قيمة شفافية SVG | اسم الوصف |
|---|---|
| 1.0 | solid |
| 0.85 | nearly solid |
| 0.7 | semi-transparent |
| 0.5 | faint |
| قيمة rotate() في SVG | اسم الوصف |
|---|---|
| 0° | upright |
| 1–20° | slightly tilted |
| 21–55° | diagonal |
| 56–75° | steeply angled |
| 76–105° | sideways |
تُوضع الأشكال على شبكة 7x7 عند الإحداثيات:
[14.3, 42.9, 71.4, 100.0, 128.6, 157.1, 185.7]
هذه تُرتبط بأسماء الأعمدة: far-left, left, inner-left, center, inner-right, right, far-right
وبأسماء الصفوف: top, upper, upper-mid, center, lower-mid, lower, bottom
لكل صورة من صور SVG الـ200:
<stop> من <linearGradient id="bg"><line> أو <circle fill="none" stroke> أو نقاط/أشكال منخفضة الشفافية<polygon> و <circle> و <path> و <rect> التي:
width="200")fill="url(#vig)")opacity="0.0x" كسمة)style="opacity:..." (جميع الأشكال الأمامية تملك هذا)fill، ويُربط عبر COLOR_MAPstyle="opacity:X"transform="... rotate(X)"transform="translate(X,Y) ..."لكل وصف نصي من الأوصاف الـ200:
(\d+) elements total (minuscule|modest|mid-sized|substantial|massive)
(solid|nearly solid|semi-transparent|faint)
(color name)
(shape type)
((rotation), at (position))
تستخدم خوارزمية المطابقة نهج التسجيل الجشع:
def score_match(img, desc):
# Hard constraints — must match exactly
if img['num'] != desc['num']: return -10000 # Shape count
if img['bg'] != desc['bg']: return -10000 # Background type
if img['overlay'] != desc['overlay']: return -10000 # Overlay type
score = 100 # Base score for matching hard constraints
# Soft scoring — color overlap
for color in img_colors:
if color in desc_colors:
score += 10
# Soft scoring — type overlap
for type in img_types:
if type in desc_types:
score += 8
# Per-shape detail matching (Hungarian-style greedy)
for each image_shape:
find best matching desc_shape by:
+20 if color matches
+15 if type matches
+10 if size matches
+8 if opacity matches
+5 if rotation matches
score += best_match_score
return score
ثم نخصّص الأزواج الأعلى درجة أولًا بطريقة جشعة:
all_scores.sort(reverse=True)
for score, img_id, desc_id in all_scores:
if img_id not in matched and desc_id not in used:
matches[img_id] = desc_id
1. Start Game
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/startGame
Body: {"email": "[email protected]"}
Response: 200 OK (2.2 MB — all game data)
2. Score Game
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/scoreGame
Body: {
"matches": {"IMG-032": "DESC-182", "IMG-198": "DESC-134", ...},
"sessionId": "69c2fd218e5b26f307c941c9"
}
Response: 200 OK
{"correctCount": 200, "scoreSeal": "3d1ac8a7e7be4856"}
3. Generate Token
POST /api/apps/69aea07cbcb9a3dd1039a58d/functions/generateToken
Body: {"sessionId": "69c2fd218e5b26f307c941c9"}
Response: 200 OK
{"token": "WKX-9245FA70-200", "score": 200}
| الخطوة | الإجراء |
|---|---|
| 21:07:45 UTC | بدأت اللعبة (تم استدعاء startGame) |
| 21:07–21:15 | تطوير سكربت الحلّال (التحليل + المطابقة) |
| ~21:15 | تم إرسال scoreGame — 200/200 صحيح |
| ~21:15 | تم استدعاء generateToken — WKX-9245FA70-200 |
إجمالي وقت الحل: ~8 دقائق من نافذة مدتها 30 دقيقة.
| المكوّن | التقنية |
|---|---|
| منصة التحدي | Base44 (أداة بناء تطبيقات منخفضة الكود) |
| الواجهة الأمامية | React SPA (حزمة JS واحدة، ~410KB) |
| الواجهة الخلفية | Python/uvicorn خلف Cloudflare |
| CDN/وكيل | Cloudflare (HTTP/3، ضغط Brotli) |
| المصادقة | JWT (HS256)، مخزَّنة في localStorage |
| تنسيق البيانات | SVG (مضمّن base64)، JSON API |
| الوقت الفعلي | WebSocket (socket.io) لتحديثات الجلسة المباشرة |
| الحلّال | Python 3 (تحليل بالتعبيرات النمطية، دون مكتبات خارجية) |
نقطة الإرسال JSON كانت العامل الحاسم — دونها، كنا سنحتاج إلى أتمتة المتصفح للنقر 400 مرة (اختيار الصورة + اختيار الوصف × 200).
التخطيط القائم على العدّ كان الاختراق للألوان والخلفيات والتراكبات. بدلًا من تخمين ما يقابله اللون السداسي #cd7f32 بالإنجليزية، عددنا مرات ظهوره في ملفات SVG فوجدناه 65 مرة، ووجدنا "brass" تظهر 65 مرة في الأوصاف — العدد الفريد = تطابق مضمون.
القيود الصارمة تستبعد المرشحين بسرعة. كل صورة لها تركيبة فريدة من (نوع الخلفية، نوع التراكب، عدد الأشكال). مع 12 خلفية × 7 تراكبات × أعداد أشكال متفاوتة، معظم الصور لديها حفنة فقط من أوصاف المرشحين المحتملين، وليس 200.
SVG بيانات منظمة. على عكس الصور النقطية (PNG/JPG)، فإن SVGs هي XML — كل شكل ولون وموضع ودوران مشفَّر صراحةً كنص. لا حاجة لرؤية حاسوبية.
كان اكتشاف التراكب صعبًا. استخدمت أنماط الخلفية 5 تقنيات SVG مختلفة: عناصر <line>، و <circle> بحدود فقط، و <circle> بسمة شفافية منخفضة، و <polygon> بسمة شفافية منخفضة، وغياب كل هذه. التمييز الأساسي كان opacity كسمة XML (للخلفية) مقابل داخل style (لأشكال المقدمة).
مفردات الألوان لم تكن واضحة. أسماء مثل "oxblood" (#800020) و"brass" (#cd7f32) و"deep sapphire" (#0f52ba) تطلبت نهج العدّ — لا يمكنك تخمينها بشكل موثوق من القيم السداسية وحدها.
تصنيف الأشكال من مسارات SVG تطلّب فهم أوامر مسار SVG:
M (moveto)، L (lineto)، A (arc)، Z (closepath)fill-rule="evenodd"