@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,53 +0,0 @@
1
- /**
2
- * `requestHolds` - whether the request holds a privilege, asked as the doors ask it and once per request.
3
- */
4
-
5
- import { loadCompiled } from './compiled'
6
-
7
- const { requestHolds } = loadCompiled('utils/check-permission')
8
- const { User } = loadCompiled('service/user/user')
9
-
10
- const DOMAIN = { id: 'domain-1', subdomain: 'plant-a' } as any
11
- const USER = { id: 'user-1', domains: [DOMAIN] } as any
12
-
13
- beforeEach(() => {
14
- ;(process as any).domainOwnerGranted = jest.fn().mockResolvedValue(false)
15
- ;(process as any).superUserGranted = jest.fn().mockResolvedValue(false)
16
- })
17
-
18
- test('a role that holds the privilege answers yes, and the database is asked once per request', async () => {
19
- User.hasPrivilege = jest.fn().mockResolvedValue(true)
20
- const context = { state: { domain: DOMAIN, user: USER } }
21
- expect(await requestHolds(context, 'course', 'query')).toBe(true)
22
- expect(await requestHolds(context, 'course', 'query')).toBe(true)
23
- expect(User.hasPrivilege).toHaveBeenCalledTimes(1)
24
- })
25
-
26
- test('each privilege is asked on its own - query and mutation are two answers', async () => {
27
- User.hasPrivilege = jest.fn().mockImplementation(async (privilege: string) => privilege === 'query')
28
- const context = { state: { domain: DOMAIN, user: USER } }
29
- expect(await requestHolds(context, 'course', 'query')).toBe(true)
30
- expect(await requestHolds(context, 'course', 'mutation')).toBe(false)
31
- expect(await requestHolds(context, 'learning', 'query')).toBe(true)
32
- expect(User.hasPrivilege).toHaveBeenCalledTimes(3)
33
- })
34
-
35
- test('the domain owner holds it, as `domainOwnerGranted: true` doors let them in', async () => {
36
- User.hasPrivilege = jest.fn().mockResolvedValue(false)
37
- ;(process as any).domainOwnerGranted = jest.fn().mockResolvedValue(true)
38
- expect(await requestHolds({ state: { domain: DOMAIN, user: USER } }, 'course', 'mutation')).toBe(true)
39
- })
40
-
41
- test('a privilege prohibited for the request is not held', async () => {
42
- User.hasPrivilege = jest.fn().mockResolvedValue(true)
43
- const state = { domain: DOMAIN, user: USER, prohibitedPrivileges: [{ category: 'course', privilege: 'mutation', by: 'license' }] }
44
- expect(await requestHolds({ state }, 'course', 'mutation')).toBe(false)
45
- })
46
-
47
- test('no domain or no user holds nothing', async () => {
48
- User.hasPrivilege = jest.fn().mockResolvedValue(true)
49
- expect(await requestHolds({ state: { user: USER } }, 'course')).toBe(false)
50
- expect(await requestHolds({ state: { domain: DOMAIN } }, 'course')).toBe(false)
51
- expect(await requestHolds(undefined, 'course')).toBe(false)
52
- expect(User.hasPrivilege).not.toHaveBeenCalled()
53
- })
@@ -1,286 +0,0 @@
1
- /**
2
- * A role name that exists in this domain **and** in the one it inherits from.
3
- *
4
- * ── How the two rows come about ─────────────────────────────────────────────
5
- * Roles are visible one level up: every read is scoped to `[domain.id, domain.parentId]`. The
6
- * four duplicate checks in `role-mutation.ts` ask only about this domain, so a domain with a
7
- * parent can be given a role whose name is already visible through it. Measured 2026-09-20:
8
- * `createRole` accepts it, and both rows then come back from every list.
9
- *
10
- * ── What was settled, and when ──────────────────────────────────────────────
11
- * ADR-0062 decision 3 (2026-09-20): the platform forbids it, and the operator may say otherwise.
12
- * The name stays the same when they do — that is the point, because moving a domain off an
13
- * inherited structure means standing up the same names here first (decision 2).
14
- *
15
- * Resolving the name was already deterministic before that, and had to be either way. `In([own, parent])`
16
- * with no `ORDER BY` answered whichever the driver handed back first — the same call could
17
- * answer the parent's role on one installation and this domain's on another, and a role is what
18
- * a privilege check reads. So these cases pin **determinism and direction**, not the semantics.
19
- *
20
- * ⚠ sqlite (`tests/db.ts`). This harness cannot reproduce another driver's ordering, which is
21
- * the point: the fix is to stop depending on ordering at all, and that is visible here as the
22
- * parent's row never being the answer while this domain has one.
23
- */
24
-
25
- import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
26
- import { loadCompiled } from './compiled'
27
-
28
- const { Role } = loadCompiled('service/role/role')
29
- const { RoleQuery } = loadCompiled('service/role/role-query')
30
- const { RoleMutation } = loadCompiled('service/role/role-mutation')
31
- const { User, UserStatus } = loadCompiled('service/user/user')
32
-
33
- const query = new RoleQuery()
34
- const mutation = new RoleMutation()
35
-
36
- let parent: any
37
- let child: any
38
- let orphan: any
39
- let actor: any
40
-
41
- function context(d: any) {
42
- return {
43
- state: { domain: d, user: actor, tx: undefined },
44
- throw: (code: number, message: string) => {
45
- const error: any = new Error(message)
46
- error.status = code
47
- throw error
48
- }
49
- } as any
50
- }
51
-
52
- /** `context.state.domain` carries `parentId`; the repository does not fill it in for us. */
53
- async function withParentId(domain: any, parentId: string | null) {
54
- return { ...(await getRepository(Domain).findOneBy({ id: domain.id })), parentId }
55
- }
56
-
57
- beforeAll(async () => {
58
- await openAuthDatabase()
59
- })
60
-
61
- afterAll(async () => {
62
- await closeAuthDatabase()
63
- })
64
-
65
- beforeEach(async () => {
66
- await resetAuthDatabase()
67
-
68
- const p = await getRepository(Domain).save({ name: 'system', subdomain: 'system' })
69
- const c = await getRepository(Domain).save({ name: 'acme', subdomain: 'acme', parent: p })
70
- const o = await getRepository(Domain).save({ name: 'beta', subdomain: 'beta' })
71
-
72
- parent = await withParentId(p, null)
73
- child = await withParentId(c, p.id)
74
- orphan = await withParentId(o, null)
75
-
76
- actor = await getRepository(User).save({
77
- username: 'operator@acme.z',
78
- email: 'operator@acme.z',
79
- name: 'operator',
80
- status: UserStatus.ACTIVATED,
81
- domains: [c]
82
- })
83
- })
84
-
85
- /** 거절의 이름을 본다 — 영어 문장이 아니라 `code` 가 계약이다(ADR-0054). */
86
- async function refusalCodeOf(promise: Promise<unknown>): Promise<string> {
87
- try {
88
- await promise
89
- } catch (error: any) {
90
- return error?.code || error?.extensions?.refusal?.code || `not a refusal: ${error?.message}`
91
- }
92
-
93
- return 'nothing was thrown'
94
- }
95
-
96
- /** 거절이 싣고 온 값. 화면이 물음을 만드는 재료다. */
97
- async function refusalParamsOf(promise: Promise<unknown>): Promise<Record<string, any>> {
98
- try {
99
- await promise
100
- } catch (error: any) {
101
- return error?.params || error?.extensions?.refusal?.params || {}
102
- }
103
-
104
- return {}
105
- }
106
-
107
- async function roleIn(domain: any, name: string, description: string) {
108
- return await getRepository(Role).save({ name, description, domain: { id: domain.id } })
109
- }
110
-
111
- describe('one role, no ambiguity', () => {
112
- it('answers this domain own role', async () => {
113
- await roleIn(child, 'approver', 'ours')
114
-
115
- expect(await query.role('approver', context(child))).toMatchObject({ description: 'ours' })
116
- })
117
-
118
- it('answers a role that only the parent has', async () => {
119
- /* Inheritance still works — this is the case the whole one-level scope exists for. */
120
- await roleIn(parent, 'auditor', 'theirs')
121
-
122
- expect(await query.role('auditor', context(child))).toMatchObject({ description: 'theirs' })
123
- })
124
-
125
- it('answers nothing for a name neither has', async () => {
126
- expect(await query.role('nobody', context(child))).toBeNull()
127
- })
128
-
129
- it('does not reach a parent that is not ours', async () => {
130
- /* A root domain inherits from nobody, and another tenant is not a parent. */
131
- await roleIn(parent, 'auditor', 'theirs')
132
-
133
- expect(await query.role('auditor', context(orphan))).toBeNull()
134
- })
135
- })
136
-
137
- describe('the same name in both', () => {
138
- it('is refused unless the operator says this domain stands its own', async () => {
139
- /*
140
- * ⚠ ADR-0062 decision 3, settled 2026-09-20. Until then the duplicate checks looked at this
141
- * domain only and nothing refused a name already visible through the parent, so the two rows
142
- * appeared silently.
143
- *
144
- * The refusal names the domain the other one stands in, because that is what the operator
145
- * needs to decide — they may not have known the parent had it.
146
- */
147
- await roleIn(parent, 'approver', 'theirs')
148
-
149
- expect(await refusalCodeOf(mutation.createRole({ name: 'approver' } as any, context(child)))).toEqual(
150
- 'role-name-stands-inherited'
151
- )
152
- })
153
-
154
- it('names the domain the other one stands in', async () => {
155
- /*
156
- * ⚠ What makes the refusal answerable. The operator's next move is to decide whether this
157
- * domain stands its own of that name, and they cannot decide without knowing where the other
158
- * one is — they may not have known the parent had it. A refusal that only said "duplicated"
159
- * would leave them guessing.
160
- *
161
- * The value rides in `params`, not in the sentence: the sentence is built in the reader's
162
- * language and the screen puts its own question together from these (ADR-0054).
163
- */
164
- await roleIn(parent, 'approver', 'theirs')
165
-
166
- expect(await refusalParamsOf(mutation.createRole({ name: 'approver' } as any, context(child)))).toMatchObject({
167
- name: 'approver',
168
- domain: 'system'
169
- })
170
- })
171
-
172
- it('stands when the operator does say so, under the same name', async () => {
173
- /*
174
- * ⚠ The escape is not "call it something else". Standing up the same names here is the first
175
- * step of moving a domain off an inherited structure (decision 2) — the child's role replaces
176
- * what it inherits, and cutting the parent last leaves one name again. Requiring a different
177
- * name would close that route.
178
- */
179
- await roleIn(parent, 'approver', 'theirs')
180
-
181
- const created = await mutation.createRole(
182
- { name: 'approver', description: 'ours', separately: true } as any,
183
- context(child)
184
- )
185
-
186
- expect(created).toMatchObject({ name: 'approver', description: 'ours' })
187
- })
188
-
189
- it('refuses when the name stands in both, whatever the operator says', async () => {
190
- /*
191
- * ⚠ The state the migration of decision 2 passes through for its whole length: this domain's
192
- * own role is standing beside the inherited one until the parent is cut, last. Trying to
193
- * stand a second one during that window must be refused as `here` — there is already one
194
- * here — and the operator's "stand it separately" must not reach it.
195
- *
196
- * It depends on the lookup asking this domain first. A single read over both domains with no
197
- * ORDER BY can answer with the parent's row, and then the refusal is the inherited one,
198
- * which `separately` is allowed to pass. That was the same defect this file already pins for
199
- * `role(name)`, reintroduced in the checking path.
200
- *
201
- * ⚠ This case could not see that version of it. sqlite answered the combined query with this
202
- * domain's row, so the outcome was right by accident — measured 2026-09-20 by putting the old
203
- * shape back: 448 tests, none red. `role-name-lookup-sentinel.test.ts` reads the query before
204
- * it is sent, which is where that shape is reachable from here. What this case does catch,
205
- * now that the order is in our code rather than the driver's, is the order being wrong.
206
- */
207
- await roleIn(parent, 'approver', 'theirs')
208
- await roleIn(child, 'approver', 'ours')
209
-
210
- expect(
211
- await refusalCodeOf(mutation.createRole({ name: 'approver', separately: true } as any, context(child)))
212
- ).toEqual('role-name-taken-here')
213
- })
214
-
215
- it('refuses the same way whichever row was written first', async () => {
216
- await roleIn(child, 'approver', 'ours')
217
- await roleIn(parent, 'approver', 'theirs')
218
-
219
- expect(
220
- await refusalCodeOf(mutation.createRole({ name: 'approver', separately: true } as any, context(child)))
221
- ).toEqual('role-name-taken-here')
222
- })
223
-
224
- it('refuses a name this domain already has, whatever the operator says', async () => {
225
- /*
226
- * ⚠ Not something to opt into. Two rows in one domain under one name would be
227
- * indistinguishable in every respect — nothing for a screen to show, nothing to choose
228
- * between.
229
- */
230
- await roleIn(child, 'approver', 'ours')
231
-
232
- expect(
233
- await refusalCodeOf(mutation.createRole({ name: 'approver', separately: true } as any, context(child)))
234
- ).toEqual('role-name-taken-here')
235
- })
236
-
237
- it('leaves a name that stands only in some third domain alone', async () => {
238
- /* Another tenant's naming does not reach us. */
239
- await roleIn(orphan, 'approver', 'theirs')
240
-
241
- const created = await mutation.createRole({ name: 'approver', description: 'ours' } as any, context(child))
242
-
243
- expect(created).toMatchObject({ description: 'ours' })
244
- })
245
-
246
- it('answers this domain own, never the parent', async () => {
247
- /*
248
- * ⚠ The case the fix is for. Before it, both rows matched one `In([own, parent])` with no
249
- * ORDER BY, so the answer was whichever row the driver handed back first.
250
- */
251
- await roleIn(parent, 'approver', 'theirs')
252
- await roleIn(child, 'approver', 'ours')
253
-
254
- expect(await query.role('approver', context(child))).toMatchObject({ description: 'ours' })
255
- })
256
-
257
- it('answers the same way whichever row was written first', async () => {
258
- /* ⚠ Insertion order is the thing that must stop mattering. */
259
- await roleIn(child, 'approver', 'ours')
260
- await roleIn(parent, 'approver', 'theirs')
261
-
262
- expect(await query.role('approver', context(child))).toMatchObject({ description: 'ours' })
263
- })
264
-
265
- it('leaves the parent domain reading its own', async () => {
266
- /* Shadowing runs one way. The parent is not affected by what a child called something. */
267
- await roleIn(parent, 'approver', 'theirs')
268
- await roleIn(child, 'approver', 'ours')
269
-
270
- expect(await query.role('approver', context(parent))).toMatchObject({ description: 'theirs' })
271
- })
272
-
273
- it('still shows both to a screen that lists them', async () => {
274
- /*
275
- * ⚠ Deliberate, and the reason the screen has to say where each came from. Resolving a name
276
- * is not the same act as offering a list to choose from: hiding the parent's row here would
277
- * silently drop a role somebody may still be assigned.
278
- */
279
- await roleIn(parent, 'approver', 'theirs')
280
- await roleIn(child, 'approver', 'ours')
281
-
282
- const offered = await query.userRoles(actor.id, context(child))
283
-
284
- expect(offered.filter(role => role.name === 'approver')).toHaveLength(2)
285
- })
286
- })
@@ -1,293 +0,0 @@
1
- /**
2
- * 역할 만들기·고치기·지우기 — **실패했을 때 무엇을 말하나**를 실 DB 로 태운다.
3
- *
4
- * 이 셋은 권한 편집 화면이 서는 자리인데, 세 갈래의 실패가 전부 잘못된 말로 나가고 있었다.
5
- *
6
- * ① 부모 도메인의 역할을 고치려 하면 `role.name` 에서 TypeError → 500. 화면은 이유를 모른다.
7
- * 목록은 상속으로 부모 역할까지 보여 주므로(`inherited: Include`) **실제로 자주 닿는다.**
8
- * ② `privileges` 없는 patch(이름만 고치기)도 `patch.privileges.map` 에서 500.
9
- * ③ `deleteRole` 의 catch 가 무엇이 났든 「사용 중이라 지울 수 없다」로 401 을 던졌다 —
10
- * 못 찾은 것도 그렇게 말했다. 관리자는 있지도 않은 사용자를 떼러 갔다.
11
- *
12
- * 그리고 `deleteRoles`(복수)는 **도메인을 아예 안 봤다** — id 만 알면 남의 도메인 역할이 지워졌다.
13
- *
14
- * 지시자는 여기서 안 돈다(스키마 층이다). 게이트는 `privilege-directive.test.ts` 가 본다.
15
- */
16
-
17
- import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
18
- import { authDataSource } from './db'
19
- import { loadCompiled } from './compiled'
20
-
21
- const { Role } = loadCompiled('service/role/role')
22
- const { Privilege } = loadCompiled('service/privilege/privilege')
23
- const { User } = loadCompiled('service/user/user')
24
- const { RoleMutation } = loadCompiled('service/role/role-mutation')
25
-
26
- const mutation = new RoleMutation()
27
-
28
- let parent: any
29
- let child: any
30
- let stranger: any
31
- let actor: any
32
- let boardQuery: any
33
- let boardMutation: any
34
-
35
- async function loadDomain(id: string) {
36
- return await getRepository(Domain).findOneBy({ id })
37
- }
38
-
39
- /** 리졸버가 실제로 쓰는 것만 담는다 — `tx`·`domain`·`user`·`t`·`throw`. */
40
- function context(domain: any) {
41
- return {
42
- state: { domain, user: actor, tx: authDataSource().manager },
43
- t: (key: string, params: any = {}) => `${key}${params.x ? `:${params.x}` : ''}`,
44
- throw: (status: number, message: string) => {
45
- throw new Error(`${status} ${message}`)
46
- }
47
- } as any
48
- }
49
-
50
- beforeAll(async () => {
51
- await openAuthDatabase()
52
- })
53
-
54
- afterAll(async () => {
55
- await closeAuthDatabase()
56
- })
57
-
58
- beforeEach(async () => {
59
- await resetAuthDatabase()
60
-
61
- const domainRepository = getRepository(Domain)
62
- const savedParent = await domainRepository.save({ name: 'HQ', subdomain: 'hq' })
63
- const savedChild = await domainRepository.save({ name: 'Plant A', subdomain: 'plant-a', parent: savedParent })
64
- const savedStranger = await domainRepository.save({ name: 'Other', subdomain: 'other' })
65
-
66
- parent = await loadDomain(savedParent.id)
67
- child = await loadDomain(savedChild.id)
68
- stranger = await loadDomain(savedStranger.id)
69
-
70
- const privilegeRepository = getRepository(Privilege)
71
- boardQuery = await privilegeRepository.save({ name: 'query', category: 'board' })
72
- boardMutation = await privilegeRepository.save({ name: 'mutation', category: 'board' })
73
-
74
- const salt = User.generateSalt()
75
- actor = await getRepository(User).save({
76
- username: 'admin',
77
- email: 'admin@example.com',
78
- name: 'Admin',
79
- salt,
80
- status: 'activated',
81
- domains: [child]
82
- })
83
- })
84
-
85
- async function makeRole(name: string, domain: any, privileges: any[] = [], users: any[] = []) {
86
- return await getRepository(Role).save({ name, domain, privileges, users })
87
- }
88
-
89
- /**
90
- * 거절의 이름을 본다. 영어 문장이 아니라 `code` 가 계약이다(ADR-0054) — 문장은 요청의 언어가
91
- * 알려진 자리에서 만들어지므로, 문장을 시험에 박으면 번역이 붙는 날 같이 깨진다.
92
- */
93
- async function refusalCodeOf(promise: Promise<unknown>): Promise<string> {
94
- try {
95
- await promise
96
- } catch (error: any) {
97
- return error?.code || error?.extensions?.refusal?.code || `not a refusal: ${error?.message}`
98
- }
99
-
100
- return 'nothing was thrown'
101
- }
102
-
103
- describe('고치기', () => {
104
- it('권한을 갈아 끼운다', async () => {
105
- const role = await makeRole('viewer', child, [boardQuery])
106
-
107
- await mutation.updateRole(role.id, { privileges: [{ id: boardMutation.id }] } as any, context(child))
108
-
109
- const saved = await getRepository(Role).findOne({ where: { id: role.id }, relations: ['privileges'] })
110
- expect(saved.privileges.map((p: any) => p.id)).toEqual([boardMutation.id])
111
- })
112
-
113
- it('`privileges` 를 안 보내면 권한을 건드리지 않는다 — 이름만 고치는 호출이 있다', async () => {
114
- /* 예전에는 이 호출이 `patch.privileges.map` 에서 TypeError 로 500 이었다. */
115
- const role = await makeRole('viewer', child, [boardQuery])
116
-
117
- await mutation.updateRole(role.id, { name: 'reader' } as any, context(child))
118
-
119
- const saved = await getRepository(Role).findOne({ where: { id: role.id }, relations: ['privileges'] })
120
- expect(saved.name).toBe('reader')
121
- expect(saved.privileges.map((p: any) => p.id)).toEqual([boardQuery.id])
122
- })
123
-
124
- it('부모 도메인의 역할은 어디서 고치는지 말하고 거절한다', async () => {
125
- /*
126
- * 목록에 보이는 것과 고칠 수 있는 것이 다르다. 예전에는 여기서 TypeError 가 나 500 이었고
127
- * 화면은 「알 수 없는 오류」만 보여 줬다.
128
- */
129
- const role = await makeRole('shared', parent, [boardQuery])
130
-
131
- await expect(mutation.updateRole(role.id, { name: 'mine' } as any, context(child))).rejects.toThrow(
132
- /parent domain/
133
- )
134
- })
135
-
136
- it('같은 이름이 이미 있으면 거절한다', async () => {
137
- await makeRole('viewer', child)
138
- const role = await makeRole('editor', child)
139
-
140
- expect(await refusalCodeOf(mutation.updateRole(role.id, { name: 'viewer' } as any, context(child)))).toEqual(
141
- 'role-name-taken-here'
142
- )
143
- })
144
- })
145
-
146
- describe('지우기 — 하나', () => {
147
- it('아무도 안 가진 역할은 지운다', async () => {
148
- const role = await makeRole('unused', child, [boardQuery])
149
-
150
- expect(await mutation.deleteRole(role.id, context(child))).toBe(true)
151
- expect(await getRepository(Role).findOneBy({ id: role.id })).toBeNull()
152
- })
153
-
154
- it('가진 사람이 있으면 그 이유로 거절한다', async () => {
155
- const role = await makeRole('in-use', child, [boardQuery], [actor])
156
-
157
- expect(await refusalCodeOf(mutation.deleteRole(role.id, context(child)))).toBe('role-in-use')
158
- expect(await getRepository(Role).findOneBy({ id: role.id })).not.toBeNull()
159
- })
160
-
161
- it('부모 도메인의 역할을 「사용 중」이라고 말하지 않는다', async () => {
162
- /*
163
- * 이것이 옛 구현의 가장 나쁜 점이었다 — 못 찾은 것을 「사용 중」이라고 말했으므로 관리자는
164
- * 있지도 않은 사용자를 찾아 떼려 했다.
165
- */
166
- const role = await makeRole('shared', parent)
167
-
168
- await expect(mutation.deleteRole(role.id, context(child))).rejects.toThrow(/parent domain/)
169
- await expect(mutation.deleteRole(role.id, context(child))).rejects.not.toThrow(/in use/)
170
- })
171
- })
172
-
173
- describe('지우기 — 여럿', () => {
174
- it('남의 도메인 역할은 id 를 알아도 안 지운다', async () => {
175
- const mine = await makeRole('mine', child)
176
- const theirs = await makeRole('theirs', stranger)
177
-
178
- await mutation.deleteRoles([mine.id, theirs.id], context(child))
179
-
180
- expect(await getRepository(Role).findOneBy({ id: mine.id })).toBeNull()
181
- expect(await getRepository(Role).findOneBy({ id: theirs.id })).not.toBeNull()
182
- })
183
-
184
- it('★ 사용 중인 역할이 하나라도 있으면 아무것도 안 지우고 그 이름과 사람 수로 거절한다 — 하나 지우기와 같은 거절이다', async () => {
185
- const unused = await makeRole('unused', child)
186
- const inUse = await makeRole('in-use', child, [], [actor])
187
-
188
- let refusal: any
189
- try {
190
- await mutation.deleteRoles([unused.id, inUse.id], context(child))
191
- } catch (error) {
192
- refusal = error
193
- }
194
-
195
- expect(refusal?.code).toBe('role-in-use')
196
- expect(refusal?.params).toEqual({ names: 'in-use (1)' })
197
- expect(await getRepository(Role).findOneBy({ id: unused.id })).not.toBeNull()
198
- expect(await getRepository(Role).findOneBy({ id: inUse.id })).not.toBeNull()
199
- })
200
-
201
- it('아무도 안 가진 역할은 함께 지운다', async () => {
202
- const one = await makeRole('one', child)
203
- const two = await makeRole('two', child)
204
-
205
- expect(await mutation.deleteRoles([one.id, two.id], context(child))).toBe(true)
206
- expect(await getRepository(Role).findOneBy({ id: one.id })).toBeNull()
207
- expect(await getRepository(Role).findOneBy({ id: two.id })).toBeNull()
208
- })
209
- })
210
-
211
- /**
212
- * 격자에서 여러 줄을 고쳐 한 번에 보내는 길.
213
- *
214
- * 목록 화면이 `dirtyRecords` 를 `cuFlag` 와 함께 보낸다. 여기서 지키는 것은 하나가 제일 크다 —
215
- * **`privileges` 를 안 보낸 patch 가 권한을 비우지 않는 것.** 격자는 이름만 고치는데 그것이
216
- * 권한을 지우면 그 역할이 아무것도 안 여는 역할이 된다.
217
- */
218
- describe('격자에서 여럿 저장', () => {
219
- it('`+` 는 만들고 `M` 은 고친다', async () => {
220
- const existing = await makeRole('viewer', child, [boardQuery])
221
-
222
- await mutation.updateMultipleRole(
223
- [
224
- { cuFlag: '+', name: 'operator', description: '현장' },
225
- { cuFlag: 'M', id: existing.id, name: 'reader' }
226
- ] as any,
227
- context(child)
228
- )
229
-
230
- const roles = await getRepository(Role).find({ where: { domain: { id: child.id } } })
231
- expect(roles.map((r: any) => r.name).sort()).toEqual(['operator', 'reader'])
232
- })
233
-
234
- it('이름만 고쳐도 권한은 그대로다 — 격자는 권한을 안 보낸다', async () => {
235
- /*
236
- * 이것이 이 자리의 가장 조용한 결함이 될 수 있었다. 관리자가 격자에서 오타를 고쳤는데
237
- * 그 역할이 아무것도 안 여는 역할이 되면, 그 역할을 가진 사람들이 다음 로그인에 막힌다.
238
- */
239
- const role = await makeRole('viewer', child, [boardQuery, boardMutation])
240
-
241
- await mutation.updateMultipleRole([{ cuFlag: 'M', id: role.id, name: 'reader' }] as any, context(child))
242
-
243
- const saved = await getRepository(Role).findOne({ where: { id: role.id }, relations: ['privileges'] })
244
- expect(saved.privileges.map((p: any) => p.id).sort()).toEqual([boardMutation.id, boardQuery.id].sort())
245
- })
246
-
247
- it('`cuFlag` 가 없는 줄은 아무것도 하지 않는다 — 짐작해서 만들지 않는다', async () => {
248
- await mutation.updateMultipleRole([{ name: 'ghost' }] as any, context(child))
249
-
250
- expect(await getRepository(Role).findOneBy({ name: 'ghost' })).toBeNull()
251
- })
252
-
253
- it('부모 도메인의 역할을 고치려 하면 어디서 고치는지 말한다', async () => {
254
- const role = await makeRole('shared', parent)
255
-
256
- await expect(
257
- mutation.updateMultipleRole([{ cuFlag: 'M', id: role.id, name: 'mine' }] as any, context(child))
258
- ).rejects.toThrow(/parent domain/)
259
- })
260
-
261
- it('이름 없이 만들려 하면 거절한다', async () => {
262
- await expect(mutation.updateMultipleRole([{ cuFlag: '+', name: ' ' }] as any, context(child))).rejects.toThrow()
263
- })
264
-
265
- it('같은 이름으로 만들려 하면 거절한다', async () => {
266
- await makeRole('operator', child)
267
-
268
- expect(
269
- await refusalCodeOf(mutation.updateMultipleRole([{ cuFlag: '+', name: 'operator' }] as any, context(child)))
270
- ).toEqual('role-name-taken-here')
271
- })
272
- })
273
-
274
- describe('만들기', () => {
275
- it('내 도메인에 만들고 고른 권한을 붙인다', async () => {
276
- const created = await mutation.createRole(
277
- { name: 'operator', privileges: [{ id: boardQuery.id }] } as any,
278
- context(child)
279
- )
280
-
281
- const saved = await getRepository(Role).findOne({ where: { id: created.id }, relations: ['privileges', 'domain'] })
282
- expect(saved.domain.id).toBe(child.id)
283
- expect(saved.privileges.map((p: any) => p.id)).toEqual([boardQuery.id])
284
- })
285
-
286
- it('같은 도메인에 같은 이름은 안 만든다', async () => {
287
- await makeRole('operator', child)
288
-
289
- expect(await refusalCodeOf(mutation.createRole({ name: 'operator' } as any, context(child)))).toEqual(
290
- 'role-name-taken-here'
291
- )
292
- })
293
- })