session-durability-b-kn.test.mjs
571 lines 20.8 KB
Raw
sha256:fbe982a22c05c6fe2e93876f250deecdb43d883f648b4e1810c7546caa9f17db docs: activate KNOWTATION- board identity and preserve livi… Human minor ⚠ breaking 5 days ago
1 /**
2 * SESSION-DURABILITY-b-KN — seven-tier matrix for hosted human-session continuity.
3 * Freeze: ~/scooling/docs/reviews/2026-08-28-session-durability.md §§4,6,7 (KN boundary).
4 */
5
6 import { describe, it, before, after } from 'node:test';
7 import assert from 'node:assert/strict';
8 import http from 'node:http';
9 import crypto from 'node:crypto';
10 import fs from 'node:fs';
11 import path from 'node:path';
12 import { fileURLToPath, pathToFileURL } from 'node:url';
13 import {
14 parseHostedHubJwtExpirySeconds,
15 verifyHumanSessionAccessToken,
16 humanSessionClaimsShapeOk,
17 admitHumanSessionPayload,
18 isEstablishRefreshBrowserOriginAllowed,
19 acceptIncludesRefreshTokenCli,
20 REFRESH_TOKEN_CLI_ACCEPT,
21 HUMAN_SESSION_LIFETIME_MIN_SECONDS,
22 HUMAN_SESSION_LIFETIME_MAX_SECONDS,
23 } from '../hub/lib/human-session-admission.mjs';
24 import { isWwwApexPair } from '../hub/gateway/cors-middleware.mjs';
25 import {
26 createEstablishRefreshHandler,
27 REFRESH_COOKIE_NAME,
28 } from '../hub/auth-session.mjs';
29
30 const __dirname = path.dirname(fileURLToPath(import.meta.url));
31 const ROOT = path.resolve(__dirname, '..');
32 const SECRET = 'session-durability-kn-test-secret-32b';
33 const HUB_JS = fs.readFileSync(path.join(ROOT, 'web', 'hub', 'hub.js'), 'utf8');
34 const INDEX_HTML = fs.readFileSync(path.join(ROOT, 'web', 'hub', 'index.html'), 'utf8');
35 const SERVER_SRC = fs.readFileSync(path.join(ROOT, 'hub', 'gateway', 'server.mjs'), 'utf8');
36 const AUTH_SESSION_SRC = fs.readFileSync(path.join(ROOT, 'hub', 'auth-session.mjs'), 'utf8');
37 const CLI_AUTH_SRC = fs.readFileSync(path.join(ROOT, 'scripts', 'lib', 'hub-session-auth.mjs'), 'utf8');
38
39 function makeJwt(payload, secret = SECRET) {
40 const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
41 const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
42 const sig = crypto
43 .createHmac('sha256', secret)
44 .update(`${header}.${body}`)
45 .digest('base64url');
46 return `${header}.${body}.${sig}`;
47 }
48
49 function sessionPayload(overrides = {}) {
50 const now = Math.floor(Date.now() / 1000);
51 return {
52 sub: 'google:durability-user',
53 provider: 'google',
54 id: 'durability-user',
55 name: 'Durability',
56 role: 'member',
57 type: 'session',
58 iat: now - 60,
59 exp: now - 60 + HUMAN_SESSION_LIFETIME_MIN_SECONDS,
60 ...overrides,
61 };
62 }
63
64 function mockRes() {
65 const headers = {};
66 const cookies = {};
67 /** @type {string[]} */
68 const cleared = [];
69 return {
70 statusCode: 200,
71 body: null,
72 set(name, value) {
73 headers[String(name).toLowerCase()] = String(value);
74 },
75 cookie(name, value) {
76 cookies[name] = value;
77 },
78 clearCookie(name) {
79 cleared.push(name);
80 },
81 status(code) {
82 this.statusCode = code;
83 return this;
84 },
85 json(payload) {
86 this.body = payload;
87 return this;
88 },
89 headers,
90 cookies,
91 cleared,
92 };
93 }
94
95 function startServer(app) {
96 const srv = http.createServer(app);
97 return new Promise((resolve, reject) => {
98 srv.listen(0, '127.0.0.1', (err) => {
99 if (err) return reject(err);
100 resolve({
101 url: `http://127.0.0.1:${srv.address().port}`,
102 close: () => new Promise((r) => srv.close(() => r())),
103 });
104 });
105 });
106 }
107
108 async function createGateway(envExtra = {}) {
109 process.env.NETLIFY = '1';
110 process.env.SESSION_SECRET = SECRET;
111 process.env.BILLING_ENFORCE = 'false';
112 process.env.CANISTER_URL = '';
113 process.env.BRIDGE_URL = '';
114 delete process.env.HUB_JWT_EXPIRY;
115 for (const [k, v] of Object.entries(envExtra)) {
116 if (v == null) delete process.env[k];
117 else process.env[k] = v;
118 }
119 const entry = pathToFileURL(path.join(ROOT, 'hub', 'gateway', 'server.mjs')).href;
120 const { app } = await import(`${entry}?sdkn=${Date.now()}-${Math.random()}`);
121 return startServer(app);
122 }
123
124 // ─── 1. UNIT ─────────────────────────────────────────────────────────────────
125
126 describe('SESSION-DURABILITY-b-KN unit', () => {
127 it('hosted expiry parser accepts integer seconds and N[smhd] in 3h–24h only', () => {
128 assert.equal(parseHostedHubJwtExpirySeconds('24h').ok, true);
129 assert.equal(parseHostedHubJwtExpirySeconds('24h').seconds, 86400);
130 assert.equal(parseHostedHubJwtExpirySeconds('3h').seconds, 10800);
131 assert.equal(parseHostedHubJwtExpirySeconds(43200).seconds, 43200);
132 assert.equal(parseHostedHubJwtExpirySeconds('10800').seconds, 10800);
133 assert.equal(parseHostedHubJwtExpirySeconds('2h').ok, false);
134 assert.equal(parseHostedHubJwtExpirySeconds('25h').ok, false);
135 assert.equal(parseHostedHubJwtExpirySeconds('24 h').ok, false);
136 assert.equal(parseHostedHubJwtExpirySeconds('1.5h').ok, false);
137 assert.equal(parseHostedHubJwtExpirySeconds('abc').ok, false);
138 assert.equal(parseHostedHubJwtExpirySeconds(10800.5).ok, false);
139 });
140
141 it('type:session + mandatory integer iat/exp admission', () => {
142 const now = Math.floor(Date.now() / 1000);
143 assert.equal(humanSessionClaimsShapeOk(sessionPayload()), true);
144 assert.equal(admitHumanSessionPayload(sessionPayload(), now).ok, true);
145 assert.equal(admitHumanSessionPayload(sessionPayload({ type: 'agent_access' }), now).ok, false);
146 assert.equal(admitHumanSessionPayload(sessionPayload({ type: undefined }), now).code, 'SESSION_INVALID');
147 assert.equal(
148 admitHumanSessionPayload(sessionPayload({ iat: now - 10, exp: now + 900 }), now).code,
149 'SESSION_INVALID',
150 );
151 });
152
153 it('SESSION_EXPIRED versus SESSION_INVALID for verified tokens', () => {
154 const now = Math.floor(Date.now() / 1000);
155 const live = makeJwt(sessionPayload({ iat: now - 60, exp: now - 60 + 10800 }));
156 assert.equal(verifyHumanSessionAccessToken(live, SECRET, null, now).ok, true);
157
158 const expiredOkShape = makeJwt(
159 sessionPayload({ iat: now - 10801, exp: now - 1, type: 'session' }),
160 );
161 assert.equal(
162 verifyHumanSessionAccessToken(expiredOkShape, SECRET, null, now).code,
163 'SESSION_EXPIRED',
164 );
165
166 const shortLived = makeJwt(sessionPayload({ iat: now - 10, exp: now + 900, type: 'session' }));
167 assert.equal(verifyHumanSessionAccessToken(shortLived, SECRET, null, now).code, 'SESSION_INVALID');
168
169 const missingType = makeJwt({
170 sub: 'google:1',
171 iat: now - 60,
172 exp: now - 60 + 10800,
173 });
174 assert.equal(verifyHumanSessionAccessToken(missingType, SECRET, null, now).code, 'SESSION_INVALID');
175
176 const agent = makeJwt(sessionPayload({ type: 'agent_access' }));
177 assert.equal(verifyHumanSessionAccessToken(agent, SECRET, null, now).code, 'SESSION_INVALID');
178
179 assert.equal(verifyHumanSessionAccessToken('not-a-jwt', SECRET, null, now).code, 'SESSION_INVALID');
180 });
181
182 it('CLI Accept helper and Origin allowlist including www/apex pairing', () => {
183 assert.equal(acceptIncludesRefreshTokenCli(REFRESH_TOKEN_CLI_ACCEPT), true);
184 assert.equal(acceptIncludesRefreshTokenCli('application/json'), false);
185 assert.equal(
186 isEstablishRefreshBrowserOriginAllowed(
187 'https://knowtation.store',
188 ['https://knowtation.store', 'https://www.knowtation.store'],
189 'https://api.knowtation.store',
190 isWwwApexPair,
191 ),
192 true,
193 );
194 // www request against apex-only allowlist (pairing branch).
195 assert.equal(
196 isEstablishRefreshBrowserOriginAllowed(
197 'https://www.knowtation.store',
198 ['https://knowtation.store'],
199 'https://api.knowtation.store',
200 isWwwApexPair,
201 ),
202 true,
203 );
204 assert.equal(
205 isEstablishRefreshBrowserOriginAllowed(
206 'https://knowtation.store',
207 ['https://www.knowtation.store'],
208 'https://api.knowtation.store',
209 isWwwApexPair,
210 ),
211 true,
212 );
213 assert.equal(
214 isEstablishRefreshBrowserOriginAllowed(
215 'https://evil.example',
216 ['https://knowtation.store'],
217 'https://api.knowtation.store',
218 isWwwApexPair,
219 ),
220 false,
221 );
222 assert.equal(
223 isEstablishRefreshBrowserOriginAllowed(
224 'null',
225 ['https://knowtation.store'],
226 'https://api.knowtation.store',
227 isWwwApexPair,
228 ),
229 false,
230 );
231 });
232
233 it('gateway signs with integer JWT_EXPIRY_SECONDS', () => {
234 assert.match(SERVER_SRC, /expiresIn:\s*JWT_EXPIRY_SECONDS/);
235 assert.match(SERVER_SRC, /expires_in:\s*JWT_EXPIRY_SECONDS/);
236 assert.match(SERVER_SRC, /parseHostedHubJwtExpirySeconds/);
237 });
238 });
239
240 // ─── 2. INTEGRATION (handler + HTTP) ─────────────────────────────────────────
241
242 describe('SESSION-DURABILITY-b-KN integration', () => {
243 it('browser Origin: cookie only, no raw refresh_token even with CLI Accept', async () => {
244 let issued = false;
245 const handler = createEstablishRefreshHandler({
246 store: {
247 issue: async (sub) => {
248 issued = true;
249 return { token: `${sub}.rawsecret` };
250 },
251 },
252 verifyHumanSession: (t) =>
253 t === 'good'
254 ? { ok: true, payload: sessionPayload({ sub: 'google:1' }) }
255 : { ok: false, code: 'SESSION_INVALID' },
256 cookieOptions: () => ({ httpOnly: true, secure: true, sameSite: 'none', path: '/api/v1/auth' }),
257 isBrowserOriginAllowed: () => true,
258 acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli,
259 });
260 const res = mockRes();
261 await handler(
262 {
263 headers: {
264 authorization: 'Bearer good',
265 origin: 'https://knowtation.store',
266 accept: REFRESH_TOKEN_CLI_ACCEPT,
267 },
268 },
269 res,
270 );
271 assert.equal(res.statusCode, 200);
272 assert.equal(res.body.established, true);
273 assert.equal(res.body.refresh_token, undefined);
274 assert.equal(res.cookies[REFRESH_COOKIE_NAME], 'google:1.rawsecret');
275 assert.equal(issued, true);
276 });
277
278 it('CLI no-Origin + Accept: refresh_token body, no Set-Cookie', async () => {
279 const handler = createEstablishRefreshHandler({
280 store: { issue: async (sub) => ({ token: `${sub}.cli` }) },
281 verifyHumanSession: () => ({ ok: true, payload: sessionPayload({ sub: 'google:2' }) }),
282 cookieOptions: () => ({ httpOnly: true, path: '/api/v1/auth' }),
283 isBrowserOriginAllowed: () => false,
284 acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli,
285 });
286 const res = mockRes();
287 await handler(
288 {
289 headers: {
290 authorization: 'Bearer good',
291 accept: REFRESH_TOKEN_CLI_ACCEPT,
292 },
293 },
294 res,
295 );
296 assert.equal(res.statusCode, 200);
297 assert.equal(res.body.refresh_token, 'google:2.cli');
298 assert.equal(res.cookies[REFRESH_COOKIE_NAME], undefined);
299 });
300
301 it('no-Origin wrong Accept → 406 and no store issue', async () => {
302 let issued = false;
303 const handler = createEstablishRefreshHandler({
304 store: {
305 issue: async () => {
306 issued = true;
307 return { token: 'x' };
308 },
309 },
310 verifyHumanSession: () => ({ ok: true, payload: sessionPayload() }),
311 cookieOptions: () => ({ httpOnly: true, path: '/api/v1/auth' }),
312 isBrowserOriginAllowed: () => true,
313 acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli,
314 });
315 const res = mockRes();
316 await handler({ headers: { authorization: 'Bearer good', accept: 'application/json' } }, res);
317 assert.equal(res.statusCode, 406);
318 assert.equal(issued, false);
319 });
320
321 it('malicious / null Origin → 403 and no issue', async () => {
322 let issued = false;
323 const handler = createEstablishRefreshHandler({
324 store: {
325 issue: async () => {
326 issued = true;
327 return { token: 'x' };
328 },
329 },
330 verifyHumanSession: () => ({ ok: true, payload: sessionPayload() }),
331 cookieOptions: () => ({ httpOnly: true, path: '/api/v1/auth' }),
332 isBrowserOriginAllowed: () => false,
333 acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli,
334 });
335 for (const origin of ['null', 'https://evil.example']) {
336 issued = false;
337 const res = mockRes();
338 await handler({ headers: { authorization: 'Bearer good', origin } }, res);
339 assert.equal(res.statusCode, 403);
340 assert.equal(issued, false);
341 }
342 });
343
344 it('admission failures create neither refresh record nor cookie', async () => {
345 let issued = false;
346 const handler = createEstablishRefreshHandler({
347 store: {
348 issue: async () => {
349 issued = true;
350 return { token: 'x' };
351 },
352 },
353 verifyHumanSession: (t) => {
354 if (t === 'expired') return { ok: false, code: 'SESSION_EXPIRED' };
355 return { ok: false, code: 'SESSION_INVALID' };
356 },
357 cookieOptions: () => ({ httpOnly: true, path: '/api/v1/auth' }),
358 isBrowserOriginAllowed: () => true,
359 acceptIncludesCliMediaType: acceptIncludesRefreshTokenCli,
360 });
361 const expiredRes = mockRes();
362 await handler(
363 { headers: { authorization: 'Bearer expired', origin: 'https://knowtation.store' } },
364 expiredRes,
365 );
366 assert.equal(expiredRes.statusCode, 401);
367 assert.equal(expiredRes.body.code, 'SESSION_EXPIRED');
368 assert.equal(issued, false);
369
370 const badRes = mockRes();
371 await handler(
372 { headers: { authorization: 'Bearer bad', origin: 'https://knowtation.store' } },
373 badRes,
374 );
375 assert.equal(badRes.statusCode, 401);
376 assert.equal(badRes.body.code, 'SESSION_INVALID');
377 assert.equal(issued, false);
378 });
379
380 let gw;
381 before(async () => {
382 gw = await createGateway({
383 HUB_CORS_ORIGIN: 'https://knowtation.store,https://www.knowtation.store',
384 HUB_BASE_URL: 'https://api.knowtation.store',
385 });
386 });
387 after(async () => {
388 await gw.close();
389 });
390
391 it('HTTP session: MCP / missing-type fail introspection', async () => {
392 const now = Math.floor(Date.now() / 1000);
393 const mcp = makeJwt(sessionPayload({ type: 'mcp_access' }));
394 const missing = makeJwt({
395 sub: 'google:1',
396 iat: now - 60,
397 exp: now - 60 + 10800,
398 });
399 for (const token of [mcp, missing]) {
400 const res = await fetch(`${gw.url}/api/v1/auth/session`, {
401 headers: { Authorization: `Bearer ${token}` },
402 });
403 const body = await res.json();
404 assert.equal(res.status, 401);
405 assert.equal(body.code, 'SESSION_INVALID');
406 }
407 });
408
409 it('HTTP session: live session returns type:session + iat/exp', async () => {
410 const token = makeJwt(sessionPayload());
411 const res = await fetch(`${gw.url}/api/v1/auth/session`, {
412 headers: { Authorization: `Bearer ${token}` },
413 });
414 const body = await res.json();
415 assert.equal(res.status, 200);
416 assert.equal(body.type, 'session');
417 assert.equal(typeof body.iat, 'number');
418 assert.equal(typeof body.exp, 'number');
419 });
420
421 it('HTTP establish-refresh browser: no refresh_token in body', async () => {
422 const token = makeJwt(sessionPayload());
423 const res = await fetch(`${gw.url}/api/v1/auth/establish-refresh`, {
424 method: 'POST',
425 headers: {
426 Authorization: `Bearer ${token}`,
427 Origin: 'https://knowtation.store',
428 Accept: REFRESH_TOKEN_CLI_ACCEPT,
429 'Content-Type': 'application/json',
430 },
431 });
432 const body = await res.json();
433 assert.equal(res.status, 200);
434 assert.equal(body.established, true);
435 assert.equal(body.refresh_token, undefined);
436 const setCookie = res.headers.get('set-cookie') || '';
437 assert.match(setCookie, /ktn_refresh=/);
438 });
439
440 it('HTTP establish-refresh allows www Origin via apex allowlist pairing', async () => {
441 const token = makeJwt(sessionPayload());
442 const res = await fetch(`${gw.url}/api/v1/auth/establish-refresh`, {
443 method: 'POST',
444 headers: {
445 Authorization: `Bearer ${token}`,
446 Origin: 'https://www.knowtation.store',
447 'Content-Type': 'application/json',
448 },
449 });
450 const body = await res.json();
451 assert.equal(res.status, 200);
452 assert.equal(body.established, true);
453 assert.equal(body.refresh_token, undefined);
454 });
455 });
456
457 // ─── 3–7. e2e / stress / data-integrity / performance / security (source + contract) ──
458
459 describe('SESSION-DURABILITY-b-KN e2e/source contracts', () => {
460 it('e2e: durability banner + typed establishPersistentSession in hub UI', () => {
461 assert.match(INDEX_HTML, /id="hub-session-durability-banner"/);
462 assert.match(INDEX_HTML, /role="status"/);
463 assert.match(HUB_JS, /async function establishPersistentSession/);
464 assert.match(HUB_JS, /SESSION_DURABILITY_WARN_KEY/);
465 assert.match(HUB_JS, /This session could not be made durable/);
466 assert.match(HUB_JS, /schema_version !== 1/);
467 assert.match(HUB_JS, /established !== true/);
468 });
469
470 it('e2e: ensureFreshHumanSession + hubApiResponse wired for copy and credentials', () => {
471 assert.match(HUB_JS, /async function ensureFreshHumanSession/);
472 assert.match(HUB_JS, /async function hubApiResponse/);
473 assert.match(HUB_JS, /btnCopyHubApiEnv\.onclick\s*=\s*async/);
474 assert.match(HUB_JS, /ensureFreshHumanSession\(120\)/);
475 assert.match(HUB_JS, /hubApiResponse\('\/api\/v1\/auth\/agent\/credentials'/);
476 assert.doesNotMatch(
477 HUB_JS,
478 /fetch\(String\(apiBase[\s\S]{0,80}\/api\/v1\/auth\/agent\/credentials/,
479 );
480 });
481
482 it('stress: single-flight refresh + mutation zero network retries', () => {
483 assert.match(HUB_JS, /let refreshInFlight = null/);
484 assert.match(
485 HUB_JS,
486 /method === 'GET' \|\| method === 'HEAD'\s*\?\s*2\s*:\s*0/,
487 );
488 });
489
490 it('data-integrity: five named consumers; new establish-refresh callers cannot bypass CLI Accept', () => {
491 assert.match(CLI_AUTH_SRC, /application\/vnd\.knowtation\.refresh-token\+json/);
492 assert.match(CLI_AUTH_SRC, /mode:\s*0o600/);
493 const namedConsumers = [
494 'scripts/lib/hub-session-auth.mjs',
495 'scripts/hub-session-refresh.mjs',
496 'scripts/verify-rhf-d-catalog-consent.mjs',
497 'scripts/verify-rhf-kn0-deploy-proof.mjs',
498 'web/hub/hub.js',
499 ];
500 for (const rel of namedConsumers) {
501 assert.ok(fs.existsSync(path.join(ROOT, rel)), rel);
502 }
503 assert.match(
504 fs.readFileSync(path.join(ROOT, 'scripts/hub-session-refresh.mjs'), 'utf8'),
505 /establishHostedRefreshFromAccess|--save-access-token/,
506 );
507 // Enumerate every runtime caller of establish-refresh. Browser hub.js is Origin mode.
508 // Every other .mjs/.js caller must go through hub-session-auth (vendor Accept) or be
509 // the shared helper itself. A new direct fetch with Accept: application/json fails this test.
510 const skipDir = new Set(['node_modules', '.git', '.muse', 'data', 'backups', 'dist', 'coverage']);
511 /** @type {string[]} */
512 const callSites = [];
513 function walk(dir) {
514 for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
515 if (skipDir.has(ent.name)) continue;
516 const full = path.join(dir, ent.name);
517 if (ent.isDirectory()) {
518 walk(full);
519 continue;
520 }
521 if (!/\.(mjs|js)$/.test(ent.name)) continue;
522 const rel = path.relative(ROOT, full);
523 if (rel.startsWith('test' + path.sep)) continue;
524 const src = fs.readFileSync(full, 'utf8');
525 if (!src.includes('/api/v1/auth/establish-refresh')) continue;
526 callSites.push(rel);
527 }
528 }
529 walk(ROOT);
530 const allowedDirect = new Set([
531 'scripts/lib/hub-session-auth.mjs',
532 'web/hub/hub.js',
533 'hub/gateway/server.mjs',
534 'hub/auth-session.mjs',
535 ]);
536 for (const rel of callSites) {
537 if (allowedDirect.has(rel)) continue;
538 // Named verification scripts must import the shared helper — not fetch establish-refresh.
539 const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
540 assert.ok(
541 src.includes('establishHostedRefreshFromAccess') ||
542 src.includes("from './lib/hub-session-auth.mjs'") ||
543 src.includes("from '../lib/hub-session-auth.mjs'") ||
544 src.includes('hub-session-auth.mjs'),
545 `${rel} calls establish-refresh outside the named delivery-mode contract`,
546 );
547 assert.ok(
548 !/fetch\([^)]*establish-refresh/.test(src.replace(/\s+/g, ' ')),
549 `${rel} must not fetch establish-refresh directly (use hub-session-auth)`,
550 );
551 }
552 assert.ok(
553 callSites.includes('scripts/lib/hub-session-auth.mjs'),
554 'CLI helper must remain an establish-refresh caller',
555 );
556 assert.ok(callSites.includes('web/hub/hub.js'), 'Hub UI must remain an establish-refresh caller');
557 });
558
559 it('performance: one 401 → one refresh + one retry; auth endpoints excluded', () => {
560 assert.match(HUB_JS, /_retriedAfterRefresh/);
561 assert.match(HUB_JS, /\/api\/v1\/auth\/establish-refresh/);
562 assert.match(HUB_JS, /path !== '\/api\/v1\/auth\/refresh'/);
563 });
564
565 it('security: browser never returns raw refresh; establish handler mode-split present', () => {
566 assert.match(AUTH_SESSION_SRC, /hasOrigin/);
567 assert.match(AUTH_SESSION_SRC, /established:\s*true/);
568 assert.match(AUTH_SESSION_SRC, /NOT_ACCEPTABLE|406/);
569 assert.match(HUB_JS, /Object\.prototype\.hasOwnProperty\.call\(data,\s*'refresh_token'\)/);
570 });
571 });
File History 1 commit
sha256:fbe982a22c05c6fe2e93876f250deecdb43d883f648b4e1810c7546caa9f17db docs: activate KNOWTATION- board identity and preserve livi… Human minor 5 days ago