Node.js SDK — @gerege/miniapp-node¶
Энэ хуудсаар: Node.js backend-аасаа Gerege App-ын API-г SDK-аар дуудаж (mini app-ын нээлт, төлбөр, буцаалт, тооцоо,
терминал, дэд худалдагч), webhook-ийн гарын үсгийг шалгана. Суулгаад эхний дуудлага хүртэл ~15 минут. Хэрэгтэй:
Node.js ≥ 20, консолоос авсан test API түлхүүр (gk_test_… / gks_test_…, эсвэл хуучин mapp_test_… / gma_cs_…),
webhook secret (whsec_…).
Хувилбар 0.2.0. Runtime хамааралгүй (глобал fetch, node:crypto), зөвхөн ESM, TypeScript-ийн төрөлтэй. SDK юу ч
лог хийхгүй, өөрөө давтахгүй.
Зөвхөн сервер дээр
Client secret, access/refresh token, webhook secret браузер, мобайл апп, терминал, лог, URL-д хэзээ ч орохгүй.
Суулгах¶
Багц npm-д хараахан нийтлэгдээгүй (тусдаа репо руу салгах үед нийтлэгдэнэ). Одоогоор эх кодыг Gerege-ээс
хүсэлтээр авна (Тусламж) — дараа нь өөрөө угсарч .tgz болгоод суулгана:
# SDK-ийн эх код дээр (sdk/miniapp-node)
npm ci && npm run build && npm pack # → gerege-miniapp-node-0.2.0.tgz
# Таны төсөл дээр
npm install ./gerege-miniapp-node-0.2.0.tgz
npm test нь платформын өөрийн кодоос үүсгэсэн тест векторыг (гарын үсэг, event-ийн body, route) уншдаг — SDK ба
сервер нэг өгөгдлөөр шалгагдана.
Client үүсгэх¶
import { GeregeMiniApp, GeregeError } from "@gerege/miniapp-node";
const gerege = new GeregeMiniApp({
clientId: process.env.GEREGE_CLIENT_ID!, // gk_test_… / gk_live_… (хуучин mapp_test_… / mapp_…)
clientSecret: process.env.GEREGE_CLIENT_SECRET!, // gks_… (хуучин gma_cs_…)
// baseUrl: "https://geregeapp.mn", timeoutMs: 20_000, userAgent, fetch (тест, proxy)
});
| Сонголт | Анхдагч | |
|---|---|---|
clientId, clientSecret |
— (заавал) | дутуу бол TypeError |
baseUrl |
https://geregeapp.mn |
|
timeoutMs |
20 000 | хүсэлт бүрийн; хэтэрвэл GeregeError network_error |
userAgent |
gerege-miniapp-node/0.2.0 |
|
fetch |
глобал fetch |
Нэвтрэлт¶
Арга бүр аль credential-аар явахаа сервертэй адил шийднэ (API-ийн нийтлэг дүрэм):
- Сүүлийн аргумент
{ accessToken }(AuthOption) өгвөл хүний token (Bearer), өгөхгүй бол апп-ын client credential (Basic) —retrieve,list,capture,cancel,unclaim,refunds.create,settlements.*. paymentIntents.create(accessToken, params, opts)— mini app-ынappintent (хүний token).paymentIntents.create(params, opts)(presentmentньqr/redirect/terminal) — худалдагчийн claimless intent (Basic). Хоёрыг холивол хүсэлт илгээхээс өмнөTypeError.terminals.*,subMerchants.*— зөвхөн Basic.oauth.*— Basic.me(accessToken)— Bearer.- Basic дуудлага бүр түлхүүрийн scope-ыг шаардана (
refunds.create→refunds:write,terminals.batch→terminals:writeг.м.); дутуу болGeregeErrorstatus: 403,code: "insufficient_scope",details.required_scope. Аль аргад ямар scope — API-ийн нийтлэг дүрэм.
Mini app: нээлт, хүн, төлбөр¶
// 1. Frontend нь entry URL-ын ?launch=<code>-ийг өөрийн backend руу илгээнэ (60 секунд, нэг удаа).
const tokens = await gerege.oauth.exchangeLaunchCode(launchCode);
// tokens.user.sub (= tokens.user.ref) — энэ апп дахь хүний тогтмол pairwise нэр.
// 2. Хугацаа дуусахад (30 мин): ШИНЭ refresh token-оо хадгал — хуучныг дахин илгээвэл бүх гинж хүчингүй.
const next = await gerege.oauth.refresh(tokens.refresh_token);
// 3. Хэн бэ (зөвшөөрсөн хэмжээгээр): { ref, sub, verified, locale, scope, environment, email?, phone? }
const me = await gerege.me(next.access_token);
// 4. Төлбөр: дүнг ӨӨРИЙН каталогоос, frontend-ээс хэзээ ч биш.
const intent = await gerege.paymentIntents.create(
next.access_token,
{ amount: 45000, description: "Japan 5GB · 15 хоног", reference: order.id },
{ idempotencyKey: `order:${order.id}` },
);
// → frontend: window.gerege.requestPayment({ intentId: intent.id })
Дэлгэрэнгүй: Mini app-ын нээлт ба token, Mini app-ын төлбөр.
Худалдагч: claimless төлбөр¶
// Redirect checkout — return_url-ийн host апп-ын return_hosts-д бүртгэлтэй.
const pi = await gerege.paymentIntents.create(
{ presentment: "redirect", return_url: `https://shop.mn/orders/${order.id}/done`,
amount: order.total, description: `Захиалга №${order.id}`, reference: order.id },
{ idempotencyKey: `order:${order.id}` }, // давтвал ижил intent, ижил pay_url (200)
);
res.redirect(303, pi.pay_url); // буцаж ирэхэд ?pi=… — retrieve-ээр шалга, URL-д бүү итгэ
// QR (kiosk, POS): { presentment: "qr", … } → pi.pay_url-ийг дэлгэцэнд QR болгоно.
// Терминал (вендинг): сагс бүрт manual capture, богино цонх.
try {
await gerege.paymentIntents.create(
{ presentment: "terminal", terminal_id: terminal.id, amount: 3500, description: "Ус 0.5л",
reference: basket.id, capture_method: "manual", capture_within: 120 },
{ idempotencyKey: basket.id },
);
} catch (err) {
if (err instanceof GeregeError && err.code === "terminal_busy") {
await gerege.paymentIntents.cancel(String(err.details?.payment_intent)); // нээлттэйг цуцлаад дахин
} else throw err;
}
pay_url нь зөвхөн create-ийн хариунд (ба давталтад) байна — retrieve, жагсаалт, webhook-д хэзээ ч үгүй.
Дэлгэрэнгүй: Gerege App-аар төлөх.
Intent ба буцаалт¶
const pi = await gerege.paymentIntents.retrieve("pi_…"); // Basic; { accessToken } өгвөл Bearer
const page = await gerege.paymentIntents.list({ reference: order.id, status: "succeeded", limit: 20 });
for await (const p of gerege.paymentIntents.listAll({ status: "succeeded" })) reconcile(p);
await gerege.paymentIntents.capture(pi.id); // manual + requires_capture; 202 = processing, дахин унш
await gerege.paymentIntents.cancel(pi.id); // төлөгдөөгүйг цуцлах эсвэл барьцааг суллах
await gerege.paymentIntents.unclaim(pi.id); // claim хийгдсэн, баталгаажаагүйг суллах
const rf = await gerege.refunds.create(pi.id, { amount: 1500, reason: "not_dispensed" },
{ idempotencyKey: `refund:${basket.id}` }); // amount орхивол үлдсэн бүгд; 90 хоногт
| Арга | HTTP | Хариу |
|---|---|---|
paymentIntents.create(accessToken, params, { idempotencyKey }) |
POST /payment-intents (Bearer) |
PaymentIntent |
paymentIntents.create(params, { idempotencyKey }) |
POST /payment-intents (Basic) |
ClaimlessPaymentIntent (pay_url) |
paymentIntents.retrieve(id, opts?) |
GET /payment-intents/{id} |
PaymentIntent |
paymentIntents.list(params?, opts?) |
GET /payment-intents |
{ data, has_more } |
paymentIntents.listAll(params?, opts?) |
дээрх, after-ийг дагана |
AsyncGenerator<PaymentIntent> |
paymentIntents.capture(id, opts?) · cancel · unclaim |
POST …/capture · /cancel · /unclaim |
PaymentIntent |
refunds.create(intentId, { amount?, reason? }, { idempotencyKey, accessToken? }) |
POST …/refunds |
Refund |
Intent-ийн талбар, төлөв: Payment intent; буцаалт: Буцаалт.
Тооцоо¶
const { items, next_cursor, has_more } = await gerege.settlements.list({ status: "paid", limit: 20 });
for await (const st of gerege.settlements.listAll({ status: "paid" })) books.record(st);
const detail = await gerege.settlements.retrieve("st_…"); // + intents[], refund_items[], truncated
Зөвхөн live — test түлхүүр хоосон жагсаалт авна. Дэлгэрэнгүй: Тооцоо.
Терминал ба дэд худалдагч¶
const t = await gerege.terminals.create({ label: "Машин №7", location: "СБД, 1-р хороо", reference: "vm-7" });
// t.url → sticker-ийн QR, NFC tag (NDEF URI). Ижил reference дахин → тэр терминал (200).
const { data: terminals } = await gerege.terminals.list();
await gerege.terminals.update(t.id, { status: "disabled" });
// Олноор (≤ 500): нэг буруу мөр бүгдийг татгалзана (400 validation_error, terminals[i].…);
// reference-тэй мөр давтахад existing — timeout-ийн дараа яг тэр batch-аа дахин илгээ.
const batch = await gerege.terminals.batch([
{ label: "Машин №8", reference: "vm-8" },
{ label: "Машин №9", reference: "vm-9", sub_merchant_code: "owner-17" },
]);
// batch.data[i].status: created | existing | failed | skipped
// Sticker-ийн PDF (test түлхүүр ч болно): terminal_ids-гүй бол бүх идэвхтэй терминал.
const pdf = await gerege.terminals.printSheet({ layout: "label-40", locale: "mn" }); // Uint8Array
await writeFile("stickers.pdf", pdf); // import { writeFile } from "node:fs/promises"
const owner = await gerege.subMerchants.create({ code: "owner-17", display_name: "Батаа ХХК" });
const link = await gerege.subMerchants.requestPayoutLink(owner.id, { account: "MN75 0098 0009 8123 4560" });
const { data: links } = await gerege.subMerchants.listPayoutLinks(owner.id);
| Арга | HTTP |
|---|---|
terminals.create(params) · list() · retrieve(id) · update(id, params) |
POST/GET /terminals, GET/PATCH /terminals/{tid} |
terminals.batch(rows) |
POST /terminals/batch → TerminalBatch (хоосон массив бол TypeError) |
terminals.printSheet({ terminal_ids?, layout?, locale?, crop_marks? }) |
POST /terminals/print-sheet → PDF (Uint8Array); layout a4-24 (анхдагч) · label-50 · label-40 · tent-a6 |
subMerchants.create(params) · list() · retrieve(id) · update(id, params) |
POST/GET /sub-merchants, GET/PATCH /sub-merchants/{sid} |
subMerchants.requestPayoutLink(id, { account }) · listPayoutLinks(id) |
POST …/payout-link · GET …/payout-links |
Бичих (batch-ийг оролцуулаад) нь live түлхүүр (test-д 403 live_only), payout-link мөн live; printSheet ба
уншилт test-д ч болно. account дутуу бол TypeError. Объектууд: Объектууд.
SDK-д хараахан ороогүй API¶
Дараах server API ажилладаг боловч SDK 0.2.0-д арга алга — энгийн fetch-ээр (ижил Basic) дуудна: GET /events,
GET /events/{id} (events:read — Events API), /webhook-endpoints… (webhooks:write,
live — Webhook), GET /statements (settlements:read — Тайлан).
Хариуг GeregeError-той адил задлахыг хүсвэл code, error талбарыг өөрөө уншина.
const auth = "Basic " + Buffer.from(`${process.env.GEREGE_CLIENT_ID}:${process.env.GEREGE_CLIENT_SECRET}`).toString("base64");
const res = await fetch(`https://geregeapp.mn/api/v1/miniapp/events?after=${lastId}&limit=100`, {
headers: { Authorization: auth, Accept: "application/json" },
});
const { data, next_cursor } = await res.json(); // хуучин нь эхэнд; data[i] нь webhook-ийн яг ижил body
Webhook¶
verifyWebhook(rawBody, header, secrets, { toleranceSec?, now? }) — түүхий body дээр (JSON parse-ээс өмнө) гарын
үсгийг constant-time шалгаж event-ийг буцаана; буруу бол GeregeWebhookError. secrets нь string эсвэл массив —
secret солих үед [шинэ, хуучин]. signatureFromHeaders(headers) нь Gerege-Signature-ийг (байхгүй бол
OpenPay-Signature) уншина. Мөн gerege.webhooks.verify / .sign / .signatureHeader / .signatureFromHeaders.
node:http (ямар ч framework-гүй):
import { createServer } from "node:http";
import { verifyWebhook, signatureFromHeaders, GeregeWebhookError } from "@gerege/miniapp-node";
const secrets = process.env.GEREGE_WEBHOOK_SECRETS!.split(","); // "шинэ,хуучин"
createServer(async (req, res) => {
const chunks: Buffer[] = [];
for await (const c of req) chunks.push(c as Buffer);
try {
const event = verifyWebhook(Buffer.concat(chunks), signatureFromHeaders(req.headers), secrets);
await queue.add(event.id, event); // event.id-аар давхардлыг арилгаад дараа нь боловсруул
res.writeHead(204).end(); // 10 секундэд
} catch (err) {
res.writeHead(err instanceof GeregeWebhookError ? 400 : 500).end();
}
}).listen(8080);
Express — express.raw заавал (express.json() body-г задалж гарын үсэг таарахгүй):
app.post("/webhooks/gerege", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, signatureFromHeaders(req.headers), secrets);
} catch {
return res.sendStatus(400);
}
switch (event.type) {
case "payment_intent.succeeded": await queue.add("fulfil", { intentId: event.data.object.id }); break;
case "payment_intent.requires_capture": await queue.add("dispense", { intentId: event.data.object.id }); break;
default: break; // мэдэхгүй төрөл: 2xx, үл тоо
}
res.sendStatus(204);
});
Олгохоос өмнө intent-ийг retrieve-ээр дахин унш. signWebhook(secret, t, raw) ба webhookSignatureHeader(secrets, t,
raw) нь өөрийн тестэд гарын үсэгтэй хүсэлт бэлдэнэ. Дэлгэрэнгүй: Гарын үсэг шалгах,
Event-ийн лавлах.
Алдаа¶
try {
await gerege.paymentIntents.capture(id);
} catch (err) {
if (err instanceof GeregeError) {
console.log(err.status, err.code, err.message, err.requestId, err.details);
if (err.retryable) return retryLater(); // 0 (сүлжээ), 429, 5xx — ижил Idempotency-Key-ээр
if (err.code === "expired") return closeOrder(); // барьцааны хугацаа өнгөрсөн
}
throw err;
}
| Класс | Хэзээ | Талбар |
|---|---|---|
GeregeError |
2xx бус хариу бүр; сүлжээ/timeout (status: 0, code: "network_error"); 2xx боловч JSON биш (invalid_response) |
status, code, message, requestId, details (payment_intent, details[], status …), retryable |
GeregeOAuthError (GeregeError-ийн дэд) |
token endpoint-ийн татгалзал | error, error_description |
GeregeWebhookError |
webhook-ийг татгалзах ёстой — 400 хариул | reason: missing_header · malformed_header · timestamp_out_of_tolerance · signature_mismatch · invalid_payload |
TypeError |
хүсэлтээс өмнө: credential, id, idempotencyKey, account дутуу; token ба presentment зөрсөн |
code-гүй хариу (404, 429, 500) нь http_<status>. Бүх кодын жагсаалт: Алдааны код.
Тогтмол ба төрөл¶
DEFAULT_BASE_URL, API_BASE_PATH (/api/v1/miniapp), SIGNATURE_HEADER (Gerege-Signature),
OMAS_SIGNATURE_HEADER, EVENT_ID_HEADER (Gerege-Event-Id), OMAS_EVENT_ID_HEADER, DEFAULT_TOLERANCE_SEC (300),
PREVIOUS_SECRET_OVERLAP_SEC (86 400), PREFIXES (launchCode, accessToken, refreshToken, clientSecret,
webhookSecret, liveClientId, testClientId — анхдагч түлхүүрийн mapp_ / gma_cs_; liveKeyClientId,
testKeyClientId, liveKeySecret, testKeySecret — API түлхүүрийн gk_<env>_ / gks_<env>_), IDEMPOTENCY_KEY_PATTERN.
Төрлүүд: PaymentIntent, ClaimlessPaymentIntent, Refund, Settlement, SettlementDetail, Terminal,
TerminalBatch, TerminalBatchRow, TerminalBatchResult, PrintSheetParams, PrintSheetLayout, SubMerchant,
PayoutLink, TokenResponse, MiniAppUser, WebhookEvent (event бүрийн discriminated union —
switch (event.type) дотор data.object зөв төрөлтэй).