@things-factory/auth-base 10.1.278 → 10.1.280

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 (284) hide show
  1. package/package.json +24 -7
  2. package/CHANGELOG.md +0 -55
  3. package/dist-client/actions/auth.js.map +0 -1
  4. package/dist-client/apply-domain-theme.js.map +0 -1
  5. package/dist-client/auth.js.map +0 -1
  6. package/dist-client/bootstrap.js.map +0 -1
  7. package/dist-client/directive/privileged.js.map +0 -1
  8. package/dist-client/domain-required.js.map +0 -1
  9. package/dist-client/index.js.map +0 -1
  10. package/dist-client/profiled.js.map +0 -1
  11. package/dist-client/reducers/auth.js.map +0 -1
  12. package/dist-client/tsconfig.tsbuildinfo +0 -1
  13. package/dist-client/verify-webauthn.js.map +0 -1
  14. package/dist-server/ai-entry/access-assistant.js.map +0 -1
  15. package/dist-server/ai-entry/access-entry-targets.js.map +0 -1
  16. package/dist-server/ai-entry/entry-target-shape.js.map +0 -1
  17. package/dist-server/constants/error-code.js.map +0 -1
  18. package/dist-server/constants/error-message.js.map +0 -1
  19. package/dist-server/constants/max-age.js.map +0 -1
  20. package/dist-server/controllers/access-request.js.map +0 -1
  21. package/dist-server/controllers/account-mail.js.map +0 -1
  22. package/dist-server/controllers/auth.js.map +0 -1
  23. package/dist-server/controllers/change-pwd.js.map +0 -1
  24. package/dist-server/controllers/confirm-identity.js.map +0 -1
  25. package/dist-server/controllers/delete-user.js.map +0 -1
  26. package/dist-server/controllers/passkey.js.map +0 -1
  27. package/dist-server/controllers/password-setup.js.map +0 -1
  28. package/dist-server/controllers/profile.js.map +0 -1
  29. package/dist-server/controllers/reset-password.js.map +0 -1
  30. package/dist-server/controllers/signin.js.map +0 -1
  31. package/dist-server/controllers/signup.js.map +0 -1
  32. package/dist-server/controllers/unlock-user.js.map +0 -1
  33. package/dist-server/controllers/utils/find-verification-token.js.map +0 -1
  34. package/dist-server/controllers/utils/make-verification-token.js.map +0 -1
  35. package/dist-server/controllers/utils/password-rule.js.map +0 -1
  36. package/dist-server/controllers/utils/save-verification-token.js.map +0 -1
  37. package/dist-server/controllers/verification.js.map +0 -1
  38. package/dist-server/errors/auth-error.js.map +0 -1
  39. package/dist-server/errors/index.js.map +0 -1
  40. package/dist-server/errors/user-domain-not-match-error.js.map +0 -1
  41. package/dist-server/index.js.map +0 -1
  42. package/dist-server/middlewares/audit-scope-middleware.js.map +0 -1
  43. package/dist-server/middlewares/authenticate-401-middleware.js.map +0 -1
  44. package/dist-server/middlewares/bypass-signin-middleware.js.map +0 -1
  45. package/dist-server/middlewares/domain-authenticate-middleware.js.map +0 -1
  46. package/dist-server/middlewares/graphql-authenticate-middleware.js.map +0 -1
  47. package/dist-server/middlewares/index.js.map +0 -1
  48. package/dist-server/middlewares/jwt-authenticate-middleware.js.map +0 -1
  49. package/dist-server/middlewares/signin-middleware.js.map +0 -1
  50. package/dist-server/middlewares/verify-recaptcha-middleware.js.map +0 -1
  51. package/dist-server/migrations/1548206416130-SeedUser.js.map +0 -1
  52. package/dist-server/migrations/1566805283882-SeedPrivilege.js.map +0 -1
  53. package/dist-server/migrations/1745000000000-MigrateDomainOwnersToTable.js.map +0 -1
  54. package/dist-server/migrations/index.js.map +0 -1
  55. package/dist-server/role-tasks.js.map +0 -1
  56. package/dist-server/router/auth-checkin-router.js.map +0 -1
  57. package/dist-server/router/auth-private-process-router.js.map +0 -1
  58. package/dist-server/router/auth-public-process-router.js.map +0 -1
  59. package/dist-server/router/auth-signin-router.js.map +0 -1
  60. package/dist-server/router/auth-signup-router.js.map +0 -1
  61. package/dist-server/router/index.js.map +0 -1
  62. package/dist-server/router/oauth2/index.js.map +0 -1
  63. package/dist-server/router/oauth2/oauth2-authorize-router.js.map +0 -1
  64. package/dist-server/router/oauth2/oauth2-router.js.map +0 -1
  65. package/dist-server/router/oauth2/oauth2-server.js.map +0 -1
  66. package/dist-server/router/oauth2/passport-oauth2-client-password.js.map +0 -1
  67. package/dist-server/router/oauth2/passport-refresh-token.js.map +0 -1
  68. package/dist-server/router/path-base-domain-router.js.map +0 -1
  69. package/dist-server/router/site-root-router.js.map +0 -1
  70. package/dist-server/router/webauthn-router.js.map +0 -1
  71. package/dist-server/routes.js.map +0 -1
  72. package/dist-server/service/access-request/access-request-mutation.js.map +0 -1
  73. package/dist-server/service/access-request/access-request-query.js.map +0 -1
  74. package/dist-server/service/access-request/access-request-types.js.map +0 -1
  75. package/dist-server/service/access-request/access-request.js.map +0 -1
  76. package/dist-server/service/access-request/index.js.map +0 -1
  77. package/dist-server/service/app-binding/app-binding-mutation.js.map +0 -1
  78. package/dist-server/service/app-binding/app-binding-query.js.map +0 -1
  79. package/dist-server/service/app-binding/app-binding-types.js.map +0 -1
  80. package/dist-server/service/app-binding/app-binding.js.map +0 -1
  81. package/dist-server/service/app-binding/index.js.map +0 -1
  82. package/dist-server/service/appliance/appliance-mutation.js.map +0 -1
  83. package/dist-server/service/appliance/appliance-query.js.map +0 -1
  84. package/dist-server/service/appliance/appliance-types.js.map +0 -1
  85. package/dist-server/service/appliance/appliance.js.map +0 -1
  86. package/dist-server/service/appliance/index.js.map +0 -1
  87. package/dist-server/service/application/application-mutation.js.map +0 -1
  88. package/dist-server/service/application/application-query.js.map +0 -1
  89. package/dist-server/service/application/application-types.js.map +0 -1
  90. package/dist-server/service/application/application.js.map +0 -1
  91. package/dist-server/service/application/index.js.map +0 -1
  92. package/dist-server/service/audit/audit-action.js.map +0 -1
  93. package/dist-server/service/audit/audit-kinds.js.map +0 -1
  94. package/dist-server/service/audit/audit-query.js.map +0 -1
  95. package/dist-server/service/audit/audit-recorder.js.map +0 -1
  96. package/dist-server/service/audit/audit-request.js.map +0 -1
  97. package/dist-server/service/audit/audit-retention.js.map +0 -1
  98. package/dist-server/service/audit/audit-subscriber.js.map +0 -1
  99. package/dist-server/service/audit/audit-target.js.map +0 -1
  100. package/dist-server/service/audit/audit-types.js.map +0 -1
  101. package/dist-server/service/audit/audit-work.js.map +0 -1
  102. package/dist-server/service/audit/index.js.map +0 -1
  103. package/dist-server/service/auth-provider/auth-provider-mutation.js.map +0 -1
  104. package/dist-server/service/auth-provider/auth-provider-parameter-spec.js.map +0 -1
  105. package/dist-server/service/auth-provider/auth-provider-query.js.map +0 -1
  106. package/dist-server/service/auth-provider/auth-provider-type.js.map +0 -1
  107. package/dist-server/service/auth-provider/auth-provider.js.map +0 -1
  108. package/dist-server/service/auth-provider/index.js.map +0 -1
  109. package/dist-server/service/domain-generator/domain-generator-mutation.js.map +0 -1
  110. package/dist-server/service/domain-generator/domain-generator-types.js.map +0 -1
  111. package/dist-server/service/domain-generator/index.js.map +0 -1
  112. package/dist-server/service/domain-link/domain-link-mutation.js.map +0 -1
  113. package/dist-server/service/domain-link/domain-link-query.js.map +0 -1
  114. package/dist-server/service/domain-link/domain-link-types.js.map +0 -1
  115. package/dist-server/service/domain-link/domain-link.js.map +0 -1
  116. package/dist-server/service/domain-link/index.js.map +0 -1
  117. package/dist-server/service/domain-owner/domain-owner-mutation.js.map +0 -1
  118. package/dist-server/service/domain-owner/domain-owner-query.js.map +0 -1
  119. package/dist-server/service/domain-owner/domain-owner.js.map +0 -1
  120. package/dist-server/service/domain-owner/index.js.map +0 -1
  121. package/dist-server/service/granted-role/granted-role-mutation.js.map +0 -1
  122. package/dist-server/service/granted-role/granted-role-query.js.map +0 -1
  123. package/dist-server/service/granted-role/granted-role.js.map +0 -1
  124. package/dist-server/service/granted-role/index.js.map +0 -1
  125. package/dist-server/service/identity-confirmation/identity-confirmation.js.map +0 -1
  126. package/dist-server/service/index.js.map +0 -1
  127. package/dist-server/service/open-fields.js.map +0 -1
  128. package/dist-server/service/partner/index.js.map +0 -1
  129. package/dist-server/service/partner/partner-mutation.js.map +0 -1
  130. package/dist-server/service/partner/partner-query.js.map +0 -1
  131. package/dist-server/service/partner/partner-types.js.map +0 -1
  132. package/dist-server/service/partner/partner.js.map +0 -1
  133. package/dist-server/service/password-history/index.js.map +0 -1
  134. package/dist-server/service/password-history/password-history.js.map +0 -1
  135. package/dist-server/service/privilege/index.js.map +0 -1
  136. package/dist-server/service/privilege/open-field-rule.js.map +0 -1
  137. package/dist-server/service/privilege/privilege-axis.js.map +0 -1
  138. package/dist-server/service/privilege/privilege-deprecation.js.map +0 -1
  139. package/dist-server/service/privilege/privilege-directive.js.map +0 -1
  140. package/dist-server/service/privilege/privilege-mutation.js.map +0 -1
  141. package/dist-server/service/privilege/privilege-query.js.map +0 -1
  142. package/dist-server/service/privilege/privilege-rejection.js.map +0 -1
  143. package/dist-server/service/privilege/privilege-sync.js.map +0 -1
  144. package/dist-server/service/privilege/privilege-types.js.map +0 -1
  145. package/dist-server/service/privilege/privilege.js.map +0 -1
  146. package/dist-server/service/role/index.js.map +0 -1
  147. package/dist-server/service/role/role-mutation.js.map +0 -1
  148. package/dist-server/service/role/role-privilege-query.js.map +0 -1
  149. package/dist-server/service/role/role-query.js.map +0 -1
  150. package/dist-server/service/role/role-types.js.map +0 -1
  151. package/dist-server/service/role/role.js.map +0 -1
  152. package/dist-server/service/role-template/index.js.map +0 -1
  153. package/dist-server/service/role-template/role-task.js.map +0 -1
  154. package/dist-server/service/role-template/role-template-gap-resolver.js.map +0 -1
  155. package/dist-server/service/role-template/role-template-mutation.js.map +0 -1
  156. package/dist-server/service/role-template/role-template-query.js.map +0 -1
  157. package/dist-server/service/role-template/role-template-types.js.map +0 -1
  158. package/dist-server/service/role-template/role-template.js.map +0 -1
  159. package/dist-server/service/user/domain-query.js.map +0 -1
  160. package/dist-server/service/user/index.js.map +0 -1
  161. package/dist-server/service/user/user-mutation.js.map +0 -1
  162. package/dist-server/service/user/user-query.js.map +0 -1
  163. package/dist-server/service/user/user-types.js.map +0 -1
  164. package/dist-server/service/user/user.js.map +0 -1
  165. package/dist-server/service/users-auth-providers/index.js.map +0 -1
  166. package/dist-server/service/users-auth-providers/users-auth-providers.js.map +0 -1
  167. package/dist-server/service/verification-token/index.js.map +0 -1
  168. package/dist-server/service/verification-token/verification-token.js.map +0 -1
  169. package/dist-server/service/web-auth-credential/index.js.map +0 -1
  170. package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js.map +0 -1
  171. package/dist-server/service/web-auth-credential/web-auth-credential-query.js.map +0 -1
  172. package/dist-server/service/web-auth-credential/web-auth-credential.js.map +0 -1
  173. package/dist-server/tsconfig.tsbuildinfo +0 -1
  174. package/dist-server/types.js.map +0 -1
  175. package/dist-server/utils/accepts.js.map +0 -1
  176. package/dist-server/utils/access-request-review.js.map +0 -1
  177. package/dist-server/utils/access-token-cookie.js.map +0 -1
  178. package/dist-server/utils/account-lock-rule.js.map +0 -1
  179. package/dist-server/utils/assignable-roles.js.map +0 -1
  180. package/dist-server/utils/build-auth-url.js.map +0 -1
  181. package/dist-server/utils/can-user-checkin-domain.js.map +0 -1
  182. package/dist-server/utils/check-permission.js.map +0 -1
  183. package/dist-server/utils/check-user-belongs-domain.js.map +0 -1
  184. package/dist-server/utils/check-user-has-role.js.map +0 -1
  185. package/dist-server/utils/checkin-domain-facts.js.map +0 -1
  186. package/dist-server/utils/checkin-domain-rule.js.map +0 -1
  187. package/dist-server/utils/credential-at-rest.js.map +0 -1
  188. package/dist-server/utils/credential-features.js.map +0 -1
  189. package/dist-server/utils/credential-serial-rule.js.map +0 -1
  190. package/dist-server/utils/domain-ownership.js.map +0 -1
  191. package/dist-server/utils/encrypt-state.js.map +0 -1
  192. package/dist-server/utils/get-aes-256-key.js.map +0 -1
  193. package/dist-server/utils/get-domain-users.js.map +0 -1
  194. package/dist-server/utils/get-secret.js.map +0 -1
  195. package/dist-server/utils/get-user-domains.js.map +0 -1
  196. package/dist-server/utils/lock-duration.js.map +0 -1
  197. package/dist-server/utils/password-reset-rule.js.map +0 -1
  198. package/dist-server/utils/profile-user.js.map +0 -1
  199. package/dist-server/utils/refuse-role-name.js.map +0 -1
  200. package/dist-server/utils/role-name-standing.js.map +0 -1
  201. package/dist-server/utils/same-origin-redirect.js.map +0 -1
  202. package/dist-server/utils/verification-token-rule.js.map +0 -1
  203. package/spec/unit/checkin-domain.spec.ts +0 -307
  204. package/spec/unit/checkin-privilege.spec.ts +0 -205
  205. package/spec/unit/domain-owner-entity.spec.ts +0 -179
  206. package/spec/unit/domain-owner-granted.spec.ts +0 -215
  207. package/spec/unit/domain-owner-migration.spec.ts +0 -236
  208. package/spec/unit/domain-owner-mutation.spec.ts +0 -337
  209. package/spec/unit/domain-ownership.spec.ts +0 -188
  210. package/tests/access-assistant-db.test.ts +0 -155
  211. package/tests/access-entry-targets-db.test.ts +0 -176
  212. package/tests/access-request-db.test.ts +0 -394
  213. package/tests/access-request-pg.test.ts +0 -83
  214. package/tests/access-request-review.test.ts +0 -193
  215. package/tests/account-lock-db.test.ts +0 -216
  216. package/tests/account-lock-rule.test.ts +0 -141
  217. package/tests/account-mail.test.ts +0 -69
  218. package/tests/activate-page-case.test.ts +0 -53
  219. package/tests/add-user-gates.test.ts +0 -34
  220. package/tests/app-binding-db.test.ts +0 -210
  221. package/tests/app-binding-delete-db.test.ts +0 -188
  222. package/tests/appliance-credential-state-db.test.ts +0 -207
  223. package/tests/appliance-delete-db.test.ts +0 -119
  224. package/tests/appliance-revoke-db.test.ts +0 -294
  225. package/tests/appliance-schema.test.ts +0 -129
  226. package/tests/application-delete-db.test.ts +0 -190
  227. package/tests/application-token-payload-db.test.ts +0 -185
  228. package/tests/assignable-roles-db.test.ts +0 -219
  229. package/tests/audit-db.test.ts +0 -410
  230. package/tests/audit-work.test.ts +0 -58
  231. package/tests/auth-base-role-tasks.test.ts +0 -58
  232. package/tests/checkin-domain-rule.test.ts +0 -128
  233. package/tests/checkin-privilege-seam-db.test.ts +0 -158
  234. package/tests/compiled.ts +0 -41
  235. package/tests/confirm-identity-db.test.ts +0 -85
  236. package/tests/credential-at-rest-db.test.ts +0 -246
  237. package/tests/credential-features-off-db.test.ts +0 -189
  238. package/tests/credential-serial-rule.test.ts +0 -84
  239. package/tests/declare-privileges.test.ts +0 -29
  240. package/tests/domain-assignees-db.test.ts +0 -95
  241. package/tests/domain-inheritance-sentinel.test.ts +0 -93
  242. package/tests/domain-required.test.ts +0 -127
  243. package/tests/has-privilege-db.test.ts +0 -198
  244. package/tests/identity-confirmation.test.ts +0 -146
  245. package/tests/list-inheritance-db.test.ts +0 -127
  246. package/tests/lock-recovery.test.ts +0 -206
  247. package/tests/mail-request-answers-db.test.ts +0 -142
  248. package/tests/open-doors.test.ts +0 -121
  249. package/tests/open-fields-enforcement.test.ts +0 -215
  250. package/tests/open-fields-framework.test.ts +0 -212
  251. package/tests/open-redirect-db.test.ts +0 -143
  252. package/tests/passkey-authenticator.ts +0 -104
  253. package/tests/passkey-db.test.ts +0 -295
  254. package/tests/passkey-signin-route-db.test.ts +0 -139
  255. package/tests/password-hashing.test.ts +0 -131
  256. package/tests/password-persistence.test.ts +0 -147
  257. package/tests/password-reset-rule-db.test.ts +0 -159
  258. package/tests/password-setup-db.test.ts +0 -215
  259. package/tests/permission-gate.test.ts +0 -228
  260. package/tests/picture-reducer.test.ts +0 -25
  261. package/tests/privilege-axis.test.ts +0 -157
  262. package/tests/privilege-deprecation.test.ts +0 -65
  263. package/tests/privilege-directive.test.ts +0 -332
  264. package/tests/privilege-rejection.test.ts +0 -104
  265. package/tests/privilege-sync-db.test.ts +0 -58
  266. package/tests/profile-user.test.ts +0 -24
  267. package/tests/refusal-cause.test.ts +0 -80
  268. package/tests/refusals-have-korean.test.ts +0 -24
  269. package/tests/request-holds.test.ts +0 -53
  270. package/tests/role-inheritance-db.test.ts +0 -286
  271. package/tests/role-mutation-db.test.ts +0 -293
  272. package/tests/role-name-lookup-sentinel.test.ts +0 -79
  273. package/tests/role-name-standing.test.ts +0 -83
  274. package/tests/role-privileges-db.test.ts +0 -252
  275. package/tests/role-task.test.ts +0 -168
  276. package/tests/role-template-gap-db.test.ts +0 -268
  277. package/tests/role-template-seed-db.test.ts +0 -180
  278. package/tests/role-template.test.ts +0 -162
  279. package/tests/role-users-gate.test.ts +0 -22
  280. package/tests/signin-policy.test.ts +0 -200
  281. package/tests/signup-duplicate-db.test.ts +0 -68
  282. package/tests/token-issuance.test.ts +0 -244
  283. package/tests/user-roles-field-db.test.ts +0 -134
  284. package/tests/verification-token-db.test.ts +0 -285
@@ -1,142 +0,0 @@
1
- const mockSentMail: any[] = []
2
- let mockMailFails = false
3
-
4
- jest.mock('@things-factory/email-base', () => ({
5
- ...jest.requireActual('@things-factory/email-base'),
6
- sendEmail: async (mail: any) => {
7
- if (mockMailFails) throw new Error('smtp down')
8
- mockSentMail.push(mail)
9
- }
10
- }))
11
-
12
- import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
13
- import { loadCompiled } from './compiled'
14
- import { i18next } from '@things-factory/i18n-base/translator'
15
-
16
- const { User } = loadCompiled('service/user/user')
17
- const { authPublicProcessRouter } = loadCompiled('router/auth-public-process-router')
18
-
19
- /**
20
- * What the two "mail me" posts answer - forgot-password and resend-verification-email.
21
- *
22
- * Both set nothing when there was no account to mail, or when the mail did not go, so koa answered
23
- * a bare 404 and the page showed "Not Found" - the same words for "no such address" and "the mail
24
- * server is down". Now they give one answer whether or not the address has an account (another
25
- * would tell anyone which addresses are registered), and fail only when a mail was due and did not
26
- * go.
27
- *
28
- * The handler is taken from the router's own table, so the path is the one the page posts to.
29
- */
30
-
31
- function handler(path: string) {
32
- const layer = authPublicProcessRouter.stack.find((layer: any) => layer.path === path && layer.methods.includes('POST'))
33
-
34
- return layer.stack[layer.stack.length - 1]
35
- }
36
-
37
- async function post(path: string, body: any) {
38
- const context: any = {
39
- t: (key: string) => key,
40
- i18next,
41
- lng: 'en-US',
42
- headers: {},
43
- header: {},
44
- request: { protocol: 'https', host: 'acme.example.com', body }
45
- }
46
-
47
- await handler(path)(context, async () => {})
48
-
49
- return { status: context.status, body: context.body }
50
- }
51
-
52
- let domain: any
53
-
54
- async function person(email: string, status: string) {
55
- const salt = User.generateSalt()
56
-
57
- return await getRepository(User).save({
58
- username: email.split('@')[0],
59
- email,
60
- name: 'Kim',
61
- salt,
62
- password: User.encode('Passw0rd!', salt),
63
- status,
64
- domains: [domain]
65
- })
66
- }
67
-
68
- beforeAll(async () => {
69
- await openAuthDatabase()
70
- })
71
-
72
- beforeEach(async () => {
73
- await resetAuthDatabase()
74
- domain = await getRepository(Domain).save({ name: 'plant-a', subdomain: 'plant-a' })
75
- mockSentMail.length = 0
76
- mockMailFails = false
77
- })
78
-
79
- afterAll(async () => {
80
- await closeAuthDatabase()
81
- })
82
-
83
- describe('forgot-password', () => {
84
- const PATH = '/auth/forgot-password'
85
-
86
- it('mails a registered address', async () => {
87
- await person('kim@example.com', 'activated')
88
-
89
- expect(await post(PATH, { email: 'kim@example.com' })).toEqual({ status: 200, body: 'text.password reset email sent' })
90
- expect(mockSentMail.map(mail => mail.receiver)).toEqual(['kim@example.com'])
91
- })
92
-
93
- it('answers an unknown address the same way, and mails nobody', async () => {
94
- await person('kim@example.com', 'activated')
95
-
96
- expect(await post(PATH, { email: 'nobody@example.com' })).toEqual({ status: 200, body: 'text.password reset email sent' })
97
- expect(mockSentMail).toEqual([])
98
- })
99
-
100
- it('says so when the mail does not go', async () => {
101
- await person('kim@example.com', 'activated')
102
- mockMailFails = true
103
-
104
- expect(await post(PATH, { email: 'kim@example.com' })).toEqual({ status: 503, body: 'error.mail could not be sent' })
105
- })
106
- })
107
-
108
- describe('resend-verification-email', () => {
109
- const PATH = '/auth/resend-verification-email'
110
-
111
- it('mails an account waiting for confirmation', async () => {
112
- await person('kim@example.com', 'inactive')
113
-
114
- expect(await post(PATH, { email: 'kim@example.com' })).toEqual({ status: 200, body: 'text.verification email sent' })
115
- expect(mockSentMail.map(mail => mail.receiver)).toEqual(['kim@example.com'])
116
- })
117
-
118
- it.each([
119
- ['an unknown address', 'nobody@example.com'],
120
- ['an account already confirmed', 'lee@example.com']
121
- ])('answers %s the same way, and mails nobody', async (_, email) => {
122
- await person('lee@example.com', 'activated')
123
-
124
- expect(await post(PATH, { email })).toEqual({ status: 200, body: 'text.verification email sent' })
125
- expect(mockSentMail).toEqual([])
126
- })
127
-
128
- it('mails nobody for a post without an address', async () => {
129
- /* ⚠ TypeORM drops an undefined condition - this matched the first account and mailed it. */
130
- await person('kim@example.com', 'inactive')
131
-
132
- expect(await post(PATH, {})).toEqual({ status: 200, body: 'text.verification email sent' })
133
- expect(mockSentMail).toEqual([])
134
- })
135
-
136
- it('says so when the mail does not go', async () => {
137
- await person('kim@example.com', 'inactive')
138
- mockMailFails = true
139
-
140
- expect(await post(PATH, { email: 'kim@example.com' })).toEqual({ status: 503, body: 'error.mail could not be sent' })
141
- })
142
- })
@@ -1,121 +0,0 @@
1
- import { buildSchema } from 'type-graphql'
2
- import { GraphQLSchema } from 'graphql'
3
-
4
- import { loadCompiled } from './compiled'
5
-
6
- /**
7
- * Which of auth-base's doors are open, and why each one is allowed to be.
8
- *
9
- * ── The thing this catches ──────────────────────────────────────────────────
10
- * `@privilege` is opt-in. A root field that declares nothing is recorded as open, and that is
11
- * indistinguishable from a field somebody decided to open — no error, no warning, nothing to
12
- * grep for. So a door goes in ungated the way `deleteAppBinding` did, and only a reader who
13
- * happens to look at that one file ever finds out.
14
- *
15
- * The list below turns the absence into a statement. Anything open that is not on it fails, and
16
- * the reason has to be written before it can be added.
17
- *
18
- * ── Why this is measured and not read off the source ────────────────────────
19
- * The schema is built from `service/index.ts`'s own `resolverClasses` — the list the server
20
- * boots from — and the directive fills `PRIVILEGE_OF_FIELD` while it walks it. A regular
21
- * expression over the files would answer about the text; this answers about the schema that
22
- * actually gets served, including every field the walk reaches that a hand-written pattern
23
- * would not.
24
- *
25
- * ── What it does not cover ──────────────────────────────────────────────────
26
- * Only auth-base. The other packages have their own resolvers and their own open doors, and
27
- * `docs/design/auth-rebuild.md` §7 is where the policy for those is still undecided.
28
- */
29
-
30
- const { schema: declared } = loadCompiled('service/index')
31
- const { privilegeDirectiveResolver, privilegeOfField } = loadCompiled('service/privilege/privilege-directive')
32
-
33
- /*
34
- * Open on purpose — read from the module's own declaration (`server/service/open-fields.ts`), which is
35
- * also what the directive enforces against. It used to be a copy kept here, and two copies of one
36
- * list are two lists.
37
- */
38
- const { openFields: declaration } = loadCompiled('service/open-fields')
39
-
40
- const OPEN_BY_DECISION: Record<string, Record<string, string>> = { Query: {}, Mutation: {} }
41
-
42
- for (const [where, reason] of Object.entries(declaration.byDesign as Record<string, string>)) {
43
- const [typeName, field] = where.split('.')
44
- OPEN_BY_DECISION[typeName][field] = reason
45
- }
46
-
47
- let schema: GraphQLSchema
48
-
49
- beforeAll(async () => {
50
- schema = await buildSchema({ resolvers: declared.resolverClasses, validate: false })
51
-
52
- /* No modules: this file measures what is open, not what would be refused. */
53
- privilegeDirectiveResolver(schema, { modules: [] })
54
- })
55
-
56
- function openFields(typeName: 'Query' | 'Mutation'): string[] {
57
- const type = typeName === 'Query' ? schema.getQueryType() : schema.getMutationType()
58
-
59
- return Object.keys(type?.getFields() || {}).filter(field => {
60
- const declaredGate = privilegeOfField(typeName, field)
61
-
62
- return declaredGate.known && declaredGate['gate'] === null
63
- })
64
- }
65
-
66
- describe('every open door is one somebody chose', () => {
67
- it.each(['Query', 'Mutation'] as const)('%s', typeName => {
68
- expect(openFields(typeName).sort()).toEqual(Object.keys(OPEN_BY_DECISION[typeName]).sort())
69
- })
70
- })
71
-
72
- describe('the doors that were closed', () => {
73
- /*
74
- * ⚠ These were all open on 2026-09-20, every one of them reachable by anyone holding a valid
75
- * token. They are listed by name so that reopening one is a visible act rather than a
76
- * deletion nobody reads.
77
- *
78
- * the five auth provider writes reading the configuration needed a privilege; replacing it
79
- * did not. An auth provider is how an external identity
80
- * signs in, so adding one is not an edit to a settings row
81
- * domainUserRoleRegister makes a domain, its roles and its users. `domainRegister`
82
- * beside it makes only a domain and is super-user only
83
- * loginHistories everybody's sign-ins - now the audit's, behind `audit:query` (ADR-0141)
84
- * inviteUser
85
- */
86
- const CLOSED = {
87
- Mutation: [
88
- 'createAuthProvider',
89
- 'updateAuthProvider',
90
- 'updateMultipleAuthProvider',
91
- 'deleteAuthProvider',
92
- 'deleteAuthProviders',
93
- 'domainUserRoleRegister',
94
- 'inviteUser'
95
- ],
96
- Query: ['auditRecords', 'auditPeople', 'auditDoors', 'auditHealth']
97
- }
98
-
99
- it.each(Object.entries(CLOSED).flatMap(([t, fields]) => fields.map(f => [t, f])))('%s.%s', (typeName, field) => {
100
- const declaredGate = privilegeOfField(typeName, field)
101
-
102
- expect(declaredGate.known).toBe(true)
103
- expect(declaredGate['gate']).not.toBeNull()
104
- })
105
- })
106
-
107
- describe('what the walk itself has to be true for', () => {
108
- it('reaches the whole schema, not a handful of fields', () => {
109
- /*
110
- * If the resolver list shrank to nothing the two assertions above would pass by having
111
- * nothing to look at. This is the floor under them.
112
- */
113
- expect(Object.keys(schema.getQueryType()?.getFields() || {}).length).toBeGreaterThan(30)
114
- expect(Object.keys(schema.getMutationType()?.getFields() || {}).length).toBeGreaterThan(40)
115
- })
116
-
117
- it('tells an undeclared field from one the schema does not have', () => {
118
- /* `known: false` must never read as open — that is how a typo becomes a gate that is not there. */
119
- expect(privilegeOfField('Query', 'noSuchField')).toEqual({ known: false })
120
- })
121
- })
@@ -1,215 +0,0 @@
1
- import { buildSchema } from 'type-graphql'
2
- import { graphql, GraphQLSchema } from 'graphql'
3
- import { makeExecutableSchema } from '@graphql-tools/schema'
4
-
5
- import { loadCompiled } from './compiled'
6
-
7
- const { schema: declared } = loadCompiled('service/index')
8
- const { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } = loadCompiled('service/privilege/privilege-directive')
9
- const { coverageOf, judgeUndeclared } = loadCompiled('service/privilege/open-field-rule')
10
-
11
- /**
12
- * A field that declares no `@privilege` and that its module does not list — what happens to it
13
- * (`docs/design/auth-rebuild.md` §8 step 1; question sheet Q1 · Q2 · Q5).
14
- *
15
- * ── The four answers ────────────────────────────────────────────────────────
16
- * byDesign open, and the module says why
17
- * notYetDeclared open, and on the module's list to close
18
- * unlisted the module opted in and says nothing: boot refuses in development, the field
19
- * refuses in production
20
- * not opted in the module declared no lists at all: exactly as before
21
- *
22
- * The last one is deliberate. The four products serve resolvers of their own from another
23
- * repository, and enforcing on them the day this lands would stop all four from booting.
24
- *
25
- * These build auth-base's real schema from the `resolverClasses` the server boots from, and vary
26
- * only the module's declaration — so what is tested is the directive against real fields, not
27
- * against a model of them.
28
- */
29
-
30
- const MODULE = 'auth-base'
31
- const REAL = declared.openFields
32
-
33
- function modules(openFields: any) {
34
- return [{ name: MODULE, schema: { resolverClasses: declared.resolverClasses, openFields } }]
35
- }
36
-
37
- /** auth-base's declaration with one reason taken out — the shape of a field somebody forgot to list. */
38
- function without(where: string) {
39
- const byDesign = { ...REAL.byDesign }
40
- delete byDesign[where]
41
- return { ...REAL, byDesign }
42
- }
43
-
44
- async function built(): Promise<GraphQLSchema> {
45
- return await buildSchema({ resolvers: declared.resolverClasses, validate: false })
46
- }
47
-
48
- async function refusalCodeOf(promise: Promise<any>): Promise<string> {
49
- const result = await promise
50
- return result?.errors?.[0]?.extensions?.refusal?.code || result?.errors?.[0]?.extensions?.code || 'no error'
51
- }
52
-
53
- describe('who is covered', () => {
54
- it('attributes each root field to the module whose class declared it', async () => {
55
- const coverage = coverageOf(modules(REAL))
56
-
57
- expect(coverage.get('Query.myRoles')).toEqual({ module: MODULE, listed: 'by-design' })
58
- expect(coverage.get('Mutation.createRole')).toEqual({ module: MODULE, listed: null })
59
- })
60
-
61
- it('covers nothing for a module that declared no lists', () => {
62
- /* ⚠ The line that keeps the four products booting. */
63
- expect(coverageOf(modules(undefined)).size).toBe(0)
64
- })
65
-
66
- it('does not cover a field of a class the module did not name', () => {
67
- const coverage = coverageOf([{ name: 'other', schema: { resolverClasses: [], openFields: {} } }])
68
-
69
- expect(judgeUndeclared('Query.myRoles', coverage)).toEqual('not-opted-in')
70
- })
71
- })
72
-
73
- describe('the verdicts', () => {
74
- const coverage = () =>
75
- coverageOf(modules({ byDesign: { 'Query.myRoles': 'mine' }, notYetDeclared: ['Query.passwordRule'] }))
76
-
77
- it('open by design', () => {
78
- expect(judgeUndeclared('Query.myRoles', coverage())).toEqual('open-by-design')
79
- })
80
-
81
- it('open, not yet declared', () => {
82
- expect(judgeUndeclared('Query.passwordRule', coverage())).toEqual('open-not-yet-declared')
83
- })
84
-
85
- it('unlisted', () => {
86
- expect(judgeUndeclared('Query.hasPrivilege', coverage())).toEqual('unlisted')
87
- })
88
-
89
- it('not opted in', () => {
90
- expect(judgeUndeclared('Query.somethingElse', coverage())).toEqual('not-opted-in')
91
- })
92
- })
93
-
94
- describe('in development — the schema refuses to build', () => {
95
- it('builds with the declaration auth-base ships', async () => {
96
- /* Every undeclared field of auth-base is in `byDesign`; nothing is unlisted today. */
97
- const schema = await built()
98
-
99
- expect(() => privilegeDirectiveResolver(schema, { modules: modules(REAL), mode: 'boot' })).not.toThrow()
100
- })
101
-
102
- it('names the field that is unlisted', async () => {
103
- /* ⚠ The case the step exists for: a field arrived and nobody said who may call it. */
104
- const schema = await built()
105
-
106
- expect(() => privilegeDirectiveResolver(schema, { modules: modules(without('Query.myRoles')), mode: 'boot' })).toThrow(
107
- /Query\.myRoles/
108
- )
109
- })
110
-
111
- it('names every unlisted field at once, not the first', async () => {
112
- const schema = await built()
113
- const declaration = without('Query.myRoles')
114
- delete declaration.byDesign['Query.passwordRule']
115
-
116
- let message = ''
117
- try {
118
- privilegeDirectiveResolver(schema, { modules: modules(declaration), mode: 'boot' })
119
- } catch (e: any) {
120
- message = e.message
121
- }
122
-
123
- expect(message).toMatch(/2 field\(s\)/)
124
- expect(message).toMatch(/Query\.myRoles/)
125
- expect(message).toMatch(/Query\.passwordRule/)
126
- })
127
-
128
- it('accepts the field once it is listed as not yet declared', async () => {
129
- const schema = await built()
130
- const declaration = { ...without('Query.myRoles'), notYetDeclared: ['Query.myRoles'] }
131
-
132
- expect(() => privilegeDirectiveResolver(schema, { modules: modules(declaration), mode: 'boot' })).not.toThrow()
133
- })
134
-
135
- it('leaves a module that declared no lists exactly as it was', async () => {
136
- const schema = await built()
137
-
138
- expect(() => privilegeDirectiveResolver(schema, { modules: modules(undefined), mode: 'boot' })).not.toThrow()
139
- })
140
- })
141
-
142
- describe('in production — only that field refuses', () => {
143
- it('still builds', async () => {
144
- const schema = await built()
145
-
146
- expect(() =>
147
- privilegeDirectiveResolver(schema, { modules: modules(without('Query.passwordRule')), mode: 'request' })
148
- ).not.toThrow()
149
- })
150
-
151
- it('refuses the unlisted field, with a name the screen can read', async () => {
152
- /*
153
- * ⚠ Never the other way. An unlisted field in production answers nobody — wrong in the worst case
154
- * means one field refuses, never one field opens.
155
- */
156
- const schema = privilegeDirectiveResolver(await built(), {
157
- modules: modules(without('Query.passwordRule')),
158
- mode: 'request'
159
- })
160
-
161
- expect(await refusalCodeOf(graphql({ schema, source: '{ passwordRule { lowerCase } }', contextValue: { state: {} } }))).toEqual(
162
- 'field-declares-no-privilege'
163
- )
164
- })
165
-
166
- it('leaves a listed field answering', async () => {
167
- const schema = privilegeDirectiveResolver(await built(), {
168
- modules: modules(without('Query.myRoles')),
169
- mode: 'request'
170
- })
171
-
172
- const result = await graphql({ schema, source: '{ passwordRule { lowerCase } }', contextValue: { state: {} } })
173
-
174
- expect(result.errors?.[0]?.extensions?.refusal).toBeUndefined()
175
- })
176
- })
177
-
178
- describe('a subscription is stopped where it opens', () => {
179
- /*
180
- * ⚠ The directive used to wrap `resolve` only. A declared subscription then connected for anyone
181
- * and failed on every event — it leaked nothing and refused nothing. Built from SDL because
182
- * auth-base has no subscription of its own; the directive sees the same field configuration.
183
- */
184
- const typeDefs = `
185
- type Query { ok: Boolean }
186
- type Subscription {
187
- guarded: String @privilege(category: "board", privilege: "query")
188
- }
189
- `
190
-
191
- function schemaWith(subscribe: () => any) {
192
- return privilegeDirectiveResolver(
193
- makeExecutableSchema({
194
- typeDefs: [privilegeDirectiveTypeDefs, typeDefs],
195
- resolvers: { Query: { ok: () => true }, Subscription: { guarded: { subscribe, resolve: (v: any) => v } } }
196
- }),
197
- { modules: [] }
198
- )
199
- }
200
-
201
- it('refuses at subscribe for somebody without the privilege', async () => {
202
- let opened = false
203
- const schema = schemaWith(() => {
204
- opened = true
205
- return (async function* () {})()
206
- })
207
-
208
- const field = schema.getSubscriptionType()!.getFields().guarded as any
209
-
210
- await expect(
211
- field.subscribe({}, {}, { state: { user: { id: 'u', domains: [] }, domain: { id: 'd' } } }, {} as any)
212
- ).rejects.toBeTruthy()
213
- expect(opened).toBe(false)
214
- })
215
- })
@@ -1,212 +0,0 @@
1
- import { buildSchema, getMetadataStorage } from 'type-graphql'
2
- import { createPubSub } from '@graphql-yoga/subscription'
3
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs'
4
- import { join } from 'path'
5
-
6
- import { loadCompiled } from './compiled'
7
-
8
- const { privilegeDirectiveResolver } = loadCompiled('service/privilege/privilege-directive')
9
- const companion = loadCompiled('service/index').schema.resolverClasses
10
-
11
- /**
12
- * Every things-factory module with resolvers says which of its fields are open (§8 step 1).
13
- *
14
- * ── Two modes ───────────────────────────────────────────────────────────────
15
- * Normally this is the check. For each module that declares `schema.openFields`, the directive
16
- * must accept it in boot mode — no field is open without being listed — and no name on its lists
17
- * may be stale (a field that has since been given @privilege, or that no longer exists). A list
18
- * that keeps names after they are closed stops telling anyone what is left.
19
- *
20
- * With `GENERATE_OPEN_FIELDS=<package>[,<package>…]` it writes that module's declaration from
21
- * what its schema serves today: every undeclared root field goes under `notYetDeclared`, and the
22
- * module's `server/service/index.ts` gains the one key. Behaviour does not change — everything
23
- * that was open is listed. Sorting a field into `byDesign` with its reason, or closing it, is the
24
- * work of step 2 and is done by hand.
25
- *
26
- * ── How a module is measured ────────────────────────────────────────────────
27
- * The way the server builds it: the module's own `resolverClasses`, `buildSchema` with a pub/sub
28
- * (modules with subscriptions do not build without one), and the privilege directive's record of
29
- * what each field declares. A module with mutations and no query cannot build alone, so every
30
- * build carries auth-base's resolvers beside it, and only the module's own fields — by the class
31
- * that declared them, from type-graphql's metadata — are counted.
32
- *
33
- * ⚠ Reads `dist-server`. After editing a module, build it before this means anything.
34
- */
35
-
36
- const PACKAGES = join(__dirname, '..', '..')
37
-
38
- /**
39
- * The modules that have declared their lists. Named rather than counted, the way `screen-words` names
40
- * the packages that owe words: a module that opts in is added here in the same commit, and the day
41
- * every module with resolvers is on it, the "not yet" half of this file goes.
42
- *
43
- * 2026-09-24: auth-base only. The generator below writes the rest; running it for the framework is
44
- * the next step of §8 step 1 and was held back from the 10.1.38 cut so it could be verified first.
45
- */
46
- const OPTED_IN = ['auth-base', 'pdf']
47
-
48
- /** Modules that cannot be measured here, and why. Adding one needs its reason. */
49
- const CANNOT_MEASURE: Record<string, string> = {
50
- calendar:
51
- "its compiled entry throws on load when required on its own (`events` of `Attendee` has no inferable type). None of the four products loads it."
52
- }
53
-
54
- type Measured = { name: string; own: string[]; undeclared: string[]; schema: any }
55
-
56
- function modulesWithResolvers(): string[] {
57
- return readdirSync(PACKAGES)
58
- .filter(name => existsSync(join(PACKAGES, name, 'dist-server', 'service', 'index.js')))
59
- .filter(name => {
60
- const source = join(PACKAGES, name, 'server', 'service', 'index.ts')
61
- return existsSync(source) && readFileSync(source, 'utf-8').includes('resolverClasses')
62
- })
63
- .sort()
64
- }
65
-
66
- async function measure(name: string): Promise<Measured> {
67
- const schema = require(join(PACKAGES, name, 'dist-server', 'service', 'index.js')).schema
68
- const classes = new Set<Function>(schema?.resolverClasses || [])
69
-
70
- const storage = getMetadataStorage() as any
71
- const own: string[] = []
72
- for (const [typeName, bucket] of [
73
- ['Query', 'queries'],
74
- ['Mutation', 'mutations'],
75
- ['Subscription', 'subscriptions']
76
- ]) {
77
- for (const field of storage[bucket] || []) {
78
- if (classes.has(field.target)) own.push(`${typeName}.${field.schemaName}`)
79
- }
80
- }
81
-
82
- process['PRIVILEGE_OF_FIELD'] = {}
83
- const built = await buildSchema({
84
- resolvers: (name === 'auth-base' ? [...classes] : [...companion, ...classes]) as any,
85
- validate: false,
86
- pubSub: createPubSub() as any
87
- })
88
- privilegeDirectiveResolver(built, { modules: [] })
89
-
90
- const record = process['PRIVILEGE_OF_FIELD'] as Record<string, unknown>
91
- const undeclared = own.filter(where => record[where] === null).sort()
92
-
93
- return { name, own: own.sort(), undeclared, schema }
94
- }
95
-
96
- function declarationSource(name: string, undeclared: string[]): string {
97
- return `/**
98
- * ${name}'s root fields that carry no \`@privilege\` (auth-base \`docs/design/auth-rebuild.md\` §8).
99
- *
100
- * Written from what this module served on the day it opted in, so nothing that was open stopped
101
- * working. Every name starts under \`notYetDeclared\`: nobody has yet decided what each one guards.
102
- * Closing one means giving it \`@privilege(category: …, privilege: …)\` and taking it off this list.
103
- * A field that is open on purpose moves to \`byDesign\`, with its reason.
104
- *
105
- * A new root field that is on neither list, and has no \`@privilege\`, stops the development server
106
- * and is refused in production — see \`privilege/open-field-rule.ts\` in auth-base.
107
- */
108
- export const openFields = {
109
- byDesign: {} as Record<string, string>,
110
- notYetDeclared: [
111
- ${undeclared.map(where => ` '${where}'`).join(',\n')}${undeclared.length ? '\n' : ''} ]
112
- }
113
- `
114
- }
115
-
116
- function optIn(name: string, undeclared: string[]) {
117
- const dir = join(PACKAGES, name, 'server', 'service')
118
- writeFileSync(join(dir, 'open-fields.ts'), declarationSource(name, undeclared))
119
-
120
- const indexPath = join(dir, 'index.ts')
121
- let index = readFileSync(indexPath, 'utf-8')
122
-
123
- if (!index.includes("from './open-fields.js'")) {
124
- /* After the last import line, so the module's own import order is left as it was. */
125
- const lines = index.split('\n')
126
- let last = -1
127
- lines.forEach((line, i) => {
128
- if (/^import .* from '.*'$/.test(line) || /^} from '.*'$/.test(line)) last = i
129
- })
130
- lines.splice(last + 1, 0, "import { openFields } from './open-fields.js'")
131
- index = lines.join('\n')
132
- }
133
-
134
- if (!/export const schema = \{\n\s*openFields,/.test(index)) {
135
- index = index.replace('export const schema = {', 'export const schema = {\n openFields,')
136
- }
137
-
138
- writeFileSync(indexPath, index)
139
- }
140
-
141
- const GENERATE = (process.env.GENERATE_OPEN_FIELDS || '').split(',').map(s => s.trim()).filter(Boolean)
142
-
143
- if (GENERATE.length) {
144
- it(`writes the declarations for ${GENERATE.join(', ')}`, async () => {
145
- for (const name of GENERATE) {
146
- const measured = await measure(name)
147
- optIn(name, measured.undeclared)
148
- }
149
- }, 600000)
150
- } else {
151
- const names = modulesWithResolvers().filter(name => !(name in CANNOT_MEASURE))
152
-
153
- describe('every module with resolvers', () => {
154
- it('finds the modules it means to check', () => {
155
- /* A walk that finds nothing passes whatever the modules say. */
156
- expect(names.length).toBeGreaterThan(50)
157
- expect(names).toContain('auth-base')
158
- expect(names).toContain('shell')
159
- })
160
-
161
- it('the modules that have opted in are exactly the ones named', async () => {
162
- /*
163
- * ⚠ Both directions. A module that declares lists without being named here is caught, and so is
164
- * one named here that has lost its lists — either way the name list stops saying what is true.
165
- */
166
- const optedIn: string[] = []
167
- for (const name of names) {
168
- const schema = require(join(PACKAGES, name, 'dist-server', 'service', 'index.js')).schema
169
- if (schema?.openFields) optedIn.push(name)
170
- }
171
-
172
- expect(optedIn.sort()).toEqual([...OPTED_IN].sort())
173
- })
174
-
175
- it.each(OPTED_IN)('%s says which of its fields are open', async name => {
176
- const measured = await measure(name)
177
-
178
- expect(measured.schema?.openFields).toBeTruthy()
179
-
180
- /* The directive in boot mode is the check the development server runs. */
181
- process['PRIVILEGE_OF_FIELD'] = {}
182
- const built = await buildSchema({
183
- resolvers: (name === 'auth-base'
184
- ? measured.schema.resolverClasses
185
- : [...companion, ...measured.schema.resolverClasses]) as any,
186
- validate: false,
187
- pubSub: createPubSub() as any
188
- })
189
-
190
- expect(() =>
191
- privilegeDirectiveResolver(built, {
192
- modules: [{ name, schema: measured.schema }],
193
- mode: 'boot'
194
- })
195
- ).not.toThrow()
196
-
197
- /* No stale names: every listed field is one this module serves and has not yet closed. */
198
- const declaration = measured.schema.openFields
199
- const listed = [...Object.keys(declaration.byDesign || {}), ...(declaration.notYetDeclared || [])]
200
- const stale = listed.filter(where => !measured.undeclared.includes(where))
201
-
202
- expect(stale).toEqual([])
203
- }, 120000)
204
- })
205
-
206
- describe('what cannot be measured here', () => {
207
- it.each(Object.entries(CANNOT_MEASURE))('%s — %s', name => {
208
- /* Still listed as a module with resolvers — the day it can be measured, this fails and it moves. */
209
- expect(modulesWithResolvers()).toContain(name)
210
- })
211
- })
212
- }