@efa-one/sdk 1.16.1 → 1.18.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 +31 -0
- package/backend/auth.d.ts +3 -0
- package/backend/auth.js +45 -7
- package/backend/index.d.ts +1 -0
- package/backend/index.js +1 -0
- package/backend/sessionStatusClient.d.ts +14 -0
- package/backend/sessionStatusClient.js +0 -0
- package/frontend/ipc.d.ts +19 -0
- package/frontend/ipc.js +79 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,24 @@ import { initI18n, registerAppInfo, DevHeader } from '@efa-one/sdk/frontend';
|
|
|
61
61
|
registerAppInfo({ appName: 'efa-chat', version: __APP_VERSION__ });
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
```tsx
|
|
65
|
+
// Frontend — receive the login token from the kernel (inside a mount effect)
|
|
66
|
+
import { subscribeConvergeAuth } from '@efa-one/sdk/frontend';
|
|
67
|
+
|
|
68
|
+
useEffect(() => subscribeConvergeAuth({
|
|
69
|
+
onAuth: async ({ token, serviceKey, theme, language }) => {
|
|
70
|
+
try { await exchange(token, serviceKey); return true; } catch { return false; }
|
|
71
|
+
},
|
|
72
|
+
onTimeout: () => setError('No login received from the platform — reload the page.'),
|
|
73
|
+
}), []);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The app asks for the token itself (`CONVERGE_AUTH_REQUEST`) as soon as its listener
|
|
77
|
+
is attached and repeats the request with backoff. Waiting passively for the kernel's
|
|
78
|
+
push is not enough: the push goes out in a fixed window after the tile opens, and an
|
|
79
|
+
app that needs longer to mount (cold cache, slow connection) would miss it and spin
|
|
80
|
+
forever.
|
|
81
|
+
|
|
64
82
|
```tsx
|
|
65
83
|
// Frontend — a fully-featured list in a few lines
|
|
66
84
|
import { DataTable, type ColumnDef } from '@efa-one/sdk/frontend/ui';
|
|
@@ -144,6 +162,19 @@ run under it. Dependabot is configured to never propose a TypeScript major.
|
|
|
144
162
|
|
|
145
163
|
SemVer. Consuming apps pull new versions with `npm update @efa-one/sdk`.
|
|
146
164
|
|
|
165
|
+
**Kernel compatibility — 1.17.0 needs an up-to-date kernel.** Since 1.17.0 the
|
|
166
|
+
`app_session` cookie is bound to the kernel session and `requireAuth` checks it
|
|
167
|
+
live via the kernel endpoint `GET /api/internal/sessions/:jti/status`. Against a
|
|
168
|
+
kernel without that endpoint, every authenticated request fails closed with
|
|
169
|
+
`503 Session service unavailable`. Roll out the kernel first, then apps on 1.17.
|
|
170
|
+
Sessions issued by an older SDK are rejected once (401) and renewed on the next
|
|
171
|
+
iframe load.
|
|
172
|
+
|
|
173
|
+
**1.18.0 — auth handshake on request.** `subscribeConvergeAuth` sends
|
|
174
|
+
`CONVERGE_AUTH_REQUEST` to the kernel. It works against any kernel: an older kernel
|
|
175
|
+
ignores the request and its fixed-window push reaches the app as before; a current
|
|
176
|
+
kernel answers the request directly. No rollout order.
|
|
177
|
+
|
|
147
178
|
## License
|
|
148
179
|
|
|
149
180
|
[Apache-2.0](./LICENSE) — permissive with a patent grant, so customers and partners
|
package/backend/auth.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface JwtPayload {
|
|
|
9
9
|
iat: number;
|
|
10
10
|
exp: number;
|
|
11
11
|
forcePasswordChange?: boolean;
|
|
12
|
+
jti?: string;
|
|
12
13
|
}
|
|
13
14
|
export interface SessionPayload {
|
|
14
15
|
sub: string;
|
|
@@ -17,6 +18,8 @@ export interface SessionPayload {
|
|
|
17
18
|
email: string | null;
|
|
18
19
|
language?: string;
|
|
19
20
|
tenant: string;
|
|
21
|
+
kernelJti?: string;
|
|
22
|
+
kernelIat?: number;
|
|
20
23
|
}
|
|
21
24
|
export interface AuthRequest extends Request {
|
|
22
25
|
user?: SessionPayload;
|
package/backend/auth.js
CHANGED
|
@@ -14,8 +14,11 @@ exports.createExchangeRouter = createExchangeRouter;
|
|
|
14
14
|
const express_1 = require("express");
|
|
15
15
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
16
16
|
const permissionClient_1 = require("./permissionClient");
|
|
17
|
+
const sessionStatusClient_1 = require("./sessionStatusClient");
|
|
17
18
|
const JWT_ISSUER = 'converge';
|
|
18
19
|
const JWT_AUDIENCE = 'converge';
|
|
20
|
+
const KERNEL_JTI_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
21
|
+
const KERNEL_SUB_RE = /^[A-Za-z0-9._:@-]{1,128}$/;
|
|
19
22
|
function getPlatformPublicKey() {
|
|
20
23
|
const s = process.env.JWT_PUBLIC_KEY;
|
|
21
24
|
if (!s)
|
|
@@ -31,14 +34,14 @@ function getSessionSecret() {
|
|
|
31
34
|
function getCookieName() {
|
|
32
35
|
return process.env.APP_SESSION_COOKIE_NAME || 'app_session';
|
|
33
36
|
}
|
|
34
|
-
function setSessionCookie(res, token) {
|
|
37
|
+
function setSessionCookie(res, token, maxAgeMs) {
|
|
35
38
|
const secure = process.env.COOKIE_SECURE
|
|
36
39
|
? ['1', 'true', 'yes', 'on'].includes(process.env.COOKIE_SECURE.toLowerCase())
|
|
37
40
|
: process.env.NODE_ENV === 'production';
|
|
38
41
|
res.cookie(getCookieName(), token, {
|
|
39
42
|
httpOnly: true,
|
|
40
43
|
sameSite: 'lax',
|
|
41
|
-
maxAge:
|
|
44
|
+
maxAge: maxAgeMs,
|
|
42
45
|
secure,
|
|
43
46
|
path: '/',
|
|
44
47
|
});
|
|
@@ -49,14 +52,36 @@ function requireAuth(req, res, next) {
|
|
|
49
52
|
res.status(401).json({ error: 'Not authenticated' });
|
|
50
53
|
return;
|
|
51
54
|
}
|
|
55
|
+
let payload;
|
|
52
56
|
try {
|
|
53
|
-
|
|
54
|
-
req.user = payload;
|
|
55
|
-
next();
|
|
57
|
+
payload = jsonwebtoken_1.default.verify(token, getSessionSecret(), { algorithms: ['HS256'] });
|
|
56
58
|
}
|
|
57
59
|
catch {
|
|
58
60
|
res.status(401).json({ error: 'Session invalid or expired' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const { kernelJti, kernelIat, exp } = payload;
|
|
64
|
+
if (typeof kernelJti !== 'string' || !kernelJti || typeof kernelIat !== 'number' || typeof exp !== 'number') {
|
|
65
|
+
res.status(401).json({ error: 'Session invalid or expired' });
|
|
66
|
+
return;
|
|
59
67
|
}
|
|
68
|
+
const verified = payload;
|
|
69
|
+
(0, sessionStatusClient_1.getSessionStatus)({
|
|
70
|
+
convergeId: verified.convergeId,
|
|
71
|
+
jti: kernelJti,
|
|
72
|
+
iat: kernelIat,
|
|
73
|
+
exp,
|
|
74
|
+
}).then((status) => {
|
|
75
|
+
if (!status.active) {
|
|
76
|
+
res.status(401).json({ error: 'Session revoked' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
req.user = verified;
|
|
80
|
+
next();
|
|
81
|
+
}, (err) => {
|
|
82
|
+
console.error(JSON.stringify({ level: 'error', msg: 'requireAuth: session status lookup failed', err: String(err) }));
|
|
83
|
+
res.status(503).json({ error: 'Session service unavailable' });
|
|
84
|
+
});
|
|
60
85
|
}
|
|
61
86
|
function requireAdmin(req, res, next) {
|
|
62
87
|
requireAuth(req, res, () => {
|
|
@@ -177,6 +202,17 @@ function createExchangeRouter(pool) {
|
|
|
177
202
|
res.status(401).json({ error: 'Invalid or expired platform token' });
|
|
178
203
|
return;
|
|
179
204
|
}
|
|
205
|
+
if (typeof convergePayload.jti !== 'string' || !KERNEL_JTI_RE.test(convergePayload.jti) ||
|
|
206
|
+
typeof convergePayload.sub !== 'string' || !KERNEL_SUB_RE.test(convergePayload.sub) ||
|
|
207
|
+
!Number.isInteger(convergePayload.iat) || !Number.isInteger(convergePayload.exp)) {
|
|
208
|
+
res.status(401).json({ error: 'Invalid or expired platform token' });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const remainingSec = convergePayload.exp - Math.floor(Date.now() / 1000);
|
|
212
|
+
if (remainingSec <= 0) {
|
|
213
|
+
res.status(401).json({ error: 'Invalid or expired platform token' });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
180
216
|
try {
|
|
181
217
|
const { rows } = await pool.query(`INSERT INTO app_users (converge_id, email, name, last_seen_at)
|
|
182
218
|
VALUES ($1, $2, $3, NOW())
|
|
@@ -197,9 +233,11 @@ function createExchangeRouter(pool) {
|
|
|
197
233
|
email: convergePayload.email ?? null,
|
|
198
234
|
language: convergePayload.language,
|
|
199
235
|
tenant: convergePayload.tenant,
|
|
236
|
+
kernelJti: convergePayload.jti,
|
|
237
|
+
kernelIat: convergePayload.iat,
|
|
200
238
|
};
|
|
201
|
-
const sessionToken = jsonwebtoken_1.default.sign(sessionPayload, getSessionSecret(), { algorithm: 'HS256', expiresIn:
|
|
202
|
-
setSessionCookie(res, sessionToken);
|
|
239
|
+
const sessionToken = jsonwebtoken_1.default.sign(sessionPayload, getSessionSecret(), { algorithm: 'HS256', expiresIn: remainingSec });
|
|
240
|
+
setSessionCookie(res, sessionToken, remainingSec * 1000);
|
|
203
241
|
res.json({
|
|
204
242
|
user: {
|
|
205
243
|
id: appUser.id,
|
package/backend/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './mail';
|
|
|
6
6
|
export * from './notifications';
|
|
7
7
|
export * from './permissions';
|
|
8
8
|
export * from './permissionClient';
|
|
9
|
+
export * from './sessionStatusClient';
|
|
9
10
|
export * from './permissionCheck';
|
|
10
11
|
export * from './customPermissions';
|
|
11
12
|
export * from './serviceClient';
|
package/backend/index.js
CHANGED
|
@@ -22,6 +22,7 @@ __exportStar(require("./mail"), exports);
|
|
|
22
22
|
__exportStar(require("./notifications"), exports);
|
|
23
23
|
__exportStar(require("./permissions"), exports);
|
|
24
24
|
__exportStar(require("./permissionClient"), exports);
|
|
25
|
+
__exportStar(require("./sessionStatusClient"), exports);
|
|
25
26
|
__exportStar(require("./permissionCheck"), exports);
|
|
26
27
|
__exportStar(require("./customPermissions"), exports);
|
|
27
28
|
__exportStar(require("./serviceClient"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type SessionInactiveReason = 'server_restarted' | 'session_revoked';
|
|
2
|
+
export interface SessionStatus {
|
|
3
|
+
active: boolean;
|
|
4
|
+
reason?: SessionInactiveReason;
|
|
5
|
+
}
|
|
6
|
+
export interface SessionRef {
|
|
7
|
+
convergeId: string;
|
|
8
|
+
jti: string;
|
|
9
|
+
iat: number;
|
|
10
|
+
exp: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function getSessionStatus(ref: SessionRef): Promise<SessionStatus>;
|
|
13
|
+
export declare function clearSessionStatusCache(): void;
|
|
14
|
+
export declare function sessionStatusCacheSize(): number;
|
|
Binary file
|
package/frontend/ipc.d.ts
CHANGED
|
@@ -24,3 +24,22 @@ export declare function navigateToApp(serviceKey: string, options?: {
|
|
|
24
24
|
path?: string;
|
|
25
25
|
}): void;
|
|
26
26
|
export declare function notifyRouteChange(path: string): void;
|
|
27
|
+
export declare const CONVERGE_AUTH = "CONVERGE_AUTH";
|
|
28
|
+
export declare const CONVERGE_AUTH_REQUEST = "CONVERGE_AUTH_REQUEST";
|
|
29
|
+
export interface ConvergeAuthMessage<TTheme = unknown> {
|
|
30
|
+
type: typeof CONVERGE_AUTH;
|
|
31
|
+
token: string;
|
|
32
|
+
serviceKey?: string;
|
|
33
|
+
language?: string;
|
|
34
|
+
theme?: TTheme;
|
|
35
|
+
embed?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface SubscribeConvergeAuthOptions<TTheme = unknown> {
|
|
38
|
+
onAuth: (message: ConvergeAuthMessage<TTheme>) => Promise<boolean>;
|
|
39
|
+
onTimeout?: () => void;
|
|
40
|
+
requestDelaysMs?: readonly number[];
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare const DEFAULT_AUTH_REQUEST_DELAYS_MS: readonly number[];
|
|
44
|
+
export declare const DEFAULT_AUTH_TIMEOUT_MS = 15000;
|
|
45
|
+
export declare function subscribeConvergeAuth<TTheme = unknown>(options: SubscribeConvergeAuthOptions<TTheme>): () => void;
|
package/frontend/ipc.js
CHANGED
|
@@ -59,3 +59,82 @@ export function notifyRouteChange(path) {
|
|
|
59
59
|
return;
|
|
60
60
|
window.parent.postMessage({ type: 'CONVERGE_ROUTE_CHANGED', path }, getParentOrigin());
|
|
61
61
|
}
|
|
62
|
+
export const CONVERGE_AUTH = 'CONVERGE_AUTH';
|
|
63
|
+
export const CONVERGE_AUTH_REQUEST = 'CONVERGE_AUTH_REQUEST';
|
|
64
|
+
export const DEFAULT_AUTH_REQUEST_DELAYS_MS = [0, 500, 1000, 2000, 4000, 8000];
|
|
65
|
+
export const DEFAULT_AUTH_TIMEOUT_MS = 15000;
|
|
66
|
+
function isAuthMessage(data) {
|
|
67
|
+
const d = data;
|
|
68
|
+
return !!d && d.type === CONVERGE_AUTH && typeof d.token === 'string' && d.token !== '';
|
|
69
|
+
}
|
|
70
|
+
export function subscribeConvergeAuth(options) {
|
|
71
|
+
if (!isEmbedded())
|
|
72
|
+
return () => { };
|
|
73
|
+
const delays = options.requestDelaysMs ?? DEFAULT_AUTH_REQUEST_DELAYS_MS;
|
|
74
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS;
|
|
75
|
+
let active = true;
|
|
76
|
+
let received = false;
|
|
77
|
+
let authenticated = false;
|
|
78
|
+
let inFlight = false;
|
|
79
|
+
let queued = null;
|
|
80
|
+
const timers = [];
|
|
81
|
+
const clearTimers = () => {
|
|
82
|
+
timers.forEach((t) => clearTimeout(t));
|
|
83
|
+
timers.length = 0;
|
|
84
|
+
};
|
|
85
|
+
const start = (message) => {
|
|
86
|
+
inFlight = true;
|
|
87
|
+
options.onAuth(message).then((ok) => finish(ok), () => finish(false));
|
|
88
|
+
};
|
|
89
|
+
const finish = (ok) => {
|
|
90
|
+
inFlight = false;
|
|
91
|
+
if (!active)
|
|
92
|
+
return;
|
|
93
|
+
if (ok) {
|
|
94
|
+
authenticated = true;
|
|
95
|
+
queued = null;
|
|
96
|
+
clearTimers();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const next = queued;
|
|
100
|
+
queued = null;
|
|
101
|
+
if (next)
|
|
102
|
+
start(next);
|
|
103
|
+
};
|
|
104
|
+
const handler = (event) => {
|
|
105
|
+
if (!isFromPlatformParent(event))
|
|
106
|
+
return;
|
|
107
|
+
if (!isAuthMessage(event.data))
|
|
108
|
+
return;
|
|
109
|
+
received = true;
|
|
110
|
+
if (authenticated)
|
|
111
|
+
return;
|
|
112
|
+
const message = event.data;
|
|
113
|
+
if (inFlight) {
|
|
114
|
+
queued = message;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
start(message);
|
|
118
|
+
};
|
|
119
|
+
const request = () => {
|
|
120
|
+
if (!active || received)
|
|
121
|
+
return;
|
|
122
|
+
window.parent.postMessage({ type: CONVERGE_AUTH_REQUEST }, getParentOrigin());
|
|
123
|
+
};
|
|
124
|
+
window.addEventListener('message', handler);
|
|
125
|
+
for (const ms of delays) {
|
|
126
|
+
if (ms <= 0)
|
|
127
|
+
request();
|
|
128
|
+
else
|
|
129
|
+
timers.push(setTimeout(request, ms));
|
|
130
|
+
}
|
|
131
|
+
timers.push(setTimeout(() => {
|
|
132
|
+
if (active && !received)
|
|
133
|
+
options.onTimeout?.();
|
|
134
|
+
}, timeoutMs));
|
|
135
|
+
return () => {
|
|
136
|
+
active = false;
|
|
137
|
+
clearTimers();
|
|
138
|
+
window.removeEventListener('message', handler);
|
|
139
|
+
};
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@efa-one/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "Offizielles SDK für efa-one-Apps: Auth/Session, Service-Discovery, Gateway-Client, Audit/Reporting/Mail/Notifications, Permission- und Capability-Registrierung sowie das Frontend-IPC-/i18n-Toolkit.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|