@tatara/cli 0.0.0-stage → 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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.mjs +2910 -0
  3. package/package.json +32 -3
  4. package/templates/features/admin/files/src/apps/admin/app.ts +4 -0
  5. package/templates/features/admin/files/src/apps/admin/server.ts +5 -0
  6. package/templates/features/admin/files/src/modules/example/routes/admin-greeting.routes.ts +24 -0
  7. package/templates/features/admin/files/src/modules/example/tests/admin.test.ts +20 -0
  8. package/templates/features/auth/files/src/core/auth/middleware.ts +29 -0
  9. package/templates/features/auth/files/src/core/auth/routes/me.routes.ts +36 -0
  10. package/templates/features/auth/files/src/core/auth/tests/me.test.ts +25 -0
  11. package/templates/features/auth-orgs/files+drizzle+mysql/src/core/auth/db/organization.schema.ts +38 -0
  12. package/templates/features/auth-orgs/files+drizzle+postgres/src/core/auth/db/organization.schema.ts +38 -0
  13. package/templates/features/auth-orgs/files+prisma/prisma/models/organization.prisma +41 -0
  14. package/templates/features/authz/files/src/core/authz/cache.ts +31 -0
  15. package/templates/features/authz/files/src/core/authz/catalog.ts +85 -0
  16. package/templates/features/authz/files/src/core/authz/commands/grant-role.ts +21 -0
  17. package/templates/features/authz/files/src/core/authz/commands/sync-permissions.ts +11 -0
  18. package/templates/features/authz/files/src/core/authz/constants.ts +28 -0
  19. package/templates/features/authz/files/src/core/authz/db/seed.ts +10 -0
  20. package/templates/features/authz/files/src/core/authz/middleware.ts +21 -0
  21. package/templates/features/authz/files/src/core/authz/permissions.ts +31 -0
  22. package/templates/features/authz/files/src/core/authz/resolver.ts +98 -0
  23. package/templates/features/authz/files/src/core/authz/routes/authorization.routes.ts +281 -0
  24. package/templates/features/authz/files/src/core/authz/services/audit.ts +24 -0
  25. package/templates/features/authz/files/src/core/authz/services/grants.ts +29 -0
  26. package/templates/features/authz/files/src/core/authz/services/members.ts +68 -0
  27. package/templates/features/authz/files/src/core/authz/services/roles.ts +73 -0
  28. package/templates/features/authz/files/src/core/authz/services/sync.ts +34 -0
  29. package/templates/features/authz/files/src/core/authz/system-roles.ts +9 -0
  30. package/templates/features/authz/files/src/core/authz/tests/authz.test.ts +146 -0
  31. package/templates/features/authz/files/src/core/authz/types.ts +85 -0
  32. package/templates/features/authz/files/src/core/authz/validators.ts +99 -0
  33. package/templates/features/authz/files/tests/helpers/authz.ts +30 -0
  34. package/templates/features/authz/files+drizzle/src/core/authz/db/store.ts +262 -0
  35. package/templates/features/authz/files+drizzle+mysql/src/core/authz/db/authz.schema.ts +90 -0
  36. package/templates/features/authz/files+drizzle+postgres/src/core/authz/db/authz.schema.ts +88 -0
  37. package/templates/features/authz/files+prisma/prisma/models/authz.prisma +75 -0
  38. package/templates/features/authz/files+prisma/src/core/authz/db/store.ts +229 -0
  39. package/templates/features/base/files/AGENTS.md +69 -0
  40. package/templates/features/base/files/CLAUDE.md +1 -0
  41. package/templates/features/base/files/Dockerfile +15 -0
  42. package/templates/features/base/files/README.md +31 -0
  43. package/templates/features/base/files/_github/workflows/ci.yml +16 -0
  44. package/templates/features/base/files/biome.template.json +20 -0
  45. package/templates/features/base/files/dockerignore +8 -0
  46. package/templates/features/base/files/gitignore +9 -0
  47. package/templates/features/base/files/src/apps/api/app.ts +3 -0
  48. package/templates/features/base/files/src/apps/api/server.ts +5 -0
  49. package/templates/features/base/files/src/apps/cli/index.ts +6 -0
  50. package/templates/features/base/files/src/core/commands/define.ts +25 -0
  51. package/templates/features/base/files/src/core/commands/runner.ts +54 -0
  52. package/templates/features/base/files/src/core/http/app.ts +52 -0
  53. package/templates/features/base/files/src/core/http/context.ts +18 -0
  54. package/templates/features/base/files/src/core/http/errors.ts +51 -0
  55. package/templates/features/base/files/src/core/http/health.ts +25 -0
  56. package/templates/features/base/files/src/core/http/middleware/error-handler.ts +36 -0
  57. package/templates/features/base/files/src/core/http/middleware/request-context.ts +24 -0
  58. package/templates/features/base/files/src/core/http/middleware/validation.ts +17 -0
  59. package/templates/features/base/files/src/core/http/response.ts +67 -0
  60. package/templates/features/base/files/src/core/http/routes.ts +28 -0
  61. package/templates/features/base/files/src/core/http/server.ts +15 -0
  62. package/templates/features/base/files/src/core/logging/logger.ts +17 -0
  63. package/templates/features/base/files/src/core/process/lifecycle.ts +34 -0
  64. package/templates/features/base/files/src/modules/example/routes/greeting.routes.ts +27 -0
  65. package/templates/features/base/files/src/modules/example/services/greeting.service.ts +3 -0
  66. package/templates/features/base/files/src/modules/example/tests/greeting.test.ts +21 -0
  67. package/templates/features/base/files/src/modules/example/validators/greeting.validators.ts +9 -0
  68. package/templates/features/base/files/tests/helpers/api.ts +54 -0
  69. package/templates/features/base/files/tests/setup.ts +2 -0
  70. package/templates/features/base/files/tsconfig.json +20 -0
  71. package/templates/features/base/files/tsdown.config.ts +15 -0
  72. package/templates/features/base/files/vitest.config.ts +16 -0
  73. package/templates/features/better-auth/files/src/core/auth/auth.ts +26 -0
  74. package/templates/features/better-auth/files/src/core/auth/db/seed.ts +24 -0
  75. package/templates/features/better-auth/files/src/core/auth/delivery.ts +48 -0
  76. package/templates/features/better-auth/files/src/core/auth/principal.ts +23 -0
  77. package/templates/features/better-auth/files/src/core/auth/sms.ts +14 -0
  78. package/templates/features/better-auth/files/tests/helpers/auth.ts +40 -0
  79. package/templates/features/better-auth/files+drizzle+mysql/src/core/auth/adapter.ts +15 -0
  80. package/templates/features/better-auth/files+drizzle+mysql/src/core/auth/db/auth.schema.ts +57 -0
  81. package/templates/features/better-auth/files+drizzle+postgres/src/core/auth/adapter.ts +15 -0
  82. package/templates/features/better-auth/files+drizzle+postgres/src/core/auth/db/auth.schema.ts +56 -0
  83. package/templates/features/better-auth/files+prisma/prisma/models/auth.prisma +62 -0
  84. package/templates/features/better-auth/files+prisma+mysql/src/core/auth/adapter.ts +4 -0
  85. package/templates/features/better-auth/files+prisma+postgres/src/core/auth/adapter.ts +4 -0
  86. package/templates/features/db/files/src/core/db/commands/migrate.ts +12 -0
  87. package/templates/features/db/files/src/core/db/commands/reset.ts +19 -0
  88. package/templates/features/db/files/src/core/db/commands/seed.ts +13 -0
  89. package/templates/features/db/files/src/core/db/id.ts +13 -0
  90. package/templates/features/db/files/src/core/db/seed.ts +14 -0
  91. package/templates/features/db/files/tests/global-setup.ts +27 -0
  92. package/templates/features/db/files/tests/helpers/database-url.ts +9 -0
  93. package/templates/features/db/files/tests/helpers/db.ts +7 -0
  94. package/templates/features/db/files+mysql+drizzle/src/core/db/admin.ts +53 -0
  95. package/templates/features/db/files+mysql+prisma/src/core/db/admin.ts +78 -0
  96. package/templates/features/db/files+postgres/src/core/db/admin.ts +50 -0
  97. package/templates/features/drizzle/files+mysql/drizzle.config.ts +12 -0
  98. package/templates/features/drizzle/files+mysql/src/core/db/client.ts +20 -0
  99. package/templates/features/drizzle/files+mysql/src/core/db/migrate.ts +13 -0
  100. package/templates/features/drizzle/files+postgres/drizzle.config.ts +12 -0
  101. package/templates/features/drizzle/files+postgres/src/core/db/client.ts +20 -0
  102. package/templates/features/drizzle/files+postgres/src/core/db/migrate.ts +13 -0
  103. package/templates/features/email/files/src/core/email/commands/send-test.ts +13 -0
  104. package/templates/features/email/files/src/core/email/send.ts +22 -0
  105. package/templates/features/email/files/src/core/email/tests/email.test.ts +8 -0
  106. package/templates/features/i18n/files/src/core/i18n/index.ts +41 -0
  107. package/templates/features/i18n/files/src/core/i18n/tests/i18n.test.ts +21 -0
  108. package/templates/features/i18n/files/src/locales/ar.json +10 -0
  109. package/templates/features/i18n/files/src/locales/en.json +10 -0
  110. package/templates/features/jobs/files/src/apps/cron/index.ts +25 -0
  111. package/templates/features/jobs/files/src/apps/worker/index.ts +24 -0
  112. package/templates/features/jobs/files/src/core/jobs/commands/enqueue.ts +13 -0
  113. package/templates/features/jobs/files/src/core/jobs/define.ts +37 -0
  114. package/templates/features/jobs/files/src/core/jobs/lock.ts +17 -0
  115. package/templates/features/jobs/files/src/core/jobs/process.ts +19 -0
  116. package/templates/features/jobs/files/src/core/jobs/queue.ts +29 -0
  117. package/templates/features/jobs/files/src/core/jobs/redis.ts +17 -0
  118. package/templates/features/jobs/files/src/modules/example/consumers/send-welcome.ts +11 -0
  119. package/templates/features/jobs/files/src/modules/example/schedules/heartbeat.ts +9 -0
  120. package/templates/features/jobs/files/src/modules/example/tests/jobs.test.ts +29 -0
  121. package/templates/features/keycloak/files/keycloak/realm.json +83 -0
  122. package/templates/features/keycloak/files/src/core/auth/commands/keycloak-token.ts +32 -0
  123. package/templates/features/keycloak/files/src/core/auth/keycloak.ts +58 -0
  124. package/templates/features/keycloak/files/src/core/auth/principal.ts +27 -0
  125. package/templates/features/keycloak/files/tests/helpers/auth.ts +52 -0
  126. package/templates/features/keycloak/files+drizzle/src/core/auth/users.ts +18 -0
  127. package/templates/features/keycloak/files+drizzle+mysql/src/core/auth/db/user.schema.ts +10 -0
  128. package/templates/features/keycloak/files+drizzle+postgres/src/core/auth/db/user.schema.ts +10 -0
  129. package/templates/features/keycloak/files+prisma/prisma/models/user.prisma +10 -0
  130. package/templates/features/keycloak/files+prisma/src/core/auth/users.ts +13 -0
  131. package/templates/features/prisma/files/prisma/models/.gitkeep +0 -0
  132. package/templates/features/prisma/files/prisma.config.ts +10 -0
  133. package/templates/features/prisma/files/src/core/db/migrate.ts +13 -0
  134. package/templates/features/prisma/files+mysql/prisma/schema.prisma +11 -0
  135. package/templates/features/prisma/files+mysql/src/core/db/client.ts +23 -0
  136. package/templates/features/prisma/files+postgres/prisma/schema.prisma +11 -0
  137. package/templates/features/prisma/files+postgres/src/core/db/client.ts +19 -0
  138. package/templates/features/storage/files/src/core/storage/commands/check.ts +18 -0
  139. package/templates/features/storage/files/src/core/storage/index.ts +8 -0
  140. package/templates/features/storage/files/src/core/storage/local.ts +34 -0
  141. package/templates/features/storage/files/src/core/storage/s3.ts +66 -0
  142. package/templates/features/storage/files/src/core/storage/storage.ts +21 -0
  143. package/templates/features/storage/files/src/core/storage/tests/storage.test.ts +22 -0
  144. package/templates/generators/command/files/src/modules/__moduleKebab__/commands/__nameKebab__.ts +11 -0
  145. package/templates/generators/consumer/files/src/modules/__moduleKebab__/consumers/__nameKebab__.ts +14 -0
  146. package/templates/generators/consumer/files/src/modules/__moduleKebab__/tests/__nameKebab__.consumer.test.ts +12 -0
  147. package/templates/generators/module/files/src/modules/__moduleKebab__/routes/__moduleKebab__.routes.ts +20 -0
  148. package/templates/generators/module/files/src/modules/__moduleKebab__/services/__moduleKebab__.service.ts +4 -0
  149. package/templates/generators/module/files/src/modules/__moduleKebab__/tests/__moduleKebab__.test.ts +10 -0
  150. package/templates/generators/module/files/src/modules/__moduleKebab__/validators/__moduleKebab__.validators.ts +5 -0
  151. package/templates/generators/permission/files/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +18 -0
  152. package/templates/generators/resource/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +126 -0
  153. package/templates/generators/resource/files/src/modules/__moduleKebab__/tests/__nameKebab__.routes.test.ts +59 -0
  154. package/templates/generators/resource/files/src/modules/__moduleKebab__/validators/__nameKebab__.validators.ts +30 -0
  155. package/templates/generators/resource/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +21 -0
  156. package/templates/generators/resource/files+drizzle/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +78 -0
  157. package/templates/generators/resource/files+drizzle+mysql/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
  158. package/templates/generators/resource/files+drizzle+postgres/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
  159. package/templates/generators/resource/files+prisma/prisma/models/__nameKebab__.prisma +9 -0
  160. package/templates/generators/resource/files+prisma/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +73 -0
  161. package/templates/generators/route/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +29 -0
  162. package/templates/generators/route/files/src/modules/__moduleKebab__/validators/__nameKebab__.validators.ts +9 -0
  163. package/templates/generators/route/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +13 -0
  164. package/templates/generators/schedule/files/src/modules/__moduleKebab__/schedules/__nameKebab__.ts +11 -0
  165. package/templates/generators/schema/files+drizzle+mysql/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
  166. package/templates/generators/schema/files+drizzle+postgres/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
  167. package/templates/generators/schema/files+prisma/prisma/models/__nameKebab__.prisma +9 -0
  168. package/templates/generators/service/files/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +4 -0
  169. package/templates/generators/service/files/src/modules/__moduleKebab__/tests/__nameKebab__.service.test.ts +8 -0
  170. package/templates/versions.json +34 -0
  171. package/README.md +0 -3
@@ -0,0 +1,75 @@
1
+ // Permission system tables. scope "" = unscoped; built-in roles have org_id "".
2
+
3
+ model AuthzRole {
4
+ id String @id @db.VarChar(36)
5
+ orgId String @default("") @map("org_id") @db.VarChar(255)
6
+ key String @db.VarChar(64)
7
+ name String @db.VarChar(120)
8
+ description String? @db.Text
9
+ isSystem Boolean @default(false) @map("is_system")
10
+ createdAt DateTime @default(now()) @map("created_at")
11
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
12
+ permissions AuthzRolePermission[]
13
+ members AuthzMemberRole[]
14
+
15
+ @@unique([orgId, key], map: "authz_roles_org_key")
16
+ @@map("authz_roles")
17
+ }
18
+
19
+ model AuthzRolePermission {
20
+ id String @id @db.VarChar(36)
21
+ roleId String @map("role_id") @db.VarChar(36)
22
+ role AuthzRole @relation(fields: [roleId], references: [id], onDelete: Cascade)
23
+ resource String @db.VarChar(64)
24
+ action String @db.VarChar(64)
25
+ scope String @default("") @db.VarChar(16)
26
+
27
+ @@unique([roleId, resource, action, scope], map: "authz_role_permissions_grant")
28
+ @@map("authz_role_permissions")
29
+ }
30
+
31
+ model AuthzMemberRole {
32
+ id String @id @db.VarChar(36)
33
+ orgId String @map("org_id") @db.VarChar(255)
34
+ userId String @map("user_id") @db.VarChar(255)
35
+ roleId String @map("role_id") @db.VarChar(36)
36
+ role AuthzRole @relation(fields: [roleId], references: [id], onDelete: Cascade)
37
+ assignedBy String? @map("assigned_by") @db.VarChar(255)
38
+ assignedAt DateTime @default(now()) @map("assigned_at")
39
+
40
+ @@unique([orgId, userId, roleId], map: "authz_member_roles_member_role")
41
+ @@index([orgId, userId], map: "authz_member_roles_member")
42
+ @@map("authz_member_roles")
43
+ }
44
+
45
+ model AuthzOverlay {
46
+ id String @id @db.VarChar(36)
47
+ orgId String @map("org_id") @db.VarChar(255)
48
+ userId String @map("user_id") @db.VarChar(255)
49
+ resource String @db.VarChar(64)
50
+ action String @db.VarChar(64)
51
+ scope String @default("") @db.VarChar(16)
52
+ effect String @db.VarChar(8)
53
+ grantedBy String? @map("granted_by") @db.VarChar(255)
54
+ reason String? @db.Text
55
+ expiresAt DateTime? @map("expires_at")
56
+ createdAt DateTime @default(now()) @map("created_at")
57
+
58
+ @@unique([orgId, userId, resource, action, scope, effect], map: "authz_overlays_grant")
59
+ @@index([orgId, userId], map: "authz_overlays_member")
60
+ @@map("authz_overlays")
61
+ }
62
+
63
+ model AuthzAuditLog {
64
+ id String @id @db.VarChar(36)
65
+ orgId String @map("org_id") @db.VarChar(255)
66
+ actorId String @map("actor_id") @db.VarChar(255)
67
+ targetUserId String? @map("target_user_id") @db.VarChar(255)
68
+ changeType String @map("change_type") @db.VarChar(48)
69
+ before String? @db.Text
70
+ after String? @db.Text
71
+ at DateTime @default(now())
72
+
73
+ @@index([orgId, at], map: "authz_audit_log_org_at")
74
+ @@map("authz_audit_log")
75
+ }
@@ -0,0 +1,229 @@
1
+ import type { OverlayEffect, Scope } from "#core/authz/constants";
2
+ import { SYSTEM_ORG_ID } from "#core/authz/constants";
3
+ import type { AuthzStore, OverlayRow, PermissionGrant, RoleRow } from "#core/authz/types";
4
+ import { db } from "#core/db/client";
5
+ import { newId } from "#core/db/id";
6
+
7
+ const toScope = (scope: string): Scope | null => (scope === "" ? null : (scope as Scope));
8
+
9
+ interface OverlayRecord {
10
+ id: string;
11
+ orgId: string;
12
+ userId: string;
13
+ resource: string;
14
+ action: string;
15
+ scope: string;
16
+ effect: string;
17
+ grantedBy: string | null;
18
+ reason: string | null;
19
+ expiresAt: Date | null;
20
+ createdAt: Date;
21
+ }
22
+
23
+ function toOverlay(row: OverlayRecord): OverlayRow {
24
+ return { ...row, scope: toScope(row.scope), effect: row.effect as OverlayEffect };
25
+ }
26
+
27
+ const roleInclude = (orgId: string) => ({
28
+ permissions: true,
29
+ _count: { select: { members: { where: { orgId } } } },
30
+ });
31
+
32
+ interface RoleRecord {
33
+ id: string;
34
+ orgId: string;
35
+ key: string;
36
+ name: string;
37
+ description: string | null;
38
+ isSystem: boolean;
39
+ permissions: { resource: string; action: string; scope: string }[];
40
+ _count: { members: number };
41
+ }
42
+
43
+ function toRole(role: RoleRecord): RoleRow {
44
+ return {
45
+ id: role.id,
46
+ orgId: role.orgId,
47
+ key: role.key,
48
+ name: role.name,
49
+ description: role.description,
50
+ isSystem: role.isSystem,
51
+ grants: role.permissions.map((g) => ({
52
+ resource: g.resource,
53
+ action: g.action,
54
+ scope: toScope(g.scope),
55
+ })),
56
+ memberCount: role._count.members,
57
+ };
58
+ }
59
+
60
+ const grantRows = (grants: PermissionGrant[]) =>
61
+ grants.map((g) => ({ id: newId(), resource: g.resource, action: g.action, scope: g.scope ?? "" }));
62
+
63
+ export const store: AuthzStore = {
64
+ async roleGrants(orgId, userId, impliedRoleKeys) {
65
+ const roles = await db.authzRole.findMany({
66
+ where: {
67
+ OR: [
68
+ { members: { some: { orgId, userId } } },
69
+ ...(impliedRoleKeys.length
70
+ ? [{ orgId: SYSTEM_ORG_ID, key: { in: impliedRoleKeys } }]
71
+ : []),
72
+ ],
73
+ },
74
+ include: { permissions: true },
75
+ });
76
+ return {
77
+ roles: roles.map((r) => r.key),
78
+ grants: roles.flatMap((r) =>
79
+ r.permissions.map((g) => ({ resource: g.resource, action: g.action, scope: toScope(g.scope) })),
80
+ ),
81
+ };
82
+ },
83
+
84
+ async activeOverlays(orgId, userId, now) {
85
+ const rows = await db.authzOverlay.findMany({
86
+ where: { orgId, userId, OR: [{ expiresAt: null }, { expiresAt: { gt: now } }] },
87
+ });
88
+ return rows.map(toOverlay);
89
+ },
90
+
91
+ async listRoles(orgId) {
92
+ const roles = await db.authzRole.findMany({
93
+ where: { orgId: { in: [SYSTEM_ORG_ID, orgId] } },
94
+ include: roleInclude(orgId),
95
+ orderBy: { key: "asc" },
96
+ });
97
+ return roles.map(toRole);
98
+ },
99
+
100
+ async findRole(orgId, roleId) {
101
+ const role = await db.authzRole.findFirst({
102
+ where: { id: roleId, orgId: { in: [SYSTEM_ORG_ID, orgId] } },
103
+ include: roleInclude(orgId),
104
+ });
105
+ return role ? toRole(role) : null;
106
+ },
107
+
108
+ async findRoleByKey(orgId, key) {
109
+ const role = await db.authzRole.findFirst({ where: { key, orgId }, include: roleInclude(orgId) });
110
+ return role ? toRole(role) : null;
111
+ },
112
+
113
+ async createRole(input) {
114
+ const id = newId();
115
+ await db.authzRole.create({
116
+ data: {
117
+ id,
118
+ orgId: input.orgId,
119
+ key: input.key,
120
+ name: input.name,
121
+ description: input.description,
122
+ isSystem: input.isSystem,
123
+ permissions: { create: grantRows(input.grants) },
124
+ },
125
+ });
126
+ return id;
127
+ },
128
+
129
+ async updateRole(roleId, input) {
130
+ await db.$transaction(async (tx) => {
131
+ await tx.authzRole.update({
132
+ where: { id: roleId },
133
+ data: {
134
+ ...(input.name === undefined ? {} : { name: input.name }),
135
+ ...(input.description === undefined ? {} : { description: input.description }),
136
+ },
137
+ });
138
+ if (input.grants) {
139
+ await tx.authzRolePermission.deleteMany({ where: { roleId } });
140
+ await tx.authzRolePermission.createMany({
141
+ data: grantRows(input.grants).map((g) => ({ ...g, roleId })),
142
+ });
143
+ }
144
+ });
145
+ },
146
+
147
+ async deleteRole(roleId) {
148
+ await db.authzRole.delete({ where: { id: roleId } });
149
+ },
150
+
151
+ async memberRoles(orgId, userId) {
152
+ const roles = await db.authzRole.findMany({
153
+ where: { members: { some: { orgId, userId } } },
154
+ include: roleInclude(orgId),
155
+ });
156
+ return roles.map(toRole);
157
+ },
158
+
159
+ async assignRole(orgId, userId, roleId, by) {
160
+ const existing = await db.authzMemberRole.findFirst({ where: { orgId, userId, roleId } });
161
+ if (existing) return false;
162
+ await db.authzMemberRole.create({ data: { id: newId(), orgId, userId, roleId, assignedBy: by } });
163
+ return true;
164
+ },
165
+
166
+ async revokeRole(orgId, userId, roleId) {
167
+ const { count } = await db.authzMemberRole.deleteMany({ where: { orgId, userId, roleId } });
168
+ return count > 0;
169
+ },
170
+
171
+ async listOverlays(orgId, userId) {
172
+ const rows = await db.authzOverlay.findMany({
173
+ where: { orgId, userId },
174
+ orderBy: { createdAt: "desc" },
175
+ });
176
+ return rows.map(toOverlay);
177
+ },
178
+
179
+ async addOverlay(input) {
180
+ const id = newId();
181
+ const scope = input.scope ?? "";
182
+ await db.authzOverlay.deleteMany({
183
+ where: {
184
+ orgId: input.orgId,
185
+ userId: input.userId,
186
+ resource: input.resource,
187
+ action: input.action,
188
+ scope,
189
+ effect: input.effect,
190
+ },
191
+ });
192
+ await db.authzOverlay.create({ data: { ...input, id, scope } });
193
+ return id;
194
+ },
195
+
196
+ async removeOverlay(orgId, userId, overlayId) {
197
+ const row = await db.authzOverlay.findFirst({ where: { id: overlayId, orgId, userId } });
198
+ if (!row) return null;
199
+ await db.authzOverlay.deleteMany({ where: { id: overlayId, orgId, userId } });
200
+ return toOverlay(row);
201
+ },
202
+
203
+ async writeAudit(input) {
204
+ await db.authzAuditLog.create({
205
+ data: {
206
+ id: newId(),
207
+ orgId: input.orgId,
208
+ actorId: input.actorId,
209
+ targetUserId: input.targetUserId,
210
+ changeType: input.changeType,
211
+ before: input.before === undefined ? null : JSON.stringify(input.before),
212
+ after: input.after === undefined ? null : JSON.stringify(input.after),
213
+ },
214
+ });
215
+ },
216
+
217
+ async listAudit(orgId, limit) {
218
+ const rows = await db.authzAuditLog.findMany({
219
+ where: { orgId },
220
+ orderBy: { id: "desc" },
221
+ take: limit,
222
+ });
223
+ return rows.map((r) => ({
224
+ ...r,
225
+ before: r.before ? (JSON.parse(r.before) as unknown) : null,
226
+ after: r.after ? (JSON.parse(r.after) as unknown) : null,
227
+ }));
228
+ },
229
+ };
@@ -0,0 +1,69 @@
1
+ # __APP_NAME__ — working agreement
2
+
3
+ Read this before changing code. It applies to people and coding agents alike.
4
+
5
+ ## Definition of done
6
+
7
+ - A change is done when `__PM_RUN__ verify` exits 0 (lint + layout + typecheck + tests + build).
8
+ - Never disable, skip or weaken a test, a type or a lint rule to make it pass. Fix the code.
9
+ - Report what you ran and its result, not that something "should work".
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ src/apps/<name>/ one folder per process; thin wiring only
15
+ src/core/<area>/ shared infrastructure; never imports from modules
16
+ src/modules/<module>/ business code, one folder per domain
17
+ src/generated/ written by `tatara sync`; never edit or commit
18
+ tests/ shared test helpers and setup
19
+ ```
20
+
21
+ A module holds `routes/`, `services/`, `validators/`, and optionally `db/`, `commands/`
22
+ and other folders described below. Business logic lives in `services/`: plain functions
23
+ with no knowledge of Hono. Routes validate input, call services and shape the response.
24
+
25
+ Rules enforced by `tatara check` (part of lint):
26
+
27
+ - Import with `#core/…`, `#modules/…`, `#apps/…`, `#generated/…`, `#tests/…`; no `../` imports.
28
+ - `src/core` never imports from `src/modules`.
29
+ - A module uses another module only through its `services/`.
30
+ - Module routers never call `use("*")`; attach middleware per route or per path.
31
+ - `process.env` is read only in `src/core/config/env.ts`.
32
+
33
+ ## Generators
34
+
35
+ Prefer generators to hand-writing boilerplate; they only add files and never overwrite:
36
+
37
+ ```
38
+ __PM_EXEC__ tatara make module <name>
39
+ __PM_EXEC__ tatara make resource <name> --module <module> # CRUD slice
40
+ __PM_EXEC__ tatara make route|service|command <name> --module <module>
41
+ __PM_EXEC__ tatara run --list # app commands
42
+ __PM_EXEC__ tatara routes # every route and what protects it
43
+ ```
44
+
45
+ ## HTTP
46
+
47
+ - Every route uses `createRoute` from `@hono/zod-openapi`, with request and response schemas.
48
+ The spec is served at `/openapi.json`, the reference UI at `/docs`.
49
+ - Responses use the envelope: `success(data)`, `paginated(items, page, pageSize, total)`.
50
+ Errors are thrown as `AppError` subclasses (`NotFoundError`, `ConflictError`,
51
+ `BusinessRuleError`, `ForbiddenError`, …) and become
52
+ `{ success: false, error, code, details }`.
53
+ - Route files are `routes/<name>.routes.ts` and default-export `defineRoutes({ app, basePath, router })`.
54
+ They are found automatically; there is no central list to edit.
55
+
56
+ ## Configuration and logging
57
+
58
+ - Add environment variables to `src/core/config/env.ts` (validated at startup) and to
59
+ `.env.example`. Never read `process.env` elsewhere.
60
+ - Log with `c.get("logger")` in requests and `logger` from `#core/logging/logger` elsewhere.
61
+ No `console.*` outside commands and tests.
62
+
63
+ ## Tests
64
+
65
+ - Tests live next to the code in `src/modules/<module>/tests/`. Use `api` from
66
+ `#tests/helpers/api` to call routes in-process.
67
+ - Test behaviour through the public surface (routes, services); mock only external services.
68
+
69
+ <!-- tatara:slot sections -->
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,15 @@
1
+ # One image for every entrypoint. Default: the API.
2
+ # Other processes: override the command, e.g. `node dist/apps/worker/index.mjs`.
3
+ FROM node:24-alpine AS base
4
+ WORKDIR /app
5
+ # tatara:slot docker-base
6
+
7
+ FROM base AS build
8
+ # tatara:slot docker-build
9
+
10
+ FROM base AS runtime
11
+ ENV NODE_ENV=production
12
+ # tatara:slot docker-runtime
13
+ USER node
14
+ EXPOSE 3000
15
+ CMD ["node", "dist/apps/api/server.mjs"]
@@ -0,0 +1,31 @@
1
+ # __APP_NAME__
2
+
3
+ A Hono API created with [tatara](https://github.com/mohammed-bageri/tatara).
4
+
5
+ ## Getting started
6
+
7
+ ```sh
8
+ __PM_RUN__ dev
9
+ ```
10
+
11
+ This starts the local services, prepares the database and runs every process with
12
+ reload. Then open:
13
+
14
+ - API: http://localhost:3000/api
15
+ - API reference: http://localhost:3000/docs
16
+ - Health: http://localhost:3000/health
17
+
18
+ ## Scripts
19
+
20
+ | Script | What it does |
21
+ |---|---|
22
+ | `dev` | Start local services and every process in watch mode |
23
+ | `dev:down` / `dev:reset` | Stop local services / stop and delete their data |
24
+ | `verify` | Lint, layout checks, typecheck, tests and build |
25
+ | `test` | Run tests |
26
+ | `build` | Bundle to `dist/` |
27
+ | `start` | Run the built API |
28
+
29
+ Conventions are in [AGENTS.md](./AGENTS.md).
30
+
31
+ <!-- tatara:slot readme -->
@@ -0,0 +1,16 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ verify:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ # tatara:slot ci-setup
14
+ - run: cp .env.example .env
15
+ # tatara:slot ci-services
16
+ - run: __PM_RUN__ verify
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/__BIOME_VERSION__/schema.json",
3
+ "files": {
4
+ "includes": ["**", "!**/node_modules", "!**/dist", "!**/coverage", "!src/generated", "!drizzle", "!prisma/migrations"]
5
+ },
6
+ "formatter": { "indentStyle": "tab", "lineWidth": 100 },
7
+ "javascript": { "formatter": { "quoteStyle": "double" } },
8
+ "linter": {
9
+ "rules": {
10
+ "style": { "useImportType": "error" },
11
+ "suspicious": { "noConsole": "error" }
12
+ }
13
+ },
14
+ "overrides": [
15
+ {
16
+ "includes": ["src/apps/cli/**", "src/**/commands/**", "tests/**", "*.config.ts"],
17
+ "linter": { "rules": { "suspicious": { "noConsole": "off" } } }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ coverage
4
+ src/generated
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+ .git
@@ -0,0 +1,9 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ src/generated/
5
+ .env
6
+ .env.local
7
+ *.log
8
+ .DS_Store
9
+ # tatara:slot ignore
@@ -0,0 +1,3 @@
1
+ import { createHttpApp } from "#core/http/app";
2
+
3
+ export const app = createHttpApp("api", { title: "__APP_NAME__ API" });
@@ -0,0 +1,5 @@
1
+ import { app } from "#apps/api/app";
2
+ import { env } from "#core/config/env";
3
+ import { startServer } from "#core/http/server";
4
+
5
+ startServer(app, env.PORT, "api");
@@ -0,0 +1,6 @@
1
+ import { runCommand } from "#core/commands/runner";
2
+ import { shutdown } from "#core/process/lifecycle";
3
+ import { commands } from "#generated/commands";
4
+
5
+ const code = await runCommand(commands, process.argv.slice(2));
6
+ await shutdown(code);
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import type { Logger } from "#core/logging/logger";
3
+
4
+ export interface CommandContext {
5
+ logger: Logger;
6
+ }
7
+
8
+ export interface CommandDefinition<S extends z.ZodType = z.ZodType> {
9
+ /** `module:action`, e.g. "billing:recalculate" */
10
+ name: string;
11
+ description: string;
12
+ /** flags parsed from `--key value`; use z.coerce for numbers and booleans */
13
+ args?: S;
14
+ run(args: z.infer<S>, ctx: CommandContext): Promise<void> | void;
15
+ }
16
+
17
+
18
+ /** Default export of every `commands/*.ts`; discovered by `tatara sync`. */
19
+ export function defineCommand<S extends z.ZodType = typeof noArgs>(
20
+ definition: CommandDefinition<S>,
21
+ ): CommandDefinition<S> {
22
+ return definition;
23
+ }
24
+
25
+ export const noArgs = z.object({});
@@ -0,0 +1,54 @@
1
+ import type { CommandDefinition } from "#core/commands/define";
2
+ import { logger } from "#core/logging/logger";
3
+
4
+ /** Parses `--key value`, `--key=value` and bare `--flag` (true) into an object. */
5
+ export function parseFlags(argv: string[]): Record<string, string | boolean> {
6
+ const flags: Record<string, string | boolean> = {};
7
+ for (let i = 0; i < argv.length; i++) {
8
+ const arg = argv[i] ?? "";
9
+ if (!arg.startsWith("--")) continue;
10
+ const [rawKey, inline] = arg.slice(2).split("=", 2);
11
+ const key = (rawKey ?? "").replace(/-([a-z])/g, (_, ch: string) => ch.toUpperCase());
12
+ if (inline !== undefined) {
13
+ flags[key] = inline;
14
+ } else if (argv[i + 1] !== undefined && !argv[i + 1]?.startsWith("--")) {
15
+ flags[key] = argv[i + 1] ?? "";
16
+ i++;
17
+ } else {
18
+ flags[key] = true;
19
+ }
20
+ }
21
+ return flags;
22
+ }
23
+
24
+ export async function runCommand(commands: CommandDefinition[], argv: string[]): Promise<number> {
25
+ const [name, ...rest] = argv;
26
+ if (!name || name === "--list" || name === "list" || name === "help") {
27
+ const width = Math.max(0, ...commands.map((c) => c.name.length));
28
+ console.log("Commands:");
29
+ for (const c of [...commands].sort((a, b) => a.name.localeCompare(b.name))) {
30
+ console.log(` ${c.name.padEnd(width)} ${c.description}`);
31
+ }
32
+ return 0;
33
+ }
34
+ const command = commands.find((c) => c.name === name);
35
+ if (!command) {
36
+ console.error(`Unknown command "${name}". Run with --list to see available commands.`);
37
+ return 1;
38
+ }
39
+ const parsed = (command.args ?? null)?.safeParse(parseFlags(rest));
40
+ if (parsed && !parsed.success) {
41
+ console.error(`Invalid arguments for ${name}:`);
42
+ for (const issue of parsed.error.issues) {
43
+ console.error(` --${issue.path.join(".")}: ${issue.message}`);
44
+ }
45
+ return 1;
46
+ }
47
+ try {
48
+ await command.run(parsed ? parsed.data : {}, { logger: logger.child({ command: name }) });
49
+ return 0;
50
+ } catch (error) {
51
+ logger.error({ err: error, command: name }, "command failed");
52
+ return 1;
53
+ }
54
+ }
@@ -0,0 +1,52 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { Scalar } from "@scalar/hono-api-reference";
3
+ import { bodyLimit } from "hono/body-limit";
4
+ import { cors } from "hono/cors";
5
+ import { secureHeaders } from "hono/secure-headers";
6
+ import { env } from "#core/config/env";
7
+ import type { AppEnv } from "#core/http/context";
8
+ import { runReadinessChecks } from "#core/http/health";
9
+ import { errorHandler, notFoundHandler } from "#core/http/middleware/error-handler";
10
+ import { requestContext } from "#core/http/middleware/request-context";
11
+ import { validationHook } from "#core/http/middleware/validation";
12
+ import type { AppName } from "#core/http/routes";
13
+ import { routes } from "#generated/routes";
14
+ // tatara:slot imports
15
+
16
+ export const API_PREFIX = "/api";
17
+
18
+ export function createHttpApp(name: AppName, options: { title: string }) {
19
+ const app = new OpenAPIHono<AppEnv>({ defaultHook: validationHook });
20
+
21
+ app.use(requestContext);
22
+ app.use(secureHeaders());
23
+ app.use(
24
+ cors({
25
+ origin: env.CORS_ORIGINS,
26
+ credentials: true,
27
+ allowHeaders: ["Content-Type", "Authorization", "X-Request-Id", "Accept-Language"],
28
+ exposeHeaders: ["X-Request-Id"],
29
+ }),
30
+ );
31
+ app.use(bodyLimit({ maxSize: env.BODY_LIMIT_BYTES }));
32
+ // tatara:slot middleware
33
+
34
+ app.get("/health", (c) => c.json({ status: "ok" }));
35
+ app.get("/ready", async (c) => {
36
+ const result = await runReadinessChecks();
37
+ return c.json(result, result.ready ? 200 : 503);
38
+ });
39
+
40
+ // tatara:slot routes
41
+
42
+ for (const definition of routes) {
43
+ if (definition.app === name) app.route(`${API_PREFIX}${definition.basePath}`, definition.router);
44
+ }
45
+
46
+ app.doc31("/openapi.json", { openapi: "3.1.0", info: { title: options.title, version: "1.0.0" } });
47
+ app.get("/docs", Scalar({ url: "/openapi.json", pageTitle: options.title }));
48
+
49
+ app.onError(errorHandler);
50
+ app.notFound(notFoundHandler);
51
+ return app;
52
+ }
@@ -0,0 +1,18 @@
1
+ import type { Logger } from "#core/logging/logger";
2
+
3
+ export interface Principal {
4
+ userId: string;
5
+ orgId: string | null;
6
+ roles: string[];
7
+ kind: "user" | "service";
8
+ claims: Record<string, unknown>;
9
+ }
10
+
11
+ export interface AppEnv {
12
+ Variables: {
13
+ requestId: string;
14
+ logger: Logger;
15
+ principal: Principal | null;
16
+ language: string;
17
+ };
18
+ }