@anteros/core 0.0.1 → 0.0.2

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/lib/files.ts CHANGED
@@ -352,12 +352,43 @@ export async function handleDelete(
352
352
  tenant_id: string,
353
353
  collection: string,
354
354
  fileId: string,
355
- filename: string,
356
355
  ): Promise<void> {
357
356
  const storage = getStorageForCollection(collection, tenant_id);
358
357
  const col = getFileCollection(collection, tenant_id);
358
+
359
+ // Look up the document to get the stored filename
360
+ let filename: string | undefined;
361
+ if (col?.trackMetaData !== false) {
362
+ try {
363
+ const rest = new useRest({ tenant_id, internal: true, useHook: false, useCustomApi: false });
364
+ const doc = await rest.findOne<any>(collection, fileId);
365
+ filename = doc?._file?.filename;
366
+ } catch {}
367
+ }
368
+
369
+ // Delete the physical file from storage
359
370
  const subpath = col?.storage?.path || undefined;
360
- await storage.delete(tenant_id, collection, fileId, filename, subpath);
371
+ if (filename) {
372
+ await storage.delete(tenant_id, collection, fileId, filename, subpath);
373
+ } else {
374
+ // Fallback: try common extensions
375
+ for (const ext of ['.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif', '.svg', '.pdf', '.mp4', '.webm', '.mp3', '.wav', '.json', '.csv', '.zip', '']) {
376
+ try {
377
+ await storage.delete(tenant_id, collection, fileId, fileId + ext, subpath);
378
+ break;
379
+ } catch {}
380
+ }
381
+ }
382
+
383
+ // Delete the metadata document from MongoDB
384
+ if (col?.trackMetaData !== false) {
385
+ try {
386
+ const rest = new useRest({ tenant_id, internal: true, useHook: false, useCustomApi: false });
387
+ await rest.deleteOne(collection, fileId);
388
+ } catch (err: any) {
389
+ console.error('Failed to delete file metadata:', err?.message || err);
390
+ }
391
+ }
361
392
  }
362
393
 
363
394
  // ─── Replication ──────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "name": "@anteros/core",
3
3
  "module": "index.ts",
4
- "version": "0.0.1",
4
+ "version": "0.0.2",
5
5
  "type": "module",
6
6
  "description": "Anteros Core",
7
7
  "private": false,
8
- "bin": {
9
- "datiox": "bin/d.ts"
10
- },
11
8
  "engines": {
12
9
  "bun": ">=1.3.0"
13
10
  },
@@ -27,8 +24,7 @@
27
24
  "ioredis": "^5.11.1",
28
25
  "joi": "^18.2.1",
29
26
  "jose": "^6.2.3",
30
- "mongodb": "^7.3.0",
31
- "os": "^0.1.2",
27
+ "mongodb": "6.21.0",
32
28
  "socket.io": "^4.8.3",
33
29
  "ufo": "^1.6.4"
34
30
  }
package/server/api.ts CHANGED
@@ -42,6 +42,59 @@ 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
+ // Boolean `true` → allow without requiring a token
73
+ if (typeof rule === 'boolean') {
74
+ if (!rule) {
75
+ throw new AppError(`${operation} not allowed`, { status: 401, code: 'ACCESS_DENIED' });
76
+ }
77
+ return;
78
+ }
79
+
80
+ // Function → requires a valid token
81
+ const t = c.get('token');
82
+ const accessToken = { value: t?.value ?? null, decoded: t?.decoded ?? null, provided: t?.provided ?? false, expired: t?.expired ?? false };
83
+
84
+ if (accessToken.expired) {
85
+ throw new AppError('Token expired', { status: 401, code: 'TOKEN_EXPIRED' });
86
+ }
87
+ if (!accessToken.value) {
88
+ throw new AppError('Authentication required', { status: 401, code: 'AUTH_REQUIRED' });
89
+ }
90
+
91
+ const rest = new useRest({ internal: false, tenant_id });
92
+ const allowed = await (rule as Function)({ rest, error: fn.error, jwt: func.jwt, token: accessToken });
93
+ if (!allowed) {
94
+ throw new AppError(`${operation} not allowed`, { status: 401, code: 'ACCESS_DENIED' });
95
+ }
96
+ }
97
+
45
98
  function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
46
99
 
47
100
  // Crud API
@@ -567,29 +620,7 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
567
620
  if (!colUpload) {
568
621
  throw new AppError('File collection `' + collection + '` not found', { status: 400, code: 'FILE_COLLECTION_NOT_FOUND' });
569
622
  }
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
- }
623
+ await checkFileAccess(colUpload?.api?.access as any, 'upload', tenant_id, c);
593
624
 
594
625
  const contentType = c.req.header('Content-Type') || '';
595
626
  if (!contentType.includes('multipart/form-data')) {
@@ -646,29 +677,7 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
646
677
  if (!colServe) {
647
678
  throw new AppError('File collection `' + collection + '` not found', { status: 400, code: 'FILE_COLLECTION_NOT_FOUND' });
648
679
  }
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
- }
680
+ await checkFileAccess(colServe?.api?.access as any, 'read', tenant_id, c);
672
681
 
673
682
  const filename = basename(c.req.param('file') as string);
674
683
  if (!filename || filename.startsWith('.') || filename.includes('..') || filename.includes('/')) {
@@ -721,37 +730,14 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
721
730
  if (!colDelete) {
722
731
  throw new AppError('File collection `' + collection + '` not found', { status: 400, code: 'FILE_COLLECTION_NOT_FOUND' });
723
732
  }
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 };
733
+ await checkFileAccess(colDelete?.api?.access as any, 'delete', tenant_id, c);
727
734
 
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
- }
747
-
748
- const filename = basename(c.req.param('file') as string);
749
- if (!filename || filename.startsWith('.') || filename.includes('..') || filename.includes('/')) {
750
- throw new AppError('Invalid filename', { status: 400, code: 'INVALID_FILENAME' });
735
+ const fileId = c.req.param('file') as string;
736
+ if (!fileId || fileId.startsWith('.') || fileId.includes('..') || fileId.includes('/')) {
737
+ throw new AppError('Invalid file id', { status: 400, code: 'INVALID_FILE_ID' });
751
738
  }
752
- const fileId = filename.replace(/\.[^.]+$/, '');
753
739
 
754
- await handleDelete(tenant_id, collection, fileId, filename);
740
+ await handleDelete(tenant_id, collection, fileId);
755
741
 
756
742
  return c.json({ message: 'File deleted', ok: true });
757
743
  } catch (err: any) {
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 */
package/bin/d.ts DELETED
File without changes