myaidev-method 0.2.8 → 0.2.9

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 (157) hide show
  1. package/.claude/agents/wordpress-admin.md +271 -0
  2. package/.env.example +0 -1
  3. package/PACKAGE_FIXES_SUMMARY.md +319 -0
  4. package/PAYLOADCMS_AUTH_UPDATE.md +248 -0
  5. package/USER_GUIDE.md +260 -0
  6. package/bin/cli.js +36 -0
  7. package/dist/server/.tsbuildinfo +1 -0
  8. package/dist/server/auth/controllers/AuthController.d.ts +34 -0
  9. package/dist/server/auth/controllers/AuthController.d.ts.map +1 -0
  10. package/dist/server/auth/controllers/AuthController.js +43 -0
  11. package/dist/server/auth/controllers/AuthController.js.map +1 -0
  12. package/dist/server/auth/example-usage.d.ts +53 -0
  13. package/dist/server/auth/example-usage.d.ts.map +1 -0
  14. package/dist/server/auth/example-usage.js +129 -0
  15. package/dist/server/auth/example-usage.js.map +1 -0
  16. package/dist/server/auth/index.d.ts +11 -0
  17. package/dist/server/auth/index.d.ts.map +1 -0
  18. package/dist/server/auth/index.js +15 -0
  19. package/dist/server/auth/index.js.map +1 -0
  20. package/dist/server/auth/layers.d.ts +19 -0
  21. package/dist/server/auth/layers.d.ts.map +1 -0
  22. package/dist/server/auth/layers.js +33 -0
  23. package/dist/server/auth/layers.js.map +1 -0
  24. package/dist/server/auth/middleware/authMiddleware.d.ts +24 -0
  25. package/dist/server/auth/middleware/authMiddleware.d.ts.map +1 -0
  26. package/dist/server/auth/middleware/authMiddleware.js +65 -0
  27. package/dist/server/auth/middleware/authMiddleware.js.map +1 -0
  28. package/dist/server/auth/routes/authRoutes.d.ts +11 -0
  29. package/dist/server/auth/routes/authRoutes.d.ts.map +1 -0
  30. package/dist/server/auth/routes/authRoutes.js +213 -0
  31. package/dist/server/auth/routes/authRoutes.js.map +1 -0
  32. package/dist/server/auth/services/AuditLogService.d.ts +21 -0
  33. package/dist/server/auth/services/AuditLogService.d.ts.map +1 -0
  34. package/dist/server/auth/services/AuditLogService.js +28 -0
  35. package/dist/server/auth/services/AuditLogService.js.map +1 -0
  36. package/dist/server/auth/services/AuthService.d.ts +27 -0
  37. package/dist/server/auth/services/AuthService.d.ts.map +1 -0
  38. package/dist/server/auth/services/AuthService.js +246 -0
  39. package/dist/server/auth/services/AuthService.js.map +1 -0
  40. package/dist/server/auth/services/PasswordService.d.ts +12 -0
  41. package/dist/server/auth/services/PasswordService.d.ts.map +1 -0
  42. package/dist/server/auth/services/PasswordService.js +31 -0
  43. package/dist/server/auth/services/PasswordService.js.map +1 -0
  44. package/dist/server/auth/services/SessionRepository.d.ts +24 -0
  45. package/dist/server/auth/services/SessionRepository.d.ts.map +1 -0
  46. package/dist/server/auth/services/SessionRepository.js +101 -0
  47. package/dist/server/auth/services/SessionRepository.js.map +1 -0
  48. package/dist/server/auth/services/TokenService.d.ts +12 -0
  49. package/dist/server/auth/services/TokenService.d.ts.map +1 -0
  50. package/dist/server/auth/services/TokenService.js +86 -0
  51. package/dist/server/auth/services/TokenService.js.map +1 -0
  52. package/dist/server/auth/services/UserRepository.d.ts +23 -0
  53. package/dist/server/auth/services/UserRepository.d.ts.map +1 -0
  54. package/dist/server/auth/services/UserRepository.js +168 -0
  55. package/dist/server/auth/services/UserRepository.js.map +1 -0
  56. package/dist/server/auth/services/example.d.ts +26 -0
  57. package/dist/server/auth/services/example.d.ts.map +1 -0
  58. package/dist/server/auth/services/example.js +221 -0
  59. package/dist/server/auth/services/example.js.map +1 -0
  60. package/dist/server/auth/services/index.d.ts +6 -0
  61. package/dist/server/auth/services/index.d.ts.map +1 -0
  62. package/dist/server/auth/services/index.js +7 -0
  63. package/dist/server/auth/services/index.js.map +1 -0
  64. package/dist/server/database/db.d.ts +28 -0
  65. package/dist/server/database/db.d.ts.map +1 -0
  66. package/dist/server/database/db.js +91 -0
  67. package/dist/server/database/db.js.map +1 -0
  68. package/dist/server/database/schema.sql +95 -0
  69. package/dist/server/hono/app.d.ts +10 -0
  70. package/dist/server/hono/app.d.ts.map +1 -0
  71. package/dist/server/hono/app.js +26 -0
  72. package/dist/server/hono/app.js.map +1 -0
  73. package/dist/server/hono/routes.d.ts +12 -0
  74. package/dist/server/hono/routes.d.ts.map +1 -0
  75. package/dist/server/hono/routes.js +40 -0
  76. package/dist/server/hono/routes.js.map +1 -0
  77. package/dist/server/main.d.ts +2 -0
  78. package/dist/server/main.d.ts.map +1 -0
  79. package/dist/server/main.js +94 -0
  80. package/dist/server/main.js.map +1 -0
  81. package/dist/server/user-management/DirectoryService.d.ts +62 -0
  82. package/dist/server/user-management/DirectoryService.d.ts.map +1 -0
  83. package/dist/server/user-management/DirectoryService.js +201 -0
  84. package/dist/server/user-management/DirectoryService.js.map +1 -0
  85. package/dist/server/user-management/LinuxUserService.d.ts +71 -0
  86. package/dist/server/user-management/LinuxUserService.d.ts.map +1 -0
  87. package/dist/server/user-management/LinuxUserService.js +192 -0
  88. package/dist/server/user-management/LinuxUserService.js.map +1 -0
  89. package/dist/server/user-management/QuotaService.d.ts +59 -0
  90. package/dist/server/user-management/QuotaService.d.ts.map +1 -0
  91. package/dist/server/user-management/QuotaService.js +148 -0
  92. package/dist/server/user-management/QuotaService.js.map +1 -0
  93. package/dist/server/user-management/UserManagementService.d.ts +74 -0
  94. package/dist/server/user-management/UserManagementService.d.ts.map +1 -0
  95. package/dist/server/user-management/UserManagementService.js +122 -0
  96. package/dist/server/user-management/UserManagementService.js.map +1 -0
  97. package/dist/server/user-management/index.d.ts +26 -0
  98. package/dist/server/user-management/index.d.ts.map +1 -0
  99. package/dist/server/user-management/index.js +26 -0
  100. package/dist/server/user-management/index.js.map +1 -0
  101. package/dist/server/user-management/layers.d.ts +27 -0
  102. package/dist/server/user-management/layers.d.ts.map +1 -0
  103. package/dist/server/user-management/layers.js +37 -0
  104. package/dist/server/user-management/layers.js.map +1 -0
  105. package/dist/shared/types.d.ts +94 -0
  106. package/dist/shared/types.d.ts.map +1 -0
  107. package/dist/shared/types.js +32 -0
  108. package/dist/shared/types.js.map +1 -0
  109. package/package.json +25 -5
  110. package/src/lib/payloadcms-utils.js +5 -12
  111. package/src/server/auth/ARCHITECTURE.md +575 -0
  112. package/src/server/auth/IMPLEMENTATION_SUMMARY.md +287 -0
  113. package/src/server/auth/QUICK_START.md +283 -0
  114. package/src/server/auth/README.md +290 -0
  115. package/src/server/auth/controllers/AuthController.ts +129 -0
  116. package/src/server/auth/example-usage.ts +159 -0
  117. package/src/server/auth/index.ts +19 -0
  118. package/src/server/auth/layers.ts +57 -0
  119. package/src/server/auth/middleware/authMiddleware.ts +118 -0
  120. package/src/server/auth/routes/authRoutes.ts +319 -0
  121. package/src/server/auth/services/AuditLogService.ts +81 -0
  122. package/src/server/auth/services/AuthService.ts +408 -0
  123. package/src/server/auth/services/IMPLEMENTATION_SUMMARY.md +404 -0
  124. package/src/server/auth/services/PasswordService.ts +85 -0
  125. package/src/server/auth/services/README.md +361 -0
  126. package/src/server/auth/services/SessionRepository.ts +227 -0
  127. package/src/server/auth/services/TokenService.ts +174 -0
  128. package/src/server/auth/services/UserRepository.ts +318 -0
  129. package/src/server/auth/services/example.ts +346 -0
  130. package/src/server/auth/services/index.ts +6 -0
  131. package/src/server/database/db.ts +161 -0
  132. package/src/server/database/schema.sql +95 -0
  133. package/src/server/hono/app.ts +41 -0
  134. package/src/server/main.ts +115 -0
  135. package/src/server/user-management/DirectoryService.ts +348 -0
  136. package/src/server/user-management/LinuxUserService.ts +338 -0
  137. package/src/server/user-management/QuotaService.ts +256 -0
  138. package/src/server/user-management/README.md +333 -0
  139. package/src/server/user-management/UserManagementService.ts +335 -0
  140. package/src/server/user-management/index.ts +26 -0
  141. package/src/server/user-management/layers.ts +51 -0
  142. package/src/shared/types.ts +111 -0
  143. package/src/templates/claude/agents/payloadcms-publish.md +34 -14
  144. package/src/templates/codex/commands/myai-astro-publish.md +8 -2
  145. package/src/templates/codex/commands/myai-content-writer.md +8 -2
  146. package/src/templates/codex/commands/myai-coolify-deploy.md +8 -2
  147. package/src/templates/codex/commands/myai-dev-architect.md +8 -2
  148. package/src/templates/codex/commands/myai-dev-code.md +8 -2
  149. package/src/templates/codex/commands/myai-dev-docs.md +8 -2
  150. package/src/templates/codex/commands/myai-dev-review.md +8 -2
  151. package/src/templates/codex/commands/myai-dev-test.md +8 -2
  152. package/src/templates/codex/commands/myai-docusaurus-publish.md +8 -2
  153. package/src/templates/codex/commands/myai-mintlify-publish.md +8 -2
  154. package/src/templates/codex/commands/myai-payloadcms-publish.md +17 -3
  155. package/src/templates/codex/commands/myai-sparc-workflow.md +8 -2
  156. package/src/templates/codex/commands/myai-wordpress-admin.md +8 -2
  157. package/src/templates/codex/commands/myai-wordpress-publish.md +8 -2
@@ -0,0 +1,338 @@
1
+ import { Effect, Context, Layer } from "effect";
2
+ import { execSync } from "child_process";
3
+
4
+ /**
5
+ * Linux User Management Service
6
+ *
7
+ * Handles creation, management, and deletion of Linux system users
8
+ * for multi-user isolation in MyAIDev Method web server.
9
+ *
10
+ * Security Features:
11
+ * - No sudo/root access granted to created users
12
+ * - Limited shell access (rbash or nologin)
13
+ * - Home directory isolation
14
+ * - Resource limits via ulimit
15
+ * - User groups for permission management
16
+ */
17
+
18
+ export interface LinuxUser {
19
+ username: string;
20
+ uid: number;
21
+ gid: number;
22
+ homeDir: string;
23
+ shell: string;
24
+ created: boolean;
25
+ }
26
+
27
+ export interface CreateLinuxUserOptions {
28
+ username: string;
29
+ shell?: "/bin/rbash" | "/usr/sbin/nologin" | "/bin/bash";
30
+ createHome?: boolean;
31
+ groups?: string[];
32
+ }
33
+
34
+ export interface LinuxUserError {
35
+ readonly _tag: "LinuxUserError";
36
+ readonly message: string;
37
+ readonly cause?: unknown;
38
+ }
39
+
40
+ const LinuxUserError = (message: string, cause?: unknown): LinuxUserError => ({
41
+ _tag: "LinuxUserError",
42
+ message,
43
+ cause,
44
+ });
45
+
46
+ export class LinuxUserService extends Context.Tag("LinuxUserService")<
47
+ LinuxUserService,
48
+ {
49
+ /**
50
+ * Create a new Linux system user
51
+ */
52
+ readonly createUser: (
53
+ options: CreateLinuxUserOptions
54
+ ) => Effect.Effect<LinuxUser, LinuxUserError>;
55
+
56
+ /**
57
+ * Check if a Linux user exists
58
+ */
59
+ readonly userExists: (
60
+ username: string
61
+ ) => Effect.Effect<boolean, LinuxUserError>;
62
+
63
+ /**
64
+ * Get Linux user information
65
+ */
66
+ readonly getUserInfo: (
67
+ username: string
68
+ ) => Effect.Effect<LinuxUser, LinuxUserError>;
69
+
70
+ /**
71
+ * Delete a Linux system user
72
+ */
73
+ readonly deleteUser: (
74
+ username: string,
75
+ removeHome?: boolean
76
+ ) => Effect.Effect<void, LinuxUserError>;
77
+
78
+ /**
79
+ * Set resource limits for a user
80
+ */
81
+ readonly setResourceLimits: (
82
+ username: string,
83
+ limits: ResourceLimits
84
+ ) => Effect.Effect<void, LinuxUserError>;
85
+
86
+ /**
87
+ * Sanitize username for Linux system use
88
+ * Converts email-based usernames to valid Linux usernames
89
+ */
90
+ readonly sanitizeUsername: (
91
+ username: string
92
+ ) => Effect.Effect<string, LinuxUserError>;
93
+ }
94
+ >() {
95
+ static Live = Layer.succeed(this, {
96
+ createUser: (options: CreateLinuxUserOptions) =>
97
+ Effect.gen(function* () {
98
+ const { username, shell = "/bin/rbash", createHome = true, groups = [] } = options;
99
+
100
+ // Validate username format (Linux username requirements)
101
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(username)) {
102
+ return yield* Effect.fail(
103
+ LinuxUserError(
104
+ `Invalid Linux username format: ${username}. Must start with lowercase letter or underscore, contain only lowercase letters, digits, underscores, and hyphens, and be 1-32 characters long.`
105
+ )
106
+ );
107
+ }
108
+
109
+ // Check if user already exists
110
+ const exists = yield* Effect.tryPromise({
111
+ try: async () => {
112
+ try {
113
+ execSync(`id -u ${username}`, { stdio: "ignore" });
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ },
119
+ catch: (error) => LinuxUserError("Failed to check if user exists", error),
120
+ });
121
+
122
+ if (exists) {
123
+ return yield* Effect.fail(
124
+ LinuxUserError(`Linux user ${username} already exists`)
125
+ );
126
+ }
127
+
128
+ // Create user command
129
+ const createHomeFlag = createHome ? "--create-home" : "--no-create-home";
130
+ const groupsFlag = groups.length > 0 ? `--groups ${groups.join(",")}` : "";
131
+ const command = `sudo useradd ${createHomeFlag} --shell ${shell} ${groupsFlag} ${username}`;
132
+
133
+ yield* Effect.tryPromise({
134
+ try: async () => {
135
+ execSync(command, { stdio: "pipe" });
136
+ },
137
+ catch: (error) =>
138
+ LinuxUserError(`Failed to create Linux user ${username}`, error),
139
+ });
140
+
141
+ // Get user info
142
+ const userInfo = yield* Effect.tryPromise({
143
+ try: async () => {
144
+ const uidOutput = execSync(`id -u ${username}`, {
145
+ encoding: "utf-8",
146
+ }).trim();
147
+ const gidOutput = execSync(`id -g ${username}`, {
148
+ encoding: "utf-8",
149
+ }).trim();
150
+ const homeDir = createHome ? `/home/${username}` : "";
151
+
152
+ return {
153
+ username,
154
+ uid: parseInt(uidOutput, 10),
155
+ gid: parseInt(gidOutput, 10),
156
+ homeDir,
157
+ shell,
158
+ created: true,
159
+ };
160
+ },
161
+ catch: (error) =>
162
+ LinuxUserError(`Failed to get user info for ${username}`, error),
163
+ });
164
+
165
+ return userInfo;
166
+ }),
167
+
168
+ userExists: (username: string) =>
169
+ Effect.tryPromise({
170
+ try: async () => {
171
+ try {
172
+ execSync(`id -u ${username}`, { stdio: "ignore" });
173
+ return true;
174
+ } catch {
175
+ return false;
176
+ }
177
+ },
178
+ catch: (error) => LinuxUserError("Failed to check if user exists", error),
179
+ }),
180
+
181
+ getUserInfo: (username: string) =>
182
+ Effect.gen(function* () {
183
+ const exists = yield* Effect.tryPromise({
184
+ try: async () => {
185
+ try {
186
+ execSync(`id -u ${username}`, { stdio: "ignore" });
187
+ return true;
188
+ } catch {
189
+ return false;
190
+ }
191
+ },
192
+ catch: (error) => LinuxUserError("Failed to check if user exists", error),
193
+ });
194
+
195
+ if (!exists) {
196
+ return yield* Effect.fail(
197
+ LinuxUserError(`Linux user ${username} does not exist`)
198
+ );
199
+ }
200
+
201
+ const userInfo = yield* Effect.tryPromise({
202
+ try: async () => {
203
+ const uidOutput = execSync(`id -u ${username}`, {
204
+ encoding: "utf-8",
205
+ }).trim();
206
+ const gidOutput = execSync(`id -g ${username}`, {
207
+ encoding: "utf-8",
208
+ }).trim();
209
+
210
+ // Get shell from /etc/passwd
211
+ const passwdLine = execSync(`getent passwd ${username}`, {
212
+ encoding: "utf-8",
213
+ }).trim();
214
+ const shell = passwdLine.split(":")[6] || "/bin/bash";
215
+
216
+ // Get home directory
217
+ const homeDir = passwdLine.split(":")[5] || `/home/${username}`;
218
+
219
+ return {
220
+ username,
221
+ uid: parseInt(uidOutput, 10),
222
+ gid: parseInt(gidOutput, 10),
223
+ homeDir,
224
+ shell,
225
+ created: true,
226
+ };
227
+ },
228
+ catch: (error) =>
229
+ LinuxUserError(`Failed to get user info for ${username}`, error),
230
+ });
231
+
232
+ return userInfo;
233
+ }),
234
+
235
+ deleteUser: (username: string, removeHome: boolean = true) =>
236
+ Effect.gen(function* () {
237
+ // Check if user exists
238
+ const exists = yield* Effect.tryPromise({
239
+ try: async () => {
240
+ try {
241
+ execSync(`id -u ${username}`, { stdio: "ignore" });
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ },
247
+ catch: (error) => LinuxUserError("Failed to check if user exists", error),
248
+ });
249
+
250
+ if (!exists) {
251
+ return yield* Effect.fail(
252
+ LinuxUserError(`Linux user ${username} does not exist`)
253
+ );
254
+ }
255
+
256
+ // Delete user
257
+ const removeHomeFlag = removeHome ? "--remove" : "";
258
+ const command = `sudo userdel ${removeHomeFlag} ${username}`;
259
+
260
+ yield* Effect.tryPromise({
261
+ try: async () => {
262
+ execSync(command, { stdio: "pipe" });
263
+ },
264
+ catch: (error) =>
265
+ LinuxUserError(`Failed to delete Linux user ${username}`, error),
266
+ });
267
+ }),
268
+
269
+ setResourceLimits: (username: string, limits: ResourceLimits) =>
270
+ Effect.gen(function* () {
271
+ // Create limits configuration file
272
+ const limitsContent = `
273
+ # Resource limits for ${username}
274
+ ${username} soft nofile ${limits.maxOpenFiles || 1024}
275
+ ${username} hard nofile ${limits.maxOpenFiles || 2048}
276
+ ${username} soft nproc ${limits.maxProcesses || 256}
277
+ ${username} hard nproc ${limits.maxProcesses || 512}
278
+ ${username} soft as ${limits.maxMemoryKB || 2097152}
279
+ ${username} hard as ${limits.maxMemoryKB || 4194304}
280
+ ${username} soft cpu ${limits.maxCPUTime || 60}
281
+ ${username} hard cpu ${limits.maxCPUTime || 120}
282
+ `.trim();
283
+
284
+ const limitsFilePath = `/etc/security/limits.d/${username}.conf`;
285
+
286
+ yield* Effect.tryPromise({
287
+ try: async () => {
288
+ // Write limits file (requires sudo)
289
+ execSync(`echo '${limitsContent}' | sudo tee ${limitsFilePath} > /dev/null`, {
290
+ stdio: "pipe",
291
+ });
292
+ },
293
+ catch: (error) =>
294
+ LinuxUserError(
295
+ `Failed to set resource limits for ${username}`,
296
+ error
297
+ ),
298
+ });
299
+ }),
300
+
301
+ sanitizeUsername: (username: string) =>
302
+ Effect.gen(function* () {
303
+ // Convert to lowercase
304
+ let sanitized = username.toLowerCase();
305
+
306
+ // Replace invalid characters with underscores
307
+ sanitized = sanitized.replace(/[^a-z0-9_-]/g, "_");
308
+
309
+ // Ensure starts with letter or underscore
310
+ if (!/^[a-z_]/.test(sanitized)) {
311
+ sanitized = `u_${sanitized}`;
312
+ }
313
+
314
+ // Truncate to 32 characters
315
+ sanitized = sanitized.substring(0, 32);
316
+
317
+ // Remove trailing hyphens or underscores
318
+ sanitized = sanitized.replace(/[-_]+$/, "");
319
+
320
+ if (sanitized.length === 0) {
321
+ return yield* Effect.fail(
322
+ LinuxUserError(
323
+ `Could not sanitize username: ${username} results in empty string`
324
+ )
325
+ );
326
+ }
327
+
328
+ return sanitized;
329
+ }),
330
+ });
331
+ }
332
+
333
+ export interface ResourceLimits {
334
+ maxOpenFiles?: number;
335
+ maxProcesses?: number;
336
+ maxMemoryKB?: number;
337
+ maxCPUTime?: number;
338
+ }
@@ -0,0 +1,256 @@
1
+ import { Effect, Context, Layer } from "effect";
2
+ import { execSync } from "child_process";
3
+
4
+ /**
5
+ * Quota Management Service
6
+ *
7
+ * Manages disk quotas and resource limits for Linux users.
8
+ * Provides storage limits to prevent individual users from
9
+ * consuming excessive disk space.
10
+ *
11
+ * Features:
12
+ * - Disk quota management (requires quota system on host)
13
+ * - Directory size tracking
14
+ * - Resource limit enforcement
15
+ * - Usage monitoring
16
+ */
17
+
18
+ export interface DiskQuota {
19
+ softLimitMB: number;
20
+ hardLimitMB: number;
21
+ }
22
+
23
+ export interface QuotaUsage {
24
+ usedMB: number;
25
+ softLimitMB: number;
26
+ hardLimitMB: number;
27
+ percentUsed: number;
28
+ isOverSoftLimit: boolean;
29
+ isOverHardLimit: boolean;
30
+ }
31
+
32
+ export interface QuotaError {
33
+ readonly _tag: "QuotaError";
34
+ readonly message: string;
35
+ readonly cause?: unknown;
36
+ }
37
+
38
+ const QuotaError = (message: string, cause?: unknown): QuotaError => ({
39
+ _tag: "QuotaError",
40
+ message,
41
+ cause,
42
+ });
43
+
44
+ export class QuotaService extends Context.Tag("QuotaService")<
45
+ QuotaService,
46
+ {
47
+ /**
48
+ * Set disk quota for a user
49
+ * Note: Requires quota support on the filesystem
50
+ */
51
+ readonly setDiskQuota: (
52
+ username: string,
53
+ quota: DiskQuota
54
+ ) => Effect.Effect<void, QuotaError>;
55
+
56
+ /**
57
+ * Get current quota usage for a user
58
+ */
59
+ readonly getQuotaUsage: (
60
+ username: string
61
+ ) => Effect.Effect<QuotaUsage, QuotaError>;
62
+
63
+ /**
64
+ * Check if quota system is available
65
+ */
66
+ readonly isQuotaAvailable: () => Effect.Effect<boolean, QuotaError>;
67
+
68
+ /**
69
+ * Get directory size for a user (fallback if quota not available)
70
+ */
71
+ readonly getDirectorySize: (
72
+ dirPath: string
73
+ ) => Effect.Effect<number, QuotaError>;
74
+
75
+ /**
76
+ * Enforce storage limits (alternative to disk quotas)
77
+ */
78
+ readonly checkStorageLimit: (
79
+ username: string,
80
+ homeDir: string,
81
+ limitMB: number
82
+ ) => Effect.Effect<boolean, QuotaError>;
83
+ }
84
+ >() {
85
+ static Live = Layer.succeed(this, {
86
+ setDiskQuota: (username: string, quota: DiskQuota) =>
87
+ Effect.gen(function* () {
88
+ // Check if quota is available
89
+ const quotaAvailable = yield* Effect.tryPromise({
90
+ try: async () => {
91
+ try {
92
+ execSync("which setquota", { stdio: "ignore" });
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ },
98
+ catch: (error) =>
99
+ QuotaError("Failed to check quota availability", error),
100
+ });
101
+
102
+ if (!quotaAvailable) {
103
+ return yield* Effect.fail(
104
+ QuotaError(
105
+ "Quota system not available. Install quota tools: sudo apt-get install quota"
106
+ )
107
+ );
108
+ }
109
+
110
+ // Set quota using setquota command
111
+ // Format: setquota -u username soft_blocks hard_blocks soft_inodes hard_inodes filesystem
112
+ const softBlocks = quota.softLimitMB * 1024; // Convert MB to blocks (1 block = 1KB)
113
+ const hardBlocks = quota.hardLimitMB * 1024;
114
+ const softInodes = 10000; // Reasonable inode limit
115
+ const hardInodes = 15000;
116
+ const filesystem = "/"; // Primary filesystem
117
+
118
+ yield* Effect.tryPromise({
119
+ try: async () => {
120
+ const command = `sudo setquota -u ${username} ${softBlocks} ${hardBlocks} ${softInodes} ${hardInodes} ${filesystem}`;
121
+ execSync(command, { stdio: "pipe" });
122
+ },
123
+ catch: (error) =>
124
+ QuotaError(`Failed to set disk quota for ${username}`, error),
125
+ });
126
+ }),
127
+
128
+ getQuotaUsage: (username: string) =>
129
+ Effect.gen(function* () {
130
+ // Check if quota is available
131
+ const quotaAvailable = yield* Effect.tryPromise({
132
+ try: async () => {
133
+ try {
134
+ execSync("which quota", { stdio: "ignore" });
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ },
140
+ catch: (error) =>
141
+ QuotaError("Failed to check quota availability", error),
142
+ });
143
+
144
+ if (!quotaAvailable) {
145
+ // Fallback: Calculate directory size
146
+ const homeDir = `/home/${username}`;
147
+ const usedMB = yield* Effect.tryPromise({
148
+ try: async () => {
149
+ const output = execSync(`sudo du -sm ${homeDir}`, {
150
+ encoding: "utf-8",
151
+ });
152
+ return parseInt(output.split("\t")[0] || "0", 10);
153
+ },
154
+ catch: (error) =>
155
+ QuotaError(
156
+ `Failed to calculate directory size for ${username}`,
157
+ error
158
+ ),
159
+ });
160
+
161
+ return {
162
+ usedMB,
163
+ softLimitMB: 1024, // Default 1GB soft limit
164
+ hardLimitMB: 2048, // Default 2GB hard limit
165
+ percentUsed: (usedMB / 2048) * 100,
166
+ isOverSoftLimit: usedMB > 1024,
167
+ isOverHardLimit: usedMB > 2048,
168
+ };
169
+ }
170
+
171
+ // Get quota usage using quota command
172
+ const usage = yield* Effect.tryPromise({
173
+ try: async () => {
174
+ const output = execSync(`sudo quota -u ${username} --show-mntpoint`, {
175
+ encoding: "utf-8",
176
+ });
177
+
178
+ // Parse quota output
179
+ // Expected format:
180
+ // Disk quotas for user username (uid 1001):
181
+ // Filesystem blocks quota limit grace files quota limit grace
182
+ // /dev/sda1 1024 1024000 2048000 100 10000 15000
183
+
184
+ const lines = output.trim().split("\n");
185
+ if (lines.length < 3) {
186
+ throw new Error("Invalid quota output format");
187
+ }
188
+
189
+ const dataLine = (lines[2] || "").trim().split(/\s+/);
190
+ const usedBlocks = parseInt(dataLine[1] || "0", 10);
191
+ const softBlocks = parseInt(dataLine[2] || "0", 10);
192
+ const hardBlocks = parseInt(dataLine[3] || "0", 10);
193
+
194
+ const usedMB = Math.round(usedBlocks / 1024);
195
+ const softLimitMB = Math.round(softBlocks / 1024);
196
+ const hardLimitMB = Math.round(hardBlocks / 1024);
197
+
198
+ return {
199
+ usedMB,
200
+ softLimitMB,
201
+ hardLimitMB,
202
+ percentUsed: (usedMB / hardLimitMB) * 100,
203
+ isOverSoftLimit: usedMB > softLimitMB,
204
+ isOverHardLimit: usedMB > hardLimitMB,
205
+ };
206
+ },
207
+ catch: (error) =>
208
+ QuotaError(`Failed to get quota usage for ${username}`, error),
209
+ });
210
+
211
+ return usage;
212
+ }),
213
+
214
+ isQuotaAvailable: () =>
215
+ Effect.tryPromise({
216
+ try: async () => {
217
+ try {
218
+ execSync("which quota", { stdio: "ignore" });
219
+ execSync("which setquota", { stdio: "ignore" });
220
+ return true;
221
+ } catch {
222
+ return false;
223
+ }
224
+ },
225
+ catch: (error) => QuotaError("Failed to check quota availability", error),
226
+ }),
227
+
228
+ getDirectorySize: (dirPath: string) =>
229
+ Effect.tryPromise({
230
+ try: async () => {
231
+ const output = execSync(`sudo du -sm ${dirPath}`, {
232
+ encoding: "utf-8",
233
+ });
234
+ return parseInt(output.split("\t")[0] || "0", 10);
235
+ },
236
+ catch: (error) =>
237
+ QuotaError(`Failed to get directory size for ${dirPath}`, error),
238
+ }),
239
+
240
+ checkStorageLimit: (username: string, homeDir: string, limitMB: number) =>
241
+ Effect.gen(function* () {
242
+ const usedMB = yield* Effect.tryPromise({
243
+ try: async () => {
244
+ const output = execSync(`sudo du -sm ${homeDir}`, {
245
+ encoding: "utf-8",
246
+ });
247
+ return parseInt(output.split("\t")[0] || "0", 10);
248
+ },
249
+ catch: (error) =>
250
+ QuotaError(`Failed to check storage for ${username}`, error),
251
+ });
252
+
253
+ return usedMB <= limitMB;
254
+ }),
255
+ });
256
+ }