@objectstack/plugin-hono-server 4.0.4 → 4.1.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/dist/index.js CHANGED
@@ -34,7 +34,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
34
34
  var index_exports = {};
35
35
  __export(index_exports, {
36
36
  HonoHttpServer: () => HonoHttpServer,
37
- HonoServerPlugin: () => HonoServerPlugin
37
+ HonoServerPlugin: () => HonoServerPlugin,
38
+ createOriginMatcher: () => createOriginMatcher,
39
+ hasWildcardPattern: () => hasWildcardPattern,
40
+ isLocalhostOrigin: () => isLocalhostOrigin,
41
+ matchOriginPattern: () => matchOriginPattern,
42
+ normalizeOriginPatterns: () => normalizeOriginPatterns
38
43
  });
39
44
  module.exports = __toCommonJS(index_exports);
40
45
 
@@ -60,7 +65,9 @@ var HonoHttpServer = class {
60
65
  wrap(handler) {
61
66
  return async (c) => {
62
67
  let body = {};
63
- if (c.req.header("content-type")?.includes("application/json")) {
68
+ const contentType = c.req.header("content-type") ?? "";
69
+ const isOctetStream = contentType.includes("application/octet-stream");
70
+ if (contentType.includes("application/json")) {
64
71
  try {
65
72
  body = await c.req.json();
66
73
  } catch (e) {
@@ -69,19 +76,31 @@ var HonoHttpServer = class {
69
76
  } catch (e2) {
70
77
  }
71
78
  }
72
- } else {
79
+ } else if (!isOctetStream) {
73
80
  try {
74
81
  body = await c.req.parseBody();
75
82
  } catch (e) {
76
83
  }
77
84
  }
85
+ const rawHeaders = c.req.header();
86
+ if (!rawHeaders.host) {
87
+ try {
88
+ const u = new URL(c.req.url);
89
+ if (u.host) rawHeaders.host = u.host;
90
+ } catch {
91
+ }
92
+ }
78
93
  const req = {
79
94
  params: c.req.param(),
80
95
  query: c.req.query(),
81
96
  body,
82
- headers: c.req.header(),
97
+ headers: rawHeaders,
83
98
  method: c.req.method,
84
- path: c.req.path
99
+ path: c.req.path,
100
+ rawBody: async () => {
101
+ const ab = await c.req.arrayBuffer();
102
+ return Buffer.from(ab);
103
+ }
85
104
  };
86
105
  let capturedResponse;
87
106
  let streamController = null;
@@ -136,13 +155,13 @@ var HonoHttpServer = class {
136
155
  streamController?.close();
137
156
  resolve2(null);
138
157
  }
139
- }).catch(() => {
158
+ }).catch((err) => {
140
159
  streamController?.close();
141
160
  resolve2(null);
142
161
  });
143
162
  });
144
163
  const streamResponse = await streamPromise;
145
- return streamResponse ?? capturedResponse;
164
+ return streamResponse ?? capturedResponse ?? c.json({ error: "No response from handler" }, 500);
146
165
  };
147
166
  }
148
167
  get(path2, handler) {
@@ -163,11 +182,23 @@ var HonoHttpServer = class {
163
182
  use(pathOrHandler, handler) {
164
183
  if (typeof pathOrHandler === "string" && handler) {
165
184
  this.app.use(pathOrHandler, async (c, next) => {
166
- await handler({}, {}, next);
185
+ let nextCalled = false;
186
+ const wrappedNext = () => {
187
+ nextCalled = true;
188
+ return next();
189
+ };
190
+ await handler({}, {}, wrappedNext);
191
+ if (!nextCalled) await next();
167
192
  });
168
193
  } else if (typeof pathOrHandler === "function") {
169
194
  this.app.use("*", async (c, next) => {
170
- await pathOrHandler({}, {}, next);
195
+ let nextCalled = false;
196
+ const wrappedNext = () => {
197
+ nextCalled = true;
198
+ return next();
199
+ };
200
+ await pathOrHandler({}, {}, wrappedNext);
201
+ if (!nextCalled) await next();
171
202
  });
172
203
  }
173
204
  }
@@ -222,9 +253,13 @@ var HonoHttpServer = class {
222
253
  return this.app;
223
254
  }
224
255
  async close() {
225
- if (this.server && typeof this.server.close === "function") {
226
- this.server.close();
256
+ if (!this.server) return;
257
+ if (typeof this.server.closeAllConnections === "function") {
258
+ this.server.closeAllConnections();
227
259
  }
260
+ await new Promise((resolve2, reject) => {
261
+ this.server.close((err) => err ? reject(err) : resolve2());
262
+ });
228
263
  }
229
264
  };
230
265
 
@@ -233,21 +268,29 @@ var import_cors = require("hono/cors");
233
268
  var import_serve_static2 = require("@hono/node-server/serve-static");
234
269
  var fs = __toESM(require("fs"));
235
270
  var path = __toESM(require("path"));
271
+
272
+ // src/pattern-matcher.ts
273
+ function isLocalhostOrigin(origin) {
274
+ return /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin);
275
+ }
236
276
  function matchOriginPattern(origin, pattern) {
277
+ if (isLocalhostOrigin(origin)) return true;
237
278
  if (pattern === "*") return true;
238
279
  if (pattern === origin) return true;
239
280
  const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
240
281
  const regex = new RegExp(`^${regexPattern}$`);
241
282
  return regex.test(origin);
242
283
  }
243
- function createOriginMatcher(patterns) {
244
- let patternList;
245
- if (typeof patterns === "string") {
246
- patternList = patterns.includes(",") ? patterns.split(",").map((s) => s.trim()).filter(Boolean) : [patterns];
247
- } else {
248
- patternList = patterns;
284
+ function normalizeOriginPatterns(patterns) {
285
+ if (Array.isArray(patterns)) {
286
+ return patterns.map((p) => p.trim()).filter(Boolean);
249
287
  }
288
+ return patterns.includes(",") ? patterns.split(",").map((s) => s.trim()).filter(Boolean) : [patterns.trim()].filter(Boolean);
289
+ }
290
+ function createOriginMatcher(patterns) {
291
+ const patternList = normalizeOriginPatterns(patterns);
250
292
  return (requestOrigin) => {
293
+ if (!requestOrigin) return null;
251
294
  for (const pattern of patternList) {
252
295
  if (matchOriginPattern(requestOrigin, pattern)) {
253
296
  return requestOrigin;
@@ -256,6 +299,12 @@ function createOriginMatcher(patterns) {
256
299
  return null;
257
300
  };
258
301
  }
302
+ function hasWildcardPattern(patterns) {
303
+ const list = Array.isArray(patterns) ? patterns : [patterns];
304
+ return list.some((p) => p.includes("*"));
305
+ }
306
+
307
+ // src/hono-plugin.ts
259
308
  var HonoServerPlugin = class {
260
309
  constructor(options = {}) {
261
310
  __publicField(this, "name", "com.objectstack.server.hono");
@@ -291,23 +340,27 @@ var HonoServerPlugin = class {
291
340
  const credentials = corsOpts.credentials ?? process.env.CORS_CREDENTIALS !== "false";
292
341
  const maxAge = corsOpts.maxAge ?? (process.env.CORS_MAX_AGE ? parseInt(process.env.CORS_MAX_AGE, 10) : 86400);
293
342
  let origin;
294
- const hasWildcard = (patterns) => {
295
- const list = Array.isArray(patterns) ? patterns : [patterns];
296
- return list.some((p) => p.includes("*"));
297
- };
298
343
  if (configuredOrigin === "*" && credentials) {
299
344
  origin = (requestOrigin) => requestOrigin || "*";
300
- } else if (hasWildcard(configuredOrigin)) {
345
+ } else if (hasWildcardPattern(configuredOrigin)) {
301
346
  origin = createOriginMatcher(configuredOrigin);
302
347
  } else {
303
- origin = configuredOrigin;
348
+ const matcher = createOriginMatcher(configuredOrigin);
349
+ origin = (requestOrigin) => matcher(requestOrigin);
304
350
  }
305
351
  const rawApp = this.server.getRawApp();
352
+ const defaultAllowHeaders = ["Content-Type", "Authorization", "X-Requested-With", "X-Tenant-ID", "X-Project-Id"];
353
+ const defaultExposeHeaders = ["set-auth-token"];
354
+ const allowHeaders = corsOpts.allowHeaders ?? defaultAllowHeaders;
355
+ const exposeHeaders = Array.from(/* @__PURE__ */ new Set([
356
+ ...defaultExposeHeaders,
357
+ ...corsOpts.exposeHeaders ?? []
358
+ ]));
306
359
  rawApp.use("*", (0, import_cors.cors)({
307
360
  origin,
308
361
  allowMethods: corsOpts.methods || ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
309
- allowHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
310
- exposeHeaders: [],
362
+ allowHeaders,
363
+ exposeHeaders,
311
364
  credentials,
312
365
  maxAge
313
366
  }));
@@ -408,10 +461,16 @@ var HonoServerPlugin = class {
408
461
  });
409
462
  }
410
463
  }
411
- ctx.hook("kernel:ready", async () => {
412
- if (this.options.registerStandardEndpoints) {
464
+ const rawAppForNotFound = this.server.getRawApp();
465
+ if (typeof rawAppForNotFound.notFound === "function") {
466
+ rawAppForNotFound.notFound((c) => c.json({ error: "Not found" }, 404));
467
+ }
468
+ if (this.options.registerStandardEndpoints) {
469
+ ctx.hook("kernel:ready", async () => {
413
470
  this.registerDiscoveryAndCrudEndpoints(ctx);
414
- }
471
+ });
472
+ }
473
+ ctx.hook("kernel:listening", async () => {
415
474
  const port = this.options.port ?? 3e3;
416
475
  ctx.logger.debug("Starting HTTP server", { port });
417
476
  await this.server.listen(port);
@@ -464,33 +523,228 @@ var HonoServerPlugin = class {
464
523
  rawApp.get(`${prefix}/discovery`, (c) => c.json({ data: discovery }));
465
524
  ctx.logger.info("Registered discovery endpoints", { prefix });
466
525
  const getObjectQL = () => ctx.getService("objectql");
526
+ const resolveCtx = async (c) => {
527
+ try {
528
+ const authService = ctx.getService("auth");
529
+ if (!authService) return void 0;
530
+ let api = authService.api;
531
+ if (!api && typeof authService.getApi === "function") {
532
+ api = await authService.getApi();
533
+ }
534
+ if (!api?.getSession) return void 0;
535
+ const session = await api.getSession({ headers: c.req.raw.headers });
536
+ if (!session?.user?.id) return void 0;
537
+ const userId = session.user.id;
538
+ const tenantId = session.session?.activeOrganizationId ?? void 0;
539
+ const permissions = [];
540
+ const roles = [];
541
+ try {
542
+ const ql = getObjectQL();
543
+ const sysCtx = { context: { isSystem: true } };
544
+ const memberRows = await ql?.find?.(
545
+ "sys_member",
546
+ {
547
+ where: tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId },
548
+ limit: 50,
549
+ ...sysCtx
550
+ }
551
+ ).catch(() => []);
552
+ for (const m of memberRows ?? []) {
553
+ if (typeof m.role === "string") {
554
+ for (const r of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
555
+ if (!roles.includes(r)) roles.push(r);
556
+ }
557
+ }
558
+ }
559
+ const upsRows = await ql?.find?.(
560
+ "sys_user_permission_set",
561
+ { where: { user_id: userId }, limit: 100, ...sysCtx }
562
+ ).catch(() => []);
563
+ const psIds = /* @__PURE__ */ new Set();
564
+ for (const r of upsRows ?? []) {
565
+ const orgScope = r.organization_id ?? null;
566
+ if (!orgScope || tenantId && orgScope === tenantId) {
567
+ const pid = r.permission_set_id ?? r.permissionSetId;
568
+ if (pid) psIds.add(pid);
569
+ }
570
+ }
571
+ if (psIds.size > 0) {
572
+ const psRows = await ql?.find?.(
573
+ "sys_permission_set",
574
+ { where: { id: { $in: Array.from(psIds) } }, limit: 500, ...sysCtx }
575
+ ).catch(() => []);
576
+ for (const ps of psRows ?? []) {
577
+ if (ps.name && !permissions.includes(ps.name)) permissions.push(ps.name);
578
+ }
579
+ }
580
+ } catch {
581
+ }
582
+ return {
583
+ userId,
584
+ tenantId,
585
+ roles,
586
+ permissions,
587
+ isSystem: false
588
+ };
589
+ } catch {
590
+ return void 0;
591
+ }
592
+ };
467
593
  rawApp.post(`${prefix}/data/:object`, async (c) => {
468
594
  const ql = getObjectQL();
469
595
  if (!ql) return c.json({ error: "Data service not available" }, 503);
470
596
  const object = c.req.param("object");
471
597
  const data = await c.req.json().catch(() => ({}));
472
- const res = await ql.insert(object, data);
473
- const record = { ...data, ...res };
474
- return c.json({ object, id: record.id, record });
598
+ const execCtx = await resolveCtx(c);
599
+ try {
600
+ const res = await ql.insert(object, data, { context: execCtx });
601
+ const record = { ...data, ...res };
602
+ return c.json({ object, id: record.id, record });
603
+ } catch (err) {
604
+ if (err?.code === "PERMISSION_DENIED" || err?.name === "PermissionDeniedError") {
605
+ return c.json({ error: err.message ?? "Forbidden" }, 403);
606
+ }
607
+ throw err;
608
+ }
475
609
  });
476
610
  rawApp.get(`${prefix}/data/:object/:id`, async (c) => {
477
611
  const ql = getObjectQL();
478
612
  if (!ql) return c.json({ error: "Data service not available" }, 503);
479
613
  const object = c.req.param("object");
480
614
  const id = c.req.param("id");
481
- let all = await ql.find(object);
482
- if (!all) all = [];
483
- const match = all.find((i) => i.id === id);
484
- return match ? c.json({ object, id, record: match }) : c.json({ error: "Not found" }, 404);
615
+ const execCtx = await resolveCtx(c);
616
+ try {
617
+ let all = await ql.find(object, { context: execCtx });
618
+ if (!all) all = [];
619
+ const match = all.find((i) => i.id === id);
620
+ return match ? c.json({ object, id, record: match }) : c.json({ error: "Not found" }, 404);
621
+ } catch (err) {
622
+ if (err?.code === "PERMISSION_DENIED" || err?.name === "PermissionDeniedError") {
623
+ return c.json({ error: err.message ?? "Forbidden" }, 403);
624
+ }
625
+ throw err;
626
+ }
485
627
  });
486
628
  rawApp.get(`${prefix}/data/:object`, async (c) => {
487
629
  const ql = getObjectQL();
488
630
  if (!ql) return c.json({ error: "Data service not available" }, 503);
489
631
  const object = c.req.param("object");
490
- let all = await ql.find(object);
491
- if (!Array.isArray(all) && all && all.value) all = all.value;
492
- if (!all) all = [];
493
- return c.json({ object, records: all, total: all.length });
632
+ const execCtx = await resolveCtx(c);
633
+ try {
634
+ let all = await ql.find(object, { context: execCtx });
635
+ if (!Array.isArray(all) && all && all.value) all = all.value;
636
+ if (!all) all = [];
637
+ return c.json({ object, records: all, total: all.length });
638
+ } catch (err) {
639
+ if (err?.code === "PERMISSION_DENIED" || err?.name === "PermissionDeniedError") {
640
+ return c.json({ error: err.message ?? "Forbidden" }, 403);
641
+ }
642
+ throw err;
643
+ }
644
+ });
645
+ rawApp.get(`${prefix}/auth/me/permissions`, async (c) => {
646
+ const execCtx = await resolveCtx(c);
647
+ if (!execCtx?.userId) {
648
+ return c.json({ authenticated: false });
649
+ }
650
+ try {
651
+ const metadata = ctx.getService("metadata");
652
+ const evaluator = ctx.getService("security.permissions");
653
+ const bootstrap = (() => {
654
+ try {
655
+ return ctx.getService("security.bootstrapPermissionSets") ?? [];
656
+ } catch {
657
+ return [];
658
+ }
659
+ })();
660
+ const fallbackName = (() => {
661
+ try {
662
+ return ctx.getService("security.fallbackPermissionSet") ?? "member_default";
663
+ } catch {
664
+ return "member_default";
665
+ }
666
+ })();
667
+ const ql = (() => {
668
+ try {
669
+ return ctx.getService("objectql");
670
+ } catch {
671
+ return null;
672
+ }
673
+ })();
674
+ const dbLoader = ql ? async (names) => {
675
+ let rows;
676
+ try {
677
+ rows = await ql.find(
678
+ "sys_permission_set",
679
+ { where: { name: { $in: names } }, limit: names.length },
680
+ { context: { isSystem: true } }
681
+ );
682
+ } catch {
683
+ rows = [];
684
+ }
685
+ const list = Array.isArray(rows) ? rows : rows?.records ?? [];
686
+ return list.map((r) => ({
687
+ name: r.name,
688
+ label: r.label,
689
+ objects: typeof r.object_permissions === "string" ? JSON.parse(r.object_permissions || "{}") : r.object_permissions ?? {},
690
+ fields: typeof r.field_permissions === "string" ? JSON.parse(r.field_permissions || "{}") : r.field_permissions ?? {}
691
+ }));
692
+ } : void 0;
693
+ if (!evaluator || !metadata) {
694
+ return c.json({
695
+ authenticated: true,
696
+ userId: execCtx.userId,
697
+ tenantId: execCtx.tenantId ?? null,
698
+ roles: execCtx.roles ?? [],
699
+ permissionSets: execCtx.permissions ?? [],
700
+ objects: {},
701
+ fields: {}
702
+ });
703
+ }
704
+ const requested = [
705
+ ...execCtx.roles ?? [],
706
+ ...execCtx.permissions ?? []
707
+ ];
708
+ let resolved = await evaluator.resolvePermissionSets(requested, metadata, bootstrap, dbLoader).catch(() => []);
709
+ if (resolved.length === 0 && fallbackName) {
710
+ resolved = await evaluator.resolvePermissionSets([fallbackName], metadata, bootstrap, dbLoader).catch(() => []);
711
+ }
712
+ const objects = {};
713
+ const fields = {};
714
+ for (const ps of resolved) {
715
+ if (ps?.objects) {
716
+ for (const [obj, perm] of Object.entries(ps.objects)) {
717
+ const acc = objects[obj] ?? {};
718
+ for (const [k, v] of Object.entries(perm)) {
719
+ if (v === true) acc[k] = true;
720
+ else if (acc[k] === void 0) acc[k] = v;
721
+ }
722
+ objects[obj] = acc;
723
+ }
724
+ }
725
+ if (ps?.fields) {
726
+ for (const [key, perm] of Object.entries(ps.fields)) {
727
+ const acc = fields[key] ?? { readable: false, editable: false };
728
+ const p = perm;
729
+ if (p.readable) acc.readable = true;
730
+ if (p.editable) acc.editable = true;
731
+ fields[key] = acc;
732
+ }
733
+ }
734
+ }
735
+ return c.json({
736
+ authenticated: true,
737
+ userId: execCtx.userId,
738
+ tenantId: execCtx.tenantId ?? null,
739
+ roles: execCtx.roles ?? [],
740
+ permissionSets: resolved.map((p) => p?.name).filter(Boolean),
741
+ objects,
742
+ fields
743
+ });
744
+ } catch (err) {
745
+ ctx.logger.warn("[hono] /auth/me/permissions failed", { err: err?.message });
746
+ return c.json({ authenticated: true, userId: execCtx.userId, objects: {}, fields: {} });
747
+ }
494
748
  });
495
749
  ctx.logger.debug("Registered standard CRUD data endpoints", { prefix });
496
750
  }
@@ -512,6 +766,11 @@ __reExport(index_exports, adapter_exports, module.exports);
512
766
  // Annotate the CommonJS export names for ESM import in node:
513
767
  0 && (module.exports = {
514
768
  HonoHttpServer,
515
- HonoServerPlugin
769
+ HonoServerPlugin,
770
+ createOriginMatcher,
771
+ hasWildcardPattern,
772
+ isLocalhostOrigin,
773
+ matchOriginPattern,
774
+ normalizeOriginPatterns
516
775
  });
517
776
  //# sourceMappingURL=index.js.map