@simple-auth-kit/cli 1.5.0 → 1.6.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 (176) hide show
  1. package/lib/copy.ts +1 -1
  2. package/package.json +1 -1
  3. package/registry/combos/express-drizzle/shared/src/{ability.ts → ability/ability.ts} +8 -2
  4. package/registry/combos/express-drizzle/shared/src/{permission-cache.ts → cache/permission-cache.ts} +25 -9
  5. package/registry/combos/express-drizzle/shared/src/{rate-limit.store.ts → cache/rate-limit.store.ts} +4 -1
  6. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/config}/auth.config.ts +1 -1
  7. package/registry/combos/express-drizzle/shared/src/{db.ts → config/db.ts} +1 -1
  8. package/registry/combos/express-drizzle/shared/src/{key-provider.ts → config/key-provider.ts} +6 -2
  9. package/registry/combos/express-drizzle/shared/src/{id.helper.ts → helpers/id.helper.ts} +6 -2
  10. package/registry/combos/{nestjs-drizzle/shared/src → express-drizzle/shared/src/helpers}/pagination.ts +17 -3
  11. package/registry/combos/express-drizzle/shared/src/middleware/auth-core-error.middleware.ts +6 -8
  12. package/registry/combos/express-drizzle/shared/src/middleware/auth.middleware.ts +6 -8
  13. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/openapi}/openapi-fragment.ts +13 -4
  14. package/registry/combos/express-drizzle/shared/src/{openapi-spec.ts → openapi/openapi-spec.ts} +251 -52
  15. package/registry/combos/express-drizzle/shared/src/repositories/oauth.repository.ts +2 -2
  16. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +2 -4
  17. package/registry/combos/express-drizzle/shared/src/repositories/session.repository.ts +3 -3
  18. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  19. package/registry/combos/express-drizzle/shared/src/route-tiers.ts +52 -11
  20. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +2 -2
  21. package/registry/combos/express-drizzle/test/bootstrap.ts +4 -2
  22. package/registry/combos/express-drizzle/test/prove-cycle.ts +1095 -308
  23. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +5 -5
  24. package/registry/combos/express-drizzle/variants/base/src/index.ts +8 -0
  25. package/registry/combos/express-drizzle/variants/base/src/middleware/authz.middleware.ts +2 -2
  26. package/registry/combos/express-drizzle/variants/base/src/openapi/openapi-admin.ts +1082 -0
  27. package/registry/combos/express-drizzle/variants/base/src/rbac.defaults.ts +60 -17
  28. package/registry/combos/express-drizzle/variants/base/src/repositories/audit-log.repository.ts +2 -2
  29. package/registry/combos/express-drizzle/variants/base/src/repositories/rbac.repository.ts +5 -5
  30. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +18 -22
  31. package/registry/combos/express-drizzle/variants/base/src/seed.ts +76 -17
  32. package/registry/combos/express-drizzle/variants/base/src/services/auth.service.ts +7 -9
  33. package/registry/combos/express-drizzle/variants/base/test/variant-hooks.ts +167 -45
  34. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +5 -5
  35. package/registry/combos/express-drizzle/variants/workspaces/src/index.ts +8 -0
  36. package/registry/combos/express-drizzle/variants/workspaces/src/middleware/authz.middleware.ts +3 -3
  37. package/registry/combos/express-drizzle/variants/workspaces/src/openapi/openapi-admin.ts +1176 -0
  38. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +1 -1
  39. package/registry/combos/express-drizzle/variants/workspaces/src/rbac.defaults.ts +66 -19
  40. package/registry/combos/express-drizzle/variants/workspaces/src/repositories/audit-log.repository.ts +2 -2
  41. package/registry/combos/express-drizzle/variants/workspaces/src/repositories/rbac.repository.ts +5 -5
  42. package/registry/combos/express-drizzle/variants/workspaces/src/repositories/workspace.repository.ts +9 -11
  43. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +18 -22
  44. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +1 -1
  45. package/registry/combos/express-drizzle/variants/workspaces/src/seed.ts +168 -35
  46. package/registry/combos/express-drizzle/variants/workspaces/src/services/auth.service.ts +7 -9
  47. package/registry/combos/express-drizzle/variants/workspaces/test/variant-hooks.ts +539 -133
  48. package/registry/combos/express-prisma/shared/src/{ability.ts → ability/ability.ts} +8 -2
  49. package/registry/combos/express-prisma/shared/src/{permission-cache.ts → cache/permission-cache.ts} +25 -9
  50. package/registry/combos/express-prisma/shared/src/{rate-limit.store.ts → cache/rate-limit.store.ts} +4 -1
  51. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/config}/auth.config.ts +1 -1
  52. package/registry/combos/express-prisma/shared/src/{key-provider.ts → config/key-provider.ts} +6 -2
  53. package/registry/combos/{nestjs-prisma/shared/src → express-prisma/shared/src/helpers}/id.helper.ts +6 -2
  54. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/helpers}/pagination.ts +17 -3
  55. package/registry/combos/express-prisma/shared/src/middleware/auth-core-error.middleware.ts +6 -8
  56. package/registry/combos/express-prisma/shared/src/middleware/auth.middleware.ts +6 -8
  57. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/openapi}/openapi-fragment.ts +13 -4
  58. package/registry/combos/express-prisma/shared/src/{openapi-spec.ts → openapi/openapi-spec.ts} +313 -55
  59. package/registry/combos/express-prisma/shared/src/repositories/oauth.repository.ts +2 -2
  60. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +2 -4
  61. package/registry/combos/express-prisma/shared/src/repositories/session.repository.ts +2 -2
  62. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  63. package/registry/combos/express-prisma/shared/src/route-tiers.ts +52 -11
  64. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +2 -2
  65. package/registry/combos/express-prisma/test/bootstrap.ts +7 -3
  66. package/registry/combos/express-prisma/test/prove-cycle.ts +1095 -308
  67. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +5 -5
  68. package/registry/combos/express-prisma/variants/base/src/index.ts +8 -0
  69. package/registry/combos/express-prisma/variants/base/src/middleware/authz.middleware.ts +2 -2
  70. package/registry/combos/express-prisma/variants/base/src/openapi/openapi-admin.ts +1102 -0
  71. package/registry/combos/express-prisma/variants/base/src/repositories/audit-log.repository.ts +2 -2
  72. package/registry/combos/express-prisma/variants/base/src/repositories/rbac.repository.ts +4 -4
  73. package/registry/combos/express-prisma/variants/base/src/routers/admin.router.ts +18 -22
  74. package/registry/combos/express-prisma/variants/base/src/services/auth.service.ts +6 -8
  75. package/registry/combos/express-prisma/variants/base/test/variant-hooks.ts +176 -47
  76. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +5 -5
  77. package/registry/combos/express-prisma/variants/workspaces/src/index.ts +8 -0
  78. package/registry/combos/express-prisma/variants/workspaces/src/middleware/authz.middleware.ts +3 -3
  79. package/registry/combos/express-prisma/variants/workspaces/src/openapi/openapi-admin.ts +1218 -0
  80. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +1 -1
  81. package/registry/combos/express-prisma/variants/workspaces/src/repositories/audit-log.repository.ts +2 -2
  82. package/registry/combos/express-prisma/variants/workspaces/src/repositories/rbac.repository.ts +4 -4
  83. package/registry/combos/express-prisma/variants/workspaces/src/repositories/workspace.repository.ts +2 -2
  84. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +18 -22
  85. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +1 -1
  86. package/registry/combos/express-prisma/variants/workspaces/src/services/auth.service.ts +6 -8
  87. package/registry/combos/express-prisma/variants/workspaces/test/variant-hooks.ts +538 -134
  88. package/registry/combos/nestjs-drizzle/shared/src/{guards → ability}/ability.guard.ts +1 -1
  89. package/registry/combos/nestjs-drizzle/shared/src/{ability.ts → ability/ability.ts} +8 -2
  90. package/registry/combos/nestjs-drizzle/shared/src/{permission-cache.ts → cache/permission-cache.ts} +27 -10
  91. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/cache}/rate-limit.store.ts +4 -1
  92. package/registry/combos/nestjs-drizzle/shared/src/{auth.config.ts → config/auth.config.ts} +1 -1
  93. package/registry/combos/nestjs-drizzle/shared/src/{db.ts → config/db.ts} +1 -1
  94. package/registry/combos/nestjs-drizzle/shared/src/{key-provider.ts → config/key-provider.ts} +6 -2
  95. package/registry/combos/nestjs-drizzle/shared/src/controllers/auth.controller.ts +1 -1
  96. package/registry/combos/nestjs-drizzle/shared/src/guards/auth.guard.ts +1 -1
  97. package/registry/combos/nestjs-drizzle/shared/src/{id.helper.ts → helpers/id.helper.ts} +6 -2
  98. package/registry/combos/{express-prisma/shared/src → nestjs-drizzle/shared/src/helpers}/pagination.ts +17 -3
  99. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +2 -2
  100. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +8 -12
  101. package/registry/combos/nestjs-drizzle/shared/src/repositories/session.repository.ts +3 -3
  102. package/registry/combos/nestjs-drizzle/shared/src/repositories/two-factor.repository.ts +1 -1
  103. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  104. package/registry/combos/nestjs-drizzle/test/bootstrap.ts +1 -1
  105. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1 -1
  106. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +10 -6
  107. package/registry/combos/nestjs-drizzle/variants/base/src/controllers/admin.controller.ts +1 -1
  108. package/registry/combos/nestjs-drizzle/variants/base/src/guards/authz.guard.ts +2 -2
  109. package/registry/combos/nestjs-drizzle/variants/base/src/index.ts +9 -0
  110. package/registry/combos/nestjs-drizzle/variants/base/src/rbac.defaults.ts +58 -16
  111. package/registry/combos/nestjs-drizzle/variants/base/src/repositories/audit-log.repository.ts +3 -3
  112. package/registry/combos/nestjs-drizzle/variants/base/src/repositories/rbac.repository.ts +4 -4
  113. package/registry/combos/nestjs-drizzle/variants/base/src/seed.ts +76 -17
  114. package/registry/combos/nestjs-drizzle/variants/base/src/services/auth.service.ts +7 -9
  115. package/registry/combos/nestjs-drizzle/variants/base/test/variant-hooks.ts +167 -45
  116. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +10 -6
  117. package/registry/combos/nestjs-drizzle/variants/workspaces/src/controllers/admin.controller.ts +1 -1
  118. package/registry/combos/nestjs-drizzle/variants/workspaces/src/controllers/workspace.controller.ts +1 -1
  119. package/registry/combos/nestjs-drizzle/variants/workspaces/src/guards/authz.guard.ts +2 -2
  120. package/registry/combos/nestjs-drizzle/variants/workspaces/src/index.ts +9 -0
  121. package/registry/combos/nestjs-drizzle/variants/workspaces/src/rbac.defaults.ts +64 -18
  122. package/registry/combos/nestjs-drizzle/variants/workspaces/src/repositories/audit-log.repository.ts +3 -3
  123. package/registry/combos/nestjs-drizzle/variants/workspaces/src/repositories/rbac.repository.ts +4 -4
  124. package/registry/combos/nestjs-drizzle/variants/workspaces/src/repositories/workspace.repository.ts +8 -10
  125. package/registry/combos/nestjs-drizzle/variants/workspaces/src/seed.ts +171 -36
  126. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +7 -9
  127. package/registry/combos/nestjs-drizzle/variants/workspaces/test/variant-hooks.ts +539 -133
  128. package/registry/combos/nestjs-prisma/shared/src/{guards → ability}/ability.guard.ts +1 -1
  129. package/registry/combos/nestjs-prisma/shared/src/{ability.ts → ability/ability.ts} +8 -2
  130. package/registry/combos/nestjs-prisma/shared/src/{permission-cache.ts → cache/permission-cache.ts} +27 -10
  131. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/cache}/rate-limit.store.ts +4 -1
  132. package/registry/combos/nestjs-prisma/shared/src/{auth.config.ts → config/auth.config.ts} +1 -1
  133. package/registry/combos/nestjs-prisma/shared/src/{key-provider.ts → config/key-provider.ts} +6 -2
  134. package/registry/combos/nestjs-prisma/shared/src/controllers/auth.controller.ts +1 -1
  135. package/registry/combos/nestjs-prisma/shared/src/guards/auth.guard.ts +1 -1
  136. package/registry/combos/{express-prisma/shared/src → nestjs-prisma/shared/src/helpers}/id.helper.ts +6 -2
  137. package/registry/combos/nestjs-prisma/shared/src/{pagination.ts → helpers/pagination.ts} +17 -3
  138. package/registry/combos/nestjs-prisma/shared/src/repositories/oauth.repository.ts +1 -1
  139. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +2 -4
  140. package/registry/combos/nestjs-prisma/shared/src/repositories/session.repository.ts +2 -2
  141. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  142. package/registry/combos/nestjs-prisma/test/bootstrap.ts +1 -1
  143. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1 -1
  144. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +9 -5
  145. package/registry/combos/nestjs-prisma/variants/base/src/controllers/admin.controller.ts +1 -1
  146. package/registry/combos/nestjs-prisma/variants/base/src/gateways/audit-log.gateway.ts +1 -1
  147. package/registry/combos/nestjs-prisma/variants/base/src/guards/authz.guard.ts +2 -2
  148. package/registry/combos/nestjs-prisma/variants/base/src/index.ts +9 -0
  149. package/registry/combos/nestjs-prisma/variants/base/src/{docs.ts → openapi/docs.ts} +23 -6
  150. package/registry/combos/nestjs-prisma/variants/base/src/repositories/audit-log.repository.ts +2 -2
  151. package/registry/combos/nestjs-prisma/variants/base/src/repositories/country.repository.ts +2 -2
  152. package/registry/combos/nestjs-prisma/variants/base/src/repositories/customer.repository.ts +6 -5
  153. package/registry/combos/nestjs-prisma/variants/base/src/repositories/language.repository.ts +2 -2
  154. package/registry/combos/nestjs-prisma/variants/base/src/repositories/rbac.repository.ts +3 -3
  155. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +6 -8
  156. package/registry/combos/nestjs-prisma/variants/base/test/variant-hooks.ts +176 -47
  157. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +9 -5
  158. package/registry/combos/nestjs-prisma/variants/workspaces/src/controllers/admin.controller.ts +1 -1
  159. package/registry/combos/nestjs-prisma/variants/workspaces/src/controllers/workspace.controller.ts +1 -1
  160. package/registry/combos/nestjs-prisma/variants/workspaces/src/guards/authz.guard.ts +2 -2
  161. package/registry/combos/nestjs-prisma/variants/workspaces/src/index.ts +9 -0
  162. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/audit-log.repository.ts +2 -2
  163. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/country.repository.ts +2 -2
  164. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/customer.repository.ts +6 -5
  165. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +2 -2
  166. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/rbac.repository.ts +3 -3
  167. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/workspace.repository.ts +1 -1
  168. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +6 -8
  169. package/registry/combos/nestjs-prisma/variants/workspaces/test/variant-hooks.ts +538 -134
  170. package/simple-auth-kit.ts +13 -9
  171. package/registry/combos/express-drizzle/variants/base/src/openapi-admin.ts +0 -565
  172. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-admin.ts +0 -596
  173. package/registry/combos/express-prisma/variants/base/src/openapi-admin.ts +0 -569
  174. package/registry/combos/express-prisma/variants/workspaces/src/openapi-admin.ts +0 -601
  175. /package/registry/combos/express-drizzle/shared/src/{http-error.ts → errors/http-error.ts} +0 -0
  176. /package/registry/combos/express-prisma/shared/src/{http-error.ts → errors/http-error.ts} +0 -0
@@ -3,13 +3,25 @@
3
3
  import "dotenv/config";
4
4
  import { PrismaPg } from "@prisma/adapter-pg";
5
5
  import { PrismaClient } from "../generated/prisma/client.js";
6
- import { toId } from "../src/id.helper.js";
7
- import { provisionDefaultRoles, SEED_ADMIN_ROLES } from "../src/rbac.defaults.js";
8
- import { POLICY_VERSION_KEY } from "../src/permission-cache.js";
6
+ import { toId } from "../src/helpers/id.helper.js";
7
+ import {
8
+ provisionDefaultRoles,
9
+ SEED_ADMIN_ROLES,
10
+ } from "../src/rbac.defaults.js";
11
+ import { POLICY_VERSION_KEY } from "../src/cache/permission-cache.js";
9
12
  import { permissionCacheStore } from "./bootstrap.js";
10
- import { renewingToken, type AdminSession, type AuthTokens, type Principal, type ProofContext, type VariantHooks } from "./harness.js";
13
+ import {
14
+ renewingToken,
15
+ type AdminSession,
16
+ type AuthTokens,
17
+ type Principal,
18
+ type ProofContext,
19
+ type VariantHooks,
20
+ } from "./harness.js";
11
21
 
12
- const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env["DATABASE_URL"] }) });
22
+ const prisma = new PrismaClient({
23
+ adapter: new PrismaPg({ connectionString: process.env["DATABASE_URL"] }),
24
+ });
13
25
 
14
26
  /** Remembered by `makeAdmin` so `freshToken()` can mint a new one whenever a section needs it. */
15
27
  let adminCredentials: { identifier: string; password: string } | undefined;
@@ -17,24 +29,43 @@ let adminCredentials: { identifier: string; password: string } | undefined;
17
29
  export const hooks: VariantHooks = {
18
30
  variant: "base",
19
31
 
20
- async makeAdmin(ctx: ProofContext, principal: Principal): Promise<AdminSession> {
32
+ async makeAdmin(
33
+ ctx: ProofContext,
34
+ principal: Principal,
35
+ ): Promise<AdminSession> {
21
36
  // Calls the same provisioning function `npm run seed` does — idempotent, safe against an
22
37
  // already-seeded database.
23
38
  await provisionDefaultRoles(prisma);
24
- const roles = await prisma.role.findMany({ where: { slug: { in: SEED_ADMIN_ROLES } }, select: { id: true } });
25
- await prisma.roleUser.createMany({ data: roles.map((role) => ({ userId: toId(principal.userId), roleId: role.id })), skipDuplicates: true });
39
+ const roles = await prisma.role.findMany({
40
+ where: { slug: { in: SEED_ADMIN_ROLES } },
41
+ select: { id: true },
42
+ });
43
+ await prisma.roleUser.createMany({
44
+ data: roles.map((role) => ({
45
+ userId: toId(principal.userId),
46
+ roleId: role.id,
47
+ })),
48
+ skipDuplicates: true,
49
+ });
26
50
 
27
- adminCredentials = { identifier: principal.email, password: principal.password };
51
+ adminCredentials = {
52
+ identifier: principal.email,
53
+ password: principal.password,
54
+ };
28
55
  // These writes went straight to the database, behind the app's back — bump the policy
29
56
  // counter ourselves so the cache doesn't keep serving a stale (empty) permission set.
30
57
  await permissionCacheStore.bump(POLICY_VERSION_KEY);
31
- const login = await ctx.call("POST", "/auth/login", { body: { identifier: principal.email, password: principal.password } });
58
+ const login = await ctx.call("POST", "/auth/login", {
59
+ body: { identifier: principal.email, password: principal.password },
60
+ });
32
61
  const token = (login.body as AuthTokens).accessToken;
33
62
  ctx.assert(!!token, `the seeded admin can log in (got ${login.status})`);
34
63
  return {
35
64
  token,
36
65
  freshToken: renewingToken(async () => {
37
- const res = await ctx.call("POST", "/auth/login", { body: adminCredentials! });
66
+ const res = await ctx.call("POST", "/auth/login", {
67
+ body: adminCredentials!,
68
+ });
38
69
  return (res.body as AuthTokens).accessToken;
39
70
  }),
40
71
  };
@@ -44,39 +75,91 @@ export const hooks: VariantHooks = {
44
75
  // Nothing to do: admin endpoints operate on the whole deployment.
45
76
  },
46
77
 
47
- async proveVariantProperties(ctx: ProofContext, admin: AdminSession): Promise<void> {
48
- const listed = await ctx.call("GET", "/auth/admin/users", { token: await admin.freshToken() });
49
- ctx.assert(listed.status === 200, `admin can list every user in the deployment (got ${listed.status})`);
50
- ctx.assert(Array.isArray(listed.body?.items) && listed.body.items.length > 0, "the user list is non-empty");
78
+ async proveVariantProperties(
79
+ ctx: ProofContext,
80
+ admin: AdminSession,
81
+ ): Promise<void> {
82
+ const listed = await ctx.call("GET", "/auth/admin/users", {
83
+ token: await admin.freshToken(),
84
+ });
85
+ ctx.assert(
86
+ listed.status === 200,
87
+ `admin can list every user in the deployment (got ${listed.status})`,
88
+ );
89
+ ctx.assert(
90
+ Array.isArray(listed.body?.items) && listed.body.items.length > 0,
91
+ "the user list is non-empty",
92
+ );
51
93
 
52
- const workspaces = await ctx.call("GET", "/workspaces", { token: await admin.freshToken() });
53
- ctx.assert(workspaces.status === 404, `no workspace endpoints exist in this variant (got ${workspaces.status})`);
94
+ const workspaces = await ctx.call("GET", "/workspaces", {
95
+ token: await admin.freshToken(),
96
+ });
97
+ ctx.assert(
98
+ workspaces.status === 404,
99
+ `no workspace endpoints exist in this variant (got ${workspaces.status})`,
100
+ );
54
101
 
55
- const me = await ctx.call("GET", "/auth/me", { token: await admin.freshToken() });
56
- ctx.assert(me.body?.roles?.includes("admin"), "roles are global: they apply to every request this user makes");
102
+ const me = await ctx.call("GET", "/auth/me", {
103
+ token: await admin.freshToken(),
104
+ });
105
+ ctx.assert(
106
+ me.body?.roles?.includes("admin"),
107
+ "roles are global: they apply to every request this user makes",
108
+ );
57
109
 
58
110
  // Authorization is not in the token, so a grant/revoke lands on the caller's next request.
59
111
  const email = ctx.uniqueEmail("immediate");
60
112
  const password = "immediate-pw-12345";
61
113
  const tokens = await ctx.signup(email, password);
62
- const userId = (await ctx.call("GET", "/auth/me", { token: tokens.accessToken })).body.sub as string;
63
-
64
- const beforeGrant = await ctx.call("GET", "/auth/admin/users", { token: tokens.accessToken });
65
- ctx.assert(beforeGrant.status === 403, `a user with no grant is refused (got ${beforeGrant.status})`);
114
+ const userId = (
115
+ await ctx.call("GET", "/auth/me", { token: tokens.accessToken })
116
+ ).body.sub as string;
66
117
 
67
- const granted = await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, { token: await admin.freshToken(), body: { permission: "users:read" } });
68
- ctx.assert(granted.status === 200 || granted.status === 201, `admin can grant a permission directly (got ${granted.status})`);
118
+ const beforeGrant = await ctx.call("GET", "/auth/admin/users", {
119
+ token: tokens.accessToken,
120
+ });
121
+ ctx.assert(
122
+ beforeGrant.status === 403,
123
+ `a user with no grant is refused (got ${beforeGrant.status})`,
124
+ );
69
125
 
70
- const afterGrant = await ctx.call("GET", "/auth/admin/users", { token: tokens.accessToken });
71
- ctx.assert(afterGrant.status === 200, `…and the grant is in effect on the very next request, on the token they already held (got ${afterGrant.status})`);
126
+ const granted = await ctx.call(
127
+ "POST",
128
+ `/auth/admin/users/${userId}/permissions`,
129
+ { token: await admin.freshToken(), body: { permission: "users:read" } },
130
+ );
131
+ ctx.assert(
132
+ granted.status === 200 || granted.status === 201,
133
+ `admin can grant a permission directly (got ${granted.status})`,
134
+ );
72
135
 
73
- const revoked = await ctx.call("POST", `/auth/admin/users/${userId}/permissions/${encodeURIComponent("users:read")}/revoke`, {
74
- token: await admin.freshToken(),
136
+ const afterGrant = await ctx.call("GET", "/auth/admin/users", {
137
+ token: tokens.accessToken,
75
138
  });
76
- ctx.assert(revoked.status === 200 || revoked.status === 201, `admin can revoke the direct grant (got ${revoked.status})`);
139
+ ctx.assert(
140
+ afterGrant.status === 200,
141
+ `…and the grant is in effect on the very next request, on the token they already held (got ${afterGrant.status})`,
142
+ );
143
+
144
+ const revoked = await ctx.call(
145
+ "POST",
146
+ `/auth/admin/users/${userId}/permissions/${encodeURIComponent("users:read")}/revoke`,
147
+ {
148
+ token: await admin.freshToken(),
149
+ },
150
+ );
151
+ ctx.assert(
152
+ revoked.status === 200 || revoked.status === 201,
153
+ `admin can revoke the direct grant (got ${revoked.status})`,
154
+ );
77
155
 
78
- const afterRevoke = await ctx.call("GET", "/auth/admin/users", { token: tokens.accessToken });
79
- ctx.assert(afterRevoke.status === 403, `and the revocation lands on the very next request too, on the same token (got ${afterRevoke.status})`);
156
+ const afterRevoke = await ctx.call("GET", "/auth/admin/users", {
157
+ token: tokens.accessToken,
158
+ });
159
+ ctx.assert(
160
+ afterRevoke.status === 403,
161
+ `and the revocation lands on the very next request too, on the same token (got ${afterRevoke.status})`,
162
+ );
80
163
 
81
164
  await proveTheCacheIsReal(ctx, admin, tokens.accessToken);
82
165
  await proveARawDatabaseEditChangesEnforcement(ctx, admin);
@@ -85,7 +168,11 @@ export const hooks: VariantHooks = {
85
168
 
86
169
  // Reads the very store the app resolves through, so "the second request didn't touch the
87
170
  // database" is a measurement rather than a claim.
88
- async function proveTheCacheIsReal(ctx: ProofContext, admin: AdminSession, token: string): Promise<void> {
171
+ async function proveTheCacheIsReal(
172
+ ctx: ProofContext,
173
+ admin: AdminSession,
174
+ token: string,
175
+ ): Promise<void> {
89
176
  const N = 8;
90
177
  await ctx.call("GET", "/auth/me", { token }); // warm, so the measurement is of the steady state
91
178
  const before = { ...permissionCacheStore.stats };
@@ -97,39 +184,81 @@ async function proveTheCacheIsReal(ctx: ProofContext, admin: AdminSession, token
97
184
  `${N} identical authorized requests resolve permissions 0 further times — all ${N} were cache hits (misses +${after.misses - before.misses}, hits +${after.hits - before.hits})`,
98
185
  );
99
186
 
100
- const userId = (await ctx.call("GET", "/auth/me", { token })).body.sub as string;
187
+ const userId = (await ctx.call("GET", "/auth/me", { token })).body
188
+ .sub as string;
101
189
  const beforeGrant = { ...permissionCacheStore.stats };
102
- await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, { token: await admin.freshToken(), body: { permission: "probe:cache" } });
190
+ await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, {
191
+ token: await admin.freshToken(),
192
+ body: { permission: "probe:cache" },
193
+ });
103
194
  const afterGrantCall = await ctx.call("GET", "/auth/me", { token });
104
195
  ctx.assert(
105
196
  permissionCacheStore.stats.misses > beforeGrant.misses,
106
197
  "a grant bumps this user's version counter, so the next request misses the cache and re-resolves rather than serving a stale entry",
107
198
  );
108
- ctx.assert((afterGrantCall.body?.permissions ?? []).includes("probe:cache"), "…and the freshly resolved answer contains the new permission");
109
- await ctx.call("POST", `/auth/admin/users/${userId}/permissions/${encodeURIComponent("probe:cache")}/revoke`, { token: await admin.freshToken() });
199
+ ctx.assert(
200
+ (afterGrantCall.body?.permissions ?? []).includes("probe:cache"),
201
+ "…and the freshly resolved answer contains the new permission",
202
+ );
203
+ await ctx.call(
204
+ "POST",
205
+ `/auth/admin/users/${userId}/permissions/${encodeURIComponent("probe:cache")}/revoke`,
206
+ { token: await admin.freshToken() },
207
+ );
110
208
  }
111
209
 
112
210
  // Writes the row directly rather than through the admin API, ruling out any in-memory side
113
211
  // channel — a `psql` session would look exactly like this.
114
- async function proveARawDatabaseEditChangesEnforcement(ctx: ProofContext, admin: AdminSession): Promise<void> {
212
+ async function proveARawDatabaseEditChangesEnforcement(
213
+ ctx: ProofContext,
214
+ admin: AdminSession,
215
+ ): Promise<void> {
115
216
  const email = ctx.uniqueEmail("rawedit");
116
217
  const tokens = await ctx.signup(email, "rawedit-pw-12345");
117
- const userId = (await ctx.call("GET", "/auth/me", { token: tokens.accessToken })).body.sub as string;
118
- await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, { token: await admin.freshToken(), body: { permission: "audit-log:read" } });
119
- ctx.assert((await ctx.call("GET", "/auth/admin/audit-log", { token: tokens.accessToken })).status === 200, "a direct grant opens the audit log");
218
+ const userId = (
219
+ await ctx.call("GET", "/auth/me", { token: tokens.accessToken })
220
+ ).body.sub as string;
221
+ await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, {
222
+ token: await admin.freshToken(),
223
+ body: { permission: "audit-log:read" },
224
+ });
225
+ ctx.assert(
226
+ (
227
+ await ctx.call("GET", "/auth/admin/audit-log", {
228
+ token: tokens.accessToken,
229
+ })
230
+ ).status === 200,
231
+ "a direct grant opens the audit log",
232
+ );
120
233
 
121
234
  // Written straight to the table, with no code change and no redeploy. The bump that follows is
122
235
  // the out-of-band writer's half of the cache contract (permission-cache.ts) — without it the
123
236
  // edit would still land, but only once the cached entry expired.
124
237
  // Written straight to the table; the bump is the out-of-band writer's half of the cache
125
238
  // contract (permission-cache.ts) — without it the edit would still land, only later.
126
- await prisma.permission.update({ where: { slug: "audit-log:read" }, data: { isActive: false } });
239
+ await prisma.permission.update({
240
+ where: { slug: "audit-log:read" },
241
+ data: { isActive: false },
242
+ });
127
243
  await permissionCacheStore.bump(POLICY_VERSION_KEY);
128
- const denied = await ctx.call("GET", "/auth/admin/audit-log", { token: tokens.accessToken });
129
- ctx.assert(denied.status === 403, `a permission deactivated by a raw database write stops opening its route (got ${denied.status})`);
244
+ const denied = await ctx.call("GET", "/auth/admin/audit-log", {
245
+ token: tokens.accessToken,
246
+ });
247
+ ctx.assert(
248
+ denied.status === 403,
249
+ `a permission deactivated by a raw database write stops opening its route (got ${denied.status})`,
250
+ );
130
251
 
131
- await prisma.permission.update({ where: { slug: "audit-log:read" }, data: { isActive: true } });
252
+ await prisma.permission.update({
253
+ where: { slug: "audit-log:read" },
254
+ data: { isActive: true },
255
+ });
132
256
  await permissionCacheStore.bump(POLICY_VERSION_KEY);
133
- const restored = await ctx.call("GET", "/auth/admin/audit-log", { token: tokens.accessToken });
134
- ctx.assert(restored.status === 200, `…and reactivating it in the database opens the route again (got ${restored.status})`);
257
+ const restored = await ctx.call("GET", "/auth/admin/audit-log", {
258
+ token: tokens.accessToken,
259
+ });
260
+ ctx.assert(
261
+ restored.status === 200,
262
+ `…and reactivating it in the database opens the route again (got ${restored.status})`,
263
+ );
135
264
  }
@@ -1,11 +1,15 @@
1
1
  import { PrismaPg } from "@prisma/adapter-pg";
2
2
  import { DynamicModule, Module } from "@nestjs/common";
3
3
  import { APP_FILTER, APP_INTERCEPTOR } from "@nestjs/core";
4
- import { AbilityGuard } from "./guards/ability.guard.js";
4
+ import { AbilityGuard } from "./ability/ability.guard.js";
5
5
  import { AdminController } from "./controllers/admin.controller.js";
6
6
  import { AuditLogRepository } from "./repositories/audit-log.repository.js";
7
7
  import { AuthCoreErrorFilter } from "./filters/auth-core-error.filter.js";
8
- import { AUTH_CONFIG, AuthConfig, defaultAuthConfig } from "./auth.config.js";
8
+ import {
9
+ AUTH_CONFIG,
10
+ AuthConfig,
11
+ defaultAuthConfig,
12
+ } from "./config/auth.config.js";
9
13
  import { AuthController } from "./controllers/auth.controller.js";
10
14
  import { AuthGuard } from "./guards/auth.guard.js";
11
15
  import { AuthService } from "./services/auth.service.js";
@@ -13,7 +17,7 @@ import { AuthzGuard, WorkspaceGuard } from "./guards/authz.guard.js";
13
17
  import { PrismaClient } from "./../generated/prisma/client.js";
14
18
  import { CountryRepository } from "./repositories/country.repository.js";
15
19
  import { CustomerRepository } from "./repositories/customer.repository.js";
16
- import { KeyProviderService } from "./key-provider.js";
20
+ import { KeyProviderService } from "./config/key-provider.js";
17
21
  import { LanguageRepository } from "./repositories/language.repository.js";
18
22
  import { OAuthRepository } from "./repositories/oauth.repository.js";
19
23
  import { PasswordResetRepository } from "./repositories/password-reset.repository.js";
@@ -21,11 +25,11 @@ import {
21
25
  InMemoryPermissionCacheStore,
22
26
  PERMISSION_CACHE_STORE,
23
27
  PermissionCache,
24
- } from "./permission-cache.js";
28
+ } from "./cache/permission-cache.js";
25
29
  import {
26
30
  InMemoryRateLimitStore,
27
31
  RATE_LIMIT_STORE,
28
- } from "./rate-limit.store.js";
32
+ } from "./cache/rate-limit.store.js";
29
33
  import { RbacRepository } from "./repositories/rbac.repository.js";
30
34
  import { ResponseInterceptor } from "./interceptors/response.interceptor.js";
31
35
  import { assertEveryRouteDeclaresATier } from "./route-tiers.js";
@@ -25,7 +25,7 @@ import {
25
25
  ApiTags,
26
26
  } from "@nestjs/swagger";
27
27
  import type { Request } from "express";
28
- import { AbilityGuard } from "../guards/ability.guard.js";
28
+ import { AbilityGuard } from "../ability/ability.guard.js";
29
29
  import { AuthGuard } from "../guards/auth.guard.js";
30
30
  import { AuthService } from "../services/auth.service.js";
31
31
  import { WORKSPACE_HEADER, WorkspaceGuard } from "../guards/authz.guard.js";
@@ -22,7 +22,7 @@ import {
22
22
  ApiTags,
23
23
  } from "@nestjs/swagger";
24
24
  import type { Request } from "express";
25
- import { AbilityGuard } from "../guards/ability.guard.js";
25
+ import { AbilityGuard } from "../ability/ability.guard.js";
26
26
  import { AuthGuard } from "../guards/auth.guard.js";
27
27
  import { WORKSPACE_HEADER, WorkspaceGuard } from "../guards/authz.guard.js";
28
28
  import { Authenticated, CheckAbility } from "../route-tiers.js";
@@ -5,8 +5,8 @@ import {
5
5
  Inject,
6
6
  Injectable,
7
7
  } from "@nestjs/common";
8
- import { defineAbilitiesFor } from "../ability.js";
9
- import { PermissionCache } from "../permission-cache.js";
8
+ import { defineAbilitiesFor } from "../ability/ability.js";
9
+ import { PermissionCache } from "../cache/permission-cache.js";
10
10
  import {
11
11
  memberCacheKey,
12
12
  RbacRepository,
@@ -0,0 +1,9 @@
1
+ export * from "./auth.module.js";
2
+
3
+ export * from "./route-tiers.js";
4
+
5
+ export * from "./guards/auth.guard.js";
6
+ export * from "./guards/authz.guard.js";
7
+
8
+ export * from "./ability/ability.js";
9
+ export * from "./ability/ability.guard.js";
@@ -1,13 +1,13 @@
1
1
  import { Inject, Injectable } from "@nestjs/common";
2
2
  import type { AuditEvent } from "@/lib/auth/core/types.js";
3
3
  import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
4
- import { toIdOrNull, toIdOrUndefined } from "../id.helper.js";
4
+ import { toIdOrNull, toIdOrUndefined } from "../helpers/id.helper.js";
5
5
  import {
6
6
  buildPageMeta,
7
7
  normalizeLimit,
8
8
  normalizePage,
9
9
  type Paginated,
10
- } from "../pagination.js";
10
+ } from "../helpers/pagination.js";
11
11
 
12
12
  export interface AuditLogEntry {
13
13
  id: string;
@@ -5,13 +5,13 @@ import {
5
5
  NotFoundException,
6
6
  } from "@nestjs/common";
7
7
  import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
8
- import { toId, toIdOrNull } from "../id.helper.js";
8
+ import { toId, toIdOrNull } from "../helpers/id.helper.js";
9
9
  import {
10
10
  buildPageMeta,
11
11
  normalizeLimit,
12
12
  normalizePage,
13
13
  type Paginated,
14
- } from "../pagination.js";
14
+ } from "../helpers/pagination.js";
15
15
 
16
16
  /** A `Country` row as `findUnique`/`findMany` return it. */
17
17
  export type CountryRow = Prisma.CountryGetPayload<object>;
@@ -5,13 +5,13 @@ import {
5
5
  NotFoundException,
6
6
  } from "@nestjs/common";
7
7
  import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
8
- import { toId, toIdOrNull } from "../id.helper.js";
8
+ import { toId, toIdOrNull } from "../helpers/id.helper.js";
9
9
  import {
10
10
  buildPageMeta,
11
11
  normalizeLimit,
12
12
  normalizePage,
13
13
  type Paginated,
14
- } from "../pagination.js";
14
+ } from "../helpers/pagination.js";
15
15
 
16
16
  /** A `Customer` row as `findUnique`/`findMany` return it. */
17
17
  export type CustomerRow = Prisma.CustomerGetPayload<object>;
@@ -88,9 +88,10 @@ export interface CustomerInput {
88
88
  isActive?: boolean;
89
89
  }
90
90
 
91
- function toPrismaInput(
92
- input: Partial<CustomerInput>,
93
- ): Omit<Partial<CustomerInput>, "dob" | "joinedDate"> & {
91
+ function toPrismaInput(input: Partial<CustomerInput>): Omit<
92
+ Partial<CustomerInput>,
93
+ "dob" | "joinedDate"
94
+ > & {
94
95
  dob?: Date | null;
95
96
  joinedDate?: Date;
96
97
  } {
@@ -5,13 +5,13 @@ import {
5
5
  NotFoundException,
6
6
  } from "@nestjs/common";
7
7
  import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
8
- import { toId, toIdOrNull } from "../id.helper.js";
8
+ import { toId, toIdOrNull } from "../helpers/id.helper.js";
9
9
  import {
10
10
  buildPageMeta,
11
11
  normalizeLimit,
12
12
  normalizePage,
13
13
  type Paginated,
14
- } from "../pagination.js";
14
+ } from "../helpers/pagination.js";
15
15
 
16
16
  /** A `Language` row as `findUnique`/`findMany` return it. */
17
17
  export type LanguageRow = Prisma.LanguageGetPayload<object>;
@@ -2,14 +2,14 @@ import { Inject, Injectable, NotFoundException } from "@nestjs/common";
2
2
  import { resolvePermissions } from "@/lib/auth/core/rbac.js";
3
3
  import { Prisma, PrismaClient } from "../../generated/prisma/client.js";
4
4
  import type { AuthzContext } from "../guards/authz.guard.js";
5
- import { PermissionCache } from "../permission-cache.js";
6
- import { toId, toIdOrNull } from "../id.helper.js";
5
+ import { PermissionCache } from "../cache/permission-cache.js";
6
+ import { toId, toIdOrNull } from "../helpers/id.helper.js";
7
7
  import {
8
8
  buildPageMeta,
9
9
  normalizeLimit,
10
10
  normalizePage,
11
11
  type Paginated,
12
- } from "../pagination.js";
12
+ } from "../helpers/pagination.js";
13
13
 
14
14
  const MEMBER_INCLUDE = {
15
15
  user: true,
@@ -10,7 +10,7 @@ import {
10
10
  WORKSPACE_CREATOR_ROLES,
11
11
  } from "../rbac.defaults.js";
12
12
  import { RbacRepository } from "./rbac.repository.js";
13
- import { toId } from "../id.helper.js";
13
+ import { toId } from "../helpers/id.helper.js";
14
14
 
15
15
  export interface WorkspaceSummary {
16
16
  id: string;
@@ -51,7 +51,7 @@ import {
51
51
  verifyTotpCode,
52
52
  } from "@/lib/auth/core/two-factor.js";
53
53
  import type { Revoker } from "@/lib/auth/core/types.js";
54
- import { AUTH_CONFIG, AuthConfig } from "../auth.config.js";
54
+ import { AUTH_CONFIG, AuthConfig } from "../config/auth.config.js";
55
55
  import type { AuthzContext } from "../guards/authz.guard.js";
56
56
  import {
57
57
  AuditLogEntry,
@@ -59,12 +59,12 @@ import {
59
59
  AuditLogRepository,
60
60
  toAuditLogEntry,
61
61
  } from "../repositories/audit-log.repository.js";
62
- import type { Paginated } from "../pagination.js";
62
+ import type { Paginated } from "../helpers/pagination.js";
63
63
  import { PrismaClient } from "../../generated/prisma/client.js";
64
- import { KeyProviderService } from "../key-provider.js";
64
+ import { KeyProviderService } from "../config/key-provider.js";
65
65
  import { OAuthRepository } from "../repositories/oauth.repository.js";
66
66
  import { PasswordResetRepository } from "../repositories/password-reset.repository.js";
67
- import { RATE_LIMIT_STORE } from "../rate-limit.store.js";
67
+ import { RATE_LIMIT_STORE } from "../cache/rate-limit.store.js";
68
68
  import {
69
69
  MemberListFilter,
70
70
  MemberListResult,
@@ -77,7 +77,7 @@ import {
77
77
  } from "../repositories/rbac.repository.js";
78
78
  import { SessionRepository } from "../repositories/session.repository.js";
79
79
  import { TwoFactorRepository } from "../repositories/two-factor.repository.js";
80
- import { toId, toIdOrNull } from "../id.helper.js";
80
+ import { toId, toIdOrNull } from "../helpers/id.helper.js";
81
81
 
82
82
  export interface AuthTokens {
83
83
  accessToken: string;
@@ -476,9 +476,7 @@ export class AuthService {
476
476
  return { twoFactorEnabled: user.twoFactorEnabled };
477
477
  }
478
478
 
479
- async listActiveSessions(
480
- userId: string,
481
- ): Promise<
479
+ async listActiveSessions(userId: string): Promise<
482
480
  Array<{
483
481
  id: string;
484
482
  createdAt: string;