Skip to content

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ố keyPrefix cho issue())
  • Đ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_key trên cột key

Máy trạng thái

Giá trị status

ts
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'
}
StatusTerminalMô tả
activatedKhôngHợp lệ và dùng được. Trạng thái duy nhất mà validation có thể trả VALID hoặc GRACE_PERIOD.
expiredKhông (renew được)Qua graceExpiresAt. Đặt tự động bởi ValidationService.checkExpiration(). renew() có thể đưa nó về activated.
suspendedKhông (đảo ngược được)Vô hiệu đảo ngược được bởi suspend(). reinstate() đưa nó về activated.
revokedTerminal. renew()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

MethodTrả 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ùng isInactive() để 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:

  1. Thay đổi license trong một transaction
  2. Log một LicenseEvent sau commit()
  3. 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/issueLicenseManagementService.issue(opts)

TrườngBắt buộcMô tả
policyIdPolicy để dựa license lên
entity.typeBucket entity (vd merchants, users)
entity.idEntity ID
nameKhôngTên hiển thị
startsAtKhôngISO timestamp; mặc định now
keyPrefixKhôngGhi đè prefix BANA mặc định

Quy trình (transactional):

  1. Tra policy (findPolicy — throw 500 nếu thiếu)
  2. Sinh một license key duy nhất
  3. expiresAt = startsAt + DurationMultipliers.toMilliseconds(policy.duration) (hoặc null nếu vĩnh viễn)
  4. graceExpiresAt = expiresAt + DurationMultipliers.toMilliseconds(policy.gracePeriod) (hoặc null)
  5. Tạo hàng License trong một transaction (status = activated)
  6. Commit
  7. Log một sự kiện created với { policyId, key }
  8. publishCertificate() — ký và cache chứng chỉ

Trả về: { data: <License> }, HTTP 201 Created.

Suspend

POST /licenses/{id}/suspendLicenseManagementService.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}/reinstateLicenseManagementService.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ó expiresAt giờ ở quá khứ sẽ được reinstate về activated và tự hết hạn ngay ở lần validation tiếp theo.

Renew

POST /licenses/{id}/renewLicenseManagementService.renew(opts)

Tiền điều kiện:

Kiểm traThất bại
License tồn tại404 Not Found
status !== suspended && status !== revoked409 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):

ts
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 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}/revokeLicenseManagementService.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 sang expired lazy, chỉ khi một lệnh gọi validate() phát hiện now > graceExpiresAt. Một license không ai validate ở activated trong 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

ts
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ệnTrigger bởiPayload data
createdLicenseManagementService.issue(){ policyId, key }
activatedLicensingBaseService.createActivation() (kích hoạt thiết bị){ fingerprint, activationId }
deactivatedActivationService.deactivate(){ fingerprint, activationId }
suspendedLicenseManagementService.suspend(){ reason }
reinstatedLicenseManagementService.reinstate()
renewedLicenseManagementService.renew(){ newExpiresAt }
expiredValidationService.validate() (auto-expiry trong checkExpiration)
revokedLicenseManagementService.revoke(){ reason }

Không có loại sự kiện validated hay heartbeat. validate() không log một hàng trên mỗi lần gọi; nó chỉ log sự kiện expired khi auto-chuyển một license ra khỏi activated.

LicenseEvent chỉ-thêm (không deletedAt) và dùng ON DELETE SET NULL nê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ởi validate()).

Trang liên quan

TrangMô tả
ArchitectureMáy trạng thái license + kịch bản runtime
Domain ModelSchema đầy đủ License + LicenseEvent
Policies & FeaturesLoại policy, cấu hình feature, override
Validation & ActivationPipeline xác thực runtime + slot activation
Certificate SystempublishCertificate() thực sự làm gì
OperationsLưu ý lazy-expiry, runbook re-publish cert

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.