@things-factory/auth-base 10.1.218 → 10.1.226

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 (52) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/controllers/confirm-identity.d.ts +8 -0
  3. package/dist-server/controllers/confirm-identity.js +33 -0
  4. package/dist-server/controllers/confirm-identity.js.map +1 -0
  5. package/dist-server/controllers/signin.d.ts +5 -0
  6. package/dist-server/controllers/signin.js +38 -31
  7. package/dist-server/controllers/signin.js.map +1 -1
  8. package/dist-server/router/webauthn-router.js +20 -1
  9. package/dist-server/router/webauthn-router.js.map +1 -1
  10. package/dist-server/service/audit/audit-kinds.js +2 -0
  11. package/dist-server/service/audit/audit-kinds.js.map +1 -1
  12. package/dist-server/service/identity-confirmation/identity-confirmation.d.ts +18 -0
  13. package/dist-server/service/identity-confirmation/identity-confirmation.js +84 -0
  14. package/dist-server/service/identity-confirmation/identity-confirmation.js.map +1 -0
  15. package/dist-server/service/index.d.ts +1 -0
  16. package/dist-server/service/index.js +3 -1
  17. package/dist-server/service/index.js.map +1 -1
  18. package/dist-server/service/privilege/privilege-directive.js +17 -1
  19. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  20. package/dist-server/service/role/role-mutation.js +5 -0
  21. package/dist-server/service/role/role-mutation.js.map +1 -1
  22. package/dist-server/service/role/role-types.d.ts +1 -0
  23. package/dist-server/service/role/role-types.js +4 -0
  24. package/dist-server/service/role/role-types.js.map +1 -1
  25. package/dist-server/service/role/role.d.ts +10 -0
  26. package/dist-server/service/role/role.js +5 -0
  27. package/dist-server/service/role/role.js.map +1 -1
  28. package/dist-server/service/role-template/role-template-gap-resolver.d.ts +2 -2
  29. package/dist-server/service/role-template/role-template-gap-resolver.js +7 -13
  30. package/dist-server/service/role-template/role-template-gap-resolver.js.map +1 -1
  31. package/dist-server/service/role-template/role-template-mutation.d.ts +7 -4
  32. package/dist-server/service/role-template/role-template-mutation.js +15 -15
  33. package/dist-server/service/role-template/role-template-mutation.js.map +1 -1
  34. package/dist-server/service/role-template/role-template-types.d.ts +3 -3
  35. package/dist-server/service/role-template/role-template-types.js.map +1 -1
  36. package/dist-server/service/role-template/role-template.d.ts +14 -15
  37. package/dist-server/service/role-template/role-template.js +8 -8
  38. package/dist-server/service/role-template/role-template.js.map +1 -1
  39. package/dist-server/tsconfig.tsbuildinfo +1 -1
  40. package/dist-server/utils/access-token-cookie.js +3 -0
  41. package/dist-server/utils/access-token-cookie.js.map +1 -1
  42. package/package.json +2 -2
  43. package/tests/confirm-identity-db.test.ts +85 -0
  44. package/tests/identity-confirmation.test.ts +146 -0
  45. package/tests/role-template-gap-db.test.ts +58 -7
  46. package/tests/role-template-seed-db.test.ts +5 -4
  47. package/translations/en.json +2 -1
  48. package/translations/es.json +2 -1
  49. package/translations/ja.json +2 -1
  50. package/translations/ko.json +2 -1
  51. package/translations/ms.json +2 -1
  52. package/translations/zh.json +2 -1
@@ -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":"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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 { 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":"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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 { 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"]}
@@ -7,6 +7,7 @@ const type_graphql_1 = require("type-graphql");
7
7
  const shell_1 = require("@things-factory/shell");
8
8
  const refuse_role_name_js_1 = require("../../utils/refuse-role-name.js");
9
9
  const privilege_js_1 = require("../privilege/privilege.js");
10
+ const role_template_js_1 = require("../role-template/role-template.js");
10
11
  const user_js_1 = require("../user/user.js");
11
12
  const role_js_1 = require("./role.js");
12
13
  const audit_recorder_js_1 = require("../audit/audit-recorder.js");
@@ -67,6 +68,10 @@ let RoleMutation = class RoleMutation {
67
68
  if (description) {
68
69
  role.description = description;
69
70
  }
71
+ /* A base template is one this installation declares (ADR-0075 decision 1, amended 2026-10-26). */
72
+ if (patch.templateId && !(0, role_template_js_1.allRoleTemplates)().some(template => template.id === patch.templateId)) {
73
+ throw new Error(`There is no role template "${patch.templateId}" in this installation.`);
74
+ }
70
75
  return await roleRepository.save({
71
76
  ...role,
72
77
  ...patch,