@tatara/cli 0.0.0-stage → 0.1.1

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 (181) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.mjs +3001 -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 +32 -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 +84 -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/tests/error-messages.test.ts +34 -0
  32. package/templates/features/authz/files/src/core/authz/tests/request-body.test.ts +27 -0
  33. package/templates/features/authz/files/src/core/authz/types.ts +87 -0
  34. package/templates/features/authz/files/src/core/authz/validators.ts +99 -0
  35. package/templates/features/authz/files/tests/helpers/authz.ts +30 -0
  36. package/templates/features/authz/files+drizzle/src/core/authz/db/store.ts +275 -0
  37. package/templates/features/authz/files+drizzle+mysql/src/core/authz/db/authz.schema.ts +90 -0
  38. package/templates/features/authz/files+drizzle+postgres/src/core/authz/db/authz.schema.ts +88 -0
  39. package/templates/features/authz/files+prisma/prisma/models/authz.prisma +75 -0
  40. package/templates/features/authz/files+prisma/src/core/authz/db/store.ts +236 -0
  41. package/templates/features/base/files/AGENTS.md +71 -0
  42. package/templates/features/base/files/CLAUDE.md +1 -0
  43. package/templates/features/base/files/Dockerfile +16 -0
  44. package/templates/features/base/files/README.md +31 -0
  45. package/templates/features/base/files/_github/workflows/ci.yml +26 -0
  46. package/templates/features/base/files/biome.template.json +20 -0
  47. package/templates/features/base/files/dockerignore +8 -0
  48. package/templates/features/base/files/gitignore +9 -0
  49. package/templates/features/base/files/src/apps/api/app.ts +3 -0
  50. package/templates/features/base/files/src/apps/api/server.ts +5 -0
  51. package/templates/features/base/files/src/apps/cli/index.ts +6 -0
  52. package/templates/features/base/files/src/core/commands/define.ts +33 -0
  53. package/templates/features/base/files/src/core/commands/runner.ts +54 -0
  54. package/templates/features/base/files/src/core/commands/tests/flags.test.ts +16 -0
  55. package/templates/features/base/files/src/core/config/tests/env.test.ts +40 -0
  56. package/templates/features/base/files/src/core/http/app.ts +57 -0
  57. package/templates/features/base/files/src/core/http/context.ts +18 -0
  58. package/templates/features/base/files/src/core/http/errors.ts +79 -0
  59. package/templates/features/base/files/src/core/http/health.ts +25 -0
  60. package/templates/features/base/files/src/core/http/middleware/error-handler.ts +36 -0
  61. package/templates/features/base/files/src/core/http/middleware/request-context.ts +24 -0
  62. package/templates/features/base/files/src/core/http/middleware/validation.ts +17 -0
  63. package/templates/features/base/files/src/core/http/response.ts +72 -0
  64. package/templates/features/base/files/src/core/http/routes.ts +28 -0
  65. package/templates/features/base/files/src/core/http/server.ts +15 -0
  66. package/templates/features/base/files/src/core/logging/logger.ts +17 -0
  67. package/templates/features/base/files/src/core/process/lifecycle.ts +34 -0
  68. package/templates/features/base/files/src/modules/example/routes/greeting.routes.ts +27 -0
  69. package/templates/features/base/files/src/modules/example/services/greeting.service.ts +3 -0
  70. package/templates/features/base/files/src/modules/example/tests/greeting.test.ts +21 -0
  71. package/templates/features/base/files/src/modules/example/validators/greeting.validators.ts +9 -0
  72. package/templates/features/base/files/tests/helpers/api.ts +54 -0
  73. package/templates/features/base/files/tests/setup.ts +2 -0
  74. package/templates/features/base/files/tsconfig.json +20 -0
  75. package/templates/features/base/files/tsdown.config.ts +15 -0
  76. package/templates/features/base/files/vitest.config.ts +16 -0
  77. package/templates/features/better-auth/files/src/core/auth/auth.ts +26 -0
  78. package/templates/features/better-auth/files/src/core/auth/db/seed.ts +24 -0
  79. package/templates/features/better-auth/files/src/core/auth/delivery.ts +48 -0
  80. package/templates/features/better-auth/files/src/core/auth/openapi.ts +60 -0
  81. package/templates/features/better-auth/files/src/core/auth/principal.ts +40 -0
  82. package/templates/features/better-auth/files/src/core/auth/sms.ts +14 -0
  83. package/templates/features/better-auth/files/src/core/auth/tests/openapi.test.ts +45 -0
  84. package/templates/features/better-auth/files/tests/helpers/auth.ts +40 -0
  85. package/templates/features/better-auth/files+admin/src/core/auth/tests/openapi-admin.test.ts +8 -0
  86. package/templates/features/better-auth/files+auth-orgs/src/core/auth/tests/principal.test.ts +44 -0
  87. package/templates/features/better-auth/files+auth-orgs+authz/src/core/auth/tests/principal-authz.test.ts +40 -0
  88. package/templates/features/better-auth/files+drizzle+mysql/src/core/auth/adapter.ts +15 -0
  89. package/templates/features/better-auth/files+drizzle+mysql/src/core/auth/db/auth.schema.ts +57 -0
  90. package/templates/features/better-auth/files+drizzle+postgres/src/core/auth/adapter.ts +15 -0
  91. package/templates/features/better-auth/files+drizzle+postgres/src/core/auth/db/auth.schema.ts +56 -0
  92. package/templates/features/better-auth/files+prisma/prisma/models/auth.prisma +62 -0
  93. package/templates/features/better-auth/files+prisma+mysql/src/core/auth/adapter.ts +4 -0
  94. package/templates/features/better-auth/files+prisma+postgres/src/core/auth/adapter.ts +4 -0
  95. package/templates/features/db/files/src/core/db/commands/migrate.ts +12 -0
  96. package/templates/features/db/files/src/core/db/commands/reset.ts +19 -0
  97. package/templates/features/db/files/src/core/db/commands/seed.ts +13 -0
  98. package/templates/features/db/files/src/core/db/id.ts +13 -0
  99. package/templates/features/db/files/src/core/db/seed.ts +14 -0
  100. package/templates/features/db/files/src/core/db/tests/reset.test.ts +7 -0
  101. package/templates/features/db/files/tests/global-setup.ts +27 -0
  102. package/templates/features/db/files/tests/helpers/database-url.ts +9 -0
  103. package/templates/features/db/files/tests/helpers/db.ts +7 -0
  104. package/templates/features/db/files+mysql+drizzle/src/core/db/admin.ts +53 -0
  105. package/templates/features/db/files+mysql+prisma/src/core/db/admin.ts +78 -0
  106. package/templates/features/db/files+postgres/src/core/db/admin.ts +50 -0
  107. package/templates/features/drizzle/files+mysql/drizzle.config.ts +12 -0
  108. package/templates/features/drizzle/files+mysql/src/core/db/client.ts +20 -0
  109. package/templates/features/drizzle/files+mysql/src/core/db/migrate.ts +13 -0
  110. package/templates/features/drizzle/files+postgres/drizzle.config.ts +12 -0
  111. package/templates/features/drizzle/files+postgres/src/core/db/client.ts +20 -0
  112. package/templates/features/drizzle/files+postgres/src/core/db/migrate.ts +13 -0
  113. package/templates/features/email/files/src/core/email/commands/send-test.ts +13 -0
  114. package/templates/features/email/files/src/core/email/send.ts +22 -0
  115. package/templates/features/email/files/src/core/email/tests/email.test.ts +8 -0
  116. package/templates/features/i18n/files/src/core/i18n/index.ts +41 -0
  117. package/templates/features/i18n/files/src/core/i18n/tests/i18n.test.ts +21 -0
  118. package/templates/features/i18n/files/src/locales/ar.json +10 -0
  119. package/templates/features/i18n/files/src/locales/en.json +10 -0
  120. package/templates/features/jobs/files/src/apps/cron/index.ts +25 -0
  121. package/templates/features/jobs/files/src/apps/worker/index.ts +24 -0
  122. package/templates/features/jobs/files/src/core/jobs/commands/enqueue.ts +13 -0
  123. package/templates/features/jobs/files/src/core/jobs/define.ts +37 -0
  124. package/templates/features/jobs/files/src/core/jobs/lock.ts +17 -0
  125. package/templates/features/jobs/files/src/core/jobs/process.ts +19 -0
  126. package/templates/features/jobs/files/src/core/jobs/queue.ts +29 -0
  127. package/templates/features/jobs/files/src/core/jobs/redis.ts +17 -0
  128. package/templates/features/jobs/files/src/modules/example/consumers/send-welcome.ts +11 -0
  129. package/templates/features/jobs/files/src/modules/example/schedules/heartbeat.ts +9 -0
  130. package/templates/features/jobs/files/src/modules/example/tests/jobs.test.ts +29 -0
  131. package/templates/features/keycloak/files/keycloak/realm.json +83 -0
  132. package/templates/features/keycloak/files/src/core/auth/commands/keycloak-token.ts +32 -0
  133. package/templates/features/keycloak/files/src/core/auth/keycloak.ts +58 -0
  134. package/templates/features/keycloak/files/src/core/auth/principal.ts +27 -0
  135. package/templates/features/keycloak/files/tests/helpers/auth.ts +52 -0
  136. package/templates/features/keycloak/files+drizzle/src/core/auth/users.ts +18 -0
  137. package/templates/features/keycloak/files+drizzle+mysql/src/core/auth/db/user.schema.ts +10 -0
  138. package/templates/features/keycloak/files+drizzle+postgres/src/core/auth/db/user.schema.ts +10 -0
  139. package/templates/features/keycloak/files+prisma/prisma/models/user.prisma +10 -0
  140. package/templates/features/keycloak/files+prisma/src/core/auth/users.ts +13 -0
  141. package/templates/features/prisma/files/prisma/models/.gitkeep +0 -0
  142. package/templates/features/prisma/files/prisma.config.ts +10 -0
  143. package/templates/features/prisma/files/src/core/db/migrate.ts +13 -0
  144. package/templates/features/prisma/files+mysql/prisma/schema.prisma +11 -0
  145. package/templates/features/prisma/files+mysql/src/core/db/client.ts +23 -0
  146. package/templates/features/prisma/files+postgres/prisma/schema.prisma +11 -0
  147. package/templates/features/prisma/files+postgres/src/core/db/client.ts +19 -0
  148. package/templates/features/storage/files/src/core/storage/commands/check.ts +18 -0
  149. package/templates/features/storage/files/src/core/storage/index.ts +8 -0
  150. package/templates/features/storage/files/src/core/storage/local.ts +34 -0
  151. package/templates/features/storage/files/src/core/storage/s3.ts +66 -0
  152. package/templates/features/storage/files/src/core/storage/storage.ts +21 -0
  153. package/templates/features/storage/files/src/core/storage/tests/storage.test.ts +22 -0
  154. package/templates/generators/command/files/src/modules/__moduleKebab__/commands/__nameKebab__.ts +11 -0
  155. package/templates/generators/consumer/files/src/modules/__moduleKebab__/consumers/__nameKebab__.ts +14 -0
  156. package/templates/generators/consumer/files/src/modules/__moduleKebab__/tests/__nameKebab__.consumer.test.ts +12 -0
  157. package/templates/generators/module/files/src/modules/__moduleKebab__/routes/__moduleKebab__.routes.ts +20 -0
  158. package/templates/generators/module/files/src/modules/__moduleKebab__/services/__moduleKebab__.service.ts +4 -0
  159. package/templates/generators/module/files/src/modules/__moduleKebab__/tests/__moduleKebab__.test.ts +10 -0
  160. package/templates/generators/module/files/src/modules/__moduleKebab__/validators/__moduleKebab__.validators.ts +5 -0
  161. package/templates/generators/permission/files/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +18 -0
  162. package/templates/generators/resource/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +127 -0
  163. package/templates/generators/resource/files/src/modules/__moduleKebab__/tests/__nameKebab__.routes.test.ts +59 -0
  164. package/templates/generators/resource/files/src/modules/__moduleKebab__/validators/__nameKebab__.validators.ts +30 -0
  165. package/templates/generators/resource/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +21 -0
  166. package/templates/generators/resource/files+drizzle/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +78 -0
  167. package/templates/generators/resource/files+drizzle+mysql/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
  168. package/templates/generators/resource/files+drizzle+postgres/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
  169. package/templates/generators/resource/files+prisma/prisma/models/__nameKebab__.prisma +9 -0
  170. package/templates/generators/resource/files+prisma/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +73 -0
  171. package/templates/generators/route/files/src/modules/__moduleKebab__/routes/__nameKebab__.routes.ts +29 -0
  172. package/templates/generators/route/files/src/modules/__moduleKebab__/validators/__nameKebab__.validators.ts +9 -0
  173. package/templates/generators/route/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +13 -0
  174. package/templates/generators/schedule/files/src/modules/__moduleKebab__/schedules/__nameKebab__.ts +11 -0
  175. package/templates/generators/schema/files+drizzle+mysql/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
  176. package/templates/generators/schema/files+drizzle+postgres/src/modules/__moduleKebab__/db/__nameKebab__.schema.ts +9 -0
  177. package/templates/generators/schema/files+prisma/prisma/models/__nameKebab__.prisma +9 -0
  178. package/templates/generators/service/files/src/modules/__moduleKebab__/services/__nameKebab__.service.ts +4 -0
  179. package/templates/generators/service/files/src/modules/__moduleKebab__/tests/__nameKebab__.service.test.ts +8 -0
  180. package/templates/versions.json +34 -0
  181. package/README.md +0 -3
@@ -0,0 +1,88 @@
1
+ import { boolean, index, pgTable, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/pg-core";
2
+
3
+ const ts = () => timestamp({ withTimezone: true, mode: "date" });
4
+
5
+ /** Built-in roles have orgId "" (they apply to every org); custom roles belong to one org. */
6
+ export const authzRoles = pgTable(
7
+ "authz_roles",
8
+ {
9
+ id: text().primaryKey(),
10
+ orgId: text().notNull().default(""),
11
+ key: varchar({ length: 64 }).notNull(),
12
+ name: varchar({ length: 120 }).notNull(),
13
+ description: text(),
14
+ isSystem: boolean().notNull().default(false),
15
+ createdAt: ts().notNull().defaultNow(),
16
+ updatedAt: ts().notNull().defaultNow(),
17
+ },
18
+ (t) => [uniqueIndex("authz_roles_org_key").on(t.orgId, t.key)],
19
+ );
20
+
21
+ /** scope "" means the action is unscoped (keeps unique keys identical on every database). */
22
+ export const authzRolePermissions = pgTable(
23
+ "authz_role_permissions",
24
+ {
25
+ id: text().primaryKey(),
26
+ roleId: text()
27
+ .notNull()
28
+ .references(() => authzRoles.id, { onDelete: "cascade" }),
29
+ resource: varchar({ length: 64 }).notNull(),
30
+ action: varchar({ length: 64 }).notNull(),
31
+ scope: varchar({ length: 16 }).notNull().default(""),
32
+ },
33
+ (t) => [uniqueIndex("authz_role_permissions_grant").on(t.roleId, t.resource, t.action, t.scope)],
34
+ );
35
+
36
+ export const authzMemberRoles = pgTable(
37
+ "authz_member_roles",
38
+ {
39
+ id: text().primaryKey(),
40
+ orgId: text().notNull(),
41
+ userId: text().notNull(),
42
+ roleId: text()
43
+ .notNull()
44
+ .references(() => authzRoles.id, { onDelete: "cascade" }),
45
+ assignedBy: text(),
46
+ assignedAt: ts().notNull().defaultNow(),
47
+ },
48
+ (t) => [
49
+ uniqueIndex("authz_member_roles_member_role").on(t.orgId, t.userId, t.roleId),
50
+ index("authz_member_roles_member").on(t.orgId, t.userId),
51
+ ],
52
+ );
53
+
54
+ export const authzOverlays = pgTable(
55
+ "authz_overlays",
56
+ {
57
+ id: text().primaryKey(),
58
+ orgId: text().notNull(),
59
+ userId: text().notNull(),
60
+ resource: varchar({ length: 64 }).notNull(),
61
+ action: varchar({ length: 64 }).notNull(),
62
+ scope: varchar({ length: 16 }).notNull().default(""),
63
+ effect: varchar({ length: 8 }).notNull(),
64
+ grantedBy: text(),
65
+ reason: text(),
66
+ expiresAt: ts(),
67
+ createdAt: ts().notNull().defaultNow(),
68
+ },
69
+ (t) => [
70
+ uniqueIndex("authz_overlays_grant").on(t.orgId, t.userId, t.resource, t.action, t.scope, t.effect),
71
+ index("authz_overlays_member").on(t.orgId, t.userId),
72
+ ],
73
+ );
74
+
75
+ export const authzAuditLog = pgTable(
76
+ "authz_audit_log",
77
+ {
78
+ id: text().primaryKey(),
79
+ orgId: text().notNull(),
80
+ actorId: text().notNull(),
81
+ targetUserId: text(),
82
+ changeType: varchar({ length: 48 }).notNull(),
83
+ before: text(),
84
+ after: text(),
85
+ at: ts().notNull().defaultNow(),
86
+ },
87
+ (t) => [index("authz_audit_log_org_at").on(t.orgId, t.at)],
88
+ );
@@ -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,236 @@
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 removeMember(orgId, userId) {
172
+ const [roles, overlays] = await db.$transaction([
173
+ db.authzMemberRole.deleteMany({ where: { orgId, userId } }),
174
+ db.authzOverlay.deleteMany({ where: { orgId, userId } }),
175
+ ]);
176
+ return { roles: roles.count, overlays: overlays.count };
177
+ },
178
+ async listOverlays(orgId, userId) {
179
+ const rows = await db.authzOverlay.findMany({
180
+ where: { orgId, userId },
181
+ orderBy: { createdAt: "desc" },
182
+ });
183
+ return rows.map(toOverlay);
184
+ },
185
+
186
+ async addOverlay(input) {
187
+ const id = newId();
188
+ const scope = input.scope ?? "";
189
+ await db.authzOverlay.deleteMany({
190
+ where: {
191
+ orgId: input.orgId,
192
+ userId: input.userId,
193
+ resource: input.resource,
194
+ action: input.action,
195
+ scope,
196
+ effect: input.effect,
197
+ },
198
+ });
199
+ await db.authzOverlay.create({ data: { ...input, id, scope } });
200
+ return id;
201
+ },
202
+
203
+ async removeOverlay(orgId, userId, overlayId) {
204
+ const row = await db.authzOverlay.findFirst({ where: { id: overlayId, orgId, userId } });
205
+ if (!row) return null;
206
+ await db.authzOverlay.deleteMany({ where: { id: overlayId, orgId, userId } });
207
+ return toOverlay(row);
208
+ },
209
+
210
+ async writeAudit(input) {
211
+ await db.authzAuditLog.create({
212
+ data: {
213
+ id: newId(),
214
+ orgId: input.orgId,
215
+ actorId: input.actorId,
216
+ targetUserId: input.targetUserId,
217
+ changeType: input.changeType,
218
+ before: input.before === undefined ? null : JSON.stringify(input.before),
219
+ after: input.after === undefined ? null : JSON.stringify(input.after),
220
+ },
221
+ });
222
+ },
223
+
224
+ async listAudit(orgId, limit) {
225
+ const rows = await db.authzAuditLog.findMany({
226
+ where: { orgId },
227
+ orderBy: { id: "desc" },
228
+ take: limit,
229
+ });
230
+ return rows.map((r) => ({
231
+ ...r,
232
+ before: r.before ? (JSON.parse(r.before) as unknown) : null,
233
+ after: r.after ? (JSON.parse(r.after) as unknown) : null,
234
+ }));
235
+ },
236
+ };
@@ -0,0 +1,71 @@
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
+ Declare request bodies with `jsonBody(schema, description)` (required; a missing body is a 400),
49
+ responses with `jsonContent(...)`.
50
+ The spec is served at `/openapi.json`, the reference UI at `/docs`.
51
+ - Responses use the envelope: `success(data)`, `paginated(items, page, pageSize, total)`.
52
+ Errors are thrown as `AppError` subclasses (`NotFoundError`, `ConflictError`,
53
+ `BusinessRuleError`, `ForbiddenError`, …) and become
54
+ `{ success: false, error, code, details }`.
55
+ - Route files are `routes/<name>.routes.ts` and default-export `defineRoutes({ app, basePath, router })`.
56
+ They are found automatically; there is no central list to edit.
57
+
58
+ ## Configuration and logging
59
+
60
+ - Add environment variables to `src/core/config/env.ts` (validated at startup) and to
61
+ `.env.example`. Never read `process.env` elsewhere.
62
+ - Log with `c.get("logger")` in requests and `logger` from `#core/logging/logger` elsewhere.
63
+ No `console.*` outside commands and tests.
64
+
65
+ ## Tests
66
+
67
+ - Tests live next to the code in `src/modules/<module>/tests/`. Use `api` from
68
+ `#tests/helpers/api` to call routes in-process.
69
+ - Test behaviour through the public surface (routes, services); mock only external services.
70
+
71
+ <!-- tatara:slot sections -->
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,16 @@
1
+ # syntax=docker/dockerfile:1
2
+ # One image for every entrypoint. Default: the API.
3
+ # Other processes: override the command, e.g. `node dist/apps/worker/index.mjs`.
4
+ FROM node:24-alpine AS base
5
+ WORKDIR /app
6
+ # tatara:slot docker-base
7
+
8
+ FROM base AS build
9
+ # tatara:slot docker-build
10
+
11
+ FROM base AS runtime
12
+ ENV NODE_ENV=production
13
+ # tatara:slot docker-runtime
14
+ USER node
15
+ EXPOSE 3000
16
+ 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,26 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ verify:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v5
16
+ # tatara:slot ci-setup
17
+ - run: cp .env.example .env
18
+ # tatara:slot ci-services
19
+ - run: __PM_RUN__ verify
20
+
21
+ image:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v5
25
+ # catch a broken Dockerfile on the PR, not at deploy
26
+ - run: docker build --tag app:ci .
@@ -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,33 @@
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 `flag()` for 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({});
26
+
27
+ /**
28
+ * A boolean flag: bare `--force` is true, `--force false|0|no|off` is false.
29
+ * (`z.coerce.boolean()` would read the string "false" as true.)
30
+ */
31
+ export function flag() {
32
+ return z.union([z.boolean(), z.stringbool()]).default(false);
33
+ }