@hotfusion/modeller 0.0.13 → 0.0.15

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.
Files changed (104) hide show
  1. package/README.md +99 -0
  2. package/dist/adapters/cipher.js +51 -0
  3. package/dist/adapters/cipher.js.map +1 -0
  4. package/dist/connector.js +81 -41
  5. package/dist/connector.js.map +1 -1
  6. package/dist/core.js +2 -48
  7. package/dist/core.js.map +1 -1
  8. package/dist/index.js +9 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/model.js +27 -50
  11. package/dist/model.js.map +1 -1
  12. package/dist/oidc/adapter.js +177 -0
  13. package/dist/oidc/adapter.js.map +1 -0
  14. package/dist/oidc/adapters/cipher.js +51 -0
  15. package/dist/oidc/adapters/cipher.js.map +1 -0
  16. package/dist/oidc/client.js +66 -0
  17. package/dist/oidc/client.js.map +1 -0
  18. package/dist/oidc/code.js +37 -0
  19. package/dist/oidc/code.js.map +1 -0
  20. package/dist/oidc/default.config.js +200 -0
  21. package/dist/oidc/default.config.js.map +1 -0
  22. package/dist/oidc/federation.js +51 -0
  23. package/dist/oidc/federation.js.map +1 -0
  24. package/dist/oidc/grant.js +37 -0
  25. package/dist/oidc/grant.js.map +1 -0
  26. package/dist/oidc/interaction.js +36 -0
  27. package/dist/oidc/interaction.js.map +1 -0
  28. package/dist/oidc/oidc.config.js +79 -0
  29. package/dist/oidc/oidc.config.js.map +1 -0
  30. package/dist/oidc/schemas/client.schema.json +62 -0
  31. package/dist/oidc/schemas/code.schema.json +16 -0
  32. package/dist/oidc/schemas/grant.schema.json +13 -0
  33. package/dist/oidc/schemas/interaction.schema.json +26 -0
  34. package/dist/oidc/schemas/session.schema.json +14 -0
  35. package/dist/oidc/schemas/token.schema.json +16 -0
  36. package/dist/oidc/schemas/user.schema.json +44 -0
  37. package/dist/oidc/session.js +36 -0
  38. package/dist/oidc/session.js.map +1 -0
  39. package/dist/oidc/session.token.js +24 -0
  40. package/dist/oidc/session.token.js.map +1 -0
  41. package/dist/oidc/token.js +23 -0
  42. package/dist/oidc/token.js.map +1 -0
  43. package/dist/oidc/user.js +95 -0
  44. package/dist/oidc/user.js.map +1 -0
  45. package/dist/oidc/utils.js +154 -0
  46. package/dist/oidc/utils.js.map +1 -0
  47. package/dist/server.js +722 -113
  48. package/dist/server.js.map +1 -1
  49. package/dist/types/adapters/cipher.d.ts +12 -0
  50. package/dist/types/adapters/cipher.d.ts.map +1 -0
  51. package/dist/types/connector.d.ts +13 -1
  52. package/dist/types/connector.d.ts.map +1 -1
  53. package/dist/types/core.d.ts +2 -2
  54. package/dist/types/core.d.ts.map +1 -1
  55. package/dist/types/index.d.ts +4 -0
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/model.d.ts +26 -2
  58. package/dist/types/model.d.ts.map +1 -1
  59. package/dist/types/oidc/adapter.d.ts +16 -0
  60. package/dist/types/oidc/adapter.d.ts.map +1 -0
  61. package/dist/types/oidc/adapters/cipher.d.ts +12 -0
  62. package/dist/types/oidc/adapters/cipher.d.ts.map +1 -0
  63. package/dist/types/oidc/client.d.ts +3 -0
  64. package/dist/types/oidc/client.d.ts.map +1 -0
  65. package/dist/types/oidc/code.d.ts +3 -0
  66. package/dist/types/oidc/code.d.ts.map +1 -0
  67. package/dist/types/oidc/default.config.d.ts +33 -0
  68. package/dist/types/oidc/default.config.d.ts.map +1 -0
  69. package/dist/types/oidc/federation.d.ts +3 -0
  70. package/dist/types/oidc/federation.d.ts.map +1 -0
  71. package/dist/types/oidc/grant.d.ts +3 -0
  72. package/dist/types/oidc/grant.d.ts.map +1 -0
  73. package/dist/types/oidc/interaction.d.ts +3 -0
  74. package/dist/types/oidc/interaction.d.ts.map +1 -0
  75. package/dist/types/oidc/oidc.config.d.ts +7 -0
  76. package/dist/types/oidc/oidc.config.d.ts.map +1 -0
  77. package/dist/types/oidc/session.d.ts +3 -0
  78. package/dist/types/oidc/session.d.ts.map +1 -0
  79. package/dist/types/oidc/session.token.d.ts +3 -0
  80. package/dist/types/oidc/session.token.d.ts.map +1 -0
  81. package/dist/types/oidc/token.d.ts +3 -0
  82. package/dist/types/oidc/token.d.ts.map +1 -0
  83. package/dist/types/oidc/user.d.ts +3 -0
  84. package/dist/types/oidc/user.d.ts.map +1 -0
  85. package/dist/types/oidc/utils.d.ts +56 -0
  86. package/dist/types/oidc/utils.d.ts.map +1 -0
  87. package/dist/types/server.d.ts +8 -3
  88. package/dist/types/server.d.ts.map +1 -1
  89. package/dist/types/types.d.ts +264 -0
  90. package/dist/types/utils/bundler.d.ts.map +1 -1
  91. package/dist/types/utils/display.d.ts +23 -0
  92. package/dist/types/utils/display.d.ts.map +1 -0
  93. package/dist/utils/_secret.key +1 -0
  94. package/dist/utils/bundler.js +47 -8
  95. package/dist/utils/bundler.js.map +1 -1
  96. package/dist/utils/display.js +207 -0
  97. package/dist/utils/display.js.map +1 -0
  98. package/package.json +28 -4
  99. package/docs/CORE.md +0 -191
  100. package/docs/ERRORS.md +0 -90
  101. package/docs/MODEL.md +0 -296
  102. package/docs/PATTERNS.md +0 -182
  103. package/docs/SERVER.md +0 -88
  104. package/docs/UTILITIES.md +0 -111
package/dist/server.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -10,11 +43,24 @@ const koa_router_1 = __importDefault(require("koa-router"));
10
43
  const koa_session_1 = __importDefault(require("koa-session"));
11
44
  const bodyparser_1 = __importDefault(require("@koa/bodyparser"));
12
45
  const cors_1 = __importDefault(require("@koa/cors"));
46
+ const koa_body_1 = require("koa-body");
13
47
  const eventemitter3_1 = require("eventemitter3");
14
48
  const jose_1 = require("jose");
49
+ const openidClient = __importStar(require("openid-client"));
50
+ const oidc_provider_1 = __importDefault(require("oidc-provider"));
15
51
  const socket_io_1 = require("socket.io");
16
52
  const keygen_js_1 = require("./utils/keygen.js");
17
53
  const koa_static_1 = __importDefault(require("koa-static"));
54
+ const display_1 = require("./utils/display");
55
+ const bundler_1 = require("./utils/bundler");
56
+ const view_1 = require("./view");
57
+ const core_1 = require("./core");
58
+ const adapter_1 = require("./oidc/adapter");
59
+ const client_1 = require("./oidc/client");
60
+ const user_1 = require("./oidc/user");
61
+ const federation_1 = require("./oidc/federation");
62
+ const token_1 = require("./oidc/token");
63
+ const utils_1 = require("./oidc/utils");
18
64
  class Utils {
19
65
  static trimPath(path) {
20
66
  return '/' + path.split('/').filter((x) => x).join('/');
@@ -25,12 +71,14 @@ class Server extends eventemitter3_1.EventEmitter {
25
71
  app;
26
72
  router;
27
73
  routes = [];
28
- providers = [];
29
74
  config;
30
75
  io;
31
76
  registeredModels = [];
32
77
  extensions = [];
33
78
  extensionConfigs = new Map();
79
+ _oidcServers = [];
80
+ _oidcProviders = new Map();
81
+ _staticFolders = [];
34
82
  API_OPERATIONS = ['insert', 'update', 'delete', 'get', 'find', 'list'];
35
83
  OPERATION_METHODS = {
36
84
  insert: 'POST',
@@ -43,60 +91,54 @@ class Server extends eventemitter3_1.EventEmitter {
43
91
  constructor(port, config) {
44
92
  super();
45
93
  this.app = new koa_1.default();
94
+ this.app.proxy = true;
46
95
  this.router = new koa_router_1.default();
47
- this.app.use((0, cors_1.default)());
48
- this.app.use((0, bodyparser_1.default)());
49
- this.app.keys = [config?.secret || keygen_js_1.KeyGen.getSystemSecret()];
50
- this.app.use((0, koa_session_1.default)({
51
- key: config?.secret || keygen_js_1.KeyGen.getSystemSecret(),
52
- maxAge: 86400000,
53
- autoCommit: true,
54
- overwrite: false,
55
- httpOnly: true,
56
- signed: true,
57
- rolling: false,
58
- renew: false,
59
- secure: process.env.NODE_ENV === 'production',
60
- }, this.app));
61
96
  this.port = port;
62
97
  this.config = config;
98
+ this.app.on('error', (err, ctx) => {
99
+ if (ctx?.res?.headersSent)
100
+ return;
101
+ display_1.display.error('server', err?.message ?? String(err));
102
+ });
103
+ process.on('unhandledRejection', (reason) => {
104
+ const message = reason instanceof Error
105
+ ? reason.stack ?? reason.message
106
+ : typeof reason === 'string'
107
+ ? reason
108
+ : JSON.stringify(reason ?? 'Unknown rejection');
109
+ display_1.display.error('server', message);
110
+ });
63
111
  }
64
- // ?? Register model ??????????????????????????????????????????????????????????
65
112
  registerModel(id, scope, model) {
66
113
  this.registeredModels.push({ id, scope, model });
67
114
  return this;
68
115
  }
69
- // ?? Register extension ??????????????????????????????????????????????????????
70
116
  registerExtension(extension) {
71
117
  this.extensions.push(extension);
72
118
  return this;
73
119
  }
74
- // ?? Setup extensions ??????????????????????????????????????????????????????
120
+ registerOIDC(adapter) {
121
+ this._oidcAdapterClass = adapter;
122
+ return this;
123
+ }
75
124
  async setupExtensions() {
76
- console.log('[Server] Setting up extensions...');
77
125
  for (const ext of this.extensions) {
78
- console.log(`[Server] Setting up extension: ${ext.id}`);
79
126
  const extConfig = this.extensionConfigs.get(ext.id) || {};
80
- console.log(`[Server] Extension ${ext.id} config:`, extConfig);
81
127
  await ext.setup(this, extConfig);
82
- console.log(`[Server] Extension ${ext.id} setup complete`);
83
128
  }
84
- console.log('[Server] All extensions setup complete');
85
129
  }
86
- // ?? Auto-mount API routes ?????????????????????????????????????????????????????????
87
- mountModelRoutes() {
130
+ async mountModelRoutes() {
88
131
  for (const { id, scope, model } of this.registeredModels) {
89
- this._mountModelRecursive(id, scope, model);
132
+ await this._mountModelRecursive(id, scope, model);
90
133
  }
91
134
  }
92
- _mountModelRecursive(id, scope, model) {
135
+ async _mountModelRecursive(id, scope, model) {
93
136
  this._mountModel(id, scope, model);
94
137
  for (const ext of model.getExtensions?.() ?? []) {
95
- this._mountModelRecursive(id, `${scope}/${ext.id}`, ext);
138
+ await this._mountModelRecursive(id, `${scope}/${ext.id}`, ext);
96
139
  }
97
140
  }
98
141
  _mountModel(id, scope, model) {
99
- // ?? CRUD operations ???????????????????????????????????????????????????
100
142
  for (const operation of this.API_OPERATIONS) {
101
143
  const routePath = `/${id}/${scope}/${operation}`;
102
144
  const handler = async (ctx) => {
@@ -111,8 +153,7 @@ class Server extends eventemitter3_1.EventEmitter {
111
153
  result = await model.update(key, data, { private: true });
112
154
  }
113
155
  else if (operation === 'delete') {
114
- const query = Object.keys(ctx.request.body ?? {}).length ? ctx.request.body : ctx.query;
115
- result = await model.delete(query);
156
+ result = await model.delete(body);
116
157
  }
117
158
  else if (operation === 'get') {
118
159
  result = await model.get(body, { private: true });
@@ -146,12 +187,11 @@ class Server extends eventemitter3_1.EventEmitter {
146
187
  else if (operation === 'list')
147
188
  this.router.get(routePath, handler);
148
189
  }
149
- // ?? Custom methods ????????????????????????????????????????????????????
150
190
  for (const method of model.getMethods?.() ?? []) {
151
191
  this.router.post(`/${id}/${scope}/${method}`, async (ctx) => {
152
192
  const { _id, ...rest } = ctx.request.body ?? {};
153
193
  try {
154
- const result = await model.call(method, { _id, ...rest });
194
+ const result = await model.call(method, { _id, ...rest }, ctx);
155
195
  ctx.status = 200;
156
196
  ctx.body = { ok: true, entry: result };
157
197
  }
@@ -161,21 +201,13 @@ class Server extends eventemitter3_1.EventEmitter {
161
201
  }
162
202
  });
163
203
  }
164
- // ?? Upload methods (multipart HTTP) ???????????????????????????????????
165
204
  for (const uploadId of model.getUploads?.() ?? []) {
166
205
  this.upload(`/${id}/${scope}/${uploadId}`, async (ctx, file, fields) => {
167
- try {
168
- return await model.callUpload(uploadId, file, fields);
169
- }
170
- catch (err) {
171
- throw err;
172
- }
206
+ return model.callUpload(uploadId, file, fields);
173
207
  });
174
208
  }
175
- // ?? View routes ??????????????????????????????????????????????????????????????????????
176
209
  for (const view of model.getViews?.() ?? []) {
177
210
  const routePath = `/${id}/${scope}${view.path}`.replace(/\/$/, '') || '/';
178
- console.log('[View] Mounting route:', routePath);
179
211
  this.router.get(routePath, async (ctx) => {
180
212
  try {
181
213
  ctx.type = 'text/html';
@@ -186,8 +218,8 @@ class Server extends eventemitter3_1.EventEmitter {
186
218
  ctx.body = { ok: false, error: err?.message ?? err };
187
219
  }
188
220
  });
221
+ display_1.display.addView(`http://localhost:${this.port}${routePath}`);
189
222
  }
190
- // ?? Static folders ????????????????????????????????????????????????????
191
223
  for (const [urlPath, { path: folderPath }] of model.getFolders?.() ?? new Map()) {
192
224
  const staticMiddleware = (0, koa_static_1.default)(folderPath);
193
225
  this.app.use(async (ctx, next) => {
@@ -198,32 +230,529 @@ class Server extends eventemitter3_1.EventEmitter {
198
230
  return next();
199
231
  });
200
232
  }
201
- // ?? Stream methods (binary socket) ????????????????????????????????????
202
- for (const streamId of model.getStreams?.() ?? []) {
203
- // Streams are registered globally in _setupStream
204
- // Store them for later reference
233
+ }
234
+ // ?? OIDC Setup ????????????????????????????????????????????????????????????
235
+ async _setupOIDC(oidcConfigRaw, mountPath) {
236
+ const secret = this.config?.secret || keygen_js_1.KeyGen.getSystemSecret();
237
+ const port = this.port;
238
+ const adapter = this._oidcAdapterClass ?? adapter_1.OIDCAdapter;
239
+ const baseUrl = process.env.BASE_URL ?? `http://localhost:${port}`;
240
+ const issuer = oidcConfigRaw?.issuer ?? baseUrl;
241
+ const scopes = oidcConfigRaw?.scopes ?? ['openid', 'email', 'profile', 'phone', 'address', 'offline_access'];
242
+ const claims = oidcConfigRaw?.claims ?? {
243
+ openid: ['sub'],
244
+ email: ['email', 'email_verified'],
245
+ phone: ['phone_number', 'phone_number_verified'],
246
+ profile: [
247
+ 'name', 'family_name', 'given_name', 'middle_name',
248
+ 'nickname', 'preferred_username', 'picture', 'website',
249
+ 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at',
250
+ ],
251
+ address: ['address'],
252
+ };
253
+ const ttl = {
254
+ AccessToken: 60 * 60,
255
+ AuthorizationCode: 10 * 60,
256
+ IdToken: 60 * 60,
257
+ RefreshToken: 14 * 24 * 60 * 60,
258
+ Session: 14 * 24 * 60 * 60,
259
+ Interaction: 60 * 60,
260
+ Grant: 14 * 24 * 60 * 60,
261
+ ...(oidcConfigRaw?.ttl ?? {}),
262
+ };
263
+ const users = oidcConfigRaw?.users ?? user_1.UserModel;
264
+ const clientModel = oidcConfigRaw?.clients ?? client_1.ClientModel;
265
+ const federationModel = oidcConfigRaw?.federation ?? federation_1.FederationModel;
266
+ const tokenModel = oidcConfigRaw?.tokens ?? token_1.TokenModel;
267
+ const federation = await federationModel.list({}, { count: 10000 }).catch(() => []);
268
+ const bundler = oidcConfigRaw?.view instanceof bundler_1.Bundler ? oidcConfigRaw.view : null;
269
+ const view = bundler ? new view_1.View(`/${mountPath}/interaction/:uid`, {}, bundler) : null;
270
+ if (bundler) {
271
+ bundler.build().then((result) => { if (view)
272
+ view.result = result; });
273
+ const fs = require('node:fs');
274
+ fs.watch(bundler.path, () => {
275
+ bundler.rebuild().then((result) => { if (view)
276
+ view.result = result; });
277
+ });
278
+ }
279
+ const findAccount = users?.findAccount
280
+ ?? (async (_ctx, id) => {
281
+ const user = await users?.get({ _id: id }, { private: false });
282
+ if (!user)
283
+ return undefined;
284
+ return {
285
+ accountId: user._id.toString(),
286
+ claims: async () => ({
287
+ sub: user._id,
288
+ email: user.email,
289
+ email_verified: user.emailVerified ?? false,
290
+ phone_number: user.phoneNumber,
291
+ phone_number_verified: user.phoneVerified ?? false,
292
+ name: user.name,
293
+ given_name: user.givenName,
294
+ family_name: user.familyName,
295
+ middle_name: user.middleName,
296
+ nickname: user.nickname,
297
+ preferred_username: user.username,
298
+ picture: user.picture,
299
+ website: user.website,
300
+ gender: user.gender,
301
+ birthdate: user.birthdate,
302
+ zoneinfo: user.zoneinfo,
303
+ locale: user.locale,
304
+ address: user.address,
305
+ updated_at: user.updatedAt
306
+ ? Math.floor(new Date(user.updatedAt).getTime() / 1000)
307
+ : undefined,
308
+ }),
309
+ };
310
+ });
311
+ const verifyUser = users?.verifyUser
312
+ ?? ((username, password) => users?.call('verify', { username, password }));
313
+ const findOrCreateFederatedUser = users?.findOrCreateFederatedUser
314
+ ?? (async (email) => {
315
+ let user = await users?.get({ email }).catch(() => null);
316
+ if (!user) {
317
+ user = await users?.insert({
318
+ username: email, email, password: Math.random().toString(36),
319
+ isActive: true, roles: 'user', _sync: true,
320
+ });
321
+ }
322
+ return user;
323
+ });
324
+ // ?? Resolve return_to from config ??
325
+ const redirect = oidcConfigRaw?.redirect;
326
+ if (!redirect)
327
+ throw new Error(`[OIDC] registerOIDCServer('${mountPath}') requires a static redirect`);
328
+ let returnTo;
329
+ if (typeof redirect === 'string' && redirect.startsWith('http')) {
330
+ returnTo = redirect;
331
+ }
332
+ else {
333
+ const model = this.registeredModels.find(m => m.scope === redirect || m.id === redirect);
334
+ if (!model)
335
+ throw new Error(`[OIDC] redirect model '${redirect}' not found`);
336
+ const views = model.model.getViews?.();
337
+ if (!views?.length)
338
+ throw new Error(`[OIDC] redirect model '${redirect}' has no views`);
339
+ returnTo = `${baseUrl}/${model.id}/${model.scope}${views[0].path}`;
340
+ }
341
+ // ?? Seed default client ??????????????????????????????????????????
342
+ try {
343
+ const clientAdapter = new adapter('Client');
344
+ const defaultClient = {
345
+ client_id: `${mountPath}-default`,
346
+ client_secret: 'default-secret',
347
+ redirect_uris: [`${issuer}/${mountPath}/callback`],
348
+ return_to: returnTo,
349
+ grant_types: ['authorization_code', 'refresh_token'],
350
+ response_types: ['code'],
351
+ token_endpoint_auth_method: 'client_secret_basic',
352
+ scopes: ['openid', 'email', 'profile', 'offline_access'],
353
+ isActive: true,
354
+ };
355
+ await clientAdapter.upsert(defaultClient.client_id, defaultClient, 0);
356
+ display_1.display.log('oidc', `Clients seeded for /${mountPath}`);
205
357
  }
358
+ catch (err) {
359
+ display_1.display.error('oidc:seed', JSON.stringify(err));
360
+ throw err;
361
+ }
362
+ // ?? Build JWKS ????????????????????????????????????????????????
363
+ const { generateKeyPair, exportJWK } = await import('jose');
364
+ const fs = require('node:fs');
365
+ const path = require('node:path');
366
+ const JWKS_PATH = path.resolve(process.cwd(), './data/oidc.jwks.json');
367
+ let jwks;
368
+ if (process.env.OIDC_JWKS) {
369
+ jwks = JSON.parse(process.env.OIDC_JWKS);
370
+ }
371
+ else if (fs.existsSync(JWKS_PATH)) {
372
+ jwks = JSON.parse(fs.readFileSync(JWKS_PATH, 'utf-8'));
373
+ }
374
+ else {
375
+ const { privateKey } = await generateKeyPair('RS256', { extractable: true });
376
+ const jwk = await exportJWK(privateKey);
377
+ jwk.use = 'sig';
378
+ jwk.alg = 'RS256';
379
+ jwk.kid = `key-${Date.now()}`;
380
+ jwks = { keys: [jwk] };
381
+ fs.mkdirSync(path.dirname(JWKS_PATH), { recursive: true });
382
+ fs.writeFileSync(JWKS_PATH, JSON.stringify(jwks, null, 2), 'utf-8');
383
+ }
384
+ // ?? Create provider ???????????????????????????????????????????????????
385
+ const provider = new oidc_provider_1.default(issuer, {
386
+ adapter,
387
+ jwks,
388
+ findAccount,
389
+ scopes,
390
+ claims,
391
+ ttl,
392
+ features: {
393
+ devInteractions: { enabled: !view },
394
+ clientCredentials: { enabled: true },
395
+ introspection: { enabled: true },
396
+ revocation: { enabled: true },
397
+ rpInitiatedLogout: { enabled: true },
398
+ },
399
+ issueRefreshToken: async (ctx, client, code) => {
400
+ return client.grantTypeAllowed('refresh_token');
401
+ },
402
+ loadExistingGrant: async (ctx) => {
403
+ if (!ctx.oidc.session?.accountId)
404
+ return undefined;
405
+ const grant = new ctx.oidc.provider.Grant({
406
+ accountId: ctx.oidc.session.accountId,
407
+ clientId: ctx.oidc.client.clientId,
408
+ });
409
+ grant.addOIDCScope('openid email profile offline_access');
410
+ await grant.save();
411
+ return grant;
412
+ },
413
+ extraClientMetadata: {
414
+ properties: ['return_to'],
415
+ },
416
+ pkce: { required: () => true },
417
+ interactions: {
418
+ url: (_ctx, interaction) => `/${mountPath}/interaction/${interaction.uid}`,
419
+ },
420
+ cookies: {
421
+ keys: [process.env.OIDC_COOKIE_SECRET ?? secret],
422
+ short: { secure: false, sameSite: 'lax' },
423
+ long: { secure: false, sameSite: 'lax' },
424
+ },
425
+ });
426
+ provider.proxy = true;
427
+ provider.on('server_error', (ctx, err) => {
428
+ display_1.display.error('oidc', err?.message ?? String(err));
429
+ });
430
+ // ?? Store local refresh token on grant.success — covers both email/password and federated ??
431
+ provider.on('grant.success', async (ctx) => {
432
+ console.log('grant.success fired', ctx.oidc.entities?.RefreshToken?.jti);
433
+ try {
434
+ const refreshToken = ctx.oidc.entities?.RefreshToken;
435
+ const account = ctx.oidc.entities?.Account;
436
+ const params = ctx.oidc.params;
437
+ if (!refreshToken || !account?.accountId)
438
+ return;
439
+ const existing = await tokenModel.find({
440
+ accountId: account.accountId,
441
+ provider: 'local',
442
+ appId: params.client_id,
443
+ }).then((r) => r?.[0] ?? null).catch(() => null);
444
+ const data = {
445
+ refreshToken: refreshToken.jti,
446
+ scope: refreshToken.scope,
447
+ expiresAt: new Date(Date.now() + ttl.RefreshToken * 1000).toISOString(),
448
+ };
449
+ if (existing) {
450
+ await tokenModel.update({ _id: existing._id }, data);
451
+ }
452
+ else {
453
+ await tokenModel.insert({
454
+ _sync: true,
455
+ accountId: account.accountId,
456
+ provider: 'local',
457
+ appId: params.client_id,
458
+ ...data,
459
+ });
460
+ }
461
+ }
462
+ catch (err) {
463
+ display_1.display.error('oidc:grant', err?.message ?? String(err));
464
+ }
465
+ });
466
+ // ?? PKCE store — keyed by code_challenge ??
467
+ const pkceStore = new Map();
468
+ // ?? Federation ??????????????????????????????????????????????????????
469
+ const federationClients = new Map();
470
+ for (const fed of federation) {
471
+ try {
472
+ const client = await openidClient.discovery(new URL(fed.issuer), fed.client_id, fed.client_secret);
473
+ federationClients.set(fed.name, { client, config: fed });
474
+ display_1.display.log('oidc', `Federation configured: ${fed.name}`);
475
+ }
476
+ catch (err) {
477
+ display_1.display.error('oidc', `Federation setup failed for ${fed.name}: ${err.message}`);
478
+ }
479
+ }
480
+ // ?? Interaction router ???????????????????????????????????????????????
481
+ const interactionRouter = new koa_router_1.default();
482
+ const body = (0, koa_body_1.koaBody)({ text: false, json: true, patchNode: true, patchKoa: true });
483
+ interactionRouter.use(async (ctx, next) => {
484
+ ctx.set('cache-control', 'no-store');
485
+ await next();
486
+ });
487
+ interactionRouter.get(`/${mountPath}/interaction/:uid`, async (ctx) => {
488
+ const details = await provider.interactionDetails(ctx.req, ctx.res);
489
+ ctx.type = 'text/html';
490
+ ctx.body = await view.render({ uid: details.uid, prompt: details.prompt.name, params: details.params });
491
+ });
492
+ interactionRouter.post(`/${mountPath}/interaction/:uid/confirm`, body, async (ctx) => {
493
+ const interactionDetails = await provider.interactionDetails(ctx.req, ctx.res);
494
+ const { prompt: { name, details }, params, session: { accountId } } = interactionDetails;
495
+ if (name !== 'consent') {
496
+ ctx.status = 400;
497
+ return;
498
+ }
499
+ let { grantId } = interactionDetails;
500
+ let grant;
501
+ if (grantId) {
502
+ grant = await provider.Grant.find(grantId);
503
+ }
504
+ else {
505
+ grant = new provider.Grant({ accountId, clientId: params.client_id });
506
+ }
507
+ if (details.missingOIDCScope)
508
+ grant.addOIDCScope(details.missingOIDCScope.join(' '));
509
+ if (details.missingOIDCClaims)
510
+ grant.addOIDCClaims(details.missingOIDCClaims);
511
+ if (details.missingResourceScopes) {
512
+ for (const [indicator, scopeStr] of Object.entries(details.missingResourceScopes))
513
+ grant.addResourceScope(indicator, scopeStr.join(' '));
514
+ }
515
+ grantId = await grant.save();
516
+ const consent = {};
517
+ if (!interactionDetails.grantId)
518
+ consent.grantId = grantId;
519
+ await provider.interactionFinished(ctx.req, ctx.res, { consent }, { mergeWithLastSubmission: true });
520
+ });
521
+ interactionRouter.post(`/${mountPath}/interaction/:uid/login`, body, async (ctx) => {
522
+ let interactionName;
523
+ try {
524
+ const { prompt: { name } } = await provider.interactionDetails(ctx.req, ctx.res);
525
+ interactionName = name;
526
+ }
527
+ catch (err) {
528
+ ctx.redirect(`${issuer}/auth?client_id=${mountPath}-default&redirect_uri=${issuer}/${mountPath}/callback&response_type=code&scope=openid%20email%20profile%20offline_access`);
529
+ return;
530
+ }
531
+ if (!['login', 'consent'].includes(interactionName)) {
532
+ ctx.status = 400;
533
+ return;
534
+ }
535
+ const { email, password } = ctx.request.body ?? {};
536
+ if (!email || !password) {
537
+ ctx.status = 400;
538
+ ctx.body = { error: 'Email and password are required' };
539
+ return;
540
+ }
541
+ try {
542
+ const result = await verifyUser(email, password);
543
+ if (!result?.ok) {
544
+ ctx.status = 401;
545
+ ctx.body = { error: result?.error ?? 'Invalid credentials' };
546
+ return;
547
+ }
548
+ const accountId = result.user._id.toString();
549
+ const redirectTo = await provider.interactionResult(ctx.req, ctx.res, {
550
+ login: { accountId },
551
+ });
552
+ ctx.body = { redirectTo };
553
+ }
554
+ catch (err) {
555
+ ctx.status = 400;
556
+ ctx.body = { error: err.message, code: err.code, details: err.details };
557
+ }
558
+ });
559
+ interactionRouter.post(`/${mountPath}/interaction/:uid/signup`, body, async (ctx) => {
560
+ let interactionName;
561
+ try {
562
+ const { prompt: { name } } = await provider.interactionDetails(ctx.req, ctx.res);
563
+ interactionName = name;
564
+ }
565
+ catch (err) {
566
+ ctx.redirect(`${issuer}/auth?client_id=${mountPath}-default&redirect_uri=${issuer}/${mountPath}/callback&response_type=code&scope=openid%20email%20profile%20offline_access`);
567
+ return;
568
+ }
569
+ if (!['login', 'consent'].includes(interactionName)) {
570
+ ctx.status = 400;
571
+ return;
572
+ }
573
+ const { email, password } = ctx.request.body ?? {};
574
+ const validation = await new core_1.Validator({
575
+ type: 'object',
576
+ properties: {
577
+ email: { type: 'string' },
578
+ password: { type: 'string' },
579
+ }
580
+ }).validate({ email, password });
581
+ if (validation) {
582
+ ctx.status = 400;
583
+ ctx.body = { error: 'Validation failed', details: validation };
584
+ return;
585
+ }
586
+ const existing = await users?.get({ email }).catch(() => null);
587
+ if (existing) {
588
+ ctx.status = 409;
589
+ ctx.body = { error: 'Email already registered' };
590
+ return;
591
+ }
592
+ const hashedPassword = await (0, utils_1.hashPassword)(password);
593
+ try {
594
+ const user = await users?.insert({
595
+ email,
596
+ password: hashedPassword,
597
+ username: email.split('@').shift(),
598
+ isActive: true,
599
+ roles: 'user',
600
+ emailVerified: false,
601
+ failedLoginAttempts: 0,
602
+ });
603
+ const accountId = user._id.toString();
604
+ const redirectTo = await provider.interactionResult(ctx.req, ctx.res, {
605
+ login: { accountId },
606
+ });
607
+ ctx.body = { redirectTo };
608
+ }
609
+ catch (err) {
610
+ ctx.status = 400;
611
+ ctx.body = { error: err.message, code: err.code, details: err.details };
612
+ }
613
+ });
614
+ interactionRouter.get(`/${mountPath}/interaction/:uid/abort`, async (ctx) => {
615
+ await provider.interactionFinished(ctx.req, ctx.res, {
616
+ error: 'access_denied', error_description: 'End-User aborted interaction',
617
+ }, { mergeWithLastSubmission: false });
618
+ });
619
+ for (const [name, { client: fedClient, config: fedConfig }] of federationClients) {
620
+ interactionRouter.post(`/${mountPath}/interaction/:uid/federated/${name}`, body, async (ctx) => {
621
+ const { prompt: { name: promptName } } = await provider.interactionDetails(ctx.req, ctx.res);
622
+ if (promptName !== 'login') {
623
+ ctx.status = 400;
624
+ return;
625
+ }
626
+ const code_verifier = openidClient.randomPKCECodeVerifier();
627
+ const BASE_URL = process.env.BASE_URL ?? `http://localhost:${port}`;
628
+ ctx.status = 303;
629
+ ctx.redirect(openidClient.buildAuthorizationUrl(fedClient, {
630
+ redirect_uri: `${BASE_URL}/${mountPath}/interaction/callback/${name}`,
631
+ scope: fedConfig.scopes,
632
+ code_challenge: await openidClient.calculatePKCECodeChallenge(code_verifier),
633
+ code_challenge_method: 'S256',
634
+ state: ctx.params.uid,
635
+ nonce: code_verifier,
636
+ access_type: 'offline', // ask Google for refresh token
637
+ prompt: 'consent', // force re-issue of refresh token
638
+ }));
639
+ });
640
+ interactionRouter.get(`/${mountPath}/interaction/callback/${name}`, async (ctx) => {
641
+ const uid = ctx.query.state;
642
+ // ?? Strip extra params Google appends (iss, authuser, prompt) that break oauth4webapi ??
643
+ const url = new URL(`http://localhost:${port}${ctx.path}`);
644
+ url.searchParams.set('code', ctx.query.code);
645
+ url.searchParams.set('state', ctx.query.state);
646
+ const tokens = await openidClient.authorizationCodeGrant(fedClient, url, { idTokenExpected: true, expectedState: uid });
647
+ const claims = tokens.claims();
648
+ if (!claims?.email)
649
+ throw new Error('No email in federated token claims');
650
+ const user = await findOrCreateFederatedUser(claims.email);
651
+ // ?? Store upstream refresh token in TokenModel ??
652
+ if (tokens.refresh_token) {
653
+ const existing = await tokenModel.find({
654
+ accountId: user._id,
655
+ provider: name,
656
+ appId: fedConfig.client_id,
657
+ }).then((r) => r?.[0] ?? null).catch(() => null);
658
+ if (existing) {
659
+ await tokenModel.update({ _id: existing._id }, { refreshToken: tokens.refresh_token });
660
+ }
661
+ else {
662
+ await tokenModel.insert({
663
+ _sync: true,
664
+ accountId: user._id,
665
+ provider: name,
666
+ appId: fedConfig.client_id,
667
+ refreshToken: tokens.refresh_token,
668
+ scope: fedConfig.scopes,
669
+ expiresAt: null,
670
+ });
671
+ }
672
+ }
673
+ await provider.interactionFinished(ctx.req, ctx.res, { login: { accountId: user._id } }, { mergeWithLastSubmission: false });
674
+ });
675
+ }
676
+ const interactionRoutes = interactionRouter.routes();
677
+ provider.use(async (ctx, next) => {
678
+ if (ctx.path.startsWith(`/${mountPath}/interaction`))
679
+ await interactionRoutes(ctx, next);
680
+ else
681
+ await next();
682
+ });
683
+ this.router.post(`/${mountPath}/token`, async (ctx) => {
684
+ const { code, code_verifier } = (ctx.request.body ?? {});
685
+ if (!code) {
686
+ ctx.status = 400;
687
+ ctx.body = { error: 'Missing code' };
688
+ return;
689
+ }
690
+ const tokenRes = await fetch(`${issuer}/token`, {
691
+ method: 'POST',
692
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
693
+ body: new URLSearchParams({
694
+ grant_type: 'authorization_code',
695
+ code,
696
+ redirect_uri: `${issuer}/${mountPath}/callback`,
697
+ client_id: `${mountPath}-default`,
698
+ client_secret: 'default-secret',
699
+ ...(code_verifier ? { code_verifier } : {}),
700
+ }).toString(),
701
+ });
702
+ const tokens = await tokenRes.json();
703
+ ctx.body = {
704
+ access_token: tokens.access_token ?? '',
705
+ refresh_token: tokens.refresh_token ?? '',
706
+ id_token: tokens.id_token ?? '',
707
+ return_to: returnTo,
708
+ };
709
+ });
710
+ this.router.get(`/${mountPath}/callback`, async (ctx) => {
711
+ const { code } = ctx.query;
712
+ if (!code) {
713
+ ctx.status = 400;
714
+ ctx.body = { error: 'Missing code' };
715
+ return;
716
+ }
717
+ ctx.type = 'text/html';
718
+ ctx.body = `<!DOCTYPE html><html><body><script>
719
+ (async () => {
720
+ const verifier = sessionStorage.getItem('pkce_verifier');
721
+ sessionStorage.removeItem('pkce_verifier');
722
+ const res = await fetch('/${mountPath}/token', {
723
+ method : 'POST',
724
+ headers : { 'Content-Type': 'application/json' },
725
+ body : JSON.stringify({ code: ${JSON.stringify(code)}, code_verifier: verifier }),
726
+ });
727
+ const data = await res.json();
728
+ const target = new URL(data.return_to, window.location.origin);
729
+ target.searchParams.set('access_token', data.access_token ?? '');
730
+ target.searchParams.set('refresh_token', data.refresh_token ?? '');
731
+ target.searchParams.set('id_token', data.id_token ?? '');
732
+ window.location.href = target.toString();
733
+ })();
734
+ </script></body></html>`;
735
+ });
736
+ provider._hasView = !!view;
737
+ this._oidcProviders.set(mountPath, { provider, federation });
738
+ display_1.display.log('oidc', `Provider ready at ${issuer}`);
739
+ const { randomBytes, createHash } = require('node:crypto');
740
+ const _verifier = randomBytes(32).toString('base64url');
741
+ const _challenge = createHash('sha256').update(_verifier).digest('base64url');
742
+ pkceStore.set(_challenge, _verifier);
743
+ display_1.display.addOIDC(`${issuer}/auth?client_id=${mountPath}-default` +
744
+ `&redirect_uri=${issuer}/${mountPath}/callback` +
745
+ `&response_type=code` +
746
+ `&scope=openid%20email%20profile%20offline_access` +
747
+ `&code_challenge=${_challenge}` +
748
+ `&code_challenge_method=S256`);
206
749
  }
207
750
  // ?? OIDC Providers ?????????????????????????????????????????????????????
208
751
  authPath;
209
- setOIDCProviders(path, providers) {
210
- console.log('[Server] setOIDCProviders called with path:', path, 'providers:', providers?.map(p => p.id));
211
- this.authPath = path;
212
- if (this.config.domain)
213
- path = `${this.config.domain}/${path}`;
214
- path = path.split('/').filter((x) => x).join('/');
215
- providers?.forEach?.((provider) => {
216
- if (!this.providers.find(x => x.clientId === provider.clientId))
217
- this.providers.push({ ...provider });
218
- });
219
- // Store OIDC config for extension
220
- this.extensionConfigs.set('oidc', {
221
- providers: providers || [],
222
- authPath: path,
223
- });
752
+ registerOIDCServer(path, config) {
753
+ this._oidcServers.push({ path: path.replace(/^\/+|\/+$/g, ''), config: config ?? {} });
224
754
  return this;
225
755
  }
226
- // ?? Upload ????????????????????????????????????????????????????????????????
227
756
  upload(path, handler) {
228
757
  this.routes.push({
229
758
  method: 'post',
@@ -261,7 +790,6 @@ class Server extends eventemitter3_1.EventEmitter {
261
790
  });
262
791
  return this;
263
792
  }
264
- // ?? Stream ????????????????????????????????????????????????????????????????
265
793
  _streamHandler;
266
794
  _uploadTmp = '/tmp/hotfusion-uploads';
267
795
  stream(handler) {
@@ -298,6 +826,19 @@ class Server extends eventemitter3_1.EventEmitter {
298
826
  catch { }
299
827
  uploads.set(uploadId, { meta: fullMeta, totalChunks, received });
300
828
  socket.emit(`upload:started:${uploadId}`);
829
+ if (totalChunks === 0) {
830
+ uploads.delete(uploadId);
831
+ const emptyBuffer = Buffer.alloc(1, ' ');
832
+ Promise.resolve().then(async () => {
833
+ try {
834
+ const result = handler ? await handler(emptyBuffer, { ...fullMeta }) : { ok: true };
835
+ socket.emit(`upload:complete:${uploadId}`, result);
836
+ }
837
+ catch (err) {
838
+ socket.emit(`upload:error:${uploadId}`, err?.message ?? String(err));
839
+ }
840
+ });
841
+ }
301
842
  });
302
843
  socket.on('upload:chunk', async ({ uploadId, index, data }) => {
303
844
  const upload = uploads.get(uploadId);
@@ -312,11 +853,13 @@ class Server extends eventemitter3_1.EventEmitter {
312
853
  socket.emit(`upload:ack:${uploadId}:${index}`);
313
854
  if (upload.received.size === upload.totalChunks) {
314
855
  const chunks = [];
315
- for (let i = 0; i < upload.totalChunks; i++) {
856
+ for (let i = 0; i < upload.totalChunks; i++)
316
857
  chunks.push(await readFile(`${uploadTmp}/${uploadId}/${i}`));
317
- }
318
858
  const buffer = Buffer.concat(chunks);
319
- await rm(`${uploadTmp}/${uploadId}`, { recursive: true, force: true });
859
+ try {
860
+ await rm(`${uploadTmp}/${uploadId}`, { recursive: true, force: true });
861
+ }
862
+ catch { }
320
863
  uploads.delete(uploadId);
321
864
  let meta = {};
322
865
  if (this.listenerCount('uploaded') > 0) {
@@ -324,8 +867,13 @@ class Server extends eventemitter3_1.EventEmitter {
324
867
  const results = await Promise.all(listeners.map(listener => new Promise(resolve => listener({ resolve, file: upload.meta, buffer }))));
325
868
  meta = Object.assign({}, ...results);
326
869
  }
327
- const result = handler ? await handler(buffer, { ...upload.meta, ...meta }) : { ok: true };
328
- socket.emit(`upload:complete:${uploadId}`, result);
870
+ try {
871
+ const result = handler ? await handler(buffer, { ...upload.meta, ...meta }) : { ok: true };
872
+ socket.emit(`upload:complete:${uploadId}`, result);
873
+ }
874
+ catch (err) {
875
+ socket.emit(`upload:error:${uploadId}`, err?.message ?? String(err));
876
+ }
329
877
  }
330
878
  }
331
879
  catch (err) {
@@ -333,7 +881,6 @@ class Server extends eventemitter3_1.EventEmitter {
333
881
  }
334
882
  });
335
883
  }
336
- // ?? Socket subscriptions ????????????????????????????????????????????????????
337
884
  _setupSubscriptions(socket) {
338
885
  const socketSubs = new Map();
339
886
  const resolveModel = (id, scope) => {
@@ -384,19 +931,16 @@ class Server extends eventemitter3_1.EventEmitter {
384
931
  }
385
932
  });
386
933
  socket.on('disconnect', () => {
387
- for (const { subscription, listener } of socketSubs.values()) {
934
+ for (const { subscription, listener } of socketSubs.values())
388
935
  subscription.off('publish', listener);
389
- }
390
936
  socketSubs.clear();
391
937
  this.registeredModels.forEach(m => m.model.emit('disconnect', socket));
392
938
  });
393
939
  }
394
- // ?? Metadata endpoint ??????????????????????????????????????????????????????
395
940
  _setupMetadataRoute() {
396
941
  this.router.get('/@metadata', async (ctx) => {
397
942
  const buildMeta = (id, scope, model) => ({
398
- id,
399
- scope,
943
+ id, scope,
400
944
  schema: model.getSchema?.(),
401
945
  schemes: model.getSchemes?.(),
402
946
  operations: this.API_OPERATIONS.map(op => ({
@@ -411,60 +955,94 @@ class Server extends eventemitter3_1.EventEmitter {
411
955
  path: `/${id}/${scope}/${m}`,
412
956
  url: `http://localhost:${this.port}/${id}/${scope}/${m}`,
413
957
  })),
414
- extensions: (model.getExtensions?.() ?? []).map((ext) => buildMeta(id, `${scope}/${ext.id}`, ext)),
958
+ extensions: (model.getExtensions?.() ?? []).map((ext) => buildMeta(id, `${scope}/${ext.id}`, ext))
415
959
  });
416
- ctx.body = { api: this.registeredModels.map(m => buildMeta(m.id, m.scope, m.model)) };
960
+ ctx.body = {
961
+ api: this.registeredModels.map(m => buildMeta(m.id, m.scope, m.model)),
962
+ oidc: [...this._oidcProviders.entries()].map(([mountPath, { federation }]) => ({
963
+ path: mountPath,
964
+ federation: federation
965
+ .filter((f) => f.isActive !== false)
966
+ .map((f) => ({
967
+ name: f.name,
968
+ issuer: f.issuer,
969
+ scopes: f.scopes,
970
+ url: `/${mountPath}/interaction/:uid/federated/${f.name}`,
971
+ })),
972
+ endpoints: {
973
+ signin: `/${mountPath}/interaction/:uid/login`,
974
+ signup: `/${mountPath}/interaction/:uid/signup`,
975
+ federate: `/${mountPath}/interaction/:uid/federated/:provider`,
976
+ }
977
+ }))
978
+ };
417
979
  });
418
980
  }
419
- // ?? Route helper (kept for backward compatibility) ??????????????????????????
420
981
  route(route) {
421
982
  route.path = Utils.trimPath(route.path);
422
983
  this.routes.push(route);
423
984
  return this;
424
985
  }
425
- // ?? Start server ????????????????????????????????????????????????????????????
986
+ folder(urlPath, folderPath) {
987
+ const fs = require('node:fs');
988
+ if (!fs.existsSync(folderPath))
989
+ throw new Error(`[Server] folder() path does not exist: ${folderPath}`);
990
+ this._staticFolders.push(urlPath);
991
+ const staticMiddleware = (0, koa_static_1.default)(folderPath);
992
+ this.app.use(async (ctx, next) => {
993
+ if (ctx.path.startsWith(urlPath)) {
994
+ ctx.path = ctx.path.slice(urlPath.length) || '/';
995
+ return staticMiddleware(ctx, next);
996
+ }
997
+ return next();
998
+ });
999
+ return this;
1000
+ }
426
1001
  async start() {
427
- console.log('[Server] Start called');
428
- // Wait a bit then start the server setup
429
- await new Promise(resolve => setTimeout(resolve, 100));
430
- console.log('[Server] Setting up app');
431
- // Error handling middleware
1002
+ this.app.use((0, cors_1.default)());
1003
+ this.app.keys = [this.config?.secret || keygen_js_1.KeyGen.getSystemSecret()];
1004
+ for (const { path: oidcPath, config: oidcConfig } of this._oidcServers) {
1005
+ await this._setupOIDC(oidcConfig, oidcPath);
1006
+ }
1007
+ this.app.use((0, bodyparser_1.default)({ enableTypes: ['json'], parsedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'] }));
1008
+ this.app.use((0, koa_session_1.default)({
1009
+ key: this.config?.secret || keygen_js_1.KeyGen.getSystemSecret(),
1010
+ maxAge: 86400000,
1011
+ autoCommit: true,
1012
+ overwrite: false,
1013
+ httpOnly: true,
1014
+ signed: true,
1015
+ rolling: false,
1016
+ renew: false,
1017
+ secure: process.env.NODE_ENV === 'production',
1018
+ }, this.app));
432
1019
  this.app.use(async (ctx, next) => {
433
1020
  try {
434
1021
  await next();
435
1022
  }
436
1023
  catch (err) {
1024
+ if (ctx.res.headersSent)
1025
+ return;
437
1026
  ctx.status = err.status || 500;
438
- ctx.body = {
439
- error: err.message || 'Internal error',
440
- status: err.status
441
- };
1027
+ ctx.body = { error: err.message || 'Internal error', status: err.status };
442
1028
  }
443
1029
  });
444
- // Mount auto-generated model routes
445
- this.mountModelRoutes();
446
- // Setup extensions FIRST (before any routes)
447
- console.log('[Server] Before setupExtensions');
1030
+ await this.mountModelRoutes();
448
1031
  await this.setupExtensions();
449
- console.log('[Server] After setupExtensions');
450
- // Mount metadata endpoint
451
1032
  this._setupMetadataRoute();
452
- // Mount manually registered routes
453
1033
  for (let i = 0; i < this.routes.length; i++) {
454
- let { method, path, callback } = this.routes[i] || {};
1034
+ const { method, path, callback } = this.routes[i] || {};
455
1035
  if (method && path && callback) {
456
1036
  const handler = async (ctx, next) => {
457
- let flags = {
458
- protected: this.routes?.[i]?.protected || false
459
- };
1037
+ const flags = { protected: this.routes?.[i]?.protected || false };
460
1038
  try {
461
1039
  if (flags.protected) {
462
- let TOKEN = ctx.headers.authorization;
1040
+ const TOKEN = ctx.headers.authorization;
463
1041
  if (!TOKEN)
464
1042
  ctx.throw(401, 'Unauthorized');
465
1043
  ctx.token = (await (0, jose_1.jwtVerify)(TOKEN.split(' ').pop(), new TextEncoder().encode(Array.isArray(this.app?.keys) ? this.app.keys[0] : undefined))).payload;
466
1044
  }
467
- let body = await callback(ctx, next);
1045
+ const body = await callback(ctx, next);
468
1046
  if (!ctx.body) {
469
1047
  ctx.status = 200;
470
1048
  ctx.body = body;
@@ -472,9 +1050,7 @@ class Server extends eventemitter3_1.EventEmitter {
472
1050
  }
473
1051
  catch (err) {
474
1052
  ctx.status = 500;
475
- ctx.body = {
476
- error: err.message || err.code || err.details || 'Internal error',
477
- };
1053
+ ctx.body = { error: err.message || err.code || err.details || 'Internal error' };
478
1054
  }
479
1055
  };
480
1056
  if (method === 'get')
@@ -491,21 +1067,54 @@ class Server extends eventemitter3_1.EventEmitter {
491
1067
  }
492
1068
  this.app.use(this.router.routes());
493
1069
  this.app.use(this.router.allowedMethods());
494
- const server = this.app.listen(this.port, '0.0.0.0', () => {
495
- this.emit("mounted", this);
496
- });
497
- this.io = new socket_io_1.Server(server, {
498
- cors: { origin: "*", methods: ["GET", "POST"] },
1070
+ const httpServer = this.app.listen(this.port, '0.0.0.0', async () => {
1071
+ await display_1.display.run(this.port, async () => {
1072
+ this.emit('mounted', this);
1073
+ });
499
1074
  });
500
- this.io.use((socket, next) => {
501
- next();
1075
+ // ?? Mount OIDC providers on raw Node server — intercepts before Koa ??????
1076
+ if (this._oidcProviders.size > 0) {
1077
+ const providers = this._oidcProviders;
1078
+ const staticFolders = this._staticFolders;
1079
+ const originalEmit = httpServer.emit.bind(httpServer);
1080
+ httpServer.emit = function (event, ...args) {
1081
+ if (event === 'request') {
1082
+ const [req, res] = args;
1083
+ const url = req.url ?? '';
1084
+ const isStatic = staticFolders.some(p => url === p || url.startsWith(p + '/') || url.startsWith(p + '?'));
1085
+ if (isStatic)
1086
+ return originalEmit(event, ...args);
1087
+ for (const [mountPath, { provider }] of providers) {
1088
+ const hasView = provider._hasView;
1089
+ const oidcPaths = [
1090
+ '/auth', '/.well-known', '/token', '/me', '/jwks',
1091
+ '/session', '/token/introspection', '/token/revocation',
1092
+ '/request',
1093
+ `/${mountPath}/interaction`,
1094
+ ...(!hasView ? ['/interaction'] : []),
1095
+ ];
1096
+ if (oidcPaths.some((p) => url === p || url.startsWith(p + '?') || url.startsWith(p + '/'))) {
1097
+ if (url === `/${mountPath}/callback` || url.startsWith(`/${mountPath}/callback?`) ||
1098
+ url === `/${mountPath}/token` || url.startsWith(`/${mountPath}/token?`)) {
1099
+ return originalEmit(event, ...args);
1100
+ }
1101
+ provider.callback()(req, res);
1102
+ return true;
1103
+ }
1104
+ }
1105
+ }
1106
+ return originalEmit(event, ...args);
1107
+ };
1108
+ }
1109
+ this.io = new socket_io_1.Server(httpServer, {
1110
+ cors: { origin: '*', methods: ['GET', 'POST'] },
502
1111
  });
503
- this.io.on("connection", (socket) => {
1112
+ this.io.use((socket, next) => { next(); });
1113
+ this.io.on('connection', (socket) => {
504
1114
  this._setupStream(socket);
505
1115
  this._setupSubscriptions(socket);
506
1116
  this.emit('connection', { socket });
507
1117
  });
508
- console.log('[Server] Start complete, listening on port', this.port);
509
1118
  return this;
510
1119
  }
511
1120
  }