@airoom/nextmin-node 0.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.
Files changed (45) hide show
  1. package/LICENSE +49 -0
  2. package/README.md +178 -0
  3. package/dist/api/apiRouter.d.ts +65 -0
  4. package/dist/api/apiRouter.js +1548 -0
  5. package/dist/database/DatabaseAdapter.d.ts +14 -0
  6. package/dist/database/DatabaseAdapter.js +2 -0
  7. package/dist/database/InMemoryAdapter.d.ts +15 -0
  8. package/dist/database/InMemoryAdapter.js +71 -0
  9. package/dist/database/MongoAdapter.d.ts +52 -0
  10. package/dist/database/MongoAdapter.js +409 -0
  11. package/dist/files/FileStorageAdapter.d.ts +35 -0
  12. package/dist/files/FileStorageAdapter.js +2 -0
  13. package/dist/files/S3FileStorageAdapter.d.ts +30 -0
  14. package/dist/files/S3FileStorageAdapter.js +84 -0
  15. package/dist/files/filename.d.ts +5 -0
  16. package/dist/files/filename.js +40 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.js +87 -0
  19. package/dist/models/BaseModel.d.ts +44 -0
  20. package/dist/models/BaseModel.js +31 -0
  21. package/dist/policy/authorize.d.ts +25 -0
  22. package/dist/policy/authorize.js +305 -0
  23. package/dist/policy/conditions.d.ts +14 -0
  24. package/dist/policy/conditions.js +30 -0
  25. package/dist/policy/types.d.ts +53 -0
  26. package/dist/policy/types.js +2 -0
  27. package/dist/policy/utils.d.ts +9 -0
  28. package/dist/policy/utils.js +118 -0
  29. package/dist/schemas/Roles.json +64 -0
  30. package/dist/schemas/Settings.json +62 -0
  31. package/dist/schemas/Users.json +123 -0
  32. package/dist/services/SchemaService.d.ts +10 -0
  33. package/dist/services/SchemaService.js +46 -0
  34. package/dist/utils/DefaultDataInitializer.d.ts +21 -0
  35. package/dist/utils/DefaultDataInitializer.js +269 -0
  36. package/dist/utils/Logger.d.ts +12 -0
  37. package/dist/utils/Logger.js +79 -0
  38. package/dist/utils/SchemaLoader.d.ts +51 -0
  39. package/dist/utils/SchemaLoader.js +323 -0
  40. package/dist/utils/apiKey.d.ts +5 -0
  41. package/dist/utils/apiKey.js +14 -0
  42. package/dist/utils/fieldCodecs.d.ts +13 -0
  43. package/dist/utils/fieldCodecs.js +133 -0
  44. package/package.json +45 -0
  45. package/tsconfig.json +8 -0
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildConditionFilter = buildConditionFilter;
4
+ exports.checkRecordCondition = checkRecordCondition;
5
+ function buildConditionFilter(condition, opts) {
6
+ const by = opts.by || (condition === 'owner' ? 'createdBy' : condition === 'sameTenant' ? 'tenantId' : undefined);
7
+ if (!by)
8
+ return {};
9
+ if (condition === 'owner') {
10
+ return { [by]: opts.context.userId || null };
11
+ }
12
+ if (condition === 'sameTenant') {
13
+ return { [by]: opts.context.tenantId || null };
14
+ }
15
+ return {};
16
+ }
17
+ function checkRecordCondition(condition, record, opts) {
18
+ const by = opts.by || (condition === 'owner' ? 'createdBy' : condition === 'sameTenant' ? 'tenantId' : undefined);
19
+ if (!by)
20
+ return true;
21
+ if (!record || typeof record !== 'object')
22
+ return false;
23
+ if (condition === 'owner') {
24
+ return String(record[by] ?? '') === String(opts.context.userId ?? '');
25
+ }
26
+ if (condition === 'sameTenant') {
27
+ return String(record[by] ?? '') === String(opts.context.tenantId ?? '');
28
+ }
29
+ return true;
30
+ }
@@ -0,0 +1,53 @@
1
+ export type Action = 'create' | 'read' | 'update' | 'delete';
2
+ export type Rule = boolean | 'owner';
3
+ export type AccessMatrix = {
4
+ public?: Partial<Record<Action, Rule>>;
5
+ authenticated?: Partial<Record<Action, Rule>>;
6
+ roles?: Record<string, Partial<Record<Action, Rule>>>;
7
+ conditions?: Record<string, {
8
+ by: string;
9
+ }>;
10
+ filters?: Record<string, any>;
11
+ readMask?: string[];
12
+ writeDeny?: string[];
13
+ restrictions?: Record<string, any>;
14
+ createDefaults?: Record<string, Record<string, any>>;
15
+ };
16
+ export type SchemaPolicy = {
17
+ allowedMethods?: Partial<Record<Action, boolean>>;
18
+ access?: AccessMatrix;
19
+ modelName?: string;
20
+ };
21
+ export type Context = {
22
+ isAuthenticated: boolean;
23
+ role?: string | null;
24
+ userId?: string | null;
25
+ tenantId?: string | null;
26
+ isSuperadmin?: boolean;
27
+ apiKeyOk?: boolean;
28
+ };
29
+ export type Decision = {
30
+ allow: boolean;
31
+ reason?: string;
32
+ queryFilter?: Record<string, any>;
33
+ readMask?: string[];
34
+ writeDeny?: string[];
35
+ createDefaults?: Record<string, any>;
36
+ restrictions?: Record<string, any>;
37
+ };
38
+ export type DynamicGrant = {
39
+ id: string;
40
+ subject: {
41
+ userId?: string;
42
+ role?: string;
43
+ };
44
+ resource: {
45
+ model: string;
46
+ recordId?: string;
47
+ query?: any;
48
+ };
49
+ actions: Action[];
50
+ conditions?: string[];
51
+ expiresAt?: string | Date | null;
52
+ metadata?: Record<string, any>;
53
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ export declare function applyReadMaskOne<T extends Record<string, any>>(doc: T, mask?: string[]): T;
2
+ export declare function applyReadMaskMany<T extends Record<string, any>>(docs: T[], mask?: string[]): T[];
3
+ export declare function stripWriteDeny(payload: any, deny?: string[]): any;
4
+ export declare function mergeCreateDefaults(payload: any, defaults?: Record<string, any>): any;
5
+ export declare function enforceRestrictions(payload: any, restrictions: Record<string, any> | undefined, context: {
6
+ role?: string;
7
+ isSuperadmin?: boolean;
8
+ }): void;
9
+ export declare function andFilter(a?: Record<string, any>, b?: Record<string, any>): Record<string, any>;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyReadMaskOne = applyReadMaskOne;
4
+ exports.applyReadMaskMany = applyReadMaskMany;
5
+ exports.stripWriteDeny = stripWriteDeny;
6
+ exports.mergeCreateDefaults = mergeCreateDefaults;
7
+ exports.enforceRestrictions = enforceRestrictions;
8
+ exports.andFilter = andFilter;
9
+ function deleteDeep(obj, path) {
10
+ let cur = obj;
11
+ for (let i = 0; i < path.length - 1; i++) {
12
+ const k = path[i];
13
+ if (!cur || typeof cur !== 'object')
14
+ return;
15
+ cur = cur[k];
16
+ }
17
+ const last = path[path.length - 1];
18
+ if (cur && typeof cur === 'object') {
19
+ delete cur[last];
20
+ }
21
+ }
22
+ function matchesWildcardPath(path, pattern) {
23
+ if (pattern.endsWith(".*")) {
24
+ const prefix = pattern.slice(0, -2);
25
+ return path === prefix || path.startsWith(prefix + ".");
26
+ }
27
+ return path === pattern || path === "-" + pattern || path === "+" + pattern;
28
+ }
29
+ function applyReadMaskOne(doc, mask) {
30
+ if (!mask || mask.length === 0)
31
+ return doc;
32
+ const clone = JSON.parse(JSON.stringify(doc));
33
+ const pathsToRemove = [];
34
+ for (const m of mask) {
35
+ const clean = m.startsWith('-') ? m.slice(1) : (m.startsWith('+') ? m.slice(1) : m);
36
+ pathsToRemove.push(clean);
37
+ }
38
+ function walk(current, pathPrefix) {
39
+ if (!current || typeof current !== 'object')
40
+ return;
41
+ for (const key of Object.keys(current)) {
42
+ const path = [...pathPrefix, key].join('.');
43
+ if (pathsToRemove.some(p => matchesWildcardPath(path, p))) {
44
+ delete current[key];
45
+ }
46
+ else {
47
+ walk(current[key], [...pathPrefix, key]);
48
+ }
49
+ }
50
+ }
51
+ walk(clone, []);
52
+ return clone;
53
+ }
54
+ function applyReadMaskMany(docs, mask) {
55
+ if (!mask || mask.length === 0)
56
+ return docs;
57
+ return docs.map(d => applyReadMaskOne(d, mask));
58
+ }
59
+ function stripWriteDeny(payload, deny) {
60
+ if (!payload || !deny || deny.length === 0)
61
+ return payload;
62
+ const clone = JSON.parse(JSON.stringify(payload));
63
+ for (const d of deny) {
64
+ const p = d.startsWith('-') ? d.slice(1) : d;
65
+ const parts = p.split('.');
66
+ deleteDeep(clone, parts);
67
+ }
68
+ return clone;
69
+ }
70
+ function mergeCreateDefaults(payload, defaults) {
71
+ if (!defaults)
72
+ return payload;
73
+ const clone = { ...payload };
74
+ for (const [k, v] of Object.entries(defaults)) {
75
+ if (clone[k] === undefined)
76
+ clone[k] = v;
77
+ }
78
+ return clone;
79
+ }
80
+ function enforceRestrictions(payload, restrictions, context) {
81
+ if (!restrictions)
82
+ return;
83
+ if (context.isSuperadmin)
84
+ return;
85
+ const role = context.role || '';
86
+ const key = `roles.${role}.cannotAssign`;
87
+ const rule = restrictions[key];
88
+ if (rule && typeof rule === 'object') {
89
+ for (const field of Object.keys(rule)) {
90
+ const forbiddenVals = rule[field] || [];
91
+ const val = payload[field];
92
+ if (val != null) {
93
+ if (Array.isArray(val)) {
94
+ const set = new Set(val.map(String));
95
+ for (const f of forbiddenVals.map(String)) {
96
+ if (set.has(f)) {
97
+ const err = new Error(`Forbidden to assign ${field}=${f}`);
98
+ err.status = 403;
99
+ throw err;
100
+ }
101
+ }
102
+ }
103
+ else if (forbiddenVals.map(String).includes(String(val))) {
104
+ const err = new Error(`Forbidden to assign ${field}=${val}`);
105
+ err.status = 403;
106
+ throw err;
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ function andFilter(a, b) {
113
+ if (!a)
114
+ return b || {};
115
+ if (!b)
116
+ return a;
117
+ return { $and: [a, b] };
118
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "modelName": "Roles",
3
+ "attributes": {
4
+ "name": { "type": "string", "required": true, "unique": true },
5
+ "description": { "type": "string" },
6
+ "type": {
7
+ "type": "string",
8
+ "enum": ["system", "default", "user"],
9
+ "default": "user",
10
+ "required": true
11
+ }
12
+ },
13
+
14
+ "allowedMethods": {
15
+ "create": true,
16
+ "read": true,
17
+ "update": true,
18
+ "delete": true
19
+ },
20
+
21
+ "access": {
22
+ "public": {
23
+ "create": true,
24
+ "read": false,
25
+ "update": false,
26
+ "delete": false
27
+ },
28
+ "authenticated": {
29
+ "create": false,
30
+ "read": "owner",
31
+ "update": false,
32
+ "delete": false
33
+ },
34
+ "roles": {
35
+ "admin": {
36
+ "create": true,
37
+ "read": true,
38
+ "update": true,
39
+ "delete": false
40
+ },
41
+ "superadmin": {
42
+ "create": true,
43
+ "read": true,
44
+ "update": true,
45
+ "delete": true
46
+ }
47
+ },
48
+
49
+ "conditions": { "owner": { "by": "id" } },
50
+ "readMask": ["-password"],
51
+ "writeDeny": ["password"],
52
+ "bypassPrivacy": { "roles": ["admin", "superadmin"] },
53
+
54
+ "createDefaults": {
55
+ "public": { "role": "user", "status": "pending" },
56
+ "roles.admin": { "status": "active" },
57
+ "roles.superadmin": { "status": "active" }
58
+ },
59
+
60
+ "restrictions": {
61
+ "roles.admin.cannotAssign": { "role": ["superadmin"] }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "modelName": "Settings",
3
+ "attributes": {
4
+ "apiKey": {
5
+ "type": "string",
6
+ "required": true,
7
+ "readOnly": true
8
+ },
9
+ "siteName": {
10
+ "type": "string",
11
+ "required": true
12
+ },
13
+ "googleMapsKey": {
14
+ "type": "string"
15
+ },
16
+ "siteLogo": [
17
+ {
18
+ "type": "string",
19
+ "fileTypes": "images/*",
20
+ "maxFilesCount": 1,
21
+ "maxFileSize": 2097152,
22
+ "required": true
23
+ }
24
+ ]
25
+ },
26
+
27
+ "allowedMethods": {
28
+ "read": true,
29
+ "update": true,
30
+ "create": false,
31
+ "delete": false
32
+ },
33
+
34
+ "access": {
35
+ "public": {
36
+ "read": true,
37
+ "update": false,
38
+ "create": false,
39
+ "delete": false
40
+ },
41
+ "authenticated": {
42
+ "read": true,
43
+ "update": false,
44
+ "create": false,
45
+ "delete": false
46
+ },
47
+ "roles": {
48
+ "admin": {
49
+ "read": true,
50
+ "update": true,
51
+ "create": false,
52
+ "delete": false
53
+ },
54
+ "superadmin": {
55
+ "read": true,
56
+ "update": true,
57
+ "create": false,
58
+ "delete": false
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,123 @@
1
+ {
2
+ "modelName": "Users",
3
+ "attributes": {
4
+ "username": {
5
+ "type": "string",
6
+ "required": true,
7
+ "unique": true
8
+ },
9
+ "email": {
10
+ "type": "string",
11
+ "required": true,
12
+ "unique": true
13
+ },
14
+ "firstName": {
15
+ "type": "string",
16
+ "required": true
17
+ },
18
+ "lastName": {
19
+ "type": "string",
20
+ "required": true
21
+ },
22
+ "password": {
23
+ "type": "string",
24
+ "required": true,
25
+ "private": true,
26
+ "writeOnly": true
27
+ },
28
+
29
+ "profilePicture": [
30
+ {
31
+ "type": "string",
32
+ "fileTypes": "images/*",
33
+ "multiple": true,
34
+ "maxFilesCount": 1,
35
+ "maxFileSize": 1000000,
36
+ "required": true
37
+ }
38
+ ],
39
+
40
+ "role": [
41
+ {
42
+ "type": "ObjectId",
43
+ "ref": "Roles",
44
+ "default": "user",
45
+ "show": "name",
46
+ "required": true
47
+ }
48
+ ],
49
+
50
+ "status": {
51
+ "type": "string",
52
+ "enum": ["pending", "active", "suspended"],
53
+ "default": "pending",
54
+ "required": true
55
+ },
56
+ "type": {
57
+ "type": "string",
58
+ "enum": ["system", "default", "user"],
59
+ "default": "user",
60
+ "required": true
61
+ }
62
+ },
63
+
64
+ "allowedMethods": {
65
+ "create": true,
66
+ "read": true,
67
+ "update": true,
68
+ "delete": false
69
+ },
70
+
71
+ "access": {
72
+ "public": {
73
+ "create": true,
74
+ "read": false,
75
+ "update": false,
76
+ "delete": false
77
+ },
78
+ "authenticated": {
79
+ "create": false,
80
+ "read": "owner",
81
+ "update": false,
82
+ "delete": false
83
+ },
84
+
85
+ "roles": {
86
+ "admin": {
87
+ "create": true,
88
+ "read": true,
89
+ "update": true,
90
+ "delete": false
91
+ },
92
+ "superadmin": {
93
+ "create": true,
94
+ "read": true,
95
+ "update": true,
96
+ "delete": true
97
+ }
98
+ },
99
+
100
+ "queryFilter": {
101
+ "authenticated": { "id": { "ne": "$CTX.userId" } },
102
+ "roles": {
103
+ "admin": { "id": { "ne": "$CTX.userId" } },
104
+ "superadmin": {}
105
+ }
106
+ },
107
+
108
+ "conditions": { "owner": { "by": "id" } },
109
+ "readMask": ["-password"],
110
+ "writeDeny": ["password"],
111
+ "bypassPrivacy": { "roles": ["admin", "superadmin"] },
112
+
113
+ "createDefaults": {
114
+ "public": { "role": "user", "status": "pending" },
115
+ "roles.admin": { "status": "active" },
116
+ "roles.superadmin": { "status": "active" }
117
+ },
118
+
119
+ "restrictions": {
120
+ "roles.admin.cannotAssign": { "role": ["superadmin"] }
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,10 @@
1
+ import type { Server as HTTPServer } from 'http';
2
+ export declare const SOCKET_PATH = "/__nextmin__/schema";
3
+ export declare const NAMESPACE = "/schema";
4
+ export interface SchemaServiceOptions {
5
+ /** Return the trusted API key used to authorize socket clients */
6
+ getApiKey?: () => string | undefined;
7
+ /** Optional CORS origin override (defaults to "*") */
8
+ corsOrigin?: string | RegExp | (string | RegExp)[];
9
+ }
10
+ export declare function startSchemaService(server: HTTPServer, opts?: SchemaServiceOptions): void;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NAMESPACE = exports.SOCKET_PATH = void 0;
7
+ exports.startSchemaService = startSchemaService;
8
+ const socket_io_1 = require("socket.io");
9
+ const SchemaLoader_1 = require("../utils/SchemaLoader");
10
+ const Logger_1 = __importDefault(require("../utils/Logger"));
11
+ exports.SOCKET_PATH = '/__nextmin__/schema';
12
+ exports.NAMESPACE = '/schema';
13
+ let started = false;
14
+ let io = null;
15
+ let nsp = null;
16
+ function startSchemaService(server, opts = {}) {
17
+ if (started)
18
+ return;
19
+ started = true;
20
+ Logger_1.default.info('SchemaService:', `Starting schema service path:${exports.SOCKET_PATH} namespace:${exports.NAMESPACE}`);
21
+ io = new socket_io_1.Server(server, {
22
+ path: exports.SOCKET_PATH,
23
+ cors: { origin: opts.corsOrigin ?? '*' },
24
+ });
25
+ nsp = io.of(exports.NAMESPACE);
26
+ // --- auth gate ---
27
+ nsp.use((socket, next) => {
28
+ const provided = socket.handshake.auth?.apiKey;
29
+ const trusted = opts.getApiKey?.();
30
+ if (trusted && provided === trusted)
31
+ return next();
32
+ next(new Error('unauthorized'));
33
+ });
34
+ const loader = SchemaLoader_1.SchemaLoader.getInstance?.() ?? new SchemaLoader_1.SchemaLoader();
35
+ nsp.on('connection', (socket) => {
36
+ Logger_1.default.info('SchemaService:', socket.id);
37
+ // Send full snapshot on connect
38
+ socket.emit('schemasData', loader.getPublicSchemaList());
39
+ });
40
+ // Broadcast updates on hot-reload / schema changes
41
+ if (typeof loader.on === 'function') {
42
+ loader.on('schemasChanged', (schemas) => {
43
+ nsp?.emit('schemasUpdated', schemas);
44
+ });
45
+ }
46
+ }
@@ -0,0 +1,21 @@
1
+ import { BaseModel } from '../models/BaseModel';
2
+ import { DatabaseAdapter } from '../database/DatabaseAdapter';
3
+ type Models = Record<string, BaseModel>;
4
+ export declare class DefaultDataInitializer {
5
+ private dbAdapter;
6
+ private models;
7
+ private apiKey;
8
+ private jwtSecret;
9
+ constructor(dbAdapter: DatabaseAdapter, models: Models, opts?: {
10
+ jwtSecret?: string;
11
+ });
12
+ getApiKey(): string | null;
13
+ initialize(): Promise<void>;
14
+ private getModel;
15
+ private inferUserRoleStorage;
16
+ private ensureHash;
17
+ private ensureRoles;
18
+ private ensureSuperAdminUser;
19
+ private ensureSettingsDocument;
20
+ }
21
+ export {};