# مواصفة OpenAPI في DaloyJS

> هذه ترجمة للصفحة الأصلية بالإنجليزية. عند وجود أي اختلاف، تُعتمد [النسخة الإنجليزية](/docs/openapi).

يُصدر DaloyJS مستند **OpenAPI 3.1** نظيفًا مباشرةً من تعريفات مساراتك، دون إضافات ودون مُزخرِفات (decorators) منفصلة. يتشارك التحقق والأنواع والمواصفة مصدرًا واحدًا للحقيقة.

**Diagram: عقد واحد وأربعة مخرجات**

- **تعريف المسار** (source, مصدر واحد للحقيقة) - app.get(path, { request, responses }, handler)
- **التحقق من الطلب والاستجابة** (وقت التشغيل) - Zod · Valibot · ArkType
- **مستند OpenAPI 3.1** (المواصفة) - GET /openapi.json · /openapi.yaml
- **واجهة التوثيق** (للبشر) - Scalar · Swagger UI · Redoc
- **SDK عميل مكتوب الأنواع** (للمستهلكين) - توليد الشيفرة مع Hey API

تكتب المسار مرة واحدة. ثم يُشتق منه التحقق، ومواصفة OpenAPI، والتوثيق التفاعلي، والعميل مكتوب الأنواع، لذا لا يمكن أن تخرج عن التزامن أبدًا.

## سطر واحد: تركيب /docs و/openapi.json و/openapi.yaml تلقائيًا

على طريقة FastAPI. مرّر `docs: "auto"` (أو `docs: true`) إلى منشئ `App`، فيسجّل DaloyJS لك `GET /openapi.json` + `GET /openapi.yaml` (المواصفة الحية بالصيغتين) و`GET /docs` (واجهة مرجع API مع Scalar).

```ts
import { App } from "@daloyjs/core";

const app = new App({
  openapi: {
    info: { title: "My API", version: "1.0.0" },
    servers: [{ url: "https://api.example.com" }],
    securitySchemes: { bearer: { type: "http", scheme: "bearer" } },
  },
  docs: "auto", // يركّب GET /docs وGET /openapi.json وGET /openapi.yaml خارج الإنتاج
});
```

لا يركّب `docs: "auto"` المسارات إلا عندما لا يكون التطبيق في الإنتاج (وهذا هو الافتراضي في قوالب `create-daloy`، فلا تنشر الواجهة البرمجية المنشورة مخططها)، أو اتركه معطلًا (الافتراضي) وركّب المسارات يدويًا بالدوال المساعدة أدناه. خصّص المسارات والواجهة والوسوم عبر صيغة الكائن. ومنذ 1.5.5 تتصرف صيغة الكائن مثل `"auto"` ما لم تضبط `enabled`، لذا فإن تخصيص الواجهة لا ينشر خريطة مساراتك في الإنتاج عن طريق الخطأ أبدًا؛ اضبط `enabled: true` (أو `docs: true`) لواجهة برمجية عامة ينبغي أن تقدّم توثيقها في الإنتاج:

```ts
new App({
  openapi: { info: { title: "My API", version: "1.0.0" } },
  docs: {
    path: "/reference",              // الافتراضي: "/docs"
    openapiPath: "/spec.json",       // الافتراضي: "/openapi.json"
    openapiYamlPath: "/spec.yaml",   // الافتراضي: "/openapi.yaml"؛ وfalse يعطّله
    ui: "scalar",                     // "scalar" (الافتراضي) | "swagger" | "redoc"
    scalar: {
      theme: "kepler",
      customCss: ":root { --scalar-color-accent: #2563eb; }",
      hideTestRequestButton: true,
    },
    tags: ["Docs"],                   // الافتراضي: ["Docs"]، مرّر [] للحذف
    enabled: "auto",                  // الافتراضي "auto" (معطّل في الإنتاج)؛ true | false
  },
});
```

> **الأمان:** المواصفة المنشورة خريطة كاملة لسطح الهجوم لديك: كل مسار ومعامل ومخطط مصادقة وشكل خطأ. مع `docs: true` أو `enabled: true` في الإنتاج، يسجّل DaloyJS الحدث `docs.public_in_production` ويقدّم المواصفة لكل من يستطيع الوصول إليها. هذا صحيح لواجهة برمجية عامة. أما لغير ذلك، فأبقِ `"auto"`، أو ضع التوثيق خلف مصادقة (فـ `bearerAuth()` على مستوى التطبيق دون `except()` لـ `/docs` و`/openapi.json` يغطيهما). علّم مسارات الإدارة والمهام المجدولة وتصحيح الأخطاء بـ `internal: true`، فتُستبعد حينها من المستند المولَّد وتجيب بـ `404` على حركة الشبكة، بينما يظل بإمكان `app.inject()` استدعاؤها. راجع [Secure admin panels](/docs/security/admin-panels) (بالإنجليزية).

## اختر واجهة: Scalar أو Swagger UI أو Redoc

اضبط `ui` على `"scalar"` (الافتراضي) أو `"swagger"` أو `"redoc"`. تعرض الثلاث المواصفة الحية نفسها، وتُركَّب على المسارات نفسها، وتأتي بسياسة CSP الصارمة نفسها وبموارد مستضافة على CDN، لذا فالتبديل تغيير بكلمة واحدة. يتضمن Scalar وSwagger UI وحدة طلبات للمطورين للمسارات المحمية. أما Redoc فواجهة مرجعية للقراءة فقط: تعرض متطلبات الأمان، لكنها لا تأتي بوحدة *Try it* مدمجة.

```ts
new App({
  openapi: { info: { title: "My API", version: "1.0.0" } },
  docs: {
    ui: "swagger",
    swagger: {
      docExpansion: "none",
      displayRequestDuration: true,
    },
  },
});
```

يحتفظ Swagger UI افتراضيًا بالتفويضات المدخلة في نافذة *Authorize* عبر إعادة التحميل. ويفعل ذلك بتخزين الرمز الملصق في `localStorage` الخاص بالمتصفح، وكذلك يفعل Scalar مع `persistAuth: true`. يمكن لأي سكربت يعمل على أصل التوثيق قراءته، ويبقى حتى بعد إغلاق التبويب. الصق رموز اختبار قصيرة العمر وبأقل صلاحيات فقط، ولا تلصق أبدًا بيانات اعتماد الإنتاج أو الإدارة، وقدّم التوثيق من أصل منفصل عن أي تطبيق ويب خاص بك. مرّر `swagger: { persistAuthorization: false }` لإبقاء الرموز في الذاكرة فقط. يختار Scalar تلقائيًا أول مخطط أمان مُعرَّف في OpenAPI، فيكون نموذج المصادقة جاهزًا ليلصق المطورون رمز bearer أو مفتاح API. إن أردت افتراضيًا مختلفًا في Scalar، فاضبطه صراحةً:

```ts
new App({
  openapi: {
    info: { title: "My API", version: "1.0.0" },
    securitySchemes: {
      bearer: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
      apiKey: { type: "apiKey", in: "header", name: "x-api-key" },
    },
  },
  docs: {
    scalar: {
      persistAuth: true,
      authentication: { preferredSecurityScheme: "bearer" },
    },
  },
});
```

إذا احتاج توثيقك إلى نقطة دخول ظاهرة لتسجيل الدخول، فأضف `docs.auth`. يُعرض الزر نفسه في Scalar وSwagger UI وRedoc. وجّهه إلى نموذج تسجيل دخول محلي، أو مسار Better Auth، أو عنوان تسجيل دخول Clerk، أو عنوان تفويض OIDC من Auth0 أو Okta أو Microsoft Entra ID أو Cognito أو Keycloak أو أي مزوّد آخر:

```ts
new App({
  openapi: {
    info: { title: "My API", version: "1.0.0" },
    securitySchemes: {
      oidc: {
        type: "oauth2",
        flows: {
          authorizationCode: {
            authorizationUrl: "https://login.example.com/oauth2/v2.0/authorize",
            tokenUrl: "https://login.example.com/oauth2/v2.0/token",
            scopes: { "api.read": "Read API data" },
          },
        },
      },
    },
  },
  docs: {
    ui: "redoc", // يعمل أيضًا مع "scalar" و"swagger"
    auth: {
      loginUrl: "/login", // أو عنوان التفويض/تسجيل الدخول لدى مزوّدك
      label: "Sign in",
      target: "popup",
    },
  },
});
```

يُمرَّر الكائن `redoc` كما هو إلى `Redoc.init(specUrl, configuration, element)`. ولأن Redoc يبني فهرس البحث في Web Worker من نوع `blob:`، فإن المسار `/docs` المركّب تلقائيًا يوسّع سياسة CSP لتلك الصفحة بـ `worker-src 'self' blob:` في حالة `ui: "redoc"` فقط. ويحتفظ Scalar وSwagger UI بالسياسة الأضيق. ويُتجاهل الخيار `scalar` ما لم يكن `ui` هو `"scalar"`، وكذلك الحال مع `swagger` و`redoc`.

يُمرَّر الكائن `scalar` إلى واجهة HTML الخاصة بـ Scalar كإعدادات JSON، بينما يُبقي Daloy المسار الحي `openapiPath` مصدرًا. استخدمه للسمات، وCSS المخصص، والتخطيط، وافتراضيات المصادقة، وظهور العملاء، دون نسخ HTML الخاص بالتوثيق.

## متقدم: توليد المواصفة يدويًا

تحتاج إلى كائن المواصفة الخام (لتوليد الشيفرة، أو اختبارات العقود، أو مسار مخصص)؟ استدعِ `generateOpenAPI(app, options)` مباشرةً:

```ts
import { generateOpenAPI } from "@daloyjs/core/openapi";

const doc = generateOpenAPI(app, {
  info: { title: "My API", version: "1.0.0" },
  servers: [{ url: "https://api.example.com" }],
  securitySchemes: { bearer: { type: "http", scheme: "bearer" } },
});

console.log(JSON.stringify(doc, null, 2));
```

## متقدم: تقديم التوثيق من مسارك الخاص

مسار التوثيق المكتوب يدويًا مسار عادي، لذا لا ينطبق عليه تقييد `docs: "auto"`. قرّر بنفسك هل ينبغي أن يوجد في الإنتاج وهل يحتاج إلى مصادقة. وينطبق الأمر نفسه على أي مسار `/openapi.json` تقدّمه يدويًا باستخدام `generateOpenAPI()`:

```ts
import { swaggerUiHtml, scalarHtml, redocHtml, htmlResponse } from "@daloyjs/core/docs";

// لا يغطيه `docs: "auto"`، لذا قيّده بنفسك.
if (process.env.NODE_ENV !== "production") {
  app.get(
    "/docs",
    {
      operationId: "docs",
      responses: { 200: { description: "API reference" } },
    },
    async () => {
      const html = scalarHtml({
        specUrl: "/openapi.json",
        title: "My API",
        configuration: { theme: "kepler" },
      });
      const res = htmlResponse(html);
      return { status: 200, body: await res.text(), headers: Object.fromEntries(res.headers) };
    },
  );
}
```

تعيد كل من `swaggerUiHtml` و`scalarHtml` و`redocHtml` صفحات HTML مكتفية ذاتيًا تحمّل مواردها من jsDelivr مع سياسة CSP صارمة لا تسمح إلا بذلك الأصل. عندما تكتب المسار يدويًا باستخدام `redocHtml`، مرّر `allowBlobWorkers: true` إلى `htmlResponse` (أو `docsContentSecurityPolicy`) حتى تسمح سياسة CSP بعامل البحث `blob:` الخاص بـ Redoc:

```ts
import { redocHtml, htmlResponse } from "@daloyjs/core/docs";

const res = htmlResponse(
  redocHtml({ specUrl: "/openapi.json", title: "My API", configuration: { hideDownloadButtons: true } }),
  { allowBlobWorkers: true },
);
```

إن أردت اختبار تجربة التوثيق مقابل عقد أكبر بكثير، فراجع [عرض REST الوهمي الكبير](/docs/tutorials/fake-rest-api) (بالإنجليزية). فهو مقياس أفضل من مثال CRUD بسيط عندما تحتاج إلى التحقق من البحث والتجميع وأداء العرض.

## الحفظ على القرص لتوليد الشيفرة

```ts
// scripts/dump-openapi.ts
import { writeFile, mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import { generateOpenAPI } from "@daloyjs/core/openapi";
import { buildApp } from "../src/build-app.js";

const app = buildApp();
const out = "./generated/openapi.json";
await mkdir(dirname(out), { recursive: true });
await writeFile(out, JSON.stringify(generateOpenAPI(app, {
  info: { title: "My API", version: "1.0.0" },
}), null, 2));
console.log(`wrote ${out}`);
```

```json
// package.json
"scripts": {
  "gen:openapi": "node scripts/dump-openapi.ts"
}
```

## ما الذي يُصدَر

- `operationId` واحد لكل مسار، والمكررات ترمي خطأً عند التسجيل.
- تُوحَّد معاملات المسار `:id` إلى `{id}`.
- تُحوَّل أجسام المخططات عبر `schema.toJSONSchema?.()` عندما يكون مدعومًا، أو عبر بديل هيكلي.
- مكوّن قابل لإعادة الاستخدام `components.schemas.Problem` لأخطاء RFC 9457.
- `tags` و`summary` و`description`، و`description` لكل رمز حالة.
- تُحذف المسارات المعلّمة بـ `internal: true` ما لم تمرّر `includeInternal: true` (مثلًا لعقد داخلي فقط لأدواتك الخاصة). لا تنشر ذلك المستند أبدًا.

## Webhooks

يسمح OpenAPI 3.1 للمنتِج بنشر **webhooks في المستوى الأعلى**، وهي عمليات يُتوقع من المستهلك تنفيذها. مرّر `webhooks` إلى `generateOpenAPI` فيُصدرها DaloyJS تحت الخريطة `webhooks` في المستوى الأعلى من المستند.

```ts
import { generateOpenAPI } from "@daloyjs/core/openapi";

const doc = generateOpenAPI(app, {
  info: { title: "Books", version: "1.0.0" },
  webhooks: {
    bookCreated: {
      method: "POST",
      operationId: "onBookCreated",
      summary: "Fires when a book is created",
      tags: ["Webhooks"],
      request: { body: z.object({ id: z.string(), title: z.string() }) },
      responses: { 200: { description: "Acknowledged" } },
      auth: { scheme: "bearer", scopes: ["webhook:receive"] },
    },
  },
});
```

الخريطة `webhooks` *تصف* العملية فقط؛ فهي لا تسلّم شيئًا ولا تؤمّنه. في عمليات التسليم الحقيقية، وقّع كل حمولة حتى يتمكن المستقبلون من التحقق من المصدر والسلامة معًا، وأضف طابعًا زمنيًا حتى يتمكنوا من رفض إعادة الإرسال. ورمز bearer ثابت وحده لا يثبت أيًا منهما. يوقّع [`createWebhookSender()`](/docs/webhook-delivery) (بالإنجليزية) كل طلب صادر ويضيف إليه طابعًا زمنيًا ويعيد محاولته ويمرره عبر `fetchGuard()`. ويتحقق المستقبلون باستخدام `verifyWebhookSignature()`. وثّق ترويسات التوقيع في `description` الخاص بالـ webhook.

## الاستدعاءات الراجعة

تصف **الاستدعاءات الراجعة (callbacks)** طلبات خارج النطاق قد تُطلقها عملية ما نحو المستهلك (مثل نقطة نهاية اشتراك ترسل لاحقًا طلب POST إلى العنوان الذي قدّمه المستدعي). أرفق خريطة `callbacks` مباشرةً بمسار أو webhook.

```ts
import { fetchGuard } from "@daloyjs/core";

// كل طلب إلى عنوان قدّمه المستدعي يمر عبر حارس SSRF.
const safeFetch = fetchGuard();

app.post(
  "/subscribe",
  {
    operationId: "subscribe",
    request: {
      body: z
        .object({
          // https فقط: يرفض file: وgopher: وjavascript: وhttp: العادي
          callbackUrl: z.url({ protocol: /^https$/ }),
        })
        .strict(),
    },
    responses: { 201: { description: "Subscribed" } },
    callbacks: {
      onEvent: {
        "{$request.body#/callbackUrl}": {
          method: "POST",
          operationId: "onEventCallback",
          request: { body: z.object({ id: z.string() }) },
          responses: {
            200: { description: "ack" },
            410: { description: "gone" },
          },
        },
      },
    },
  },
  async () => ({ status: 201, body: undefined }),
);
```

> **الأمان:** عنوان الاستدعاء الراجع الذي يقدّمه المستدعي نقطة تصريف لتزوير الطلبات من جهة الخادم (SSRF). التحقق من المخطط لا يكفي. فـ `z.url()` وحده يقبل `http://169.254.169.254/` (بيانات السحابة الوصفية)، و`http://localhost:6379/`، وعناوين `file:`/`gopher:`، وحتى `z.url({ protocol: /^https$/ })` يظل يقبل `https://169.254.169.254/` أو اسم مضيف يُحل إلى عنوان خاص. أرسل كل استدعاء راجع عبر `safeFetch` من [`fetchGuard()`](/docs/security/fetch-guard) (بالإنجليزية)، الذي يفحص العنوان المحلول وكل قفزة إعادة توجيه. والأفضل من ذلك، سلّم باستخدام [`createWebhookSender()`](/docs/webhook-delivery) (بالإنجليزية)، الذي يوقّع الحمولة أيضًا. وفكّر في التحقق من ملكية العنوان، مثلًا بطلب تحدٍّ، قبل تخزينه.

يرتبط كل اسم استدعاء راجع بمفتاح تعبير وقت تشغيل واحد أو أكثر (مثل `"{$request.body#/callbackUrl}"`)، ويرتبط كل منها بعملية واحدة أو أكثر مفهرسة بطريقة HTTP. وتُتخطى الخرائط والمصفوفات الفارغة، فتمرير استدعاء راجع فارغ لا ينتج أبدًا مواصفة مشوّهة.

## الاتحادات المميَّزة

الحقل `discriminator` في OpenAPI 3.1 هو الطريقة المعتمدة لوصف الاتحادات الموسومة. يوفّر DaloyJS دالتين مساعدتين من `@daloyjs/core/openapi` (ومن الحزمة الجذرية):

- `discriminator(propertyName, mapping?)`: باني المواصفة المجرد. استخدمه عندما يكون لديك JSON Schema مكتوب يدويًا وتريد إرفاق الحقل بشكل نظيف.
- `discriminatedUnion(propertyName, variants, opts?)`: غلاف متوافق مع Standard Schema *يتحقق* في وقت التشغيل (موزّعًا حسب قيمة المميِّز) *ويعرض* أيضًا `.toJSONSchema()` حتى يُصدر مولّد OpenAPI البنية `{ oneOf, discriminator }` تلقائيًا.

```ts
import { z } from "zod";
import { discriminatedUnion } from "@daloyjs/core";

// يرفض .strict() المفاتيح غير المعروفة (مثل `isAdmin` محقون) بالرمز 422
// بدلًا من حذفها بصمت.
const Cat = z.object({ kind: z.literal("cat"), meow: z.boolean() }).strict();
const Dog = z.object({ kind: z.literal("dog"), bark: z.boolean() }).strict();

const Animal = discriminatedUnion(
  "kind",
  { cat: Cat, dog: Dog },
  { mapping: { cat: "#/components/schemas/Cat", dog: "#/components/schemas/Dog" } },
);

app.post(
  "/animals",
  {
    operationId: "createAnimal",
    request: { body: Animal },
    responses: { 201: { description: "ok", body: Animal } },
  },
  async ({ body }) => ({ status: 201, body }),
);
```

في وقت التشغيل، يرفض الغلاف غير الكائنات، والمميِّزات المفقودة أو غير النصية، وقيم المميِّز غير المعروفة (بما فيها الأسماء الموروثة مثل `__proto__` و`constructor` و`toString`) مع مشكلة Standard Schema واضحة، ثم يُحيل كل ما عدا ذلك إلى أداة التحقق الخاصة بالمتغير المطابق.

آخر تحديث: 30 أكتوبر 2026

---

Source: https://daloyjs.dev/ar/docs/openapi