@anteros/core 0.0.1-alpha.2 → 0.0.1-alpha.4

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/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import "./lib/buffer-polyfill";
1
2
  import { define } from "./lib/define";
2
3
  import { bootApp } from "./server/boot";
3
4
  import { useRest } from "./database/rest";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Polyfill for Bun's Buffer to accept numeric encodings.
3
+ *
4
+ * Node.js allows encoding to be passed as a number (e.g. 3 = 'latin1'),
5
+ * but Bun throws "Invalid encoding: 3". This patches Buffer.from,
6
+ * Buffer.alloc, and Buffer.allocUnsafe to convert numeric encodings
7
+ * to their string equivalents before calling the original methods.
8
+ *
9
+ * Required by: mongodb driver (bson), and any library that uses
10
+ * legacy numeric encoding values internally.
11
+ */
12
+ const ENCODING_MAP: Record<number, string> = {
13
+ 0: "utf8",
14
+ 1: "utf-8",
15
+ 2: "utf16le",
16
+ 3: "latin1",
17
+ 4: "latin1", // binary alias
18
+ 5: "base64",
19
+ 6: "base64url",
20
+ 7: "hex",
21
+ 8: "ascii",
22
+ 9: "ucs2",
23
+ 10: "ucs-2",
24
+ };
25
+
26
+ function normalizeEncoding(
27
+ encoding: string | number | undefined,
28
+ ): BufferEncoding | undefined {
29
+ if (encoding === undefined || typeof encoding === "string") {
30
+ return encoding as BufferEncoding | undefined;
31
+ }
32
+ const mapped = ENCODING_MAP[encoding];
33
+ if (mapped) return mapped as BufferEncoding;
34
+ return undefined;
35
+ }
36
+
37
+ // Patch Buffer.from
38
+ const _Buffer_from = Buffer.from.bind(Buffer) as typeof Buffer.from;
39
+ (Buffer as any).from = function (
40
+ ...args: any[]
41
+ ): Buffer {
42
+ if (args.length >= 2 && typeof args[1] === "number") {
43
+ args[1] = normalizeEncoding(args[1]);
44
+ }
45
+ return _Buffer_from(...args as [any, any?, any?]);
46
+ };
47
+
48
+ // Patch Buffer.alloc
49
+ const _Buffer_alloc = Buffer.alloc.bind(Buffer);
50
+ (Buffer as any).alloc = function (
51
+ ...args: any[]
52
+ ): Buffer {
53
+ if (args.length >= 2 && typeof args[1] === "number") {
54
+ args[1] = normalizeEncoding(args[1]);
55
+ }
56
+ return _Buffer_alloc(...args as [any, any?, any?]);
57
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anteros/core",
3
3
  "module": "index.ts",
4
- "version": "0.0.1-alpha.2",
4
+ "version": "0.0.1-alpha.4",
5
5
  "type": "module",
6
6
  "description": "Anteros Core",
7
7
  "private": false,
package/server/api.ts CHANGED
@@ -42,6 +42,55 @@ const ActionsValues = [
42
42
 
43
43
 
44
44
 
45
+ // ─── File access helper ────────────────────────────────────────────────────
46
+
47
+ async function checkFileAccess(
48
+ access: { [key: string]: boolean | ((ctx: any) => boolean | Promise<boolean>) | undefined } | undefined,
49
+ operation: string,
50
+ tenant_id: string,
51
+ c: any,
52
+ ): Promise<void> {
53
+ // No access config at all → deny (secure by default)
54
+ if (!access) {
55
+ throw new AppError('Access denied', { status: 401, code: 'ACCESS_DENIED' });
56
+ }
57
+
58
+ const hasWildcard = access['*'] !== undefined;
59
+ const hasSpecific = access[operation] !== undefined;
60
+
61
+ // No matching rule at all → deny
62
+ if (!hasWildcard && !hasSpecific) {
63
+ throw new AppError('Access denied', { status: 401, code: 'ACCESS_DENIED' });
64
+ }
65
+
66
+ // Specific rule takes precedence over wildcard
67
+ const rule = hasSpecific ? access[operation] : access['*'];
68
+ if (rule === undefined) {
69
+ throw new AppError('Access denied', { status: 401, code: 'ACCESS_DENIED' });
70
+ }
71
+
72
+ const t = c.get('token');
73
+ const accessToken = { value: t?.value ?? null, decoded: t?.decoded ?? null, provided: t?.provided ?? false, expired: t?.expired ?? false };
74
+
75
+ if (accessToken.expired) {
76
+ throw new AppError('Token expired', { status: 401, code: 'TOKEN_EXPIRED' });
77
+ }
78
+ if (!accessToken.value) {
79
+ throw new AppError('Authentication required', { status: 401, code: 'AUTH_REQUIRED' });
80
+ }
81
+
82
+ let allowed: boolean;
83
+ if (typeof rule === 'function') {
84
+ const rest = new useRest({ internal: false, tenant_id });
85
+ allowed = await rule({ rest, error: fn.error, jwt: func.jwt, token: accessToken });
86
+ } else {
87
+ allowed = rule;
88
+ }
89
+ if (!allowed) {
90
+ throw new AppError(`${operation} not allowed`, { status: 401, code: 'ACCESS_DENIED' });
91
+ }
92
+ }
93
+
45
94
  function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
46
95
 
47
96
  // Crud API
@@ -567,29 +616,7 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
567
616
  if (!colUpload) {
568
617
  throw new AppError('File collection `' + collection + '` not found', { status: 400, code: 'FILE_COLLECTION_NOT_FOUND' });
569
618
  }
570
- if (colUpload?.api?.access?.upload) {
571
- const t = c.get('token');
572
- const accessToken = { value: t?.value ?? null, decoded: t?.decoded ?? null, provided: t?.provided ?? false, expired: t?.expired ?? false };
573
-
574
- if (accessToken.expired) {
575
- throw new AppError('Token expired', { status: 401, code: 'TOKEN_EXPIRED' });
576
- }
577
- if (!accessToken.value) {
578
- throw new AppError('Authentication required', { status: 401, code: 'AUTH_REQUIRED' });
579
- }
580
-
581
- const accessCheck = colUpload.api.access.upload;
582
- let allowed: boolean;
583
- if (typeof accessCheck === 'function') {
584
- const rest = new useRest({ internal: false, tenant_id });
585
- allowed = await accessCheck({ rest, error: fn.error, jwt: func.jwt, token: accessToken });
586
- } else {
587
- allowed = accessCheck;
588
- }
589
- if (!allowed) {
590
- throw new AppError('Upload not allowed', { status: 401, code: 'ACCESS_DENIED' });
591
- }
592
- }
619
+ await checkFileAccess(colUpload?.api?.access as any, 'upload', tenant_id, c);
593
620
 
594
621
  const contentType = c.req.header('Content-Type') || '';
595
622
  if (!contentType.includes('multipart/form-data')) {
@@ -646,29 +673,7 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
646
673
  if (!colServe) {
647
674
  throw new AppError('File collection `' + collection + '` not found', { status: 400, code: 'FILE_COLLECTION_NOT_FOUND' });
648
675
  }
649
- if (colServe?.api?.access?.read) {
650
- const t = c.get('token');
651
- const accessToken = { value: t?.value ?? null, decoded: t?.decoded ?? null, provided: t?.provided ?? false, expired: t?.expired ?? false };
652
-
653
- if (accessToken.expired) {
654
- throw new AppError('Token expired', { status: 401, code: 'TOKEN_EXPIRED' });
655
- }
656
- if (!accessToken.value) {
657
- throw new AppError('Authentication required', { status: 401, code: 'AUTH_REQUIRED' });
658
- }
659
-
660
- const accessCheck = colServe.api.access.read;
661
- let allowed: boolean;
662
- if (typeof accessCheck === 'function') {
663
- const rest = new useRest({ internal: false, tenant_id });
664
- allowed = await accessCheck({ rest, error: fn.error, jwt: func.jwt, token: accessToken });
665
- } else {
666
- allowed = accessCheck;
667
- }
668
- if (!allowed) {
669
- throw new AppError('Access denied', { status: 401, code: 'ACCESS_DENIED' });
670
- }
671
- }
676
+ await checkFileAccess(colServe?.api?.access as any, 'read', tenant_id, c);
672
677
 
673
678
  const filename = basename(c.req.param('file') as string);
674
679
  if (!filename || filename.startsWith('.') || filename.includes('..') || filename.includes('/')) {
@@ -721,29 +726,7 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
721
726
  if (!colDelete) {
722
727
  throw new AppError('File collection `' + collection + '` not found', { status: 400, code: 'FILE_COLLECTION_NOT_FOUND' });
723
728
  }
724
- if (colDelete?.api?.access?.delete) {
725
- const t = c.get('token');
726
- const accessToken = { value: t?.value ?? null, decoded: t?.decoded ?? null, provided: t?.provided ?? false, expired: t?.expired ?? false };
727
-
728
- if (accessToken.expired) {
729
- throw new AppError('Token expired', { status: 401, code: 'TOKEN_EXPIRED' });
730
- }
731
- if (!accessToken.value) {
732
- throw new AppError('Authentication required', { status: 401, code: 'AUTH_REQUIRED' });
733
- }
734
-
735
- const accessCheck = colDelete.api.access.delete;
736
- let allowed: boolean;
737
- if (typeof accessCheck === 'function') {
738
- const rest = new useRest({ internal: false, tenant_id });
739
- allowed = await accessCheck({ rest, error: fn.error, jwt: func.jwt, token: accessToken });
740
- } else {
741
- allowed = accessCheck;
742
- }
743
- if (!allowed) {
744
- throw new AppError('Access denied', { status: 401, code: 'ACCESS_DENIED' });
745
- }
746
- }
729
+ await checkFileAccess(colDelete?.api?.access as any, 'delete', tenant_id, c);
747
730
 
748
731
  const filename = basename(c.req.param('file') as string);
749
732
  if (!filename || filename.startsWith('.') || filename.includes('..') || filename.includes('/')) {
package/types/file.d.ts CHANGED
@@ -13,6 +13,8 @@ type FileAccessHandler = (ctx: {
13
13
  }) => boolean | Promise<boolean>;
14
14
 
15
15
  type FileApiAccess = {
16
+ /** Default access rule applied when no per-operation rule is set */
17
+ '*'?: boolean | FileAccessHandler;
16
18
  /** Access control for POST /upload/:tenant_id/:slug */
17
19
  upload?: boolean | FileAccessHandler;
18
20
  /** Access control for GET /files/:tenant_id/:slug/:file */