@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 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../server/types.ts"],"names":[],"mappings":"","sourcesContent":["import { TFunction } from 'i18next'\nimport { EntityManager } from 'typeorm'\n\nimport { Domain } from '@things-factory/shell'\n\nimport { User } from './service/user/user.js'\n\ndeclare global {\n export type ResolverContext = {\n state: IContextState\n t?: TFunction\n [key: string]: any\n }\n\n interface IContextState {\n domain: Domain\n user: User\n tx?: EntityManager\n [key: string]: any\n }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"accepts.js","sourceRoot":"","sources":["../../server/utils/accepts.ts"],"names":[],"mappings":";;AAAA,0BAUC;AAVD,SAAgB,OAAO,CAAC,MAAW,EAAE,OAAQ;IAC3C,OAAO,GAAG,OAAO,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;IAEzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAC9C,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["export function accepts(header: any, accepts?) {\n accepts = accepts || ['text/html', '*/*']\n \n for (let i = 0; i < accepts.length; i++) {\n if ((header || '').indexOf(accepts[i]) !== -1) {\n return true\n }\n }\n\n return false\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"access-request-review.js","sourceRoot":"","sources":["../../server/utils/access-request-review.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The checks an access decision passes - one function, read by the list screen, by the AI 입력 draft card\n * (its `check`, when the card is made and again when it is pressed) and by `decideAccessRequest` itself\n * (ADR-0103 decision 4: the card and the screen cannot say different things).\n *\n * Pure: the facts are collected by `access-request-facts.ts`; this decides what they mean. A model never\n * judges a role as risky - this does, the same way every time, and the model only words the result.\n *\n * Blocking issues keep the decision from being written. The rest are told to the person deciding.\n */\n\nexport type AccessRequestState = 'PENDING' | 'GRANTED' | 'DENIED' | 'WITHDRAWN' | 'EXPIRED'\n\nexport type AccessDecisionKind = 'GRANT' | 'DENY'\n\nexport type PrivilegeName = { category: string; privilege: string }\n\n/** The privilege that lets its holder put people into roles - a role carrying it hands that power on. */\nexport const ROLE_GRANTING_PRIVILEGE: PrivilegeName = { category: 'user', privilege: 'mutation' }\n\n/** How long a request waits for a decision (questions Q7). */\nexport const ACCESS_REQUEST_PENDING_DAYS = 14\n\nexport interface DecisionFacts {\n request: {\n state: AccessRequestState\n requesterId: string\n category: string\n privilege: string\n expiresAt?: Date | string | null\n } | null\n deciderId: string\n decision: AccessDecisionKind\n reason?: string | null\n /** The role chosen to grant - absent when it is not this domain's to assign. */\n role?: { id: string; name: string; privileges: PrivilegeName[] } | null\n /** A role id was named but not found in the domain's scope. */\n roleNamedButNotAssignable?: boolean\n /** The chosen role is the parent domain's - and whether the decider may give it (ADR-0103 decision 8). */\n roleIsParents?: boolean\n deciderMayChangeParentRoles?: boolean\n requester: { active: boolean; inDomain: boolean } | null\n /** The requester holds the privilege now - through some other role, since the request was made. */\n requesterHoldsIt: boolean\n /** Every assignable role that grants the privilege, with how many privileges each carries and whether it holds the task asked. */\n candidates: { id: string; name: string; privilegeCount: number; holdsTask?: boolean }[]\n /** The task the requester asked for, with its declared privileges - null when none was asked or it is gone (ADR-0103 decision 9). */\n task?: { id: string; privileges: PrivilegeName[] } | null\n /** The decider's own privileges - to tell them when a role gives what they do not have. */\n deciderPrivileges: PrivilegeName[]\n /** Whether the decider is a domain owner - an owner may give anything. */\n deciderIsOwner: boolean\n /** The latest earlier denial of the same requester and privilege. */\n deniedBefore?: { by: string; at: Date | string; reason?: string | null } | null\n now: Date\n}\n\nexport interface DecisionIssue {\n code: string\n blocking: boolean\n field?: 'request' | 'decision' | 'role' | 'reason'\n params?: Record<string, string | number | boolean | null>\n}\n\nconst same = (a: PrivilegeName, b: PrivilegeName) => a.category === b.category && a.privilege === b.privilege\nconst nameOf = (p: PrivilegeName) => `${p.category}:${p.privilege}`\n\n/** A pending request past its time is expired, whatever the column still says. */\nexport function effectiveState(state: AccessRequestState, expiresAt: Date | string | null | undefined, now: Date): AccessRequestState {\n if (state !== 'PENDING' || !expiresAt) return state\n const at = new Date(expiresAt)\n return isNaN(at.getTime()) || at.getTime() > now.getTime() ? state : 'EXPIRED'\n}\n\nexport function expiryFrom(now: Date, days = ACCESS_REQUEST_PENDING_DAYS): Date {\n return new Date(now.getTime() + days * 24 * 60 * 60 * 1000)\n}\n\nexport function reviewAccessDecision(facts: DecisionFacts): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n const { request } = facts\n\n if (!request) {\n return [{ code: 'access-request-not-found', blocking: true, field: 'request' }]\n }\n\n const state = effectiveState(request.state, request.expiresAt, facts.now)\n if (state !== 'PENDING') {\n issues.push({ code: 'request-not-pending', blocking: true, field: 'request', params: { state } })\n }\n\n if (request.requesterId === facts.deciderId) {\n issues.push({ code: 'own-request', blocking: true, field: 'request' })\n }\n\n if (!facts.requester || !facts.requester.inDomain || !facts.requester.active) {\n issues.push({ code: 'requester-unavailable', blocking: true, field: 'request' })\n }\n\n if (facts.decision === 'DENY') {\n if (!facts.reason || !facts.reason.trim()) {\n issues.push({ code: 'deny-needs-reason', blocking: true, field: 'reason' })\n }\n } else {\n issues.push(...grantIssues(facts, { category: request.category, privilege: request.privilege }))\n }\n\n if (facts.deniedBefore) {\n issues.push({\n code: 'denied-before',\n blocking: false,\n field: 'request',\n params: { by: facts.deniedBefore.by, at: new Date(facts.deniedBefore.at).toISOString(), reason: facts.deniedBefore.reason ?? '' }\n })\n }\n\n return issues\n}\n\nfunction grantIssues(facts: DecisionFacts, asked: PrivilegeName): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n\n if (facts.requesterHoldsIt) {\n issues.push({ code: 'already-holds', blocking: true, field: 'request', params: { privilege: nameOf(asked) } })\n }\n\n if (!facts.role) {\n issues.push({\n code: facts.roleNamedButNotAssignable ? 'role-not-assignable' : 'role-required',\n blocking: true,\n field: 'role'\n })\n return issues\n }\n\n const { role } = facts\n if (!role.privileges.some(p => same(p, asked))) {\n issues.push({ code: 'role-does-not-grant', blocking: true, field: 'role', params: { role: role.name, privilege: nameOf(asked) } })\n return issues\n }\n\n if (facts.roleIsParents && !facts.deciderMayChangeParentRoles) {\n issues.push({ code: 'role-belongs-to-parent', blocking: true, field: 'role', params: { role: role.name } })\n }\n\n const more = role.privileges.filter(p => !same(p, asked))\n if (more.length > 0) {\n issues.push({\n code: 'role-grants-more',\n blocking: false,\n field: 'role',\n params: { role: role.name, count: more.length, privileges: more.map(nameOf).join(', ') }\n })\n }\n\n /* Told, not refused: the role opens the refused door, and the rest of the work asked for is the decider's call. */\n const missingOfTask = (facts.task?.privileges || []).filter(p => !role.privileges.some(r => same(r, p)))\n if (facts.task && missingOfTask.length > 0) {\n issues.push({ code: 'role-lacks-task', blocking: false, field: 'role', params: { role: role.name, task: facts.task.id, privileges: missingOfTask.map(nameOf).join(', ') } })\n }\n if (!same(asked, ROLE_GRANTING_PRIVILEGE) && role.privileges.some(p => same(p, ROLE_GRANTING_PRIVILEGE))) {\n issues.push({ code: 'role-grants-admin', blocking: false, field: 'role', params: { role: role.name } })\n }\n\n /* A smaller role that drops the task asked for is not a better answer - only one holding it as the chosen does counts. */\n const choiceHoldsTask = !!facts.task && missingOfTask.length === 0\n const smaller = facts.candidates\n .filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length && (!choiceHoldsTask || c.holdsTask))\n .sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n if (smaller) {\n issues.push({ code: 'smaller-role-exists', blocking: false, field: 'role', params: { role: smaller.name, count: smaller.privilegeCount } })\n }\n\n if (!facts.deciderIsOwner) {\n const lacking = role.privileges.filter(p => !facts.deciderPrivileges.some(d => same(d, p)))\n if (lacking.length > 0) {\n /* Told, not refused (questions Q6) - the role screen gives the same role today; ruling both is a separate decision. */\n issues.push({ code: 'decider-lacks-it', blocking: false, field: 'role', params: { privileges: lacking.map(nameOf).join(', ') } })\n }\n }\n\n return issues\n}\n\n/**\n * The candidate the draft card proposes by default - one holding the task asked, if any does, then the smallest. Ties go\n * by name, so the answer is the same every time; the order is `rolesGranting`'s.\n */\nexport function smallestRole<T extends { name: string; privilegeCount: number; holdsTask?: boolean }>(candidates: T[]): T | undefined {\n return [...candidates].sort((a, b) => Number(!!b.holdsTask) - Number(!!a.holdsTask) || a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n}\n\nexport const isBlocked = (issues: DecisionIssue[]) => issues.some(issue => issue.blocking)\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"access-token-cookie.js","sourceRoot":"","sources":["../../server/utils/access-token-cookie.ts"],"names":[],"mappings":";;AAMA,oDAEC;AAED,oDAWC;AAED,sDASC;AAED,wDAkBC;AApDD,6CAA4C;AAC5C,wDAAiD;AACjD,wGAA8F;AAE9F,MAAM,oBAAoB,GAAG,YAAM,CAAC,GAAG,CAAC,sBAAsB,EAAE,cAAc,CAAC,CAAA;AAE/E,SAAgB,oBAAoB,CAAC,OAAO;IAC1C,OAAO,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,oBAAoB,CAAC,CAAA;AACpD,CAAC;AAED,SAAgB,oBAAoB,CAAC,OAAO,EAAE,KAAK;IACjD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,oBAAO;QACf,QAAQ,EAAE,KAAK;KAChB,CAAA;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AAC1D,CAAC;AAED,SAAgB,qBAAqB,CAAC,OAAO;IAC3C,0FAA0F;IAC1F,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE9B,OAAO,CAAC,OAAO,GAAG;QAChB,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,QAAQ,EAAE,IAAI,CAAC,IAAI;QACnB,MAAM,EAAE,IAAI,CAAC,KAAK;KACnB,CAAA;AACH,CAAC;AAED,SAAgB,sBAAsB,CAAC,OAAO;IAC5C,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IAE1B,IAAI,MAAM,GAAG;QACX,MAAM;QACN,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,KAAK;KAChB,CAAA;IAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;IACrD,0FAA0F;IAC1F,IAAA,6CAAkB,EAAC,OAAO,CAAC,CAAA;IAC3B;;;OAGG;IACH,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,EAAE,MAAM,CAAC,CAAA;IAC1C,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;AACxB,CAAC","sourcesContent":["import { config } from '@things-factory/env'\nimport { MAX_AGE } from '../constants/max-age.js'\nimport { forgetConfirmation } from '../service/identity-confirmation/identity-confirmation.js'\n\nconst accessTokenCookieKey = config.get('accessTokenCookieKey', 'access_token')\n\nexport function getAccessTokenCookie(context) {\n return context?.cookies?.get(accessTokenCookieKey)\n}\n\nexport function setAccessTokenCookie(context, token) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true,\n maxAge: MAX_AGE,\n sameSite: 'Lax'\n }\n\n context.cookies.set(accessTokenCookieKey, token, cookie)\n}\n\nexport function setSessionAccessToken(context) {\n /* koa-session 을 사용하는 경우에는, cookie 직접 설정이 작동되지 않는다. 그런 경우에는 session에 설정해서 cookie를 변경한다. */\n const { user } = context.state\n\n context.session = {\n id: user.id,\n userType: user.type,\n status: user.state\n }\n}\n\nexport function clearAccessTokenCookie(context) {\n const { secure } = context\n\n var cookie = {\n secure,\n httpOnly: true,\n sameSite: 'Lax'\n }\n\n context.cookies.set(accessTokenCookieKey, '', cookie)\n /* A confirmation that it was the person ends with the sign-in it confirmed (ADR-0168). */\n forgetConfirmation(context)\n /*\n * TODO clear i18next cookie as well - need to support domain\n * https://github.com/hatiolab/things-factory/issues/70\n */\n context.cookies.set('i18next', '', cookie)\n context.session = null\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"account-lock-rule.js","sourceRoot":"","sources":["../../server/utils/account-lock-rule.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;AA4BH,oDAeC;AAyBD,0CAYC;AAGD,oDAEC;AA3ED,0CAA0C;AAC7B,QAAA,YAAY,GAAG,CAAC,CAAA;AAE7B;;;;;;GAMG;AACU,QAAA,aAAa,GAAG,QAAQ,CAAA;AAErC;;;;;GAKG;AACH,SAAgB,oBAAoB,CAAC,KAAuB,EAAE,eAAuB;IACnF,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC,CAAC,eAAe,GAAG,oBAAY,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IACxC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,eAAe,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;AACnE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,SAAgB,eAAe,CAAC,KAAuB,EAAE,GAAS,EAAE,eAAuB;IACzF,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,SAAS,GAAG,oBAAoB,CAAC,KAAK,EAAE,eAAe,CAAC,CAAA;IAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,kEAAkE;QAClE,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,CAAA;AAC5C,CAAC;AAED,2CAA2C;AAC3C,SAAgB,oBAAoB,CAAC,KAAuB,EAAE,GAAS,EAAE,eAAuB;IAC9F,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,eAAe,CAAC,CAAA;AAC/E,CAAC;AAED,SAAS,cAAc,CAAC,KAAuB;IAC7C,OAAO,KAAK,EAAE,MAAM,KAAK,qBAAa,CAAA;AACxC,CAAC;AAED,4CAA4C;AAC5C,SAAS,MAAM,CAAC,KAAuC;IACrD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,IAAI,GAAG,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAA;IAE5D,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC","sourcesContent":["/**\n * 계정 잠금이 아직 유효한지를 판정하는 규칙 (순수 함수).\n *\n * ## 왜 따로 있나\n *\n * 잠금을 보는 자리가 둘이다 — `signin`(들어오려는 사람)과 `User.checkAuth`(요청마다 토큰을\n * 확인하는 자리). 두 곳이 각자 「아직 잠겼나」를 판단하면 서로 다른 답을 낼 수 있고, 그러면\n * 로그인은 되는데 요청은 거절되는(또는 그 반대) 상태가 생긴다. 판정은 한 곳에 둔다\n * (`checkin-domain-rule` 과 같은 규율).\n *\n * ## 자동해제를 주기 작업으로 하지 않는 이유\n *\n * 계정이 10만이면 「만료된 잠금을 풀어 주는 일」이 따로 일이 된다. 잠금은 **읽는 시점에**\n * 판정한다 — 그 계정을 보는 사람이 있을 때만 계산하면 되고, 그때가 유일하게 답이 필요한\n * 순간이다.\n *\n * 행을 정리하는 것은 판정과 별개다. `signin` 은 판정이 「풀렸다」로 나오면 그 자리에서 행을\n * 되돌리고(`ACTIVATED` · `failCount` 0 · `lockedAt` 비움) 계속 진행한다. `checkAuth` 는\n * 매 요청 경로이므로 **읽기만 한다** — 판정만 보고 통과시키고, 행 정리는 다음 `signin` 이 한다.\n *\n * ## 잠금이 곧 셈의 창이다\n *\n * 자동해제 때 `failCount` 를 0 으로 두므로, 실패 셈은 「한 번의 잠금 안에서」만 쌓인다.\n * 그래서 칸을 하나만 더해도 창이 생긴다.\n *\n * 남는 것: 잠기지 **않은** 동안의 셈은 여전히 누적이다(성공하면 0 으로 돌아간다). 그래서 오타를\n * 드물게 다섯 번 내는 사람은 한 번 잠긴다 — 그 뒤 자동으로 풀리므로 견딜 수 있지만, 창을\n * 정확히 하려면 「실패가 시작된 시각」 칸이 하나 더 필요하다.\n */\n\n/** 판정에 필요한 사용자 측 사실들 — 조회는 부르는 쪽이 한다. */\nexport type AccountLockFacts = {\n /** `UserStatus` 값. `'locked'` 가 아니면 잠금이 아니다. */\n status?: string | null\n /** 잠긴 시각. 이 변경 전에 잠긴 행에는 없다. */\n lockedAt?: Date | string | null\n}\n\n/** 잠금이 풀리지 않는 설정값 — 0 이하는 「자동해제 없음」이다. */\nexport const LOCK_FOREVER = 0\n\n/**\n * `UserStatus.LOCKED` 의 값.\n *\n * 엔티티를 import 하지 않는다 — 그러면 이 모듈이 type-graphql 데코레이터 그래프를 끌고 들어와\n * 순수 시험이 닿지 못한다(`tests/compiled.ts`). 대신 시험이 이 값과 `UserStatus.LOCKED` 가\n * 같은지 확인하므로, 한쪽만 바뀌면 시험이 깨진다.\n */\nexport const LOCKED_STATUS = 'locked'\n\n/**\n * 잠금이 언제 풀리나. 잠금이 아니거나 자동해제가 없으면 `null`.\n *\n * `lockedAt` 이 없으면 계산할 수 없으므로 `null` 을 준다 — 그때 `isAccountLocked` 는\n * **잠금을 유지한다**(아래 참조).\n */\nexport function accountLockExpiresAt(facts: AccountLockFacts, durationMinutes: number): Date | null {\n if (!isLockedStatus(facts)) {\n return null\n }\n\n if (!(durationMinutes > LOCK_FOREVER)) {\n return null\n }\n\n const lockedAt = toDate(facts?.lockedAt)\n if (!lockedAt) {\n return null\n }\n\n return new Date(lockedAt.getTime() + durationMinutes * 60 * 1000)\n}\n\n/**\n * 잠금이 아직 유효한가.\n *\n * ## 한 칸이 두 물음에 답하지 않는다\n *\n * 섞이기 쉬운 두 사실이다.\n *\n * ```\n * ① 이 계정이 잠겼나 ← `status` 가 답한다. 이 함수의 문이다\n * ② 언제 자동으로 풀리나 ← `lockedAt` 이 답한다. 잠긴 것에 대해서만 묻는다\n * ```\n *\n * 그래서 `status` 를 먼저 보고, 잠금이 아니면 **`lockedAt` 을 아예 보지 않는다.** 이 순서를\n * 뒤집으면 사고가 양쪽으로 난다 — `lockedAt` 만 보고 판정하면 **한 번도 잠긴 적 없는 계정**\n * (칸이 비어 있다)까지 잠김이 되어 배포하는 순간 전원이 못 들어오고, 반대로 짜면 지금 잠긴\n * 계정이 전부 풀린다.\n *\n * `lockedAt` 이 비어 있다는 것은 「잠겼다」가 아니라 **「자동 해제가 없다」**는 뜻이다. 이 변경\n * 전에 잠긴 행이 그렇고, 그 경우 관리자(`activateUser`)와 잠금 해제 메일이 푸는 길로 남는다 —\n * 계산할 수 없는 것을 풀어 주는 쪽으로 기울면 잠긴 계정 전체가 조용히 열린다.\n *\n * @param now 판정 시각. 시험이 시계를 넘길 수 있게 인자로 받는다.\n */\nexport function isAccountLocked(facts: AccountLockFacts, now: Date, durationMinutes: number): boolean {\n if (!isLockedStatus(facts)) {\n return false\n }\n\n const expiresAt = accountLockExpiresAt(facts, durationMinutes)\n if (!expiresAt) {\n /* 자동해제가 없거나(`durationMinutes <= 0`) 언제 풀릴지 모른다(`lockedAt` 없음). */\n return true\n }\n\n return now.getTime() < expiresAt.getTime()\n}\n\n/** 잠금이 시간이 지나 풀렸는가 — 행을 되돌려야 하는지 묻는 자리. */\nexport function isAccountLockExpired(facts: AccountLockFacts, now: Date, durationMinutes: number): boolean {\n return isLockedStatus(facts) && !isAccountLocked(facts, now, durationMinutes)\n}\n\nfunction isLockedStatus(facts: AccountLockFacts): boolean {\n return facts?.status === LOCKED_STATUS\n}\n\n/** `Date` 로 오기도 하고 드라이버에 따라 문자열로 오기도 한다. */\nfunction toDate(value: Date | string | null | undefined): Date | null {\n if (!value) {\n return null\n }\n\n const date = value instanceof Date ? value : new Date(value)\n\n return Number.isNaN(date.getTime()) ? null : date\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"assignable-roles.js","sourceRoot":"","sources":["../../server/utils/assignable-roles.ts"],"names":[],"mappings":"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","sourcesContent":["import { Brackets, EntityManager, In, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'\n\nimport { Domain, getRepository, Refusal } from '@things-factory/shell'\n\nimport { GrantedRole } from '../service/granted-role/granted-role.js'\nimport { Role } from '../service/role/role.js'\nimport { User } from '../service/user/user.js'\n\n/*\n * The roles a domain can assign - and the one place a person is put into or taken out of them.\n *\n * ── The scope ─────────────────────────────────────────────────────────────\n * Three kinds: the domain's own roles, its parent's (one level, as `userRoles` · `myRoles`), and roles a\n * partner granted to it (`grantedRoles`). `User.roles` answers exactly this scope (cb9a41e040), so the\n * user screen's checkboxes and what can be saved are the same set.\n *\n * ── Why the ids are found again ──────────────────────────────────────────\n * `updateUserRoles` took the client's `selectedRoles` and concatenated them onto the user, and took its\n * `availableRoles` and removed every held role with one of those ids. Neither was looked at. A role id of\n * another tenant was added as sent, and another tenant's role id in `availableRoles` took that role away from\n * the person in the other domain. Both are found here, in this domain's scope, by id. A role to add that is not\n * there is refused by name (ADR-0103 decision 2); a role to remove that is not there is not this domain's to take.\n *\n * The access request grants through this too, so the two ways into a role are one.\n */\n\n/** Restricts `alias` (a Role) to what `domain` can assign. Parameters are named after the alias, so two uses do not collide. */\nexport function whereAssignableBy<T extends WhereExpressionBuilder>(qb: T, alias: string, domain: Domain): T {\n const domainIds = [domain.id, domain.parentId].filter(Boolean)\n const own = `${alias}_assignableDomainIds`\n const grantedTo = `${alias}_assignableGrantedTo`\n\n qb.andWhere(\n new Brackets(inner => {\n inner.where(`${alias}.domain IN (:...${own})`, { [own]: domainIds })\n inner.orWhere(sub => {\n const granted = (sub as unknown as SelectQueryBuilder<Role>)\n .subQuery()\n .select('GRANTED.role')\n .from(GrantedRole, 'GRANTED')\n .where(`GRANTED.domain = :${grantedTo}`, { [grantedTo]: domain.id })\n .getQuery()\n return `${alias}.id IN ${granted}`\n })\n })\n )\n return qb\n}\n\n/** The roles among `ids` that `domain` can assign. */\nexport async function findAssignableRoles(ids: string[], domain: Domain, tx?: EntityManager): Promise<Role[]> {\n const unique = [...new Set((ids || []).filter(Boolean))]\n if (unique.length === 0) return []\n\n const qb = getRepository(Role, tx).createQueryBuilder('ROLE').where({ id: In(unique) })\n return await whereAssignableBy(qb, 'ROLE', domain).getMany()\n}\n\n/** The roles `domain` can assign whose name is exactly `name`, with their privileges. */\nexport async function findAssignableRolesNamed(name: string, domain: Domain, tx?: EntityManager): Promise<Role[]> {\n const said = (name || '').trim()\n if (!said) return []\n const qb = getRepository(Role, tx).createQueryBuilder('ROLE').leftJoinAndSelect('ROLE.privileges', 'ROLE_PRIVILEGE').where('ROLE.name = :said', { said })\n return await whereAssignableBy(qb, 'ROLE', domain).getMany()\n}\n\n/** Refuses with the ids that are not this domain's to assign - a name and values, spoken by `SpeakRefusals`. */\nexport async function requireAssignableRoles(ids: string[], domain: Domain, tx?: EntityManager): Promise<Role[]> {\n const found = await findAssignableRoles(ids, domain, tx)\n const foundIds = new Set(found.map(role => role.id))\n const missing = [...new Set((ids || []).filter(Boolean))].filter(id => !foundIds.has(id))\n\n if (missing.length > 0) {\n throw new Refusal(\n 'role-not-assignable-here',\n { roles: missing.join(', ') },\n `This domain cannot assign the role(s) ${missing.join(', ')}.`\n )\n }\n return found\n}\n\n/**\n * Puts `user` into `add` and takes them out of `remove` - every id found again in `domain`'s scope first.\n * Roles the user holds outside that scope are not touched, whatever `remove` says.\n *\n * @returns the saved user, and the roles that were added (not already held).\n */\n/**\n * May `actor` change who holds the parent domain's roles? Only someone who could decide in the parent too: its owner,\n * or a holder of the role-giving privilege there (ADR-0103 decision 8).\n *\n * A role's holders are not tied to a domain - a parent's role given from a child counts wherever the person is a\n * member: in the parent, and in the parent's other children. So the child's own deciders cannot be enough.\n */\nexport async function mayChangeParentRoles(actor: User | undefined, domain: Domain, tx?: EntityManager): Promise<boolean> {\n if (!actor?.id || !domain?.parentId) return false\n const parent = await getRepository(Domain, tx).findOneBy({ id: domain.parentId })\n if (!parent) return false\n const { isDomainOwner } = await import('./domain-ownership.js')\n if (await isDomainOwner(parent, actor, tx)) return true\n return await User.hasPrivilege('mutation', 'user', parent, actor)\n}\n\nexport async function assignRoles(\n user: User,\n { add = [], remove = [] }: { add?: string[]; remove?: string[] },\n domain: Domain,\n tx?: EntityManager,\n actor?: User\n): Promise<{ user: User; added: Role[] }> {\n const adding = await requireAssignableRoles(add, domain, tx)\n /*\n * `remove` is the caller's scope (\"every role this screen shows\"), not a demand - `user-detail.ts` builds it\n * from the list it read. Inside this domain's scope it is honoured; an id outside it is a role this domain\n * does not own, and it stays with the person.\n */\n const removing = await findAssignableRoles(remove, domain, tx)\n\n const addingIds = new Set(adding.map(role => role.id))\n const removingIds = new Set(removing.map(role => role.id).filter(id => !addingIds.has(id)))\n\n const held = (user.roles || []).filter(role => !removingIds.has(role.id))\n const heldIds = new Set(held.map(role => role.id))\n const added = adding.filter(role => !heldIds.has(role.id))\n\n /*\n * Only what actually changes is asked: a screen sends every role it shows as `remove` and the kept ones again as `add`,\n * so a parent role left as it was is no change. Granted roles are not the parent's - their domain delegated them\n * to this one through grantRoles and takes them back the same way, so assigning them here is the contract.\n */\n const removed = (user.roles || []).filter(role => removingIds.has(role.id))\n const parentChanges = [...added, ...removed].filter(role => domain.parentId && role.domainId === domain.parentId)\n if (parentChanges.length > 0 && !(await mayChangeParentRoles(actor, domain, tx))) {\n const names = [...new Set(parentChanges.map(role => role.name))].join(', ')\n throw new Refusal(\n 'role-belongs-to-parent',\n { roles: names },\n `${names}: a role of the parent domain - only someone who decides roles there can give or take it.`\n )\n }\n\n user.roles = [...held, ...added]\n const saved = await getRepository(User, tx).save(user)\n return { user: saved, added }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-auth-url.js","sourceRoot":"","sources":["../../server/utils/build-auth-url.ts"],"names":[],"mappings":";;AA6CA,oCA0CC;AAvFD,6BAAyB;AAEzB,6CAA4C;AAE5C;;;GAGG;AACH,MAAM,QAAQ,GAAW,YAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,SAAgB,YAAY,CAAC,QAAgB,EAAE,OAAwB;IAErE,MAAM,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAA;IAC7D,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACxD,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;IAExD,IAAI,gBAAgB,IAAI,YAAY,EAAE,CAAC;QACrC,IAAI,GAAG,GAAQ,IAAI,SAAG,CAAC,GAAG,gBAAgB,MAAM,YAAY,EAAE,CAAC,CAAA;QAC/D,IAAI,YAAY,EAAE,CAAC;YACjB,GAAG,CAAC,IAAI,GAAG,YAAY,CAAA;QACzB,CAAC;IACH,CAAC;SAAM,CAAC;QACR;;;;;;;;;;;;;;;;;;;WAmBG;QACD,IAAI,GAAG,GAAQ,IAAI,SAAG,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IACjF,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAA;IACzB,CAAC;IAED,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAEvB,OAAO,GAAG,CAAC,IAAI,CAAA;AACjB,CAAC","sourcesContent":["import { URL } from 'url'\n\nimport { config } from '@things-factory/env'\n\n/*\n * 프록시가 헤더를 안 실어 주면서 TLS 종료를 하는 배치를 위한 못. 모듈이 실릴 때 한 번 읽는다\n * — 옮겨 오기 전에도 같은 자리에서 같은 방식으로 읽고 있었다.\n */\nconst protocol: string = config.get('protocol')\n\n/**\n * **이 요청이 들어온 우리 주소**에 경로를 붙여 절대 주소를 만든다.\n *\n * 로그인·OAuth 의 주소를 화면과 저장 값에 내려 줄 때 쓴다.\n *\n * buildAuthURL('/oauth/authorize', context)\n * buildAuthURL('/oauth2-client/callback', context)\n *\n * ── 왜 공용인가 ─────────────────────────────────────────────────────────────\n * 이 열두 줄이 auth-base 와 oauth2-client 에 **똑같이 베껴져** 있었다(2026-09-04).\n * x-forwarded 셋 읽기 · 폴백 · protocol 덮기 · pathname 붙이기가 같은 코드였다.\n * 한쪽만 고치는 일이 실제로 났다 — 그래서 한 집으로 모았다.\n *\n * ── 프록시 뒤에서는 프록시가 본 주소를 쓴다 ─────────────────────────────────\n * 리버스 프록시를 거치면 소켓이 보는 것은 프록시와의 연결이라 공개 주소가 아니다. 그때는\n * 프록시가 실어 주는 x-forwarded-* 가 유일한 단서다.\n *\n * ── 폴백에서 request.origin 을 쓰지 않는 이유 ───────────────────────────────\n * Koa 2 에서는 origin 이 정확히 우리가 원하는 값이다.\n *\n * [읽었음] node_modules/koa/lib/request.js:98\n * get origin() { return `${this.protocol}://${this.host}` }\n *\n * 그런데 **Koa 3 은 그 뜻을 바꾼다** — 현재 호스트가 아니라 브라우저가 보낸 Origin 헤더를\n * 준다(CORS 표준에 맞춘 변경, koajs/koa v3.0.0 릴리즈 노트). 그 헤더는 같은 출처 요청에는\n * 아예 없고 어떤 경우엔 문자열 null 이다. 그러면 new URL(...) 이 던지고 **로그인과 OAuth\n * 콜백이 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서 로그인해 봐야 안다.\n *\n * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 버전 모두 「이 요청이 들어온\n * 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**\n *\n * ── config 의 protocol 이 마지막에 이긴다 ───────────────────────────────────\n * 프록시가 헤더를 안 실어 주면서 TLS 종료를 하는 배치가 있다. 그때 여기서 읽은 것은 http 인데\n * 브라우저가 아는 것은 https 다. 그 경우를 설정으로 못 박는다.\n */\nexport function buildAuthURL(pathname: string, context: ResolverContext): string {\n\n const originalProtocol = context.headers['x-forwarded-proto']\n const originalHost = context.headers['x-forwarded-host']\n const originalPort = context.headers['x-forwarded-port']\n\n if (originalProtocol && originalHost) {\n var url: URL = new URL(`${originalProtocol}://${originalHost}`)\n if (originalPort) {\n url.port = originalPort\n }\n } else {\n /*\n * ── 왜 request.origin 을 쓰지 않나 ──────────────────────────────────────\n *\n * 여기가 하려는 일은 **이 요청이 들어온 우리 주소**를 얻는 것이다. 위의 x-forwarded-*\n * 가 없을 때(프록시를 안 거친 직접 접속) 그 값을 요청에서 읽는다.\n *\n * 전에는 context.request.origin 이었다. Koa 2 에서는 그것이 정확히 우리가 원하는 값이다.\n *\n * [읽었음] node_modules/koa/lib/request.js:98\n * get origin() { return `${this.protocol}://${this.host}` }\n *\n * 그런데 **Koa 3 은 그 뜻을 바꾼다** — 현재 호스트가 아니라 브라우저가 보낸 Origin 헤더를\n * 준다(CORS 표준에 맞춘 변경, koajs/koa v3.0.0 릴리즈 노트). 그 헤더는 같은 출처 요청에는\n * 아예 없고 어떤 경우엔 문자열 null 이다. 그러면 new URL(...) 이 던지고, **로그인·OAuth\n * 콜백 주소를 만드는 이 자리에서 런타임에 깨진다.** 타입도 맞고 테스트도 도니까 띄워서\n * 로그인해 봐야 안다.\n *\n * 그래서 Koa 2 의 정의를 그대로 펼쳐 쓴다. protocol 과 host 는 두 버전 모두 「이 요청이\n * 들어온 곳」이고 app.proxy 설정도 그대로 존중한다. **오늘 값은 바뀌지 않는다.**\n */\n var url: URL = new URL(`${context.request.protocol}://${context.request.host}`)\n }\n\n if (protocol) {\n url.protocol = protocol\n }\n\n url.pathname = pathname\n\n return url.href\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"can-user-checkin-domain.js","sourceRoot":"","sources":["../../server/utils/can-user-checkin-domain.ts"],"names":[],"mappings":";;AAcA,wDAQC;AAGD,oDAEC;AA1BD,uEAAwE;AACxE,qEAAgG;AAEhG;;;;;;;;;GASG;AACI,KAAK,UAAU,sBAAsB,CAAC,IAAU,EAAE,MAA8B;IACrF,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,IAAA,sDAA4B,EAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,IAAA,wCAAe,EAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,CAAA;IAE/G,OAAO,IAAA,wCAAe,EAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AACvC,CAAC;AAED,8BAA8B;AACvB,KAAK,UAAU,oBAAoB,CAAC,IAAU,EAAE,MAA8B;IACnF,OAAO,CAAC,MAAM,sBAAsB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;AAChE,CAAC","sourcesContent":["import { User } from '../service/user/user.js'\nimport { collectCheckinFactsForDomain } from './checkin-domain-facts.js'\nimport { CheckinDomainCandidate, CheckinGrant, checkinGrantsOf } from './checkin-domain-rule.js'\n\n/**\n * 사용자가 이 도메인에 체크인할 수 있는 근거를 반환한다 (없으면 빈 배열).\n *\n * 매 요청 통과하는 도메인 인증 경로용. 도메인 전체 목록(`getUserDomains`)을 계산하지 않고\n * 대상 도메인 하나만 판정하므로, 통상적인 경우 병렬 2회 조회로 끝난다.\n *\n * @param domain 상속·소유권 판정에 `parentId`·`owner` 를 사용하므로 온전히 적재된 도메인이어야\n * 한다 (`context.state.domain`). 필드가 누락된 부분 객체를 넘기면 그 경로는\n * 성립하지 않는 것으로 취급된다(fail-closed).\n */\nexport async function checkinGrantsForDomain(user: User, domain: CheckinDomainCandidate): Promise<CheckinGrant[]> {\n if (!user?.id || !domain?.id) {\n return []\n }\n\n const facts = await collectCheckinFactsForDomain(user, domain, facts => !checkinGrantsOf(domain, facts).length)\n\n return checkinGrantsOf(domain, facts)\n}\n\n/** 사용자가 이 도메인에 체크인할 수 있는지. */\nexport async function canUserCheckinDomain(user: User, domain: CheckinDomainCandidate): Promise<boolean> {\n return (await checkinGrantsForDomain(user, domain)).length > 0\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"check-permission.js","sourceRoot":"","sources":["../../server/utils/check-permission.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain } from '@things-factory/shell'\nimport { PrivilegeObject } from '../service/privilege/privilege.js'\nimport { User } from '../service/user/user.js'\n\n/**\n * Why a gate said no - what the person can do about it differs (ADR-0103 decision 1).\n *\n * role none of their roles holds category:privilege - a role can be given (an access request)\n * address refused from where they are: the domain's protected address list\n * license refused by what this installation is licensed for\n * ownership the declaration opens only to domain or system owners - no role gives it\n * unknown a prohibition that does not say where it came from - the reason is not made up\n */\nexport type RefusalCause = 'role' | 'address' | 'license' | 'ownership' | 'unknown'\n\n/**\n * A privilege refused by the request, whatever the person's roles. `by` says who refused it:\n * the domain's address list (shell domain-middleware) or the installation's license (board-service).\n */\nexport type ProhibitedPrivilege = { category: string; privilege: string; by?: 'address' | 'license' }\n\n/**\n * What opened a gate - the audit keeps it with every check (ADR-0141 §3.2), so a pass that rested on\n * superuser is told from one a role gave.\n */\nexport type GrantBasis = 'role' | 'domain-owner' | 'superuser'\n\nexport type PermissionJudgement = { permitted: true; by?: GrantBasis } | { permitted: false; because: RefusalCause }\n\n/* A gate that declares nothing - nothing opened it. */\nconst PERMITTED: PermissionJudgement = { permitted: true }\n\nfunction granted(by: GrantBasis): PermissionJudgement {\n return { permitted: true, by }\n}\n\nfunction refused(because: RefusalCause): PermissionJudgement {\n return { permitted: false, because }\n}\n\nfunction prohibitionOf(prohibitedPrivileges: ProhibitedPrivilege[] | undefined, category: string, privilege: string) {\n return (prohibitedPrivileges || []).find(pp => pp.category == category && pp.privilege == privilege)\n}\n\n/**\n * The gate, with its reason. `checkPermission` answers only the boolean and is what most callers\n * want; the privilege directive reads this so a refusal can say which of the four it was.\n */\nexport async function judgePermission(\n privilegeObject: PrivilegeObject,\n user: User,\n domain: Domain,\n unsafeIP?: boolean,\n prohibitedPrivileges?: ProhibitedPrivilege[]\n): Promise<PermissionJudgement> {\n if (!privilegeObject) {\n return PERMITTED\n }\n\n const { owner: domainOwnerGranted, super: superUserGranted, category, privilege } = privilegeObject\n\n /*\n * Ownership as the gate has always asked it - `process` carries the answer (domain-authenticate-middleware\n * installs it). Domain owner first, then superuser, as the `||` it replaces; it now says which one opened.\n */\n const ownershipBasis = async (): Promise<GrantBasis | null> => {\n if (domainOwnerGranted && (await (process as any).domainOwnerGranted(domain, user))) return 'domain-owner'\n if (superUserGranted && (await (process as any).superUserGranted(domain, user))) return 'superuser'\n return null\n }\n const ownershipOpens = async () => !!(await ownershipBasis())\n\n /*\n * From an unsafe address ownership never opened anything and was never asked. It is asked now only on the\n * way to a refusal, to tell \"address\" from \"role\" - and a missing answer is \"no\", so the gate refuses as it did.\n */\n const ownershipWouldHaveOpened = async () => {\n try {\n return await ownershipOpens()\n } catch {\n return false\n }\n }\n\n if (unsafeIP) {\n if (privilege && category) {\n // unsafeIP 상황에서는 ownership granted는 적용되지 않는다.\n const prohibition = prohibitionOf(prohibitedPrivileges, category, privilege)\n if (prohibition) {\n return refused(prohibition.by ?? 'unknown')\n }\n\n if (await User.hasPrivilege(privilege, category, domain, user)) {\n return granted('role')\n }\n\n /* An owner without the role would pass from a safe address - what stopped them is where they are. */\n return refused((await ownershipWouldHaveOpened()) ? 'address' : 'role')\n }\n\n /*\n * Ownership does not count from an unsafe address, so a declaration that rests on\n * ownership alone has nothing left to stand on here and the answer is no.\n *\n * This read `!domainOwnerGranted && !superUserGranted` until 2026-09-15, which answered\n * **yes** for an object that named nothing at all — the same `{}` a safe address\n * refused. An unsafe address was the more permissive of the two, which is backwards.\n * `privilege-directive.ts` now refuses to build a declaration that names nothing, but\n * this function is exported and called directly, so the rule is fixed here too.\n */\n return refused('address')\n } else {\n if (!privilege || !category) {\n // privilege, category가 설정되지 않은 경우에는 ownership granted만을 적용한다.\n const basis = await ownershipBasis()\n return basis ? granted(basis) : refused('ownership')\n }\n\n const basis = await ownershipBasis()\n if (basis) {\n return granted(basis)\n }\n\n const prohibition = prohibitionOf(prohibitedPrivileges, category, privilege)\n if (prohibition) {\n return refused(prohibition.by ?? 'unknown')\n }\n\n return (await User.hasPrivilege(privilege, category, domain, user)) ? granted('role') : refused('role')\n }\n}\n\nexport async function checkPermission(\n privilegeObject: PrivilegeObject,\n user: User,\n domain: Domain,\n unsafeIP?: boolean,\n prohibitedPrivileges?: ProhibitedPrivilege[]\n): Promise<boolean> {\n return (await judgePermission(privilegeObject, user, domain, unsafeIP, prohibitedPrivileges)).permitted\n}\n\n/**\n * Whether the request holds `category:privilege`, asked as the doors ask it (`domainOwnerGranted: true`) - the answer\n * a code path needs when one door serves people of different reach (a manager every row, anyone else their own).\n * Asked once per request: a field resolver runs for every row of a list, and the answer is kept on `context.state`.\n * No domain or no user holds nothing.\n */\nexport async function requestHolds(context: any, category: string, privilege: 'query' | 'mutation' = 'mutation'): Promise<boolean> {\n const state = context?.state\n const { domain, user, unsafeIP, prohibitedPrivileges } = state ?? {}\n if (!domain || !user) return false\n const asked: Map<string, Promise<boolean>> = (state.__privilegesHeld ??= new Map())\n const key = `${category}:${privilege}`\n if (!asked.has(key)) asked.set(key, checkPermission({ category, privilege, owner: true } as any, user, domain, unsafeIP, prohibitedPrivileges))\n return await asked.get(key)!\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"check-user-belongs-domain.js","sourceRoot":"","sources":["../../server/utils/check-user-belongs-domain.ts"],"names":[],"mappings":";;AAWA,wDAOC;AAlBD,iDAA6D;AAE7D,qDAA8C;AAE9C;;;;;;GAMG;AACI,KAAK,UAAU,sBAAsB,CAAC,MAAc,EAAE,IAAU;IACrE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAC1B,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;IAC9F,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAS,IAAI,CAAA;IAC3C,OAAO,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,UAAkB,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;AACvF,CAAC","sourcesContent":["import { Domain, getRepository } from '@things-factory/shell'\n\nimport { User } from '../service/user/user.js'\n\n/**\n * @description Based on domain and user information,\n * Find out whether the user belongs domain or user has partnership with domain\n *\n * @param domain\n * @param user\n */\nexport async function checkUserBelongsDomain(domain: Domain, user: User): Promise<Boolean> {\n if (!user.domains?.length) {\n user = await getRepository(User).findOne({ where: { id: user.id }, relations: ['domains'] })\n }\n\n const { domains: userDomains }: User = user\n return Boolean(userDomains.find((userDomain: Domain) => userDomain.id === domain.id))\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"check-user-has-role.js","sourceRoot":"","sources":["../../server/utils/check-user-has-role.ts"],"names":[],"mappings":";;AAcA,4CAcC;AA5BD,iDAA6D;AAE7D,qDAA8C;AAG9C;;;;;;;;GAQG;AACI,KAAK,UAAU,gBAAgB,CAAC,MAAc,EAAE,MAAc,EAAE,IAAU;IAC/E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,OAAO,CAAC;QAC3C,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE;QACtB,SAAS,EAAE,CAAC,OAAO,CAAC;KACrB,CAAC,CAAA;IAEF,OAAO,EAAE,CAAC,KAAK;SACZ,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC;SACrG,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;SACpB,QAAQ,CAAC,MAAM,CAAC,CAAA;AACrB,CAAC","sourcesContent":["import { Domain, getRepository } from '@things-factory/shell'\n\nimport { User } from '../service/user/user.js'\nimport { Role } from '../service/role/role.js'\n\n/**\n * @description 사용자가 특정 도메인 또는 상위 도메인에서 특정 역할을 가지고 있는지 확인합니다.\n *\n * @param roleId 확인할 역할의 ID\n * @param domain 역할을 확인할 도메인\n * @param user 역할을 확인할 사용자\n *\n * @returns 사용자가 도메인 또는 상위 도메인에서 역할을 가지고 있는지 여부를 나타내는 boolean을 반환하는 Promise\n */\nexport async function checkUserHasRole(roleId: string, domain: Domain, user: User): Promise<Boolean> {\n if (!roleId) {\n return true\n }\n\n const me = await getRepository(User).findOne({\n where: { id: user.id },\n relations: ['roles']\n })\n\n return me.roles\n .filter(role => role.domainId === domain.id || (domain.parentId && role.domainId === domain.parentId))\n .map(role => role.id)\n .includes(roleId)\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"checkin-domain-facts.js","sourceRoot":"","sources":["../../server/utils/checkin-domain-facts.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { DomainOwner } from '../service/domain-owner/domain-owner.js'\nimport { GrantedRole } from '../service/granted-role/granted-role.js'\nimport { Role } from '../service/role/role.js'\nimport { User } from '../service/user/user.js'\nimport { CheckinDomainCandidate, CheckinFacts } from './checkin-domain-rule.js'\nimport { isDomainOwner } from './domain-ownership.js'\n\n/**\n * 체크인 판정(`checkin-domain-rule.ts`)에 필요한 사실을 데이터베이스에서 수집한다.\n *\n * 판정 규칙과 조회를 분리한 이유:\n * - 규칙은 순수 함수로 두어 시나리오를 빠짐없이 단위 테스트할 수 있게 한다.\n * - 조회는 목적에 따라 두 가지 비용 프로파일을 갖는다. 도메인 하나에 대한 가부 판정(매 요청\n * 통과하는 핫패스)과, 체크인 화면용 전체 목록 계산(로그인·부트스트랩 시점)이다.\n *\n * 모든 조회는 TypeORM 리포지토리/쿼리빌더 API 만 사용한다 (raw SQL·DB 고유 문법 없음).\n * `IN` 절은 빈 배열이면 일부 드라이버에서 문법 오류가 되므로 항상 길이를 먼저 확인한다.\n */\n\n/**\n * \"이 도메인의 멤버로서 부여받은 역할\" 이 정의된 도메인 id.\n *\n * 멤버십(`users_domains`)과 역할 보유(`users_roles`)를 한 번의 조회로 함께 확인한다 —\n * 매 요청 통과하는 판정이라 왕복 횟수를 최소화한다. 결과가 비어 있으면 \"멤버가 아님\" 과\n * \"부여된 역할이 없음\" 을 구분할 수 없지만, 두 경우 모두 이 경로로는 체크인할 수 없으므로\n * 판정에는 차이가 없다.\n */\nasync function findGrantedRoleDomainIds(user: User, domainId: string, roleDomainIds: string[]): Promise<string[]> {\n if (!roleDomainIds.length) {\n return []\n }\n\n const rows = await getRepository(User)\n .createQueryBuilder('USER')\n .innerJoin('USER.domains', 'MEMBER_DOMAIN', 'MEMBER_DOMAIN.id = :domainId', { domainId })\n .innerJoin('USER.roles', 'ROLE')\n .innerJoin('ROLE.domain', 'ROLE_DOMAIN')\n .where('USER.id = :userId', { userId: user.id })\n .andWhere('ROLE_DOMAIN.id IN (:...roleDomainIds)', { roleDomainIds })\n .select('ROLE_DOMAIN.id', 'roleDomainId')\n .distinct(true)\n .getRawMany()\n\n return [...new Set<string>(rows.map(row => row.roleDomainId).filter(Boolean))]\n}\n\n/**\n * 파트너십 경로 — 이 도메인에 정의된 역할이 사용자의 소속 도메인에 grant 되어 있는지.\n * (공급자 도메인이 자기 역할을 고객 도메인에 grant 하고, 고객 사용자가 그 역할을 부여받는 구조)\n */\nasync function isPartnerGrantedDomain(user: User, domainId: string): Promise<boolean> {\n return await getRepository(GrantedRole)\n .createQueryBuilder('GRANTED_ROLE')\n .innerJoin('GRANTED_ROLE.role', 'ROLE')\n .innerJoin('ROLE.users', 'ROLE_USER')\n .innerJoin('ROLE_USER.domains', 'MEMBER_DOMAIN')\n .where('ROLE_USER.id = :userId', { userId: user.id })\n .andWhere('ROLE.domain = :domainId', { domainId })\n .andWhere('GRANTED_ROLE.domain = MEMBER_DOMAIN.id')\n .getExists()\n}\n\n/**\n * 도메인 하나에 대한 판정용 사실 수집 (핫패스).\n *\n * 통상적인 통과(그 도메인의 멤버로서 역할을 부여받은 사용자)는 **1회 조회**로 끝난다.\n * 파트너십·소유권 조회는 그것만으로 근거가 서지 않을 때만 병렬로 수행한다.\n *\n * @param domain 판정 대상 도메인. 상속·소유권 판정에 `parentId`·`owner` 가 필요하므로\n * `context.state.domain` 처럼 온전히 적재된 도메인을 넘겨야 한다.\n * @param stillNeeded 1차 사실만으로 근거가 섰는지 판단하는 콜백 (규칙 함수를 주입해 규칙의\n * 단일 정의를 유지한다).\n */\nexport async function collectCheckinFactsForDomain(\n user: User,\n domain: CheckinDomainCandidate,\n stillNeeded: (facts: CheckinFacts) => boolean\n): Promise<CheckinFacts> {\n /* 상속은 부모 1단까지 — shell 의 `applyDomainInheritanceFilter` 규약과 같다. */\n const inheritableDomainIds = [domain.id, domain.parentId].filter(Boolean) as string[]\n\n const grantedRoleDomainIds = await findGrantedRoleDomainIds(user, domain.id, inheritableDomainIds)\n\n const facts: CheckinFacts = {\n userId: user.id,\n memberDomainIds: grantedRoleDomainIds.length ? [domain.id] : [],\n roleDomainIds: grantedRoleDomainIds,\n partnerRoleDomainIds: [],\n ownedDomainIds: []\n }\n\n if (!stillNeeded(facts)) {\n return facts\n }\n\n const [partner, owner] = await Promise.all([\n isPartnerGrantedDomain(user, domain.id),\n isDomainOwner(domain, user)\n ])\n\n return {\n ...facts,\n /* 파트너 조회가 \"이 도메인에 정의된 역할 보유\" 까지 함께 확인하므로 역할 보유 사실도 채운다. */\n roleDomainIds: partner ? [...new Set([...grantedRoleDomainIds, domain.id])] : grantedRoleDomainIds,\n partnerRoleDomainIds: partner ? [domain.id] : [],\n ownedDomainIds: owner ? [domain.id] : []\n }\n}\n\n/** 전체 목록 판정용 사실 + 판정 대상 도메인(행까지 적재된 상태). */\nexport type UserCheckinCandidates = {\n facts: CheckinFacts\n candidates: Domain[]\n}\n\n/**\n * 사용자가 체크인할 수 있는 도메인 목록을 계산하기 위한 사실 + 후보 도메인 수집.\n * 로그인·체크인 화면 등 목록이 필요한 지점에서만 쓴다 (매 요청 경로에서는\n * `collectCheckinFactsForDomain` 을 쓴다).\n *\n * 조회 설계:\n * - 경로별(소속·소유·파트너) 조회가 **도메인 행을 직접** 반환한다. 후보 id 를 모은 뒤 다시\n * 적재하는 단계가 없어져 왕복이 한 묶음으로 줄었다.\n * - 네 조회는 서로 의존하지 않으므로 한 번에 병렬로 던진다.\n * - 관계를 적재한 엔티티(`findOne({relations})`)는 쓰지 않는다 — 판정에 필요한 것은 id 뿐인데\n * 엔티티 적재는 역할·도메인 전체를 hydration 한다.\n * - 판정 자체는 하지 않는다. 어떤 후보가 실제로 체크인 가능한지는 `checkinGrantsOf` 한 곳에서만\n * 결정한다.\n */\nexport async function collectCheckinCandidates(user: User): Promise<UserCheckinCandidates> {\n const [memberDomains, roleDomainIds, ownedDomains, partnerRoleDomainIds] = await Promise.all([\n findMemberDomains(user),\n findRoleDomainIds(user),\n findOwnedDomains(user),\n findPartnerRoleDomainIds(user)\n ])\n\n /* 파트너 경로로 열린 도메인은 소속 도메인이 아니므로 별도로 적재한다 (해당 사용자가 있을 때만). */\n const partnerDomains: Domain[] = partnerRoleDomainIds.size\n ? await getRepository(Domain).find({ where: { id: In([...partnerRoleDomainIds]) } })\n : []\n\n const candidateById = new Map<string, Domain>()\n for (const domain of [...memberDomains, ...ownedDomains, ...partnerDomains]) {\n candidateById.set(domain.id, domain)\n }\n\n return {\n facts: {\n userId: user.id,\n memberDomainIds: new Set<string>(memberDomains.map((domain: Domain) => domain.id)),\n roleDomainIds,\n partnerRoleDomainIds,\n ownedDomainIds: new Set<string>(ownedDomains.map((domain: Domain) => domain.id))\n },\n candidates: [...candidateById.values()]\n }\n}\n\n/** 사용자가 소속된 도메인 (`users_domains`). */\nasync function findMemberDomains(user: User): Promise<Domain[]> {\n return await getRepository(Domain)\n .createQueryBuilder('DOMAIN')\n .where(qb => {\n const subQuery = qb\n .subQuery()\n .select('MEMBER_DOMAIN.id')\n .from(User, 'MEMBER_USER')\n .innerJoin('MEMBER_USER.domains', 'MEMBER_DOMAIN')\n .where('MEMBER_USER.id = :userId')\n .getQuery()\n return 'DOMAIN.id IN ' + subQuery\n })\n .setParameter('userId', user.id)\n .getMany()\n}\n\n/** 사용자가 소유한 도메인 — 대표 오너 캐시 컬럼과 `DomainOwner` 테이블 양쪽. */\nasync function findOwnedDomains(user: User): Promise<Domain[]> {\n // 주의: `Domain.owner` 는 varchar(사용자 id 문자열 캐시)이고 `DomainOwner.user` 는 uuid FK 다.\n // 두 비교에 **동일 파라미터를 재사용하면** Postgres 가 준비된 구문의 파라미터를 하나의\n // 타입으로만 추론하다 `operator does not exist: uuid = text` 로 실패한다. → 값은 같아도\n // owner(varchar) 는 :ownerId, user_id(uuid) 는 :userId 로 **파라미터를 분리**한다.\n return await getRepository(Domain)\n .createQueryBuilder('DOMAIN')\n .where('DOMAIN.owner = :ownerId')\n .orWhere(qb => {\n const subQuery = qb\n .subQuery()\n .select('OWNED_DOMAIN.id')\n .from(DomainOwner, 'DOMAIN_OWNER')\n .innerJoin('DOMAIN_OWNER.domain', 'OWNED_DOMAIN')\n .where('DOMAIN_OWNER.user = :userId')\n .getQuery()\n return 'DOMAIN.id IN ' + subQuery\n })\n .setParameter('ownerId', user.id)\n .setParameter('userId', user.id)\n .getMany()\n}\n\n/** 사용자가 보유한 역할이 정의된 도메인 id. */\nasync function findRoleDomainIds(user: User): Promise<Set<string>> {\n const rows = await getRepository(Role)\n .createQueryBuilder('ROLE')\n .innerJoin('ROLE.users', 'ROLE_USER')\n .innerJoin('ROLE.domain', 'ROLE_DOMAIN')\n .where('ROLE_USER.id = :userId', { userId: user.id })\n .select('ROLE_DOMAIN.id', 'roleDomainId')\n .distinct(true)\n .getRawMany()\n\n return new Set<string>(rows.map(row => row.roleDomainId).filter(Boolean))\n}\n\n/**\n * 파트너십 경로로 열린 도메인 id — 사용자가 보유한 역할 중, 그 역할이 사용자의 소속 도메인에\n * grant 된 것들의 정의 도메인. 역할 보유·grant·소속을 한 조회에서 함께 확인한다.\n */\nasync function findPartnerRoleDomainIds(user: User): Promise<Set<string>> {\n const rows = await getRepository(GrantedRole)\n .createQueryBuilder('GRANTED_ROLE')\n .innerJoin('GRANTED_ROLE.role', 'ROLE')\n .innerJoin('ROLE.domain', 'ROLE_DOMAIN')\n .innerJoin('ROLE.users', 'ROLE_USER')\n .innerJoin('ROLE_USER.domains', 'MEMBER_DOMAIN')\n .where('ROLE_USER.id = :userId', { userId: user.id })\n .andWhere('GRANTED_ROLE.domain = MEMBER_DOMAIN.id')\n .select('ROLE_DOMAIN.id', 'roleDomainId')\n .distinct(true)\n .getRawMany()\n\n return new Set<string>(rows.map(row => row.roleDomainId).filter(Boolean))\n}\n\n/** 사용자가 보유한 역할 중 주어진 권한을 가진 역할이 정의된 도메인 id 집합. */\nexport async function findRoleDomainIdsWithPrivilege(\n user: User,\n privilege: string,\n category: string\n): Promise<Set<string>> {\n const roles: Role[] = await getRepository(Role)\n .createQueryBuilder('ROLE')\n .innerJoin('ROLE.users', 'ROLE_USER')\n .innerJoin('ROLE.privileges', 'PRIVILEGE')\n .innerJoinAndSelect('ROLE.domain', 'ROLE_DOMAIN')\n .where('ROLE_USER.id = :userId', { userId: user.id })\n .andWhere('PRIVILEGE.name = :privilege', { privilege })\n .andWhere('PRIVILEGE.category = :category', { category })\n .getMany()\n\n return new Set<string>(roles.map((role: Role) => role.domain?.id).filter(Boolean))\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"checkin-domain-rule.js","sourceRoot":"","sources":["../../server/utils/checkin-domain-rule.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;;AA0DH,0CA8BC;AAGD,4CAEC;AAnDD,SAAS,SAAS,CAAC,KAAmB;IACpC,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,eAAe,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,EAAE,CAAC;QACrD,aAAa,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,aAAa,IAAI,EAAE,CAAC;QACjD,oBAAoB,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,oBAAoB,IAAI,EAAE,CAAC;QAC/D,cAAc,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,cAAc,IAAI,EAAE,CAAC;KACpD,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,MAA8B,EAAE,KAAmB;IACjF,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC;QAChB,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;IACzG,MAAM,MAAM,GAAmB,EAAE,CAAA;IAEjC,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QAC/E,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;IAC/B,CAAC;IAED,IAAI,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QACnC,mCAAmC;QACnC,IAAI,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAA;QACvD,CAAC;QAED,wCAAwC;QACxC,IAAI,MAAM,CAAC,QAAQ,IAAI,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1D,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;QACvE,CAAC;IACH,CAAC;IAED,6DAA6D;IAC7D,IAAI,oBAAoB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QACxE,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAA;IAC1D,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,iBAAiB;AACjB,SAAgB,gBAAgB,CAAC,MAA8B,EAAE,KAAmB;IAClF,OAAO,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;AAClD,CAAC","sourcesContent":["/**\n * 사용자가 어떤 도메인에 체크인할 수 있는지를 판정하는 규칙 (순수 함수).\n *\n * ## 왜 \"역할이 정의된 도메인\" 이 아닌가\n *\n * 역할(Role)은 도메인에 정의되지만, 자식 도메인은 부모 도메인의 역할을 **상속**한다\n * (단일 소스: `applyDomainInheritanceFilter` — 상속은 부모 1단까지).\n * 따라서 부모 도메인에 정의된 역할을 자식 도메인 화면에서 사용자에게 부여하는 것이 정상 동작이며,\n * 이때 체크인 대상은 **역할을 부여한 도메인(자식)** 이지 역할을 정의한 도메인(부모)이 아니다.\n *\n * `users_roles` 조인 테이블에는 부여 도메인이 기록되지 않으므로, 부여 도메인은\n * `users_domains`(멤버십)로 복원한다. 역할 부여 mutation(`updateUserRoles`)이\n * \"대상 사용자가 현재 도메인의 멤버여야 함\"을 강제하기 때문에 이 복원은 성립한다.\n *\n * ## 멤버십을 AND 조건으로 거는 이유\n *\n * 부모 도메인의 역할은 모든 자식 도메인이 상속한다. 멤버십 없이 \"부모 역할 보유\"만으로 체크인을\n * 허용하면, 한 자식 도메인용으로 부여한 역할이 형제 자식 도메인 전체에 대한 접근권이 되어\n * 테넌트 격리가 깨진다.\n *\n * ## 파트너십 경로\n *\n * 파트너십(`GrantedRole`)은 공급자 도메인이 자기 역할을 고객 도메인에 grant 하고, 고객 도메인\n * 관리자가 그 역할을 자기 사용자에게 부여하는 구조다. 이 사용자는 공급자 도메인의 멤버가 아니지만\n * 공급자 도메인에 체크인해야 한다. 그래서 멤버십 대신 \"내 소속 도메인에 grant 된 역할\" 을 근거로\n * 삼는 별도 경로를 둔다.\n */\n\n/** 판정 대상 도메인 — 판정에 필요한 최소 필드만 요구한다. */\nexport type CheckinDomainCandidate = {\n id: string\n parentId?: string | null\n /** 하위 호환 단일 owner 캐시 컬럼 (권위 있는 소유권은 `DomainOwner` 테이블). */\n owner?: string | null\n}\n\n/** 판정에 필요한 사용자 측 사실들 — 조회는 호출자(`get-user-domains`)가 담당한다. */\nexport type CheckinFacts = {\n userId: string\n /** `users_domains` — 사용자가 멤버인 도메인. 역할이 \"부여된\" 도메인의 근거. */\n memberDomainIds: Iterable<string>\n /** 사용자가 보유한 역할이 \"정의된\" 도메인 (role.domainId). */\n roleDomainIds: Iterable<string>\n /** `GrantedRole` 로 사용자의 소속 도메인에 grant 된 역할이 정의된 도메인. */\n partnerRoleDomainIds?: Iterable<string>\n /** `DomainOwner` 테이블 기준 사용자가 소유한 도메인. */\n ownedDomainIds?: Iterable<string>\n}\n\n/**\n * 체크인이 허용되는 근거.\n * `roleDomainId` 는 그 근거가 된 역할이 정의된 도메인으로, 권한(privilege) 판정에서\n * \"어느 도메인의 역할을 근거로 들어왔는지\" 를 다시 확인하는 데 쓴다.\n */\nexport type CheckinGrant =\n | { via: 'owner' }\n | { via: 'role'; roleDomainId: string }\n | { via: 'inherited-role'; roleDomainId: string }\n | { via: 'partner'; roleDomainId: string }\n\ntype NormalizedFacts = {\n userId: string\n memberDomainIds: Set<string>\n roleDomainIds: Set<string>\n partnerRoleDomainIds: Set<string>\n ownedDomainIds: Set<string>\n}\n\nfunction normalize(facts: CheckinFacts): NormalizedFacts {\n return {\n userId: facts.userId,\n memberDomainIds: new Set(facts.memberDomainIds || []),\n roleDomainIds: new Set(facts.roleDomainIds || []),\n partnerRoleDomainIds: new Set(facts.partnerRoleDomainIds || []),\n ownedDomainIds: new Set(facts.ownedDomainIds || [])\n }\n}\n\n/**\n * 도메인에 대한 체크인 근거를 모두 반환한다. 비어 있으면 체크인 불가.\n *\n * 근거를 하나만 반환하지 않는 이유: 같은 도메인이 소유권·자기 역할·상속 역할 중 둘 이상으로\n * 정당화될 수 있고, 권한 판정은 그중 \"권한을 가진 역할\" 을 근거로 삼아야 하기 때문이다.\n */\nexport function checkinGrantsOf(domain: CheckinDomainCandidate, facts: CheckinFacts): CheckinGrant[] {\n if (!domain?.id) {\n return []\n }\n\n const { userId, memberDomainIds, roleDomainIds, partnerRoleDomainIds, ownedDomainIds } = normalize(facts)\n const grants: CheckinGrant[] = []\n\n if ((domain.owner && domain.owner === userId) || ownedDomainIds.has(domain.id)) {\n grants.push({ via: 'owner' })\n }\n\n if (memberDomainIds.has(domain.id)) {\n /* 이 도메인에 정의된 역할을 이 도메인에서 부여받았다. */\n if (roleDomainIds.has(domain.id)) {\n grants.push({ via: 'role', roleDomainId: domain.id })\n }\n\n /* 부모 도메인에 정의된 역할(상속)을 이 도메인에서 부여받았다. */\n if (domain.parentId && roleDomainIds.has(domain.parentId)) {\n grants.push({ via: 'inherited-role', roleDomainId: domain.parentId })\n }\n }\n\n /* 파트너십으로 이 도메인의 역할이 내 소속 도메인에 grant 된 경우 — 멤버십을 요구하지 않는다. */\n if (partnerRoleDomainIds.has(domain.id) && roleDomainIds.has(domain.id)) {\n grants.push({ via: 'partner', roleDomainId: domain.id })\n }\n\n return grants\n}\n\n/** 체크인 가능 여부. */\nexport function isCheckinAllowed(domain: CheckinDomainCandidate, facts: CheckinFacts): boolean {\n return checkinGrantsOf(domain, facts).length > 0\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"credential-at-rest.js","sourceRoot":"","sources":["../../server/utils/credential-at-rest.ts"],"names":[],"mappings":";;;AAEA,iDAA0D;AAE1D,qEAAoE;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACU,QAAA,2BAA2B,GAAqB;IAC3D,EAAE,EAAE,CAAC,WAAmB,EAAE,EAAE,CAAC,CAAC,IAAA,kDAAyB,GAAE,CAAC,CAAC,CAAC,0BAAkB,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IAE7G,IAAI,EAAE,CAAC,aAAqB,EAAE,EAAE,CAAC,0BAAkB,CAAC,IAAI,CAAC,aAAa,CAAC;CACxE,CAAA","sourcesContent":["import { ValueTransformer } from 'typeorm'\n\nimport { encryptTransformer } from '@things-factory/shell'\n\nimport { encryptsCredentialsAtRest } from './credential-features.js'\n\n/**\n * A credential column that encrypts only where the installation asked for it (ADR-0085).\n *\n * ── The switch governs writing. Reading always takes both ───────────────────\n * `shell`'s `encryptTransformer` reads a value that is not ciphertext-shaped as plaintext, so\n * rows written before the switch was turned on stay readable, and each row converts the next\n * time something writes it. That behaviour does **not** move with the switch — if it did,\n * turning the feature off would lose every row written while it was on.\n *\n * So off means \"write it the way we always did\", not \"pretend the encrypted rows are not\n * there\".\n *\n * ── The switch is read per write, not once at import ────────────────────────\n * A module-level constant would freeze whatever the config looked like when the entity file was\n * first loaded, which is not the same thing in tests and not obviously the same thing at boot.\n * `to` asks each time; it is a property lookup.\n *\n * ── Where this is NOT used: `User.password` ─────────────────────────────────\n * That column holds a different thing depending on who the row is. For a person it is an HMAC\n * of their password — already not the secret, so encrypting it protects nothing. For an\n * appliance or an app binding it is the token itself, which is exactly what this is for.\n *\n * A column transformer cannot tell those apart, and encrypting people's hashes to reach the\n * machines' tokens is the wrong trade. The machine credential belongs somewhere of its own;\n * that is the Principal / Credential split, §8 step 4 of `docs/design/auth-rebuild.md`.\n */\nexport const credentialAtRestTransformer: ValueTransformer = {\n to: (entityValue: string) => (encryptsCredentialsAtRest() ? encryptTransformer.to(entityValue) : entityValue),\n\n from: (databaseValue: string) => encryptTransformer.from(databaseValue)\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"credential-features.js","sourceRoot":"","sources":["../../server/utils/credential-features.ts"],"names":[],"mappings":";;AA2BA,8DAEC;AAWD,gEAEC;AA1CD,6CAA4C;AAE5C;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;GAMG;AACH,SAAgB,yBAAyB;IACvC,OAAO,CAAC,CAAC,YAAM,CAAC,GAAG,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAA;AACxD,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,0BAA0B;IACxC,OAAO,CAAC,CAAC,YAAM,CAAC,GAAG,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAA;AACpD,CAAC","sourcesContent":["import { config } from '@things-factory/env'\n\n/**\n * The two credential features an installation turns on for itself (ADR-0085).\n *\n * ── Why they are switches and not just code ─────────────────────────────────\n * An appliance token lives a year, and the ones already out in the field were issued by the\n * version running now. Shipping either feature as plain behaviour would change what those\n * deployments do the moment they take the release. So the installation decides, and a\n * deployment that changes nothing keeps working exactly as it does today.\n *\n * `config.get(key, false)` answers the default when the key is absent, so \"not configured\"\n * and \"off\" are the same thing here.\n *\n * ⚠ That `get` treats every falsy value as absent, so writing `false` in the config file also\n * lands on the default. It matches while the default is `false`. **Do not flip these defaults\n * to `true`** — turning them off again would stop working, and the only way off would be to\n * delete the key.\n */\n\n/**\n * Store credentials as ciphertext rather than plaintext.\n *\n * ⚠ This governs **writing only**. Reading always accepts both shapes — see\n * `shell/server/typeorm/encrypt-transform.ts`. If reading followed the switch, turning the\n * feature off would lose every row written while it was on.\n */\nexport function encryptsCredentialsAtRest(): boolean {\n return !!config.get('credential/encryptAtRest', false)\n}\n\n/**\n * Offer revocation of an issued credential.\n *\n * ⚠ This governs **the door only** — whether the mutation and its screen exist. It does not\n * govern whether an existing revocation holds. A credential revoked while this was on stays\n * revoked after it is turned off, because a lost device must not come back through a config\n * change. The check that enforces it does nothing on its own: a subject that was never\n * revoked carries no serial, and the comparison never fires.\n */\nexport function offersCredentialRevocation(): boolean {\n return !!config.get('credential/revocable', false)\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"credential-serial-rule.js","sourceRoot":"","sources":["../../server/utils/credential-serial-rule.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;AAkBH,0DAQC;AAdD;;;;;GAKG;AACH,SAAgB,uBAAuB,CAAC,OAA0B,EAAE,MAAwB;IAC1F,MAAM,OAAO,GAAG,OAAO,EAAE,gBAAgB,CAAA;IAEzC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC9C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,MAAM,EAAE,EAAE,KAAK,OAAO,CAAA;AAC/B,CAAC","sourcesContent":["/**\n * Does this token still name the current generation of its subject's credential? (Pure rule.)\n *\n * ## Why the rule is its own function\n *\n * `checkAuth` runs on every request, and this is the one line in it that can refuse a token\n * whose signature is perfectly good. A rule that decides that belongs somewhere it can be read\n * and tested on its own, next to `account-lock-rule` and `checkin-domain-rule`.\n *\n * ## What an absent serial means, and why it must pass\n *\n * An appliance token lives a year. The ones in the field were signed before the column existed,\n * so they carry no serial, and the subjects they name have none recorded. **Those have to pass**\n * — otherwise taking the release cuts off every device at once, and the point of the design was\n * that it does not (ADR-0085 decision 3).\n *\n * A subject gains a serial the first time its credential is issued or revoked. From that moment\n * its tokens have to carry the same number, and an older one no longer does.\n *\n * ## Why this closes what a status could not\n *\n * Revocation used to live entirely in the shadow user's status, which meant putting the status\n * back let every token ever issued to that appliance work again. The serial moves on revoke and\n * never moves back, so reactivating a subject does not resurrect its old credentials — only\n * issuing a new one does, and that one carries the new number.\n */\n\n/** The parts of the subject this rule reads. */\nexport type CredentialSubject = {\n credentialSerial?: number | null\n}\n\n/** The parts of a verified token this rule reads. `cs` is absent on anything issued earlier. */\nexport type CredentialClaims = {\n cs?: number | null\n}\n\n/**\n * True when the token may still be used for this subject.\n *\n * Absent-on-the-subject means this subject has never been through the new path, and everything\n * passes. Present means the token has to match it exactly.\n */\nexport function credentialSerialMatches(subject: CredentialSubject, claims: CredentialClaims): boolean {\n const current = subject?.credentialSerial\n\n if (current === null || current === undefined) {\n return true\n }\n\n return claims?.cs === current\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"domain-ownership.js","sourceRoot":"","sources":["../../server/utils/domain-ownership.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain, getRepository } from '@things-factory/shell'\nimport { EntityManager } from 'typeorm'\n\nimport { DomainOwner } from '../service/domain-owner/domain-owner.js'\nimport { User } from '../service/user/user.js'\n\n/**\n * 도메인 소유권(= 도메인 관리자) 판정의 단일 소스.\n *\n * 소유권은 두 경로 중 하나로 인식된다:\n * 1) `DomainOwner` 테이블 엔트리 — 권위 있는 정보. 한 도메인에 여러 명 등록 가능.\n * 2) `Domain.owner` 컬럼 — 하위 호환 + \"대표 오너\" 표시용 캐시.\n *\n * 도메인 범위의 권한 판정은 둘 중 하나만 맞으면 오너로 인정해야 한다. 컬럼 한 개만 보면\n * 테이블로만 등록된 공동 오너가 권한을 잃는다.\n *\n * 예외: 전역 특권인 수퍼유저(`process.superUserGranted`)는 의도적으로 SYSTEM 도메인의\n * **대표 오너(`Domain.owner` 컬럼)** 한 명으로 제한한다. SYSTEM 도메인의 공동 오너는\n * SYSTEM 도메인의 오너로서만 권한을 갖는다.\n */\n/** 소유권 판정에 필요한 최소 필드 (`owner` 는 대표 오너 캐시로, 없을 수도 있다). */\nexport type DomainOwnershipTarget = { id: string; owner?: string | null }\n\nexport async function isDomainOwner(\n domain: DomainOwnershipTarget | null | undefined,\n user: Pick<User, 'id'> | null | undefined,\n tx?: EntityManager\n): Promise<boolean> {\n if (!domain?.id || !user?.id) {\n return false\n }\n\n /* 대표 오너 캐시 — 조회 없이 판정되는 경로. */\n if (domain.owner === user.id) {\n return true\n }\n\n return await getRepository(DomainOwner, tx)\n .createQueryBuilder('DOMAIN_OWNER')\n .where('DOMAIN_OWNER.domain = :domainId', { domainId: domain.id })\n .andWhere('DOMAIN_OWNER.user = :userId', { userId: user.id })\n .getExists()\n}\n\n/**\n * 도메인의 모든 오너 사용자 id. 사용자 목록에 오너 여부를 표시할 때처럼\n * 여러 사용자를 한 번에 판정해야 하는 곳에서 쓴다 (사용자마다 조회하지 않도록).\n */\nexport async function findDomainOwnerUserIds(\n domain: DomainOwnershipTarget | null | undefined,\n tx?: EntityManager\n): Promise<Set<string>> {\n if (!domain?.id) {\n return new Set<string>()\n }\n\n const ownerships: DomainOwner[] = await getRepository(DomainOwner, tx).find({\n where: { domain: { id: domain.id } },\n relations: ['user']\n })\n\n const ownerIds = new Set<string>(\n ownerships.map((ownership: DomainOwner) => ownership.user?.id || ownership.userId).filter(Boolean)\n )\n\n if (domain.owner) {\n ownerIds.add(domain.owner)\n }\n\n return ownerIds\n}\n\n/**\n * 대표 오너 캐시(`Domain.owner`)와 `DomainOwner` 테이블을 함께 갱신한다.\n * 도메인 생성·오너 이관처럼 컬럼만 세팅해 두면 두 곳이 어긋나는 경로에서 쓴다.\n *\n * 기존 오너 엔트리는 제거하지 않는다 — 오너 제거는 `removeDomainOwner` 의 책임이고\n * 거기에 \"마지막 오너 보호\" 규칙이 있다.\n */\nexport async function setPrimaryDomainOwner(\n domain: Pick<Domain, 'id'>,\n ownerUserId: string,\n grantedBy?: Pick<User, 'id'> | null,\n tx?: EntityManager\n): Promise<void> {\n if (!domain?.id || !ownerUserId) {\n throw new Error('domain and owner user are required to set the primary domain owner')\n }\n\n const ownerRepository = getRepository(DomainOwner, tx)\n\n const existing: DomainOwner = await ownerRepository.findOne({\n where: { domain: { id: domain.id }, user: { id: ownerUserId } }\n })\n\n if (!existing) {\n await ownerRepository.save({\n domain: { id: domain.id } as Domain,\n user: { id: ownerUserId } as User,\n grantedBy: grantedBy?.id ? ({ id: grantedBy.id } as User) : null\n } as DomainOwner)\n }\n\n await getRepository(Domain, tx).update(domain.id, { owner: ownerUserId })\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"encrypt-state.js","sourceRoot":"","sources":["../../server/utils/encrypt-state.ts"],"names":[],"mappings":";;AAKA,oCAMC;AAED,oCAQC;;AArBD,4DAA2B;AAE3B,+CAA+C;AAC/C,MAAM,GAAG,GAAG,gBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;AAElC,SAAgB,YAAY,CAAC,IAAY;IACvC,MAAM,EAAE,GAAG,gBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;IACjC,MAAM,MAAM,GAAG,gBAAM,CAAC,cAAc,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;IACzE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAErC,OAAO,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC9F,CAAC;AAED,SAAgB,YAAY,CAAC,IAAY;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,KAAK,CAAC,CAAA;IAChD,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAA;IAC7D,MAAM,QAAQ,GAAG,gBAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;IAC7E,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IAEhD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;AAChE,CAAC","sourcesContent":["import crypto from 'crypto'\n\n/* only for short-term life state encryption */\nconst KEY = crypto.randomBytes(32)\n\nexport function encryptState(text: string) {\n const iv = crypto.randomBytes(16)\n const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(KEY), iv)\n const encrypted = cipher.update(text)\n\n return iv.toString('hex') + ':' + Buffer.concat([encrypted, cipher.final()]).toString('hex')\n}\n\nexport function decryptState(text: string) {\n const textParts = text.split(':')\n const iv = Buffer.from(textParts.shift(), 'hex')\n const encryptedText = Buffer.from(textParts.join(':'), 'hex')\n const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(KEY), iv)\n const decrypted = decipher.update(encryptedText)\n\n return Buffer.concat([decrypted, decipher.final()]).toString()\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-aes-256-key.js","sourceRoot":"","sources":["../../server/utils/get-aes-256-key.ts"],"names":[],"mappings":";;;AAAA,6CAA4C;AAE5C,IAAI,YAAY,GAAG,YAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;AAE5C,IAAI,CAAC,YAAY,EAAE,CAAC;IAClB,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,YAAY,EAAE,CAAC;QACzC,MAAM,IAAI,SAAS,CAAC,6BAA6B,CAAC,CAAA;IACpD,CAAC;SAAM,CAAC;QACN,YAAY,GAAG,kCAAkC,CAAA;IACnD,CAAC;AACH,CAAC;AAEY,QAAA,WAAW,GAAG,YAAY,CAAA","sourcesContent":["import { config } from '@things-factory/env'\n\nvar _AES_256_KEY = config.get('AES_256_KEY')\n\nif (!_AES_256_KEY) {\n if (process.env.NODE_ENV == 'production') {\n throw new TypeError('AES_256_KEY not configured.')\n } else {\n _AES_256_KEY = 'V6g5oHJZb7KcYzIyL6cM95XvIDouon5b'\n }\n}\n\nexport const AES_256_KEY = _AES_256_KEY\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-domain-users.js","sourceRoot":"","sources":["../../server/utils/get-domain-users.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager, In, Repository, SelectQueryBuilder } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { User, UserStatus } from '../service/user/user.js'\nimport { DomainOwnershipTarget, findDomainOwnerUserIds, isDomainOwner } from './domain-ownership.js'\n\nexport async function getDomainUsers(domain: Partial<Domain>, trxMgr?: EntityManager): Promise<User[]> {\n const domainRepo: Repository<Domain> = trxMgr?.getRepository(Domain) || getRepository(Domain)\n\n /* A domain given by its other fields (subdomain, name …) is found by them first. */\n if (!domain.id) {\n const { id: _id, ...condition } = domain\n const foundDomain: Domain = Object.keys(condition).length ? await domainRepo.findOne({ where: condition as any }) : null\n if (!foundDomain) throw new Error(`Failed to find domain by passed condition, ${JSON.stringify(condition)}`)\n\n domain = foundDomain\n }\n\n const qb: SelectQueryBuilder<User> = buildDomainUsersQueryBuilder(domain.id, 'USER', trxMgr)\n return await qb.getMany()\n}\n\nexport function buildDomainUsersQueryBuilder(\n domainId: string,\n alias: string = 'USER',\n trxMgr?: EntityManager\n): SelectQueryBuilder<User> {\n const userRepo: Repository<User> = trxMgr?.getRepository(User) || getRepository(User)\n const qb: SelectQueryBuilder<User> = userRepo.createQueryBuilder(alias)\n qb.select().andWhere(\n `${alias}.id IN ${qb\n .subQuery()\n .select('USERS_DOMAINS.users_id')\n .from('users_domains', 'USERS_DOMAINS')\n .where('USERS_DOMAINS.domains_id = :domainId', { domainId })\n .getQuery()}`\n )\n\n return qb\n}\n\n/**\n * The people work in a domain may be given to: activated users who enter it — its members (`users_domains`) and its\n * owners, who enter by ownership (`checkin-domain-rule`). The one answer for every 「who may hold this here」 list —\n * a license's recipient, a sales opportunity's owner. By name.\n */\nexport async function getDomainAssignees(domain: DomainOwnershipTarget, trxMgr?: EntityManager): Promise<User[]> {\n const members = await buildDomainUsersQueryBuilder(domain.id, 'USER', trxMgr).andWhere('USER.status = :status', { status: UserStatus.ACTIVATED }).getMany()\n const ownerIds = [...(await findDomainOwnerUserIds(domain, trxMgr))].filter(id => !members.some(one => one.id === id))\n const owners = ownerIds.length ? await (trxMgr?.getRepository(User) || getRepository(User)).find({ where: { id: In(ownerIds), status: UserStatus.ACTIVATED } }) : []\n return [...members, ...owners].sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))\n}\n\n/**\n * Whether a user enters the domain as a member or an owner — the membership half of `getDomainAssignees`. The account's\n * status is the caller's to refuse first, in its own words.\n */\nexport async function entersDomainAsAssignee(domain: DomainOwnershipTarget, user: Pick<User, 'id'>, trxMgr?: EntityManager): Promise<boolean> {\n if (!domain?.id || !user?.id) return false\n const member = await buildDomainUsersQueryBuilder(domain.id, 'USER', trxMgr).andWhere('USER.id = :userId', { userId: user.id }).getExists()\n return member || (await isDomainOwner(domain, user, trxMgr))\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-secret.js","sourceRoot":"","sources":["../../server/utils/get-secret.ts"],"names":[],"mappings":";;;AAAA,6CAA4C;AAE5C,IAAI,OAAO,GAAG,YAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;AAElC,IAAI,CAAC,OAAO,EAAE,CAAC;IACb,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,YAAY,EAAE,CAAC;QACzC,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAA;IACnD,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,4CAA4C,CAAA;IACxD,CAAC;AACH,CAAC;AAEY,QAAA,MAAM,GAAG,OAAO,CAAA","sourcesContent":["import { config } from '@things-factory/env'\n\nvar _SECRET = config.get('SECRET')\n\nif (!_SECRET) {\n if (process.env.NODE_ENV == 'production') {\n throw new TypeError('SECRET key not configured.')\n } else {\n _SECRET = '0xD58F835B69D207A76CC5F84a70a1D0d4C79dAC95'\n }\n}\n\nexport const SECRET = _SECRET\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"get-user-domains.js","sourceRoot":"","sources":["../../server/utils/get-user-domains.ts"],"names":[],"mappings":";;AA6BA,wCAYC;AAQD,0DAsBC;AAED,8CAMC;AA/ED,iDAA6D;AAC7D,6CAA4C;AAG5C,uEAAoG;AACpG,qEAA0D;AAE1D,MAAM,WAAW,GAAG,YAAM,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;AAEjD,SAAS,MAAM,CAAC,IAAY,EAAE,KAAa;IACzC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;AAC1D,CAAC;AAED,SAAS,KAAK,CAAC,MAAc;IAC3B,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,MAAM,CAAA;IACnF,wCAAwC;IACxC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,CAAA;AAC7E,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,cAAc,CAAC,IAAU;IAC7C,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;QACd,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,IAAA,kDAAwB,EAAC,IAAI,CAAC,CAAA;IAElE,OAAO,UAAU;SACd,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,IAAA,wCAAe,EAAC,MAAM,EAAE,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3D,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;SACzE,IAAI,CAAC,MAAM,CAAC;SACZ,GAAG,CAAC,KAAK,CAAC,CAAA;AACf,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,uBAAuB,CAC3C,IAAU,EACV,SAAiB,EACjB,QAAgB;IAEhB,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC;QACd,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,uBAAuB,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACzE,IAAA,kDAAwB,EAAC,IAAI,CAAC;QAC9B,IAAA,wDAA8B,EAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC;KAC1D,CAAC,CAAA;IAEF,OAAO,UAAU;SACd,MAAM,CAAC,MAAM,CAAC,EAAE,CACf,IAAA,wCAAe,EAAC,MAAM,EAAE,KAAK,CAAC,CAAC,IAAI,CACjC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAClF,CACF;SACA,IAAI,CAAC,MAAM,CAAC;SACZ,GAAG,CAAC,KAAK,CAAC,CAAA;AACf,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,IAAU;IAChD,OAAO,CACL,MAAM,IAAA,qBAAa,EAAC,cAAM,CAAC,CAAC,IAAI,CAAC;QAC/B,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE;KAC1B,CAAC,CACH,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;AACd,CAAC","sourcesContent":["import { Domain, getRepository } from '@things-factory/shell'\nimport { config } from '@things-factory/env'\n\nimport { User } from '../service/user/user.js'\nimport { collectCheckinCandidates, findRoleDomainIdsWithPrivilege } from './checkin-domain-facts.js'\nimport { checkinGrantsOf } from './checkin-domain-rule.js'\n\nconst domainTypes = config.get('domainTypes', [])\n\nfunction byName(left: Domain, right: Domain): number {\n return (left.name || '').localeCompare(right.name || '')\n}\n\nfunction brief(domain: Domain): Partial<Domain> {\n const { id, name, description, subdomain, extType, brandName, brandImage } = domain\n /* do not remove id column for result */\n return { id, name, description, subdomain, extType, brandName, brandImage }\n}\n\n/**\n * 사용자가 체크인할 수 있는 도메인 목록.\n *\n * 판정 규칙과 그 근거는 `checkin-domain-rule.ts` 에 있다. 요약하면 체크인 대상은\n * \"역할을 정의한 도메인\" 이 아니라 \"역할을 부여한 도메인\" 이며, 부모 도메인으로부터 상속받은\n * 역할을 부여한 경우에도 부여가 일어난 자식 도메인이 대상이다.\n *\n * 목록이 필요한 지점(체크인 화면·부트스트랩)에서만 사용한다. 도메인 하나에 대한 가부만\n * 필요하면 `canUserCheckinDomain` 을 쓴다.\n */\nexport async function getUserDomains(user: User): Promise<Partial<Domain>[]> {\n if (!user?.id) {\n return []\n }\n\n const { facts, candidates } = await collectCheckinCandidates(user)\n\n return candidates\n .filter(domain => checkinGrantsOf(domain, facts).length > 0)\n .filter(domain => !domain.extType || domainTypes.includes(domain.extType))\n .sort(byName)\n .map(brief)\n}\n\n/**\n * 체크인 가능한 도메인 중, 체크인 근거가 된 역할이 주어진 권한을 가진 도메인 목록.\n *\n * 상속 역할로 체크인하는 자식 도메인은 부모 도메인에 정의된 역할의 권한으로 판정한다.\n * 도메인 소유자는 권한 판정 없이 포함한다 (기존 동작 유지).\n */\nexport async function getDomainsWithPrivilege(\n user: User,\n privilege: string,\n category: string\n): Promise<Partial<Domain>[]> {\n if (!user?.id) {\n return []\n }\n\n const [{ facts, candidates }, privilegedRoleDomainIds] = await Promise.all([\n collectCheckinCandidates(user),\n findRoleDomainIdsWithPrivilege(user, privilege, category)\n ])\n\n return candidates\n .filter(domain =>\n checkinGrantsOf(domain, facts).some(\n grant => grant.via === 'owner' || privilegedRoleDomainIds.has(grant.roleDomainId)\n )\n )\n .sort(byName)\n .map(brief)\n}\n\nexport async function getDomainsAsOwner(user: User): Promise<Partial<Domain>[]> {\n return (\n await getRepository(Domain).find({\n where: { owner: user.id }\n })\n ).map(brief)\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"lock-duration.js","sourceRoot":"","sources":["../../server/utils/lock-duration.ts"],"names":[],"mappings":";;;AAoBA,kDAeC;AAnCD,6CAA4C;AAE5C,iEAAqD;AAmC5C,6FAnCA,mCAAY,OAmCA;AAjCrB;;;;;;;;;;;;;GAaG;AACH,MAAM,6BAA6B,GAAG,EAAE,CAAA;AAExC,SAAgB,mBAAmB;IACjC,MAAM,UAAU,GAAG,CAAC,YAAM,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,mBAAmB,CAAA;IAErE,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACpD,OAAO,6BAA6B,CAAA;IACtC,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,CAAA;IAElC;;;;OAIG;IACH,OAAO,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAA;AAC3E,CAAC","sourcesContent":["import { config } from '@things-factory/env'\n\nimport { LOCK_FOREVER } from './account-lock-rule.js'\n\n/**\n * 잠금이 스스로 풀리기까지의 시간(분). **설정 이름이 있는 유일한 자리다.**\n *\n * 판정 규칙(`account-lock-rule.ts`)은 이 값을 인자로 받는다 — 설정을 읽지 않으므로 순수하게\n * 남고, 시험이 시계와 기간을 둘 다 넘길 수 있다. 그 값을 어디서 가져오나는 이 파일이 안다.\n *\n * 잠금은 비밀번호 실패가 만드는 것이므로 기존 `password` 설정 블록 안에 둔다.\n *\n * password:\n * lockDurationMinutes: 10 # 기본값. 0 이하면 자동해제 없음(관리자·메일로만 풀린다)\n *\n * ⚠ 이름은 아키텍트 확인 대기다 — 블록의 자리(`password`)는 확인받았고 칸 이름은 이것으로\n * 진행했다. 바뀌면 이 파일 한 줄만 고친다.\n */\nconst DEFAULT_LOCK_DURATION_MINUTES = 10\n\nexport function lockDurationMinutes(): number {\n const configured = (config.get('password') || {}).lockDurationMinutes\n\n if (configured === undefined || configured === null) {\n return DEFAULT_LOCK_DURATION_MINUTES\n }\n\n const minutes = Number(configured)\n\n /*\n * 설정에 읽을 수 없는 값이 들어오면 **기본값으로 돌아간다.** `NaN` 을 그대로 흘리면\n * 판정 규칙이 그것을 「자동해제 없음」으로 읽어(0 보다 크지 않으므로) 잠금이 영구가 된다 —\n * 오타 하나가 조용히 정책을 바꾸는 자리가 된다.\n */\n return Number.isFinite(minutes) ? minutes : DEFAULT_LOCK_DURATION_MINUTES\n}\n\nexport { LOCK_FOREVER }\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"password-reset-rule.js","sourceRoot":"","sources":["../../server/utils/password-reset-rule.ts"],"names":[],"mappings":";;AAiBA,oDAuBC;AAxCD,iDAAsE;AAGtE,6EAAqE;AAGrE;;;;;;;;;;GAUG;AACI,KAAK,UAAU,oBAAoB,CACxC,MAAY,EACZ,MAAc,EACd,EAAkB;IAElB,IAAI,MAAM,CAAC,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;QACpE,OAAO,IAAI,eAAO,CAChB,8BAA8B,EAC9B,EAAE,EACF,2EAA2E,CAC5E,CAAA;IACH,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,UAAkB,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,CAAC,CAAA;IACpG,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,eAAO,CAChB,sCAAsC,EACtC,EAAE,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,UAAkB,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAC9E,6BAA6B,SAAS,CAAC,GAAG,CAAC,CAAC,UAAkB,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,gDAAgD,CAC/I,CAAA;IACH,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,iHAAiH;AACjH,KAAK,UAAU,aAAa,CAAC,IAAU,EAAE,EAAkB;IACzD,IAAI,MAAM,IAAA,qBAAa,EAAC,cAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;QAC1E,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,MAAM,IAAA,qBAAa,EAAC,6BAAW,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AAC1F,CAAC","sourcesContent":["import { Domain, getRepository, Refusal } from '@things-factory/shell'\nimport { EntityManager } from 'typeorm'\n\nimport { DomainOwner } from '../service/domain-owner/domain-owner.js'\nimport { User } from '../service/user/user.js'\n\n/**\n * Whom a domain may put back on the default password.\n *\n * A person has one password for every domain they work in, and the default password is known to everyone who\n * was ever handed it. Resetting one is therefore the same as taking the account: the first one to sign in with\n * the default is sent to set a new password. A domain may do that only to an account that is wholly its own -\n * one that works in no other domain and owns no domain. The system domain's owner (the super user) and any\n * domain's owner are refused here, whichever domain asks; so are `admin` accounts, as `inactivateUser` refuses them.\n *\n * @returns the refusal, or `null` when `target` may be reset from `domain`. `target` must carry its `domains`.\n */\nexport async function passwordResetRefusal(\n target: User,\n domain: Domain,\n tx?: EntityManager\n): Promise<Refusal | null> {\n if (target.userType == 'admin' || (await ownsAnyDomain(target, tx))) {\n return new Refusal(\n 'password-reset-owner-account',\n {},\n \"A domain owner's or administrator's password is not reset to the default.\"\n )\n }\n\n const elsewhere = (target.domains || []).filter((userDomain: Domain) => userDomain.id !== domain.id)\n if (elsewhere.length > 0) {\n return new Refusal(\n 'password-reset-reaches-other-domains',\n { domains: elsewhere.map((userDomain: Domain) => userDomain.name).join(', ') },\n `The account also works in ${elsewhere.map((userDomain: Domain) => userDomain.name).join(', ')} - its password is not this domain's to reset.`\n )\n }\n\n return null\n}\n\n/** Owner of any domain - through the `DomainOwner` table or the primary owner column (`domain-ownership.ts`). */\nasync function ownsAnyDomain(user: User, tx?: EntityManager): Promise<boolean> {\n if (await getRepository(Domain, tx).exists({ where: { owner: user.id } })) {\n return true\n }\n\n return await getRepository(DomainOwner, tx).exists({ where: { user: { id: user.id } } })\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"profile-user.js","sourceRoot":"","sources":["../../server/utils/profile-user.ts"],"names":[],"mappings":";;AAOA,kCAaC;AApBD;;;;;;GAMG;AACH,SAAgB,WAAW,CACzB,IAAyG,EACzG,MAAqF;IAErF,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,GAAG,MAAM;KACV,CAAA;AACH,CAAC","sourcesContent":["/**\n * The signed-in person as the client keeps them (`/auth/profile`, the store's `auth.user`).\n *\n * `id` is their own id, so nothing new is exposed. The client names whose unsaved drafts are kept in this\n * browser by it (ADR-0098, `draftOwner()` in @operato/shell); without it no draft had an owner and none\n * was written (ADR-0190, since 19ed1646178 left it out).\n */\nexport function profileUser(\n user: { id: string; username: string; email?: string; locale?: string; name?: string; userType?: string },\n grants: { owner: boolean; super: boolean; unsafeIP?: boolean; privileges: unknown[] }\n) {\n return {\n id: user.id,\n username: user.username,\n email: user.email,\n locale: user.locale,\n name: user.name,\n userType: user.userType,\n ...grants\n }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"refuse-role-name.js","sourceRoot":"","sources":["../../server/utils/refuse-role-name.ts"],"names":[],"mappings":";;AA2BA,4DAoDC;AAcD,8CAUC;AArGD,iDAA8D;AAE9D,qDAA8C;AAC9C,mEAAoF;AAEpF;;;;;;;;;;;;;;;;;;;GAmBG;AACI,KAAK,UAAU,wBAAwB,CAAC,EAC7C,IAAI,EACJ,MAAM,EACN,UAAU,EACV,EAAE,EAMH;IACC,MAAM,UAAU,GAAG,IAAA,qBAAa,EAAC,cAAI,EAAE,EAAE,CAAC,CAAA;IAE1C;;;;;;;;;;;;;;OAcG;IACH,MAAM,GAAG,GAAG,MAAM,iBAAiB,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAA;IAEzE,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,iBAAiB,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAEnH,MAAM,QAAQ,GAAG,IAAA,kCAAU,EAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC,CAAA;IAEvF,IAAI,CAAC,IAAA,mCAAW,EAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC;QACvC,OAAM;IACR,CAAC;IAED,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,MAAM,IAAI,eAAO,CACf,sBAAsB,EACtB,EAAE,IAAI,EAAE,EACR,0CAA0C,IAAI,IAAI,CACnD,CAAA;IACH,CAAC;IAED,MAAM,IAAI,eAAO,CACf,4BAA4B,EAC5B,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,EAAE,EAC3C,kBAAkB,IAAI,0CAA0C,KAAK,EAAE,MAAM,EAAE,IAAI,KAAK;QACtF,2EAA2E,CAC9E,CAAA;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAgB,iBAAiB,CAC/B,UAA4B,EAC5B,IAAY,EACZ,QAAgB;IAEhB,OAAO,UAAU;SACd,kBAAkB,CAAC,MAAM,CAAC;SAC1B,iBAAiB,CAAC,aAAa,EAAE,aAAa,CAAC;SAC/C,KAAK,CAAC,mBAAmB,EAAE,EAAE,IAAI,EAAE,CAAC;SACpC,QAAQ,CAAC,4BAA4B,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAA;AACzD,CAAC","sourcesContent":["import { EntityManager, Repository, SelectQueryBuilder } from 'typeorm'\n\nimport { getRepository, Refusal } from '@things-factory/shell'\n\nimport { Role } from '../service/role/role.js'\nimport { nameStands, refusesName, type AskingDomain } from './role-name-standing.js'\n\n/**\n * The one place a role name is checked before it is written (ADR-0062 decision 3).\n *\n * ── Why it is one place ─────────────────────────────────────────────────────\n * Five sites write a role name — `createRole`, the rename in `updateRole`, both halves of\n * `updateMultipleRoles`, and `createRoleFromTemplate`. All five asked `domain: { id: domain.id }`\n * and none of them looked at what the domain inherits. Five copies of a check is five chances\n * for one of them to keep the old question.\n *\n * ── Why the refusal names the domain ────────────────────────────────────────\n * The operator's next step differs by where the name stands. If it stands here they rename; if\n * it is inherited they may decide this domain stands its own. They cannot choose without being\n * told which it is and from where — a refusal that only says \"duplicated\" leaves them guessing\n * at a domain they may not have known existed.\n *\n * ── Why a `Refusal` and not a sentence ──────────────────────────────────────\n * The rest of this file's neighbours throw English strings, which puts English on a Korean\n * screen and makes the screen write the reason itself (ADR-0054). This refusal is new, so it\n * starts as a name and values; the older sentences around it are left alone.\n */\nexport async function refuseRoleNameIfStanding({\n name,\n domain,\n separately,\n tx\n}: {\n name: string\n domain: AskingDomain\n separately?: boolean\n tx?: EntityManager\n}): Promise<void> {\n const repository = getRepository(Role, tx)\n\n /*\n * ⚠ This domain is asked first, on its own, and the parent only if it has nothing.\n *\n * One read over both with `In([own, parent])` has no `ORDER BY`, so when the name stands in\n * **both** the answer is whichever row the driver hands back. If that is the parent's, the\n * refusal is the inherited one — and `separately` is allowed to pass that, which stands a\n * second row of the name in a domain that already has it. Exactly the state the rule exists to\n * prevent, reached through the rule.\n *\n * Standing in both is not a corner: the migration of ADR-0062 decision 2 passes through it for\n * its whole length, because the domain's own role stands up first and the parent is cut last.\n *\n * This is the same defect that `role(name)` had and the same fix. It went back in here because\n * \"one read\" looked like the tidier shape.\n */\n const own = await standingRoleQuery(repository, name, domain.id).getOne()\n\n const found = own || (domain.parentId ? await standingRoleQuery(repository, name, domain.parentId).getOne() : null)\n\n const standing = nameStands({ domainId: found?.domain?.id || found?.domainId }, domain)\n\n if (!refusesName(standing, separately)) {\n return\n }\n\n if (standing === 'here') {\n throw new Refusal(\n 'role-name-taken-here',\n { name },\n `This domain already has a role called \"${name}\".`\n )\n }\n\n throw new Refusal(\n 'role-name-stands-inherited',\n { name, domain: found?.domain?.name || '' },\n `A role called \"${name}\" is already visible here, defined in \"${found?.domain?.name}\". ` +\n `Stand this domain's own of that name only if that is what you mean to do.`\n )\n}\n\n/**\n * The read behind the check: a role of this name **in one named domain**.\n *\n * ── Why this is a builder and not a `findOne` ───────────────────────────────\n * One domain at a time is the whole point, and the harness that would catch it going back to\n * both is sqlite — which returns the rows in an order that happens to be the right answer, so it\n * cannot fail the way a server does. The repository's own note says as much about column\n * dialects, and `domain-inheritance-sentinel.test.ts` says it about this exact class.\n *\n * So the query is something a test can read **before it is sent**. What we build is the same on\n * every driver, which is the only way to reach a defect our sqlite development cannot see.\n */\nexport function standingRoleQuery(\n repository: Repository<Role>,\n name: string,\n domainId: string\n): SelectQueryBuilder<Role> {\n return repository\n .createQueryBuilder('ROLE')\n .leftJoinAndSelect('ROLE.domain', 'ROLE_DOMAIN')\n .where('ROLE.name = :name', { name })\n .andWhere('ROLE_DOMAIN.id = :domainId', { domainId })\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"role-name-standing.js","sourceRoot":"","sources":["../../server/utils/role-name-standing.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;;AAsBH,gCAgBC;AASD,kCAUC;AAzCD;;;;;GAKG;AACH,SAAgB,UAAU,CAAC,KAAmB,EAAE,MAAoB;IAClE,MAAM,KAAK,GAAG,KAAK,EAAE,QAAQ,CAAA;IAE7B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,IAAI,KAAK,KAAK,MAAM,EAAE,EAAE,EAAE,CAAC;QACzB,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,MAAM,EAAE,QAAQ,IAAI,KAAK,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC;QAClD,OAAO,WAAW,CAAA;IACpB,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,QAAsB,EAAE,UAAoB;IACtE,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;QAC7B,OAAO,CAAC,UAAU,CAAA;IACpB,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["/**\n * Whether a role may be given a name that is already standing. (Pure rule — ADR-0062 decision 3.)\n *\n * ## The situation\n *\n * A domain sees its own roles and its parent's. Until this rule, the duplicate checks asked only\n * about the domain itself, so a domain with a parent could be given a role whose name was already\n * visible through inheritance. Two rows then answered to one name: every list showed both, and\n * resolving the name picked one. An approval line could end up pointing at the one nobody looks\n * at.\n *\n * ## What is refused, and what is not\n *\n * ```\n * here the domain already has this name always refused\n * inherited the parent has it and this domain not refused unless the operator says so\n * nowhere neither stands\n * ```\n *\n * ## Why the escape hatch does not ask for a different name\n *\n * ⚠ This is the part that is easy to get backwards. ADR-0062 decision 2 sets out the only path\n * for moving a domain off an inherited structure: **stand the role up here → move the people to\n * the role of the same name → cut the parent last.** The name being the same is the point of it\n * — the domain's own role is standing up to *replace* what it inherits, and once the parent is\n * cut there is one name again.\n *\n * So requiring a different name would close the one route decision 2 defines. The escape is the\n * operator saying, in as many words, that this domain stands its own — and the name stays.\n * Asking for a different name remains available on top of that, for when somebody genuinely\n * wants a second seat with the same privileges; it is a choice, never a requirement.\n */\n\n/** Where a role of this name already stands, seen from the domain that is asking. */\nexport type NameStanding = 'nowhere' | 'here' | 'inherited'\n\n/** The domain asking. `parentId` absent means it inherits from nobody. */\nexport type AskingDomain = {\n id: string\n parentId?: string | null\n}\n\n/** A role that was found carrying the name, or nothing. */\nexport type StandingRole = {\n domainId?: string | null\n} | null | undefined\n\n/**\n * Where the name stands.\n *\n * A role found in neither this domain nor its parent is `nowhere` — the caller is expected to\n * have looked in those two places, and a row from somewhere else is not this domain's business.\n */\nexport function nameStands(found: StandingRole, domain: AskingDomain): NameStanding {\n const where = found?.domainId\n\n if (!where) {\n return 'nowhere'\n }\n\n if (where === domain?.id) {\n return 'here'\n }\n\n if (domain?.parentId && where === domain.parentId) {\n return 'inherited'\n }\n\n return 'nowhere'\n}\n\n/**\n * Does the name refuse?\n *\n * `separately` is the operator saying this domain stands its own of that name. It reaches only\n * the inherited case — a name already taken **in this domain** is not a thing anybody can opt\n * into, because the two rows would be indistinguishable in every respect.\n */\nexport function refusesName(standing: NameStanding, separately?: boolean): boolean {\n if (standing === 'here') {\n return true\n }\n\n if (standing === 'inherited') {\n return !separately\n }\n\n return false\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"same-origin-redirect.js","sourceRoot":"","sources":["../../server/utils/same-origin-redirect.ts"],"names":[],"mappings":";;AAkBA,wDAqBC;AAvCD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,YAAY,GAAG,4BAA4B,CAAA;AAEjD,SAAgB,sBAAsB,CAAC,UAAmB,EAAE,OAAyC;IACnG,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;QACzD,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,IAAI,GAAQ,CAAA;IACZ,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,EAAE,GAAG,YAAY,GAAG,CAAC,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACtF,MAAM,UAAU,GACd,GAAG,CAAC,MAAM,KAAK,YAAY,IAAI,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;IAEzH,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;AACvE,CAAC","sourcesContent":["/**\n * Where a sign-in or check-in may send the browser afterwards.\n *\n * `redirect_to` arrives in the query string, so anyone can write a link that passes through our\n * login page and lands on a host of their choosing — the address bar shows our host until the\n * last hop. Before this, `/auth/checkin?redirect_to=https://elsewhere` did exactly that: in\n * fixed-domain mode `getRedirectSubdomainPath` hands the value back untouched, and otherwise\n * `new URL(value, base)` keeps the value's own host.\n *\n * The answer here is always a path on this site. A value that resolves to another origin becomes\n * `'/'` — the same place a missing value goes. An absolute URL on our own host is kept as its\n * path, since the 401 redirect and older links may carry one.\n *\n * Leading slashes are collapsed to one: `/.//elsewhere` resolves to the path `//elsewhere`, which\n * a browser reads as a host.\n */\nconst PROBE_ORIGIN = 'http://same-origin.invalid'\n\nexport function sameOriginRedirectPath(redirectTo: unknown, context: { host?: string; headers?: any }): string {\n if (typeof redirectTo !== 'string' || !redirectTo.trim()) {\n return '/'\n }\n\n let url: URL\n try {\n url = new URL(redirectTo, `${PROBE_ORIGIN}/`)\n } catch {\n return '/'\n }\n\n const ownHosts = [context.host, context.headers?.['x-forwarded-host']].filter(Boolean)\n const sameOrigin =\n url.origin === PROBE_ORIGIN || ((url.protocol === 'http:' || url.protocol === 'https:') && ownHosts.includes(url.host))\n\n if (!sameOrigin) {\n return '/'\n }\n\n return `${url.pathname.replace(/^\\/+/, '/')}${url.search}${url.hash}`\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"verification-token-rule.js","sourceRoot":"","sources":["../../server/utils/verification-token-rule.ts"],"names":[],"mappings":";;;AA6CA,sDAEC;AAQD,gEAQC;AAMD,0DAYC;AAjFD,mCAAmC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAEd,QAAA,8BAA8B,GAAqC,MAAM,CAAC,MAAM,CAAC;IAC5F,gBAAgB,EAAE,CAAC,GAAG,IAAI;IAC1B,qGAAqG;IACrG,gBAAgB,EAAE,EAAE,GAAG,IAAI;IAC3B,MAAM,EAAE,CAAC,GAAG,IAAI;IAChB,UAAU,EAAE,EAAE,GAAG,IAAI;CACtB,CAAC,CAAA;AAEF,6CAA6C;AAC7C,SAAgB,qBAAqB,CAAC,KAAa;IACjD,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACjE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,0BAA0B,CAAC,IAAY,EAAE,MAAY,IAAI,IAAI,EAAE;IAC7E,MAAM,QAAQ,GAAG,sCAA8B,CAAC,IAAI,CAAC,CAAA;IAErD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,4BAA4B,IAAI,mBAAmB,CAAC,CAAA;IACtE,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAA;AAC3C,CAAC;AAKD,0GAA0G;AAC1G,SAAgB,uBAAuB,CAAC,MAA+B,EAAE,MAAY,IAAI,IAAI,EAAE;IAC7F,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;QACvB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAE3F,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAA;AACrC,CAAC","sourcesContent":["import { createHash } from 'crypto'\n\n/**\n * How long a mailed link lives, and what the table keeps of it. (Pure rule.)\n *\n * Password reset, unlock and sign-up confirmation each mail a link that carries a token, and the\n * token is the whole credential: whoever holds the link resets the password or unlocks the account.\n * Before 2026-09-24 those tokens never expired, were stored as written and were found by that same\n * value — so a reset mail from last year still changed a password today, and a copy of the table\n * (a backup, a replica, a dump handed to support) was a set of live reset links.\n *\n * ── The lifetimes ──────────────────────────────────────────────────────────\n *\n * password-reset 1 hour hands the account over at once, so short\n * unlock 1 hour the same\n * activation 48 hours the person may read the mail late; it only confirms an address\n *\n * Decided with the architect, 2026-09-24 (`docs/design/auth-rebuild-questions-2026-09-24.md` Q4).\n *\n * ── Expiry is its own column ───────────────────────────────────────────────\n * Not derived from `createdAt` or `updatedAt`: `updatedAt` moves every time the row is saved again,\n * and a rule that reads it would extend a link by touching the row. `expiresAt` is written once,\n * with the token.\n *\n * A row without `expiresAt` reads as **expired**. Such rows\n * were written before this existed, with the token in plain text; refusing them is also how links\n * already sent stop working when this ships.\n *\n * ── What is stored is a hash ───────────────────────────────────────────────\n * sha256 of the token. The token is 16 random bytes, so there is nothing for a slow hash to protect\n * — it guards against guessing, and there is no guessing a random 128-bit value. The point is only\n * that the table no longer holds anything a link can be made from.\n */\n\nconst HOUR = 60 * 60 * 1000\n\nexport const VERIFICATION_TOKEN_LIFETIME_MS: Readonly<Record<string, number>> = Object.freeze({\n 'password-reset': 1 * HOUR,\n /* Handed over by mail or by hand, so it lives as long as an invitation did (ADR-0161 decision 3). */\n 'password-setup': 48 * HOUR,\n unlock: 1 * HOUR,\n activation: 48 * HOUR\n})\n\n/** The value the table keeps for a token. */\nexport function hashVerificationToken(token: string): string {\n return createHash('sha256').update(token, 'utf8').digest('hex')\n}\n\n/**\n * When a token of this type, issued at `now`, stops working.\n *\n * Throws for a type with no lifetime here — a new kind of mailed link has to say how long it lives\n * rather than inherit forever.\n */\nexport function verificationTokenExpiresAt(type: string, now: Date = new Date()): Date {\n const lifetime = VERIFICATION_TOKEN_LIFETIME_MS[type]\n\n if (!lifetime) {\n throw new Error(`verification token type '${type}' has no lifetime`)\n }\n\n return new Date(now.getTime() + lifetime)\n}\n\n/** The parts of a stored token this rule reads. */\nexport type VerificationTokenRecord = { expiresAt?: Date | string | null } | null | undefined\n\n/** May this token still be used, as of `now`? Absence of an expiry refuses; the expiry itself refuses. */\nexport function isVerificationTokenLive(record: VerificationTokenRecord, now: Date = new Date()): boolean {\n if (!record?.expiresAt) {\n return false\n }\n\n const at = record.expiresAt instanceof Date ? record.expiresAt : new Date(record.expiresAt)\n\n if (Number.isNaN(at.getTime())) {\n return false\n }\n\n return now.getTime() < at.getTime()\n}\n"]}