License Lifecycle
Định dạng License Key
Mỗi license nhận một key duy nhất sinh lúc phát hành bởi LicensingBaseService.generateKey():
BANA-AAAAAAAA-BBBBBBBB-CCCCCCCC-DDDDDDDD
└──┘ └────────────────────────────────────┘
prefix 4 đoạn × 8 ký tự hex- Prefix mặc định:
BANA(có thể ghi đè qua tham sốkeyPrefixchoissue()) - Đoạn: mỗi đoạn là
crypto.randomUUID().replace(/-/g, '').slice(0, 8).toUpperCase()— 8 ký tự hex - Entropy: 4 đoạn × 32 bit = 128 bit mỗi key
- Tính duy nhất: cưỡng chế bởi ràng buộc unique
UQ_License_keytrên cộtkey
Máy trạng thái
Giá trị status
class LicenseStatuses {
static readonly ACTIVATED = Statuses.ACTIVATED; // 'activated'
static readonly EXPIRED = Statuses.EXPIRED; // 'expired'
static readonly SUSPENDED = Statuses.SUSPENDED; // 'suspended'
static readonly REVOKED = Statuses.REVOKED; // 'revoked'
}| Status | Terminal | Mô tả |
|---|---|---|
activated | Không | Hợp lệ và dùng được. Trạng thái duy nhất mà validation có thể trả VALID hoặc GRACE_PERIOD. |
expired | Không (renew được) | Qua graceExpiresAt. Đặt tự động bởi ValidationService.checkExpiration(). renew() có thể đưa nó về activated. |
suspended | Không (đảo ngược được) | Vô hiệu đảo ngược được bởi suspend(). reinstate() đưa nó về activated. |
revoked | Có | Terminal. renew() và suspend() từ chối trạng thái này. Chỉ phát hành một license hoàn toàn mới mới khôi phục dịch vụ được. |
Helper status
| Method | Trả true cho |
|---|---|
LicenseStatuses.isValid(status) | Bất kỳ trong bốn status đã biết |
LicenseStatuses.isActive(status) | Chỉ activated |
LicenseStatuses.isInactive(status) | suspended, revoked, expired (dùng bởi validation: trả LICENSE_${status}) |
Không có helper
isTerminal(). DùngisInactive()để phát hiện bất kỳ trạng thái non-active nào.
Thao tác
Cả năm thao tác nằm trong LicenseManagementService. issue() bọc công việc của nó trong một transaction; bốn cái còn lại lấy row lock SELECT … FOR UPDATE trên license trước khi thay đổi nó. Mỗi thao tác:
- Thay đổi license trong một transaction
- Log một
LicenseEventsaucommit() - Re-publish chứng chỉ qua
LicensingBaseService.publishCertificate()
Nếu transaction thất bại, không side effect nào (log sự kiện, re-publish chứng chỉ) được phát.
Issue
POST /licenses/issue — LicenseManagementService.issue(opts)
| Trường | Bắt buộc | Mô tả |
|---|---|---|
policyId | Có | Policy để dựa license lên |
entity.type | Có | Bucket entity (vd merchants, users) |
entity.id | Có | Entity ID |
name | Không | Tên hiển thị |
startsAt | Không | ISO timestamp; mặc định now |
keyPrefix | Không | Ghi đè prefix BANA mặc định |
Quy trình (transactional):
- Tra policy (
findPolicy— throw 500 nếu thiếu) - Sinh một license key duy nhất
expiresAt = startsAt + DurationMultipliers.toMilliseconds(policy.duration)(hoặcnullnếu vĩnh viễn)graceExpiresAt = expiresAt + DurationMultipliers.toMilliseconds(policy.gracePeriod)(hoặcnull)- Tạo hàng
Licensetrong một transaction (status = activated) - Commit
- Log một sự kiện
createdvới{ policyId, key } publishCertificate()— ký và cache chứng chỉ
Trả về: { data: <License> }, HTTP 201 Created.
Suspend
POST /licenses/{id}/suspend — LicenseManagementService.suspend(opts) — body: { reason? }
Tiền điều kiện: license tồn tại; LicenseStatuses.isActive(license.status) — tức hiện activated. Bất kỳ cái khác → 409 Conflict.
Hiệu ứng: status → suspended, log suspended với { reason }, re-publish chứng chỉ.
Reinstate
POST /licenses/{id}/reinstate — LicenseManagementService.reinstate(opts)
Tiền điều kiện: status === suspended. Bất kỳ cái khác → 409 Conflict.
Hiệu ứng: status → activated, log reinstated, re-publish chứng chỉ.
Lưu ý: reinstate không kiểm tra license chưa hết hạn trong thời gian đó. Một license suspended có
expiresAtgiờ ở quá khứ sẽ được reinstate vềactivatedvà tự hết hạn ngay ở lần validation tiếp theo.
Renew
POST /licenses/{id}/renew — LicenseManagementService.renew(opts)
Tiền điều kiện:
| Kiểm tra | Thất bại |
|---|---|
| License tồn tại | 404 Not Found |
status !== suspended && status !== revoked | 409 Conflict |
policy.duration được đặt (không vĩnh viễn) | 400 Bad Request ("Cannot renew a perpetual license") |
Quy trình (transactional, với LockStrengths.UPDATE trên hàng license):
const currentExpiry = license.expiresAt ? new Date(license.expiresAt) : now;
const base = currentExpiry > now ? currentExpiry : now; // không bao giờ mở rộng lùi
const newExpiresAt = new Date(base.getTime() + durationMs);
const newGraceExpiresAt = graceMs ? new Date(newExpiresAt.getTime() + graceMs) : null;License sau đó được cập nhật với ngày mới và status: activated (nên renew một license expired hồi sinh nó). Log renewed với { newExpiresAt } và re-publish chứng chỉ.
Ngữ nghĩa renew-từ-expired
Nếu license đã qua expiresAt, kỳ mới bắt đầu từ bây giờ, không hồi tố từ expiry cũ. Merchant nhận một kỳ tươi đầy đủ; họ không "mất" những ngày đã lapse.
Cửa sổ race
Lock UPDATE ngăn hai renew đồng thời double-extend license, nhưng nó không ngăn một lệnh gọi validate() xen kẽ auto-expire license giữa lúc lấy lock và update — kiểm tra hết hạn của validate() là một updateBy(... where status = activated) riêng và sẽ không trip khi renew giữ hàng.
Revoke
POST /licenses/{id}/revoke — LicenseManagementService.revoke(opts) — body: { reason? }
Tiền điều kiện: license tồn tại; status !== revoked. Đã revoked → 409 Conflict.
Hiệu ứng: status → revoked (terminal), log revoked với { reason }, re-publish chứng chỉ.
Re-publish chứng chỉ trên mỗi thay đổi trạng thái
Mỗi thao tác vòng đời thành công gọi publishCertificate() sau khi transaction commit. Cái này ký lại chứng chỉ với status / ngày mới và đẩy nó lên Redis tại lic:certs:{entityType}:{entityId} để service hạ nguồn tiêu thụ LicenseMiddleware thấy thay đổi ở request tiếp theo. Xem Certificate System để biết chi tiết ký.
Background workers
Không có. Package này không đăng ký cron job, queue worker, hay scheduled task nào. Cụ thể:
- Không có
WorkerService.processExpirations()— license chuyển sangexpiredlazy, chỉ khi một lệnh gọivalidate()phát hiệnnow > graceExpiresAt. Một license không ai validate ởactivatedtrong database mãi mãi, kể cả quá hạn. - Không có reaper heartbeat cũ, vì không có heartbeat nào cả.
Nếu cần hết hạn eager (vd cho báo cáo hoặc để quét merchant bỏ rơi), nó phải được thêm như chức năng mới.
Log audit License Events
Mỗi hành động đổi trạng thái ghi một hàng vào LicenseEvent. Log chỉ-thêm và dùng ON DELETE SET NULL thay vì cascade nên audit trail sống sót qua việc xoá license.
Loại sự kiện
class LicenseEventTypes {
static readonly CREATED = 'created';
static readonly ACTIVATED = 'activated';
static readonly DEACTIVATED = 'deactivated';
static readonly SUSPENDED = 'suspended';
static readonly REINSTATED = 'reinstated';
static readonly RENEWED = 'renewed';
static readonly EXPIRED = 'expired';
static readonly REVOKED = 'revoked';
}| Sự kiện | Trigger bởi | Payload data |
|---|---|---|
created | LicenseManagementService.issue() | { policyId, key } |
activated | LicensingBaseService.createActivation() (kích hoạt thiết bị) | { fingerprint, activationId } |
deactivated | ActivationService.deactivate() | { fingerprint, activationId } |
suspended | LicenseManagementService.suspend() | { reason } |
reinstated | LicenseManagementService.reinstate() | — |
renewed | LicenseManagementService.renew() | { newExpiresAt } |
expired | ValidationService.validate() (auto-expiry trong checkExpiration) | — |
revoked | LicenseManagementService.revoke() | { reason } |
Không có loại sự kiện
validatedhayheartbeat.validate()không log một hàng trên mỗi lần gọi; nó chỉ log sự kiệnexpiredkhi auto-chuyển một license ra khỏiactivated.
LicenseEventchỉ-thêm (khôngdeletedAt) và dùngON DELETE SET NULLnên audit trail sống sót qua việc xoá license. Tham chiếu cột đầy đủ: Domain Model → LicenseEvent.
Đọc log
Hiện không có endpoint REST riêng cho license event — hằng path RestPaths.LICENSE_EVENTS = '/license-events' tồn tại nhưng không controller nào đăng ký cho nó. Để query sự kiện bạn phải qua database trực tiếp, hoặc expose một controller như task tiếp theo.
Bảng License
Tham chiếu cột đầy đủ, index, và ràng buộc nằm trong Domain Model → License. Cột liên quan vòng đời:
status(máy trạng thái ở trên),expiresAt/graceExpiresAt(điều khiển renew & lazy expiry),certificate(ký lại sau mỗi thao tác), vàlastValidatedAt(động fire-and-forget bởivalidate()).
Trang liên quan
| Trang | Mô tả |
|---|---|
| Architecture | Máy trạng thái license + kịch bản runtime |
| Domain Model | Schema đầy đủ License + LicenseEvent |
| Policies & Features | Loại policy, cấu hình feature, override |
| Validation & Activation | Pipeline xác thực runtime + slot activation |
| Certificate System | publishCertificate() thực sự làm gì |
| Operations | Lưu ý lazy-expiry, runbook re-publish cert |