@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.mjs CHANGED
@@ -22,7 +22,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  HonoHttpServer: () => HonoHttpServer,
25
- HonoServerPlugin: () => HonoServerPlugin
25
+ HonoServerPlugin: () => HonoServerPlugin,
26
+ createOriginMatcher: () => createOriginMatcher,
27
+ hasWildcardPattern: () => hasWildcardPattern,
28
+ isLocalhostOrigin: () => isLocalhostOrigin,
29
+ matchOriginPattern: () => matchOriginPattern,
30
+ normalizeOriginPatterns: () => normalizeOriginPatterns
26
31
  });
27
32
 
28
33
  // src/adapter.ts
@@ -48,7 +53,9 @@ var HonoHttpServer = class {
48
53
  wrap(handler) {
49
54
  return async (c) => {
50
55
  let body = {};
51
- if (c.req.header("content-type")?.includes("application/json")) {
56
+ const contentType = c.req.header("content-type") ?? "";
57
+ const isOctetStream = contentType.includes("application/octet-stream");
58
+ if (contentType.includes("application/json")) {
52
59
  try {
53
60
  body = await c.req.json();
54
61
  } catch (e) {
@@ -57,19 +64,31 @@ var HonoHttpServer = class {
57
64
  } catch (e2) {
58
65
  }
59
66
  }
60
- } else {
67
+ } else if (!isOctetStream) {
61
68
  try {
62
69
  body = await c.req.parseBody();
63
70
  } catch (e) {
64
71
  }
65
72
  }
73
+ const rawHeaders = c.req.header();
74
+ if (!rawHeaders.host) {
75
+ try {
76
+ const u = new URL(c.req.url);
77
+ if (u.host) rawHeaders.host = u.host;
78
+ } catch {
79
+ }
80
+ }
66
81
  const req = {
67
82
  params: c.req.param(),
68
83
  query: c.req.query(),
69
84
  body,
70
- headers: c.req.header(),
85
+ headers: rawHeaders,
71
86
  method: c.req.method,
72
- path: c.req.path
87
+ path: c.req.path,
88
+ rawBody: async () => {
89
+ const ab = await c.req.arrayBuffer();
90
+ return Buffer.from(ab);
91
+ }
73
92
  };
74
93
  let capturedResponse;
75
94
  let streamController = null;
@@ -124,13 +143,13 @@ var HonoHttpServer = class {
124
143
  streamController?.close();
125
144
  resolve2(null);
126
145
  }
127
- }).catch(() => {
146
+ }).catch((err) => {
128
147
  streamController?.close();
129
148
  resolve2(null);
130
149
  });
131
150
  });
132
151
  const streamResponse = await streamPromise;
133
- return streamResponse ?? capturedResponse;
152
+ return streamResponse ?? capturedResponse ?? c.json({ error: "No response from handler" }, 500);
134
153
  };
135
154
  }
136
155
  get(path2, handler) {
@@ -151,11 +170,23 @@ var HonoHttpServer = class {
151
170
  use(pathOrHandler, handler) {
152
171
  if (typeof pathOrHandler === "string" && handler) {
153
172
  this.app.use(pathOrHandler, async (c, next) => {
154
- await handler({}, {}, next);
173
+ let nextCalled = false;
174
+ const wrappedNext = () => {
175
+ nextCalled = true;
176
+ return next();
177
+ };
178
+ await handler({}, {}, wrappedNext);
179
+ if (!nextCalled) await next();
155
180
  });
156
181
  } else if (typeof pathOrHandler === "function") {
157
182
  this.app.use("*", async (c, next) => {
158
- await pathOrHandler({}, {}, next);
183
+ let nextCalled = false;
184
+ const wrappedNext = () => {
185
+ nextCalled = true;
186
+ return next();
187
+ };
188
+ await pathOrHandler({}, {}, wrappedNext);
189
+ if (!nextCalled) await next();
159
190
  });
160
191
  }
161
192
  }
@@ -210,9 +241,13 @@ var HonoHttpServer = class {
210
241
  return this.app;
211
242
  }
212
243
  async close() {
213
- if (this.server && typeof this.server.close === "function") {
214
- this.server.close();
244
+ if (!this.server) return;
245
+ if (typeof this.server.closeAllConnections === "function") {
246
+ this.server.closeAllConnections();
215
247
  }
248
+ await new Promise((resolve2, reject) => {
249
+ this.server.close((err) => err ? reject(err) : resolve2());
250
+ });
216
251
  }
217
252
  };
218
253
 
@@ -221,21 +256,29 @@ import { cors } from "hono/cors";
221
256
  import { serveStatic as serveStatic2 } from "@hono/node-server/serve-static";
222
257
  import * as fs from "fs";
223
258
  import * as path from "path";
259
+
260
+ // src/pattern-matcher.ts
261
+ function isLocalhostOrigin(origin) {
262
+ return /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin);
263
+ }
224
264
  function matchOriginPattern(origin, pattern) {
265
+ if (isLocalhostOrigin(origin)) return true;
225
266
  if (pattern === "*") return true;
226
267
  if (pattern === origin) return true;
227
268
  const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
228
269
  const regex = new RegExp(`^${regexPattern}$`);
229
270
  return regex.test(origin);
230
271
  }
231
- function createOriginMatcher(patterns) {
232
- let patternList;
233
- if (typeof patterns === "string") {
234
- patternList = patterns.includes(",") ? patterns.split(",").map((s) => s.trim()).filter(Boolean) : [patterns];
235
- } else {
236
- patternList = patterns;
272
+ function normalizeOriginPatterns(patterns) {
273
+ if (Array.isArray(patterns)) {
274
+ return patterns.map((p) => p.trim()).filter(Boolean);
237
275
  }
276
+ return patterns.includes(",") ? patterns.split(",").map((s) => s.trim()).filter(Boolean) : [patterns.trim()].filter(Boolean);
277
+ }
278
+ function createOriginMatcher(patterns) {
279
+ const patternList = normalizeOriginPatterns(patterns);
238
280
  return (requestOrigin) => {
281
+ if (!requestOrigin) return null;
239
282
  for (const pattern of patternList) {
240
283
  if (matchOriginPattern(requestOrigin, pattern)) {
241
284
  return requestOrigin;
@@ -244,6 +287,12 @@ function createOriginMatcher(patterns) {
244
287
  return null;
245
288
  };
246
289
  }
290
+ function hasWildcardPattern(patterns) {
291
+ const list = Array.isArray(patterns) ? patterns : [patterns];
292
+ return list.some((p) => p.includes("*"));
293
+ }
294
+
295
+ // src/hono-plugin.ts
247
296
  var HonoServerPlugin = class {
248
297
  constructor(options = {}) {
249
298
  __publicField(this, "name", "com.objectstack.server.hono");
@@ -279,23 +328,27 @@ var HonoServerPlugin = class {
279
328
  const credentials = corsOpts.credentials ?? process.env.CORS_CREDENTIALS !== "false";
280
329
  const maxAge = corsOpts.maxAge ?? (process.env.CORS_MAX_AGE ? parseInt(process.env.CORS_MAX_AGE, 10) : 86400);
281
330
  let origin;
282
- const hasWildcard = (patterns) => {
283
- const list = Array.isArray(patterns) ? patterns : [patterns];
284
- return list.some((p) => p.includes("*"));
285
- };
286
331
  if (configuredOrigin === "*" && credentials) {
287
332
  origin = (requestOrigin) => requestOrigin || "*";
288
- } else if (hasWildcard(configuredOrigin)) {
333
+ } else if (hasWildcardPattern(configuredOrigin)) {
289
334
  origin = createOriginMatcher(configuredOrigin);
290
335
  } else {
291
- origin = configuredOrigin;
336
+ const matcher = createOriginMatcher(configuredOrigin);
337
+ origin = (requestOrigin) => matcher(requestOrigin);
292
338
  }
293
339
  const rawApp = this.server.getRawApp();
340
+ const defaultAllowHeaders = ["Content-Type", "Authorization", "X-Requested-With", "X-Tenant-ID", "X-Project-Id"];
341
+ const defaultExposeHeaders = ["set-auth-token"];
342
+ const allowHeaders = corsOpts.allowHeaders ?? defaultAllowHeaders;
343
+ const exposeHeaders = Array.from(/* @__PURE__ */ new Set([
344
+ ...defaultExposeHeaders,
345
+ ...corsOpts.exposeHeaders ?? []
346
+ ]));
294
347
  rawApp.use("*", cors({
295
348
  origin,
296
349
  allowMethods: corsOpts.methods || ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
297
- allowHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
298
- exposeHeaders: [],
350
+ allowHeaders,
351
+ exposeHeaders,
299
352
  credentials,
300
353
  maxAge
301
354
  }));
@@ -396,10 +449,16 @@ var HonoServerPlugin = class {
396
449
  });
397
450
  }
398
451
  }
399
- ctx.hook("kernel:ready", async () => {
400
- if (this.options.registerStandardEndpoints) {
452
+ const rawAppForNotFound = this.server.getRawApp();
453
+ if (typeof rawAppForNotFound.notFound === "function") {
454
+ rawAppForNotFound.notFound((c) => c.json({ error: "Not found" }, 404));
455
+ }
456
+ if (this.options.registerStandardEndpoints) {
457
+ ctx.hook("kernel:ready", async () => {
401
458
  this.registerDiscoveryAndCrudEndpoints(ctx);
402
- }
459
+ });
460
+ }
461
+ ctx.hook("kernel:listening", async () => {
403
462
  const port = this.options.port ?? 3e3;
404
463
  ctx.logger.debug("Starting HTTP server", { port });
405
464
  await this.server.listen(port);
@@ -452,33 +511,228 @@ var HonoServerPlugin = class {
452
511
  rawApp.get(`${prefix}/discovery`, (c) => c.json({ data: discovery }));
453
512
  ctx.logger.info("Registered discovery endpoints", { prefix });
454
513
  const getObjectQL = () => ctx.getService("objectql");
514
+ const resolveCtx = async (c) => {
515
+ try {
516
+ const authService = ctx.getService("auth");
517
+ if (!authService) return void 0;
518
+ let api = authService.api;
519
+ if (!api && typeof authService.getApi === "function") {
520
+ api = await authService.getApi();
521
+ }
522
+ if (!api?.getSession) return void 0;
523
+ const session = await api.getSession({ headers: c.req.raw.headers });
524
+ if (!session?.user?.id) return void 0;
525
+ const userId = session.user.id;
526
+ const tenantId = session.session?.activeOrganizationId ?? void 0;
527
+ const permissions = [];
528
+ const roles = [];
529
+ try {
530
+ const ql = getObjectQL();
531
+ const sysCtx = { context: { isSystem: true } };
532
+ const memberRows = await ql?.find?.(
533
+ "sys_member",
534
+ {
535
+ where: tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId },
536
+ limit: 50,
537
+ ...sysCtx
538
+ }
539
+ ).catch(() => []);
540
+ for (const m of memberRows ?? []) {
541
+ if (typeof m.role === "string") {
542
+ for (const r of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
543
+ if (!roles.includes(r)) roles.push(r);
544
+ }
545
+ }
546
+ }
547
+ const upsRows = await ql?.find?.(
548
+ "sys_user_permission_set",
549
+ { where: { user_id: userId }, limit: 100, ...sysCtx }
550
+ ).catch(() => []);
551
+ const psIds = /* @__PURE__ */ new Set();
552
+ for (const r of upsRows ?? []) {
553
+ const orgScope = r.organization_id ?? null;
554
+ if (!orgScope || tenantId && orgScope === tenantId) {
555
+ const pid = r.permission_set_id ?? r.permissionSetId;
556
+ if (pid) psIds.add(pid);
557
+ }
558
+ }
559
+ if (psIds.size > 0) {
560
+ const psRows = await ql?.find?.(
561
+ "sys_permission_set",
562
+ { where: { id: { $in: Array.from(psIds) } }, limit: 500, ...sysCtx }
563
+ ).catch(() => []);
564
+ for (const ps of psRows ?? []) {
565
+ if (ps.name && !permissions.includes(ps.name)) permissions.push(ps.name);
566
+ }
567
+ }
568
+ } catch {
569
+ }
570
+ return {
571
+ userId,
572
+ tenantId,
573
+ roles,
574
+ permissions,
575
+ isSystem: false
576
+ };
577
+ } catch {
578
+ return void 0;
579
+ }
580
+ };
455
581
  rawApp.post(`${prefix}/data/:object`, async (c) => {
456
582
  const ql = getObjectQL();
457
583
  if (!ql) return c.json({ error: "Data service not available" }, 503);
458
584
  const object = c.req.param("object");
459
585
  const data = await c.req.json().catch(() => ({}));
460
- const res = await ql.insert(object, data);
461
- const record = { ...data, ...res };
462
- return c.json({ object, id: record.id, record });
586
+ const execCtx = await resolveCtx(c);
587
+ try {
588
+ const res = await ql.insert(object, data, { context: execCtx });
589
+ const record = { ...data, ...res };
590
+ return c.json({ object, id: record.id, record });
591
+ } catch (err) {
592
+ if (err?.code === "PERMISSION_DENIED" || err?.name === "PermissionDeniedError") {
593
+ return c.json({ error: err.message ?? "Forbidden" }, 403);
594
+ }
595
+ throw err;
596
+ }
463
597
  });
464
598
  rawApp.get(`${prefix}/data/:object/:id`, async (c) => {
465
599
  const ql = getObjectQL();
466
600
  if (!ql) return c.json({ error: "Data service not available" }, 503);
467
601
  const object = c.req.param("object");
468
602
  const id = c.req.param("id");
469
- let all = await ql.find(object);
470
- if (!all) all = [];
471
- const match = all.find((i) => i.id === id);
472
- return match ? c.json({ object, id, record: match }) : c.json({ error: "Not found" }, 404);
603
+ const execCtx = await resolveCtx(c);
604
+ try {
605
+ let all = await ql.find(object, { context: execCtx });
606
+ if (!all) all = [];
607
+ const match = all.find((i) => i.id === id);
608
+ return match ? c.json({ object, id, record: match }) : c.json({ error: "Not found" }, 404);
609
+ } catch (err) {
610
+ if (err?.code === "PERMISSION_DENIED" || err?.name === "PermissionDeniedError") {
611
+ return c.json({ error: err.message ?? "Forbidden" }, 403);
612
+ }
613
+ throw err;
614
+ }
473
615
  });
474
616
  rawApp.get(`${prefix}/data/:object`, async (c) => {
475
617
  const ql = getObjectQL();
476
618
  if (!ql) return c.json({ error: "Data service not available" }, 503);
477
619
  const object = c.req.param("object");
478
- let all = await ql.find(object);
479
- if (!Array.isArray(all) && all && all.value) all = all.value;
480
- if (!all) all = [];
481
- return c.json({ object, records: all, total: all.length });
620
+ const execCtx = await resolveCtx(c);
621
+ try {
622
+ let all = await ql.find(object, { context: execCtx });
623
+ if (!Array.isArray(all) && all && all.value) all = all.value;
624
+ if (!all) all = [];
625
+ return c.json({ object, records: all, total: all.length });
626
+ } catch (err) {
627
+ if (err?.code === "PERMISSION_DENIED" || err?.name === "PermissionDeniedError") {
628
+ return c.json({ error: err.message ?? "Forbidden" }, 403);
629
+ }
630
+ throw err;
631
+ }
632
+ });
633
+ rawApp.get(`${prefix}/auth/me/permissions`, async (c) => {
634
+ const execCtx = await resolveCtx(c);
635
+ if (!execCtx?.userId) {
636
+ return c.json({ authenticated: false });
637
+ }
638
+ try {
639
+ const metadata = ctx.getService("metadata");
640
+ const evaluator = ctx.getService("security.permissions");
641
+ const bootstrap = (() => {
642
+ try {
643
+ return ctx.getService("security.bootstrapPermissionSets") ?? [];
644
+ } catch {
645
+ return [];
646
+ }
647
+ })();
648
+ const fallbackName = (() => {
649
+ try {
650
+ return ctx.getService("security.fallbackPermissionSet") ?? "member_default";
651
+ } catch {
652
+ return "member_default";
653
+ }
654
+ })();
655
+ const ql = (() => {
656
+ try {
657
+ return ctx.getService("objectql");
658
+ } catch {
659
+ return null;
660
+ }
661
+ })();
662
+ const dbLoader = ql ? async (names) => {
663
+ let rows;
664
+ try {
665
+ rows = await ql.find(
666
+ "sys_permission_set",
667
+ { where: { name: { $in: names } }, limit: names.length },
668
+ { context: { isSystem: true } }
669
+ );
670
+ } catch {
671
+ rows = [];
672
+ }
673
+ const list = Array.isArray(rows) ? rows : rows?.records ?? [];
674
+ return list.map((r) => ({
675
+ name: r.name,
676
+ label: r.label,
677
+ objects: typeof r.object_permissions === "string" ? JSON.parse(r.object_permissions || "{}") : r.object_permissions ?? {},
678
+ fields: typeof r.field_permissions === "string" ? JSON.parse(r.field_permissions || "{}") : r.field_permissions ?? {}
679
+ }));
680
+ } : void 0;
681
+ if (!evaluator || !metadata) {
682
+ return c.json({
683
+ authenticated: true,
684
+ userId: execCtx.userId,
685
+ tenantId: execCtx.tenantId ?? null,
686
+ roles: execCtx.roles ?? [],
687
+ permissionSets: execCtx.permissions ?? [],
688
+ objects: {},
689
+ fields: {}
690
+ });
691
+ }
692
+ const requested = [
693
+ ...execCtx.roles ?? [],
694
+ ...execCtx.permissions ?? []
695
+ ];
696
+ let resolved = await evaluator.resolvePermissionSets(requested, metadata, bootstrap, dbLoader).catch(() => []);
697
+ if (resolved.length === 0 && fallbackName) {
698
+ resolved = await evaluator.resolvePermissionSets([fallbackName], metadata, bootstrap, dbLoader).catch(() => []);
699
+ }
700
+ const objects = {};
701
+ const fields = {};
702
+ for (const ps of resolved) {
703
+ if (ps?.objects) {
704
+ for (const [obj, perm] of Object.entries(ps.objects)) {
705
+ const acc = objects[obj] ?? {};
706
+ for (const [k, v] of Object.entries(perm)) {
707
+ if (v === true) acc[k] = true;
708
+ else if (acc[k] === void 0) acc[k] = v;
709
+ }
710
+ objects[obj] = acc;
711
+ }
712
+ }
713
+ if (ps?.fields) {
714
+ for (const [key, perm] of Object.entries(ps.fields)) {
715
+ const acc = fields[key] ?? { readable: false, editable: false };
716
+ const p = perm;
717
+ if (p.readable) acc.readable = true;
718
+ if (p.editable) acc.editable = true;
719
+ fields[key] = acc;
720
+ }
721
+ }
722
+ }
723
+ return c.json({
724
+ authenticated: true,
725
+ userId: execCtx.userId,
726
+ tenantId: execCtx.tenantId ?? null,
727
+ roles: execCtx.roles ?? [],
728
+ permissionSets: resolved.map((p) => p?.name).filter(Boolean),
729
+ objects,
730
+ fields
731
+ });
732
+ } catch (err) {
733
+ ctx.logger.warn("[hono] /auth/me/permissions failed", { err: err?.message });
734
+ return c.json({ authenticated: true, userId: execCtx.userId, objects: {}, fields: {} });
735
+ }
482
736
  });
483
737
  ctx.logger.debug("Registered standard CRUD data endpoints", { prefix });
484
738
  }
@@ -499,6 +753,11 @@ __publicField(HonoServerPlugin, "DISCOVERY_ENDPOINT_PRIORITY", 900);
499
753
  __reExport(index_exports, adapter_exports);
500
754
  export {
501
755
  HonoHttpServer,
502
- HonoServerPlugin
756
+ HonoServerPlugin,
757
+ createOriginMatcher,
758
+ hasWildcardPattern,
759
+ isLocalhostOrigin,
760
+ matchOriginPattern,
761
+ normalizeOriginPatterns
503
762
  };
504
763
  //# sourceMappingURL=index.mjs.map