@nnsi/sdk-electron 0.1.0
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/README.md +432 -0
- package/index.js +41 -0
- package/package.json +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# @nnsi/sdk-electron
|
|
2
|
+
|
|
3
|
+
Официальный SDK для Windows-приложений на Electron, которые распространяются и запускаются через **NNSI App**.
|
|
4
|
+
|
|
5
|
+
SDK проверяет короткоживущий подписанный `launch ticket`. Лаунчер получает ticket после проверки пользователя и лицензии в Supabase, а приложение проверяет его локально по публичному RSA-ключу.
|
|
6
|
+
|
|
7
|
+
> Важно: SDK не заменяет code signing, DRM и серверную авторизацию критичных операций. Клиентское приложение работает на компьютере пользователя, поэтому особо важную логику не следует полностью доверять клиенту.
|
|
8
|
+
|
|
9
|
+
## Архитектура
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Вход в NNSI App
|
|
13
|
+
-> Edge Function получает access token, app_id, machine_id
|
|
14
|
+
-> проверяет app_licenses
|
|
15
|
+
-> подписывает RS256 launch ticket
|
|
16
|
+
-> лаунчер передаёт ticket приложению через environment
|
|
17
|
+
-> приложение проверяет ticket через SDK
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Приватный RSA-ключ хранится только в Edge Function. В приложении и лаунчере находится только public key: им можно проверять подпись, но нельзя выпускать tickets.
|
|
21
|
+
|
|
22
|
+
## Установка
|
|
23
|
+
|
|
24
|
+
Для локальной разработки добавьте в `package.json` приложения:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@nnsi/sdk-electron": "file:../sdk-electron"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Путь считается относительно `package.json`. Затем:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
npm install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
После публикации в npm-реестре:
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
npm install @nnsi/sdk-electron
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
В production фиксируйте точную версию пакета.
|
|
47
|
+
|
|
48
|
+
## Быстрое подключение
|
|
49
|
+
|
|
50
|
+
Проверку нужно выполнять в главном процессе Electron, до создания основных окон:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
const { app, dialog } = require('electron');
|
|
54
|
+
const { requireLauncher } = require('@nnsi/sdk-electron');
|
|
55
|
+
|
|
56
|
+
let nnsiContext;
|
|
57
|
+
try {
|
|
58
|
+
nnsiContext = requireLauncher({
|
|
59
|
+
appId: 'myapp',
|
|
60
|
+
publicKey: process.env.NNSI_TICKET_PUBLIC_KEY
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
dialog.showErrorBox(
|
|
64
|
+
'My App',
|
|
65
|
+
error instanceof Error ? error.message : 'Запуск через NNSI App не подтверждён.'
|
|
66
|
+
);
|
|
67
|
+
app.quit();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log('NNSI user:', nnsiContext.user_id);
|
|
72
|
+
// Только после этого создаём BrowserWindow.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Проверки только в renderer недостаточно: основной процесс должен завершиться до создания интерфейса.
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const {
|
|
81
|
+
requireLauncher,
|
|
82
|
+
getLaunchContext,
|
|
83
|
+
verifyTicket
|
|
84
|
+
} = require('@nnsi/sdk-electron');
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `requireLauncher(options)`
|
|
88
|
+
|
|
89
|
+
Читает ticket из environment и проверяет его. При ошибке выбрасывает `Error`.
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const context = requireLauncher({
|
|
93
|
+
appId: 'tiktimer',
|
|
94
|
+
publicKey: process.env.NNSI_TICKET_PUBLIC_KEY
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Параметры:
|
|
99
|
+
|
|
100
|
+
| Параметр | Тип | Назначение |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `appId` | string | ID приложения; ticket другого приложения будет отклонён |
|
|
103
|
+
| `publicKey` | string | RSA public key в PEM-формате |
|
|
104
|
+
| `issuer` | string | Издатель; по умолчанию `nnsi-app` |
|
|
105
|
+
| `ticketEnv` | string | Имя переменной ticket; по умолчанию `NNSI_LAUNCH_TICKET` |
|
|
106
|
+
|
|
107
|
+
### `getLaunchContext(options)`
|
|
108
|
+
|
|
109
|
+
Псевдоним `requireLauncher`, возвращает payload проверенного ticket:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const context = getLaunchContext({
|
|
113
|
+
appId: 'myapp',
|
|
114
|
+
publicKey: PUBLIC_KEY
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `verifyTicket(ticket, options)`
|
|
119
|
+
|
|
120
|
+
Проверяет уже прочитанную JWT-строку. Полезно для тестов:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
const context = verifyTicket(ticket, {
|
|
124
|
+
appId: 'myapp',
|
|
125
|
+
publicKey: PUBLIC_KEY
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Проверяются формат JWT, алгоритм `RS256`, подпись RSA-SHA256, `iss`, `app_id`, `exp` и `nbf`. Payload возвращается замороженным через `Object.freeze()`.
|
|
130
|
+
|
|
131
|
+
## Environment-переменные
|
|
132
|
+
|
|
133
|
+
Лаунчер передаёт:
|
|
134
|
+
|
|
135
|
+
| Переменная | Значение |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `NNSI_LAUNCH_TICKET` | Подписанный короткоживущий JWT |
|
|
138
|
+
| `NNSI_TICKET_PUBLIC_KEY` | Публичный RSA-ключ в PEM |
|
|
139
|
+
|
|
140
|
+
Для локального теста:
|
|
141
|
+
|
|
142
|
+
```powershell
|
|
143
|
+
$env:NNSI_LAUNCH_TICKET = 'test-ticket'
|
|
144
|
+
$env:NNSI_TICKET_PUBLIC_KEY = (Get-Content .\keys\nnsi-public.pem -Raw)
|
|
145
|
+
npm start
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Не храните production ticket, private key или service role key в `.env`, Git, EXE или ZIP.
|
|
149
|
+
|
|
150
|
+
## Формат launch ticket
|
|
151
|
+
|
|
152
|
+
Ticket — JWT с подписью `RS256`:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"iss": "nnsi-app",
|
|
157
|
+
"sub": "supabase-user-uuid",
|
|
158
|
+
"user_id": "supabase-user-uuid",
|
|
159
|
+
"app_id": "tiktimer",
|
|
160
|
+
"machine_id": "stable-machine-id",
|
|
161
|
+
"jti": "unique-ticket-id",
|
|
162
|
+
"iat": 1770000000,
|
|
163
|
+
"nbf": 1769999995,
|
|
164
|
+
"exp": 1770000300
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- `iss` — издатель;
|
|
169
|
+
- `sub`, `user_id` — пользователь Supabase;
|
|
170
|
+
- `app_id` — разрешённое приложение;
|
|
171
|
+
- `machine_id` — компьютер лицензии;
|
|
172
|
+
- `jti` — уникальный ticket;
|
|
173
|
+
- `iat`, `nbf`, `exp` — времена выпуска, начала и окончания действия в Unix seconds.
|
|
174
|
+
|
|
175
|
+
Текущая Edge Function выдаёт ticket на 5 минут. Перед каждым запуском лаунчер должен получать новый ticket.
|
|
176
|
+
|
|
177
|
+
## Генерация RSA-ключей
|
|
178
|
+
|
|
179
|
+
```powershell
|
|
180
|
+
New-Item -ItemType Directory -Force .\keys | Out-Null
|
|
181
|
+
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out .\keys\nnsi-private.pem
|
|
182
|
+
openssl rsa -pubout -in .\keys\nnsi-private.pem -out .\keys\nnsi-public.pem
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Private key должен начинаться с:
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
-----BEGIN PRIVATE KEY-----
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Добавьте в `.gitignore`:
|
|
192
|
+
|
|
193
|
+
```gitignore
|
|
194
|
+
keys/
|
|
195
|
+
*.pem
|
|
196
|
+
.env
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Supabase Edge Function
|
|
200
|
+
|
|
201
|
+
Функция находится в:
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
supabase/functions/issue-launch-ticket/index.ts
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Она:
|
|
208
|
+
|
|
209
|
+
1. принимает Bearer access token;
|
|
210
|
+
2. получает пользователя через `auth.getUser()`;
|
|
211
|
+
3. принимает `app_id` и `machine_id`;
|
|
212
|
+
4. ищет активную запись `public.app_licenses`;
|
|
213
|
+
5. проверяет `expires_at`;
|
|
214
|
+
6. подписывает ticket;
|
|
215
|
+
7. пишет аудит в `public.license_launches`;
|
|
216
|
+
8. возвращает ticket.
|
|
217
|
+
|
|
218
|
+
Тело запроса:
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"app_id": "tiktimer",
|
|
223
|
+
"machine_id": "computer-identifier"
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Успех:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"ticket": "eyJ...",
|
|
232
|
+
"expires_at": "2026-09-05T12:00:00.000Z"
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Если лицензия не найдена, функция возвращает HTTP 403.
|
|
237
|
+
|
|
238
|
+
### Схема и лицензия
|
|
239
|
+
|
|
240
|
+
Выполните актуальный `launcher/supabase-schema.sql`. Для теста добавьте лицензию:
|
|
241
|
+
|
|
242
|
+
```sql
|
|
243
|
+
insert into public.app_licenses
|
|
244
|
+
(user_id, app_id, machine_id, is_active, expires_at)
|
|
245
|
+
values
|
|
246
|
+
('USER_UUID', 'tiktimer', 'computer-identifier', true, null);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`user_id` должен быть UUID текущего пользователя, а `machine_id` — тем же значением, которое отправляет лаунчер.
|
|
250
|
+
|
|
251
|
+
### Deploy и secrets
|
|
252
|
+
|
|
253
|
+
```powershell
|
|
254
|
+
supabase login
|
|
255
|
+
supabase link --project-ref YOUR_PROJECT_REF
|
|
256
|
+
supabase secrets set SUPABASE_SERVICE_ROLE_KEY="YOUR_SERVICE_ROLE_KEY"
|
|
257
|
+
supabase secrets set NNSI_TICKET_PRIVATE_KEY="YOUR_PRIVATE_RSA_PEM"
|
|
258
|
+
supabase functions deploy issue-launch-ticket
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Private key и service role key задаются только как secrets Edge Function. Если CLI некорректно передаёт многострочный PEM, задайте его через Supabase Dashboard.
|
|
262
|
+
|
|
263
|
+
## Подключение лаунчера
|
|
264
|
+
|
|
265
|
+
Лаунчер вызывает Edge Function с access token текущей Supabase-сессии:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
async function issueLaunchTicket({ supabaseUrl, anonKey, accessToken, appId, machineId }) {
|
|
269
|
+
const response = await fetch(
|
|
270
|
+
`${supabaseUrl}/functions/v1/issue-launch-ticket`,
|
|
271
|
+
{
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: {
|
|
274
|
+
Authorization: `Bearer ${accessToken}`,
|
|
275
|
+
apikey: anonKey,
|
|
276
|
+
'Content-Type': 'application/json'
|
|
277
|
+
},
|
|
278
|
+
body: JSON.stringify({ app_id: appId, machine_id: machineId })
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const data = await response.json();
|
|
283
|
+
if (!response.ok) throw new Error(data.error || `Ticket request failed: ${response.status}`);
|
|
284
|
+
return data;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Запуск дочернего процесса:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
const childEnv = {
|
|
292
|
+
...process.env,
|
|
293
|
+
NNSI_LAUNCH_TICKET: ticket,
|
|
294
|
+
NNSI_TICKET_PUBLIC_KEY: publicKey
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
delete childEnv.ELECTRON_RUN_AS_NODE;
|
|
298
|
+
|
|
299
|
+
spawn(executablePath, [], {
|
|
300
|
+
cwd: appDirectory,
|
|
301
|
+
env: childEnv,
|
|
302
|
+
windowsHide: false
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Public key лучше хранить в самом лаунчере или подписанной конфигурации, а не брать из непроверенного каталога приложения.
|
|
307
|
+
|
|
308
|
+
## Модель лицензий
|
|
309
|
+
|
|
310
|
+
Пользователь не должен иметь публичные INSERT/UPDATE policies на `app_licenses`. Иначе он сможет выдать себе лицензию.
|
|
311
|
+
|
|
312
|
+
Рекомендуется:
|
|
313
|
+
|
|
314
|
+
- публичный доступ только к опубликованному каталогу;
|
|
315
|
+
- чтение лицензии через Edge Function с service role;
|
|
316
|
+
- изменение `is_active`, `expires_at`, `app_id` и `machine_id` только через админ-панель или защищённую серверную функцию;
|
|
317
|
+
- аудит запусков в `license_launches`.
|
|
318
|
+
|
|
319
|
+
Старую клиентскую логику TikTimer, которая напрямую читает или меняет `app_licenses`, нужно заменить серверным API. Не ослабляйте RLS ради совместимости.
|
|
320
|
+
|
|
321
|
+
## Пример для TikTimer
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
const { app, dialog } = require('electron');
|
|
325
|
+
const { getLaunchContext } = require('@nnsi/sdk-electron');
|
|
326
|
+
|
|
327
|
+
let nnsiContext;
|
|
328
|
+
try {
|
|
329
|
+
nnsiContext = getLaunchContext({
|
|
330
|
+
appId: 'tiktimer',
|
|
331
|
+
publicKey: process.env.NNSI_TICKET_PUBLIC_KEY
|
|
332
|
+
});
|
|
333
|
+
} catch (error) {
|
|
334
|
+
dialog.showErrorBox(
|
|
335
|
+
'TikTimer',
|
|
336
|
+
'TikTimer можно запустить только через NNSI App.\n\n' +
|
|
337
|
+
(error instanceof Error ? error.message : 'Проверка не пройдена.')
|
|
338
|
+
);
|
|
339
|
+
app.quit();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log('Authorized user:', nnsiContext.user_id);
|
|
344
|
+
// Создание BrowserWindow только здесь, после проверки.
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
После добавления зависимости:
|
|
348
|
+
|
|
349
|
+
```powershell
|
|
350
|
+
npm install
|
|
351
|
+
npm run build
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Проверьте, что SDK попал в `resources/app.asar` и включён в `build.files`.
|
|
355
|
+
|
|
356
|
+
## Тестовый чек-лист
|
|
357
|
+
|
|
358
|
+
Проверьте все сценарии:
|
|
359
|
+
|
|
360
|
+
1. Запуск EXE напрямую без env — отказ и отсутствие главного окна.
|
|
361
|
+
2. Запуск через лаунчер с активной лицензией — успех.
|
|
362
|
+
3. Неверная пара RSA-ключей — отказ.
|
|
363
|
+
4. Ticket с другим `app_id` — отказ.
|
|
364
|
+
5. Просроченный ticket — отказ.
|
|
365
|
+
6. Удалённая или истёкшая лицензия — Edge Function возвращает 403.
|
|
366
|
+
7. Перезапуск после истечения ticket — лаунчер получает новый ticket.
|
|
367
|
+
8. Сборка на чистом ПК — модуль SDK находится внутри приложения.
|
|
368
|
+
|
|
369
|
+
## Ошибки
|
|
370
|
+
|
|
371
|
+
| Сообщение | Причина |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `This application can only be launched through NNSI App` | Нет `NNSI_LAUNCH_TICKET` |
|
|
374
|
+
| `NNSI ticket public key is not configured` | Не передан public key |
|
|
375
|
+
| `Invalid NNSI launch ticket signature` | Public key не соответствует private key или ticket повреждён |
|
|
376
|
+
| `NNSI ticket belongs to another application` | Не совпали `appId` и `app_id` |
|
|
377
|
+
| `NNSI launch ticket has expired` | Нужно запросить новый ticket |
|
|
378
|
+
| `Active license not found` | Неверны user, app, machine или состояние лицензии |
|
|
379
|
+
| `Cannot find module '@nnsi/sdk-electron'` | SDK не установлен или не попал в сборку |
|
|
380
|
+
|
|
381
|
+
В логах не записывайте полный ticket:
|
|
382
|
+
|
|
383
|
+
```js
|
|
384
|
+
console.log({
|
|
385
|
+
hasTicket: Boolean(process.env.NNSI_LAUNCH_TICKET),
|
|
386
|
+
hasPublicKey: Boolean(process.env.NNSI_TICKET_PUBLIC_KEY)
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Безопасность
|
|
391
|
+
|
|
392
|
+
1. Никогда не помещайте private key и service role key в Electron, GitHub, EXE, ZIP или renderer.
|
|
393
|
+
2. Используйте HTTPS и актуальный Electron.
|
|
394
|
+
3. Выдавайте короткоживущий ticket перед каждым запуском.
|
|
395
|
+
4. Проверяйте `app_id` и на сервере, и в приложении.
|
|
396
|
+
5. Подписывайте Windows-сборки code-signing сертификатом.
|
|
397
|
+
6. После утечки private key создайте новую RSA-пару, смените secret и выпустите приложения с новым public key.
|
|
398
|
+
7. Ранее опубликованные Supabase secret/service-role ключи считайте скомпрометированными и ротируйте.
|
|
399
|
+
|
|
400
|
+
SDK не может сделать локальный EXE абсолютно не взламываемым: пользователь контролирует файлы на своём компьютере. Его задача — связать официальный запуск с серверной лицензией и повысить стоимость обхода.
|
|
401
|
+
|
|
402
|
+
## Публикация SDK
|
|
403
|
+
|
|
404
|
+
Перед публикацией:
|
|
405
|
+
|
|
406
|
+
```powershell
|
|
407
|
+
node --check .\sdk-electron\index.js
|
|
408
|
+
node -e "require('./sdk-electron')"
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Увеличьте `version` в `sdk-electron/package.json`, затем:
|
|
412
|
+
|
|
413
|
+
```powershell
|
|
414
|
+
cd sdk-electron
|
|
415
|
+
npm publish --access restricted
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
В приложении зафиксируйте новую версию и обновите lock-файл:
|
|
419
|
+
|
|
420
|
+
```powershell
|
|
421
|
+
npm install
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Файлы проекта
|
|
425
|
+
|
|
426
|
+
```text
|
|
427
|
+
sdk-electron/index.js SDK и проверка RS256
|
|
428
|
+
sdk-electron/package.json метаданные npm-пакета
|
|
429
|
+
supabase/functions/issue-launch-ticket/index.ts выдача tickets
|
|
430
|
+
launcher/supabase-schema.sql таблицы лицензий и аудита
|
|
431
|
+
```
|
|
432
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function decodeBase64Url(value) {
|
|
6
|
+
const normalized = value.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(value.length / 4) * 4, '=');
|
|
7
|
+
return Buffer.from(normalized, 'base64');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readJsonPart(value) {
|
|
11
|
+
return JSON.parse(decodeBase64Url(value).toString('utf8'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function verifyTicket(ticket, options) {
|
|
15
|
+
if (typeof ticket !== 'string' || ticket.split('.').length !== 3) throw new Error('NNSI launch ticket is missing or malformed');
|
|
16
|
+
const [encodedHeader, encodedPayload, encodedSignature] = ticket.split('.');
|
|
17
|
+
const header = readJsonPart(encodedHeader);
|
|
18
|
+
const payload = readJsonPart(encodedPayload);
|
|
19
|
+
if (header.alg !== 'RS256') throw new Error('Unsupported NNSI ticket algorithm');
|
|
20
|
+
if (!options.publicKey) throw new Error('NNSI ticket public key is not configured');
|
|
21
|
+
const valid = crypto.verify('RSA-SHA256', Buffer.from(`${encodedHeader}.${encodedPayload}`), options.publicKey, decodeBase64Url(encodedSignature));
|
|
22
|
+
if (!valid) throw new Error('Invalid NNSI launch ticket signature');
|
|
23
|
+
const now = Math.floor(Date.now() / 1000);
|
|
24
|
+
if (payload.iss !== (options.issuer || 'nnsi-app')) throw new Error('Invalid NNSI ticket issuer');
|
|
25
|
+
if (options.appId && payload.app_id !== options.appId) throw new Error('NNSI ticket belongs to another application');
|
|
26
|
+
if (!payload.exp || payload.exp <= now) throw new Error('NNSI launch ticket has expired');
|
|
27
|
+
if (payload.nbf && payload.nbf > now + 30) throw new Error('NNSI launch ticket is not active yet');
|
|
28
|
+
return Object.freeze(payload);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function requireLauncher(options = {}) {
|
|
32
|
+
const ticket = process.env[options.ticketEnv || 'NNSI_LAUNCH_TICKET'];
|
|
33
|
+
if (!ticket) throw new Error('This application can only be launched through NNSI App');
|
|
34
|
+
return verifyTicket(ticket, options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getLaunchContext(options = {}) {
|
|
38
|
+
return requireLauncher(options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { requireLauncher, getLaunchContext, verifyTicket };
|