@things-factory/auth-base 10.1.223 → 10.1.227

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 (44) hide show
  1. package/dist-client/domain-required.d.ts +1 -1
  2. package/dist-client/domain-required.js +1 -1
  3. package/dist-client/domain-required.js.map +1 -1
  4. package/dist-client/tsconfig.tsbuildinfo +1 -1
  5. package/dist-server/controllers/confirm-identity.d.ts +8 -0
  6. package/dist-server/controllers/confirm-identity.js +33 -0
  7. package/dist-server/controllers/confirm-identity.js.map +1 -0
  8. package/dist-server/controllers/signin.d.ts +5 -0
  9. package/dist-server/controllers/signin.js +38 -31
  10. package/dist-server/controllers/signin.js.map +1 -1
  11. package/dist-server/router/webauthn-router.js +20 -1
  12. package/dist-server/router/webauthn-router.js.map +1 -1
  13. package/dist-server/service/audit/audit-kinds.js +2 -0
  14. package/dist-server/service/audit/audit-kinds.js.map +1 -1
  15. package/dist-server/service/identity-confirmation/identity-confirmation.d.ts +18 -0
  16. package/dist-server/service/identity-confirmation/identity-confirmation.js +84 -0
  17. package/dist-server/service/identity-confirmation/identity-confirmation.js.map +1 -0
  18. package/dist-server/service/index.d.ts +1 -0
  19. package/dist-server/service/index.js +3 -1
  20. package/dist-server/service/index.js.map +1 -1
  21. package/dist-server/service/privilege/privilege-directive.js +17 -1
  22. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  23. package/dist-server/service/role/role-mutation.js +18 -6
  24. package/dist-server/service/role/role-mutation.js.map +1 -1
  25. package/dist-server/service/role/role-query.d.ts +5 -0
  26. package/dist-server/service/role/role-query.js +18 -0
  27. package/dist-server/service/role/role-query.js.map +1 -1
  28. package/dist-server/tsconfig.tsbuildinfo +1 -1
  29. package/dist-server/utils/access-token-cookie.js +3 -0
  30. package/dist-server/utils/access-token-cookie.js.map +1 -1
  31. package/dist-server/utils/build-auth-url.d.ts +1 -1
  32. package/dist-server/utils/build-auth-url.js +2 -2
  33. package/dist-server/utils/build-auth-url.js.map +1 -1
  34. package/package.json +5 -5
  35. package/tests/confirm-identity-db.test.ts +85 -0
  36. package/tests/identity-confirmation.test.ts +146 -0
  37. package/tests/role-mutation-db.test.ts +20 -4
  38. package/tests/role-privileges-db.test.ts +26 -0
  39. package/translations/en.json +3 -1
  40. package/translations/es.json +3 -1
  41. package/translations/ja.json +3 -1
  42. package/translations/ko.json +3 -1
  43. package/translations/ms.json +3 -1
  44. package/translations/zh.json +3 -1
@@ -1 +1 @@
1
- {"version":3,"file":"signin.js","sourceRoot":"","sources":["../../server/controllers/signin.ts"],"names":[],"mappings":"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","sourcesContent":["import { ILike } from 'typeorm'\nimport { getRepository } from '@things-factory/shell'\n\nimport { sendUnlockUserEmail } from '../controllers/unlock-user.js'\nimport { AuthError } from '../errors/auth-error.js'\nimport { User, UserStatus } from '../service/user/user.js'\nimport { isAccountLocked } from '../utils/account-lock-rule.js'\nimport { lockDurationMinutes } from '../utils/lock-duration.js'\nimport { recordAuditAction } from '../service/audit/audit-recorder.js'\nimport { errorCodeOf } from '../service/audit/audit-target.js'\n\nexport type SigninMethod = 'password' | 'passkey'\n\nexport type SigninAttempt = { userId?: string }\n\n/**\n * A sign-in, and its audit (ADR-0141 §3.3): a success or a failure with its code. A failure is\n * written on its own and stays when the request fails (§4-3). An account that does not exist leaves\n * no actor - the name typed in is not a person.\n */\nexport async function signin(attrs: { username: string; password: string }, context?) {\n return await auditedSignin('password', context, attempt => signinOnce(attrs, context, attempt))\n}\n\n/**\n * The audit around one way of signing in. `run` puts the person in `attempt` as soon as it knows\n * who it is, so a failure after that names them. `detail.method` tells a password from a passkey\n * (ADR-0156 decision 4).\n */\nexport async function auditedSignin<T extends { user: User }>(\n method: SigninMethod,\n context,\n run: (attempt: SigninAttempt) => Promise<T>\n): Promise<T> {\n const attempt: SigninAttempt = {}\n const domainId = context?.state?.domain?.id ?? null\n const detail = { method }\n\n /*\n * Not awaited: the answer does not wait on its record, and a write that fails is counted, never\n * thrown (`recordAuditAction` without a transaction).\n */\n try {\n const answer = await run(attempt)\n void recordAuditAction({ kind: 'auth.signin', outcome: 'ok', detail }, { actorId: answer.user.id, domainId })\n return answer\n } catch (error) {\n void recordAuditAction({ kind: 'auth.signin', outcome: 'error', code: errorCodeOf(error), detail }, { actorId: attempt.userId ?? null, domainId })\n throw error\n }\n}\n\nasync function signinOnce(attrs: { username: string; password: string }, context, attempt: SigninAttempt) {\n const { username } = attrs\n\n const repository = getRepository(User)\n\n var user = await repository.findOne({\n where: { username },\n relations: ['domains']\n })\n\n if (!user && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(username)) {\n user = await repository.findOne({\n where: {\n email: ILike(username)\n },\n relations: ['domains']\n })\n }\n\n if (!user)\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_NOT_FOUND\n })\n\n attempt.userId = user.id\n\n return await admitSignin(user, context, async () => {\n if (!User.verify(user.password, attrs.password, user.salt)) {\n user.failCount++\n if (user.failCount >= 5) {\n user.status = UserStatus.LOCKED\n /* 언제 풀리나를 계산할 수 있게 시각을 남긴다. 이 칸이 비면 판정 규칙이\n 잠금을 유지하는 쪽으로 기운다. */\n user.lockedAt = new Date()\n }\n await repository.save(user)\n if (user.status == UserStatus.LOCKED) {\n sendUnlockUserEmail({\n user,\n context\n })\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_LOCKED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.PASSWORD_NOT_MATCHED,\n detail: {\n username: user.username,\n email: user.email,\n failCount: user.failCount\n }\n })\n } else {\n user.failCount = 0\n await repository.save(user)\n }\n })\n}\n\n/**\n * The account gate every way of signing in goes through (ADR-0156 decision 4): deleted, locked -\n * and a lock that has run out lifts here - then the secret, then inactive. Only `verifySecret`\n * differs between a password and a passkey; a passkey arrives already verified and passes nothing.\n *\n * `user` must carry `domains`.\n */\nexport async function admitSignin(user: User, context, verifySecret: () => Promise<void>) {\n const { domain } = context?.state || {}\n const repository = getRepository(User)\n\n if (user.status == UserStatus.DELETED) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_DELETED\n })\n }\n\n if (user.status == UserStatus.LOCKED) {\n /* 시계는 한 번만 읽는다 — 두 번 읽으면 경계에 걸린 잠금이 두 판정 사이에서 갈린다. */\n const now = new Date()\n\n if (isAccountLocked(user, now, lockDurationMinutes())) {\n sendUnlockUserEmail({\n user,\n context\n })\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_LOCKED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n\n /*\n * 시간이 지나 스스로 풀렸다 — 그 자리에서 행을 되돌리고 계속 진행한다. 주기 작업으로\n * 풀지 않는 이유는 `account-lock-rule.ts` 에 있다(계정이 10만이면 그것이 따로 일이 된다).\n *\n * `failCount` 도 함께 0 으로 둔다. 그래서 **잠금이 곧 실패 셈의 창**이 된다 — 이 계정이\n * 다시 잠기려면 지금부터 다섯 번을 틀려야 한다.\n */\n user.status = UserStatus.ACTIVATED\n user.failCount = 0\n user.lockedAt = null\n\n await repository.save(user)\n }\n\n /*\n * Waiting for the password its person sets through a setup link (ADR-0161). Refused before the secret is looked at:\n * the password it carries is one nobody holds, so a guess against it is not a failure to count - and a passkey does\n * not open it either.\n */\n if (user.status == UserStatus.PWD_RESET_REQUIRED) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.PASSWORD_SETUP_PENDING\n })\n }\n\n await verifySecret()\n\n if (user.status == UserStatus.INACTIVE) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_NOT_ACTIVATED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n\n return {\n user,\n token: await user.sign({ subdomain: domain?.subdomain }),\n domains: user.domains || []\n }\n}\n"]}
1
+ {"version":3,"file":"signin.js","sourceRoot":"","sources":["../../server/controllers/signin.ts"],"names":[],"mappings":"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","sourcesContent":["import { ILike } from 'typeorm'\nimport { getRepository } from '@things-factory/shell'\n\nimport { sendUnlockUserEmail } from '../controllers/unlock-user.js'\nimport { AuthError } from '../errors/auth-error.js'\nimport { User, UserStatus } from '../service/user/user.js'\nimport { isAccountLocked } from '../utils/account-lock-rule.js'\nimport { lockDurationMinutes } from '../utils/lock-duration.js'\nimport { recordAuditAction } from '../service/audit/audit-recorder.js'\nimport { errorCodeOf } from '../service/audit/audit-target.js'\n\nexport type SigninMethod = 'password' | 'passkey'\n\nexport type SigninAttempt = { userId?: string }\n\n/**\n * A sign-in, and its audit (ADR-0141 §3.3): a success or a failure with its code. A failure is\n * written on its own and stays when the request fails (§4-3). An account that does not exist leaves\n * no actor - the name typed in is not a person.\n */\nexport async function signin(attrs: { username: string; password: string }, context?) {\n return await auditedSignin('password', context, attempt => signinOnce(attrs, context, attempt))\n}\n\n/**\n * The audit around one way of signing in. `run` puts the person in `attempt` as soon as it knows\n * who it is, so a failure after that names them. `detail.method` tells a password from a passkey\n * (ADR-0156 decision 4).\n */\nexport async function auditedSignin<T extends { user: User }>(\n method: SigninMethod,\n context,\n run: (attempt: SigninAttempt) => Promise<T>\n): Promise<T> {\n const attempt: SigninAttempt = {}\n const domainId = context?.state?.domain?.id ?? null\n const detail = { method }\n\n /*\n * Not awaited: the answer does not wait on its record, and a write that fails is counted, never\n * thrown (`recordAuditAction` without a transaction).\n */\n try {\n const answer = await run(attempt)\n void recordAuditAction({ kind: 'auth.signin', outcome: 'ok', detail }, { actorId: answer.user.id, domainId })\n return answer\n } catch (error) {\n void recordAuditAction({ kind: 'auth.signin', outcome: 'error', code: errorCodeOf(error), detail }, { actorId: attempt.userId ?? null, domainId })\n throw error\n }\n}\n\nasync function signinOnce(attrs: { username: string; password: string }, context, attempt: SigninAttempt) {\n const { username } = attrs\n\n const repository = getRepository(User)\n\n var user = await repository.findOne({\n where: { username },\n relations: ['domains']\n })\n\n if (!user && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(username)) {\n user = await repository.findOne({\n where: {\n email: ILike(username)\n },\n relations: ['domains']\n })\n }\n\n if (!user)\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_NOT_FOUND\n })\n\n attempt.userId = user.id\n\n return await admitSignin(user, context, () => verifyPassword(user, attrs.password, context))\n}\n\n/**\n * The password against `user`, counting a miss: the fifth locks the account and mails how to unlock it. Signing in\n * and confirming it is still the person (ADR-0168) count misses together.\n */\nexport async function verifyPassword(user: User, password: string, context) {\n const repository = getRepository(User)\n\n if (!User.verify(user.password, password, user.salt)) {\n user.failCount++\n if (user.failCount >= 5) {\n user.status = UserStatus.LOCKED\n /* 언제 풀리나를 계산할 수 있게 시각을 남긴다. 이 칸이 비면 판정 규칙이\n 잠금을 유지하는 쪽으로 기운다. */\n user.lockedAt = new Date()\n }\n await repository.save(user)\n if (user.status == UserStatus.LOCKED) {\n sendUnlockUserEmail({\n user,\n context\n })\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_LOCKED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.PASSWORD_NOT_MATCHED,\n detail: {\n username: user.username,\n email: user.email,\n failCount: user.failCount\n }\n })\n } else {\n user.failCount = 0\n await repository.save(user)\n }\n}\n\n/**\n * The account gate every way of signing in goes through (ADR-0156 decision 4): deleted, locked -\n * and a lock that has run out lifts here - then the secret, then inactive. Only `verifySecret`\n * differs between a password and a passkey; a passkey arrives already verified and passes nothing.\n *\n * `user` must carry `domains`.\n */\nexport async function admitSignin(user: User, context, verifySecret: () => Promise<void>) {\n const { domain } = context?.state || {}\n const repository = getRepository(User)\n\n if (user.status == UserStatus.DELETED) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_DELETED\n })\n }\n\n if (user.status == UserStatus.LOCKED) {\n /* 시계는 한 번만 읽는다 — 두 번 읽으면 경계에 걸린 잠금이 두 판정 사이에서 갈린다. */\n const now = new Date()\n\n if (isAccountLocked(user, now, lockDurationMinutes())) {\n sendUnlockUserEmail({\n user,\n context\n })\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_LOCKED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n\n /*\n * 시간이 지나 스스로 풀렸다 — 그 자리에서 행을 되돌리고 계속 진행한다. 주기 작업으로\n * 풀지 않는 이유는 `account-lock-rule.ts` 에 있다(계정이 10만이면 그것이 따로 일이 된다).\n *\n * `failCount` 도 함께 0 으로 둔다. 그래서 **잠금이 곧 실패 셈의 창**이 된다 — 이 계정이\n * 다시 잠기려면 지금부터 다섯 번을 틀려야 한다.\n */\n user.status = UserStatus.ACTIVATED\n user.failCount = 0\n user.lockedAt = null\n\n await repository.save(user)\n }\n\n /*\n * Waiting for the password its person sets through a setup link (ADR-0161). Refused before the secret is looked at:\n * the password it carries is one nobody holds, so a guess against it is not a failure to count - and a passkey does\n * not open it either.\n */\n if (user.status == UserStatus.PWD_RESET_REQUIRED) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.PASSWORD_SETUP_PENDING\n })\n }\n\n await verifySecret()\n\n if (user.status == UserStatus.INACTIVE) {\n throw new AuthError({\n errorCode: AuthError.ERROR_CODES.USER_NOT_ACTIVATED,\n detail: {\n username: user.username,\n email: user.email\n }\n })\n }\n\n return {\n user,\n token: await user.sign({ subdomain: domain?.subdomain }),\n domains: user.domains || []\n }\n}\n"]}
@@ -5,6 +5,9 @@ const tslib_1 = require("tslib");
5
5
  const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
6
6
  const env_1 = require("@things-factory/env");
7
7
  const passkey_js_1 = require("../controllers/passkey.js");
8
+ const confirm_identity_js_1 = require("../controllers/confirm-identity.js");
9
+ const signin_js_1 = require("../controllers/signin.js");
10
+ const identity_confirmation_js_1 = require("../service/identity-confirmation/identity-confirmation.js");
8
11
  const access_token_cookie_js_1 = require("../utils/access-token-cookie.js");
9
12
  const same_origin_redirect_js_1 = require("../utils/same-origin-redirect.js");
10
13
  exports.webAuthnGlobalPublicRouter = new koa_router_1.default();
@@ -27,11 +30,27 @@ exports.webAuthnGlobalPrivateRouter.get('/auth/verify-webauthn/challenge', async
27
30
  (0, passkey_js_1.rememberChallenge)(context, options.challenge);
28
31
  context.body = options;
29
32
  });
33
+ /* A passkey that answers is also the recent confirmation a door may ask for (ADR-0168). */
30
34
  exports.webAuthnGlobalPrivateRouter.post('/auth/verify-webauthn', async (context) => {
31
35
  const challenge = (0, passkey_js_1.takeChallenge)(context);
32
- await (0, passkey_js_1.reverifyWithPasskey)(context.state.user, context.request.body, challenge, (0, passkey_js_1.siteOf)(context));
36
+ await (0, confirm_identity_js_1.confirmIdentity)('passkey', context, user => (0, passkey_js_1.reverifyWithPasskey)(user, context.request.body, challenge, (0, passkey_js_1.siteOf)(context)));
33
37
  context.body = { verified: true, message: 'Biometric authentication successful' };
34
38
  });
39
+ /* The same confirmation by password - the body is `{ password }` (ADR-0168). */
40
+ exports.webAuthnGlobalPrivateRouter.post('/auth/confirm-identity', async (context) => {
41
+ const { password } = context.request.body || {};
42
+ await (0, confirm_identity_js_1.confirmIdentity)('password', context, user => (0, signin_js_1.verifyPassword)(user, String(password ?? ''), context));
43
+ context.body = { confirmed: true };
44
+ });
45
+ /* Until when the confirmation holds - a screen puts away what it shows at that moment. Null when there is none. */
46
+ exports.webAuthnGlobalPrivateRouter.get('/auth/confirm-identity', async (context) => {
47
+ context.body = { confirmedUntil: (0, identity_confirmation_js_1.confirmedUntil)(context, context.state.user?.id)?.toISOString() ?? null };
48
+ });
49
+ /* Letting the confirmation go before it ends by itself. */
50
+ exports.webAuthnGlobalPrivateRouter.delete('/auth/confirm-identity', async (context) => {
51
+ (0, identity_confirmation_js_1.forgetConfirmation)(context);
52
+ context.body = { confirmed: false };
53
+ });
35
54
  // Adding a passkey for the signed-in person
36
55
  exports.webAuthnGlobalPrivateRouter.get('/auth/register-webauthn/challenge', async (context) => {
37
56
  const options = await (0, passkey_js_1.registrationOptions)(context.state.user, rpName, (0, passkey_js_1.siteOf)(context));
@@ -1 +1 @@
1
- {"version":3,"file":"webauthn-router.js","sourceRoot":"","sources":["../../server/router/webauthn-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAC/B,6CAAwD;AAExD,0DAUkC;AAClC,4EAAsE;AACtE,8EAAyE;AAE5D,QAAA,0BAA0B,GAAG,IAAI,oBAAM,EAAE,CAAA;AACzC,QAAA,2BAA2B,GAAG,IAAI,oBAAM,EAAE,CAAA;AAEvD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAiB,CAAA;AAE1C,MAAM,0BAA0B,GAAG,YAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAA;AACvF,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;AAErD;;;GAGG;AAEH,6EAA6E;AAC7E,mCAA2B,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACjF,MAAM,OAAO,GAAG,MAAM,IAAA,4BAAe,EAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAE1E,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAO,CAAC,IAAI,GAAG,EAAE,KAAK,EAAE,mDAAmD,EAAE,CAAA;QAC7E,OAAM;IACR,CAAC;IAED,IAAA,8BAAiB,EAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,GAAG,OAAO,CAAA;AACxB,CAAC,CAAC,CAAA;AAEF,mCAA2B,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACxE,MAAM,SAAS,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAA;IACxC,MAAM,IAAA,gCAAmB,EAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAE/F,OAAO,CAAC,IAAI,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,qCAAqC,EAAE,CAAA;AACnF,CAAC,CAAC,CAAA;AAEF,4CAA4C;AAC5C,mCAA2B,CAAC,GAAG,CAAC,mCAAmC,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACnF,MAAM,OAAO,GAAG,MAAM,IAAA,gCAAmB,EAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAEtF,IAAA,8BAAiB,EAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,GAAG,OAAO,CAAA;AACxB,CAAC,CAAC,CAAA;AAEF,iGAAiG;AACjG,mCAA2B,CAAC,IAAI,CAAC,2BAA2B,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IAC5E,MAAM,SAAS,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAA;IACxC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAA;IAErD,MAAM,IAAA,4BAAe,EAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAErF,OAAO,CAAC,IAAI,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;AACnC,CAAC,CAAC,CAAA;AAEF;;;GAGG;AACH,mCAA2B,CAAC,GAAG,CAAC,qBAAqB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACrE,MAAM,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE;QAChC,WAAW,EAAE,oBAAoB;QACjC,aAAa,EAAE,wBAAwB;QACvC,IAAI,EAAE;YACJ,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ;YACrC,UAAU,EAAE,IAAA,gDAAsB,EAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,OAAO,CAAC;YACtE,0BAA0B;YAC1B,SAAS;SACV;KACF,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,aAAa;AACb,kCAA0B,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IAChF,MAAM,OAAO,GAAG,MAAM,IAAA,0BAAa,EAAC,IAAA,mBAAM,EAAC,OAAO,CAAC,CAAC,CAAA;IAEpD,IAAA,8BAAiB,EAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,GAAG,OAAO,CAAA;AACxB,CAAC,CAAC,CAAA;AAEF,6CAA6C;AAC7C,kCAA0B,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACvE,MAAM,SAAS,GAAG,IAAA,0BAAa,EAAC,OAAO,CAAC,CAAA;IACxC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAA;IAC3D,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEhC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAA,8BAAiB,EAAC,QAAQ,EAAE,SAAS,EAAE,IAAA,mBAAM,EAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAA;IACxF,IAAA,6CAAoB,EAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAEpC,uGAAuG;IACvG,MAAM,WAAW,GAAG,gBAAgB,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,gBAAgB,kBAAkB,CAAC,UAAU,IAAI,GAAG,CAAC,EAAE,CAAA;IAE/H,OAAO,CAAC,IAAI,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;AAChD,CAAC,CAAC,CAAA","sourcesContent":["import Router from 'koa-router'\nimport { appPackage, config } from '@things-factory/env'\n\nimport {\n registerPasskey,\n registrationOptions,\n rememberChallenge,\n reverifyOptions,\n reverifyWithPasskey,\n signinOptions,\n signinWithPasskey,\n siteOf,\n takeChallenge\n} from '../controllers/passkey.js'\nimport { setAccessTokenCookie } from '../utils/access-token-cookie.js'\nimport { sameOriginRedirectPath } from '../utils/same-origin-redirect.js'\n\nexport const webAuthnGlobalPublicRouter = new Router()\nexport const webAuthnGlobalPrivateRouter = new Router()\n\nconst { name: rpName } = appPackage as any\n\nconst disableUserFavoredLanguage = config.get('i18n/disableUserFavoredLanguage', false)\nconst languages = config.get('i18n/languages', false)\n\n/*\n * A refusal throws `AuthError`; `authenticate401Middleware` answers it with 401 and the refusal in\n * the request's language as the body. The screens read that body as the message.\n */\n\n// Confirming it is the signed-in person - only their own passkeys can answer\nwebAuthnGlobalPrivateRouter.get('/auth/verify-webauthn/challenge', async context => {\n const options = await reverifyOptions(context.state.user, siteOf(context))\n\n if (!options) {\n context.status = 400\n context.body = { error: 'No biometric credentials registered for this user' }\n return\n }\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\nwebAuthnGlobalPrivateRouter.post('/auth/verify-webauthn', async context => {\n const challenge = takeChallenge(context)\n await reverifyWithPasskey(context.state.user, context.request.body, challenge, siteOf(context))\n\n context.body = { verified: true, message: 'Biometric authentication successful' }\n})\n\n// Adding a passkey for the signed-in person\nwebAuthnGlobalPrivateRouter.get('/auth/register-webauthn/challenge', async context => {\n const options = await registrationOptions(context.state.user, rpName, siteOf(context))\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* The body is `{ response, name }` - the browser's answer, and the label the screen proposes. */\nwebAuthnGlobalPrivateRouter.post('/auth/verify-registration', async context => {\n const challenge = takeChallenge(context)\n const { response, name } = context.request.body || {}\n\n await registerPasskey(context.state.user, response, name, challenge, siteOf(context))\n\n context.body = { verified: true }\n})\n\n/*\n * After a password sign-in on the screen, before check-in: an offer to add a passkey on this device\n * (ADR-0156 decision 5). The page moves on by itself where it has nothing to offer.\n */\nwebAuthnGlobalPrivateRouter.get('/auth/passkey-offer', async context => {\n await context.render('auth-page', {\n pageElement: 'auth-passkey-offer',\n elementScript: '/auth/passkey-offer.js',\n data: {\n username: context.state.user.username,\n redirectTo: sameOriginRedirectPath(context.query.redirect_to, context),\n disableUserFavoredLanguage,\n languages\n }\n })\n})\n\n// Signing in\nwebAuthnGlobalPublicRouter.get('/auth/signin-webauthn/challenge', async context => {\n const options = await signinOptions(siteOf(context))\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* The body is `{ response, redirectTo }`. */\nwebAuthnGlobalPublicRouter.post('/auth/signin-webauthn', async context => {\n const challenge = takeChallenge(context)\n const { response, redirectTo } = context.request.body || {}\n const { domain } = context.state\n\n const { token } = await signinWithPasskey(response, challenge, siteOf(context), context)\n setAccessTokenCookie(context, token)\n\n /* A two-step exchange run by fetch in the browser cannot follow a 3xx, so the next address is data. */\n const redirectURL = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(redirectTo || '/')}`\n\n context.body = { redirectURL, verified: true }\n})\n"]}
1
+ {"version":3,"file":"webauthn-router.js","sourceRoot":"","sources":["../../server/router/webauthn-router.ts"],"names":[],"mappings":"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","sourcesContent":["import Router from 'koa-router'\nimport { appPackage, config } from '@things-factory/env'\n\nimport {\n registerPasskey,\n registrationOptions,\n rememberChallenge,\n reverifyOptions,\n reverifyWithPasskey,\n signinOptions,\n signinWithPasskey,\n siteOf,\n takeChallenge\n} from '../controllers/passkey.js'\nimport { confirmIdentity } from '../controllers/confirm-identity.js'\nimport { verifyPassword } from '../controllers/signin.js'\nimport { confirmedUntil, forgetConfirmation } from '../service/identity-confirmation/identity-confirmation.js'\nimport { setAccessTokenCookie } from '../utils/access-token-cookie.js'\nimport { sameOriginRedirectPath } from '../utils/same-origin-redirect.js'\n\nexport const webAuthnGlobalPublicRouter = new Router()\nexport const webAuthnGlobalPrivateRouter = new Router()\n\nconst { name: rpName } = appPackage as any\n\nconst disableUserFavoredLanguage = config.get('i18n/disableUserFavoredLanguage', false)\nconst languages = config.get('i18n/languages', false)\n\n/*\n * A refusal throws `AuthError`; `authenticate401Middleware` answers it with 401 and the refusal in\n * the request's language as the body. The screens read that body as the message.\n */\n\n// Confirming it is the signed-in person - only their own passkeys can answer\nwebAuthnGlobalPrivateRouter.get('/auth/verify-webauthn/challenge', async context => {\n const options = await reverifyOptions(context.state.user, siteOf(context))\n\n if (!options) {\n context.status = 400\n context.body = { error: 'No biometric credentials registered for this user' }\n return\n }\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* A passkey that answers is also the recent confirmation a door may ask for (ADR-0168). */\nwebAuthnGlobalPrivateRouter.post('/auth/verify-webauthn', async context => {\n const challenge = takeChallenge(context)\n await confirmIdentity('passkey', context, user => reverifyWithPasskey(user, context.request.body, challenge, siteOf(context)))\n\n context.body = { verified: true, message: 'Biometric authentication successful' }\n})\n\n/* The same confirmation by password - the body is `{ password }` (ADR-0168). */\nwebAuthnGlobalPrivateRouter.post('/auth/confirm-identity', async context => {\n const { password } = context.request.body || {}\n await confirmIdentity('password', context, user => verifyPassword(user, String(password ?? ''), context))\n\n context.body = { confirmed: true }\n})\n\n/* Until when the confirmation holds - a screen puts away what it shows at that moment. Null when there is none. */\nwebAuthnGlobalPrivateRouter.get('/auth/confirm-identity', async context => {\n context.body = { confirmedUntil: confirmedUntil(context, context.state.user?.id)?.toISOString() ?? null }\n})\n\n/* Letting the confirmation go before it ends by itself. */\nwebAuthnGlobalPrivateRouter.delete('/auth/confirm-identity', async context => {\n forgetConfirmation(context)\n context.body = { confirmed: false }\n})\n\n// Adding a passkey for the signed-in person\nwebAuthnGlobalPrivateRouter.get('/auth/register-webauthn/challenge', async context => {\n const options = await registrationOptions(context.state.user, rpName, siteOf(context))\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* The body is `{ response, name }` - the browser's answer, and the label the screen proposes. */\nwebAuthnGlobalPrivateRouter.post('/auth/verify-registration', async context => {\n const challenge = takeChallenge(context)\n const { response, name } = context.request.body || {}\n\n await registerPasskey(context.state.user, response, name, challenge, siteOf(context))\n\n context.body = { verified: true }\n})\n\n/*\n * After a password sign-in on the screen, before check-in: an offer to add a passkey on this device\n * (ADR-0156 decision 5). The page moves on by itself where it has nothing to offer.\n */\nwebAuthnGlobalPrivateRouter.get('/auth/passkey-offer', async context => {\n await context.render('auth-page', {\n pageElement: 'auth-passkey-offer',\n elementScript: '/auth/passkey-offer.js',\n data: {\n username: context.state.user.username,\n redirectTo: sameOriginRedirectPath(context.query.redirect_to, context),\n disableUserFavoredLanguage,\n languages\n }\n })\n})\n\n// Signing in\nwebAuthnGlobalPublicRouter.get('/auth/signin-webauthn/challenge', async context => {\n const options = await signinOptions(siteOf(context))\n\n rememberChallenge(context, options.challenge)\n context.body = options\n})\n\n/* The body is `{ response, redirectTo }`. */\nwebAuthnGlobalPublicRouter.post('/auth/signin-webauthn', async context => {\n const challenge = takeChallenge(context)\n const { response, redirectTo } = context.request.body || {}\n const { domain } = context.state\n\n const { token } = await signinWithPasskey(response, challenge, siteOf(context), context)\n setAccessTokenCookie(context, token)\n\n /* A two-step exchange run by fetch in the browser cannot follow a 3xx, so the next address is data. */\n const redirectURL = `/auth/checkin${domain ? '/' + domain.subdomain : ''}?redirect_to=${encodeURIComponent(redirectTo || '/')}`\n\n context.body = { redirectURL, verified: true }\n})\n"]}
@@ -53,6 +53,8 @@ registerAuditKind('door.result');
53
53
  registerAuditKind('auth.signin', ['method']);
54
54
  registerAuditKind('auth.checkin');
55
55
  registerAuditKind('auth.signout');
56
+ /* Confirming it is still the signed-in person before a door that asks so (ADR-0168) - password or passkey. */
57
+ registerAuditKind('auth.confirm-identity', ['method']);
56
58
  registerAuditKind('password.change');
57
59
  registerAuditKind('password.reset-by-admin');
58
60
  registerAuditKind('credential.issue');
@@ -1 +1 @@
1
- {"version":3,"file":"audit-kinds.js","sourceRoot":"","sources":["../../../server/service/audit/audit-kinds.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The kinds of audit action, and the `detail` keys each may carry (ADR-0141 §3.4).\n *\n * The audit stores only fields it allows. The fixed columns of `AuditAction` hold no business\n * value; `detail` is the one open place, so it takes only the keys its kind registered here, and\n * a kind nobody registered is refused - a module that wants a new event says so here, at import,\n * and a typo does not become a new kind quietly.\n *\n * A key holds a security state or a count, never a business value: a user's status (`activated` →\n * `locked`) is the account's security state; a salary is not, and has no key to go in.\n */\n\nexport type AuditDetailValue = string | number | boolean | null\n\nconst KINDS = new Map<string, readonly string[]>()\n\nexport function registerAuditKind(kind: string, detail: readonly string[] = []) {\n const known = KINDS.get(kind)\n if (known && known.join() !== detail.join()) {\n throw new Error(`Audit kind \"${kind}\" is registered twice with different detail keys.`)\n }\n KINDS.set(kind, detail)\n}\n\nexport function isAuditKind(kind: string): boolean {\n return KINDS.has(kind)\n}\n\n/**\n * The detail as it may be stored: unknown kinds throw, keys the kind did not register throw, and\n * values are cut to 64 characters. Throwing is right - this is a program writing the wrong thing.\n */\nexport function allowedDetail(\n kind: string,\n detail?: Record<string, AuditDetailValue>\n): Record<string, AuditDetailValue> | undefined {\n const keys = KINDS.get(kind)\n if (!keys) {\n throw new Error(`Audit kind \"${kind}\" is not registered. Register it with registerAuditKind() first.`)\n }\n if (!detail) return undefined\n\n const kept: Record<string, AuditDetailValue> = {}\n for (const [key, value] of Object.entries(detail)) {\n if (!keys.includes(key)) {\n throw new Error(`Audit kind \"${kind}\" does not allow detail \"${key}\". Allowed: ${keys.join(', ') || 'none'}.`)\n }\n kept[key] = typeof value === 'string' ? value.slice(0, 64) : value\n }\n return Object.keys(kept).length ? kept : undefined\n}\n\n/* Doors */\nregisterAuditKind('door.check')\nregisterAuditKind('door.result')\n\n/* Signing in and out (ADR-0141 §3.3). The reason of a failure is the error code column. */\n/* `method` is password or passkey (ADR-0156 decision 4). */\nregisterAuditKind('auth.signin', ['method'])\nregisterAuditKind('auth.checkin')\nregisterAuditKind('auth.signout')\nregisterAuditKind('password.change')\nregisterAuditKind('password.reset-by-admin')\nregisterAuditKind('credential.issue')\nregisterAuditKind('webauthn.register')\nregisterAuditKind('webauthn.remove')\n\n/* Who may do what */\nregisterAuditKind('role.grant')\nregisterAuditKind('role.revoke')\nregisterAuditKind('role.privilege.add')\nregisterAuditKind('role.privilege.remove')\nregisterAuditKind('role.delete')\nregisterAuditKind('user.status', ['from', 'to'])\nregisterAuditKind('domain.owner.add')\nregisterAuditKind('domain.owner.remove')\n/* Invitations are gone (ADR-0161 decision 6); their records stay and are still read. */\nregisterAuditKind('invitation.create')\nregisterAuditKind('invitation.status', ['from', 'to'])\n"]}
1
+ {"version":3,"file":"audit-kinds.js","sourceRoot":"","sources":["../../../server/service/audit/audit-kinds.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The kinds of audit action, and the `detail` keys each may carry (ADR-0141 §3.4).\n *\n * The audit stores only fields it allows. The fixed columns of `AuditAction` hold no business\n * value; `detail` is the one open place, so it takes only the keys its kind registered here, and\n * a kind nobody registered is refused - a module that wants a new event says so here, at import,\n * and a typo does not become a new kind quietly.\n *\n * A key holds a security state or a count, never a business value: a user's status (`activated` →\n * `locked`) is the account's security state; a salary is not, and has no key to go in.\n */\n\nexport type AuditDetailValue = string | number | boolean | null\n\nconst KINDS = new Map<string, readonly string[]>()\n\nexport function registerAuditKind(kind: string, detail: readonly string[] = []) {\n const known = KINDS.get(kind)\n if (known && known.join() !== detail.join()) {\n throw new Error(`Audit kind \"${kind}\" is registered twice with different detail keys.`)\n }\n KINDS.set(kind, detail)\n}\n\nexport function isAuditKind(kind: string): boolean {\n return KINDS.has(kind)\n}\n\n/**\n * The detail as it may be stored: unknown kinds throw, keys the kind did not register throw, and\n * values are cut to 64 characters. Throwing is right - this is a program writing the wrong thing.\n */\nexport function allowedDetail(\n kind: string,\n detail?: Record<string, AuditDetailValue>\n): Record<string, AuditDetailValue> | undefined {\n const keys = KINDS.get(kind)\n if (!keys) {\n throw new Error(`Audit kind \"${kind}\" is not registered. Register it with registerAuditKind() first.`)\n }\n if (!detail) return undefined\n\n const kept: Record<string, AuditDetailValue> = {}\n for (const [key, value] of Object.entries(detail)) {\n if (!keys.includes(key)) {\n throw new Error(`Audit kind \"${kind}\" does not allow detail \"${key}\". Allowed: ${keys.join(', ') || 'none'}.`)\n }\n kept[key] = typeof value === 'string' ? value.slice(0, 64) : value\n }\n return Object.keys(kept).length ? kept : undefined\n}\n\n/* Doors */\nregisterAuditKind('door.check')\nregisterAuditKind('door.result')\n\n/* Signing in and out (ADR-0141 §3.3). The reason of a failure is the error code column. */\n/* `method` is password or passkey (ADR-0156 decision 4). */\nregisterAuditKind('auth.signin', ['method'])\nregisterAuditKind('auth.checkin')\nregisterAuditKind('auth.signout')\n/* Confirming it is still the signed-in person before a door that asks so (ADR-0168) - password or passkey. */\nregisterAuditKind('auth.confirm-identity', ['method'])\nregisterAuditKind('password.change')\nregisterAuditKind('password.reset-by-admin')\nregisterAuditKind('credential.issue')\nregisterAuditKind('webauthn.register')\nregisterAuditKind('webauthn.remove')\n\n/* Who may do what */\nregisterAuditKind('role.grant')\nregisterAuditKind('role.revoke')\nregisterAuditKind('role.privilege.add')\nregisterAuditKind('role.privilege.remove')\nregisterAuditKind('role.delete')\nregisterAuditKind('user.status', ['from', 'to'])\nregisterAuditKind('domain.owner.add')\nregisterAuditKind('domain.owner.remove')\n/* Invitations are gone (ADR-0161 decision 6); their records stay and are still read. */\nregisterAuditKind('invitation.create')\nregisterAuditKind('invitation.status', ['from', 'to'])\n"]}
@@ -0,0 +1,18 @@
1
+ import { GraphQLError } from 'graphql';
2
+ /** The refusal's code - a screen that asks for the confirmation itself names it in `handledRefusals`. */
3
+ export declare const IDENTITY_CONFIRMATION_REFUSAL = "identity-confirmation-required";
4
+ export declare const recentConfirmationDirectiveTypeDefs: import("graphql").DocumentNode;
5
+ /** How long a confirmation lives from the moment it was given, in minutes - 10 unless configured. */
6
+ export declare function confirmationMinutes(): number;
7
+ /** Remember that `userId` confirmed just now. */
8
+ export declare function rememberConfirmation(context: any, userId: string): void;
9
+ /**
10
+ * Until when `userId`'s confirmation in this browser holds - null when there is none, it is another person's, or it
11
+ * ran out. A screen reads it to put away what it shows the moment the confirmation ends.
12
+ */
13
+ export declare function confirmedUntil(context: any, userId: string | undefined): Date | null;
14
+ /** Whether `userId` confirmed within the last minutes - in this browser, as this person. */
15
+ export declare function isRecentlyConfirmed(context: any, userId: string | undefined): boolean;
16
+ export declare function forgetConfirmation(context: any): void;
17
+ /** The refusal of a door that asks for a recent confirmation, in the reader's words when there are some. */
18
+ export declare function confirmationRefusal(context: any): GraphQLError;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recentConfirmationDirectiveTypeDefs = exports.IDENTITY_CONFIRMATION_REFUSAL = void 0;
4
+ exports.confirmationMinutes = confirmationMinutes;
5
+ exports.rememberConfirmation = rememberConfirmation;
6
+ exports.confirmedUntil = confirmedUntil;
7
+ exports.isRecentlyConfirmed = isRecentlyConfirmed;
8
+ exports.forgetConfirmation = forgetConfirmation;
9
+ exports.confirmationRefusal = confirmationRefusal;
10
+ const tslib_1 = require("tslib");
11
+ const graphql_1 = require("graphql");
12
+ const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
13
+ const jsonwebtoken_1 = tslib_1.__importDefault(require("jsonwebtoken"));
14
+ const env_1 = require("@things-factory/env");
15
+ const get_secret_js_1 = require("../../utils/get-secret.js");
16
+ /*
17
+ * ── A recent confirmation that it is still the signed-in person (ADR-0168) ──────────────
18
+ * A door whose facts must not open to whoever sits at an unattended desk says so with
19
+ * `@recentConfirmation` beside its `@privilege`. The person confirms with their password or a
20
+ * passkey; the confirmation is a signed cookie of its own, apart from the access token, that lives
21
+ * ten minutes from the moment it was given - opening a door does not lengthen it (the user,
22
+ * 2026-10-28). Every way to the door - a screen, an assistant tool - meets the same question there.
23
+ */
24
+ const COOKIE = 'identity_confirmed';
25
+ const PURPOSE = 'identity-confirmation';
26
+ /** The refusal's code - a screen that asks for the confirmation itself names it in `handledRefusals`. */
27
+ exports.IDENTITY_CONFIRMATION_REFUSAL = 'identity-confirmation-required';
28
+ exports.recentConfirmationDirectiveTypeDefs = (0, graphql_tag_1.default) `
29
+ directive @recentConfirmation on FIELD_DEFINITION
30
+ `;
31
+ /** How long a confirmation lives from the moment it was given, in minutes - 10 unless configured. */
32
+ function confirmationMinutes() {
33
+ const minutes = Number(env_1.config.get('identityConfirmation', {})?.minutes);
34
+ return minutes > 0 ? minutes : 10;
35
+ }
36
+ function cookieOptions(context, maxAge) {
37
+ return { secure: context.secure, httpOnly: true, sameSite: 'Lax', ...(maxAge ? { maxAge } : {}) };
38
+ }
39
+ /** Remember that `userId` confirmed just now. */
40
+ function rememberConfirmation(context, userId) {
41
+ const minutes = confirmationMinutes();
42
+ const token = jsonwebtoken_1.default.sign({ sub: userId, purpose: PURPOSE }, get_secret_js_1.SECRET, { expiresIn: minutes * 60 });
43
+ context.cookies?.set(COOKIE, token, cookieOptions(context, minutes * 60 * 1000));
44
+ }
45
+ /**
46
+ * Until when `userId`'s confirmation in this browser holds - null when there is none, it is another person's, or it
47
+ * ran out. A screen reads it to put away what it shows the moment the confirmation ends.
48
+ */
49
+ function confirmedUntil(context, userId) {
50
+ const token = context?.cookies?.get(COOKIE);
51
+ if (!token || !userId)
52
+ return null;
53
+ try {
54
+ const payload = jsonwebtoken_1.default.verify(token, get_secret_js_1.SECRET);
55
+ return payload.purpose === PURPOSE && payload.sub === userId && payload.exp ? new Date(payload.exp * 1000) : null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ /** Whether `userId` confirmed within the last minutes - in this browser, as this person. */
62
+ function isRecentlyConfirmed(context, userId) {
63
+ return confirmedUntil(context, userId) !== null;
64
+ }
65
+ function forgetConfirmation(context) {
66
+ context?.cookies?.set(COOKIE, '', cookieOptions(context));
67
+ }
68
+ /** The refusal of a door that asks for a recent confirmation, in the reader's words when there are some. */
69
+ function confirmationRefusal(context) {
70
+ const fallback = 'Confirm it is you to see this';
71
+ let message = fallback;
72
+ try {
73
+ const translated = typeof context?.t === 'function' ? context.t('error.identity confirmation required') : null;
74
+ if (translated && translated !== 'error.identity confirmation required')
75
+ message = translated;
76
+ }
77
+ catch {
78
+ /* the refusal stands without its translation */
79
+ }
80
+ return new graphql_1.GraphQLError(message, {
81
+ extensions: { code: 'IDENTITY_CONFIRMATION_REQUIRED', refusal: { code: exports.IDENTITY_CONFIRMATION_REFUSAL, params: {} } }
82
+ });
83
+ }
84
+ //# sourceMappingURL=identity-confirmation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-confirmation.js","sourceRoot":"","sources":["../../../server/service/identity-confirmation/identity-confirmation.ts"],"names":[],"mappings":"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","sourcesContent":["import { GraphQLError } from 'graphql'\nimport gql from 'graphql-tag'\nimport jwt from 'jsonwebtoken'\n\nimport { config } from '@things-factory/env'\n\nimport { SECRET } from '../../utils/get-secret.js'\n\n/*\n * ── A recent confirmation that it is still the signed-in person (ADR-0168) ──────────────\n * A door whose facts must not open to whoever sits at an unattended desk says so with\n * `@recentConfirmation` beside its `@privilege`. The person confirms with their password or a\n * passkey; the confirmation is a signed cookie of its own, apart from the access token, that lives\n * ten minutes from the moment it was given - opening a door does not lengthen it (the user,\n * 2026-10-28). Every way to the door - a screen, an assistant tool - meets the same question there.\n */\n\nconst COOKIE = 'identity_confirmed'\nconst PURPOSE = 'identity-confirmation'\n\n/** The refusal's code - a screen that asks for the confirmation itself names it in `handledRefusals`. */\nexport const IDENTITY_CONFIRMATION_REFUSAL = 'identity-confirmation-required'\n\nexport const recentConfirmationDirectiveTypeDefs = gql`\n directive @recentConfirmation on FIELD_DEFINITION\n`\n\n/** How long a confirmation lives from the moment it was given, in minutes - 10 unless configured. */\nexport function confirmationMinutes(): number {\n const minutes = Number(config.get('identityConfirmation', {})?.minutes)\n return minutes > 0 ? minutes : 10\n}\n\nfunction cookieOptions(context, maxAge?: number) {\n return { secure: context.secure, httpOnly: true, sameSite: 'Lax' as const, ...(maxAge ? { maxAge } : {}) }\n}\n\n/** Remember that `userId` confirmed just now. */\nexport function rememberConfirmation(context, userId: string) {\n const minutes = confirmationMinutes()\n const token = jwt.sign({ sub: userId, purpose: PURPOSE }, SECRET, { expiresIn: minutes * 60 })\n\n context.cookies?.set(COOKIE, token, cookieOptions(context, minutes * 60 * 1000))\n}\n\n/**\n * Until when `userId`'s confirmation in this browser holds - null when there is none, it is another person's, or it\n * ran out. A screen reads it to put away what it shows the moment the confirmation ends.\n */\nexport function confirmedUntil(context, userId: string | undefined): Date | null {\n const token = context?.cookies?.get(COOKIE)\n if (!token || !userId) return null\n\n try {\n const payload = jwt.verify(token, SECRET) as { sub?: string; purpose?: string; exp?: number }\n return payload.purpose === PURPOSE && payload.sub === userId && payload.exp ? new Date(payload.exp * 1000) : null\n } catch {\n return null\n }\n}\n\n/** Whether `userId` confirmed within the last minutes - in this browser, as this person. */\nexport function isRecentlyConfirmed(context, userId: string | undefined): boolean {\n return confirmedUntil(context, userId) !== null\n}\n\nexport function forgetConfirmation(context) {\n context?.cookies?.set(COOKIE, '', cookieOptions(context))\n}\n\n/** The refusal of a door that asks for a recent confirmation, in the reader's words when there are some. */\nexport function confirmationRefusal(context): GraphQLError {\n const fallback = 'Confirm it is you to see this'\n let message = fallback\n try {\n const translated = typeof context?.t === 'function' ? context.t('error.identity confirmation required') : null\n if (translated && translated !== 'error.identity confirmation required') message = translated\n } catch {\n /* the refusal stands without its translation */\n }\n\n return new GraphQLError(message, {\n extensions: { code: 'IDENTITY_CONFIRMATION_REQUIRED', refusal: { code: IDENTITY_CONFIRMATION_REFUSAL, params: {} } }\n })\n}\n"]}
@@ -42,6 +42,7 @@ export declare const schema: {
42
42
  typeDefs: {
43
43
  privilegeDirectiveTypeDefs: import("graphql").DocumentNode;
44
44
  auditTargetDirectiveTypeDefs: import("graphql").DocumentNode;
45
+ recentConfirmationDirectiveTypeDefs: import("graphql").DocumentNode;
45
46
  };
46
47
  openFields: import("./privilege/open-field-rule.js").OpenFieldsDeclaration;
47
48
  resolverClasses: any[];
@@ -13,6 +13,7 @@ const index_js_7 = require("./domain-generator/index.js");
13
13
  const index_js_8 = require("./granted-role/index.js");
14
14
  const index_js_9 = require("./audit/index.js");
15
15
  const audit_target_js_1 = require("./audit/audit-target.js");
16
+ const identity_confirmation_js_1 = require("./identity-confirmation/identity-confirmation.js");
16
17
  const index_js_10 = require("./partner/index.js");
17
18
  const index_js_11 = require("./password-history/index.js");
18
19
  const index_js_12 = require("./privilege/index.js");
@@ -98,7 +99,8 @@ exports.subscribers = [...index_js_9.subscribers];
98
99
  exports.schema = {
99
100
  typeDefs: {
100
101
  privilegeDirectiveTypeDefs: privilege_directive_js_1.privilegeDirectiveTypeDefs,
101
- auditTargetDirectiveTypeDefs: audit_target_js_1.auditTargetDirectiveTypeDefs
102
+ auditTargetDirectiveTypeDefs: audit_target_js_1.auditTargetDirectiveTypeDefs,
103
+ recentConfirmationDirectiveTypeDefs: identity_confirmation_js_1.recentConfirmationDirectiveTypeDefs
102
104
  },
103
105
  /* The fields that carry no @privilege, and why — `open-fields.ts`, read by the directive. */
104
106
  openFields: open_fields_js_1.openFields,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,mCAAmC;AACnC,wDAAkH;AAClH,8DAGwC;AACxC,uDAA+G;AAC/G,qDAAwE;AACxE,mDAAqG;AACrG,qDAA2G;AAC3G,0DAAkF;AAClF,sDAA2G;AAC3G,+CAIyB;AACzB,6DAAsE;AACtE,kDAA+F;AAC/F,2DAAiF;AACjF,oDAAqG;AACrG,+EAA2G;AAC3G,qDAA6C;AAC7C,+CAAsF;AACtF,wDAA6E;AAC7E,+CAAsF;AACtF,6DAAqF;AACrF,8DAA+H;AAC/H,sDAAwG;AACxG,uDAA4G;AAE5G,yBAAyB;AACzB,yFAA8D;AAC9D,2EAAgD;AAChD,uEAA4C;AAC5C,mEAAwC;AACxC,mEAAwC;AACxC,yDAA8B;AAC9B,wEAA6C;AAC7C,gHAAgH;AAChH,6EAAqF;AAA5E,0HAAA,gBAAgB,OAAA;AACzB,2FAA2F;AAC3F,6EAAuE;AAA9D,4HAAA,kBAAkB,OAAA;AAG3B,2EAAgD;AAChD,uEAA4C;AAC5C,iFAAsD;AACtD,yDAA8B;AAC9B,+DAAoC;AACpC,yEAA8C;AAC9C,uEAA4C;AAC5C,iFAAsD;AACtD,qFAA0D;AAC1D,mEAAwC;AACxC,kEAAuC;AACvC,yDAA0D;AAAjD,mHAAA,iBAAiB,OAAA;AAC1B,+DAAiH;AAAxG,sHAAA,iBAAiB,OAAA;AAAE,oHAAA,eAAe,OAAA;AAAE,+GAAA,UAAU,OAAA;AACvD,iEAAmF;AAA1E,uHAAA,iBAAiB,OAAA;AAAE,yHAAA,mBAAmB,OAAA;AAC/C,uFAA4D;AAC5D,uEAA4C;AAC5C,yEAA8C;AAC9C,6EAAkD;AAElD,kBAAkB;AAClB,6EAAkD;AAClD,yEAA8C;AAC9C,6EAAkD;AAClD,uFAA4D;AAC5D,qEAA0C;AAC1C,yEAA8C;AAC9C,+DAAoC;AACpC,+DAAoC;AACpC,6EAAkD;AAClD,mFAAwD;AAE3C,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,mBAA0B;IAC7B,GAAG,mBAAoB;IACvB,GAAG,mBAAmB;IACtB,GAAG,mBAAiB;IACpB,GAAG,oBAAiB;IACpB,GAAG,oBAAY;IACf,GAAG,oBAAY;IACf,GAAG,oBAAe;IAClB,GAAG,mBAAmB;IACtB,GAAG,oBAAuB;IAC1B,GAAG,oBAAyB;IAC5B,GAAG,mBAAa;IAChB,GAAG,oBAAyB;IAC5B,GAAG,oBAAkB;IACrB,GAAG,oBAAmB;IACtB,GAAG,mBAAqB;CACzB,CAAA;AAED,8EAA8E;AACjE,QAAA,WAAW,GAAG,CAAC,GAAG,sBAAgB,CAAC,CAAA;AAEnC,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE;QACR,0BAA0B,EAA1B,mDAA0B;QAC1B,4BAA4B,EAA5B,8CAA4B;KAC7B;IAED,6FAA6F;IAC7F,UAAU,EAAV,2BAAU;IAEV,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,oBAA2B;QAC9B,GAAG,oBAAqB;QACxB,GAAG,oBAAoB;QACvB,GAAG,oBAAkB;QACrB,GAAG,qBAAkB;QACrB,GAAG,qBAAa;QAChB,GAAG,qBAAqB;QACxB,GAAG,qBAAa;QAChB,GAAG,qBAAgB;QACnB,GAAG,oBAAmB;QACtB,GAAG,oBAAkB;QACrB,GAAG,oBAAuB;QAC1B,GAAG,oBAAc;QACjB,GAAG,qBAAkB;QACrB,GAAG,qBAAoB;QACvB,GAAG,oBAAsB;QACzB,GAAG,qBAA0B;KAC9B;IACD,UAAU,EAAE;QACV,SAAS,EAAE,mDAA0B;KACtC;CACF,CAAA","sourcesContent":["/* IMPORT ENTITIES AND RESOLVERS */\nimport { entities as AccessRequestEntities, resolvers as AccessRequestResolvers } from './access-request/index.js'\nimport {\n entities as UsersAuthProvidersEntities,\n resolvers as UsersAuthProvidersResolvers\n} from './users-auth-providers/index.js'\nimport { entities as AuthProviderEntities, resolvers as AuthProviderResolvers } from './auth-provider/index.js'\nimport { resolvers as AppbindingResolver } from './app-binding/index.js'\nimport { entities as ApplianceEntities, resolvers as ApplianceResolvers } from './appliance/index.js'\nimport { entities as ApplicationEntities, resolvers as ApplicationResolvers } from './application/index.js'\nimport { resolvers as DomainGeneratorResolver } from './domain-generator/index.js'\nimport { entities as GrantedRoleEntities, resolvers as GrantedRoleResolver } from './granted-role/index.js'\nimport {\n entities as AuditEntities,\n resolvers as AuditResolvers,\n subscribers as AuditSubscribers\n} from './audit/index.js'\nimport { auditTargetDirectiveTypeDefs } from './audit/audit-target.js'\nimport { entities as PartnerEntities, resolvers as PartnerResolvers } from './partner/index.js'\nimport { entities as PasswordHistoryEntities } from './password-history/index.js'\nimport { entities as PrivilegeEntities, resolvers as PrivilegeResolvers } from './privilege/index.js'\nimport { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } from './privilege/privilege-directive.js'\nimport { openFields } from './open-fields.js'\nimport { entities as RoleEntities, resolvers as RoleResolvers } from './role/index.js'\nimport { resolvers as RoleTemplateResolvers } from './role-template/index.js'\nimport { entities as UserEntities, resolvers as UserResolvers } from './user/index.js'\nimport { entities as VerificationTokenEntities } from './verification-token/index.js'\nimport { entities as WebAuthCredentialEntities, resolvers as WebAuthCredentialResolvers } from './web-auth-credential/index.js'\nimport { entities as DomainLinkEntities, resolvers as DomainLinkResolver } from './domain-link/index.js'\nimport { entities as DomainOwnerEntities, resolvers as DomainOwnerResolvers } from './domain-owner/index.js'\n\n/* EXPORT ENTITY TYPES */\nexport * from './users-auth-providers/users-auth-providers.js'\nexport * from './auth-provider/auth-provider.js'\nexport * from './application/application.js'\nexport * from './appliance/appliance.js'\nexport * from './privilege/privilege.js'\nexport * from './role/role.js'\nexport * from './privilege/privilege-axis.js'\n/* What a field asks for, by name — for another entrance to the same door (operato-twin ADR-0072 decision 5). */\nexport { privilegeOfField, type FieldGate } from './privilege/privilege-directive.js'\n/* Other entrances report AUTH's refusal without copying its cause or localized wording. */\nexport { privilegeRejection } from './privilege/privilege-rejection.js'\n/* How a module declares the fields it leaves open — see `privilege/open-field-rule.ts`. */\nexport { type OpenFieldsDeclaration } from './privilege/open-field-rule.js'\nexport * from './role-template/role-template.js'\nexport * from './role-template/role-task.js'\nexport * from './role-template/role-template-types.js'\nexport * from './user/user.js'\nexport * from './partner/partner.js'\nexport * from './granted-role/granted-role.js'\nexport * from './app-binding/app-binding.js'\nexport * from './password-history/password-history.js'\nexport * from './verification-token/verification-token.js'\nexport * from './audit/audit-request.js'\nexport * from './audit/audit-action.js'\nexport { registerAuditKind } from './audit/audit-kinds.js'\nexport { recordAuditAction, noteAuditAction, flushAudit, type AuditActionInput } from './audit/audit-recorder.js'\nexport { purgeExpiredAudit, startAuditRetention } from './audit/audit-retention.js'\nexport * from './web-auth-credential/web-auth-credential.js'\nexport * from './domain-link/domain-link.js'\nexport * from './domain-owner/domain-owner.js'\nexport * from './access-request/access-request.js'\n\n/* EXPORT TYPES */\nexport * from './app-binding/app-binding-types.js'\nexport * from './appliance/appliance-types.js'\nexport * from './application/application-types.js'\nexport * from './domain-generator/domain-generator-types.js'\nexport * from './partner/partner-types.js'\nexport * from './privilege/privilege-types.js'\nexport * from './role/role-types.js'\nexport * from './user/user-types.js'\nexport * from './domain-link/domain-link-types.js'\nexport * from './access-request/access-request-types.js'\n\nexport const entities = [\n /* ENTITIES */\n ...UsersAuthProvidersEntities,\n ...AuthProviderEntities,\n ...ApplicationEntities,\n ...ApplianceEntities,\n ...PrivilegeEntities,\n ...RoleEntities,\n ...UserEntities,\n ...PartnerEntities,\n ...GrantedRoleEntities,\n ...PasswordHistoryEntities,\n ...VerificationTokenEntities,\n ...AuditEntities,\n ...WebAuthCredentialEntities,\n ...DomainLinkEntities,\n ...DomainOwnerEntities,\n ...AccessRequestEntities\n]\n\n/* The audit of who may do what, written where it is saved (ADR-0141 §3.3). */\nexport const subscribers = [...AuditSubscribers]\n\nexport const schema = {\n typeDefs: {\n privilegeDirectiveTypeDefs,\n auditTargetDirectiveTypeDefs\n },\n\n /* The fields that carry no @privilege, and why — `open-fields.ts`, read by the directive. */\n openFields,\n\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...UsersAuthProvidersResolvers,\n ...AuthProviderResolvers,\n ...ApplicationResolvers,\n ...ApplianceResolvers,\n ...PrivilegeResolvers,\n ...RoleResolvers,\n ...RoleTemplateResolvers,\n ...UserResolvers,\n ...PartnerResolvers,\n ...GrantedRoleResolver,\n ...AppbindingResolver,\n ...DomainGeneratorResolver,\n ...AuditResolvers,\n ...DomainLinkResolver,\n ...DomainOwnerResolvers,\n ...AccessRequestResolvers,\n ...WebAuthCredentialResolvers\n ],\n directives: {\n privilege: privilegeDirectiveResolver\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,mCAAmC;AACnC,wDAAkH;AAClH,8DAGwC;AACxC,uDAA+G;AAC/G,qDAAwE;AACxE,mDAAqG;AACrG,qDAA2G;AAC3G,0DAAkF;AAClF,sDAA2G;AAC3G,+CAIyB;AACzB,6DAAsE;AACtE,+FAAsG;AACtG,kDAA+F;AAC/F,2DAAiF;AACjF,oDAAqG;AACrG,+EAA2G;AAC3G,qDAA6C;AAC7C,+CAAsF;AACtF,wDAA6E;AAC7E,+CAAsF;AACtF,6DAAqF;AACrF,8DAA+H;AAC/H,sDAAwG;AACxG,uDAA4G;AAE5G,yBAAyB;AACzB,yFAA8D;AAC9D,2EAAgD;AAChD,uEAA4C;AAC5C,mEAAwC;AACxC,mEAAwC;AACxC,yDAA8B;AAC9B,wEAA6C;AAC7C,gHAAgH;AAChH,6EAAqF;AAA5E,0HAAA,gBAAgB,OAAA;AACzB,2FAA2F;AAC3F,6EAAuE;AAA9D,4HAAA,kBAAkB,OAAA;AAG3B,2EAAgD;AAChD,uEAA4C;AAC5C,iFAAsD;AACtD,yDAA8B;AAC9B,+DAAoC;AACpC,yEAA8C;AAC9C,uEAA4C;AAC5C,iFAAsD;AACtD,qFAA0D;AAC1D,mEAAwC;AACxC,kEAAuC;AACvC,yDAA0D;AAAjD,mHAAA,iBAAiB,OAAA;AAC1B,+DAAiH;AAAxG,sHAAA,iBAAiB,OAAA;AAAE,oHAAA,eAAe,OAAA;AAAE,+GAAA,UAAU,OAAA;AACvD,iEAAmF;AAA1E,uHAAA,iBAAiB,OAAA;AAAE,yHAAA,mBAAmB,OAAA;AAC/C,uFAA4D;AAC5D,uEAA4C;AAC5C,yEAA8C;AAC9C,6EAAkD;AAElD,kBAAkB;AAClB,6EAAkD;AAClD,yEAA8C;AAC9C,6EAAkD;AAClD,uFAA4D;AAC5D,qEAA0C;AAC1C,yEAA8C;AAC9C,+DAAoC;AACpC,+DAAoC;AACpC,6EAAkD;AAClD,mFAAwD;AAE3C,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,mBAA0B;IAC7B,GAAG,mBAAoB;IACvB,GAAG,mBAAmB;IACtB,GAAG,mBAAiB;IACpB,GAAG,oBAAiB;IACpB,GAAG,oBAAY;IACf,GAAG,oBAAY;IACf,GAAG,oBAAe;IAClB,GAAG,mBAAmB;IACtB,GAAG,oBAAuB;IAC1B,GAAG,oBAAyB;IAC5B,GAAG,mBAAa;IAChB,GAAG,oBAAyB;IAC5B,GAAG,oBAAkB;IACrB,GAAG,oBAAmB;IACtB,GAAG,mBAAqB;CACzB,CAAA;AAED,8EAA8E;AACjE,QAAA,WAAW,GAAG,CAAC,GAAG,sBAAgB,CAAC,CAAA;AAEnC,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE;QACR,0BAA0B,EAA1B,mDAA0B;QAC1B,4BAA4B,EAA5B,8CAA4B;QAC5B,mCAAmC,EAAnC,8DAAmC;KACpC;IAED,6FAA6F;IAC7F,UAAU,EAAV,2BAAU;IAEV,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,oBAA2B;QAC9B,GAAG,oBAAqB;QACxB,GAAG,oBAAoB;QACvB,GAAG,oBAAkB;QACrB,GAAG,qBAAkB;QACrB,GAAG,qBAAa;QAChB,GAAG,qBAAqB;QACxB,GAAG,qBAAa;QAChB,GAAG,qBAAgB;QACnB,GAAG,oBAAmB;QACtB,GAAG,oBAAkB;QACrB,GAAG,oBAAuB;QAC1B,GAAG,oBAAc;QACjB,GAAG,qBAAkB;QACrB,GAAG,qBAAoB;QACvB,GAAG,oBAAsB;QACzB,GAAG,qBAA0B;KAC9B;IACD,UAAU,EAAE;QACV,SAAS,EAAE,mDAA0B;KACtC;CACF,CAAA","sourcesContent":["/* IMPORT ENTITIES AND RESOLVERS */\nimport { entities as AccessRequestEntities, resolvers as AccessRequestResolvers } from './access-request/index.js'\nimport {\n entities as UsersAuthProvidersEntities,\n resolvers as UsersAuthProvidersResolvers\n} from './users-auth-providers/index.js'\nimport { entities as AuthProviderEntities, resolvers as AuthProviderResolvers } from './auth-provider/index.js'\nimport { resolvers as AppbindingResolver } from './app-binding/index.js'\nimport { entities as ApplianceEntities, resolvers as ApplianceResolvers } from './appliance/index.js'\nimport { entities as ApplicationEntities, resolvers as ApplicationResolvers } from './application/index.js'\nimport { resolvers as DomainGeneratorResolver } from './domain-generator/index.js'\nimport { entities as GrantedRoleEntities, resolvers as GrantedRoleResolver } from './granted-role/index.js'\nimport {\n entities as AuditEntities,\n resolvers as AuditResolvers,\n subscribers as AuditSubscribers\n} from './audit/index.js'\nimport { auditTargetDirectiveTypeDefs } from './audit/audit-target.js'\nimport { recentConfirmationDirectiveTypeDefs } from './identity-confirmation/identity-confirmation.js'\nimport { entities as PartnerEntities, resolvers as PartnerResolvers } from './partner/index.js'\nimport { entities as PasswordHistoryEntities } from './password-history/index.js'\nimport { entities as PrivilegeEntities, resolvers as PrivilegeResolvers } from './privilege/index.js'\nimport { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } from './privilege/privilege-directive.js'\nimport { openFields } from './open-fields.js'\nimport { entities as RoleEntities, resolvers as RoleResolvers } from './role/index.js'\nimport { resolvers as RoleTemplateResolvers } from './role-template/index.js'\nimport { entities as UserEntities, resolvers as UserResolvers } from './user/index.js'\nimport { entities as VerificationTokenEntities } from './verification-token/index.js'\nimport { entities as WebAuthCredentialEntities, resolvers as WebAuthCredentialResolvers } from './web-auth-credential/index.js'\nimport { entities as DomainLinkEntities, resolvers as DomainLinkResolver } from './domain-link/index.js'\nimport { entities as DomainOwnerEntities, resolvers as DomainOwnerResolvers } from './domain-owner/index.js'\n\n/* EXPORT ENTITY TYPES */\nexport * from './users-auth-providers/users-auth-providers.js'\nexport * from './auth-provider/auth-provider.js'\nexport * from './application/application.js'\nexport * from './appliance/appliance.js'\nexport * from './privilege/privilege.js'\nexport * from './role/role.js'\nexport * from './privilege/privilege-axis.js'\n/* What a field asks for, by name — for another entrance to the same door (operato-twin ADR-0072 decision 5). */\nexport { privilegeOfField, type FieldGate } from './privilege/privilege-directive.js'\n/* Other entrances report AUTH's refusal without copying its cause or localized wording. */\nexport { privilegeRejection } from './privilege/privilege-rejection.js'\n/* How a module declares the fields it leaves open — see `privilege/open-field-rule.ts`. */\nexport { type OpenFieldsDeclaration } from './privilege/open-field-rule.js'\nexport * from './role-template/role-template.js'\nexport * from './role-template/role-task.js'\nexport * from './role-template/role-template-types.js'\nexport * from './user/user.js'\nexport * from './partner/partner.js'\nexport * from './granted-role/granted-role.js'\nexport * from './app-binding/app-binding.js'\nexport * from './password-history/password-history.js'\nexport * from './verification-token/verification-token.js'\nexport * from './audit/audit-request.js'\nexport * from './audit/audit-action.js'\nexport { registerAuditKind } from './audit/audit-kinds.js'\nexport { recordAuditAction, noteAuditAction, flushAudit, type AuditActionInput } from './audit/audit-recorder.js'\nexport { purgeExpiredAudit, startAuditRetention } from './audit/audit-retention.js'\nexport * from './web-auth-credential/web-auth-credential.js'\nexport * from './domain-link/domain-link.js'\nexport * from './domain-owner/domain-owner.js'\nexport * from './access-request/access-request.js'\n\n/* EXPORT TYPES */\nexport * from './app-binding/app-binding-types.js'\nexport * from './appliance/appliance-types.js'\nexport * from './application/application-types.js'\nexport * from './domain-generator/domain-generator-types.js'\nexport * from './partner/partner-types.js'\nexport * from './privilege/privilege-types.js'\nexport * from './role/role-types.js'\nexport * from './user/user-types.js'\nexport * from './domain-link/domain-link-types.js'\nexport * from './access-request/access-request-types.js'\n\nexport const entities = [\n /* ENTITIES */\n ...UsersAuthProvidersEntities,\n ...AuthProviderEntities,\n ...ApplicationEntities,\n ...ApplianceEntities,\n ...PrivilegeEntities,\n ...RoleEntities,\n ...UserEntities,\n ...PartnerEntities,\n ...GrantedRoleEntities,\n ...PasswordHistoryEntities,\n ...VerificationTokenEntities,\n ...AuditEntities,\n ...WebAuthCredentialEntities,\n ...DomainLinkEntities,\n ...DomainOwnerEntities,\n ...AccessRequestEntities\n]\n\n/* The audit of who may do what, written where it is saved (ADR-0141 §3.3). */\nexport const subscribers = [...AuditSubscribers]\n\nexport const schema = {\n typeDefs: {\n privilegeDirectiveTypeDefs,\n auditTargetDirectiveTypeDefs,\n recentConfirmationDirectiveTypeDefs\n },\n\n /* The fields that carry no @privilege, and why — `open-fields.ts`, read by the directive. */\n openFields,\n\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...UsersAuthProvidersResolvers,\n ...AuthProviderResolvers,\n ...ApplicationResolvers,\n ...ApplianceResolvers,\n ...PrivilegeResolvers,\n ...RoleResolvers,\n ...RoleTemplateResolvers,\n ...UserResolvers,\n ...PartnerResolvers,\n ...GrantedRoleResolver,\n ...AppbindingResolver,\n ...DomainGeneratorResolver,\n ...AuditResolvers,\n ...DomainLinkResolver,\n ...DomainOwnerResolvers,\n ...AccessRequestResolvers,\n ...WebAuthCredentialResolvers\n ],\n directives: {\n privilege: privilegeDirectiveResolver\n }\n}\n"]}
@@ -10,6 +10,7 @@ const env_1 = require("@things-factory/env");
10
10
  const check_permission_js_1 = require("../../utils/check-permission.js");
11
11
  const audit_recorder_js_1 = require("../audit/audit-recorder.js");
12
12
  const audit_target_js_1 = require("../audit/audit-target.js");
13
+ const identity_confirmation_js_1 = require("../identity-confirmation/identity-confirmation.js");
13
14
  const privilege_axis_js_1 = require("./privilege-axis.js");
14
15
  const privilege_rejection_js_1 = require("./privilege-rejection.js");
15
16
  const open_field_rule_js_1 = require("./open-field-rule.js");
@@ -78,6 +79,12 @@ const privilegeDirectiveResolver = (schema, options = {}) => {
78
79
  const mapped = (0, utils_1.mapSchema)(schema, {
79
80
  [utils_1.MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => {
80
81
  const privilegeDirective = (0, utils_1.getDirective)(schema, fieldConfig, DIRECTIVE)?.[0];
82
+ /* A door that also asks for a recent confirmation that it is still the person (ADR-0168). */
83
+ const asksConfirmation = !!(0, utils_1.getDirective)(schema, fieldConfig, 'recentConfirmation')?.[0];
84
+ if (asksConfirmation && !privilegeDirective) {
85
+ throw new Error(`@recentConfirmation on ${typeName}.${fieldName} has no @privilege beside it. The confirmation is asked by ` +
86
+ `the privilege gate, after the privilege - a door that asks only for it would ask nobody.`);
87
+ }
81
88
  /* A root field with no directive is recorded as open, so "open" and "no such field" differ. */
82
89
  if (!privilegeDirective && isRootType(schema, typeName)) {
83
90
  const where = `${typeName}.${fieldName}`;
@@ -173,7 +180,8 @@ const privilegeDirectiveResolver = (schema, options = {}) => {
173
180
  // 권한 정보를 포함한 새로운 description 생성
174
181
  const privilegeDescription = `\n\n🔒 Requires privilege: ${category}:${privilege}` +
175
182
  (domainOwnerGranted ? ', Domain ownership' : '') +
176
- (superUserGranted ? ', System ownership' : '');
183
+ (superUserGranted ? ', System ownership' : '') +
184
+ (asksConfirmation ? ', a recent confirmation that it is the person' : '');
177
185
  // 기존 description과 결합
178
186
  fieldConfig.description = `${existingDescription} ${privilegeDescription}`.trim();
179
187
  fieldConfig.resolve = async function (source, args, context, info) {
@@ -188,6 +196,14 @@ const privilegeDirectiveResolver = (schema, options = {}) => {
188
196
  const targetIds = target ? (0, audit_target_js_1.targetIdsOf)(args, target.arg) : undefined;
189
197
  const targetRole = target ? 'requested' : undefined;
190
198
  if (judgement.permitted === true) {
199
+ /*
200
+ * The privilege is the person's; the confirmation says the person is the one at the screen (ADR-0168).
201
+ * Passing does not lengthen it - it ends ten minutes after it was given.
202
+ */
203
+ if (asksConfirmation && !(0, identity_confirmation_js_1.isRecentlyConfirmed)(context, user?.id)) {
204
+ (0, audit_recorder_js_1.noteAuditAction)({ kind: 'door.check', door, decision: 'denied', basis: 'identity-confirmation', targetKind, targetIds, targetRole });
205
+ throw (0, identity_confirmation_js_1.confirmationRefusal)(context);
206
+ }
191
207
  (0, audit_recorder_js_1.noteAuditAction)({ kind: 'door.check', door, decision: 'granted', basis: judgement.by, targetKind, targetIds, targetRole });
192
208
  /* Passing the door is not doing the work - what the work came to is its own row (§3.2). */
193
209
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"privilege-directive.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-directive.ts"],"names":[],"mappings":";;;AAuDA,4CAIC;;AA3DD,qCAA2E;AAC3E,sEAA6B;AAE7B,gDAA0E;AAC1E,6CAA4C;AAE5C,yEAAiE;AACjE,kEAA4D;AAC5D,8DAAoF;AACpF,2DAAkE;AAClE,qEAA6D;AAC7D,6DAO6B;AAE7B,OAAO,CAAC,YAAY,CAAC,GAAG,EAAE,CAAA;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,CAAC,oBAAoB,CAAC,GAAG,EAAE,CAAA;AAUlC;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,SAAiB;IAClE,MAAM,GAAG,GAAG,OAAO,CAAC,oBAAoB,CAAqC,CAAA;IAC7E,MAAM,KAAK,GAAG,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAA;IACxC,OAAO,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;AAC5E,CAAC;AAED,MAAM,SAAS,GAAG,WAAW,CAAA;AAE7B;;;;;;;;;;;;;;GAcG;AACU,QAAA,0BAA0B,GAAG,IAAA,qBAAG,EAAA;;;;;;;CAO5C,CAAA;AACD;;;GAGG;AACI,MAAM,0BAA0B,GAAG,CACxC,MAAqB,EACrB,UAAmE,EAAE,EACrE,EAAE;IACF,MAAM,QAAQ,GAAG,IAAA,+BAAU,EAAC,OAAO,CAAC,OAAO,IAAI,IAAA,sCAAiB,GAAE,CAAC,CAAA;IACnE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,IAAA,oCAAe,GAAE,CAAA;IAC9C,MAAM,QAAQ,GAAa,EAAE,CAAA;IAE7B,MAAM,MAAM,GAAG,IAAA,iBAAS,EAAC,MAAM,EAAE;QAC/B,CAAC,kBAAU,CAAC,YAAY,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;YACtE,MAAM,kBAAkB,GAAG,IAAA,oBAAY,EAAC,MAAM,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE5E,+FAA+F;YAC/F,IAAI,CAAC,kBAAkB,IAAI,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;gBACxD,MAAM,KAAK,GAAG,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAA;gBAExC,OAAO,CAAC,oBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAA;gBAE3C;;;;;mBAKG;gBACH,IAAI,IAAA,oCAAe,EAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,UAAU,EAAE,CAAC;oBACpD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;wBACpB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;oBACtB,CAAC;yBAAM,CAAC;wBACN,cAAc,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;oBACpC,CAAC;gBACH,CAAC;YACH,CAAC;YAED,IAAI,kBAAkB,EAAE,CAAC;gBACvB,MAAM,EAAE,OAAO,GAAG,8BAAoB,EAAE,IAAI,EAAE,GAAG,WAAW,CAAA;gBAE5D,IAAI,CAAC,IAAI,EAAE,CAAC;oBACV,MAAM,IAAI,KAAK,CAAC,8EAA8E,SAAS,GAAG,CAAC,CAAA;gBAC7G,CAAC;gBAED,MAAM,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,kBAAkB,CAAA;gBACxF,MAAM,KAAK,GAAG,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAA;gBAExC;;;mBAGG;gBACH,MAAM,IAAI,GACR,QAAQ,IAAI,SAAS;oBACnB,CAAC,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE;oBAC5B,CAAC,CAAC,CAAC,kBAAkB,IAAI,OAAO,EAAE,gBAAgB,IAAI,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;gBAC5F,MAAM,MAAM,GAAG,IAAA,oBAAY,EAAC,MAAM,EAAE,WAAW,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC,CAA8C,CAAA;gBAEjH;;;;;;mBAMG;gBACH,IAAI,CAAC,QAAQ,KAAK,CAAC,SAAS,EAAE,CAAC;oBAC7B,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,8BAA8B,IAAI;wBAC5G,gEAAgE,CACnE,CAAA;gBACH,CAAC;gBAED;;;;;;;;;;;;;;;mBAeG;gBACH,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS,IAAI,CAAC,kBAAkB,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBACxE,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,qBAAqB;wBACzC,mGAAmG;wBACnG,uEAAuE,CAC1E,CAAA;gBACH,CAAC;gBAED;;;;;;;mBAOG;gBACH,IAAI,SAAS,IAAI,CAAC,IAAA,kCAAc,EAAC,SAAS,CAAC,EAAE,CAAC;oBAC5C,MAAM,IAAI,KAAK,CACb,iBAAiB,KAAK,eAAe,SAAS,+CAA+C;wBAC3F,kBAAkB,CAAC,GAAG,IAAA,gCAAY,GAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;wBAC3D,2CAA2C,SAAS,uCAAuC;wBAC3F,oFAAoF,CACvF,CAAA;gBACH,CAAC;gBAED,IAAI,QAAQ,IAAI,SAAS,EAAE,CAAC;oBAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;gBAC3E,CAAC;gBAED,sFAAsF;gBACtF,OAAO,CAAC,oBAAoB,CAAC,CAAC,KAAK,CAAC,GAAG;oBACrC,QAAQ,EAAE,QAAQ,IAAI,SAAS;oBAC/B,SAAS,EAAE,SAAS,IAAI,SAAS;oBACjC,KAAK,EAAE,CAAC,CAAC,kBAAkB;oBAC3B,KAAK,EAAE,CAAC,CAAC,gBAAgB;iBACN,CAAA;gBAErB,0BAA0B;gBAC1B,MAAM,mBAAmB,GAAG,WAAW,CAAC,WAAW,IAAI,EAAE,CAAA;gBAEzD,gCAAgC;gBAChC,MAAM,oBAAoB,GACxB,8BAA8B,QAAQ,IAAI,SAAS,EAAE;oBACrD,CAAC,kBAAkB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;oBAChD,CAAC,gBAAgB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;gBAEhD,qBAAqB;gBACrB,WAAW,CAAC,WAAW,GAAG,GAAG,mBAAmB,IAAI,oBAAoB,EAAE,CAAC,IAAI,EAAE,CAAA;gBAEjF,WAAW,CAAC,OAAO,GAAG,KAAK,WAAW,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI;oBAC/D,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;oBAEtE,MAAM,SAAS,GAAG,MAAM,IAAA,qCAAe,EACrC;wBACE,QAAQ;wBACR,SAAS;wBACT,KAAK,EAAE,kBAAkB;wBACzB,KAAK,EAAE,gBAAgB;qBACxB,EACD,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,oBAAoB,CACrB,CAAA;oBAED,MAAM,UAAU,GAAG,MAAM,EAAE,IAAI,CAAA;oBAC/B,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,IAAA,6BAAW,EAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;oBACpE,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAE,WAAqB,CAAC,CAAC,CAAC,SAAS,CAAA;oBAE9D,IAAI,SAAS,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;wBACjC,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;wBAE1H,2FAA2F;wBAC3F,IAAI,CAAC;4BACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;4BACpE,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAA,iCAAe,EAAC,MAAM,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;4BACnI,OAAO,MAAM,CAAA;wBACf,CAAC;wBAAC,OAAO,KAAK,EAAE,CAAC;4BACf,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAA,6BAAW,EAAC,KAAK,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;4BAC7H,MAAM,KAAK,CAAA;wBACb,CAAC;oBACH,CAAC;oBAED,IAAA,mCAAe,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAA;oBAE9H;;;;;;;uBAOG;oBACH,MAAM,IAAA,2CAAkB,EAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,CAAA;gBACvG,CAAC,CAAA;gBAED;;;;;mBAKG;gBACH,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;oBAC1B,MAAM,SAAS,GAAG,WAAW,CAAC,SAAS,CAAA;oBAEvC,WAAW,CAAC,SAAS,GAAG,KAAK,WAAW,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI;wBACjE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;wBAEtE,MAAM,SAAS,GAAG,MAAM,IAAA,qCAAe,EACrC,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAC3E,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,oBAAoB,CACrB,CAAA;wBAED,IAAI,SAAS,CAAC,SAAS,KAAK,KAAK,EAAE,CAAC;4BAClC,MAAM,IAAA,2CAAkB,EAAC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,OAAO,EAAE,cAAc,CAAC,CAAA;wBAC3F,CAAC;wBAED,OAAO,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;oBAChE,CAAC,CAAA;gBACH,CAAC;gBAED,OAAO,WAAW,CAAA;YACpB,CAAC;QACH,CAAC;KACF,CAAC,CAAA;IAEF;;;;OAIG;IACH,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,GAAG,QAAQ,CAAC,MAAM,+EAA+E;YAC/F,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAC9C,iGAAiG;YACjG,+FAA+F;YAC/F,8DAA8D,CACjE,CAAA;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AApOY,QAAA,0BAA0B,8BAoOtC;AAED;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAA;AAElC,SAAS,cAAc,CAAC,WAAgB,EAAE,KAAa;IACrD,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACnB,YAAM,CAAC,KAAK,CACV,GAAG,KAAK,4FAA4F,CACrG,CAAA;QACH,CAAC;QAED,OAAO,IAAI,sBAAY,CAAC,GAAG,KAAK,8CAA8C,EAAE;YAC9E,UAAU,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,6BAA6B,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;SAC9G,CAAC,CAAA;IACJ,CAAC,CAAA;IAED,WAAW,CAAC,OAAO,GAAG,KAAK,IAAI,EAAE;QAC/B,MAAM,MAAM,EAAE,CAAA;IAChB,CAAC,CAAA;IAED,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;QAC1B,WAAW,CAAC,SAAS,GAAG,KAAK,IAAI,EAAE;YACjC,MAAM,MAAM,EAAE,CAAA;QAChB,CAAC,CAAA;IACH,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAAqB,EAAE,QAAgB;IACzD,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,MAAM,CAAC,eAAe,EAAE,EAAE,MAAM,CAAC,mBAAmB,EAAE,CAAC,CAAC,IAAI,CACzF,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ,CAChC,CAAA;AACH,CAAC","sourcesContent":["import { defaultFieldResolver, GraphQLError, GraphQLSchema } from 'graphql'\nimport gql from 'graphql-tag'\n\nimport { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'\nimport { logger } from '@things-factory/env'\n\nimport { judgePermission } from '../../utils/check-permission.js'\nimport { noteAuditAction } from '../audit/audit-recorder.js'\nimport { errorCodeOf, returnedCountOf, targetIdsOf } from '../audit/audit-target.js'\nimport { declaredAxes, isDeclaredAxis } from './privilege-axis.js'\nimport { privilegeRejection } from './privilege-rejection.js'\nimport {\n coverageOf,\n DeclaringModule,\n enforcementMode,\n EnforcementMode,\n judgeUndeclared,\n modulesFromLoader\n} from './open-field-rule.js'\n\nprocess['PRIVILEGES'] = {}\n\n/*\n * ── What each field asks for, by `Type.field` ─────────────────────────────────\n * `PRIVILEGES` above is the list of pairs this installation declares; it cannot answer \"what\n * does `Query.jobOrders` ask for\". Something that reaches a door by another entrance has to ask\n * exactly what the door asks — an AI tool is one (operato-twin ADR-0071 decision 2, ADR-0072\n * decision 5). Without this map each caller copied the pair by hand, and a copy drifts the day\n * the door's declaration changes.\n *\n * Three answers, and they are not the same:\n * - not in the map the schema has no such root field. A caller that named it has a typo,\n * and reading that as \"open\" is how a gate opens without an error.\n * - `null` the field exists and declares nothing. It is open.\n * - a gate what `checkPermission` is asked at that field.\n *\n * Every root field is recorded (Query / Mutation / Subscription) so the first two can be told\n * apart; a field of any other type is recorded only when it carries the directive.\n * Kept on `process` for the same reason `PRIVILEGES` is.\n */\nprocess['PRIVILEGE_OF_FIELD'] = {}\n\n/** What `checkPermission` is asked at a field — the same four things the directive passes it. */\nexport interface FieldGate {\n category?: string\n privilege?: string\n owner: boolean\n super: boolean\n}\n\n/**\n * What the field `Type.field` asks for. Valid once the schema has been built.\n *\n * `known: false` means the schema has no such root field — treat it as a mistake, never as open.\n */\nexport function privilegeOfField(typeName: string, fieldName: string): { known: false } | { known: true; gate: FieldGate | null } {\n const map = process['PRIVILEGE_OF_FIELD'] as Record<string, FieldGate | null>\n const where = `${typeName}.${fieldName}`\n return where in map ? { known: true, gate: map[where] } : { known: false }\n}\n\nconst DIRECTIVE = 'privilege'\n\n/**\n * ⚠ **다음에 목록 인자를 붙이는 사람에게.**\n *\n * 여기 `legacy` 인자가 있었다 — 권한 이름을 옮기는 동안 옛 이름도 받아 주는 별칭이었다.\n * **걷어냈다.** 이 저장소는 대개조 중이고 **호환 계층을 끼우지 않는다**: 이름을 바꾸면 바꾼\n * 것이고, 부여는 관리자가 다시 놓는다. 별칭은 「한동안」이 영원이 되는 길이었다.\n *\n * 곧 필요해지는 목록 인자는 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서\n * 당겨오면서 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다**\n * 요구해야 한다.\n *\n * 그것을 `privileges: [...]` 처럼 중립적으로 이름 지으면 다음 사람이 「이 중 하나」로 읽고,\n * 그렇게 쓰는 날 게이트가 **오류 없이** 열린다. `requiresAll: [...]` 처럼 **이름이 AND 라고\n * 말하게** 한다.\n */\nexport const privilegeDirectiveTypeDefs = gql`\n directive @privilege(\n category: String\n privilege: String\n domainOwnerGranted: Boolean\n superUserGranted: Boolean\n ) on FIELD_DEFINITION\n`\n/**\n * The server calls this with the merged schema only. `modules` and `mode` are for tests, which\n * have no module walk of their own (`orderedModuleNames` is empty under jest) — see `open-fields.ts`.\n */\nexport const privilegeDirectiveResolver = (\n schema: GraphQLSchema,\n options: { modules?: DeclaringModule[]; mode?: EnforcementMode } = {}\n) => {\n const coverage = coverageOf(options.modules ?? modulesFromLoader())\n const mode = options.mode ?? enforcementMode()\n const unlisted: string[] = []\n\n const mapped = mapSchema(schema, {\n [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => {\n const privilegeDirective = getDirective(schema, fieldConfig, DIRECTIVE)?.[0]\n\n /* A root field with no directive is recorded as open, so \"open\" and \"no such field\" differ. */\n if (!privilegeDirective && isRootType(schema, typeName)) {\n const where = `${typeName}.${fieldName}`\n\n process['PRIVILEGE_OF_FIELD'][where] = null\n\n /*\n * ── The field that arrived without saying anything ───────────────────\n * Its module opted in (declared `openFields`) and names it in neither list. That is how\n * `deleteAppBinding` went in, and the whole point of §8 step 1 is that it cannot happen\n * quietly any more. A field of a module that has not opted in is left exactly as before.\n */\n if (judgeUndeclared(where, coverage) === 'unlisted') {\n if (mode === 'boot') {\n unlisted.push(where)\n } else {\n refuseUnlisted(fieldConfig, where)\n }\n }\n }\n\n if (privilegeDirective) {\n const { resolve = defaultFieldResolver, args } = fieldConfig\n\n if (!args) {\n throw new Error(`Unexpected Error. args should be defined in @privilege directive for field ${fieldName}.`)\n }\n\n const { domainOwnerGranted, superUserGranted, category, privilege } = privilegeDirective\n const where = `${typeName}.${fieldName}`\n\n /*\n * The audit's name for this door (ADR-0141 §3.2), and what the field says it acts on (§3.3).\n * A door that asks ownership only is named by the ownership it asks.\n */\n const door =\n category && privilege\n ? `${category}:${privilege}`\n : [domainOwnerGranted && 'owner', superUserGranted && 'super'].filter(Boolean).join('+')\n const target = getDirective(schema, fieldConfig, 'auditTarget')?.[0] as { kind: string; arg: string } | undefined\n\n /*\n * A declaration has to name something.\n *\n * `category` and `privilege` are one thing, not two: together they name a row in the\n * privilege table, and either alone names no row, so `User.hasPrivilege` is never\n * reached and the field falls back on ownership flags it did not set.\n */\n if (!category !== !privilege) {\n throw new Error(\n `@privilege on ${where} gives ${category ? 'a category with no privilege' : 'a privilege with no category'}. ` +\n `The pair names the privilege to look for; one half names none.`\n )\n }\n\n /*\n * `@privilege()` with nothing in it reads like a gate and guards nothing. Until\n * 2026-09-15 what it did depended on where the caller sat: `checkPermission` answered\n * false from a safe address and refused everyone, true from an unsafe one and let\n * everyone through. Both are false now, so the shape is merely useless rather than\n * dangerous — and a field that is open should say so by carrying no directive, not by\n * carrying an empty one.\n *\n * It is also the shape someone reaches for wanting \"being signed in is enough\". That\n * is a real thing to want and this directive does not express it; the answer settled\n * on 2026-09-15 is a category for the published side of a module, `<module>-published`.\n *\n * Throwing stops the boot, which is what §6-2 of docs/design/auth-rebuild.md settled\n * for a resolver that declares nothing. A decorator that declares nothing is that\n * same resolver wearing one.\n */\n if (!category && !privilege && !domainOwnerGranted && !superUserGranted) {\n throw new Error(\n `@privilege on ${where} declares nothing. ` +\n `Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +\n `ownership gate. A field open to everyone carries no directive at all.`\n )\n }\n\n /*\n * The axis has to be one this installation declared.\n *\n * Without this, `privilege: \"mutaiton\"` registered a pair nobody would ever be granted\n * — a gate shut forever, with no error to read. The list is not the framework's: see\n * `privilege-axis.ts` for why, and for the fact that it must be declared at module\n * import time rather than on a bootstrap hook.\n */\n if (privilege && !isDeclaredAxis(privilege)) {\n throw new Error(\n `@privilege on ${where} uses axis \"${privilege}\", which this installation does not declare. ` +\n `Declared axes: ${[...declaredAxes()].sort().join(', ')}. ` +\n `Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +\n `server/index.ts — a bootstrap hook runs after the schema is built and is too late.`\n )\n }\n\n if (category && privilege) {\n process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege]\n }\n\n /* After every refusal above, so a declaration that names nothing never lands here. */\n process['PRIVILEGE_OF_FIELD'][where] = {\n category: category || undefined,\n privilege: privilege || undefined,\n owner: !!domainOwnerGranted,\n super: !!superUserGranted\n } satisfies FieldGate\n\n // 필드의 기존 description 가져오기\n const existingDescription = fieldConfig.description || ''\n\n // 권한 정보를 포함한 새로운 description 생성\n const privilegeDescription =\n `\\n\\n🔒 Requires privilege: ${category}:${privilege}` +\n (domainOwnerGranted ? ', Domain ownership' : '') +\n (superUserGranted ? ', System ownership' : '')\n\n // 기존 description과 결합\n fieldConfig.description = `${existingDescription} ${privilegeDescription}`.trim()\n\n fieldConfig.resolve = async function (source, args, context, info) {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n const judgement = await judgePermission(\n {\n category,\n privilege,\n owner: domainOwnerGranted,\n super: superUserGranted\n },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n const targetKind = target?.kind\n const targetIds = target ? targetIdsOf(args, target.arg) : undefined\n const targetRole = target ? ('requested' as const) : undefined\n\n if (judgement.permitted === true) {\n noteAuditAction({ kind: 'door.check', door, decision: 'granted', basis: judgement.by, targetKind, targetIds, targetRole })\n\n /* Passing the door is not doing the work - what the work came to is its own row (§3.2). */\n try {\n const result = await resolve.call(this, source, args, context, info)\n noteAuditAction({ kind: 'door.result', door, outcome: 'ok', returned: returnedCountOf(result), targetKind, targetIds, targetRole })\n return result\n } catch (error) {\n noteAuditAction({ kind: 'door.result', door, outcome: 'error', code: errorCodeOf(error), targetKind, targetIds, targetRole })\n throw error\n }\n }\n\n noteAuditAction({ kind: 'door.check', door, decision: 'denied', basis: judgement.because, targetKind, targetIds, targetRole })\n\n /*\n * 안 열렸으면 **거기서 끝이다.**\n *\n * 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지\n * 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.\n *\n * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.\n */\n throw privilegeRejection(context, category, privilege, judgement.because, info?.operation?.operation)\n }\n\n /*\n * ── A subscription is opened by `subscribe`, not `resolve` ────────────\n * Wrapping only `resolve` left a declared subscription connectable by anyone: the stream\n * opened and every event then failed. It leaked nothing, and it refused nothing either.\n * The same question is asked where the stream starts.\n */\n if (fieldConfig.subscribe) {\n const subscribe = fieldConfig.subscribe\n\n fieldConfig.subscribe = async function (source, args, context, info) {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n const judgement = await judgePermission(\n { category, privilege, owner: domainOwnerGranted, super: superUserGranted },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n if (judgement.permitted === false) {\n throw privilegeRejection(context, category, privilege, judgement.because, 'subscription')\n }\n\n return await subscribe.call(this, source, args, context, info)\n }\n }\n\n return fieldConfig\n }\n }\n })\n\n /*\n * Every unlisted field at once, rather than the first one. The person who meets this has usually\n * added several resolvers together, and fixing them one boot at a time is how a list gets\n * abandoned half done.\n */\n if (unlisted.length) {\n throw new Error(\n `${unlisted.length} field(s) declare no @privilege and are not listed as open by their module:\\n` +\n unlisted.map(where => ` ${where}`).join('\\n') +\n `\\n\\nFor each, either give it @privilege(category: …, privilege: …), or name it in the module's ` +\n `schema.openFields — under byDesign with the reason it is open, or under notYetDeclared if it ` +\n `is waiting to be closed. See docs/design/auth-rebuild.md §8.`\n )\n }\n\n return mapped\n}\n\n/**\n * Production's answer to an unlisted field: that field refuses, and says so in the log once.\n *\n * The server stays up. What cannot happen is the field answering — it arrived without saying who\n * may call it, and the answer to that is nobody until somebody says.\n */\nconst reported = new Set<string>()\n\nfunction refuseUnlisted(fieldConfig: any, where: string) {\n const refuse = () => {\n if (!reported.has(where)) {\n reported.add(where)\n logger.error(\n `${where} declares no @privilege and is not listed in its module's schema.openFields — refusing it.`\n )\n }\n\n return new GraphQLError(`${where} is not available: it declares no privilege.`, {\n extensions: { code: 'FORBIDDEN', refusal: { code: 'field-declares-no-privilege', params: { field: where } } }\n })\n }\n\n fieldConfig.resolve = async () => {\n throw refuse()\n }\n\n if (fieldConfig.subscribe) {\n fieldConfig.subscribe = async () => {\n throw refuse()\n }\n }\n}\n\nfunction isRootType(schema: GraphQLSchema, typeName: string): boolean {\n return [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].some(\n root => root?.name === typeName\n )\n}\n"]}
1
+ {"version":3,"file":"privilege-directive.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-directive.ts"],"names":[],"mappings":"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","sourcesContent":["import { defaultFieldResolver, GraphQLError, GraphQLSchema } from 'graphql'\nimport gql from 'graphql-tag'\n\nimport { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'\nimport { logger } from '@things-factory/env'\n\nimport { judgePermission } from '../../utils/check-permission.js'\nimport { noteAuditAction } from '../audit/audit-recorder.js'\nimport { errorCodeOf, returnedCountOf, targetIdsOf } from '../audit/audit-target.js'\nimport { confirmationRefusal, isRecentlyConfirmed } from '../identity-confirmation/identity-confirmation.js'\nimport { declaredAxes, isDeclaredAxis } from './privilege-axis.js'\nimport { privilegeRejection } from './privilege-rejection.js'\nimport {\n coverageOf,\n DeclaringModule,\n enforcementMode,\n EnforcementMode,\n judgeUndeclared,\n modulesFromLoader\n} from './open-field-rule.js'\n\nprocess['PRIVILEGES'] = {}\n\n/*\n * ── What each field asks for, by `Type.field` ─────────────────────────────────\n * `PRIVILEGES` above is the list of pairs this installation declares; it cannot answer \"what\n * does `Query.jobOrders` ask for\". Something that reaches a door by another entrance has to ask\n * exactly what the door asks — an AI tool is one (operato-twin ADR-0071 decision 2, ADR-0072\n * decision 5). Without this map each caller copied the pair by hand, and a copy drifts the day\n * the door's declaration changes.\n *\n * Three answers, and they are not the same:\n * - not in the map the schema has no such root field. A caller that named it has a typo,\n * and reading that as \"open\" is how a gate opens without an error.\n * - `null` the field exists and declares nothing. It is open.\n * - a gate what `checkPermission` is asked at that field.\n *\n * Every root field is recorded (Query / Mutation / Subscription) so the first two can be told\n * apart; a field of any other type is recorded only when it carries the directive.\n * Kept on `process` for the same reason `PRIVILEGES` is.\n */\nprocess['PRIVILEGE_OF_FIELD'] = {}\n\n/** What `checkPermission` is asked at a field — the same four things the directive passes it. */\nexport interface FieldGate {\n category?: string\n privilege?: string\n owner: boolean\n super: boolean\n}\n\n/**\n * What the field `Type.field` asks for. Valid once the schema has been built.\n *\n * `known: false` means the schema has no such root field — treat it as a mistake, never as open.\n */\nexport function privilegeOfField(typeName: string, fieldName: string): { known: false } | { known: true; gate: FieldGate | null } {\n const map = process['PRIVILEGE_OF_FIELD'] as Record<string, FieldGate | null>\n const where = `${typeName}.${fieldName}`\n return where in map ? { known: true, gate: map[where] } : { known: false }\n}\n\nconst DIRECTIVE = 'privilege'\n\n/**\n * ⚠ **다음에 목록 인자를 붙이는 사람에게.**\n *\n * 여기 `legacy` 인자가 있었다 — 권한 이름을 옮기는 동안 옛 이름도 받아 주는 별칭이었다.\n * **걷어냈다.** 이 저장소는 대개조 중이고 **호환 계층을 끼우지 않는다**: 이름을 바꾸면 바꾼\n * 것이고, 부여는 관리자가 다시 놓는다. 별칭은 「한동안」이 영원이 되는 길이었다.\n *\n * 곧 필요해지는 목록 인자는 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서\n * 당겨오면서 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다**\n * 요구해야 한다.\n *\n * 그것을 `privileges: [...]` 처럼 중립적으로 이름 지으면 다음 사람이 「이 중 하나」로 읽고,\n * 그렇게 쓰는 날 게이트가 **오류 없이** 열린다. `requiresAll: [...]` 처럼 **이름이 AND 라고\n * 말하게** 한다.\n */\nexport const privilegeDirectiveTypeDefs = gql`\n directive @privilege(\n category: String\n privilege: String\n domainOwnerGranted: Boolean\n superUserGranted: Boolean\n ) on FIELD_DEFINITION\n`\n/**\n * The server calls this with the merged schema only. `modules` and `mode` are for tests, which\n * have no module walk of their own (`orderedModuleNames` is empty under jest) — see `open-fields.ts`.\n */\nexport const privilegeDirectiveResolver = (\n schema: GraphQLSchema,\n options: { modules?: DeclaringModule[]; mode?: EnforcementMode } = {}\n) => {\n const coverage = coverageOf(options.modules ?? modulesFromLoader())\n const mode = options.mode ?? enforcementMode()\n const unlisted: string[] = []\n\n const mapped = mapSchema(schema, {\n [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName, schema) => {\n const privilegeDirective = getDirective(schema, fieldConfig, DIRECTIVE)?.[0]\n /* A door that also asks for a recent confirmation that it is still the person (ADR-0168). */\n const asksConfirmation = !!getDirective(schema, fieldConfig, 'recentConfirmation')?.[0]\n\n if (asksConfirmation && !privilegeDirective) {\n throw new Error(\n `@recentConfirmation on ${typeName}.${fieldName} has no @privilege beside it. The confirmation is asked by ` +\n `the privilege gate, after the privilege - a door that asks only for it would ask nobody.`\n )\n }\n\n /* A root field with no directive is recorded as open, so \"open\" and \"no such field\" differ. */\n if (!privilegeDirective && isRootType(schema, typeName)) {\n const where = `${typeName}.${fieldName}`\n\n process['PRIVILEGE_OF_FIELD'][where] = null\n\n /*\n * ── The field that arrived without saying anything ───────────────────\n * Its module opted in (declared `openFields`) and names it in neither list. That is how\n * `deleteAppBinding` went in, and the whole point of §8 step 1 is that it cannot happen\n * quietly any more. A field of a module that has not opted in is left exactly as before.\n */\n if (judgeUndeclared(where, coverage) === 'unlisted') {\n if (mode === 'boot') {\n unlisted.push(where)\n } else {\n refuseUnlisted(fieldConfig, where)\n }\n }\n }\n\n if (privilegeDirective) {\n const { resolve = defaultFieldResolver, args } = fieldConfig\n\n if (!args) {\n throw new Error(`Unexpected Error. args should be defined in @privilege directive for field ${fieldName}.`)\n }\n\n const { domainOwnerGranted, superUserGranted, category, privilege } = privilegeDirective\n const where = `${typeName}.${fieldName}`\n\n /*\n * The audit's name for this door (ADR-0141 §3.2), and what the field says it acts on (§3.3).\n * A door that asks ownership only is named by the ownership it asks.\n */\n const door =\n category && privilege\n ? `${category}:${privilege}`\n : [domainOwnerGranted && 'owner', superUserGranted && 'super'].filter(Boolean).join('+')\n const target = getDirective(schema, fieldConfig, 'auditTarget')?.[0] as { kind: string; arg: string } | undefined\n\n /*\n * A declaration has to name something.\n *\n * `category` and `privilege` are one thing, not two: together they name a row in the\n * privilege table, and either alone names no row, so `User.hasPrivilege` is never\n * reached and the field falls back on ownership flags it did not set.\n */\n if (!category !== !privilege) {\n throw new Error(\n `@privilege on ${where} gives ${category ? 'a category with no privilege' : 'a privilege with no category'}. ` +\n `The pair names the privilege to look for; one half names none.`\n )\n }\n\n /*\n * `@privilege()` with nothing in it reads like a gate and guards nothing. Until\n * 2026-09-15 what it did depended on where the caller sat: `checkPermission` answered\n * false from a safe address and refused everyone, true from an unsafe one and let\n * everyone through. Both are false now, so the shape is merely useless rather than\n * dangerous — and a field that is open should say so by carrying no directive, not by\n * carrying an empty one.\n *\n * It is also the shape someone reaches for wanting \"being signed in is enough\". That\n * is a real thing to want and this directive does not express it; the answer settled\n * on 2026-09-15 is a category for the published side of a module, `<module>-published`.\n *\n * Throwing stops the boot, which is what §6-2 of docs/design/auth-rebuild.md settled\n * for a resolver that declares nothing. A decorator that declares nothing is that\n * same resolver wearing one.\n */\n if (!category && !privilege && !domainOwnerGranted && !superUserGranted) {\n throw new Error(\n `@privilege on ${where} declares nothing. ` +\n `Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +\n `ownership gate. A field open to everyone carries no directive at all.`\n )\n }\n\n /*\n * The axis has to be one this installation declared.\n *\n * Without this, `privilege: \"mutaiton\"` registered a pair nobody would ever be granted\n * — a gate shut forever, with no error to read. The list is not the framework's: see\n * `privilege-axis.ts` for why, and for the fact that it must be declared at module\n * import time rather than on a bootstrap hook.\n */\n if (privilege && !isDeclaredAxis(privilege)) {\n throw new Error(\n `@privilege on ${where} uses axis \"${privilege}\", which this installation does not declare. ` +\n `Declared axes: ${[...declaredAxes()].sort().join(', ')}. ` +\n `Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +\n `server/index.ts — a bootstrap hook runs after the schema is built and is too late.`\n )\n }\n\n if (category && privilege) {\n process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege]\n }\n\n /* After every refusal above, so a declaration that names nothing never lands here. */\n process['PRIVILEGE_OF_FIELD'][where] = {\n category: category || undefined,\n privilege: privilege || undefined,\n owner: !!domainOwnerGranted,\n super: !!superUserGranted\n } satisfies FieldGate\n\n // 필드의 기존 description 가져오기\n const existingDescription = fieldConfig.description || ''\n\n // 권한 정보를 포함한 새로운 description 생성\n const privilegeDescription =\n `\\n\\n🔒 Requires privilege: ${category}:${privilege}` +\n (domainOwnerGranted ? ', Domain ownership' : '') +\n (superUserGranted ? ', System ownership' : '') +\n (asksConfirmation ? ', a recent confirmation that it is the person' : '')\n\n // 기존 description과 결합\n fieldConfig.description = `${existingDescription} ${privilegeDescription}`.trim()\n\n fieldConfig.resolve = async function (source, args, context, info) {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n const judgement = await judgePermission(\n {\n category,\n privilege,\n owner: domainOwnerGranted,\n super: superUserGranted\n },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n const targetKind = target?.kind\n const targetIds = target ? targetIdsOf(args, target.arg) : undefined\n const targetRole = target ? ('requested' as const) : undefined\n\n if (judgement.permitted === true) {\n /*\n * The privilege is the person's; the confirmation says the person is the one at the screen (ADR-0168).\n * Passing does not lengthen it - it ends ten minutes after it was given.\n */\n if (asksConfirmation && !isRecentlyConfirmed(context, user?.id)) {\n noteAuditAction({ kind: 'door.check', door, decision: 'denied', basis: 'identity-confirmation', targetKind, targetIds, targetRole })\n throw confirmationRefusal(context)\n }\n\n noteAuditAction({ kind: 'door.check', door, decision: 'granted', basis: judgement.by, targetKind, targetIds, targetRole })\n\n /* Passing the door is not doing the work - what the work came to is its own row (§3.2). */\n try {\n const result = await resolve.call(this, source, args, context, info)\n noteAuditAction({ kind: 'door.result', door, outcome: 'ok', returned: returnedCountOf(result), targetKind, targetIds, targetRole })\n return result\n } catch (error) {\n noteAuditAction({ kind: 'door.result', door, outcome: 'error', code: errorCodeOf(error), targetKind, targetIds, targetRole })\n throw error\n }\n }\n\n noteAuditAction({ kind: 'door.check', door, decision: 'denied', basis: judgement.because, targetKind, targetIds, targetRole })\n\n /*\n * 안 열렸으면 **거기서 끝이다.**\n *\n * 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지\n * 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.\n *\n * 문장 만들기는 `privilege-rejection.ts` 가 소유한다.\n */\n throw privilegeRejection(context, category, privilege, judgement.because, info?.operation?.operation)\n }\n\n /*\n * ── A subscription is opened by `subscribe`, not `resolve` ────────────\n * Wrapping only `resolve` left a declared subscription connectable by anyone: the stream\n * opened and every event then failed. It leaked nothing, and it refused nothing either.\n * The same question is asked where the stream starts.\n */\n if (fieldConfig.subscribe) {\n const subscribe = fieldConfig.subscribe\n\n fieldConfig.subscribe = async function (source, args, context, info) {\n const { domain, user, unsafeIP, prohibitedPrivileges } = context.state\n\n const judgement = await judgePermission(\n { category, privilege, owner: domainOwnerGranted, super: superUserGranted },\n user,\n domain,\n unsafeIP,\n prohibitedPrivileges\n )\n\n if (judgement.permitted === false) {\n throw privilegeRejection(context, category, privilege, judgement.because, 'subscription')\n }\n\n return await subscribe.call(this, source, args, context, info)\n }\n }\n\n return fieldConfig\n }\n }\n })\n\n /*\n * Every unlisted field at once, rather than the first one. The person who meets this has usually\n * added several resolvers together, and fixing them one boot at a time is how a list gets\n * abandoned half done.\n */\n if (unlisted.length) {\n throw new Error(\n `${unlisted.length} field(s) declare no @privilege and are not listed as open by their module:\\n` +\n unlisted.map(where => ` ${where}`).join('\\n') +\n `\\n\\nFor each, either give it @privilege(category: …, privilege: …), or name it in the module's ` +\n `schema.openFields — under byDesign with the reason it is open, or under notYetDeclared if it ` +\n `is waiting to be closed. See docs/design/auth-rebuild.md §8.`\n )\n }\n\n return mapped\n}\n\n/**\n * Production's answer to an unlisted field: that field refuses, and says so in the log once.\n *\n * The server stays up. What cannot happen is the field answering — it arrived without saying who\n * may call it, and the answer to that is nobody until somebody says.\n */\nconst reported = new Set<string>()\n\nfunction refuseUnlisted(fieldConfig: any, where: string) {\n const refuse = () => {\n if (!reported.has(where)) {\n reported.add(where)\n logger.error(\n `${where} declares no @privilege and is not listed in its module's schema.openFields — refusing it.`\n )\n }\n\n return new GraphQLError(`${where} is not available: it declares no privilege.`, {\n extensions: { code: 'FORBIDDEN', refusal: { code: 'field-declares-no-privilege', params: { field: where } } }\n })\n }\n\n fieldConfig.resolve = async () => {\n throw refuse()\n }\n\n if (fieldConfig.subscribe) {\n fieldConfig.subscribe = async () => {\n throw refuse()\n }\n }\n}\n\nfunction isRootType(schema: GraphQLSchema, typeName: string): boolean {\n return [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].some(\n root => root?.name === typeName\n )\n}\n"]}
@@ -155,9 +155,7 @@ let RoleMutation = class RoleMutation {
155
155
  throw new Error(context.t('error.failed to find x', { x: context.t('label.role') }) +
156
156
  ' — a role defined in a parent domain can only be deleted in that domain.');
157
157
  }
158
- if (role.users.length) {
159
- throw new Error('cannot delete role because it is currently in use');
160
- }
158
+ refuseRolesInUse([role]);
161
159
  try {
162
160
  role.privileges = [];
163
161
  role = await tx.getRepository(role_js_1.Role).save(role);
@@ -178,14 +176,17 @@ let RoleMutation = class RoleMutation {
178
176
  * 사람이 남의 도메인 역할을 id 만으로 지울 수 있었다. `deleteRole`(단수)은 처음부터
179
177
  * 도메인으로 좁혀 찾았으므로, 둘의 규칙이 달랐다.
180
178
  *
181
- * 그리고 **사용 중인 역할은 남긴다** — 단수 쪽과 같은 규칙이다. 지운 것과 남은 것을 세어
182
- * 돌려주지는 않는다(반환형이 `Boolean` 이다). 목록이 다시 읽으면 무엇이 남았는지 보인다.
179
+ * 그리고 **사용 중인 역할이 하나라도 있으면 아무것도 지우지 않고 그 이름으로 거절한다** — 단수
180
+ * 쪽과 같은 규칙이다. 예전에는 사용 중인 것만 조용히 남기고 `true` 를 돌려주어, 관리자는
181
+ * 무엇이 왜 안 지워졌는지 목록을 다시 보고 짐작해야 했다(2026-10-28 사용자: 「역할이 참조되는
182
+ * 것이 있으면, 삭제가 안되어야하니」).
183
183
  */
184
184
  const roles = await (0, shell_1.getRepository)(role_js_1.Role).find({
185
185
  where: { domain: { id: domain.id }, id: (0, typeorm_1.In)(ids) },
186
186
  relations: ['users']
187
187
  });
188
- const deletable = roles.filter(role => !role.users?.length).map(role => role.id);
188
+ refuseRolesInUse(roles);
189
+ const deletable = roles.map(role => role.id);
189
190
  if (deletable.length) {
190
191
  /* The deletion and its record stand or fall together (ADR-0141 §4-1). */
191
192
  await (0, shell_1.getRepository)(role_js_1.Role).manager.transaction(async (manager) => {
@@ -249,4 +250,15 @@ exports.RoleMutation = RoleMutation = tslib_1.__decorate([
249
250
  (0, type_graphql_1.Resolver)(role_js_1.Role),
250
251
  (0, type_graphql_1.UseMiddleware)(shell_1.SpeakRefusals)
251
252
  ], RoleMutation);
253
+ /**
254
+ * A role someone holds is not deleted - the people holding it would lose what it opens without anyone deciding so.
255
+ * One refusal for one role or many, naming each and how many hold it, so the screen can say what to take off first.
256
+ */
257
+ function refuseRolesInUse(roles) {
258
+ const held = roles.filter(role => role.users?.length);
259
+ if (!held.length)
260
+ return;
261
+ const names = held.map(role => `${role.name} (${role.users.length})`).join(', ');
262
+ throw new shell_1.Refusal('role-in-use', { names }, `These roles are held by users and are currently in use: ${names}. Take them off the users first.`);
263
+ }
252
264
  //# sourceMappingURL=role-mutation.js.map