Validation & Activation
Pipeline Validation
POST /validation/validate → ValidationService.validate(opts). Yêu cầu authentication (JWT hoặc Basic); không có code permission riêng.
Request
interface IValidateRequest {
key: string; // license key — bắt buộc
fingerprint?: string; // fingerprint thiết bị — kích hoạt xử lý slot khi có
label?: string; // tên hiển thị thiết bị
platform?: string; // định danh OS / platform
ip?: string; // route handler forward khi áp dụng
userAgent?: string; // route handler forward khi áp dụng
}Schema body request (ValidateRequestSchema trong controllers/validation/definitions.ts) chỉ chấp nhận { key, fingerprint?, label?, platform? } từ client. ip / userAgent được điền bởi tầng controller, không phải client.
Pipeline
Bước 1 — Tìm license
licenseRepository.findOne({ filter: { where: { key } } }). Nếu null, trả LICENSE_NOT_FOUND ngay không làm gì thêm.
Bước 2 — Cổng status / ngày-bắt-đầu (checkLicenseStatus)
| Điều kiện | Result code |
|---|---|
LicenseStatuses.isInactive(license.status) (bất kỳ trong suspended, revoked, expired) | LICENSE_${license.status.toUpperCase()} → LICENSE_SUSPENDED, LICENSE_REVOKED, LICENSE_EXPIRED |
new Date(license.startsAt) > new Date() | LICENSE_NOT_STARTED |
Bước 3 — Kiểm tra hết hạn (checkExpiration)
Ba nhánh, đánh giá với now:
| Điều kiện | Hiệu ứng |
|---|---|
expiresAt là null (vĩnh viễn) HOẶC expiresAt >= now | Tiếp tục, không grace |
expiresAt < now VÀ graceExpiresAt > now | inGracePeriod = true, tiếp tục |
| Qua grace (hoặc không grace nào) | Auto-expire |
Nhánh auto-expire thực thi:
const { count } = await licenseRepository.updateBy({
data: { status: 'expired' },
where: { id: license.id, status: 'activated' },
options: { shouldReturn: false },
});Mệnh đề where status = activated là guard race. Nếu hai request cùng phát hiện một license qua-grace, chỉ một trong chúng nhận count === 1. Cái thua:
- Đọc lại license với
findLicense({ id }) - Trả
LICENSE_${current.status}dựa trên bất kỳ cái nào cái thắng để lại
Cái thắng bổ sung:
- Log một
LicenseEventexpired - Gọi
publishCertificate({ license: { ...license, status: 'expired' } })để consumer thấy status mới
Bước 4 — Giải song song
Khi license đã qua status và hết hạn, hai query chạy qua Promise.all:
resolveFeatures({ policyId, featuresOverride: license.override?.features ?? null })resolveActivationForValidation({ license, fingerprint, label, platform, ip, userAgent })
resolveActivationForValidation bản thân nạp policy và danh sách activation song song bên trong Promise.all riêng của nó.
Bước 5 — Slot activation (resolveActivationForValidation)
Logic slot bị bỏ qua hoàn toàn nếu thiếu fingerprint. Response khi đó mang activation: { id: null, used: <total>, limit: <policy hoặc override limit, hoặc null> } nhưng không tiêu một slot.
Nếu cung cấp fingerprint:
- Tìm một activation
(licenseId, fingerprint)có sẵn trong danh sách đã nạp. Nếu thấy, tái dùng. - Ngược lại gọi
tryCreateActivation().
Tạo activation hai pha (tryCreateActivation)
| Pha | Vì sao nó tồn tại |
|---|---|
| Fast path (không lock) | Nếu giới hạn đã đạt ở count không-lock, ta có thể từ chối mà không trả phí cho một transaction. Nếu không có giới hạn nào, ta chỉ tạo hàng trực tiếp. |
| Slow path (có lock) | Khi ta dưới giới hạn ở kiểm tra không-lock, ta lấy row lock LockStrengths.UPDATE trên license cha và re-count bên trong transaction. Cái này serialize các activation đồng thời với cùng license và ngăn bypass giới hạn. |
Nếu re-count slow-path thấy license giờ ở hoặc vượt giới hạn, transaction rollback và caller nhận isLimitReached: true.
Khi isLimitReached, validate() short-circuit với:
{
"valid": false,
"code": "ACTIVATION_LIMIT_REACHED",
"license": { ... },
"features": {},
"activation": { "id": null, "used": <currentCount>, "limit": <limit> }
}Bước 6 — Bookkeeping fire-and-forget
Sau một validation thành công, validate() cập nhật last_validated_at mà không await promise:
this.licenseRepository
.updateById({ id: license.id, data: { lastValidatedAt: nowIso } })
.catch(err => this.logger.for('validate').error('Failed to update lastValidatedAt | Error: %s', err));Nếu update thất bại, lỗi được log nhưng response validation vẫn thành công. Không có LicenseEvent validated ghi bởi đường này — sự kiện duy nhất validate() phát là expired, và chỉ ở nhánh auto-expiry trong bước 3.
Response (IValidationResult)
interface IValidationResult {
valid: boolean;
code: string;
license: {
id: string;
key: string;
status: string;
expiresAt: string | null;
};
features: Record<string, unknown>;
activation: {
id: string | null;
used: number;
limit: number | null;
};
certificate?: string;
}Response không mang một trường
licenseKeyriêng.license.keyhiện diện khi tìm thấy một license;certificatecache (nếu có) là cái client forward tới các service khác. Không có secret giải mã AES trong response — xem Certificate System.
Mã kết quả validation
| Code | valid | Đặt bởi | Ý nghĩa |
|---|---|---|---|
VALID | ✅ | fall-through bước 6 | License active và chưa hết hạn |
GRACE_PERIOD | ✅ | bước 3 (isInGracePeriod) | Qua expiresAt nhưng trong graceExpiresAt |
LICENSE_NOT_FOUND | ❌ | bước 1 | Không hàng nào khớp key |
LICENSE_SUSPENDED | ❌ | bước 2 | status === 'suspended' |
LICENSE_REVOKED | ❌ | bước 2 | status === 'revoked' |
LICENSE_EXPIRED | ❌ | bước 2 (đã expired) hoặc bước 3 (auto-expiry) | Qua grace |
LICENSE_NOT_STARTED | ❌ | bước 2 | startsAt ở tương lai |
ACTIVATION_LIMIT_REACHED | ❌ | bước 5 | Đạt giới hạn slot, không slot nào tiêu |
Nếu nhánh race-loser auto-expiry kích hoạt (bước 3), code trả về là
LICENSE_${actual_status}— thườngLICENSE_EXPIREDnhưng có thểLICENSE_SUSPENDEDhoặcLICENSE_REVOKEDnếu cái thắng tình cờ là một thao tác khác.
ActivationService
ActivationService là cái back controller CRUD chuẩn /activations và cũng được tái dùng bởi pipeline validation. Nó expose hai method:
activate({ licenseId, fingerprint, label?, platform?, hostname?, ip? })
Hai pha, tương tự tryCreateActivation của validate(), nhưng dùng khi một client muốn đăng ký một thiết bị rõ ràng thay vì lúc validation.
- Fast path (không lock): tìm một
(licenseId, fingerprint)có sẵn. Nếu thấy, trả nó nguyên trạng. - Slow path (transaction):
SELECT license FOR UPDATE404nếu thiếu;409nếu!isActive(status)- Giải config activation (
license.override.activation ?? policy.activation ?? null) - Nếu config non-null, count activation bên trong transaction; nếu đạt giới hạn →
409 Conflict(Activation limit reached (<limit>)) - Insert hàng activation bên trong transaction
- Log một
LicenseEventactivatedvới{ fingerprint, activationId } - Commit và trả
{ data: <activation> }
deactivate({ activationId, ip? })
findById—404nếu thiếudeleteById- Log một
LicenseEventdeactivatedvới{ fingerprint, activationId }
Không có endpoint REST có tên cho
deactivate; client gọiDELETE /activations/{id}chuẩn và dựa vào wiring controller hoặc gọi service này trực tiếp. Cũng không có đường "deactivate by fingerprint" — client phải biết activation id.
Không heartbeat, không auto-reaping
Không có method heartbeat(), không method isAlive(), không cột lastHeartbeatAt trên Activation, không endpoint POST /validation/heartbeat, không TTL Redis cho activation, và không reaper background. Một slot activation được giữ tới khi ai đó gọi deactivate() (hoặc tới khi license cha bị xoá, cascade). Nếu một thiết bị mất, admin phải xoá hàng activation của nó thủ công.
Bảng Activation
Tham chiếu cột đầy đủ nằm trong Domain Model → Activation. Điểm chính cho trang này:
fingerprintlưu nguyên văn; ràng buộc partial-uniqueUQ_Activation_licenseId_fingerprintcưỡng chế một hàng mỗi thiết bị mỗi license (và cấp năng lượng cho fast-path idempotent);ON DELETE CASCADEtừLicensegiải phóng mọi seat khi một license bị xoá.
Trang liên quan
| Trang | Mô tả |
|---|---|
| Architecture | Sequence diagram validate + activate |
| Domain Model | Schema đầy đủ Activation + bất biến |
| Policies & Features | Cấu hình giới hạn activation, giải feature |
| License Lifecycle | Chuyển trạng thái và log audit |
| Certificate System | Cách trường certificate trong response được tạo |