@simple-auth-kit/cli 1.5.1 → 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 (182) hide show
  1. package/package.json +1 -1
  2. package/registry/combos/express-drizzle/shared/src/{ability.ts → ability/ability.ts} +8 -2
  3. package/registry/combos/express-drizzle/shared/src/{permission-cache.ts → cache/permission-cache.ts} +25 -9
  4. package/registry/combos/express-drizzle/shared/src/{rate-limit.store.ts → cache/rate-limit.store.ts} +4 -1
  5. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/config}/auth.config.ts +1 -1
  6. package/registry/combos/express-drizzle/shared/src/{db.ts → config/db.ts} +1 -1
  7. package/registry/combos/express-drizzle/shared/src/{key-provider.ts → config/key-provider.ts} +6 -2
  8. package/registry/combos/express-drizzle/shared/src/{id.helper.ts → helpers/id.helper.ts} +6 -2
  9. package/registry/combos/{nestjs-drizzle/shared/src → express-drizzle/shared/src/helpers}/pagination.ts +17 -3
  10. package/registry/combos/express-drizzle/shared/src/middleware/auth-core-error.middleware.ts +6 -8
  11. package/registry/combos/express-drizzle/shared/src/middleware/auth.middleware.ts +6 -8
  12. package/registry/combos/{express-prisma/shared/src → express-drizzle/shared/src/openapi}/openapi-fragment.ts +13 -4
  13. package/registry/combos/express-drizzle/shared/src/{openapi-spec.ts → openapi/openapi-spec.ts} +251 -52
  14. package/registry/combos/express-drizzle/shared/src/repositories/oauth.repository.ts +2 -2
  15. package/registry/combos/express-drizzle/shared/src/repositories/password-reset.repository.ts +2 -4
  16. package/registry/combos/express-drizzle/shared/src/repositories/session.repository.ts +3 -3
  17. package/registry/combos/express-drizzle/shared/src/request-context.ts +1 -1
  18. package/registry/combos/express-drizzle/shared/src/route-tiers.ts +52 -11
  19. package/registry/combos/express-drizzle/shared/src/routers/auth.router.ts +2 -2
  20. package/registry/combos/express-drizzle/test/bootstrap.ts +4 -2
  21. package/registry/combos/express-drizzle/test/prove-cycle.ts +1095 -308
  22. package/registry/combos/express-drizzle/variants/base/src/create-auth-app.ts +5 -5
  23. package/registry/combos/express-drizzle/variants/base/src/index.ts +8 -0
  24. package/registry/combos/express-drizzle/variants/base/src/middleware/authz.middleware.ts +2 -2
  25. package/registry/combos/express-drizzle/variants/base/src/openapi/openapi-admin.ts +1082 -0
  26. package/registry/combos/express-drizzle/variants/base/src/rbac.defaults.ts +60 -17
  27. package/registry/combos/express-drizzle/variants/base/src/repositories/audit-log.repository.ts +2 -2
  28. package/registry/combos/express-drizzle/variants/base/src/repositories/rbac.repository.ts +5 -5
  29. package/registry/combos/express-drizzle/variants/base/src/routers/admin.router.ts +18 -22
  30. package/registry/combos/express-drizzle/variants/base/src/seed.ts +76 -17
  31. package/registry/combos/express-drizzle/variants/base/src/services/auth.service.ts +7 -9
  32. package/registry/combos/express-drizzle/variants/base/test/variant-hooks.ts +167 -45
  33. package/registry/combos/express-drizzle/variants/workspaces/src/create-auth-app.ts +5 -5
  34. package/registry/combos/express-drizzle/variants/workspaces/src/index.ts +8 -0
  35. package/registry/combos/express-drizzle/variants/workspaces/src/middleware/authz.middleware.ts +3 -3
  36. package/registry/combos/express-drizzle/variants/workspaces/src/openapi/openapi-admin.ts +1176 -0
  37. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-workspace.ts +1 -1
  38. package/registry/combos/express-drizzle/variants/workspaces/src/rbac.defaults.ts +66 -19
  39. package/registry/combos/express-drizzle/variants/workspaces/src/repositories/audit-log.repository.ts +2 -2
  40. package/registry/combos/express-drizzle/variants/workspaces/src/repositories/rbac.repository.ts +5 -5
  41. package/registry/combos/express-drizzle/variants/workspaces/src/repositories/workspace.repository.ts +9 -11
  42. package/registry/combos/express-drizzle/variants/workspaces/src/routers/admin.router.ts +18 -22
  43. package/registry/combos/express-drizzle/variants/workspaces/src/routers/workspace.router.ts +1 -1
  44. package/registry/combos/express-drizzle/variants/workspaces/src/seed.ts +168 -35
  45. package/registry/combos/express-drizzle/variants/workspaces/src/services/auth.service.ts +7 -9
  46. package/registry/combos/express-drizzle/variants/workspaces/test/variant-hooks.ts +539 -133
  47. package/registry/combos/express-prisma/shared/src/{ability.ts → ability/ability.ts} +8 -2
  48. package/registry/combos/express-prisma/shared/src/{permission-cache.ts → cache/permission-cache.ts} +25 -9
  49. package/registry/combos/express-prisma/shared/src/{rate-limit.store.ts → cache/rate-limit.store.ts} +4 -1
  50. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/config}/auth.config.ts +1 -1
  51. package/registry/combos/express-prisma/shared/src/{key-provider.ts → config/key-provider.ts} +6 -2
  52. package/registry/combos/{nestjs-prisma/shared/src → express-prisma/shared/src/helpers}/id.helper.ts +6 -2
  53. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/helpers}/pagination.ts +17 -3
  54. package/registry/combos/express-prisma/shared/src/middleware/auth-core-error.middleware.ts +6 -8
  55. package/registry/combos/express-prisma/shared/src/middleware/auth.middleware.ts +6 -8
  56. package/registry/combos/{express-drizzle/shared/src → express-prisma/shared/src/openapi}/openapi-fragment.ts +13 -4
  57. package/registry/combos/express-prisma/shared/src/{openapi-spec.ts → openapi/openapi-spec.ts} +313 -55
  58. package/registry/combos/express-prisma/shared/src/repositories/oauth.repository.ts +2 -2
  59. package/registry/combos/express-prisma/shared/src/repositories/password-reset.repository.ts +2 -4
  60. package/registry/combos/express-prisma/shared/src/repositories/session.repository.ts +2 -2
  61. package/registry/combos/express-prisma/shared/src/request-context.ts +1 -1
  62. package/registry/combos/express-prisma/shared/src/route-tiers.ts +52 -11
  63. package/registry/combos/express-prisma/shared/src/routers/auth.router.ts +2 -2
  64. package/registry/combos/express-prisma/test/bootstrap.ts +7 -3
  65. package/registry/combos/express-prisma/test/prove-cycle.ts +1095 -308
  66. package/registry/combos/express-prisma/variants/base/src/create-auth-app.ts +6 -6
  67. package/registry/combos/express-prisma/variants/base/src/index.ts +8 -0
  68. package/registry/combos/express-prisma/variants/base/src/middleware/authz.middleware.ts +2 -2
  69. package/registry/combos/express-prisma/variants/base/src/openapi/openapi-admin.ts +1102 -0
  70. package/registry/combos/express-prisma/variants/base/src/rbac.defaults.ts +1 -1
  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/seed.ts +1 -1
  75. package/registry/combos/express-prisma/variants/base/src/services/auth.service.ts +6 -8
  76. package/registry/combos/express-prisma/variants/base/test/variant-hooks.ts +176 -47
  77. package/registry/combos/express-prisma/variants/workspaces/src/create-auth-app.ts +6 -6
  78. package/registry/combos/express-prisma/variants/workspaces/src/index.ts +8 -0
  79. package/registry/combos/express-prisma/variants/workspaces/src/middleware/authz.middleware.ts +3 -3
  80. package/registry/combos/express-prisma/variants/workspaces/src/openapi/openapi-admin.ts +1218 -0
  81. package/registry/combos/express-prisma/variants/workspaces/src/openapi-workspace.ts +1 -1
  82. package/registry/combos/express-prisma/variants/workspaces/src/rbac.defaults.ts +1 -1
  83. package/registry/combos/express-prisma/variants/workspaces/src/repositories/audit-log.repository.ts +2 -2
  84. package/registry/combos/express-prisma/variants/workspaces/src/repositories/rbac.repository.ts +4 -4
  85. package/registry/combos/express-prisma/variants/workspaces/src/repositories/workspace.repository.ts +2 -2
  86. package/registry/combos/express-prisma/variants/workspaces/src/routers/admin.router.ts +18 -22
  87. package/registry/combos/express-prisma/variants/workspaces/src/routers/workspace.router.ts +1 -1
  88. package/registry/combos/express-prisma/variants/workspaces/src/seed.ts +1 -1
  89. package/registry/combos/express-prisma/variants/workspaces/src/services/auth.service.ts +6 -8
  90. package/registry/combos/express-prisma/variants/workspaces/test/variant-hooks.ts +538 -134
  91. package/registry/combos/nestjs-drizzle/shared/src/{guards → ability}/ability.guard.ts +1 -1
  92. package/registry/combos/nestjs-drizzle/shared/src/{ability.ts → ability/ability.ts} +8 -2
  93. package/registry/combos/nestjs-drizzle/shared/src/{permission-cache.ts → cache/permission-cache.ts} +27 -10
  94. package/registry/combos/{nestjs-prisma/shared/src → nestjs-drizzle/shared/src/cache}/rate-limit.store.ts +4 -1
  95. package/registry/combos/nestjs-drizzle/shared/src/{auth.config.ts → config/auth.config.ts} +1 -1
  96. package/registry/combos/nestjs-drizzle/shared/src/{db.ts → config/db.ts} +1 -1
  97. package/registry/combos/nestjs-drizzle/shared/src/{key-provider.ts → config/key-provider.ts} +6 -2
  98. package/registry/combos/nestjs-drizzle/shared/src/controllers/auth.controller.ts +1 -1
  99. package/registry/combos/nestjs-drizzle/shared/src/guards/auth.guard.ts +1 -1
  100. package/registry/combos/nestjs-drizzle/shared/src/{id.helper.ts → helpers/id.helper.ts} +6 -2
  101. package/registry/combos/{express-prisma/shared/src → nestjs-drizzle/shared/src/helpers}/pagination.ts +17 -3
  102. package/registry/combos/nestjs-drizzle/shared/src/repositories/oauth.repository.ts +2 -2
  103. package/registry/combos/nestjs-drizzle/shared/src/repositories/password-reset.repository.ts +8 -12
  104. package/registry/combos/nestjs-drizzle/shared/src/repositories/session.repository.ts +3 -3
  105. package/registry/combos/nestjs-drizzle/shared/src/repositories/two-factor.repository.ts +1 -1
  106. package/registry/combos/nestjs-drizzle/shared/src/request-context.ts +1 -1
  107. package/registry/combos/nestjs-drizzle/test/bootstrap.ts +1 -1
  108. package/registry/combos/nestjs-drizzle/test/prove-cycle.ts +1 -1
  109. package/registry/combos/nestjs-drizzle/variants/base/src/auth.module.ts +10 -6
  110. package/registry/combos/nestjs-drizzle/variants/base/src/controllers/admin.controller.ts +1 -1
  111. package/registry/combos/nestjs-drizzle/variants/base/src/guards/authz.guard.ts +2 -2
  112. package/registry/combos/nestjs-drizzle/variants/base/src/index.ts +9 -0
  113. package/registry/combos/nestjs-drizzle/variants/base/src/rbac.defaults.ts +58 -16
  114. package/registry/combos/nestjs-drizzle/variants/base/src/repositories/audit-log.repository.ts +3 -3
  115. package/registry/combos/nestjs-drizzle/variants/base/src/repositories/rbac.repository.ts +4 -4
  116. package/registry/combos/nestjs-drizzle/variants/base/src/seed.ts +76 -17
  117. package/registry/combos/nestjs-drizzle/variants/base/src/services/auth.service.ts +7 -9
  118. package/registry/combos/nestjs-drizzle/variants/base/test/variant-hooks.ts +167 -45
  119. package/registry/combos/nestjs-drizzle/variants/workspaces/src/auth.module.ts +10 -6
  120. package/registry/combos/nestjs-drizzle/variants/workspaces/src/controllers/admin.controller.ts +1 -1
  121. package/registry/combos/nestjs-drizzle/variants/workspaces/src/controllers/workspace.controller.ts +1 -1
  122. package/registry/combos/nestjs-drizzle/variants/workspaces/src/guards/authz.guard.ts +2 -2
  123. package/registry/combos/nestjs-drizzle/variants/workspaces/src/index.ts +9 -0
  124. package/registry/combos/nestjs-drizzle/variants/workspaces/src/rbac.defaults.ts +64 -18
  125. package/registry/combos/nestjs-drizzle/variants/workspaces/src/repositories/audit-log.repository.ts +3 -3
  126. package/registry/combos/nestjs-drizzle/variants/workspaces/src/repositories/rbac.repository.ts +4 -4
  127. package/registry/combos/nestjs-drizzle/variants/workspaces/src/repositories/workspace.repository.ts +8 -10
  128. package/registry/combos/nestjs-drizzle/variants/workspaces/src/seed.ts +171 -36
  129. package/registry/combos/nestjs-drizzle/variants/workspaces/src/services/auth.service.ts +7 -9
  130. package/registry/combos/nestjs-drizzle/variants/workspaces/test/variant-hooks.ts +539 -133
  131. package/registry/combos/nestjs-prisma/shared/src/{guards → ability}/ability.guard.ts +1 -1
  132. package/registry/combos/nestjs-prisma/shared/src/{ability.ts → ability/ability.ts} +8 -2
  133. package/registry/combos/nestjs-prisma/shared/src/{permission-cache.ts → cache/permission-cache.ts} +27 -10
  134. package/registry/combos/{nestjs-drizzle/shared/src → nestjs-prisma/shared/src/cache}/rate-limit.store.ts +4 -1
  135. package/registry/combos/nestjs-prisma/shared/src/{auth.config.ts → config/auth.config.ts} +1 -1
  136. package/registry/combos/nestjs-prisma/shared/src/{key-provider.ts → config/key-provider.ts} +6 -2
  137. package/registry/combos/nestjs-prisma/shared/src/controllers/auth.controller.ts +1 -1
  138. package/registry/combos/nestjs-prisma/shared/src/guards/auth.guard.ts +1 -1
  139. package/registry/combos/{express-prisma/shared/src → nestjs-prisma/shared/src/helpers}/id.helper.ts +6 -2
  140. package/registry/combos/nestjs-prisma/shared/src/{pagination.ts → helpers/pagination.ts} +17 -3
  141. package/registry/combos/nestjs-prisma/shared/src/repositories/oauth.repository.ts +1 -1
  142. package/registry/combos/nestjs-prisma/shared/src/repositories/password-reset.repository.ts +2 -4
  143. package/registry/combos/nestjs-prisma/shared/src/repositories/session.repository.ts +2 -2
  144. package/registry/combos/nestjs-prisma/shared/src/request-context.ts +1 -1
  145. package/registry/combos/nestjs-prisma/test/bootstrap.ts +1 -1
  146. package/registry/combos/nestjs-prisma/test/prove-cycle.ts +1 -1
  147. package/registry/combos/nestjs-prisma/variants/base/src/auth.module.ts +10 -6
  148. package/registry/combos/nestjs-prisma/variants/base/src/controllers/admin.controller.ts +1 -1
  149. package/registry/combos/nestjs-prisma/variants/base/src/gateways/audit-log.gateway.ts +1 -1
  150. package/registry/combos/nestjs-prisma/variants/base/src/guards/authz.guard.ts +2 -2
  151. package/registry/combos/nestjs-prisma/variants/base/src/index.ts +9 -0
  152. package/registry/combos/nestjs-prisma/variants/base/src/{docs.ts → openapi/docs.ts} +23 -6
  153. package/registry/combos/nestjs-prisma/variants/base/src/rbac.defaults.ts +1 -1
  154. package/registry/combos/nestjs-prisma/variants/base/src/repositories/audit-log.repository.ts +2 -2
  155. package/registry/combos/nestjs-prisma/variants/base/src/repositories/country.repository.ts +2 -2
  156. package/registry/combos/nestjs-prisma/variants/base/src/repositories/customer.repository.ts +6 -5
  157. package/registry/combos/nestjs-prisma/variants/base/src/repositories/language.repository.ts +2 -2
  158. package/registry/combos/nestjs-prisma/variants/base/src/repositories/rbac.repository.ts +3 -3
  159. package/registry/combos/nestjs-prisma/variants/base/src/seed.ts +1 -1
  160. package/registry/combos/nestjs-prisma/variants/base/src/services/auth.service.ts +6 -8
  161. package/registry/combos/nestjs-prisma/variants/base/test/variant-hooks.ts +176 -47
  162. package/registry/combos/nestjs-prisma/variants/workspaces/src/auth.module.ts +10 -6
  163. package/registry/combos/nestjs-prisma/variants/workspaces/src/controllers/admin.controller.ts +1 -1
  164. package/registry/combos/nestjs-prisma/variants/workspaces/src/controllers/workspace.controller.ts +1 -1
  165. package/registry/combos/nestjs-prisma/variants/workspaces/src/guards/authz.guard.ts +2 -2
  166. package/registry/combos/nestjs-prisma/variants/workspaces/src/index.ts +9 -0
  167. package/registry/combos/nestjs-prisma/variants/workspaces/src/rbac.defaults.ts +1 -1
  168. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/audit-log.repository.ts +2 -2
  169. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/country.repository.ts +2 -2
  170. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/customer.repository.ts +6 -5
  171. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/language.repository.ts +2 -2
  172. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/rbac.repository.ts +3 -3
  173. package/registry/combos/nestjs-prisma/variants/workspaces/src/repositories/workspace.repository.ts +1 -1
  174. package/registry/combos/nestjs-prisma/variants/workspaces/src/seed.ts +1 -1
  175. package/registry/combos/nestjs-prisma/variants/workspaces/src/services/auth.service.ts +6 -8
  176. package/registry/combos/nestjs-prisma/variants/workspaces/test/variant-hooks.ts +538 -134
  177. package/registry/combos/express-drizzle/variants/base/src/openapi-admin.ts +0 -565
  178. package/registry/combos/express-drizzle/variants/workspaces/src/openapi-admin.ts +0 -596
  179. package/registry/combos/express-prisma/variants/base/src/openapi-admin.ts +0 -569
  180. package/registry/combos/express-prisma/variants/workspaces/src/openapi-admin.ts +0 -601
  181. /package/registry/combos/express-drizzle/shared/src/{http-error.ts → errors/http-error.ts} +0 -0
  182. /package/registry/combos/express-prisma/shared/src/{http-error.ts → errors/http-error.ts} +0 -0
@@ -7,14 +7,24 @@ import "dotenv/config";
7
7
  import { eq, inArray } from "drizzle-orm";
8
8
  import { drizzle } from "drizzle-orm/node-postgres";
9
9
  import { Pool } from "pg";
10
- import type { Database } from "../src/db.js";
11
- import { toId } from "../src/id.helper.js";
12
- import { POLICY_VERSION_KEY } from "../src/permission-cache.js";
13
- import { provisionDefaultRoles, SEED_ADMIN_ROLES } from "../src/rbac.defaults.js";
10
+ import type { Database } from "../src/config/db.js";
11
+ import { toId } from "../src/helpers/id.helper.js";
12
+ import { POLICY_VERSION_KEY } from "../src/cache/permission-cache.js";
13
+ import {
14
+ provisionDefaultRoles,
15
+ SEED_ADMIN_ROLES,
16
+ } from "../src/rbac.defaults.js";
14
17
  import * as schema from "../src/schema.js";
15
18
  import { permissions, roleUser, roles } from "../src/schema.js";
16
19
  import { permissionCacheStore } from "./bootstrap.js";
17
- import { renewingToken, type AdminSession, type AuthTokens, type Principal, type ProofContext, type VariantHooks } from "./harness.js";
20
+ import {
21
+ renewingToken,
22
+ type AdminSession,
23
+ type AuthTokens,
24
+ type Principal,
25
+ type ProofContext,
26
+ type VariantHooks,
27
+ } from "./harness.js";
18
28
 
19
29
  /** One short-lived pool per call, closed on the way out, so the proof process has no lingering handle to wait on. */
20
30
  async function withDb<T>(fn: (db: Database) => Promise<T>): Promise<T> {
@@ -32,7 +42,10 @@ let adminCredentials: { identifier: string; password: string } | undefined;
32
42
  export const hooks: VariantHooks = {
33
43
  variant: "base",
34
44
 
35
- async makeAdmin(ctx: ProofContext, principal: Principal): Promise<AdminSession> {
45
+ async makeAdmin(
46
+ ctx: ProofContext,
47
+ principal: Principal,
48
+ ): Promise<AdminSession> {
36
49
  // The role slug on its own confers nothing — no guard checks for the string "admin". Authority
37
50
  // comes from the permission_role rows behind it, and in this variant the seeder is the only
38
51
  // thing that writes them, so the hook does what `npm run seed` does: calls the same
@@ -46,24 +59,36 @@ export const hooks: VariantHooks = {
46
59
  .where(inArray(roles.slug, SEED_ADMIN_ROLES));
47
60
  await db
48
61
  .insert(roleUser)
49
- .values(adminRoles.map((role) => ({ userId: toId(principal.userId), roleId: role.id })))
62
+ .values(
63
+ adminRoles.map((role) => ({
64
+ userId: toId(principal.userId),
65
+ roleId: role.id,
66
+ })),
67
+ )
50
68
  .onConflictDoNothing({ target: [roleUser.userId, roleUser.roleId] });
51
69
  });
52
70
 
53
- adminCredentials = { identifier: principal.email, password: principal.password };
71
+ adminCredentials = {
72
+ identifier: principal.email,
73
+ password: principal.password,
74
+ };
54
75
  // Those writes went straight to the database, behind the running app's back, so nothing
55
76
  // bumped a version counter and the caller's already-cached (empty) permissions would still be
56
77
  // served. Bumping the policy counter is exactly what an out-of-band writer is supposed to do —
57
78
  // see permission-cache.ts. Logging in would not have helped: nothing about authorization is
58
79
  // in the token.
59
80
  await permissionCacheStore.bump(POLICY_VERSION_KEY);
60
- const login = await ctx.call("POST", "/auth/login", { body: { identifier: principal.email, password: principal.password } });
81
+ const login = await ctx.call("POST", "/auth/login", {
82
+ body: { identifier: principal.email, password: principal.password },
83
+ });
61
84
  const token = (login.body as AuthTokens).accessToken;
62
85
  ctx.assert(!!token, `the seeded admin can log in (got ${login.status})`);
63
86
  return {
64
87
  token,
65
88
  freshToken: renewingToken(async () => {
66
- const res = await ctx.call("POST", "/auth/login", { body: adminCredentials! });
89
+ const res = await ctx.call("POST", "/auth/login", {
90
+ body: adminCredentials!,
91
+ });
67
92
  return (res.body as AuthTokens).accessToken;
68
93
  }),
69
94
  };
@@ -73,16 +98,37 @@ export const hooks: VariantHooks = {
73
98
  // Nothing to do: admin endpoints operate on the whole deployment.
74
99
  },
75
100
 
76
- async proveVariantProperties(ctx: ProofContext, admin: AdminSession): Promise<void> {
77
- const listed = await ctx.call("GET", "/auth/admin/users", { token: await admin.freshToken() });
78
- ctx.assert(listed.status === 200, `admin can list every user in the deployment (got ${listed.status})`);
79
- ctx.assert(Array.isArray(listed.body?.items) && listed.body.items.length > 0, "the user list is non-empty");
101
+ async proveVariantProperties(
102
+ ctx: ProofContext,
103
+ admin: AdminSession,
104
+ ): Promise<void> {
105
+ const listed = await ctx.call("GET", "/auth/admin/users", {
106
+ token: await admin.freshToken(),
107
+ });
108
+ ctx.assert(
109
+ listed.status === 200,
110
+ `admin can list every user in the deployment (got ${listed.status})`,
111
+ );
112
+ ctx.assert(
113
+ Array.isArray(listed.body?.items) && listed.body.items.length > 0,
114
+ "the user list is non-empty",
115
+ );
80
116
 
81
- const workspaces = await ctx.call("GET", "/workspaces", { token: await admin.freshToken() });
82
- ctx.assert(workspaces.status === 404, `no workspace endpoints exist in this variant (got ${workspaces.status})`);
117
+ const workspaces = await ctx.call("GET", "/workspaces", {
118
+ token: await admin.freshToken(),
119
+ });
120
+ ctx.assert(
121
+ workspaces.status === 404,
122
+ `no workspace endpoints exist in this variant (got ${workspaces.status})`,
123
+ );
83
124
 
84
- const me = await ctx.call("GET", "/auth/me", { token: await admin.freshToken() });
85
- ctx.assert(me.body?.roles?.includes("admin"), "roles are global: they apply to every request this user makes");
125
+ const me = await ctx.call("GET", "/auth/me", {
126
+ token: await admin.freshToken(),
127
+ });
128
+ ctx.assert(
129
+ me.body?.roles?.includes("admin"),
130
+ "roles are global: they apply to every request this user makes",
131
+ );
86
132
 
87
133
  // --- authorization is not in the token, so a change lands on the next *request* ---
88
134
  //
@@ -96,24 +142,55 @@ export const hooks: VariantHooks = {
96
142
  const email = ctx.uniqueEmail("immediate");
97
143
  const password = "immediate-pw-12345";
98
144
  const tokens = await ctx.signup(email, password);
99
- const userId = (await ctx.call("GET", "/auth/me", { token: tokens.accessToken })).body.sub as string;
100
-
101
- const beforeGrant = await ctx.call("GET", "/auth/admin/users", { token: tokens.accessToken });
102
- ctx.assert(beforeGrant.status === 403, `a user with no grant is refused (got ${beforeGrant.status})`);
145
+ const userId = (
146
+ await ctx.call("GET", "/auth/me", { token: tokens.accessToken })
147
+ ).body.sub as string;
103
148
 
104
- const granted = await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, { token: await admin.freshToken(), body: { permission: "users:read" } });
105
- ctx.assert(granted.status === 200 || granted.status === 201, `admin can grant a permission directly (got ${granted.status})`);
149
+ const beforeGrant = await ctx.call("GET", "/auth/admin/users", {
150
+ token: tokens.accessToken,
151
+ });
152
+ ctx.assert(
153
+ beforeGrant.status === 403,
154
+ `a user with no grant is refused (got ${beforeGrant.status})`,
155
+ );
106
156
 
107
- const afterGrant = await ctx.call("GET", "/auth/admin/users", { token: tokens.accessToken });
108
- 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})`);
157
+ const granted = await ctx.call(
158
+ "POST",
159
+ `/auth/admin/users/${userId}/permissions`,
160
+ { token: await admin.freshToken(), body: { permission: "users:read" } },
161
+ );
162
+ ctx.assert(
163
+ granted.status === 200 || granted.status === 201,
164
+ `admin can grant a permission directly (got ${granted.status})`,
165
+ );
109
166
 
110
- const revoked = await ctx.call("POST", `/auth/admin/users/${userId}/permissions/${encodeURIComponent("users:read")}/revoke`, {
111
- token: await admin.freshToken(),
167
+ const afterGrant = await ctx.call("GET", "/auth/admin/users", {
168
+ token: tokens.accessToken,
112
169
  });
113
- ctx.assert(revoked.status === 200 || revoked.status === 201, `admin can revoke the direct grant (got ${revoked.status})`);
170
+ ctx.assert(
171
+ afterGrant.status === 200,
172
+ `…and the grant is in effect on the very next request, on the token they already held (got ${afterGrant.status})`,
173
+ );
114
174
 
115
- const afterRevoke = await ctx.call("GET", "/auth/admin/users", { token: tokens.accessToken });
116
- ctx.assert(afterRevoke.status === 403, `and the revocation lands on the very next request too, on the same token (got ${afterRevoke.status})`);
175
+ const revoked = await ctx.call(
176
+ "POST",
177
+ `/auth/admin/users/${userId}/permissions/${encodeURIComponent("users:read")}/revoke`,
178
+ {
179
+ token: await admin.freshToken(),
180
+ },
181
+ );
182
+ ctx.assert(
183
+ revoked.status === 200 || revoked.status === 201,
184
+ `admin can revoke the direct grant (got ${revoked.status})`,
185
+ );
186
+
187
+ const afterRevoke = await ctx.call("GET", "/auth/admin/users", {
188
+ token: tokens.accessToken,
189
+ });
190
+ ctx.assert(
191
+ afterRevoke.status === 403,
192
+ `and the revocation lands on the very next request too, on the same token (got ${afterRevoke.status})`,
193
+ );
117
194
 
118
195
  await proveTheCacheIsReal(ctx, admin, tokens.accessToken);
119
196
  await proveARawDatabaseEditChangesEnforcement(ctx, admin);
@@ -125,7 +202,11 @@ export const hooks: VariantHooks = {
125
202
  * app resolves through (`test/bootstrap.ts` hands it to `AuthModule.forRoot`), so "the second
126
203
  * request did not touch the database" is a measurement rather than a claim.
127
204
  */
128
- async function proveTheCacheIsReal(ctx: ProofContext, admin: AdminSession, token: string): Promise<void> {
205
+ async function proveTheCacheIsReal(
206
+ ctx: ProofContext,
207
+ admin: AdminSession,
208
+ token: string,
209
+ ): Promise<void> {
129
210
  const N = 8;
130
211
  await ctx.call("GET", "/auth/me", { token }); // warm, so the measurement is of the steady state
131
212
  const before = { ...permissionCacheStore.stats };
@@ -137,16 +218,27 @@ async function proveTheCacheIsReal(ctx: ProofContext, admin: AdminSession, token
137
218
  `${N} identical authorized requests resolve permissions 0 further times — all ${N} were cache hits (misses +${after.misses - before.misses}, hits +${after.hits - before.hits})`,
138
219
  );
139
220
 
140
- const userId = (await ctx.call("GET", "/auth/me", { token })).body.sub as string;
221
+ const userId = (await ctx.call("GET", "/auth/me", { token })).body
222
+ .sub as string;
141
223
  const beforeGrant = { ...permissionCacheStore.stats };
142
- await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, { token: await admin.freshToken(), body: { permission: "probe:cache" } });
224
+ await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, {
225
+ token: await admin.freshToken(),
226
+ body: { permission: "probe:cache" },
227
+ });
143
228
  const afterGrantCall = await ctx.call("GET", "/auth/me", { token });
144
229
  ctx.assert(
145
230
  permissionCacheStore.stats.misses > beforeGrant.misses,
146
231
  "a grant bumps this user's version counter, so the next request misses the cache and re-resolves rather than serving a stale entry",
147
232
  );
148
- ctx.assert((afterGrantCall.body?.permissions ?? []).includes("probe:cache"), "…and the freshly resolved answer contains the new permission");
149
- await ctx.call("POST", `/auth/admin/users/${userId}/permissions/${encodeURIComponent("probe:cache")}/revoke`, { token: await admin.freshToken() });
233
+ ctx.assert(
234
+ (afterGrantCall.body?.permissions ?? []).includes("probe:cache"),
235
+ "…and the freshly resolved answer contains the new permission",
236
+ );
237
+ await ctx.call(
238
+ "POST",
239
+ `/auth/admin/users/${userId}/permissions/${encodeURIComponent("probe:cache")}/revoke`,
240
+ { token: await admin.freshToken() },
241
+ );
150
242
  }
151
243
 
152
244
  /**
@@ -154,26 +246,56 @@ async function proveTheCacheIsReal(ctx: ProofContext, admin: AdminSession, token
154
246
  * doing the writing — this rules out any possibility that it is also doing something in memory.
155
247
  * A `psql` session would look exactly like this.
156
248
  */
157
- async function proveARawDatabaseEditChangesEnforcement(ctx: ProofContext, admin: AdminSession): Promise<void> {
249
+ async function proveARawDatabaseEditChangesEnforcement(
250
+ ctx: ProofContext,
251
+ admin: AdminSession,
252
+ ): Promise<void> {
158
253
  const email = ctx.uniqueEmail("rawedit");
159
254
  const tokens = await ctx.signup(email, "rawedit-pw-12345");
160
- const userId = (await ctx.call("GET", "/auth/me", { token: tokens.accessToken })).body.sub as string;
161
- await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, { token: await admin.freshToken(), body: { permission: "audit-log:read" } });
162
- ctx.assert((await ctx.call("GET", "/auth/admin/audit-log", { token: tokens.accessToken })).status === 200, "a direct grant opens the audit log");
255
+ const userId = (
256
+ await ctx.call("GET", "/auth/me", { token: tokens.accessToken })
257
+ ).body.sub as string;
258
+ await ctx.call("POST", `/auth/admin/users/${userId}/permissions`, {
259
+ token: await admin.freshToken(),
260
+ body: { permission: "audit-log:read" },
261
+ });
262
+ ctx.assert(
263
+ (
264
+ await ctx.call("GET", "/auth/admin/audit-log", {
265
+ token: tokens.accessToken,
266
+ })
267
+ ).status === 200,
268
+ "a direct grant opens the audit log",
269
+ );
163
270
 
164
271
  const setActive = async (isActive: boolean) => {
165
272
  // Written straight to the table, with no code change and no redeploy. The bump that follows is
166
273
  // the out-of-band writer's half of the cache contract (permission-cache.ts) — without it the
167
274
  // edit would still land, but only once the cached entry expired.
168
- await withDb((db) => db.update(permissions).set({ isActive }).where(eq(permissions.slug, "audit-log:read")));
275
+ await withDb((db) =>
276
+ db
277
+ .update(permissions)
278
+ .set({ isActive })
279
+ .where(eq(permissions.slug, "audit-log:read")),
280
+ );
169
281
  await permissionCacheStore.bump(POLICY_VERSION_KEY);
170
282
  };
171
283
 
172
284
  await setActive(false);
173
- const denied = await ctx.call("GET", "/auth/admin/audit-log", { token: tokens.accessToken });
174
- ctx.assert(denied.status === 403, `a permission deactivated by a raw database write stops opening its route (got ${denied.status})`);
285
+ const denied = await ctx.call("GET", "/auth/admin/audit-log", {
286
+ token: tokens.accessToken,
287
+ });
288
+ ctx.assert(
289
+ denied.status === 403,
290
+ `a permission deactivated by a raw database write stops opening its route (got ${denied.status})`,
291
+ );
175
292
 
176
293
  await setActive(true);
177
- const restored = await ctx.call("GET", "/auth/admin/audit-log", { token: tokens.accessToken });
178
- ctx.assert(restored.status === 200, `…and reactivating it in the database opens the route again (got ${restored.status})`);
294
+ const restored = await ctx.call("GET", "/auth/admin/audit-log", {
295
+ token: tokens.accessToken,
296
+ });
297
+ ctx.assert(
298
+ restored.status === 200,
299
+ `…and reactivating it in the database opens the route again (got ${restored.status})`,
300
+ );
179
301
  }
@@ -6,7 +6,7 @@ import type { RateLimitDeps } from "@/lib/auth/core/rate-limit.js";
6
6
  import { createAdminRouter } from "./routers/admin.router.js";
7
7
  import { AuditLogRepository } from "./repositories/audit-log.repository.js";
8
8
  import { authCoreErrorMiddleware } from "./middleware/auth-core-error.middleware.js";
9
- import { AuthConfig, defaultAuthConfig } from "./auth.config.js";
9
+ import { AuthConfig, defaultAuthConfig } from "./config/auth.config.js";
10
10
  import { createAuthMiddleware } from "./middleware/auth.middleware.js";
11
11
  import { createAuthRouter } from "./routers/auth.router.js";
12
12
  import { AuthService } from "./services/auth.service.js";
@@ -14,15 +14,15 @@ import {
14
14
  createAuthzMiddleware,
15
15
  createWorkspaceMiddleware,
16
16
  } from "./middleware/authz.middleware.js";
17
- import { KeyProviderService } from "./key-provider.js";
17
+ import { KeyProviderService } from "./config/key-provider.js";
18
18
  import { OAuthRepository } from "./repositories/oauth.repository.js";
19
- import { openApiSpec } from "./openapi-spec.js";
19
+ import { openApiSpec } from "./openapi/openapi-spec.js";
20
20
  import { PasswordResetRepository } from "./repositories/password-reset.repository.js";
21
21
  import {
22
22
  InMemoryPermissionCacheStore,
23
23
  PermissionCache,
24
- } from "./permission-cache.js";
25
- import { InMemoryRateLimitStore } from "./rate-limit.store.js";
24
+ } from "./cache/permission-cache.js";
25
+ import { InMemoryRateLimitStore } from "./cache/rate-limit.store.js";
26
26
  import { RbacRepository } from "./repositories/rbac.repository.js";
27
27
  import { responseEnvelope } from "./middleware/response-envelope.middleware.js";
28
28
  import * as schema from "./schema.js";
@@ -0,0 +1,8 @@
1
+ export * from "./create-auth-app.js";
2
+
3
+ export * from "./route-tiers.js";
4
+
5
+ export * from "./middleware/auth.middleware.js";
6
+ export * from "./middleware/authz.middleware.js";
7
+
8
+ export * from "./ability/ability.js";
@@ -1,7 +1,7 @@
1
1
  import type { NextFunction, Request, RequestHandler, Response } from "express";
2
- import { defineAbilitiesFor } from "../ability.js";
3
- import { HttpError } from "../http-error.js";
4
- import type { PermissionCache } from "../permission-cache.js";
2
+ import { defineAbilitiesFor } from "../ability/ability.js";
3
+ import { HttpError } from "../errors/http-error.js";
4
+ import type { PermissionCache } from "../cache/permission-cache.js";
5
5
  import {
6
6
  memberCacheKey,
7
7
  type RbacRepository,