@things-factory/auth-base 10.1.188 → 10.1.198

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 (105) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/controllers/signin.d.ts +5 -0
  3. package/dist-server/controllers/signin.js +25 -0
  4. package/dist-server/controllers/signin.js.map +1 -1
  5. package/dist-server/controllers/verification.d.ts +5 -1
  6. package/dist-server/controllers/verification.js +11 -6
  7. package/dist-server/controllers/verification.js.map +1 -1
  8. package/dist-server/index.js +7 -0
  9. package/dist-server/index.js.map +1 -1
  10. package/dist-server/middlewares/audit-scope-middleware.d.ts +7 -0
  11. package/dist-server/middlewares/audit-scope-middleware.js +15 -0
  12. package/dist-server/middlewares/audit-scope-middleware.js.map +1 -0
  13. package/dist-server/middlewares/authenticate-401-middleware.js +4 -0
  14. package/dist-server/middlewares/authenticate-401-middleware.js.map +1 -1
  15. package/dist-server/middlewares/index.js +3 -0
  16. package/dist-server/middlewares/index.js.map +1 -1
  17. package/dist-server/role-tasks.d.ts +2 -0
  18. package/dist-server/role-tasks.js +20 -0
  19. package/dist-server/role-tasks.js.map +1 -0
  20. package/dist-server/router/auth-checkin-router.js +3 -5
  21. package/dist-server/router/auth-checkin-router.js.map +1 -1
  22. package/dist-server/router/auth-public-process-router.js +26 -14
  23. package/dist-server/router/auth-public-process-router.js.map +1 -1
  24. package/dist-server/router/site-root-router.d.ts +2 -0
  25. package/dist-server/router/site-root-router.js +2 -0
  26. package/dist-server/router/site-root-router.js.map +1 -1
  27. package/dist-server/service/access-request/index.d.ts +1 -1
  28. package/dist-server/service/audit/audit-action.d.ts +36 -0
  29. package/dist-server/service/audit/audit-action.js +137 -0
  30. package/dist-server/service/audit/audit-action.js.map +1 -0
  31. package/dist-server/service/audit/audit-kinds.d.ts +19 -0
  32. package/dist-server/service/audit/audit-kinds.js +70 -0
  33. package/dist-server/service/audit/audit-kinds.js.map +1 -0
  34. package/dist-server/service/audit/audit-query.d.ts +11 -0
  35. package/dist-server/service/audit/audit-query.js +295 -0
  36. package/dist-server/service/audit/audit-query.js.map +1 -0
  37. package/dist-server/service/audit/audit-recorder.d.ts +77 -0
  38. package/dist-server/service/audit/audit-recorder.js +247 -0
  39. package/dist-server/service/audit/audit-recorder.js.map +1 -0
  40. package/dist-server/service/audit/audit-request.d.ts +28 -0
  41. package/dist-server/service/audit/audit-request.js +96 -0
  42. package/dist-server/service/audit/audit-request.js.map +1 -0
  43. package/dist-server/service/audit/audit-retention.d.ts +12 -0
  44. package/dist-server/service/audit/audit-retention.js +42 -0
  45. package/dist-server/service/audit/audit-retention.js.map +1 -0
  46. package/dist-server/service/audit/audit-subscriber.d.ts +39 -0
  47. package/dist-server/service/audit/audit-subscriber.js +191 -0
  48. package/dist-server/service/audit/audit-subscriber.js.map +1 -0
  49. package/dist-server/service/audit/audit-target.d.ts +19 -0
  50. package/dist-server/service/audit/audit-target.js +46 -0
  51. package/dist-server/service/audit/audit-target.js.map +1 -0
  52. package/dist-server/service/audit/audit-types.d.ts +44 -0
  53. package/dist-server/service/audit/audit-types.js +161 -0
  54. package/dist-server/service/audit/audit-types.js.map +1 -0
  55. package/dist-server/service/audit/index.d.ts +6 -0
  56. package/dist-server/service/audit/index.js +11 -0
  57. package/dist-server/service/audit/index.js.map +1 -0
  58. package/dist-server/service/domain-owner/domain-owner-mutation.js +3 -0
  59. package/dist-server/service/domain-owner/domain-owner-mutation.js.map +1 -1
  60. package/dist-server/service/index.d.ts +9 -2
  61. package/dist-server/service/index.js +19 -4
  62. package/dist-server/service/index.js.map +1 -1
  63. package/dist-server/service/open-fields.js +1 -1
  64. package/dist-server/service/open-fields.js.map +1 -1
  65. package/dist-server/service/privilege/privilege-directive.js +25 -1
  66. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  67. package/dist-server/service/role/role-mutation.js +8 -1
  68. package/dist-server/service/role/role-mutation.js.map +1 -1
  69. package/dist-server/service/role-template/index.d.ts +1 -1
  70. package/dist-server/service/role-template/role-task.d.ts +74 -0
  71. package/dist-server/service/role-template/role-task.js +113 -0
  72. package/dist-server/service/role-template/role-task.js.map +1 -0
  73. package/dist-server/service/role-template/role-template-gap-resolver.d.ts +9 -5
  74. package/dist-server/service/role-template/role-template-gap-resolver.js +41 -13
  75. package/dist-server/service/role-template/role-template-gap-resolver.js.map +1 -1
  76. package/dist-server/service/role-template/role-template-mutation.js +10 -1
  77. package/dist-server/service/role-template/role-template-mutation.js.map +1 -1
  78. package/dist-server/service/role-template/role-template-query.js +4 -2
  79. package/dist-server/service/role-template/role-template-query.js.map +1 -1
  80. package/dist-server/service/role-template/role-template-types.d.ts +24 -0
  81. package/dist-server/service/role-template/role-template-types.js +63 -1
  82. package/dist-server/service/role-template/role-template-types.js.map +1 -1
  83. package/dist-server/service/role-template/role-template.d.ts +63 -10
  84. package/dist-server/service/role-template/role-template.js +92 -18
  85. package/dist-server/service/role-template/role-template.js.map +1 -1
  86. package/dist-server/tsconfig.tsbuildinfo +1 -1
  87. package/dist-server/utils/check-permission.d.ts +6 -0
  88. package/dist-server/utils/check-permission.js +23 -8
  89. package/dist-server/utils/check-permission.js.map +1 -1
  90. package/package.json +3 -2
  91. package/tests/activate-page-case.test.ts +53 -0
  92. package/tests/audit-db.test.ts +394 -0
  93. package/tests/auth-base-role-tasks.test.ts +58 -0
  94. package/tests/list-inheritance-db.test.ts +0 -7
  95. package/tests/mail-request-answers-db.test.ts +139 -0
  96. package/tests/open-doors.test.ts +2 -2
  97. package/tests/refusal-cause.test.ts +1 -1
  98. package/tests/role-task.test.ts +168 -0
  99. package/tests/role-template-gap-db.test.ts +77 -2
  100. package/translations/en.json +16 -3
  101. package/translations/es.json +16 -3
  102. package/translations/ja.json +16 -3
  103. package/translations/ko.json +16 -3
  104. package/translations/ms.json +16 -3
  105. package/translations/zh.json +16 -3
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerRoleTask = registerRoleTask;
4
+ exports.roleTasks = roleTasks;
5
+ exports.roleTaskById = roleTaskById;
6
+ exports.roleTaskModule = roleTaskModule;
7
+ exports.mergeGrants = mergeGrants;
8
+ exports.privilegeKey = privilegeKey;
9
+ exports.undeclaredTaskGrants = undeclaredTaskGrants;
10
+ exports.taskCoverageOf = taskCoverageOf;
11
+ const env_1 = require("@things-factory/env");
12
+ const REGISTRY = 'ROLE_TASKS';
13
+ process[REGISTRY] = {};
14
+ function registry() {
15
+ return process[REGISTRY];
16
+ }
17
+ /**
18
+ * Declares a task. Call it from the module's server entry, beside its `@privilege` doors.
19
+ *
20
+ * Privileges are not checked here, for the reason templates are not: modules load before the schema is built and
21
+ * `process['PRIVILEGES']` is filled. Boot checks them (`reportRoleDefinitionErrors`).
22
+ */
23
+ function registerRoleTask(task) {
24
+ const prefix = task?.id?.split('.')[0];
25
+ if (!task?.id || !prefix || prefix === task.id) {
26
+ throw new Error(`A role task needs an id of the form <module>.<work>, the declaring module first — "${task?.id ?? ''}" is not.`);
27
+ }
28
+ if (!Array.isArray(task.grants) || task.grants.length === 0) {
29
+ throw new Error(`Role task "${task.id}" holds no privileges. A task is the privileges a piece of work takes; one that takes none is not a task.`);
30
+ }
31
+ for (const grant of task.grants) {
32
+ if (!grant?.category || !Array.isArray(grant.axes) || grant.axes.length === 0) {
33
+ throw new Error(`Role task "${task.id}" has a grant without a category or with no axes. Axes are always listed one by one.`);
34
+ }
35
+ }
36
+ if (registry()[task.id]) {
37
+ throw new Error(`Role task "${task.id}" is declared twice.`);
38
+ }
39
+ const grants = mergeGrants([task.grants]);
40
+ const written = task.grants.reduce((count, grant) => count + grant.axes.length, 0);
41
+ const kept = grants.reduce((count, grant) => count + grant.axes.length, 0);
42
+ if (kept !== written) {
43
+ env_1.logger.warn(`[role-task] "${task.id}" names the same privilege more than once; it is held once.`);
44
+ }
45
+ registry()[task.id] = { id: task.id, grants };
46
+ }
47
+ function roleTasks() {
48
+ return Object.values(registry());
49
+ }
50
+ function roleTaskById(id) {
51
+ return registry()[id];
52
+ }
53
+ /** The module a task is declared by — the part of its id before the first dot. */
54
+ function roleTaskModule(task) {
55
+ return task.id.split('.')[0];
56
+ }
57
+ /**
58
+ * Several lists of grants as one: one entry per category, each axis once, in the order first written.
59
+ * A privilege held through two tasks, or through a task and a direct grant, is held once.
60
+ */
61
+ function mergeGrants(lists) {
62
+ const axesOf = new Map();
63
+ for (const grant of lists.flat()) {
64
+ const axes = axesOf.get(grant.category) || [];
65
+ for (const axis of grant.axes) {
66
+ if (!axes.includes(axis))
67
+ axes.push(axis);
68
+ }
69
+ axesOf.set(grant.category, axes);
70
+ }
71
+ return [...axesOf.entries()].map(([category, axes]) => ({ category, axes }));
72
+ }
73
+ /** `category axis` — the key `process['PRIVILEGES']` and a role's held set use. */
74
+ function privilegeKey(category, axis) {
75
+ return `${category} ${axis}`;
76
+ }
77
+ /** A task's privileges this installation does not declare, as `category:axis` — a definition error of the task. */
78
+ function undeclaredTaskGrants(task, declared = process['PRIVILEGES'] || {}) {
79
+ return task.grants.flatMap(grant => grant.axes.filter(axis => !declared[privilegeKey(grant.category, axis)]).map(axis => `${grant.category}:${axis}`));
80
+ }
81
+ /**
82
+ * **A role's privileges laid against every task** — what the role screen groups by.
83
+ *
84
+ * It says which of a task's privileges the role holds, nothing more. `all` is not "this person can do the work":
85
+ * another role of theirs, the data's own scope, an approval line or a field's door decide that too. `some` is not a
86
+ * fault: a privilege shared by several tasks makes each of them `some`. `none` is the common case.
87
+ *
88
+ * `unclassified` is what the role holds, declared here, that no task names. A privilege the role holds that nothing
89
+ * declares any more is not here — the screen already strikes it through (`isDeprecatedPrivilege`).
90
+ */
91
+ function taskCoverageOf(held, tasks = roleTasks(), declared = process['PRIVILEGES'] || {}) {
92
+ const named = new Set();
93
+ const coverage = tasks.map(task => {
94
+ const pick = (keep) => task.grants.map(grant => ({ category: grant.category, axes: grant.axes.filter(axis => keep(grant.category, axis)) })).filter(grant => grant.axes.length > 0);
95
+ task.grants.forEach(grant => grant.axes.forEach(axis => named.add(privilegeKey(grant.category, axis))));
96
+ const undeclared = pick((category, axis) => !declared[privilegeKey(category, axis)]);
97
+ const holds = pick((category, axis) => Boolean(declared[privilegeKey(category, axis)]) && held.has(privilegeKey(category, axis)));
98
+ const missing = pick((category, axis) => Boolean(declared[privilegeKey(category, axis)]) && !held.has(privilegeKey(category, axis)));
99
+ const status = undeclared.length ? 'broken' : !missing.length ? 'all' : holds.length ? 'some' : 'none';
100
+ return { taskId: task.id, module: roleTaskModule(task), status, held: holds, missing, undeclared };
101
+ });
102
+ const unclassified = mergeGrants([
103
+ [...held]
104
+ .filter(key => declared[key] && !named.has(key))
105
+ .sort()
106
+ .map(key => {
107
+ const [category, axis] = key.split(' ');
108
+ return { category, axes: [axis] };
109
+ })
110
+ ]);
111
+ return { tasks: coverage, unclassified };
112
+ }
113
+ //# sourceMappingURL=role-task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-task.js","sourceRoot":"","sources":["../../../server/service/role-template/role-task.ts"],"names":[],"mappings":"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","sourcesContent":["import { logger } from '@things-factory/env'\n\nimport type { RoleTemplateGrant } from './role-template.js'\n\n/**\n * Role tasks — a piece of work named by the module that owns it, and the privileges it takes (ADR-0139 decision 11).\n *\n * ── Why a task and not a template ────────────────────────────────────────────\n * A template is a seat in one application: who does what in an office or a plant. A module does not know the seats\n * of the applications that install it, but it does know its own work — accounting knows that writing an expenditure\n * request takes one's own requests and picking a counterparty and an account subject. So the module names the work\n * and the application builds its seats from the work (`RoleTemplate.tasks`). An application names work of its own\n * in the same form, under its own prefix.\n *\n * ── What a task is not ───────────────────────────────────────────────────────\n * - Not a narrower door. A task holds whole privileges, and each privilege opens what its doors open. A task called\n * \"read the books\" that holds `accounting:query` opens everything `accounting:query` opens; the name and the\n * description say so rather than hide it.\n * - Not stored. Nothing on a role records which tasks it was built from — the role screen compares a role's\n * privileges with the tasks, computed, the same way a template's gap is (`templateGapOf`).\n * - Not nested. A task lists privileges only; seats are where tasks are put together.\n * - Not declared twice. One piece of work has one task. Two tasks may share a privilege — picking a counterparty is\n * part of more than one piece of work — and that is not a duplicate.\n *\n * Its words live in the declaring module's translations, `task.<id>.name` and `task.<id>.description`, so a module\n * that is not installed takes its words away with it. The screen draws the id when there are none.\n */\nexport type RoleTask = {\n /** `<module>.<work>` — the declaring module's name first: `accounting.expense-authoring`. */\n id: string\n /** Privileges by category, axes listed one by one — the shape a template's grants have. */\n grants: RoleTemplateGrant[]\n}\n\nconst REGISTRY = 'ROLE_TASKS'\n\nprocess[REGISTRY] = {}\n\nfunction registry(): Record<string, RoleTask> {\n return process[REGISTRY]\n}\n\n/**\n * Declares a task. Call it from the module's server entry, beside its `@privilege` doors.\n *\n * Privileges are not checked here, for the reason templates are not: modules load before the schema is built and\n * `process['PRIVILEGES']` is filled. Boot checks them (`reportRoleDefinitionErrors`).\n */\nexport function registerRoleTask(task: RoleTask): void {\n const prefix = task?.id?.split('.')[0]\n\n if (!task?.id || !prefix || prefix === task.id) {\n throw new Error(`A role task needs an id of the form <module>.<work>, the declaring module first — \"${task?.id ?? ''}\" is not.`)\n }\n\n if (!Array.isArray(task.grants) || task.grants.length === 0) {\n throw new Error(`Role task \"${task.id}\" holds no privileges. A task is the privileges a piece of work takes; one that takes none is not a task.`)\n }\n\n for (const grant of task.grants) {\n if (!grant?.category || !Array.isArray(grant.axes) || grant.axes.length === 0) {\n throw new Error(`Role task \"${task.id}\" has a grant without a category or with no axes. Axes are always listed one by one.`)\n }\n }\n\n if (registry()[task.id]) {\n throw new Error(`Role task \"${task.id}\" is declared twice.`)\n }\n\n const grants = mergeGrants([task.grants])\n const written = task.grants.reduce((count, grant) => count + grant.axes.length, 0)\n const kept = grants.reduce((count, grant) => count + grant.axes.length, 0)\n\n if (kept !== written) {\n logger.warn(`[role-task] \"${task.id}\" names the same privilege more than once; it is held once.`)\n }\n\n registry()[task.id] = { id: task.id, grants }\n}\n\nexport function roleTasks(): RoleTask[] {\n return Object.values(registry())\n}\n\nexport function roleTaskById(id: string): RoleTask | undefined {\n return registry()[id]\n}\n\n/** The module a task is declared by — the part of its id before the first dot. */\nexport function roleTaskModule(task: RoleTask): string {\n return task.id.split('.')[0]\n}\n\n/**\n * Several lists of grants as one: one entry per category, each axis once, in the order first written.\n * A privilege held through two tasks, or through a task and a direct grant, is held once.\n */\nexport function mergeGrants(lists: RoleTemplateGrant[][]): RoleTemplateGrant[] {\n const axesOf = new Map<string, string[]>()\n\n for (const grant of lists.flat()) {\n const axes = axesOf.get(grant.category) || []\n for (const axis of grant.axes) {\n if (!axes.includes(axis)) axes.push(axis)\n }\n axesOf.set(grant.category, axes)\n }\n\n return [...axesOf.entries()].map(([category, axes]) => ({ category, axes }))\n}\n\n/** `category axis` — the key `process['PRIVILEGES']` and a role's held set use. */\nexport function privilegeKey(category: string, axis: string): string {\n return `${category} ${axis}`\n}\n\n/** A task's privileges this installation does not declare, as `category:axis` — a definition error of the task. */\nexport function undeclaredTaskGrants(task: RoleTask, declared: Record<string, unknown> = process['PRIVILEGES'] || {}): string[] {\n return task.grants.flatMap(grant => grant.axes.filter(axis => !declared[privilegeKey(grant.category, axis)]).map(axis => `${grant.category}:${axis}`))\n}\n\nexport type RoleTaskStatus = 'all' | 'some' | 'none' | 'broken'\n\nexport type RoleTaskCoverage = {\n taskId: string\n module: string\n status: RoleTaskStatus\n held: RoleTemplateGrant[]\n missing: RoleTemplateGrant[]\n /** Privileges the task names that this installation does not declare. Any of them makes the task `broken`. */\n undeclared: RoleTemplateGrant[]\n}\n\n/**\n * **A role's privileges laid against every task** — what the role screen groups by.\n *\n * It says which of a task's privileges the role holds, nothing more. `all` is not \"this person can do the work\":\n * another role of theirs, the data's own scope, an approval line or a field's door decide that too. `some` is not a\n * fault: a privilege shared by several tasks makes each of them `some`. `none` is the common case.\n *\n * `unclassified` is what the role holds, declared here, that no task names. A privilege the role holds that nothing\n * declares any more is not here — the screen already strikes it through (`isDeprecatedPrivilege`).\n */\nexport function taskCoverageOf(\n held: ReadonlySet<string>,\n tasks: RoleTask[] = roleTasks(),\n declared: Record<string, unknown> = process['PRIVILEGES'] || {}\n): { tasks: RoleTaskCoverage[]; unclassified: RoleTemplateGrant[] } {\n const named = new Set<string>()\n\n const coverage = tasks.map(task => {\n const pick = (keep: (category: string, axis: string) => boolean) =>\n task.grants.map(grant => ({ category: grant.category, axes: grant.axes.filter(axis => keep(grant.category, axis)) })).filter(grant => grant.axes.length > 0)\n\n task.grants.forEach(grant => grant.axes.forEach(axis => named.add(privilegeKey(grant.category, axis))))\n\n const undeclared = pick((category, axis) => !declared[privilegeKey(category, axis)])\n const holds = pick((category, axis) => Boolean(declared[privilegeKey(category, axis)]) && held.has(privilegeKey(category, axis)))\n const missing = pick((category, axis) => Boolean(declared[privilegeKey(category, axis)]) && !held.has(privilegeKey(category, axis)))\n\n const status: RoleTaskStatus = undeclared.length ? 'broken' : !missing.length ? 'all' : holds.length ? 'some' : 'none'\n\n return { taskId: task.id, module: roleTaskModule(task), status, held: holds, missing, undeclared }\n })\n\n const unclassified = mergeGrants([\n [...held]\n .filter(key => declared[key] && !named.has(key))\n .sort()\n .map(key => {\n const [category, axis] = key.split(' ')\n return { category, axes: [axis] }\n })\n ])\n\n return { tasks: coverage, unclassified }\n}\n"]}
@@ -1,10 +1,14 @@
1
1
  import { Role } from '../role/role.js';
2
- import { RoleTemplateGapView } from './role-template-types.js';
2
+ import { RoleTaskCoverageListView, RoleTemplateGapView } from './role-template-types.js';
3
3
  /**
4
- * `Role.templateGap` - the role management screen shows, per role, what its template grants that it
5
- * does not hold, and offers to add it (ADR-0075 decision 1). The rule is `templateGapOf`; this only
6
- * reads the role's privileges.
4
+ * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against a template and against
5
+ * the tasks (ADR-0075 decision 1, ADR-0139 decision 11). The rules are `templateGapAgainst` and `taskCoverageOf`;
6
+ * this only reads the role's privileges.
7
+ *
8
+ * Both are fields of a role, so they are reached only through a door that already returned the role - the same
9
+ * privilege and the same domain the role list asks for. They add no door of their own.
7
10
  */
8
11
  export declare class RoleTemplateGapResolver {
9
- templateGap(role: Role): Promise<RoleTemplateGapView | null>;
12
+ templateGap(role: Role, templateId?: string): Promise<RoleTemplateGapView | null>;
13
+ taskCoverage(role: Role): Promise<RoleTaskCoverageListView>;
10
14
  }
@@ -7,35 +7,63 @@ const shell_1 = require("@things-factory/shell");
7
7
  const privilege_js_1 = require("../privilege/privilege.js");
8
8
  const role_js_1 = require("../role/role.js");
9
9
  const role_template_js_1 = require("./role-template.js");
10
+ const role_task_js_1 = require("./role-task.js");
10
11
  const role_template_types_js_1 = require("./role-template-types.js");
11
12
  /**
12
- * `Role.templateGap` - the role management screen shows, per role, what its template grants that it
13
- * does not hold, and offers to add it (ADR-0075 decision 1). The rule is `templateGapOf`; this only
14
- * reads the role's privileges.
13
+ * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against a template and against
14
+ * the tasks (ADR-0075 decision 1, ADR-0139 decision 11). The rules are `templateGapAgainst` and `taskCoverageOf`;
15
+ * this only reads the role's privileges.
16
+ *
17
+ * Both are fields of a role, so they are reached only through a door that already returned the role - the same
18
+ * privilege and the same domain the role list asks for. They add no door of their own.
15
19
  */
16
20
  let RoleTemplateGapResolver = class RoleTemplateGapResolver {
17
- async templateGap(role) {
18
- const held = await (0, shell_1.getRepository)(privilege_js_1.Privilege)
19
- .createQueryBuilder('PRIVILEGE')
20
- .select(['PRIVILEGE.category', 'PRIVILEGE.name'])
21
- .innerJoin('PRIVILEGE.roles', 'ROLE')
22
- .where('ROLE.id = :id', { id: role.id })
23
- .getMany();
24
- return (0, role_template_js_1.templateGapOf)(role.name, new Set(held.map(privilege => `${privilege.category} ${privilege.name}`)));
21
+ async templateGap(role, templateId) {
22
+ const held = await heldBy(role);
23
+ if (!templateId) {
24
+ return (0, role_template_js_1.templateGapOf)(role.name, held);
25
+ }
26
+ const template = (0, role_template_js_1.allRoleTemplates)().find(one => one.id === templateId);
27
+ if (!template) {
28
+ throw new Error(`There is no role template "${templateId}" in this installation.`);
29
+ }
30
+ return (0, role_template_js_1.templateGapAgainst)(template, held);
31
+ }
32
+ async taskCoverage(role) {
33
+ return (0, role_task_js_1.taskCoverageOf)(await heldBy(role));
25
34
  }
26
35
  };
27
36
  exports.RoleTemplateGapResolver = RoleTemplateGapResolver;
28
37
  tslib_1.__decorate([
29
38
  (0, type_graphql_1.FieldResolver)(type => role_template_types_js_1.RoleTemplateGapView, {
30
39
  nullable: true,
31
- description: "What the template of this role's name grants here that the role does not hold. Null when no template carries the name."
40
+ description: "What a template grants here that the role does not hold, and what the role holds beyond it. Without templateId, the template of the role's name - null when no template carries it."
32
41
  }),
33
42
  tslib_1.__param(0, (0, type_graphql_1.Root)()),
43
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('templateId', { nullable: true, description: 'The template to lay the role against. Defaults to the template of the role\'s name.' })),
34
44
  tslib_1.__metadata("design:type", Function),
35
- tslib_1.__metadata("design:paramtypes", [role_js_1.Role]),
45
+ tslib_1.__metadata("design:paramtypes", [role_js_1.Role, String]),
36
46
  tslib_1.__metadata("design:returntype", Promise)
37
47
  ], RoleTemplateGapResolver.prototype, "templateGap", null);
48
+ tslib_1.__decorate([
49
+ (0, type_graphql_1.FieldResolver)(type => role_template_types_js_1.RoleTaskCoverageListView, {
50
+ description: "The role's privileges laid against every registered task, and those no task names."
51
+ }),
52
+ tslib_1.__param(0, (0, type_graphql_1.Root)()),
53
+ tslib_1.__metadata("design:type", Function),
54
+ tslib_1.__metadata("design:paramtypes", [role_js_1.Role]),
55
+ tslib_1.__metadata("design:returntype", Promise)
56
+ ], RoleTemplateGapResolver.prototype, "taskCoverage", null);
38
57
  exports.RoleTemplateGapResolver = RoleTemplateGapResolver = tslib_1.__decorate([
39
58
  (0, type_graphql_1.Resolver)(role_js_1.Role)
40
59
  ], RoleTemplateGapResolver);
60
+ async function heldBy(role) {
61
+ const held = await (0, shell_1.getRepository)(privilege_js_1.Privilege)
62
+ .createQueryBuilder('PRIVILEGE')
63
+ .select(['PRIVILEGE.category', 'PRIVILEGE.name'])
64
+ .innerJoin('PRIVILEGE.roles', 'ROLE')
65
+ .where('ROLE.id = :id', { id: role.id })
66
+ .getMany();
67
+ return new Set(held.map(privilege => `${privilege.category} ${privilege.name}`));
68
+ }
41
69
  //# sourceMappingURL=role-template-gap-resolver.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"role-template-gap-resolver.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-gap-resolver.ts"],"names":[],"mappings":";;;;AAAA,+CAA4D;AAE5D,iDAAqD;AAErD,4DAAqD;AACrD,6CAAsC;AACtC,yDAAkD;AAClD,qEAA8D;AAE9D;;;;GAIG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAM5B,AAAN,KAAK,CAAC,WAAW,CAAS,IAAU;QAClC,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,wBAAS,CAAC;aACxC,kBAAkB,CAAC,WAAW,CAAC;aAC/B,MAAM,CAAC,CAAC,oBAAoB,EAAE,gBAAgB,CAAC,CAAC;aAChD,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC;aACpC,KAAK,CAAC,eAAe,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;aACvC,OAAO,EAAE,CAAA;QAEZ,OAAO,IAAA,gCAAa,EAAC,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAC5G,CAAC;CACF,CAAA;AAhBY,0DAAuB;AAM5B;IALL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,4CAAmB,EAAE;QAC1C,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,wHAAwH;KAC3H,CAAC;IACiB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,cAAI;;0DASnC;kCAfU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,cAAI,CAAC;GACF,uBAAuB,CAgBnC","sourcesContent":["import { FieldResolver, Resolver, Root } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { Role } from '../role/role.js'\nimport { templateGapOf } from './role-template.js'\nimport { RoleTemplateGapView } from './role-template-types.js'\n\n/**\n * `Role.templateGap` - the role management screen shows, per role, what its template grants that it\n * does not hold, and offers to add it (ADR-0075 decision 1). The rule is `templateGapOf`; this only\n * reads the role's privileges.\n */\n@Resolver(Role)\nexport class RoleTemplateGapResolver {\n @FieldResolver(type => RoleTemplateGapView, {\n nullable: true,\n description:\n \"What the template of this role's name grants here that the role does not hold. Null when no template carries the name.\"\n })\n async templateGap(@Root() role: Role): Promise<RoleTemplateGapView | null> {\n const held = await getRepository(Privilege)\n .createQueryBuilder('PRIVILEGE')\n .select(['PRIVILEGE.category', 'PRIVILEGE.name'])\n .innerJoin('PRIVILEGE.roles', 'ROLE')\n .where('ROLE.id = :id', { id: role.id })\n .getMany()\n\n return templateGapOf(role.name, new Set(held.map(privilege => `${privilege.category} ${privilege.name}`)))\n }\n}\n"]}
1
+ {"version":3,"file":"role-template-gap-resolver.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-gap-resolver.ts"],"names":[],"mappings":";;;;AAAA,+CAAiE;AAEjE,iDAAqD;AAErD,4DAAqD;AACrD,6CAAsC;AACtC,yDAAwF;AACxF,iDAA+C;AAC/C,qEAAwF;AAExF;;;;;;;GAOG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAM5B,AAAN,KAAK,CAAC,WAAW,CACP,IAAU,EAElB,UAAmB;QAEnB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;QAE/B,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,IAAA,gCAAa,EAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;QACvC,CAAC;QAED,MAAM,QAAQ,GAAG,IAAA,mCAAgB,GAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,UAAU,CAAC,CAAA;QACtE,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,8BAA8B,UAAU,yBAAyB,CAAC,CAAA;QACpF,CAAC;QAED,OAAO,IAAA,qCAAkB,EAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC3C,CAAC;IAKK,AAAN,KAAK,CAAC,YAAY,CAAS,IAAU;QACnC,OAAO,IAAA,6BAAc,EAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3C,CAAC;CACF,CAAA;AA/BY,0DAAuB;AAM5B;IALL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,4CAAmB,EAAE;QAC1C,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,qLAAqL;KACxL,CAAC;IAEC,mBAAA,IAAA,mBAAI,GAAE,CAAA;IACN,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qFAAqF,EAAE,CAAC,CAAA;;6CAD5H,cAAI;;0DAgBnB;AAKK;IAHL,IAAA,4BAAa,EAAC,IAAI,CAAC,EAAE,CAAC,iDAAwB,EAAE;QAC/C,WAAW,EAAE,oFAAoF;KAClG,CAAC;IACkB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;6CAAO,cAAI;;2DAEpC;kCA9BU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,cAAI,CAAC;GACF,uBAAuB,CA+BnC;AAED,KAAK,UAAU,MAAM,CAAC,IAAU;IAC9B,MAAM,IAAI,GAAG,MAAM,IAAA,qBAAa,EAAC,wBAAS,CAAC;SACxC,kBAAkB,CAAC,WAAW,CAAC;SAC/B,MAAM,CAAC,CAAC,oBAAoB,EAAE,gBAAgB,CAAC,CAAC;SAChD,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC;SACpC,KAAK,CAAC,eAAe,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;SACvC,OAAO,EAAE,CAAA;IAEZ,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { Arg, FieldResolver, Resolver, Root } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { Role } from '../role/role.js'\nimport { allRoleTemplates, templateGapAgainst, templateGapOf } from './role-template.js'\nimport { taskCoverageOf } from './role-task.js'\nimport { RoleTaskCoverageListView, RoleTemplateGapView } from './role-template-types.js'\n\n/**\n * `Role.templateGap` and `Role.taskCoverage` - the role management screen lays a role against a template and against\n * the tasks (ADR-0075 decision 1, ADR-0139 decision 11). The rules are `templateGapAgainst` and `taskCoverageOf`;\n * this only reads the role's privileges.\n *\n * Both are fields of a role, so they are reached only through a door that already returned the role - the same\n * privilege and the same domain the role list asks for. They add no door of their own.\n */\n@Resolver(Role)\nexport class RoleTemplateGapResolver {\n @FieldResolver(type => RoleTemplateGapView, {\n nullable: true,\n description:\n \"What a template grants here that the role does not hold, and what the role holds beyond it. Without templateId, the template of the role's name - null when no template carries it.\"\n })\n async templateGap(\n @Root() role: Role,\n @Arg('templateId', { nullable: true, description: 'The template to lay the role against. Defaults to the template of the role\\'s name.' })\n templateId?: string\n ): Promise<RoleTemplateGapView | null> {\n const held = await heldBy(role)\n\n if (!templateId) {\n return templateGapOf(role.name, held)\n }\n\n const template = allRoleTemplates().find(one => one.id === templateId)\n if (!template) {\n throw new Error(`There is no role template \"${templateId}\" in this installation.`)\n }\n\n return templateGapAgainst(template, held)\n }\n\n @FieldResolver(type => RoleTaskCoverageListView, {\n description: \"The role's privileges laid against every registered task, and those no task names.\"\n })\n async taskCoverage(@Root() role: Role): Promise<RoleTaskCoverageListView> {\n return taskCoverageOf(await heldBy(role))\n }\n}\n\nasync function heldBy(role: Role): Promise<Set<string>> {\n const held = await getRepository(Privilege)\n .createQueryBuilder('PRIVILEGE')\n .select(['PRIVILEGE.category', 'PRIVILEGE.name'])\n .innerJoin('PRIVILEGE.roles', 'ROLE')\n .where('ROLE.id = :id', { id: role.id })\n .getMany()\n\n return new Set(held.map(privilege => `${privilege.category} ${privilege.name}`))\n}\n"]}
@@ -41,6 +41,15 @@ let RoleTemplateMutation = class RoleTemplateMutation {
41
41
  if (!template) {
42
42
  throw new Error(`There is no role template "${templateId}" in this installation.`);
43
43
  }
44
+ /*
45
+ * A template whose tasks are not all there is refused whole, before a role is made or a privilege added
46
+ * (ADR-0139 decision 11). Applying the rest would stand up a seat short of work its name says it does, and
47
+ * the gap would look like the administrator's choice.
48
+ */
49
+ const errors = (0, role_template_js_1.templateDefinitionErrors)(template);
50
+ if (errors.length) {
51
+ throw new Error(`Role template "${template.id}" cannot be applied: ${errors.join('; ')}.`);
52
+ }
44
53
  const roleName = (name || template.name).trim();
45
54
  if (!roleName) {
46
55
  throw new Error('A role needs a name.');
@@ -70,7 +79,7 @@ let RoleTemplateMutation = class RoleTemplateMutation {
70
79
  });
71
80
  }
72
81
  const held = new Set((role.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`));
73
- const wanted = template.grants.flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })));
82
+ const wanted = (0, role_template_js_1.templateGrantsOf)(template).flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })));
74
83
  /*
75
84
  * Privileges have no domain — the whole server shares one set and what differs per
76
85
  * domain is only which role holds them (`Privilege` has no domain column; auth-base's
@@ -1 +1 @@
1
- {"version":3,"file":"role-template-mutation.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { getRepository, SpeakRefusals } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { refuseRoleNameIfStanding } from '../../utils/refuse-role-name.js'\nimport { Role } from '../role/role.js'\nimport { roleTemplateById, computedRoleTemplates, RegisteredRoleTemplate } from './role-template.js'\n\n/* `refuseRoleNameIfStanding` throws a `Refusal`; this is the exit that turns it into a sentence. */\n@Resolver(Role)\n@UseMiddleware(SpeakRefusals)\nexport class RoleTemplateMutation {\n /**\n * Creates a role from a template — once per name.\n *\n * ── Why an existing name is used rather than replaced ────────────────────────\n * Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`\n * by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the\n * first one while the administrator edits the second, and nothing would report it: the\n * screen shows two roles and the approvals go to the one nobody is looking at.\n *\n * So an existing role of that name is used as it stands. operato-plant's seed already\n * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);\n * this is the same rule, stated once.\n *\n * ── Why privileges are added and never removed ───────────────────────────────\n * Two applications may declare a template with the same role name on purpose, and the\n * privileges are then the union — to a user \"quality inspector\" is one seat. The union\n * only widens: if a template later drops a privilege it stays on roles already seeded,\n * because the seed runs once and the role belongs to the administrator afterwards.\n *\n * A privilege that is no longer declared anywhere is already handled elsewhere and not\n * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses\n * to switch it back on, and removing it is the administrator's call. Reclaiming it from\n * this side would mean overwriting, and would be unable to tell a stale grant from one\n * the administrator added on purpose.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Role, { description: 'To create a role from a role template' })\n async createRoleFromTemplate(\n @Arg('templateId') templateId: string,\n /**\n * What this plant calls the seat. Defaults to the template's name.\n *\n * The template knows which seat exists in a factory running this product; it does not\n * know what the people there call it. And because a role is also what an approval line\n * points at, the same seat is sometimes needed twice — a day shift and a night shift\n * inspector hold the same privileges and take different approvals. Without a name of\n * their own a template could be used exactly once per domain.\n */\n @Arg('name', { nullable: true }) name: string | undefined,\n @Ctx() context: ResolverContext,\n /**\n * The operator saying this domain stands its own role of a name it already inherits\n * (ADR-0062 decision 3).\n *\n * ⚠ This is where the rule bites hardest, and stopping here is the point of it. A template\n * seeds the seats a product needs, and on a tenant with a parent those names are often\n * already visible through inheritance — so the template would quietly put a second row of\n * the same name beside the inherited one, which is the state the whole rule exists to\n * prevent.\n *\n * The name is not changed to get past it. Standing up the same names in this domain is the\n * first step of moving off an inherited structure (decision 2), so this says \"yes, that is\n * what I am doing\" rather than \"call it something else\". `name` above remains for the other\n * thing — a second seat with the same privileges under a name of its own.\n */\n @Arg('separately', { nullable: true }) separately: boolean | undefined\n ): Promise<Role> {\n const { domain, user, tx } = context.state\n\n const template = findTemplate(templateId)\n\n if (!template) {\n throw new Error(`There is no role template \"${templateId}\" in this installation.`)\n }\n\n const roleName = (name || template.name).trim()\n\n if (!roleName) {\n throw new Error('A role needs a name.')\n }\n\n const roleRepository = tx.getRepository(Role)\n\n let role = await roleRepository.findOne({\n where: { domain: { id: domain.id }, name: roleName },\n relations: ['privileges']\n })\n\n if (!role) {\n /*\n * Only on the way to creating one. Topping up an existing role of ours is not a new name\n * and must not be refused — running the same template twice is how a seat gets privileges\n * the template gained since.\n */\n await refuseRoleNameIfStanding({ name: roleName, domain, separately, tx })\n\n role = await roleRepository.save({\n domain,\n name: roleName,\n description: template.description,\n creator: user,\n updater: user\n } as any)\n\n role = await roleRepository.findOne({\n where: { id: role!.id },\n relations: ['privileges']\n })\n }\n\n const held = new Set((role!.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`))\n const wanted = template.grants.flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })))\n\n /*\n * Privileges have no domain — the whole server shares one set and what differs per\n * domain is only which role holds them (`Privilege` has no domain column; auth-base's\n * boot creates them by category and name alone). Filtering by domain finds nothing.\n */\n const privilegeRepository = tx.getRepository(Privilege)\n const added: Privilege[] = []\n\n for (const one of wanted) {\n if (held.has(`${one.category} ${one.name}`)) {\n continue\n }\n\n const privilege = await privilegeRepository.findOne({ where: { category: one.category, name: one.name } })\n\n /*\n * A privilege this installation does not declare is skipped, never created. Creating\n * it here would make a row no `@privilege` asks for: the role would look as if it\n * had been granted something, and open nothing. Granted-but-closed is the worst of\n * the three outcomes. `roleTemplates` reports these as `missingCount` before the\n * administrator picks the template.\n */\n if (privilege) {\n added.push(privilege)\n }\n }\n\n /*\n * Nothing records that this role came from a template.\n *\n * A `templateVersions` column stood here so a later change to the template could be\n * reported. It bought less than it looked like: what an administrator needs is \"these\n * two privileges were added to the template\", and that is the template's current\n * grants against the role's current privileges — computed, not stored. Telling them\n * apart from a privilege the administrator removed on purpose would take the seeded\n * content itself, not a version of it, and no screen asks for either yet.\n *\n * So the seed ends here. If reporting template drift is designed later, what to store\n * follows from that design rather than being guessed at now.\n */\n role!.privileges = [...(role!.privileges || []), ...added]\n role!.updater = user\n\n return await roleRepository.save(role!)\n }\n}\n\nfunction findTemplate(id: string): RegisteredRoleTemplate | undefined {\n return roleTemplateById(id) || computedRoleTemplates().find(template => template.id === id)\n}\n"]}
1
+ {"version":3,"file":"role-template-mutation.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { getRepository, SpeakRefusals } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { refuseRoleNameIfStanding } from '../../utils/refuse-role-name.js'\nimport { Role } from '../role/role.js'\nimport { roleTemplateById, computedRoleTemplates, RegisteredRoleTemplate, templateDefinitionErrors, templateGrantsOf } from './role-template.js'\n\n/* `refuseRoleNameIfStanding` throws a `Refusal`; this is the exit that turns it into a sentence. */\n@Resolver(Role)\n@UseMiddleware(SpeakRefusals)\nexport class RoleTemplateMutation {\n /**\n * Creates a role from a template — once per name.\n *\n * ── Why an existing name is used rather than replaced ────────────────────────\n * Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`\n * by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the\n * first one while the administrator edits the second, and nothing would report it: the\n * screen shows two roles and the approvals go to the one nobody is looking at.\n *\n * So an existing role of that name is used as it stands. operato-plant's seed already\n * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);\n * this is the same rule, stated once.\n *\n * ── Why privileges are added and never removed ───────────────────────────────\n * Two applications may declare a template with the same role name on purpose, and the\n * privileges are then the union — to a user \"quality inspector\" is one seat. The union\n * only widens: if a template later drops a privilege it stays on roles already seeded,\n * because the seed runs once and the role belongs to the administrator afterwards.\n *\n * A privilege that is no longer declared anywhere is already handled elsewhere and not\n * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses\n * to switch it back on, and removing it is the administrator's call. Reclaiming it from\n * this side would mean overwriting, and would be unable to tell a stale grant from one\n * the administrator added on purpose.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Role, { description: 'To create a role from a role template' })\n async createRoleFromTemplate(\n @Arg('templateId') templateId: string,\n /**\n * What this plant calls the seat. Defaults to the template's name.\n *\n * The template knows which seat exists in a factory running this product; it does not\n * know what the people there call it. And because a role is also what an approval line\n * points at, the same seat is sometimes needed twice — a day shift and a night shift\n * inspector hold the same privileges and take different approvals. Without a name of\n * their own a template could be used exactly once per domain.\n */\n @Arg('name', { nullable: true }) name: string | undefined,\n @Ctx() context: ResolverContext,\n /**\n * The operator saying this domain stands its own role of a name it already inherits\n * (ADR-0062 decision 3).\n *\n * ⚠ This is where the rule bites hardest, and stopping here is the point of it. A template\n * seeds the seats a product needs, and on a tenant with a parent those names are often\n * already visible through inheritance — so the template would quietly put a second row of\n * the same name beside the inherited one, which is the state the whole rule exists to\n * prevent.\n *\n * The name is not changed to get past it. Standing up the same names in this domain is the\n * first step of moving off an inherited structure (decision 2), so this says \"yes, that is\n * what I am doing\" rather than \"call it something else\". `name` above remains for the other\n * thing — a second seat with the same privileges under a name of its own.\n */\n @Arg('separately', { nullable: true }) separately: boolean | undefined\n ): Promise<Role> {\n const { domain, user, tx } = context.state\n\n const template = findTemplate(templateId)\n\n if (!template) {\n throw new Error(`There is no role template \"${templateId}\" in this installation.`)\n }\n\n /*\n * A template whose tasks are not all there is refused whole, before a role is made or a privilege added\n * (ADR-0139 decision 11). Applying the rest would stand up a seat short of work its name says it does, and\n * the gap would look like the administrator's choice.\n */\n const errors = templateDefinitionErrors(template)\n if (errors.length) {\n throw new Error(`Role template \"${template.id}\" cannot be applied: ${errors.join('; ')}.`)\n }\n\n const roleName = (name || template.name).trim()\n\n if (!roleName) {\n throw new Error('A role needs a name.')\n }\n\n const roleRepository = tx.getRepository(Role)\n\n let role = await roleRepository.findOne({\n where: { domain: { id: domain.id }, name: roleName },\n relations: ['privileges']\n })\n\n if (!role) {\n /*\n * Only on the way to creating one. Topping up an existing role of ours is not a new name\n * and must not be refused — running the same template twice is how a seat gets privileges\n * the template gained since.\n */\n await refuseRoleNameIfStanding({ name: roleName, domain, separately, tx })\n\n role = await roleRepository.save({\n domain,\n name: roleName,\n description: template.description,\n creator: user,\n updater: user\n } as any)\n\n role = await roleRepository.findOne({\n where: { id: role!.id },\n relations: ['privileges']\n })\n }\n\n const held = new Set((role!.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`))\n const wanted = templateGrantsOf(template).flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })))\n\n /*\n * Privileges have no domain — the whole server shares one set and what differs per\n * domain is only which role holds them (`Privilege` has no domain column; auth-base's\n * boot creates them by category and name alone). Filtering by domain finds nothing.\n */\n const privilegeRepository = tx.getRepository(Privilege)\n const added: Privilege[] = []\n\n for (const one of wanted) {\n if (held.has(`${one.category} ${one.name}`)) {\n continue\n }\n\n const privilege = await privilegeRepository.findOne({ where: { category: one.category, name: one.name } })\n\n /*\n * A privilege this installation does not declare is skipped, never created. Creating\n * it here would make a row no `@privilege` asks for: the role would look as if it\n * had been granted something, and open nothing. Granted-but-closed is the worst of\n * the three outcomes. `roleTemplates` reports these as `missingCount` before the\n * administrator picks the template.\n */\n if (privilege) {\n added.push(privilege)\n }\n }\n\n /*\n * Nothing records that this role came from a template.\n *\n * A `templateVersions` column stood here so a later change to the template could be\n * reported. It bought less than it looked like: what an administrator needs is \"these\n * two privileges were added to the template\", and that is the template's current\n * grants against the role's current privileges — computed, not stored. Telling them\n * apart from a privilege the administrator removed on purpose would take the seeded\n * content itself, not a version of it, and no screen asks for either yet.\n *\n * So the seed ends here. If reporting template drift is designed later, what to store\n * follows from that design rather than being guessed at now.\n */\n role!.privileges = [...(role!.privileges || []), ...added]\n role!.updater = user\n\n return await roleRepository.save(role!)\n }\n}\n\nfunction findTemplate(id: string): RegisteredRoleTemplate | undefined {\n return roleTemplateById(id) || computedRoleTemplates().find(template => template.id === id)\n}\n"]}
@@ -55,7 +55,7 @@ exports.RoleTemplateQuery = RoleTemplateQuery = tslib_1.__decorate([
55
55
  ], RoleTemplateQuery);
56
56
  function view(template, existing) {
57
57
  const declared = process['PRIVILEGES'] || {};
58
- const grants = template.grants.map(grant => {
58
+ const grants = (0, role_template_js_1.templateGrantsOf)(template).map(grant => {
59
59
  const present = grant.axes.filter(axis => declared[`${grant.category} ${axis}`]);
60
60
  const missingAxes = grant.axes.filter(axis => !declared[`${grant.category} ${axis}`]);
61
61
  return { category: grant.category, axes: present, missingAxes };
@@ -65,6 +65,8 @@ function view(template, existing) {
65
65
  name: template.name,
66
66
  description: template.description,
67
67
  grants,
68
+ tasks: template.tasks || [],
69
+ definitionErrors: (0, role_template_js_1.templateDefinitionErrors)(template),
68
70
  privilegeCount: grants.reduce((count, grant) => count + grant.axes.length, 0),
69
71
  missingCount: grants.reduce((count, grant) => count + grant.missingAxes.length, 0),
70
72
  defaultNameTaken: existing.has(template.name),
@@ -73,7 +75,7 @@ function view(template, existing) {
73
75
  * grants all went missing in this installation, which is a defect and shows a
74
76
  * `missingCount` instead.
75
77
  */
76
- grantsNothingDeliberately: template.grants.length === 0
78
+ grantsNothingDeliberately: !template.tasks?.length && !template.grants?.length
77
79
  };
78
80
  }
79
81
  //# sourceMappingURL=role-template-query.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"role-template-query.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Query, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Role } from '../role/role.js'\nimport { allRoleTemplates, RegisteredRoleTemplate } from './role-template.js'\nimport { RoleTemplateView } from './role-template-types.js'\n\n@Resolver(RoleTemplateView)\nexport class RoleTemplateQuery {\n /**\n * The templates this domain can start a role from.\n *\n * Gated the same way role editing is: choosing a template creates a role, so whoever may\n * do this is whoever may create roles.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [RoleTemplateView], { description: 'To fetch the role templates this installation offers' })\n async roleTemplates(@Ctx() context: ResolverContext): Promise<RoleTemplateView[]> {\n const { domain } = context.state\n\n /*\n * Read the existing role names once. The alternative — a field resolver asking per\n * template — runs a query per row for an answer the list already has.\n */\n const existing = new Set(\n (\n await getRepository(Role)\n .createQueryBuilder('ROLE')\n .select('ROLE.name')\n .where('ROLE.domain = :domain', { domain: domain.id })\n .getMany()\n ).map(role => role.name)\n )\n\n return allRoleTemplates().map(template => view(template, existing))\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => RoleTemplateView, { nullable: true, description: 'To fetch one role template by id' })\n async roleTemplate(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<RoleTemplateView | null> {\n const templates = await this.roleTemplates(context)\n\n return templates.find(template => template.id === id) || null\n }\n}\n\nfunction view(template: RegisteredRoleTemplate, existing: Set<string>): RoleTemplateView {\n const declared = process['PRIVILEGES'] || {}\n\n const grants = template.grants.map(grant => {\n const present = grant.axes.filter(axis => declared[`${grant.category} ${axis}`])\n const missingAxes = grant.axes.filter(axis => !declared[`${grant.category} ${axis}`])\n\n return { category: grant.category, axes: present, missingAxes }\n })\n\n return {\n id: template.id,\n name: template.name,\n description: template.description,\n grants,\n privilegeCount: grants.reduce((count, grant) => count + grant.axes.length, 0),\n missingCount: grants.reduce((count, grant) => count + grant.missingAxes.length, 0),\n defaultNameTaken: existing.has(template.name),\n /*\n * Declared with no grants at all — an approval seat. Distinct from a template whose\n * grants all went missing in this installation, which is a defect and shows a\n * `missingCount` instead.\n */\n grantsNothingDeliberately: template.grants.length === 0\n }\n}\n"]}
1
+ {"version":3,"file":"role-template-query.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Query, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Role } from '../role/role.js'\nimport { allRoleTemplates, RegisteredRoleTemplate, templateDefinitionErrors, templateGrantsOf } from './role-template.js'\nimport { RoleTemplateView } from './role-template-types.js'\n\n@Resolver(RoleTemplateView)\nexport class RoleTemplateQuery {\n /**\n * The templates this domain can start a role from.\n *\n * Gated the same way role editing is: choosing a template creates a role, so whoever may\n * do this is whoever may create roles.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [RoleTemplateView], { description: 'To fetch the role templates this installation offers' })\n async roleTemplates(@Ctx() context: ResolverContext): Promise<RoleTemplateView[]> {\n const { domain } = context.state\n\n /*\n * Read the existing role names once. The alternative — a field resolver asking per\n * template — runs a query per row for an answer the list already has.\n */\n const existing = new Set(\n (\n await getRepository(Role)\n .createQueryBuilder('ROLE')\n .select('ROLE.name')\n .where('ROLE.domain = :domain', { domain: domain.id })\n .getMany()\n ).map(role => role.name)\n )\n\n return allRoleTemplates().map(template => view(template, existing))\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => RoleTemplateView, { nullable: true, description: 'To fetch one role template by id' })\n async roleTemplate(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<RoleTemplateView | null> {\n const templates = await this.roleTemplates(context)\n\n return templates.find(template => template.id === id) || null\n }\n}\n\nfunction view(template: RegisteredRoleTemplate, existing: Set<string>): RoleTemplateView {\n const declared = process['PRIVILEGES'] || {}\n\n const grants = templateGrantsOf(template).map(grant => {\n const present = grant.axes.filter(axis => declared[`${grant.category} ${axis}`])\n const missingAxes = grant.axes.filter(axis => !declared[`${grant.category} ${axis}`])\n\n return { category: grant.category, axes: present, missingAxes }\n })\n\n return {\n id: template.id,\n name: template.name,\n description: template.description,\n grants,\n tasks: template.tasks || [],\n definitionErrors: templateDefinitionErrors(template),\n privilegeCount: grants.reduce((count, grant) => count + grant.axes.length, 0),\n missingCount: grants.reduce((count, grant) => count + grant.missingAxes.length, 0),\n defaultNameTaken: existing.has(template.name),\n /*\n * Declared with no grants at all — an approval seat. Distinct from a template whose\n * grants all went missing in this installation, which is a defect and shows a\n * `missingCount` instead.\n */\n grantsNothingDeliberately: !template.tasks?.length && !template.grants?.length\n }\n}\n"]}
@@ -24,6 +24,12 @@ export declare class RoleTemplateView {
24
24
  name: string;
25
25
  description?: string;
26
26
  grants: RoleTemplateGrantView[];
27
+ tasks: string[];
28
+ /**
29
+ * What stops this template from being applied at all — a task nobody registered, or a task naming a
30
+ * privilege this installation does not declare (ADR-0139 decision 11). Empty when it can be applied.
31
+ */
32
+ definitionErrors: string[];
27
33
  privilegeCount: number;
28
34
  missingCount: number;
29
35
  /**
@@ -58,4 +64,22 @@ export declare class RoleTemplateGapView {
58
64
  templateName: string;
59
65
  missing: RoleTemplateGapGrant[];
60
66
  missingCount: number;
67
+ /**
68
+ * What the role holds beyond the template. Not a fault — a site may widen a role on purpose — so the screen
69
+ * calls it "wider than the template" and nothing takes it away.
70
+ */
71
+ extra: RoleTemplateGapGrant[];
72
+ extraCount: number;
73
+ }
74
+ export declare class RoleTaskCoverageView {
75
+ taskId: string;
76
+ module: string;
77
+ status: string;
78
+ held: RoleTemplateGapGrant[];
79
+ missing: RoleTemplateGapGrant[];
80
+ undeclared: RoleTemplateGapGrant[];
81
+ }
82
+ export declare class RoleTaskCoverageListView {
83
+ tasks: RoleTaskCoverageView[];
84
+ unclassified: RoleTemplateGapGrant[];
61
85
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RoleTemplateGapView = exports.RoleTemplateGapGrant = exports.RoleTemplateView = exports.RoleTemplateGrantView = void 0;
3
+ exports.RoleTaskCoverageListView = exports.RoleTaskCoverageView = exports.RoleTemplateGapView = exports.RoleTemplateGapGrant = exports.RoleTemplateView = exports.RoleTemplateGrantView = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const type_graphql_1 = require("type-graphql");
6
6
  /**
@@ -49,6 +49,14 @@ tslib_1.__decorate([
49
49
  (0, type_graphql_1.Field)(type => [RoleTemplateGrantView], { description: 'The modules and axes this template grants.' }),
50
50
  tslib_1.__metadata("design:type", Array)
51
51
  ], RoleTemplateView.prototype, "grants", void 0);
52
+ tslib_1.__decorate([
53
+ (0, type_graphql_1.Field)(type => [String], { description: 'The tasks this template is built from, by id - each a piece of work a module or application names.' }),
54
+ tslib_1.__metadata("design:type", Array)
55
+ ], RoleTemplateView.prototype, "tasks", void 0);
56
+ tslib_1.__decorate([
57
+ (0, type_graphql_1.Field)(type => [String], { description: 'Definition errors that refuse this template when applied: unregistered tasks, or tasks naming undeclared privileges.' }),
58
+ tslib_1.__metadata("design:type", Array)
59
+ ], RoleTemplateView.prototype, "definitionErrors", void 0);
52
60
  tslib_1.__decorate([
53
61
  (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'How many privileges this template would actually grant here.' }),
54
62
  tslib_1.__metadata("design:type", Number)
@@ -103,9 +111,63 @@ tslib_1.__decorate([
103
111
  }),
104
112
  tslib_1.__metadata("design:type", Number)
105
113
  ], RoleTemplateGapView.prototype, "missingCount", void 0);
114
+ tslib_1.__decorate([
115
+ (0, type_graphql_1.Field)(type => [RoleTemplateGapGrant], { description: 'Declared privileges the role holds that the template does not grant, by category.' }),
116
+ tslib_1.__metadata("design:type", Array)
117
+ ], RoleTemplateGapView.prototype, "extra", void 0);
118
+ tslib_1.__decorate([
119
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'How many privileges the role holds beyond the template.' }),
120
+ tslib_1.__metadata("design:type", Number)
121
+ ], RoleTemplateGapView.prototype, "extraCount", void 0);
106
122
  exports.RoleTemplateGapView = RoleTemplateGapView = tslib_1.__decorate([
107
123
  (0, type_graphql_1.ObjectType)({
108
124
  description: "What the template of a role's name grants in this installation that the role does not hold."
109
125
  })
110
126
  ], RoleTemplateGapView);
127
+ let RoleTaskCoverageView = class RoleTaskCoverageView {
128
+ };
129
+ exports.RoleTaskCoverageView = RoleTaskCoverageView;
130
+ tslib_1.__decorate([
131
+ (0, type_graphql_1.Field)({ description: 'The task id, `<module>.<work>`. Its words are the translation keys task.<id>.name and task.<id>.description.' }),
132
+ tslib_1.__metadata("design:type", String)
133
+ ], RoleTaskCoverageView.prototype, "taskId", void 0);
134
+ tslib_1.__decorate([
135
+ (0, type_graphql_1.Field)({ description: 'The module or application that declares the task - the first part of its id.' }),
136
+ tslib_1.__metadata("design:type", String)
137
+ ], RoleTaskCoverageView.prototype, "module", void 0);
138
+ tslib_1.__decorate([
139
+ (0, type_graphql_1.Field)({
140
+ description: "all, some, none or broken. Says which of the task's privileges the role holds - not whether the work can be done. broken: the task names a privilege this installation does not declare."
141
+ }),
142
+ tslib_1.__metadata("design:type", String)
143
+ ], RoleTaskCoverageView.prototype, "status", void 0);
144
+ tslib_1.__decorate([
145
+ (0, type_graphql_1.Field)(type => [RoleTemplateGapGrant], { description: "The task's privileges the role holds." }),
146
+ tslib_1.__metadata("design:type", Array)
147
+ ], RoleTaskCoverageView.prototype, "held", void 0);
148
+ tslib_1.__decorate([
149
+ (0, type_graphql_1.Field)(type => [RoleTemplateGapGrant], { description: "The task's declared privileges the role does not hold." }),
150
+ tslib_1.__metadata("design:type", Array)
151
+ ], RoleTaskCoverageView.prototype, "missing", void 0);
152
+ tslib_1.__decorate([
153
+ (0, type_graphql_1.Field)(type => [RoleTemplateGapGrant], { description: 'Privileges the task names that this installation does not declare.' }),
154
+ tslib_1.__metadata("design:type", Array)
155
+ ], RoleTaskCoverageView.prototype, "undeclared", void 0);
156
+ exports.RoleTaskCoverageView = RoleTaskCoverageView = tslib_1.__decorate([
157
+ (0, type_graphql_1.ObjectType)({ description: "One task laid against a role's privileges." })
158
+ ], RoleTaskCoverageView);
159
+ let RoleTaskCoverageListView = class RoleTaskCoverageListView {
160
+ };
161
+ exports.RoleTaskCoverageListView = RoleTaskCoverageListView;
162
+ tslib_1.__decorate([
163
+ (0, type_graphql_1.Field)(type => [RoleTaskCoverageView], { description: 'Every registered task, with what the role holds of it.' }),
164
+ tslib_1.__metadata("design:type", Array)
165
+ ], RoleTaskCoverageListView.prototype, "tasks", void 0);
166
+ tslib_1.__decorate([
167
+ (0, type_graphql_1.Field)(type => [RoleTemplateGapGrant], { description: 'Declared privileges the role holds that no task names.' }),
168
+ tslib_1.__metadata("design:type", Array)
169
+ ], RoleTaskCoverageListView.prototype, "unclassified", void 0);
170
+ exports.RoleTaskCoverageListView = RoleTaskCoverageListView = tslib_1.__decorate([
171
+ (0, type_graphql_1.ObjectType)({ description: "A role's privileges laid against every registered task." })
172
+ ], RoleTaskCoverageListView);
111
173
  //# sourceMappingURL=role-template-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"role-template-types.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-types.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, Int, ObjectType } from 'type-graphql'\n\n/**\n * What a template looks like *to this installation*.\n *\n * The declared template and the offered template are not the same thing: an app declares\n * grants across modules it expects to be installed, and a given installation has a subset.\n * operato-plant declares 14 categories; the plant app running on :4000 sees 33 of the 47\n * categories declared across the repository. So the answer has to say which parts of the\n * template this installation can actually honour.\n */\n@ObjectType({ description: 'One module and the axes a role template grants on it.' })\nexport class RoleTemplateGrantView {\n @Field({ description: 'The privilege category — what is guarded.' })\n category: string\n\n @Field(type => [String], { description: 'The privilege axes granted on that category.' })\n axes: string[]\n\n /**\n * Axes this installation does not declare.\n *\n * Dropping them quietly would hand the administrator a role that does not match the\n * template's name, with no way to tell. They are reported instead, and the screen says\n * \"not present in this installation\".\n */\n @Field(type => [String], { description: 'Axes named by the template that this installation does not declare.' })\n missingAxes: string[]\n}\n\n@ObjectType({ description: 'A role template offered by this installation.' })\nexport class RoleTemplateView {\n @Field({ description: 'Declaration key, namespaced by the declaring application.' })\n id: string\n\n @Field({ description: 'The role name this template creates in a domain.' })\n name: string\n\n @Field({ nullable: true, description: 'A description of the seat this template creates.' })\n description?: string\n\n @Field(type => [RoleTemplateGrantView], { description: 'The modules and axes this template grants.' })\n grants: RoleTemplateGrantView[]\n\n @Field(type => Int, { description: 'How many privileges this template would actually grant here.' })\n privilegeCount: number\n\n @Field(type => Int, { description: 'How many privileges the template names that this installation does not have.' })\n missingCount: number\n\n /**\n * Whether a role already carries this template's default name in the current domain.\n *\n * The screen warns before creating one, because seeding onto an existing name does not\n * make a second role — approval lines hold a role's id, so a duplicate would leave the\n * approval routed to the role nobody edits. The existing role is used as it stands and\n * the template's privileges are added to it.\n *\n * It says \"default name\" rather than \"seeded\" because the administrator may give the\n * role a name of their own, and then nothing here can tell whether this template was\n * used. That is deliberate: provenance is not stored.\n */\n @Field({ description: \"Whether a role already carries this template's default name in the current domain.\" })\n defaultNameTaken: boolean\n\n /**\n * True when the template deliberately opens nothing.\n *\n * A seat that only receives approvals is legitimate — operato-twin declares seven roles\n * with no privileges at all, used as the target an approval line points at. The screen\n * must not draw that as \"0 privileges, something is wrong\", so the distinction is\n * carried here rather than inferred from an empty list.\n */\n @Field({ description: 'Whether this template grants nothing on purpose, rather than by omission.' })\n grantsNothingDeliberately: boolean\n}\n\n@ObjectType({ description: 'Privileges on one category that a role lacks against its template.' })\nexport class RoleTemplateGapGrant {\n @Field({ description: 'The privilege category.' })\n category: string\n\n @Field(type => [String], { description: 'The axes the template grants here and the role does not hold.' })\n axes: string[]\n}\n\n@ObjectType({\n description: \"What the template of a role's name grants in this installation that the role does not hold.\"\n})\nexport class RoleTemplateGapView {\n @Field({ description: 'The template the role is paired with, by name.' })\n templateId: string\n\n @Field({ description: 'The template name - the same as the role name.' })\n templateName: string\n\n @Field(type => [RoleTemplateGapGrant], { description: 'The privileges missing from the role, by category.' })\n missing: RoleTemplateGapGrant[]\n\n @Field(type => Int, {\n description: 'How many privileges the role lacks against its template. Zero means up to date.'\n })\n missingCount: number\n}\n"]}
1
+ {"version":3,"file":"role-template-types.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-types.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, Int, ObjectType } from 'type-graphql'\n\n/**\n * What a template looks like *to this installation*.\n *\n * The declared template and the offered template are not the same thing: an app declares\n * grants across modules it expects to be installed, and a given installation has a subset.\n * operato-plant declares 14 categories; the plant app running on :4000 sees 33 of the 47\n * categories declared across the repository. So the answer has to say which parts of the\n * template this installation can actually honour.\n */\n@ObjectType({ description: 'One module and the axes a role template grants on it.' })\nexport class RoleTemplateGrantView {\n @Field({ description: 'The privilege category — what is guarded.' })\n category: string\n\n @Field(type => [String], { description: 'The privilege axes granted on that category.' })\n axes: string[]\n\n /**\n * Axes this installation does not declare.\n *\n * Dropping them quietly would hand the administrator a role that does not match the\n * template's name, with no way to tell. They are reported instead, and the screen says\n * \"not present in this installation\".\n */\n @Field(type => [String], { description: 'Axes named by the template that this installation does not declare.' })\n missingAxes: string[]\n}\n\n@ObjectType({ description: 'A role template offered by this installation.' })\nexport class RoleTemplateView {\n @Field({ description: 'Declaration key, namespaced by the declaring application.' })\n id: string\n\n @Field({ description: 'The role name this template creates in a domain.' })\n name: string\n\n @Field({ nullable: true, description: 'A description of the seat this template creates.' })\n description?: string\n\n @Field(type => [RoleTemplateGrantView], { description: 'The modules and axes this template grants.' })\n grants: RoleTemplateGrantView[]\n\n @Field(type => [String], { description: 'The tasks this template is built from, by id - each a piece of work a module or application names.' })\n tasks: string[]\n\n /**\n * What stops this template from being applied at all — a task nobody registered, or a task naming a\n * privilege this installation does not declare (ADR-0139 decision 11). Empty when it can be applied.\n */\n @Field(type => [String], { description: 'Definition errors that refuse this template when applied: unregistered tasks, or tasks naming undeclared privileges.' })\n definitionErrors: string[]\n\n @Field(type => Int, { description: 'How many privileges this template would actually grant here.' })\n privilegeCount: number\n\n @Field(type => Int, { description: 'How many privileges the template names that this installation does not have.' })\n missingCount: number\n\n /**\n * Whether a role already carries this template's default name in the current domain.\n *\n * The screen warns before creating one, because seeding onto an existing name does not\n * make a second role — approval lines hold a role's id, so a duplicate would leave the\n * approval routed to the role nobody edits. The existing role is used as it stands and\n * the template's privileges are added to it.\n *\n * It says \"default name\" rather than \"seeded\" because the administrator may give the\n * role a name of their own, and then nothing here can tell whether this template was\n * used. That is deliberate: provenance is not stored.\n */\n @Field({ description: \"Whether a role already carries this template's default name in the current domain.\" })\n defaultNameTaken: boolean\n\n /**\n * True when the template deliberately opens nothing.\n *\n * A seat that only receives approvals is legitimate — operato-twin declares seven roles\n * with no privileges at all, used as the target an approval line points at. The screen\n * must not draw that as \"0 privileges, something is wrong\", so the distinction is\n * carried here rather than inferred from an empty list.\n */\n @Field({ description: 'Whether this template grants nothing on purpose, rather than by omission.' })\n grantsNothingDeliberately: boolean\n}\n\n@ObjectType({ description: 'Privileges on one category that a role lacks against its template.' })\nexport class RoleTemplateGapGrant {\n @Field({ description: 'The privilege category.' })\n category: string\n\n @Field(type => [String], { description: 'The axes the template grants here and the role does not hold.' })\n axes: string[]\n}\n\n@ObjectType({\n description: \"What the template of a role's name grants in this installation that the role does not hold.\"\n})\nexport class RoleTemplateGapView {\n @Field({ description: 'The template the role is paired with, by name.' })\n templateId: string\n\n @Field({ description: 'The template name - the same as the role name.' })\n templateName: string\n\n @Field(type => [RoleTemplateGapGrant], { description: 'The privileges missing from the role, by category.' })\n missing: RoleTemplateGapGrant[]\n\n @Field(type => Int, {\n description: 'How many privileges the role lacks against its template. Zero means up to date.'\n })\n missingCount: number\n\n /**\n * What the role holds beyond the template. Not a fault — a site may widen a role on purpose — so the screen\n * calls it \"wider than the template\" and nothing takes it away.\n */\n @Field(type => [RoleTemplateGapGrant], { description: 'Declared privileges the role holds that the template does not grant, by category.' })\n extra: RoleTemplateGapGrant[]\n\n @Field(type => Int, { description: 'How many privileges the role holds beyond the template.' })\n extraCount: number\n}\n\n@ObjectType({ description: \"One task laid against a role's privileges.\" })\nexport class RoleTaskCoverageView {\n @Field({ description: 'The task id, `<module>.<work>`. Its words are the translation keys task.<id>.name and task.<id>.description.' })\n taskId: string\n\n @Field({ description: 'The module or application that declares the task - the first part of its id.' })\n module: string\n\n @Field({\n description:\n \"all, some, none or broken. Says which of the task's privileges the role holds - not whether the work can be done. broken: the task names a privilege this installation does not declare.\"\n })\n status: string\n\n @Field(type => [RoleTemplateGapGrant], { description: \"The task's privileges the role holds.\" })\n held: RoleTemplateGapGrant[]\n\n @Field(type => [RoleTemplateGapGrant], { description: \"The task's declared privileges the role does not hold.\" })\n missing: RoleTemplateGapGrant[]\n\n @Field(type => [RoleTemplateGapGrant], { description: 'Privileges the task names that this installation does not declare.' })\n undeclared: RoleTemplateGapGrant[]\n}\n\n@ObjectType({ description: \"A role's privileges laid against every registered task.\" })\nexport class RoleTaskCoverageListView {\n @Field(type => [RoleTaskCoverageView], { description: 'Every registered task, with what the role holds of it.' })\n tasks: RoleTaskCoverageView[]\n\n @Field(type => [RoleTemplateGapGrant], { description: 'Declared privileges the role holds that no task names.' })\n unclassified: RoleTemplateGapGrant[]\n}\n"]}