@corelicense/node 0.5.0 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -162
- package/dist/client/http-error-utils.d.ts +10 -0
- package/dist/client/http-error-utils.d.ts.map +1 -0
- package/dist/client/http-error-utils.js +38 -0
- package/dist/client/http-error-utils.js.map +1 -0
- package/dist/client/license-api-client.d.ts.map +1 -1
- package/dist/client/license-api-client.js +26 -6
- package/dist/client/license-api-client.js.map +1 -1
- package/dist/client/map-client-error.d.ts.map +1 -1
- package/dist/client/map-client-error.js +38 -12
- package/dist/client/map-client-error.js.map +1 -1
- package/dist/client/public-keys-client.d.ts.map +1 -1
- package/dist/client/public-keys-client.js +48 -4
- package/dist/client/public-keys-client.js.map +1 -1
- package/dist/core-license.js +1 -1
- package/dist/crypto/key-loader.d.ts +1 -1
- package/dist/crypto/key-loader.d.ts.map +1 -1
- package/dist/crypto/key-loader.js +34 -9
- package/dist/crypto/key-loader.js.map +1 -1
- package/dist/errors/api-error.d.ts +1 -0
- package/dist/errors/api-error.d.ts.map +1 -1
- package/dist/errors/api-error.js +2 -2
- package/dist/errors/api-error.js.map +1 -1
- package/dist/errors/codes.d.ts +5 -0
- package/dist/errors/codes.d.ts.map +1 -1
- package/dist/errors/codes.js +5 -0
- package/dist/errors/codes.js.map +1 -1
- package/dist/errors/license-error.d.ts +2 -0
- package/dist/errors/license-error.d.ts.map +1 -1
- package/dist/errors/license-error.js +13 -2
- package/dist/errors/license-error.js.map +1 -1
- package/dist/errors/throw-for-status.d.ts.map +1 -1
- package/dist/errors/throw-for-status.js +3 -1
- package/dist/errors/throw-for-status.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/types/status.d.ts +1 -1
- package/dist/types/status.d.ts.map +1 -1
- package/docs/api-reference.md +261 -0
- package/docs/installation.md +188 -0
- package/docs/integration.md +789 -0
- package/package.json +6 -4
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
# Tích hợp
|
|
2
|
+
|
|
3
|
+
Hướng dẫn chi tiết để tích hợp `@corelicense/node` vào hệ thống production — dành cho developer và AI agent generate code.
|
|
4
|
+
|
|
5
|
+
**Đọc trước:** [Cài đặt](./installation.md)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Mục lục
|
|
10
|
+
|
|
11
|
+
1. [Kiến trúc & nguyên tắc](#1-kiến-trúc--nguyên-tắc)
|
|
12
|
+
2. [Vòng đời runtime](#2-vòng-đời-runtime)
|
|
13
|
+
3. [Tích hợp Express (đầy đủ)](#3-tích-hợp-express-đầy-đủ)
|
|
14
|
+
4. [Tích hợp Fastify](#4-tích-hợp-fastify)
|
|
15
|
+
5. [Tích hợp NestJS](#5-tích-hợp-nestjs)
|
|
16
|
+
6. [Feature gate, limit & quota](#6-feature-gate-limit--quota)
|
|
17
|
+
7. [Worker & CLI guard](#7-worker--cli-guard)
|
|
18
|
+
8. [Tích hợp sâu — Module Registry & DI](#8-tích-hợp-sâu--module-registry--di)
|
|
19
|
+
9. [Pattern ứng dụng lớn](#9-pattern-ứng-dụng-lớn)
|
|
20
|
+
10. [Xử lý lỗi](#10-xử-lý-lỗi)
|
|
21
|
+
11. [Events & background sync](#11-events--background-sync)
|
|
22
|
+
12. [Bảo mật & dữ liệu gửi server](#12-bảo-mật--dữ-liệu-gửi-server)
|
|
23
|
+
13. [Production checklist](#13-production-checklist)
|
|
24
|
+
14. [Troubleshooting](#14-troubleshooting)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Kiến trúc & nguyên tắc
|
|
29
|
+
|
|
30
|
+
### Middleware không đủ
|
|
31
|
+
|
|
32
|
+
HTTP middleware chỉ chặn request. Attacker hoặc bug có thể gọi service/worker trực tiếp. **Tích hợp sâu** nghĩa là:
|
|
33
|
+
|
|
34
|
+
- Module/service **không được bind** nếu feature tắt trong policy
|
|
35
|
+
- Mỗi lần `get()` / `resolve()` **re-check** license (revoke chặn ngay)
|
|
36
|
+
- Worker/CLI có guard riêng
|
|
37
|
+
- Quota/limit enforce tại điểm tiêu thụ
|
|
38
|
+
|
|
39
|
+
### Hai lớp bảo vệ
|
|
40
|
+
|
|
41
|
+
| Lớp | Cơ chế | Khi nào |
|
|
42
|
+
|-----|--------|---------|
|
|
43
|
+
| **HTTP** | `express()` / `fastifyPlugin()` / Nest guards | Mọi request API |
|
|
44
|
+
| **Runtime** | `requireFeature`, registry, DI, worker guard | Service, job, CLI |
|
|
45
|
+
|
|
46
|
+
### Policy là nguồn sự thật
|
|
47
|
+
|
|
48
|
+
Policy token (JWT, EdDSA) chứa:
|
|
49
|
+
|
|
50
|
+
- `status`: `active` | `suspended` | `revoked` | ...
|
|
51
|
+
- `features`: map feature → boolean
|
|
52
|
+
- `limits`: số tối đa (users, workers, ...)
|
|
53
|
+
- `quotas` / usage local: tiêu thụ theo ngày
|
|
54
|
+
|
|
55
|
+
Admin đổi policy trên server → SDK refresh → runtime cập nhật. **Bind module/service** chỉ xảy ra lúc `bootstrap()` — đổi feature cần **restart app** để mount/unmount module; nhưng `get()`/`resolve()` vẫn chặn ngay khi revoke.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2. Vòng đời runtime
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
constructor → boot() → [bootstrap()] → app chạy
|
|
63
|
+
↓
|
|
64
|
+
background check (6h mặc định)
|
|
65
|
+
↓
|
|
66
|
+
syncFromServer (mutex — không race)
|
|
67
|
+
↓
|
|
68
|
+
requireActive() trên mọi gate quan trọng
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `boot()` — bắt buộc trước mọi thứ
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await license.boot();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Fail-closed v0.5: nếu license revoked/expired và không còn offline grace → **throw**, process nên exit
|
|
78
|
+
- Gọi `boot()` **một lần** khi process start (trước `listen()`)
|
|
79
|
+
|
|
80
|
+
### Instance ID
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
instance: {
|
|
84
|
+
strategy: 'install-id-domain-hash', // khuyến nghị production
|
|
85
|
+
domain: process.env.APP_DOMAIN, // ví dụ: app.customer.com
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `install-id`: UUID lưu file trong `cachePath` — ổn định qua restart
|
|
90
|
+
- `domain-hash`: SHA-256 domain — phân biệt deploy theo domain
|
|
91
|
+
- `custom`: `instance.customId` — container/K8s pod id tùy chỉnh
|
|
92
|
+
|
|
93
|
+
Server đếm **max instances** theo license — mỗi cặp install-id + domain = một instance.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 3. Tích hợp Express (đầy đủ)
|
|
98
|
+
|
|
99
|
+
### App skeleton
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import express from 'express';
|
|
103
|
+
import { CoreLicense, loadOptionsFromEnv, registerLicensedRoutes } from '@corelicense/node';
|
|
104
|
+
|
|
105
|
+
const app = express();
|
|
106
|
+
app.use(express.json());
|
|
107
|
+
|
|
108
|
+
const license = new CoreLicense(
|
|
109
|
+
loadOptionsFromEnv({
|
|
110
|
+
cachePath: './storage/corelicense',
|
|
111
|
+
instance: { strategy: 'install-id-domain-hash', domain: process.env.APP_DOMAIN },
|
|
112
|
+
checkIntervalMs: 6 * 60 * 60 * 1000,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
async function main() {
|
|
117
|
+
await license.boot();
|
|
118
|
+
|
|
119
|
+
// --- Routes KHÔNG cần license ---
|
|
120
|
+
app.get('/health', (_req, res) => res.json({ ok: true }));
|
|
121
|
+
app.get('/license/status', async (_req, res) => {
|
|
122
|
+
res.json(await license.getStatus());
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// --- Middleware license (toàn app) ---
|
|
126
|
+
app.use(
|
|
127
|
+
license.express({
|
|
128
|
+
allowPaths: ['/health', '/license/status', '/license/reactivate'],
|
|
129
|
+
mode: 'block',
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// --- Deep integration (tùy chọn) ---
|
|
134
|
+
const { modules, services, skipped } = await license.bootstrap({
|
|
135
|
+
modules: [
|
|
136
|
+
{
|
|
137
|
+
name: 'automation',
|
|
138
|
+
feature: 'automation',
|
|
139
|
+
factory: ({ license }) => new AutomationModule(license),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'export',
|
|
143
|
+
feature: 'export',
|
|
144
|
+
factory: ({ license }) => new ExportModule(license),
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
services: [
|
|
148
|
+
{
|
|
149
|
+
name: 'ExportService',
|
|
150
|
+
feature: 'export',
|
|
151
|
+
singleton: true,
|
|
152
|
+
factory: ({ license }) => new ExportService(license),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
console.log('Skipped (feature off):', skipped);
|
|
158
|
+
|
|
159
|
+
// Mount router CHỈ khi module đã registered
|
|
160
|
+
registerLicensedRoutes(app, modules, [
|
|
161
|
+
{ path: '/api/automation', module: 'automation', router: automationRouter },
|
|
162
|
+
{ path: '/api/export', module: 'export', router: exportRouter },
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
// --- Route thường (đã qua middleware requireActive) ---
|
|
166
|
+
app.get('/api/me', async (_req, res) => {
|
|
167
|
+
await license.requireFeature('dashboard');
|
|
168
|
+
res.json({ ok: true });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
app.listen(process.env.PORT ?? 3000);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch((err) => {
|
|
175
|
+
console.error('Boot failed:', err);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `allowPaths`
|
|
181
|
+
|
|
182
|
+
Exact match `req.path`. Luôn whitelist:
|
|
183
|
+
|
|
184
|
+
- `/health` — load balancer
|
|
185
|
+
- `/license/status` — ops dashboard
|
|
186
|
+
- `/license/reactivate` — flow kích hoạt lại (nếu có)
|
|
187
|
+
|
|
188
|
+
### Middleware response 403
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"ok": false,
|
|
193
|
+
"code": "LICENSE_REVOKED",
|
|
194
|
+
"source": "core-license.requireActive",
|
|
195
|
+
"message": "..."
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Dùng `error.toClientPayload()` — không leak stack trace.
|
|
200
|
+
|
|
201
|
+
### `registerLicensedRoutes`
|
|
202
|
+
|
|
203
|
+
- Chỉ `app.use(path, router)` nếu `modules.has(name)`
|
|
204
|
+
- Feature off → route **không tồn tại** (404), không phải 403 — khó discover API bị khóa
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 4. Tích hợp Fastify
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import Fastify from 'fastify';
|
|
212
|
+
import { CoreLicense, loadOptionsFromEnv } from '@corelicense/node';
|
|
213
|
+
|
|
214
|
+
const app = Fastify({ logger: true });
|
|
215
|
+
|
|
216
|
+
const license = new CoreLicense(loadOptionsFromEnv({ cachePath: './storage/corelicense' }));
|
|
217
|
+
|
|
218
|
+
async function main() {
|
|
219
|
+
await license.boot();
|
|
220
|
+
|
|
221
|
+
app.get('/health', async () => ({ ok: true }));
|
|
222
|
+
|
|
223
|
+
await app.register(
|
|
224
|
+
license.fastifyPlugin({
|
|
225
|
+
allowPaths: ['/health', '/license/status'],
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
app.get('/api/data', async () => {
|
|
230
|
+
await license.requireFeature('api');
|
|
231
|
+
return { data: [] };
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await app.listen({ port: 3000, host: '0.0.0.0' });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
main().catch((err) => {
|
|
238
|
+
app.log.error(err);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Import trực tiếp:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { createFastifyPlugin } from '@corelicense/node/fastify';
|
|
247
|
+
await app.register(createFastifyPlugin(license, { allowPaths: ['/health'] }));
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 5. Tích hợp NestJS
|
|
253
|
+
|
|
254
|
+
### `AppModule`
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// app.module.ts
|
|
258
|
+
import { Module } from '@nestjs/common';
|
|
259
|
+
import { CoreLicenseModule } from '@corelicense/node/nestjs';
|
|
260
|
+
import { loadOptionsFromEnv } from '@corelicense/node';
|
|
261
|
+
import { AutomationModule } from './automation/automation.module.js';
|
|
262
|
+
|
|
263
|
+
@Module({
|
|
264
|
+
imports: [
|
|
265
|
+
CoreLicenseModule.forRoot({
|
|
266
|
+
...loadOptionsFromEnv({
|
|
267
|
+
cachePath: './storage/corelicense',
|
|
268
|
+
instance: { strategy: 'install-id-domain-hash', domain: process.env.APP_DOMAIN },
|
|
269
|
+
}),
|
|
270
|
+
}),
|
|
271
|
+
AutomationModule,
|
|
272
|
+
],
|
|
273
|
+
})
|
|
274
|
+
export class AppModule {}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`CoreLicenseModule.forRoot()`:
|
|
278
|
+
|
|
279
|
+
- Tạo `CoreLicense`, gọi `boot()` trong factory
|
|
280
|
+
- Đăng ký `CoreLicenseGuard`, `CoreLicenseFeatureGuard`
|
|
281
|
+
- Đăng ký `CoreLicenseExceptionFilter` → HTTP 403 + `toClientPayload()`
|
|
282
|
+
|
|
283
|
+
### Controller — license active
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { Controller, Get, UseGuards } from '@nestjs/common';
|
|
287
|
+
import { CoreLicense } from '@corelicense/node';
|
|
288
|
+
import { CoreLicenseGuard } from '@corelicense/node/nestjs';
|
|
289
|
+
|
|
290
|
+
@UseGuards(CoreLicenseGuard)
|
|
291
|
+
@Controller('api')
|
|
292
|
+
export class ApiController {
|
|
293
|
+
constructor(private readonly license: CoreLicense) {}
|
|
294
|
+
|
|
295
|
+
@Get('status')
|
|
296
|
+
async status() {
|
|
297
|
+
return this.license.getStatus();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Controller — feature cụ thể
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import {
|
|
306
|
+
Controller,
|
|
307
|
+
Get,
|
|
308
|
+
UseGuards,
|
|
309
|
+
} from '@nestjs/common';
|
|
310
|
+
import {
|
|
311
|
+
CoreLicenseGuard,
|
|
312
|
+
CoreLicenseFeatureGuard,
|
|
313
|
+
RequireFeature,
|
|
314
|
+
} from '@corelicense/node/nestjs';
|
|
315
|
+
|
|
316
|
+
@RequireFeature('automation')
|
|
317
|
+
@UseGuards(CoreLicenseGuard, CoreLicenseFeatureGuard)
|
|
318
|
+
@Controller('automation')
|
|
319
|
+
export class AutomationController {
|
|
320
|
+
@Get()
|
|
321
|
+
list() {
|
|
322
|
+
return { items: [] };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Health controller (không guard)
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
@Controller('health')
|
|
331
|
+
export class HealthController {
|
|
332
|
+
@Get()
|
|
333
|
+
health() {
|
|
334
|
+
return { ok: true };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Đặt `HealthController` **không** dùng `CoreLicenseGuard` — hoặc dùng global guard với `@SetMetadata` skip (tùy app).
|
|
340
|
+
|
|
341
|
+
### Inject `CoreLicense` vào service
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import { Injectable } from '@nestjs/common';
|
|
345
|
+
import { CoreLicense, LicensedService } from '@corelicense/node';
|
|
346
|
+
|
|
347
|
+
@Injectable()
|
|
348
|
+
export class ExportService extends LicensedService {
|
|
349
|
+
constructor(license: CoreLicense) {
|
|
350
|
+
super(license);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async exportUsers() {
|
|
354
|
+
await this.require('export.users');
|
|
355
|
+
await this.license.consumeQuota('export.users', 1);
|
|
356
|
+
// business logic...
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## 6. Feature gate, limit & quota
|
|
364
|
+
|
|
365
|
+
### Feature
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
await license.requireFeature('automation');
|
|
369
|
+
await license.requireFeature('export.pdf');
|
|
370
|
+
await license.requireFeature('worker.video');
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Policy `features` ví dụ:
|
|
374
|
+
|
|
375
|
+
```json
|
|
376
|
+
{
|
|
377
|
+
"automation": true,
|
|
378
|
+
"export": true,
|
|
379
|
+
"export.pdf": false,
|
|
380
|
+
"worker.video": true
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`requireFeature` → audit log `feature_allowed`. Throw `FeatureDisabledError` (`FEATURE_DISABLED`).
|
|
385
|
+
|
|
386
|
+
### Limit (số hiện tại vs trần policy)
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
const running = await jobQueue.countActive();
|
|
390
|
+
await license.checkLimit('maxConcurrentJobs', running);
|
|
391
|
+
|
|
392
|
+
const userCount = await db.users.count();
|
|
393
|
+
await license.checkLimit('maxUsers', userCount);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Throw khi `currentValue >= limit` trong policy.
|
|
397
|
+
|
|
398
|
+
### Quota (tiêu thụ tích lũy theo ngày, local cache)
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
await license.consumeQuota('export.daily', 1);
|
|
402
|
+
await license.consumeQuota('api.calls', 10);
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- Usage lưu `usage.json` trong `cachePath`
|
|
406
|
+
- Reset theo ngày (UTC date string)
|
|
407
|
+
- `consumeQuota` / `checkLimit` / `getPolicy` đều gọi `requireActive()` trước
|
|
408
|
+
|
|
409
|
+
### Runtime config
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
const runtime = await license.getRuntimeConfig();
|
|
413
|
+
// {
|
|
414
|
+
// status: 'active',
|
|
415
|
+
// modules: { dashboard: true, automation: false, ... },
|
|
416
|
+
// limits: { maxWorkers: 2, maxUsers: 100 },
|
|
417
|
+
// policyVersion: 'v3',
|
|
418
|
+
// workers: { ... } // nếu policy định nghĩa
|
|
419
|
+
// }
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Dùng để **điều kiện hóa UI** hoặc config worker count — không thay thế `requireFeature` cho security.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## 7. Worker & CLI guard
|
|
427
|
+
|
|
428
|
+
### BullMQ / queue worker
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
import { Worker } from 'bullmq';
|
|
432
|
+
|
|
433
|
+
await license.boot();
|
|
434
|
+
await license.guardWorker('main-worker');
|
|
435
|
+
|
|
436
|
+
const worker = new Worker('jobs', async (job) => {
|
|
437
|
+
await license.requireFeature(`job.${job.name}`);
|
|
438
|
+
await license.consumeQuota(`job.${job.name}`, 1);
|
|
439
|
+
return processJob(job);
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
`guardWorker(name)` kiểm tra policy cho phép worker name — throw `WORKER_NOT_ALLOWED`.
|
|
444
|
+
|
|
445
|
+
### Cron / CLI
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
async function main() {
|
|
449
|
+
await license.boot();
|
|
450
|
+
await license.guardCommand('nightly-export');
|
|
451
|
+
await runExport();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
main().catch(console.error);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Throw `COMMAND_NOT_ALLOWED` nếu feature/command không licensed.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## 8. Tích hợp sâu — Module Registry & DI
|
|
462
|
+
|
|
463
|
+
### Khi nào dùng
|
|
464
|
+
|
|
465
|
+
- App có nhiều module lớn (automation, export, AI, ...)
|
|
466
|
+
- Muốn **không load code** module bị khóa license
|
|
467
|
+
- Muốn mount route / register provider có điều kiện
|
|
468
|
+
|
|
469
|
+
### `bootstrap()` — một lần sau `boot()`
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
const result = await license.bootstrap({
|
|
473
|
+
modules: ModuleDefinition[],
|
|
474
|
+
services: ServiceDefinition[],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// result.modules → LicensedModuleRegistry
|
|
478
|
+
// result.services → LicensedServiceContainer
|
|
479
|
+
// result.skipped → [{ name, feature, reason: 'feature_disabled' }]
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### `ModuleDefinition`
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
{
|
|
486
|
+
name: 'automation', // unique id
|
|
487
|
+
feature: 'automation', // key trong policy.features
|
|
488
|
+
factory: ({ license, runtime }) => new AutomationModule(license),
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### `ServiceDefinition`
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
{
|
|
496
|
+
name: 'ExportService',
|
|
497
|
+
feature: 'export',
|
|
498
|
+
singleton: true, // default true — factory chạy 1 lần
|
|
499
|
+
factory: ({ license }) => new ExportService(license),
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### `modules.get(name)`
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
const automation = await modules.get<AutomationModule>('automation');
|
|
507
|
+
await automation.runJob(payload);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
- Module chưa registered → `ModuleNotRegisteredError`
|
|
511
|
+
- Feature off lúc bootstrap → không registered → `ModuleNotLicensedError`
|
|
512
|
+
- **Mỗi `get()`** gọi `requireFeature` — revoke suspend chặn ngay
|
|
513
|
+
|
|
514
|
+
### `services.resolve(name)`
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
const svc = await services.resolve<ExportService>('ExportService');
|
|
518
|
+
await svc.exportUsers();
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- Singleton: instance cache nội bộ, nhưng **mỗi `resolve()` re-check license**
|
|
522
|
+
- Re-check **không** ghi audit (khác `license.requireFeature()`)
|
|
523
|
+
|
|
524
|
+
### Tự tạo registry (advanced)
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
const modules = license.createLicensedRegistry();
|
|
528
|
+
const services = license.createLicensedContainer();
|
|
529
|
+
|
|
530
|
+
const ctx = {
|
|
531
|
+
license,
|
|
532
|
+
runtime: await license.getRuntimeConfig(),
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
await modules.bootstrap(definitions, ctx);
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### `LicensedService` base class
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
import { LicensedService, CoreLicense } from '@corelicense/node';
|
|
542
|
+
|
|
543
|
+
export class ExportService extends LicensedService {
|
|
544
|
+
constructor(license: CoreLicense) {
|
|
545
|
+
super(license);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async exportUsers() {
|
|
549
|
+
await this.require('export.users');
|
|
550
|
+
await this.license.consumeQuota('export.users', 1);
|
|
551
|
+
// ...
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## 9. Pattern ứng dụng lớn
|
|
559
|
+
|
|
560
|
+
### Cấu trúc thư mục gợi ý
|
|
561
|
+
|
|
562
|
+
```
|
|
563
|
+
src/
|
|
564
|
+
bootstrap/
|
|
565
|
+
license.ts # khởi tạo CoreLicense + boot()
|
|
566
|
+
modules.ts # định nghĩa bootstrap modules/services
|
|
567
|
+
modules/
|
|
568
|
+
automation/
|
|
569
|
+
automation.module.ts
|
|
570
|
+
automation.router.ts
|
|
571
|
+
export/
|
|
572
|
+
export.service.ts
|
|
573
|
+
app.ts # express + registerLicensedRoutes
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### `bootstrap/license.ts`
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
import { CoreLicense, loadOptionsFromEnv } from '@corelicense/node';
|
|
580
|
+
|
|
581
|
+
let license: CoreLicense;
|
|
582
|
+
|
|
583
|
+
export async function initLicense() {
|
|
584
|
+
license = new CoreLicense(loadOptionsFromEnv({ cachePath: './storage/corelicense' }));
|
|
585
|
+
await license.boot();
|
|
586
|
+
return license;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function getLicense() {
|
|
590
|
+
if (!license) throw new Error('License not initialized');
|
|
591
|
+
return license;
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### `bootstrap/modules.ts`
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
import type { BootstrapResult } from '@corelicense/node';
|
|
599
|
+
import { getLicense } from './license.js';
|
|
600
|
+
import { AutomationModule } from '../modules/automation/automation.module.js';
|
|
601
|
+
import { ExportService } from '../modules/export/export.service.js';
|
|
602
|
+
|
|
603
|
+
export async function bootstrapApp(): Promise<BootstrapResult> {
|
|
604
|
+
return getLicense().bootstrap({
|
|
605
|
+
modules: [
|
|
606
|
+
{ name: 'automation', feature: 'automation', factory: ({ license }) => new AutomationModule(license) },
|
|
607
|
+
],
|
|
608
|
+
services: [
|
|
609
|
+
{ name: 'ExportService', feature: 'export', factory: ({ license }) => new ExportService(license) },
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Graceful shutdown
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
process.on('SIGTERM', () => {
|
|
619
|
+
license.stopBackgroundCheck();
|
|
620
|
+
process.exit(0);
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Docker
|
|
625
|
+
|
|
626
|
+
```dockerfile
|
|
627
|
+
ENV CORELICENSE_KEY=...
|
|
628
|
+
ENV CORELICENSE_PRODUCT_ID=...
|
|
629
|
+
ENV APP_DOMAIN=app.customer.com
|
|
630
|
+
VOLUME ["/app/storage/corelicense"]
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Persist `cachePath` — mất volume = mất install-id → tính instance mới.
|
|
634
|
+
|
|
635
|
+
### Multi-tenant
|
|
636
|
+
|
|
637
|
+
- **Một license / một deploy** (khuyến nghị): mỗi customer một deployment + key riêng
|
|
638
|
+
- Không share một `CoreLicense` instance cho nhiều tenant — instance ID và policy gắn một license
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## 10. Xử lý lỗi
|
|
643
|
+
|
|
644
|
+
### Type guard
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
import {
|
|
648
|
+
isLicenseError,
|
|
649
|
+
formatLicenseError,
|
|
650
|
+
LicenseErrorCode,
|
|
651
|
+
} from '@corelicense/node';
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
await license.boot();
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (isLicenseError(error)) {
|
|
657
|
+
console.error(error.code, error.source, error.message);
|
|
658
|
+
// CONFIG_MISSING_ENV, LICENSE_REVOKED, OFFLINE_GRACE_EXPIRED, ...
|
|
659
|
+
}
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### HTTP API
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
if (isLicenseError(error)) {
|
|
668
|
+
res.status(403).json(error.toClientPayload());
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Mã lỗi thường gặp
|
|
673
|
+
|
|
674
|
+
| Code | Ý nghĩa |
|
|
675
|
+
|------|---------|
|
|
676
|
+
| `CONFIG_MISSING_ENV` | Thiếu `CORELICENSE_KEY` / `CORELICENSE_PRODUCT_ID` |
|
|
677
|
+
| `LICENSE_REVOKED` | Key bị revoke |
|
|
678
|
+
| `LICENSE_SUSPENDED` | Tạm ngưng |
|
|
679
|
+
| `LICENSE_EXPIRED` | Hết hạn |
|
|
680
|
+
| `OFFLINE_GRACE_EXPIRED` | Server unreachable quá lâu |
|
|
681
|
+
| `FEATURE_DISABLED` | Feature không trong policy |
|
|
682
|
+
| `MODULE_NOT_LICENSED` | Module không bind / feature off |
|
|
683
|
+
| `QUOTA_EXCEEDED` | Vượt quota ngày |
|
|
684
|
+
| `MAX_INSTANCES` | Vượt số instance |
|
|
685
|
+
| `INSTANCE_BLOCKED` | Instance bị block trên admin |
|
|
686
|
+
| `POLICY_SIGNATURE_ERROR` | Token không verify được |
|
|
687
|
+
|
|
688
|
+
Xem đầy đủ: [API Reference — Error codes](./api-reference.md#error-codes)
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## 11. Events & background sync
|
|
693
|
+
|
|
694
|
+
### Event bus
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
license.on('licenseRevoked', (payload) => {
|
|
698
|
+
console.error('License revoked', payload);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
license.on('policyUpdated', (payload) => {
|
|
703
|
+
console.log('Policy updated', payload);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
license.on('offlineMode', (payload) => {
|
|
707
|
+
console.warn('Offline mode', payload);
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Events: `statusChanged`, `policyUpdated`, `licenseRevoked`, `licenseSuspended`, `offlineMode`
|
|
712
|
+
|
|
713
|
+
### Background check
|
|
714
|
+
|
|
715
|
+
`boot()` tự start nếu `checkIntervalMs > 0`. Thủ công:
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
license.startBackgroundCheck();
|
|
719
|
+
license.stopBackgroundCheck();
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
`syncFromServer` có **mutex** — parallel refresh không race.
|
|
723
|
+
|
|
724
|
+
### Manual refresh
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
await license.refresh();
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
Endpoint `refresh` trên License Server.
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## 12. Bảo mật & dữ liệu gửi server
|
|
735
|
+
|
|
736
|
+
SDK **chỉ gửi**:
|
|
737
|
+
|
|
738
|
+
- `productId`, `licenseKey`, `instanceId`
|
|
739
|
+
- SDK metadata (name, version, runtime)
|
|
740
|
+
- `domainHash` (hash, không plain domain nếu cấu hình đúng)
|
|
741
|
+
- `currentPolicyHash` (khi check)
|
|
742
|
+
|
|
743
|
+
**Không gửi:** database, user data, business logs.
|
|
744
|
+
|
|
745
|
+
### apiUrl pin (v0.5)
|
|
746
|
+
|
|
747
|
+
Server có thể trả `apiUrl` trong response. SDK **chỉ chấp nhận** URL trong whitelist (trusted apiUrl, env, embedded key). Redirect độc hại bị reject + audit `api_url_redirect_rejected`.
|
|
748
|
+
|
|
749
|
+
### Public key
|
|
750
|
+
|
|
751
|
+
- Mặc định: fetch `GET /v1/public/products/{productId}/keys`
|
|
752
|
+
- Enterprise: pin `CORELICENSE_PUBLIC_KEY`
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## 13. Production checklist
|
|
757
|
+
|
|
758
|
+
- [ ] `CORELICENSE_KEY` + `CORELICENSE_PRODUCT_ID` trong env (secret manager)
|
|
759
|
+
- [ ] `APP_DOMAIN` / `instance.domain` set đúng
|
|
760
|
+
- [ ] `cachePath` persist (volume)
|
|
761
|
+
- [ ] `boot()` fail → `process.exit(1)` — không start app “half-licensed”
|
|
762
|
+
- [ ] `/health` trong `allowPaths`
|
|
763
|
+
- [ ] License Server: `SDK_PUBLIC_URL=https://api.corelicense.net`
|
|
764
|
+
- [ ] Policy signer configured trên Go API
|
|
765
|
+
- [ ] Log audit path có rotate
|
|
766
|
+
- [ ] Test revoke: admin revoke → app chặn trong vòng refresh / ngay tại `requireFeature`
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## 14. Troubleshooting
|
|
771
|
+
|
|
772
|
+
| Triệu chứng | Nguyên nhân | Xử lý |
|
|
773
|
+
|-------------|-------------|--------|
|
|
774
|
+
| `CONFIG_MISSING_ENV` | Thiếu env | Set `CORELICENSE_*` |
|
|
775
|
+
| `boot()` fail offline | Grace hết | Kết nối server hoặc tạm `offlineGrace: false` chỉ khi hiểu rủi ro |
|
|
776
|
+
| `MAX_INSTANCES` | Quá số instance | Xóa instance cũ trên admin hoặc tăng limit |
|
|
777
|
+
| `POLICY_SIGNATURE_ERROR` | Key rotation / sai pin | Cập nhật public key hoặc bỏ pin |
|
|
778
|
+
| Middleware 403 mọi route | Thiếu `allowPaths` | Thêm `/health`, ... |
|
|
779
|
+
| Module 404 | Feature off | Đúng thiết kế — `registerLicensedRoutes` skip |
|
|
780
|
+
| Nest 403 không JSON chuẩn | Thiếu filter | Dùng `CoreLicenseModule.forRoot()` (đã có filter) |
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
## Liên kết
|
|
785
|
+
|
|
786
|
+
- [Cài đặt](./installation.md)
|
|
787
|
+
- [API Reference](./api-reference.md)
|
|
788
|
+
- [Client API (License Server)](https://corelicense.net/docs/client-api)
|
|
789
|
+
- [README](../README.md)
|