Instalare și pornire rapidă
@type/site-client este clientul de server pentru API-ul site-ului: metode tipizate, erori sub formă de clase și verificarea semnăturii webhook-ului. Nu are dependențe; are nevoie de fetch și Web Crypto, de aceea funcționează în Node.js 20+ și în runtime-ul Edge. Nu este legat de un framework — mai jos sunt exemple pentru Next.js (App Router).
Instalare
Pachetul nu este încă publicat în registrul public npm. Arhiva type-site-client-0.5.0.tgz o oferă proprietarul contului CRM; puneți-o în depozitul site-ului (de exemplu, în vendor/) și instalați:
pnpm add ./vendor/type-site-client-0.5.0.tgz
# sau: npm install ./vendor/type-site-client-0.5.0.tgzVersiunea curentă — 0.5.0, modificările — în istoricul versiunilor.
Pachetul funcționează doar pe server. La import într-un bundle de browser aruncă imediat o eroare — astfel cheia nu ajunge din greșeală pe pagină. În Next.js adăugați și
import 'server-only'în modulul care creează clientul.
Variabile de mediu
Doar de server — fără prefixul NEXT_PUBLIC_:
TYPE_API_URL=https://crm.example.com # adresa CRM-ului dvs., fără cale
TYPE_API_KEY=tk_<key_id>_<secret> # cheia cu dreptul „Chirii: site”
TYPE_WEBHOOK_SECRET=<secretul webhook-ului> # Setări → Chirii → SiteFișierele .env* cu aceste valori nu trebuie să ajungă în depozit.
Prima cerere
// lib/crm.ts
import 'server-only';
import { createSiteClient } from '@type/site-client';
export const crm = createSiteClient({
baseUrl: process.env.TYPE_API_URL!,
apiKey: process.env.TYPE_API_KEY!,
});
// Verificarea conexiunii: compania, moneda, limbile, ora check-in și check-out.
const info = await crm.ping();baseUrl se acceptă doar cu https:// (în afară de localhost), altfel cheia ar pleca în text clar.
Metode
| Metodă | Rută | Ce întoarce |
|---|---|---|
ping() | GET /ping | compania, moneda, limbile, ora check-in și check-out |
groups() | GET /groups | raioanele cu numărul de obiecte |
units({ lang }) | GET /units | toate obiectele (în afară de cele arhivate) |
unit(id, { lang }) | GET /units/{id} | un obiect |
occupancy({ date_from, date_to, unit_ids }) | GET /occupancy | intervalele ocupate; date_to exclusiv, perioada de până la 400 de zile |
quote({ unit_id, check_in, check_out, guests }) | GET /quote | calculul pe nopți; la date ocupate — available: false și violations |
prices({ check_in, check_out, guests, group_id }) | GET /prices | prețul și disponibilitatea tuturor obiectelor pe date |
createBookingRequest(body, { visitorIp }) | POST /booking-requests | cererea de rezervare |
legacyUrls() | GET /legacy-urls | adresele vechi pentru redirecționări |
photoUrl(url) | — | adresa absolută a fotografiei |
Separat, fără client, se exportă funcția verifyWebhook(body, headers, secret) — verificarea semnăturii webhook-ului.
Căile rutelor pornesc de la /api/modules/rental/site. Fiecare metodă primește ca ultim argument { signal, init }: signal — pentru anulare, init — câmpuri suplimentare fetch (antetul Authorization nu poate fi suprascris prin ele).
Implicit clientul setează cache: 'no-store'. Puneți rezultatele în cache singuri — de exemplu, unstable_cache cu etichete — și goliți-le prin webhook.
Catalog: componentă de server
// app/[lang]/apartamente/page.tsx
import { unstable_cache } from 'next/cache';
import { crm } from '@/lib/crm';
import type { Lang } from '@type/site-client';
const getUnits = unstable_cache((lang: Lang) => crm.units({ lang }), ['rental-units'], {
tags: ['rental:units'],
revalidate: 3600,
});
export default async function Catalog({ params }: { params: Promise<{ lang: Lang }> }) {
const { lang } = await params;
const { units } = await getUnits(lang);
return (
<ul>
{units.map((u) => (
<li key={u.id}>
<a href={`/${lang}/apartament/${u.slug}`}>
{u.photos[0] && <img src={crm.photoUrl(u.photos[0].url)} alt={u.name} />}
<h2>{u.name}</h2>
<p>{u.price} {u.currency}</p>
</a>
</li>
))}
</ul>
);
}Linkurile la fotografii sunt permanente și se deschid fără cheie: CRM-ul răspunde cu redirecționare 302 spre fișierul din stocare. Pentru next/image permiteți în images.remotePatterns host-ul CRM-ului și host-ul stocării sau folosiți unoptimized.
Cererea de rezervare: route handler
// app/api/booking/route.ts
import { RentalUnavailableError, SiteNetworkError, TooManyAttemptsError } from '@type/site-client';
import { crm } from '@/lib/crm';
export async function POST(req: Request) {
const f = await req.json();
// IP-ul vizitatorului — din antetul proxy-ului dvs.; după el CRM-ul numără limita cererilor.
const visitorIp = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
try {
const res = await crm.createBookingRequest(
{
request_id: f.request_id, // UUID-ul formularului: repetarea cu același id nu creează a doua cerere
unit_id: f.unit_id,
check_in: f.check_in,
check_out: f.check_out,
guests: Number(f.guests ?? 1),
name: f.name,
phone: f.phone || null,
email: f.email || null,
message: f.message || null,
lang: f.lang,
page_url: f.page_url ?? null,
consent: true,
hp: f.hp ?? '', // câmp-capcană ascuns, trebuie să fie gol
elapsed_ms: Number(f.elapsed_ms ?? 0), // cât timp a fost deschis formularul
},
visitorIp ? { visitorIp } : {},
);
return Response.json(res, { status: 201 });
} catch (e) {
if (e instanceof RentalUnavailableError) return Response.json({ violations: e.violations }, { status: 422 });
if (e instanceof TooManyAttemptsError) {
return Response.json({ error: 'too_many_attempts' }, { status: 429, headers: { 'Retry-After': String(e.retryAfter ?? 60) } });
}
if (e instanceof SiteNetworkError) return Response.json({ error: 'unavailable' }, { status: 503 });
throw e;
}
}Cererea creează în CRM o tranzacție la etapa „Cerere” cu calculul prețului; clientul se caută după telefon, apoi după e-mail. Dacă CRM-ul a fost indisponibil, repetați trimiterea cu același request_id — a doua tranzacție nu va apărea.
Antispam: câmpul hp trebuie să fie gol, elapsed_ms — cel puțin 3000. Dacă verificarea nu trece, răspunsul este la fel ca la succes, dar cererea nu se creează.
Erori
Toate erorile moștenesc SiteApiError cu câmpurile status, code și body.
| Clasă | Când |
|---|---|
ApiKeyInvalidError | 401 api_key_invalid — cheia lipsește, e greșită, revocată, expirată sau IP-ul nu e permis. Motivul se vede doar în CRM, în jurnalul cheii |
ScopeMissingError | 403 scope_missing — cheia nu are dreptul; scope — care (rental:site) |
ScopeUnavailableError | 403 scope_unavailable — dreptul există, dar modulul de închiriere este dezactivat |
TooManyAttemptsError | 429 too_many_attempts; retryAfter — secundele din Retry-After sau null |
RentalUnavailableError | 422 rental_unavailable la cerere; violations — de ce |
SiteApiError | restul: validation_error, not_found, bad_response… |
SiteNetworkError | CRM-ul nu a răspuns: rețea sau timeout (5 s pentru citire, 10 s pentru cerere) |
Erorile cheii (401, 403) sunt o problemă de configurare a site-ului, nu a vizitatorului: înregistrați code în jurnal și arătați vizitatorului un mesaj neutru „serviciul este temporar indisponibil”. Cheia în sine nu apare în mesajele erorilor.