@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 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: 8 * 60 * 60 * 1000,
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
- const payload = jsonwebtoken_1.default.verify(token, getSessionSecret(), { algorithms: ['HS256'] });
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: '8h' });
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,
@@ -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.16.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",