الـ Object Storage والـ Media: presigned URLs و uploads مباشرة
الـ users بيرفعوا صور وفيديوهات، والـ server بيستلم كل byte في الـ memory، ويحفظها على الـ disk بتاعه، وبالاسم اللي الـ user اختاره، وأي حد يعرف الـ URL يشوفها.
نص شرح المعلم والكود الذي كتبه ونتيجة تشغيله، بنفس ترتيب الدرس.
أهلًا بيك. الـ chat بتاعنا بقى فيه صور. بص على upload dot naive dot js. الـ client بيبعت الصورة base64 في JSON، والـ server بيحفظها في public uploads، ويرجّع الـ URL. وده code بتلاقيه في tutorials كتير. الـ file كله بيعدّي على الـ server، وفي الـ memory، و base64 كمان يعني أكبر بالتلت. عشرين user بيرفعوا فيديو، والـ server وقع. الـ disk بتاع instance واحد. الـ instance التاني مش شايف الصورة، ولما تعمل deploy، الـ container الجديد فاضي. الاسم من الـ user زي ما هو. يبعت dot dot slash، ويكتب فوق ملفات الـ app. ده path traversal. النوع ما اتشافش. يرفع HTML فيه script ويقول إنها صورة، وأي حد يفتحها الـ script يشتغل على الـ domain بتاعك. مفيش حد للحجم، والـ resize جُوّه الـ request. والـ public folder: أي حد يعرف الـ URL يشوف صورة البطاقة بتاعتك.
الحل الأول: الـ files ما تتحفظش على الـ server خالص. بتتحفظ في object storage: S3 من AWS، أو R2 من Cloudflare، أو GCS، أو MinIO لو عايز تشغّله بنفسك. فيه buckets، وجُوّه كل bucket فيه objects. كل object ليه key، زي uploads slash sara slash id، والـ bytes، و metadata زي الـ content type. مفيش folders بجد. الـ slash دي جزء من الاسم، بس تقدر تعمل list بـ prefix. والـ object ما بيتعدّلش في النص. يا تكتبه كله، يا تكتب واحد جديد. وكل الـ instances بتشوف نفس الـ storage، والـ deploy ما بيلمسوش، والـ storage بيعمل نسخ لوحده، وبيستحمل أي حجم.
طب إزاي الـ client يرفع للـ storage من غير ما ياخد الـ credentials بتاعتنا؟ بالـ presigned URL. الـ server بيكتب تصريح: PUT، على الـ key ده بالضبط، بالـ content type ده، بالحجم ده بالكتير، لحد ساعة معيّنة. وبيعمل signature للتصريح ده بـ HMAC بالـ secret اللي هو والـ storage بس يعرفوه. وبيحط التصريح والـ signature في الـ URL. الـ client بيعمل PUT على الـ URL ده مباشرة. والـ storage بيحسب الـ signature تاني، ولو أي حاجة اتغيرت، الـ key أو الحجم أو الوقت، الـ signature مش هتطابق، والـ request يترفض. كده الـ server ما شافش ولا byte من الـ file، والـ client ما خدش أي secret، والتصريح ده بيخلص بعد خمس دقايق، وما ينفعش يتستخدم لـ key تاني. في S3 الحقيقي، الـ signature اسمها SigV4، وأعقد شوية، بس نفس الفكرة بالضبط. وفيه حاجتين لازم تعرفهم: الـ presigned PUT في S3 ما بيقدرش يحدّد حجم أقصى. لو عايز تحدّد الحجم، بتستخدم presigned POST بـ policy فيها content-length-range. أو تتأكد بعد الـ upload، وده اللي هنعمله كمان. والـ files الكبيرة، زي فيديو جيجا، ما بتترفعش في request واحد. بتستخدم multipart upload: بتبدأ upload، وتاخد presigned URL لكل part، خمسة ميجا، والـ client يرفع الـ parts بالتوازي، ولو part وقع يعيده هو بس، وفي الآخر complete بيجمعهم. وكل multipart ما خلصش بياخد مساحة وانت بتدفع عليها، فلازم lifecycle rule تمسحه.
الـ client رفع. نصدّقه؟ لأ. الـ client بيقول للـ server خلصت، يعني complete. والـ server بيعمل: head على الـ object: موجود فعلًا؟ حجمه زي اللي اتقال؟ ويقرا أول كام byte، اللي اسمهم magic bytes. كل نوع file بيبدأ ببصمة: PNG بيبدأ بـ تمن bytes معروفين، و JPEG بـ تلاتة. الـ content type اللي الـ client قاله مجرد كلام، إنما الـ bytes دي هي الحقيقة. لو مش مطابقين، امسح الـ object، وارفض. وبعد كده الـ processing: resize، و thumbnails، و virus scan، وشيل الـ EXIF اللي فيه مكان الصورة. ده تقيل، فبيتحط في queue، والـ worker يعمله في background، والـ status يبقى processing لحد ما يخلص. والـ complete لازم يبقى idempotent: لو الـ client بعته مرتين، ما يتحطش في الـ queue مرتين.
والـ download. الـ bucket يفضل private. محدش يقدر يقرا منه من غير signature. ولما صاحب الصورة يطلبها، الـ server يتأكد إنها بتاعته، وإنها ready، ويعمل presigned GET لمدة دقيقة. ولو الصور public بجد، زي صور المنتجات، حط CDN قدام الـ bucket، والـ CDN يعمل cache قريب من الـ users. وفيه CDNs بتعمل signed URLs برضه، و image resizing وقت الـ request. وخلّي بالك من الـ content-disposition: الـ files اللي الـ users رفعوها تتعرض من domain تاني غير الـ app، علشان لو حاجة عدّت، ما توصلش للـ cookies بتاعتك. وآخر حاجة، الـ cleanup. الـ client خد URL ورفع ومشي، أو ما رفعش أصلًا. الـ records دي بتفضل pending، والـ objects بتاخد مساحة. job بيعدّي كل شوية، وأي pending عدّى عليه وقت كبير يتمسح. وده بيتعمل كمان بـ lifecycle rules في الـ bucket نفسه.
التحدي. عندك storage dot js، object storage صغير على HTTP زي S3، بيتأكد من الـ presigned URLs بنفسه، ومعاه functions للـ server زي الـ SDK. و lab dot js بيمشي الـ flow كله، وبيجرّب محاولات غلط. والمطلوب media dot js. كل التفاصيل في الكومنت: presign بنفس طريقة الـ storage بالضبط، و sniff و pngSize و safeFilename، و createMediaService بالـ createUpload والـ completeUpload والـ processNext والـ downloadUrl والـ cleanup. اقرا storage dot js الأول علشان تفهم بيتأكد من إيه، وبعدين الكومنت، ونشغّل test dot js لحد ما تبقى أربعتاشر من أربعتاشر.
اكتب media.js: MediaError، و presign بـ HMAC-SHA256 بنفس الـ string والـ query اللي storage.js بيتأكد منهم، و sniff و pngSize و safeFilename، و createMediaService بـ createUpload و completeUpload (head والحجم والـ magic bytes) و processNext (meta.json والـ retries) و downloadUrl و cleanup و get و pending. كل التفاصيل في الكومنت، وشغّل node test.js.
الحل. presign: الـ string بالترتيب، و HMAC، و URLSearchParams بالترتيب، و encodeURIComponent لكل جزء من الـ key. الـ key من الـ user والـ id بس، والـ filename بيتخزّن في الـ record للعرض، متنضّف. الـ complete: head، والحجم، وأول ستاشر byte، ولو مش مطابق delete. الـ processNext: لو وقع، يرجع لآخر الـ queue لحد تلات مرات. نشغّل lab dot js.
// media.js: الـ uploads بتروح للـ storage مباشرة بـ presigned URLs، والـ server بيتأكد ويعمل processing في background
import { createHmac, createHash, randomUUID } from "node:crypto";
export class MediaError extends Error {
constructor(code, message = code) {
super(message);
this.code = code;
}
}
export function presign({ base, bucket, secret, method, key, ttlMs, contentType = null, maxBytes = null, now = Date.now }) {
const expires = now() + ttlMs;
const toSign = [method, bucket, key, expires, contentType ?? "", maxBytes ?? ""].join("\n");
const sig = createHmac("sha256", secret).update(toSign).digest("hex");
const q = new URLSearchParams({ "X-Method": method, "X-Expires": String(expires) });
if (contentType) q.set("X-Content-Type", contentType);
if (maxBytes != null) q.set("X-Max-Bytes", String(maxBytes));
q.set("X-Signature", sig);
const path = key.split("/").map(encodeURIComponent).join("/");
return { url: `${base}/${bucket}/${path}?${q}`, expiresAt: expires };
}
const MAGIC = {
"image/png": [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
"image/jpeg": [0xff, 0xd8, 0xff],
};
export function sniff(buf) {
for (const [type, sig] of Object.entries(MAGIC)) if (sig.every((b, i) => buf[i] === b)) return type;
return null;
}
export function pngSize(buf) {
if (sniff(buf) !== "image/png" || buf.length < 24) return null;
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
}
export function safeFilename(name) {
const base = String(name ?? "").split(/[\\/]/).pop().normalize("NFKC");
const clean = base.replace(/[^\p{L}\p{N}._-]+/gu, "_").replace(/^\.+/, "").slice(0, 100);
return clean || "file";
}
export function createMediaService({
storage, bucket, secret, now = Date.now,
maxBytes = 5 * 1024 * 1024, allowed = ["image/png", "image/jpeg"],
uploadTtl = 300000, downloadTtl = 60000,
}) {
const records = new Map();
const queue = [];
const sign = (method, key, ttlMs, extra = {}) => presign({ base: storage.url, bucket, secret, method, key, ttlMs, now, ...extra });
function mine(userId, id) {
const r = records.get(id);
if (!r || r.userId !== userId) throw new MediaError("NOT_FOUND");
return r;
}
function createUpload(userId, { filename, contentType, size } = {}) {
if (!allowed.includes(contentType)) throw new MediaError("UNSUPPORTED_TYPE");
if (!Number.isInteger(size) || size <= 0 || size > maxBytes) throw new MediaError("TOO_LARGE");
const id = randomUUID();
const key = `uploads/${userId}/${id}`;
const rec = { id, userId, key, filename: safeFilename(filename), contentType, size, status: "pending", createdAt: now() };
records.set(id, rec);
const { url, expiresAt } = sign("PUT", key, uploadTtl, { contentType, maxBytes: size });
return { id, key, url, expiresAt };
}
async function completeUpload(userId, id) {
const rec = mine(userId, id);
if (rec.status !== "pending") return { ...rec };
const head = await stora
…المخرجات
$ node lab.js — 1) الـ client بيطلب upload، والـ file بيروح للـ storage على طول — الـ server رجّع key: uploads/sara/bdef5ca7-5472-46fc-9379-cbbefa4a6506 والـ URL صالح لحد: 14:51:35 وفيه: X-Method X-Expires X-Content-Type X-Max-Bytes X-Signature PUT للـ storage: 200 تمام الـ server استلم كام byte من الـ file؟ صفر. الـ storage استلم: 150000 — 2) محاولات غلط — نفس الـ URL على key تاني: 403 SignatureDoesNotMatch file أكبر من اللي اتقال: 413 EntityTooLarge content-type تاني: 403 ContentTypeMismatch بعد ما الـ URL انتهى: 403 RequestExpired HTML على إنه PNG، الـ PUT نفسه: 200 تمام بس الـ complete: BAD_CONTENT ، والـ object اتمسح: true — 3) complete، والـ processing في background — بعد الـ complete: processing ، والـ filename المتنضّف: صورة_البروفايل.png download قبل ما يخلص: NOT_READY الـ worker خلّص: ready 1080x720 sha256 2e269eef766d الـ objects: [ 'derived/bdef5ca7-5472-46fc-9379-cbbefa4a6506/meta.json', 'uploads/sara/bdef5ca7-5472-46fc-9379-cbbefa4a6506' ] — 4) الـ download: private، و URL قصير العمر — GET: 200 image/png 150000 bytes omar بيحاول ياخد URL لصورة سارة: NOT_FOUND الـ path من غير signature: 403 نفس الـ URL بعد دقيقة: 403 — 5) upl …
الأول: الـ key ما فيهوش الاسم اللي فيه dot dot slash، والـ URL فيه التصريح كله والـ signature، والـ storage استلم الـ bytes، والـ server صفر. التاني: key تاني، أو حجم أكبر، أو نوع تاني، أو بعد الوقت، كله اترفض. والـ HTML عدّى من الـ PUT، لأن الـ storage ما بيفتحش الـ file، بس الـ complete مسكه بالـ magic bytes ومسحه. التالت: processing، والـ download قبل ما يخلص اترفض، والـ worker طلّع الأبعاد و meta dot json. الرابع: الـ download بـ URL صاحبه بس، والـ path من غير signature اترفض، ونفس الـ URL بعد دقيقة اترفض. الخامس: الـ cleanup مسح الاتنين اللي اتسابوا. ونشغّل test dot js.
المخرجات
$ node test.js
✅ presign: الـ storage بيقبل الـ URL الصح، وبيرفض لو حرف واحد اتغير في الـ signature {"r1":{"status":200,"error":null},"r1b":{"status":403,"error":"SignatureDoesNotMatch"},"p":{"url":"http://s/b/x/y?X-Method=GET&X-Expires=6000&X-Signature=a1523dc06c016257f3eec5c6d8d65af4de565a08c023b1ac3bfe709c3454d9d1","expiresAt":6000}}
✅ URL انتهى ← RequestExpired، و URL للـ PUT ما ينفعش GET، و content-type غير المتسجّل ← رفض {"r2":{"status":403,"error":"RequestExpired"},"r3a":{"error":"MethodNotAllowed"},"r3b":{"status":403,"error":"ContentTypeMismatch"}}
✅ أكبر من X-Max-Bytes ← 413 EntityTooLarge {"status":413,"error":"EntityTooLarge"}
✅ الـ signature مربوط بالـ key والحجم: ما ينفعش تغيّر الـ path ولا X-Max-Bytes {"r4b":{"status":403,"error":"SignatureDoesNotMatch"},"r4c":{"status":403,"error":"SignatureDoesNotMatch"},"s":{"puts":1,"gets":0,"rejected":7}}
✅ sniff و pngSize و safeFilename ["صورة_العيد.png","hidden"]
✅ createUpload: الـ validation، والـ key من الـ user والـ id بس، والـ filename متنضّف {"e6":["UNSUPPORTED_TYPE","TOO_LARGE","TOO_LARGE","TOO_LARGE"],"up":"uploads/u1/a37975cd-ceb0-4dab-bc9a-52cb9e3dee66"}
✅ الـ flow كله: PUT مباشرة للـ storage، و complete ← processing
…كله نجح. الصورة بقت ready. وبعدين؟ نبعت إيميل، ونعمل notification، ونحطها في الـ search. الدرس الجاي: الـ integrations مع الـ email providers والـ push notifications والـ search engines، والـ webhooks بتاعتهم، والـ retries، وإزاي تخلّي الـ index ماشي مع الـ database.
// upload.naive.js: الـ file بيعدّي على الـ server، وبيتحفظ على الـ disk بتاعه
import { createServer } from "node:http";
import { writeFileSync, mkdirSync } from "node:fs";
mkdirSync("public/uploads", { recursive: true });
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/upload") return res.writeHead(404).end();
let raw = "";
for await (const c of req) raw += c; // الـ file كله في الـ memory، وكمان base64 يعني أكبر بالتلت
const { filename, data } = JSON.parse(raw);
// الاسم من الـ user زي ما هو: ../../server.js ؟
writeFileSync(`public/uploads/${filename}`, Buffer.from(data, "base64"));
// وبعدين resize هنا، والـ request مستني
res.end(JSON.stringify({ url: `/uploads/${filename}` })); // public لأي حد يعرف الاسم
}).listen(3000);
// والمشاكل:
// - كل upload واخد memory و bandwidth و connection من الـ server طول ما هو بيترفع
// - الـ disk بتاع instance واحد: الـ instance التاني مش شايف الـ file، والـ deploy بيمسحه
// - الاسم من الـ user ← path traversal، والنوع ما اتشافش ← HTML متخبّي على إنه صورة
// - مفيش حد للحجم، والـ processing جُوّه الـ request// storage.js: object storage صغير زي S3، في الـ memory. ما تعدّلش فيه.
// - HTTP: PUT و GET على /bucket/key بـ presigned URL بس، وبيتأكد من الـ signature والوقت والـ method والـ content-type والحجم
// - وللـ server نفسه (زي الـ SDK بالـ credentials): head و get و put و delete و list
import { createServer } from "node:http";
import { createHmac, createHash, timingSafeEqual } from "node:crypto";
export async function startStorage({ secret, now = Date.now } = {}) {
const objects = new Map(); // "bucket/key" -> { body, contentType, size, etag }
const stats = { puts: 0, gets: 0, rejected: 0 };
const save = (bucket, key, body, contentType) => {
const etag = createHash("md5").update(body).digest("hex");
objects.set(`${bucket}/${key}`, { body, contentType, size: body.length, etag });
return etag;
};
const deny = (res, status, code) => {
stats.rejected++;
res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify({ error: code }));
};
const server = createServer(async (req, res) => {
const url = new URL(req.url, "http://x");
const [, bucket, ...rest] = url.pathname.split("/");
const key = decodeURIComponent(rest.join("/"));
const q = Object.fromEntries(url.searchParams);
const toSign = [q["X-Method"], bucket, key, q["X-Expires"], q["X-Content-Type"] ?? "", q["X-Max-Bytes"] ?? ""].join("\n");
const want = createHmac("sha256", secret).update(toSign).digest();
const got = Buffer.from(q["X-Signature"] ?? "", "hex");
if (got.length !== want.length || !timingSafeEqual(got, want)) return deny(res, 403, "SignatureDoesNotMatch");
if (Number(q["X-Expires"]) <= now()) return deny(res, 403, "RequestExpired");
if (q["X-Method"] !== req.method) return deny(res, 403, "MethodNotAllowed");
if (req.method === "PUT") {
const ct = req.headers["content-type"] ?? "";
if (q["X-Content-Type"] && ct !== q["X-Content-Type"]) return deny(res, 403, "ContentTypeMismatch");
const max = q["X-Max-Bytes"] ? Number(q["X-Max-Bytes"]) : Infinity;
const chunks = [];
let size = 0;
for await (const c of req) {
size += c.length;
if (size > max) return deny(res, 413, "EntityTooLarge");
chunks.push(c);
}
stats.puts++;
const etag = save(bucket, key, Buffer.concat(chunks), ct);
return res.writeHead(200, { etag }).end();
}
if (req.method === "GET") {
const o = objects.get(`${bucket}/${key}`);
if (!o) return deny(res, 404, "NoSuchKey");
stats.gets++;
return res.writeHead(200, { "content-type": o.contentType, "content-length": o.size, etag: o.etag }).end(o.body);
}
deny(res, 405, "MethodNotAllowed");
});
await new Promise((r) => server.listen(0, "127.0.0.1", r));
return {
url: `http://127.0.0.1:${server.address().port}`,
stats,
async head(bucket, key) {
const o = objects.get(`${bucket}/${key}`);
return o ? { size: o.size, contentType: o.contentTyp
…03 / دروس المسار (122)
- 01أهلًا بيك: يعني إيه backend، والكورس ده ماشي إزاي
- 02الملفات والفولدرات والـ terminal: تتحرك على جهازك زي المبرمجين
- 03Node وأول برنامج: تشغّل وتقرا رسالة الخطأ
- 04القيم والمتغيرات والأنواع: const و let و typeof
- 05القرارات والتكرار: if و === والـ truthy والـ loops
- 06الـ functions: parameters و return والـ arrow functions والـ scope
- 07الـ objects والـ arrays: destructuring و spread و map و filter و reduce
- 08JSON والنصوص والتواريخ: اللغة اللي الـ backend بيتكلم بيها
- 09الـ callbacks والـ closures وأول لمسة async: setTimeout
- 10الـ classes والـ errors: throw و try/catch و stack trace
- 11git: تحفَظ كل خطوة وترجعلها، وما تسرّبش أسرارك
- 12ESM و CommonJS و package exports: Node بيلاقي الـ modules إزاي
- 13الـ Promises و async/await: sequential و parallel والأخطاء والإلغاء
- 14الـ event loop والـ microtasks: Node بيعمل كل حاجة بـ thread واحد إزاي
- 15الـ Streams والـ Backpressure: ملفات أكبر من الـ memory
- 16الـ File System والـ paths: قراية وكتابة صح، ومن غير path traversal
- 17الـ Buffers والبيانات الـ binary: الـ bytes من جُوّه
- 18Crypto APIs والأسرار: random و hash و HMAC و AES-GCM
- 19الـ Worker Threads: شغل CPU تقيل من غير ما توقف الـ server
- 20الـ Errors على مستوى الـ process: operational و programmer و crash-only
- 21الـ Node test runner والـ debugging: tests من غير مكتبات
- 22HTTP من جُوّه: الـ methods والـ status codes والـ headers
- 23Express والـ Routing: الـ params والـ query والترتيب والـ Router
- 24Express 5: إيه اللي اتغيّر، وإزاي تنقل مشروع قديم
- 25الـ Middleware ودورة حياة الـ request جُوّه Express
- 26Validation للمدخلات بـ Zod: ما تصدّقش أي حاجة جاية من برة
- 27تصنيف الأخطاء و Problem Details: شكل واحد لكل الـ errors
- 28الـ CORS والـ Cookies: المتصفح بيحمي المستخدم إزاي
- 29رفع الملفات وحدود الحجم: multer من غير ثغرات
- 30الـ Realtime: Server-Sent Events و WebSockets
- 31OpenAPI والـ API Versioning: عقد مكتوب، وتغيير من غير ما تكسر حد
- 32PostgreSQL: الـ DDL وأنواع البيانات الصح لكل عمود
- 33الـ Constraints: PK و FK و UNIQUE و CHECK — الـ database بتحمي نفسها
- 34الـ Normalization والـ ERD: تقسّم البيانات صح من أول يوم
- 35الـ JOINs والتجميع: INNER و LEFT و GROUP BY و HAVING و window functions
- 36JSONB في PostgreSQL: إمتى تستخدمه، وإزاي تسأله وتعمله index
- 37الـ Indexes من جُوّه: B-tree و GIN و BRIN
- 38Partial و Covering indexes: indexes أصغر وأسرع
- 39الـ Pagination بالـ Keyset: ليه OFFSET بيبطّأ وبيكرر
- 40Full Text Search في Postgres: بحث بالكلمات، بالعربي والإنجليزي
- 41EXPLAIN ANALYZE والـ Planner: اقرا الـ plan وصلّح الـ query
- 42الـ Transactions بـ node-postgres: كله أو ولا حاجة
- 43الـ MVCC ومستويات الـ Isolation: transactions كتير في نفس الوقت
- 44الـ Locks والـ Deadlocks: مين مستني مين
- 45SKIP LOCKED و Job Leasing: Postgres كـ job queue
- 46Row Level Security والـ Multi-tenancy: كل عميل يشوف بياناته بس
- 47Migrations من غير downtime: expand و backfill و contract
- 48الـ Replication والـ Read Replicas: نسخ حية من الـ database
- 49الـ Connection Pooling و PgBouncer: connections أقل، requests أكتر
- 50الـ Partitioning والـ VACUUM والمراقبة: Postgres على المدى الطويل
- 51Redis وأنواع البيانات: strings و hashes و lists و sets و sorted sets
- 52Cache-aside والـ TTL: Redis قدام الـ database
- 53الـ Cache invalidation والـ Stampede: لما الـ key الشهير يموت
- 54Lua والـ Atomicity في Redis: كام أمر كأنهم أمر واحد
- 55الـ Rate Limiting في Redis: fixed window و sliding window و token bucket
- 56الـ Sessions في Redis: تسجيل دخول تقدر تلغيه في أي وقت
- 57Redis Streams والـ Consumer Groups: log بيتوزع على workers
- 58Pub/Sub و BullMQ: إشارات بين الـ instances، وطوابير شغل محترمة
- 59Redis لما يقع: الـ persistence (RDB و AOF) والـ replication
- 60Redis في الإنتاج: الـ ACLs والـ Cluster والمراقبة
- 61تخزين الـ Passwords بـ Argon2id: صح من أول مرة
- 62Session Fixation والـ Rotation: id جديد مع كل صلاحية جديدة
- 63الـ Cookie Flags والـ CSRF: المتصفح بيبعت الـ cookie، فلازم تتأكد مين اللي طلب
- 64الـ XSS والـ Content Security Policy: ما تخليش النص يتحوّل لـ code
- 65الـ OAuth و OpenID Connect و PKCE: login بـ Google من غير ما تشوف الـ password
- 66التأكد من الـ JWT صح: التوقيع والـ claims والـ scopes
- 67الـ BOLA و BFLA: الـ token السليم مش معناه إن الطلب مسموح
- 68الـ RBAC و ABAC والملكية: policy واحدة بدل ifs متفرقة
- 69الـ MFA والـ recovery: التاني factor اللي بجد
- 70الـ Passkeys و WebAuthn: login من غير password وضد الـ phishing
- 71الـ Threat Modeling بـ STRIDE: فكّر زي المهاجم قبل ما تكتب code
- 72الـ SQL Injection والـ Parameterization: الـ data ما تبقاش code
- 73الـ SSRF والـ Egress: ما تخليش الـ server بتاعك يزور الشبكة الداخلية
- 74الـ Security Headers والـ TLS: كل response يقول للـ browser يحمي المستخدم إزاي
- 75توقيع الـ Webhooks والـ Replay: الـ webhook ده جاي فعلًا من Stripe؟
- 76تغيير الأسرار والـ HMAC: إزاي تغيّر مفتاح من غير ما كل حاجة تقع
- 77سلسلة التوريد والـ SBOM: إنت واثق في ألف package ما تعرفهمش؟
- 78الـ Audit والـ Privacy Logs: سجّل مين عمل إيه، من غير ما تسجّل الأسرار
- 79اختبار الأمان و OWASP ASVS: الأمن جُوّه الـ CI، مش audit آخر السنة
- 80الـ Prototype Pollution والـ Resource DoS: request واحد يخلّي الكل admin أو يوقّع الـ server
- 81الـ SLI والـ SLO والـ Error Budget: إمتى تصحّي الـ on-call وإمتى تسيبه نايم
- 82الـ Event Loop Delay والـ ELU: لما Node نفسه يتخنق
- 83الـ Worker Threads والـ CPU Profiling: الشغل التقيل بره الـ event loop، والوقت راح فين
- 84الـ Memory والـ GC والـ Leaks: الـ process اللي بيكبر لحد ما يقع
- 85الـ N+1 Queries والـ DataLoader: صفحة واحدة بمِيّتين query
- 86الـ HTTP Caching والـ Compression: أسرع request هو اللي ما بيوصلش
- 87الـ Load Testing والـ Soak Testing: الـ server بيستحمل قد إيه بجد
- 88الـ Concurrency Limits والـ Bulkheads: dependency بطيئة ما توقعش الـ app كله
- 89الـ Timeouts والـ Retries والـ Circuit Breakers: إزاي تتعامل مع dependency بطيئة أو واقعة
- 90OpenTelemetry والـ Distributed Tracing: الـ request راح فين وضاع وقته فين
- 91Docker Layers والـ Build Context: ليه الـ build بياخد دقيقة وفيه الـ .env بتاعك
- 92Multi-stage Builds و Non-root: image صغيرة ومن غير root
- 93Docker Compose والـ Networks: الـ services بتلاقي بعض إزاي
- 94Docker Volumes: الـ data بتتحفَظ فين
- 95Healthcheck و Restart و Resource Limits: running مش معناها شغّال
- 96BuildKit والـ Build Secrets: الـ token ما يدخلش الـ image
- 97Reverse Proxy و TLS: اللي واقف قدام الـ app
- 98Multi-arch Images والـ Native Modules: اشتغلت عندي
- 99CI/CD و Rollback و Canary: الـ deploy يبقى ممل
- 100Kubernetes من الأول: Pods و Deployments و Services و Probes
- 101Modular Monolith و DDD: حدود واضحة من غير network
- 102Transactional Outbox و Inbox: الـ event ما يضيعش وما يتنفذش مرتين
- 103Sagas: عملية واحدة على تلات services من غير transaction واحدة
- 104CQRS و Event Sourcing: الـ events هي الحقيقة
- 105الـ Queues والـ Brokers: BullMQ و RabbitMQ و Kafka و SQS
- 106Realtime على أكتر من instance: Redis pub/sub و presence و resume
- 107الـ Object Storage والـ Media: presigned URLs و uploads مباشرةانت هنا
- 108التكامل مع الـ Email والـ Search: retries و webhooks و indexes
- 109GraphQL و gRPC في الإنتاج: contracts و DataLoader و limits و deadlines
- 110Kafka على أكتر من zone و region: الـ replication والـ ISR والـ acks والـ MirrorMaker والـ failover
- 111TypeScript و runtime boundaries: الـ types بتختفي، والـ schemas لأ
- 112ORM ولا SQL: query builders و repositories و migrations و transactions
- 113الـ Serverless من جُوّه: cold starts و freeze و timeouts و connections
- 114الـ Testing Pyramid: unit و integration و e2e على Postgres حقيقية
- 115Contract Testing و Property-Based Testing بـ fast-check
- 116الـ Payments: idempotency و webhooks و refunds و reconciliation
- 117الـ Timezones والـ Internationalization: UTC و DST والـ locales والـ plurals والفلوس
- 118الـ Privacy والـ Data Retention: consent و export و erasure و crypto shredding
- 119الـ ADRs والـ Feature Flags: القرارات المكتوبة والـ rollout من غير deploy
- 120Multi-region active-active: حل الـ conflicts بـ HLC و CRDTs
- 121Readiness و Liveness و Startup Probes: الـ pod جاهز ولا عايش؟
- 122Incident Runbooks: من الـ alert للـ postmortem