@efa-one/sdk 1.16.1 → 1.17.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
@@ -144,6 +144,14 @@ run under it. Dependabot is configured to never propose a TypeScript major.
144
144
 
145
145
  SemVer. Consuming apps pull new versions with `npm update @efa-one/sdk`.
146
146
 
147
+ **Kernel compatibility — 1.17.0 needs an up-to-date kernel.** Since 1.17.0 the
148
+ `app_session` cookie is bound to the kernel session and `requireAuth` checks it
149
+ live via the kernel endpoint `GET /api/internal/sessions/:jti/status`. Against a
150
+ kernel without that endpoint, every authenticated request fails closed with
151
+ `503 Session service unavailable`. Roll out the kernel first, then apps on 1.17.
152
+ Sessions issued by an older SDK are rejected once (401) and renewed on the next
153
+ iframe load.
154
+
147
155
  ## License
148
156
 
149
157
  [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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efa-one/sdk",
3
- "version": "1.16.1",
3
+ "version": "1.17.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",