ข้ามไปยังเนื้อหา
getnextpdf.com

ส่งมอบ PDF ที่สร้างขึ้นผ่าน URL ที่ลงนามและหมดอายุ

คุณสร้างไฟล์ Portable Document Format (PDF) และต้องส่งมอบให้ไคลเอนต์ เส้นทางที่ง่ายที่สุดคือสตรีมไบต์ตรงผ่านคอนโทรลเลอร์ แต่นั่นผูก worker ของแอปพลิเคชันไว้ตลอดการดาวน์โหลด รันทราฟฟิกผ่านเซิร์ฟเวอร์ของคุณ และเปิดเผยไฟล์ให้ทุกคนที่เข้าถึงเส้นทางนั้นได้ รูปแบบการส่งมอบในหน้านี้ทำตรงกันข้าม: สร้าง PDF, เก็บไบต์ไว้ใน object storage แล้วคืน Uniform Resource Locator (URL) ที่ลงนามและอายุสั้นซึ่งไคลเอนต์ดึงตรงจาก storage แอปของคุณส่งคืน payload JavaScript Object Notation (JSON) เล็ก ๆ พร้อม URL ส่วน storage ให้บริการไบต์

ฝั่ง NextPDF คือการเรียกหนึ่งครั้ง: getPdfData() บนเอกสารคืนไบนารี PDF ดิบเป็นสตริง ทุกอย่างหลังจากนั้น — การวางอ็อบเจกต์และการสร้างลิงก์ที่ลงนามและจำกัดเวลา — เป็นงานของเฟรมเวิร์กหรือผู้ให้บริการคลาวด์ของคุณ primitive สำหรับการลงนามเป็น API จริงที่มีเอกสารกำกับ: Laravel Storage::temporaryUrl() และ URL::temporarySignedRoute(), Symfony UriSigner และการดำเนินการ presigned-URL ของ Amazon Simple Storage Service (S3) หรือ Google Cloud Storage (GCS) ใน software development kit (SDK) ของพวกเขา NextPDF ไม่ได้กำหนดตัวช่วย URL ของตัวเอง อย่ามองหามัน

ตรวจสอบชิ้นส่วนเหล่านี้ก่อน:

  • ติดตั้ง core ของ NextPDF แล้ว และคุณสร้างเอกสารได้
  • คุณมี object storage ที่เฟรมเวิร์กลงนามให้ได้: bucket S3 หรือที่เข้ากันได้กับ S3, bucket GCS หรือ Laravel disk ที่ไดรเวอร์รองรับ temporary URL
  • credential อยู่ในตัวแปรสภาพแวดล้อมหรือ secret manager ไม่เคยอยู่ในไฟล์กำหนดค่าที่ commit

นี่คือคู่มือวิธีทำ โดยถือว่าคุณรู้วิธีกำหนดเส้นทางคำขอไปยังคอนโทรลเลอร์อยู่แล้ว สำหรับการคืนไบต์โดยตรงแทน โปรดอ่านส่งคืน PDF ที่สร้างจากคอนโทรลเลอร์

รูปแบบนี้มีสามขั้นตอน และมีเพียงขั้นแรกที่แตะ NextPDF:

  1. สร้าง สร้างเอกสารแล้วเรียก getPdfData() เพื่อรับไบต์
  2. เก็บ เขียนไบต์เหล่านั้นไปยังคีย์ของ object-storage (reports/2026/r-42.pdf)
  3. ลงนาม ขอ URL ที่ลงนามไปยังคีย์นั้นจากเฟรมเวิร์กหรือ cloud SDK พร้อมเวลาหมดอายุ แล้วคืน URL ให้ไคลเอนต์

เหตุใดจึงเก็บและลงนามแทนการ proxy ไบต์:

  • ลดภาระแบนด์วิดท์ object storage (หรือ edge ของ content-delivery-network) ให้บริการการดาวน์โหลด worker ของแอปพลิเคชันคืน JSON ไม่กี่ร้อยไบต์และว่างทันที แทนที่จะถูกจองตลอดความยาวของการถ่ายโอนหลายเมกะไบต์
  • จำกัดขอบเขตการเข้าถึง URL ที่ลงนามให้สิทธิ์เข้าถึง อ็อบเจกต์เดียว ในช่วง เวลาที่กำหนด ตัว bucket เองยังคงเป็นส่วนตัว ไม่มีเส้นทางสาธารณะให้ brute force และไม่มีการให้สิทธิ์อ่าน bucket แบบกว้าง
  • เวลาหมดอายุ ลายเซ็นฝัง timestamp หมดอายุไว้ หลังจากผ่านไปแล้ว ลิงก์จะตาย URL ที่รั่วไหลจะหยุดทำงานเอง ซึ่งจำกัดรัศมีความเสียหายของการแชร์โดยไม่ตั้งใจ

มีโมเดลการลงนามสองแบบที่แตกต่างกัน และต่างกันที่ สิ่งใด ที่ถูกลงนาม:

  • presigned URL ของ object-storage (S3, GCS หรือ temporaryUrl() ของ Laravel บน disk S3/GCS) ชี้ไปยัง อ็อบเจกต์ของ storage โดยตรง การดาวน์โหลดไม่ไปถึงแอปของคุณเลย
  • เส้นทางที่ลงนามของแอปพลิเคชัน (Laravel URL::temporarySignedRoute(), Symfony UriSigner) ชี้ไปยัง เส้นทางของคุณเอง คำขอยังคงเข้าถึงแอปของคุณ ซึ่งตรวจสอบลายเซ็น แล้วสตรีมหรือ redirect ไปยังอ็อบเจกต์ ใช้สิ่งเหล่านี้เมื่อคุณต้องรันการอนุญาต การบันทึก หรือการนับในแต่ละการดาวน์โหลด หรือเมื่อ storage ของคุณ presign ไม่ได้
ConcernNextPDFLaravelSymfony
รับไบต์ PDFNextPDF\Core\Document::getPdfData(): stringsamesame
เก็บไบต์Storage::disk($d)->put($key, $bytes)Filesystem::dumpFile($path, $bytes) or Flysystem write()
presigned storage URLStorage::disk($d)->temporaryUrl($key, $expiresAt)AWS/GCS SDK presigner (below)
เส้นทางแอปที่ลงนามURL::temporarySignedRoute($name, $expiresAt, $params)UriSigner::sign($url)
ตรวจสอบเส้นทางแอปที่ลงนามsigned route middleware / $request->hasValidSignature()UriSigner::check() / checkRequest()

การเรียกเอนจิน NextPDF เพียงอย่างเดียวที่รูปแบบการส่งมอบนี้ต้องการคือ getPdfData() ส่วนตัวเอกสารเองถูกสร้างด้วยวิธีที่แอปของคุณสร้างเอกสารอยู่แล้ว (เช่น DocumentFactoryInterface ที่ฉีดเข้า / PdfFactory ของ Symfony) getPdfData() ถูกประกาศใน trait HasOutput บน NextPDF\Core\Document มันเรียกตัวเขียนหนึ่งครั้งและคืน PDF ทั้งฉบับเป็นสตริง พี่น้องของมัน save(string $path): void เขียนไบต์เดียวกันลงดิสก์ผ่านตัวเขียนแบบ atomic ให้ใช้มันก็ต่อเมื่อ storage ของคุณเป็น path ไฟล์ระบบในเครื่องจริง สำหรับ object storage ให้เลือกใช้ getPdfData() แล้วให้ storage SDK เป็นเจ้าของการถ่ายโอน

เอกสารถูกสร้างเมื่อคุณเรียก getPdfData() (หรือ save()) และ build ไม่เป็น idempotent เรียกมัน ครั้งเดียว ต่อเอกสาร เก็บสตริงไว้ แล้วนำสตริงนั้นกลับมาใช้ซ้ำทั้งสำหรับการอัปโหลดและขนาดหรือ checksum ใดที่คุณคำนวณ

นามธรรมไฟล์ระบบของ Laravel ลงนามให้คุณ บน disk S3 (หรือที่เข้ากันได้กับ S3) Storage::temporaryUrl() คืน presigned URL ตรงไปยังอ็อบเจกต์ ไคลเอนต์ดาวน์โหลดจาก storage ส่วนแอ็กชันของคุณคืนเฉพาะ JSON

app/Http/Controllers/ReportDeliveryController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Storage;
use NextPDF\Contracts\DocumentFactoryInterface;
use Psr\Log\LoggerInterface;
use Throwable;
final class ReportDeliveryController extends Controller
{
public function __construct(
private readonly DocumentFactoryInterface $documents,
private readonly LoggerInterface $logger,
) {}
public function store(int $reportId): JsonResponse
{
try {
// 1. Generate. Build once; getPdfData() returns the raw bytes.
$document = $this->documents->create();
$document->addPage();
$document->cell(0, 10, "Report #{$reportId}", newLine: true);
$bytes = $document->getPdfData();
// 2. Store under a non-guessable key on a private disk.
$key = sprintf('reports/%d/%s.pdf', $reportId, bin2hex(random_bytes(16)));
Storage::disk('s3')->put($key, $bytes, ['visibility' => 'private']);
// 3. Sign. A presigned URL straight to the object, valid 10 minutes.
$url = Storage::disk('s3')->temporaryUrl($key, now()->addMinutes(10));
return new JsonResponse(['download_url' => $url], 201);
} catch (Throwable $exception) {
// Log the class, never the message or trace, so detail does not leak.
$this->logger->error('Report PDF delivery failed', [
'report_id' => $reportId,
'exception' => $exception::class,
]);
return new JsonResponse(['error' => 'Could not prepare the report.'], 500);
}
}
}

disk ต้องเป็นตัวที่ไดรเวอร์รองรับ temporary URL — ไดรเวอร์ s3 ที่มากับชุดรองรับ การเรียก temporaryUrl() บนไดรเวอร์ local จะ throw เว้นแต่คุณลงทะเบียน generator ให้มัน เพราะ disk ในเครื่องไม่มีอะไรให้ presign

เมื่อคุณต้องการให้การดาวน์โหลดอยู่บนเส้นทางของคุณเอง — เพื่อรันการอนุญาตต่อคำขอหรือบันทึกการเข้าถึงแต่ละครั้ง — ให้ลงนามเส้นทางแทนด้วย URL::temporarySignedRoute() middleware signed ของเส้นทางจะปฏิเสธลิงก์ที่ถูกแก้ไขหรือหมดอายุก่อนแอ็กชันของคุณจะรัน

routes/web.php
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
// Mint the link elsewhere:
// URL::temporarySignedRoute('reports.download', now()->addMinutes(10),
// ['report' => $reportId]);
Route::get('/reports/{report}/download', DownloadReportController::class)
->name('reports.download')
->middleware('signed');

Symfony ไม่มี storage facade แบบ Laravel ดังนั้นคุณลงนาม เส้นทางของคุณเอง ด้วย Symfony\Component\HttpFoundation\UriSigner ของเฟรมเวิร์ก แล้วให้เส้นทางนั้น redirect ไปยัง presigned storage URL (หรือสตรีมอ็อบเจกต์) UriSigner::sign() ต่อท้ายแฮชที่ใช้คีย์ ส่วน checkRequest() ปฏิเสธลิงก์ที่ถูกแก้ไข เพื่อให้ตัวอย่างพกพาข้ามเวอร์ชัน Symfony ได้ ให้ฝังพารามิเตอร์ query expires ของคุณเอง (Unix timestamp ที่ห่างออกไปไม่กี่นาที) ก่อน ลงนาม แล้วตรวจสอบพารามิเตอร์นั้นด้วยตัวเองในเส้นทาง download หลังลายเซ็นผ่าน วิธีนี้ทำงานได้บนทุกเวอร์ชัน Symfony เพราะ UriSigner::sign(string $uri) รับเฉพาะ URL

src/Controller/ReportDeliveryController.php
<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Service\PdfFactory;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class ReportDeliveryController
{
// 1 + 2 + sign: build, store, and return a signed URL to our own route.
#[Route('/reports/{reportId}', name: 'report_prepare', methods: ['POST'])]
public function prepare(
int $reportId,
PdfFactory $pdf,
UriSigner $signer,
UrlGeneratorInterface $urls,
ReportStorage $storage, // your storage adapter
): JsonResponse {
$document = $pdf->create();
$document->addPage();
$document->cell(0, 10, "Report #{$reportId}", newLine: true);
$key = $storage->put($reportId, $document->getPdfData());
$url = $urls->generate(
'report_download',
['reportId' => $reportId, 'key' => $key],
UrlGeneratorInterface::ABSOLUTE_URL,
);
// Embed our own expiry (a Unix timestamp 10 minutes out), then sign the
// URL only. UriSigner::sign(string $uri) is portable across all versions.
$url .= (str_contains($url, '?') ? '&' : '?')
. 'expires=' . ((new \DateTimeImmutable('+10 minutes'))->getTimestamp());
return new JsonResponse(['download_url' => $signer->sign($url)]);
}
// verify: the signed route. checkRequest() rejects a tampered link; then we
// enforce the embedded expiry ourselves.
#[Route('/reports/{reportId}/download', name: 'report_download', methods: ['GET'])]
public function download(
Request $request,
UriSigner $signer,
ReportStorage $storage,
): Response {
if (!$signer->checkRequest($request)) {
return new Response('Link invalid.', 403);
}
// Enforce the embedded expiry: reject once the timestamp is in the past.
$expires = (int) $request->query->get('expires');
if ($expires < time()) {
return new Response('Link expired.', 410);
}
// Redirect to a presigned storage URL, or stream the object here.
return new Response('', 302, ['Location' => $storage->presign(
(string) $request->query->get('key'),
)]);
}
}

UriSigner ถูกสร้างด้วย secret (Symfony autowire มันจากพารามิเตอร์ %kernel.secret% / APP_SECRET) ตัวอย่างด้านบนคือเส้นทางที่พกพาได้: UriSigner::sign(string $uri) ลงนามเฉพาะ URL และมีอยู่ในทุกเวอร์ชัน Symfony ดังนั้นเวลาหมดอายุจึงเดินทางเป็นพารามิเตอร์ query expires ของคุณเอง ลายเซ็นครอบคลุมพารามิเตอร์นั้น จึงไม่สามารถถูกแก้ไขได้ — และหลังจาก checkRequest() ผ่าน เส้นทาง download บังคับใช้มันโดยเทียบ timestamp กับเวลาปัจจุบันและคืน 410 Gone เมื่อมันอยู่ในอดีต

บนเวอร์ชัน Symfony ที่ UriSigner::sign() รับอาร์กิวเมนต์ DateTimeInterface สำหรับเวลาหมดอายุ คุณส่งเวลาหมดอายุได้โดยตรง — $signer->sign($url, new \DateTimeImmutable('+10 minutes')) — แล้วให้ checkRequest() ปฏิเสธลิงก์ที่หมดอายุให้คุณ โดยตัดพารามิเตอร์ expires ที่ทำเองและการตรวจสอบของมันออก ยืนยันลายเซ็นของ UriSigner::sign() ใน Symfony ที่ติดตั้งของคุณก่อนพึ่งพามัน รูปแบบที่พกพาได้ด้านบนทำงานได้ไม่ว่าจะกรณีใด

หากคุณลงนามด้วย cloud SDK โดยตรงแทนการผ่าน framework disk รูปร่างจะเหมือนกัน: วางอ็อบเจกต์ แล้วขอให้ SDK presign GET ให้มัน นี่คือ S3 ล้วน (โฟลว์ GCS สะท้อนมัน: รับอ็อบเจกต์ด้วย $bucket->object($key) แล้วเรียก $object->signedUrl($expiresAt, [...]))

store-and-presign.php
<?php
declare(strict_types=1);
use Aws\S3\S3Client;
use NextPDF\Core\Document;
/** @var Document $document Already built by your generation code. */
$bytes = $document->getPdfData(); // NextPDF: the only engine call.
$s3 = new S3Client(['region' => 'eu-central-1', 'version' => 'latest']);
$key = 'reports/' . bin2hex(random_bytes(16)) . '.pdf';
// Store the object privately.
$s3->putObject([
'Bucket' => 'my-private-reports',
'Key' => $key,
'Body' => $bytes,
'ContentType' => 'application/pdf',
]);
// Presign a GET valid for 10 minutes. The returned URI is the signed URL.
$command = $s3->getCommand('GetObject', [
'Bucket' => 'my-private-reports',
'Key' => $key,
]);
$signedUrl = (string) $s3->createPresignedRequest($command, '+10 minutes')->getUri();

สำหรับ GCS ให้สร้างไบต์ด้วยวิธีเดียวกันด้วย getPdfData() อัปโหลดอ็อบเจกต์ด้วยไคลเอนต์ Cloud Storage แล้วรับ storage object ด้วย $bucket->object($key) และเรียก $object->signedUrl($expiresAt, [...]) ด้วยเวลาหมดอายุแบบ Carbon/DateTime เพื่อสร้างลิงก์ที่เทียบเท่า เวลาหมดอายุของ signed URL บนผู้ให้บริการทั้งสองถูกจำกัดด้วยชนิด credential ปรึกษาเอกสารของผู้ให้บริการสำหรับอายุสูงสุดที่ credential ของคุณอนุญาต

  • สร้างเอกสารเพียงครั้งเดียวพอดี getPdfData() กระตุ้น build และ build ไม่เป็น idempotent เรียกมันครั้งเดียว เก็บสตริงไว้ แล้วนำกลับมาใช้ซ้ำทั้งสำหรับการอัปโหลดและ Content-Length, checksum หรือ ETag ใดที่คุณคำนวณ อย่าเรียกมันอีกเพื่อ “อ่านซ้ำ” ไบต์
  • temporaryUrl() ต้องการไดรเวอร์ที่ presign ได้ ไดรเวอร์ s3 ของ Laravel presign ได้ ส่วนไดรเวอร์ local จะ throw บน temporaryUrl() เว้นแต่คุณลงทะเบียน generator ที่กำหนดเองด้วย Storage::disk('local')->buildTemporaryUrlsUsing(...) เลือก disk ที่ลงนามได้ หรือลงนามเส้นทางแอปแทน
  • ตั้ง content type ของอ็อบเจกต์ เก็บด้วย Content-Type: application/pdf (ตัวเลือกอัปโหลด ContentType หรือเมทาดาตาของ disk) เพื่อให้เบราว์เซอร์เปิด presigned link เป็น PDF แทนการดาวน์โหลดเป็น octet-stream
  • เวลาหมดอายุที่สั้นอาจตายก่อนไคลเอนต์ที่ช้า หากผู้ใช้คลิกลิงก์หลังจากคุณสร้างมันไปนาน หน้าต่าง 60 วินาทีอาจตายไปแล้ว ให้กำหนดขนาดเวลาหมดอายุให้พอดีกับช่องว่างที่เป็นจริงระหว่างการสร้างและไบต์แรก — เป็นนาที ไม่ใช่วินาที — แล้วสร้างใหม่ตามต้องการ แทนการยืดมันออกไปเป็นชั่วโมง
  • URL ที่ลงนามคือการเข้าถึงแบบ bearer ใครก็ตามที่ถือ URL ก่อนมันหมดอายุดาวน์โหลดอ็อบเจกต์ได้ ให้เวลาหมดอายุสั้น เลือกขอบเขตอ็อบเจกต์เดียว และไม่เคย log URL ที่ลงนามฉบับเต็ม — ลายเซ็นเป็น token โดยพฤตินัย
  • อย่าฝังอินพุตผู้ใช้ในคีย์ของอ็อบเจกต์โดยไม่ทำความสะอาด สร้างคีย์จากค่าที่คุณควบคุมบวกไบต์สุ่ม (bin2hex(random_bytes(16))) คีย์ที่คาดเดาได้เชื้อเชิญการ enumerate เมื่อ bucket ถูกเปิดเผยแม้เพียงบางส่วน

รูปแบบนี้แลกการถ่ายโอนแบบ synchronous หนึ่งครั้งกับการอัปโหลดหนึ่งครั้งบวกการตอบสนอง JSON เล็ก ๆ worker ของแอปพลิเคชันถูกจองเฉพาะสำหรับ build PDF และการอัปโหลดไปยัง storage ไม่ใช่สำหรับการดาวน์โหลดทั้งหมดของไคลเอนต์ การดาวน์โหลดเองรันระหว่างไคลเอนต์และ storage (หรือ edge ของมัน) จึงไม่กิน app worker เลย

build ยังคงเป็น synchronous และยังครองงบสำหรับเอกสารขนาดใหญ่หรือหลายหน้า — getPdfData() สร้าง PDF ทั้งฉบับในหน่วยความจำก่อนที่คุณจะอัปโหลดได้ สำหรับเอกสารหนัก ให้ย้ายการสร้างและอัปโหลดไปยัง queued job แล้วส่งมอบ URL ที่ลงนามนอกแถบ (ตัวอย่างเช่น โดยแจ้งไคลเอนต์เมื่ออ็อบเจกต์พร้อม) ดูสร้าง PDF ใน queued job

  • เก็บ bucket ไว้เป็นส่วนตัว ให้ลายเซ็นเป็นผู้ให้สิทธิ์ ไม่เคยทำให้อ็อบเจกต์อ่านได้แบบสาธารณะเพื่อ “ทำให้การส่งมอบง่ายขึ้น” จุดประสงค์ทั้งหมดคือการเข้าถึงไหลผ่านลายเซ็นอายุสั้นเท่านั้น
  • เวลาหมดอายุที่สั้นและจำกัดขอบเขต ลงนามสำหรับหน้าต่างเล็กที่สุดที่พอดีกับโฟลว์ของคุณ และจำกัดขอบเขตแต่ละ URL ไว้ที่อ็อบเจกต์เดียว ลิงก์ที่รั่วไหลจะหมดอายุเองและไม่เปิดเผยสิ่งอื่นใด
  • secret จากสภาพแวดล้อม credential S3/GCS และ APP_SECRET ของ Symfony ที่หนุน UriSigner มาจากตัวแปรสภาพแวดล้อมหรือ secret manager ไม่เคยอยู่ในไฟล์กำหนดค่าที่ commit การหมุน signing secret ทำให้ทุกเส้นทางที่ลงนามที่ค้างอยู่เป็นโมฆะทันที
  • ตรวจสอบก่อนให้บริการบนเส้นทางที่ลงนามของแอป เมื่อการดาวน์โหลดข้ามแอปของคุณ (Laravel signed middleware, Symfony UriSigner::checkRequest()) ให้ตรวจสอบลายเซ็น ก่อน การเข้าถึง storage หรือการอนุญาตใด ๆ ปฏิเสธลิงก์ที่ถูกแก้ไขหรือหมดอายุด้วยสถานะที่กำหนดไว้
  • ไม่เคย log URL ที่ลงนามฉบับเต็ม ลายเซ็นเป็น credential แบบ bearer ให้ log คีย์ของอ็อบเจกต์และตัวระบุ correlation ไม่ใช่ URL ที่ลงนาม และ log คลาส exception เมื่อล้มเหลว — ไม่เคย log ข้อความหรือ stack trace
  • ไม่มี catch ว่าง ทุกตัวอย่าง log คลาสความล้มเหลวและคืนการตอบสนองข้อผิดพลาดที่กำหนดไว้

คู่มือนี้ไม่อ้างมาตรฐานเชิงบรรทัดฐานใด การเรียกเอนจิน NextPDF เพียงอย่างเดียวที่รูปแบบการส่งมอบนี้ต้องการคือ NextPDF\Core\Document::getPdfData() ซึ่งเป็นเมธอดสาธารณะที่ตรวจยืนยันแล้วและคืนไบนารี PDF ดิบ ส่วนตัวเอกสารเองถูกสร้างด้วยวิธีที่แอปของคุณสร้างเอกสารอยู่แล้ว (เช่น DocumentFactoryInterface ที่ฉีดเข้า / PdfFactory ของ Symfony) primitive สำหรับการลงนามเป็น API ของเฟรมเวิร์กและคลาวด์ที่มีเอกสารกำกับ — Laravel Storage::temporaryUrl() และ URL::temporarySignedRoute(), Symfony UriSigner และการดำเนินการ presigned-URL SDK ของ S3/GCS — และลายเซ็นที่แน่นอน ไดรเวอร์ที่รองรับ และหน้าต่างเวลาหมดอายุสูงสุดถูกควบคุมโดยโปรเจกต์ต้นน้ำเหล่านั้น ปรึกษาเอกสารของพวกเขาสำหรับสัญญาที่เชื่อถือได้บนแต่ละแพลตฟอร์ม