@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,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.S3FileStorageAdapter = void 0;
4
+ const client_s3_1 = require("@aws-sdk/client-s3");
5
+ // Encode per path segment, keep slashes
6
+ function encodeKeySegments(key) {
7
+ return key.split('/').map(encodeURIComponent).join('/');
8
+ }
9
+ class S3FileStorageAdapter {
10
+ constructor(opts) {
11
+ this.name = 's3';
12
+ this.bucket = opts.bucket;
13
+ this.regionStr = opts.region; // <- store plain region
14
+ this.endpoint = opts.endpoint?.replace(/\/+$/, '');
15
+ this.forcePathStyle = opts.forcePathStyle ?? true; // sane default for MinIO
16
+ this.defaultACL = opts.defaultACL;
17
+ this.publicBaseUrl = opts.publicBaseUrl;
18
+ this.s3 = new client_s3_1.S3Client({
19
+ region: this.regionStr,
20
+ endpoint: this.endpoint,
21
+ forcePathStyle: this.forcePathStyle,
22
+ credentials: opts.credentials,
23
+ });
24
+ }
25
+ async upload(input) {
26
+ if (!input.body)
27
+ throw new Error('S3 upload: "body" is required');
28
+ const key = input.key ?? this.randomKey();
29
+ // Build params without ACL by default
30
+ const params = {
31
+ Bucket: this.bucket,
32
+ Key: key,
33
+ Body: input.body,
34
+ ContentType: input.contentType,
35
+ Metadata: input.metadata,
36
+ CacheControl: input.contentType?.startsWith('image/')
37
+ ? 'public, max-age=31536000, immutable'
38
+ : 'public, max-age=86400',
39
+ };
40
+ // Only attach ACL for S3-compatible endpoints that require it (e.g., MinIO)
41
+ // i.e., when you explicitly configured an endpoint AND set defaultACL
42
+ if (this.endpoint && this.defaultACL) {
43
+ params.ACL = this.defaultACL;
44
+ }
45
+ await this.s3.send(new client_s3_1.PutObjectCommand(params));
46
+ const url = this.getPublicUrl(key);
47
+ return {
48
+ provider: 's3',
49
+ bucket: this.bucket,
50
+ key,
51
+ url,
52
+ contentType: input.contentType,
53
+ size: Buffer.isBuffer(input.body) ? input.body.length : undefined,
54
+ metadata: input.metadata,
55
+ };
56
+ }
57
+ async delete(key) {
58
+ await this.s3.send(new client_s3_1.DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
59
+ return { deleted: true };
60
+ }
61
+ getPublicUrl(key) {
62
+ const encKey = encodeKeySegments(key);
63
+ if (this.publicBaseUrl) {
64
+ return `${this.publicBaseUrl.replace(/\/+$/, '')}/${encKey}`;
65
+ }
66
+ if (this.endpoint) {
67
+ // MinIO / custom endpoint → path-style
68
+ return `${this.endpoint}/${this.bucket}/${encKey}`;
69
+ }
70
+ // AWS S3 standard
71
+ return `https://${this.bucket}.s3.${this.regionStr}.amazonaws.com/${encKey}`;
72
+ }
73
+ // ---- private helpers ----
74
+ randomKey() {
75
+ const ts = new Date();
76
+ const y = ts.getUTCFullYear();
77
+ const m = String(ts.getUTCMonth() + 1).padStart(2, '0');
78
+ const d = String(ts.getUTCDate()).padStart(2, '0');
79
+ const rand = Math.random().toString(36).slice(2, 10);
80
+ // You wanted keys like uploads/YYYY/MM/DD/uid.ext (ext is added by router)
81
+ return `uploads/${y}/${m}/${d}/${rand}`;
82
+ }
83
+ }
84
+ exports.S3FileStorageAdapter = S3FileStorageAdapter;
@@ -0,0 +1,5 @@
1
+ export declare function sanitizeFilename(name: string): string;
2
+ export declare function withTimestampPrefix(name: string): string;
3
+ export declare function joinKey(...parts: string[]): string;
4
+ export declare function ensureExt(name: string, fallbackExt?: string): string;
5
+ export declare function extFromMime(mime: string): string | undefined;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeFilename = sanitizeFilename;
4
+ exports.withTimestampPrefix = withTimestampPrefix;
5
+ exports.joinKey = joinKey;
6
+ exports.ensureExt = ensureExt;
7
+ exports.extFromMime = extFromMime;
8
+ function sanitizeFilename(name) {
9
+ const base = name.replace(/[/\\?%*:|"<>]/g, '-').replace(/\s+/g, '-');
10
+ return base.replace(/-+/g, '-').toLowerCase();
11
+ }
12
+ function withTimestampPrefix(name) {
13
+ const ts = Date.now();
14
+ return `${ts}-${name}`;
15
+ }
16
+ function joinKey(...parts) {
17
+ return parts
18
+ .filter(Boolean)
19
+ .map((p) => p.replace(/^\/+|\/+$/g, ''))
20
+ .join('/');
21
+ }
22
+ function ensureExt(name, fallbackExt) {
23
+ const hasExt = /\.[A-Za-z0-9]{1,8}$/.test(name);
24
+ if (hasExt || !fallbackExt)
25
+ return name;
26
+ return `${name}.${fallbackExt.replace(/^\./, '')}`;
27
+ }
28
+ function extFromMime(mime) {
29
+ const map = {
30
+ 'image/jpeg': 'jpg',
31
+ 'image/png': 'png',
32
+ 'image/webp': 'webp',
33
+ 'image/gif': 'gif',
34
+ 'application/pdf': 'pdf',
35
+ 'text/plain': 'txt',
36
+ 'text/csv': 'csv',
37
+ 'video/mp4': 'mp4',
38
+ };
39
+ return map[mime];
40
+ }
@@ -0,0 +1,13 @@
1
+ import { APIRouterOptions } from './api/apiRouter';
2
+ export { MongoAdapter } from './database/MongoAdapter';
3
+ export { generateApiKey } from './utils/apiKey';
4
+ export * from './files/FileStorageAdapter';
5
+ export * from './files/S3FileStorageAdapter';
6
+ export * from './files/filename';
7
+ /**
8
+ * Compose the public router:
9
+ * - JSON & urlencoded body parsers
10
+ * - a guarded "body required" check for JSON/urlencoded ONLY
11
+ * - mounts APIRouter (which includes /files using multer)
12
+ */
13
+ export declare function createNextMinRouter(options: APIRouterOptions): import("express-serve-static-core").Router;
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.generateApiKey = exports.MongoAdapter = void 0;
21
+ exports.createNextMinRouter = createNextMinRouter;
22
+ const express_1 = __importDefault(require("express"));
23
+ const body_parser_1 = __importDefault(require("body-parser"));
24
+ const apiRouter_1 = require("./api/apiRouter");
25
+ var MongoAdapter_1 = require("./database/MongoAdapter");
26
+ Object.defineProperty(exports, "MongoAdapter", { enumerable: true, get: function () { return MongoAdapter_1.MongoAdapter; } });
27
+ var apiKey_1 = require("./utils/apiKey");
28
+ Object.defineProperty(exports, "generateApiKey", { enumerable: true, get: function () { return apiKey_1.generateApiKey; } });
29
+ __exportStar(require("./files/FileStorageAdapter"), exports);
30
+ __exportStar(require("./files/S3FileStorageAdapter"), exports);
31
+ __exportStar(require("./files/filename"), exports);
32
+ function isMultipart(req) {
33
+ const ct = (req.headers['content-type'] || '').toLowerCase();
34
+ return ct.startsWith('multipart/form-data');
35
+ }
36
+ function isJson(req) {
37
+ // req.is is safer if body-parser/express has set it up
38
+ return (Boolean(req.is?.('application/json')) ||
39
+ (req.headers['content-type'] || '')
40
+ .toLowerCase()
41
+ .startsWith('application/json'));
42
+ }
43
+ function isUrlEncoded(req) {
44
+ return (Boolean(req.is?.('application/x-www-form-urlencoded')) ||
45
+ (req.headers['content-type'] || '')
46
+ .toLowerCase()
47
+ .startsWith('application/x-www-form-urlencoded'));
48
+ }
49
+ /**
50
+ * Compose the public router:
51
+ * - JSON & urlencoded body parsers
52
+ * - a guarded "body required" check for JSON/urlencoded ONLY
53
+ * - mounts APIRouter (which includes /files using multer)
54
+ */
55
+ function createNextMinRouter(options) {
56
+ const apiRouter = new apiRouter_1.APIRouter(options).getRouter();
57
+ const router = express_1.default.Router();
58
+ // ✅ Parse JSON & urlencoded (does NOT parse multipart/form-data)
59
+ router.use(body_parser_1.default.json({ limit: '2mb' }));
60
+ router.use(body_parser_1.default.urlencoded({ extended: true }));
61
+ // ✅ Require body ONLY for JSON / urlencoded requests
62
+ // Skip multipart (handled by multer inside APIRouter)
63
+ router.use((req, res, next) => {
64
+ // Allow non-mutating methods and preflight
65
+ if (!['POST', 'PUT', 'PATCH'].includes(req.method))
66
+ return next();
67
+ // Never block /files (multer needs to see raw multipart)
68
+ if (req.path.startsWith('/files') || isMultipart(req))
69
+ return next();
70
+ // Enforce body presence only for JSON or urlencoded
71
+ const mustHaveBody = isJson(req) || isUrlEncoded(req);
72
+ if (mustHaveBody) {
73
+ const empty = !req.body ||
74
+ (typeof req.body === 'object' && Object.keys(req.body).length === 0);
75
+ if (empty) {
76
+ return res.status(400).json({
77
+ error: true,
78
+ message: 'Request body is required for JSON/urlencoded requests.',
79
+ });
80
+ }
81
+ }
82
+ next();
83
+ });
84
+ // ✅ Mount the actual API (includes /files with multer.any())
85
+ router.use(apiRouter);
86
+ return router;
87
+ }
@@ -0,0 +1,44 @@
1
+ import { DatabaseAdapter } from '../database/DatabaseAdapter';
2
+ export type SortDir = 1 | -1;
3
+ export type SortSpec = Record<string, SortDir>;
4
+ export type ReadOptions = {
5
+ sort?: SortSpec;
6
+ projection?: Record<string, 0 | 1>;
7
+ };
8
+ export interface Attributes {
9
+ type: string;
10
+ required?: boolean;
11
+ default?: any;
12
+ enum?: any[];
13
+ ref?: string;
14
+ select?: string[];
15
+ private?: boolean;
16
+ unique?: boolean;
17
+ sensitive?: boolean;
18
+ writeOnly?: boolean;
19
+ }
20
+ export interface Schema {
21
+ modelName: string;
22
+ attributes: {
23
+ [key: string]: Attributes;
24
+ };
25
+ private?: boolean;
26
+ allowedMethods: {
27
+ create?: boolean;
28
+ read?: boolean;
29
+ update?: boolean;
30
+ delete?: boolean;
31
+ };
32
+ extends?: string;
33
+ }
34
+ export declare class BaseModel {
35
+ private schema;
36
+ private adapter;
37
+ private collectionName;
38
+ constructor(schema: Schema, adapter: DatabaseAdapter);
39
+ create(data: any, includePrivateFields?: boolean): Promise<any>;
40
+ read(query: any, limit?: number, skip?: number, includePrivateFields?: boolean, options?: ReadOptions): Promise<any[]>;
41
+ update(id: string, data: any, includePrivateFields?: boolean): Promise<any>;
42
+ delete(id: string, includePrivateFields?: boolean): Promise<any>;
43
+ count(query: any, includePrivateFields?: boolean): Promise<number>;
44
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseModel = void 0;
4
+ class BaseModel {
5
+ constructor(schema, adapter) {
6
+ this.schema = schema;
7
+ this.adapter = adapter;
8
+ this.collectionName = schema.modelName.toLowerCase();
9
+ }
10
+ async create(data, includePrivateFields) {
11
+ for (const [key, attr] of Object.entries(this.schema.attributes)) {
12
+ if (data[key] === undefined && attr.default !== undefined) {
13
+ data[key] = attr.default;
14
+ }
15
+ }
16
+ return await this.adapter.create(this.collectionName, data, this.schema, includePrivateFields);
17
+ }
18
+ async read(query, limit, skip, includePrivateFields, options) {
19
+ return await this.adapter.read(this.collectionName, query, limit, skip, this.schema, includePrivateFields, options);
20
+ }
21
+ async update(id, data, includePrivateFields) {
22
+ return await this.adapter.update(this.collectionName, id, data, this.schema, includePrivateFields);
23
+ }
24
+ async delete(id, includePrivateFields) {
25
+ return await this.adapter.delete(this.collectionName, id, this.schema, includePrivateFields);
26
+ }
27
+ async count(query, includePrivateFields) {
28
+ return await this.adapter.count(this.collectionName, query, this.schema, includePrivateFields);
29
+ }
30
+ }
31
+ exports.BaseModel = BaseModel;
@@ -0,0 +1,25 @@
1
+ export type Action = 'create' | 'read' | 'update' | 'delete';
2
+ export interface PolicyContext {
3
+ isAuthenticated: boolean;
4
+ role: string | null;
5
+ userId: string | null;
6
+ isSuperadmin: boolean;
7
+ apiKeyOk: boolean;
8
+ }
9
+ export interface Decision {
10
+ allow: boolean;
11
+ readMask?: string[];
12
+ writeDeny?: string[];
13
+ createDefaults?: Record<string, any>;
14
+ restrictions?: Record<string, any>;
15
+ queryFilter?: Record<string, any>;
16
+ exposePrivate?: boolean;
17
+ sensitiveMask?: string[];
18
+ }
19
+ export declare function authorize(modelNameLC: string, action: Action, schemaPolicy: any, ctx: PolicyContext, doc?: any): Decision;
20
+ export declare function applyReadMaskOne(doc: any, mask?: string[]): any;
21
+ export declare function applyReadMaskMany(docs: any[], mask?: string[]): any[];
22
+ export declare function stripWriteDeny(payload: any, deny?: string[]): any;
23
+ export declare function mergeCreateDefaults(payload: any, defs?: Record<string, any>): any;
24
+ export declare function enforceRestrictions(payload: any, restrictions?: Record<string, any>, _ctx?: PolicyContext): void;
25
+ export declare function andFilter(a?: Record<string, any>, b?: Record<string, any>): Record<string, any>;
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ // src/policy/authorize.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.authorize = authorize;
5
+ exports.applyReadMaskOne = applyReadMaskOne;
6
+ exports.applyReadMaskMany = applyReadMaskMany;
7
+ exports.stripWriteDeny = stripWriteDeny;
8
+ exports.mergeCreateDefaults = mergeCreateDefaults;
9
+ exports.enforceRestrictions = enforceRestrictions;
10
+ exports.andFilter = andFilter;
11
+ const EMPTY_DECISION = {
12
+ allow: false,
13
+ readMask: [],
14
+ writeDeny: [],
15
+ createDefaults: {},
16
+ restrictions: {},
17
+ queryFilter: {},
18
+ };
19
+ /* ----------------- privacy helpers ----------------- */
20
+ function shouldBypassPrivacy(schemaPolicy, role) {
21
+ if (!role)
22
+ return false;
23
+ const roles = schemaPolicy?.access?.bypassPrivacy?.roles;
24
+ if (!Array.isArray(roles))
25
+ return false;
26
+ return roles
27
+ .map((r) => String(r).toLowerCase())
28
+ .includes(String(role).toLowerCase());
29
+ }
30
+ function computeSensitiveMask(schemaPolicy) {
31
+ const negs = [];
32
+ for (const [name, attr] of Object.entries(schemaPolicy?.attributes || {})) {
33
+ if (!Array.isArray(attr) &&
34
+ (attr.sensitive === true || attr.writeOnly === true)) {
35
+ negs.push(`-${name}`);
36
+ }
37
+ }
38
+ return negs;
39
+ }
40
+ /* ----------------- general helpers ----------------- */
41
+ function asBool(v) {
42
+ return typeof v === 'boolean' ? v : undefined;
43
+ }
44
+ function ownerKey(schemaPolicy) {
45
+ const by = schemaPolicy?.conditions?.owner?.by;
46
+ return typeof by === 'string' && by.trim() ? by.trim() : 'id';
47
+ }
48
+ function ownerFilter(schemaPolicy, ctx) {
49
+ const key = ownerKey(schemaPolicy);
50
+ if (!ctx.userId)
51
+ return {};
52
+ return { [key]: ctx.userId };
53
+ }
54
+ function isOwnerDoc(schemaPolicy, ctx, doc) {
55
+ if (!doc || !ctx.userId)
56
+ return false;
57
+ const key = ownerKey(schemaPolicy);
58
+ const val = doc[key];
59
+ if (val == null)
60
+ return false;
61
+ const candidate = typeof val === 'object' ? (val.id ?? val._id ?? String(val)) : String(val);
62
+ return String(candidate) === String(ctx.userId);
63
+ }
64
+ /* =================================================== */
65
+ /* =============== MAIN AUTHORIZATION ================= */
66
+ /* =================================================== */
67
+ function authorize(modelNameLC, action, schemaPolicy, ctx, doc) {
68
+ // 0) action enabled at all?
69
+ const allowed = !!schemaPolicy?.allowedMethods?.[action];
70
+ if (!allowed)
71
+ return { ...EMPTY_DECISION, allow: false };
72
+ const access = schemaPolicy?.access || {};
73
+ const baseReadMask = [].concat(access?.readMask || []);
74
+ const baseWriteDeny = [].concat(access?.writeDeny || []);
75
+ const restrictionsBase = access?.restrictions || {};
76
+ const createDefaultsBase = access?.createDefaults || {};
77
+ const queryFilterBase = access?.queryFilter || {};
78
+ const bypass = shouldBypassPrivacy(schemaPolicy, ctx.role);
79
+ const sensitiveMask = computeSensitiveMask(schemaPolicy);
80
+ const mkMasks = (readMaskIn, writeDenyIn) => {
81
+ const fullMask = [...(readMaskIn || []), ...sensitiveMask];
82
+ const effectiveReadMask = bypass ? sensitiveMask : fullMask;
83
+ const effectiveWriteDeny = bypass ? [] : writeDenyIn || [];
84
+ return { effectiveReadMask, effectiveWriteDeny };
85
+ };
86
+ // 1) no access block → sensible defaults
87
+ if (!schemaPolicy?.access) {
88
+ const allowedByDefault = action === 'read' ? true : !!ctx.isAuthenticated;
89
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
90
+ return {
91
+ allow: allowedByDefault,
92
+ readMask: effectiveReadMask,
93
+ writeDeny: effectiveWriteDeny,
94
+ createDefaults: {},
95
+ restrictions: {},
96
+ queryFilter: action === 'read' ? {} : {},
97
+ exposePrivate: bypass, // will normally be false if no bypassPrivacy set
98
+ sensitiveMask,
99
+ };
100
+ }
101
+ // 2) PUBLIC
102
+ const pubRule = access?.public?.[action];
103
+ const pubBool = asBool(pubRule);
104
+ if (pubBool === true) {
105
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
106
+ return {
107
+ allow: true,
108
+ readMask: effectiveReadMask,
109
+ writeDeny: effectiveWriteDeny,
110
+ createDefaults: createDefaultsBase?.public || {},
111
+ restrictions: restrictionsBase?.public || {},
112
+ queryFilter: queryFilterBase?.public || {},
113
+ exposePrivate: bypass, // if caller has a bypass role, they get private fields (still masked by sensitive)
114
+ sensitiveMask,
115
+ };
116
+ }
117
+ // if false → continue to roles/auth checks
118
+ // 3) ROLES
119
+ const roleName = (ctx.role || '').toLowerCase();
120
+ if (roleName && access?.roles?.[roleName]) {
121
+ const rule = access.roles[roleName][action];
122
+ const rBool = asBool(rule);
123
+ if (rBool === true) {
124
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
125
+ return {
126
+ allow: true,
127
+ readMask: effectiveReadMask,
128
+ writeDeny: effectiveWriteDeny,
129
+ createDefaults: createDefaultsBase?.roles?.[roleName] || {},
130
+ restrictions: restrictionsBase?.roles?.[roleName] || {},
131
+ queryFilter: queryFilterBase?.roles?.[roleName] || {},
132
+ exposePrivate: bypass,
133
+ sensitiveMask,
134
+ };
135
+ }
136
+ if (rBool === false) {
137
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
138
+ return {
139
+ ...EMPTY_DECISION,
140
+ readMask: effectiveReadMask,
141
+ writeDeny: effectiveWriteDeny,
142
+ exposePrivate: false,
143
+ sensitiveMask,
144
+ };
145
+ }
146
+ // role === 'owner'
147
+ if (rule === 'owner') {
148
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
149
+ if (action === 'read') {
150
+ if (doc) {
151
+ return {
152
+ allow: isOwnerDoc(schemaPolicy, ctx, doc),
153
+ readMask: effectiveReadMask,
154
+ writeDeny: effectiveWriteDeny,
155
+ createDefaults: {},
156
+ restrictions: {},
157
+ queryFilter: {},
158
+ exposePrivate: bypass,
159
+ sensitiveMask,
160
+ };
161
+ }
162
+ return {
163
+ allow: true,
164
+ readMask: effectiveReadMask,
165
+ writeDeny: effectiveWriteDeny,
166
+ createDefaults: {},
167
+ restrictions: {},
168
+ queryFilter: ownerFilter(schemaPolicy, ctx),
169
+ exposePrivate: bypass,
170
+ sensitiveMask,
171
+ };
172
+ }
173
+ return {
174
+ allow: isOwnerDoc(schemaPolicy, ctx, doc),
175
+ readMask: effectiveReadMask,
176
+ writeDeny: effectiveWriteDeny,
177
+ createDefaults: {},
178
+ restrictions: {},
179
+ queryFilter: {},
180
+ exposePrivate: bypass,
181
+ sensitiveMask,
182
+ };
183
+ }
184
+ }
185
+ // 4) AUTHENTICATED
186
+ const authRule = access?.authenticated?.[action];
187
+ const aBool = asBool(authRule);
188
+ if (aBool === true && ctx.isAuthenticated) {
189
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
190
+ return {
191
+ allow: true,
192
+ readMask: effectiveReadMask,
193
+ writeDeny: effectiveWriteDeny,
194
+ createDefaults: createDefaultsBase?.authenticated || {},
195
+ restrictions: restrictionsBase?.authenticated || {},
196
+ queryFilter: queryFilterBase?.authenticated || {},
197
+ exposePrivate: bypass,
198
+ sensitiveMask,
199
+ };
200
+ }
201
+ if (aBool === false && ctx.isAuthenticated) {
202
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
203
+ return {
204
+ ...EMPTY_DECISION,
205
+ readMask: effectiveReadMask,
206
+ writeDeny: effectiveWriteDeny,
207
+ exposePrivate: false,
208
+ sensitiveMask,
209
+ };
210
+ }
211
+ if (authRule === 'owner' && ctx.isAuthenticated) {
212
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
213
+ if (action === 'read') {
214
+ if (doc) {
215
+ return {
216
+ allow: isOwnerDoc(schemaPolicy, ctx, doc),
217
+ readMask: effectiveReadMask,
218
+ writeDeny: effectiveWriteDeny,
219
+ createDefaults: {},
220
+ restrictions: {},
221
+ queryFilter: {},
222
+ exposePrivate: bypass,
223
+ sensitiveMask,
224
+ };
225
+ }
226
+ return {
227
+ allow: true,
228
+ readMask: effectiveReadMask,
229
+ writeDeny: effectiveWriteDeny,
230
+ createDefaults: {},
231
+ restrictions: {},
232
+ queryFilter: ownerFilter(schemaPolicy, ctx),
233
+ exposePrivate: bypass,
234
+ sensitiveMask,
235
+ };
236
+ }
237
+ return {
238
+ allow: isOwnerDoc(schemaPolicy, ctx, doc),
239
+ readMask: effectiveReadMask,
240
+ writeDeny: effectiveReadMask, // (typo guard: will be ignored by callers; leave as is)
241
+ createDefaults: {},
242
+ restrictions: {},
243
+ queryFilter: {},
244
+ exposePrivate: bypass,
245
+ sensitiveMask,
246
+ };
247
+ }
248
+ // 5) default deny
249
+ const { effectiveReadMask, effectiveWriteDeny } = mkMasks(baseReadMask, baseWriteDeny);
250
+ return {
251
+ ...EMPTY_DECISION,
252
+ readMask: effectiveReadMask,
253
+ writeDeny: effectiveWriteDeny,
254
+ exposePrivate: false,
255
+ sensitiveMask,
256
+ };
257
+ }
258
+ /* ===== Helpers consumed by the router ===== */
259
+ function applyReadMaskOne(doc, mask) {
260
+ if (!doc || !mask?.length)
261
+ return doc;
262
+ const result = { ...doc };
263
+ for (const m of mask) {
264
+ if (typeof m === 'string' && m.startsWith('-')) {
265
+ delete result[m.slice(1)];
266
+ }
267
+ }
268
+ return result;
269
+ }
270
+ function applyReadMaskMany(docs, mask) {
271
+ if (!Array.isArray(docs) || !mask?.length)
272
+ return docs;
273
+ return docs.map((d) => applyReadMaskOne(d, mask));
274
+ }
275
+ function stripWriteDeny(payload, deny) {
276
+ if (!deny?.length || !payload)
277
+ return payload;
278
+ const out = { ...payload };
279
+ for (const k of deny)
280
+ delete out[k];
281
+ return out;
282
+ }
283
+ function mergeCreateDefaults(payload, defs) {
284
+ if (!defs || !Object.keys(defs).length)
285
+ return payload;
286
+ return { ...defs, ...payload };
287
+ }
288
+ function enforceRestrictions(payload, restrictions, _ctx) {
289
+ if (!payload || !restrictions)
290
+ return;
291
+ for (const [field, blocked] of Object.entries(restrictions)) {
292
+ if (Array.isArray(blocked) && blocked.includes(payload[field])) {
293
+ throw new Error(`Restricted value for field: ${field}`);
294
+ }
295
+ }
296
+ }
297
+ function andFilter(a, b) {
298
+ const A = a && Object.keys(a).length ? a : {};
299
+ const B = b && Object.keys(b).length ? b : {};
300
+ if (!Object.keys(A).length)
301
+ return B;
302
+ if (!Object.keys(B).length)
303
+ return A;
304
+ return { $and: [A, B] };
305
+ }
@@ -0,0 +1,14 @@
1
+ export declare function buildConditionFilter(condition: string, opts: {
2
+ by?: string;
3
+ context: {
4
+ userId?: string | null;
5
+ tenantId?: string | null;
6
+ };
7
+ }): Record<string, any>;
8
+ export declare function checkRecordCondition(condition: string, record: any, opts: {
9
+ by?: string;
10
+ context: {
11
+ userId?: string | null;
12
+ tenantId?: string | null;
13
+ };
14
+ }): boolean;